DatastarPlug

Hex versionHex docsCILicense: MIT

Stateless Server-Sent Events (SSE) helpers for Datastar in any Plug or Phoenix application.

DatastarPlug gives you a small set of composable, pipeline-friendly functions that write Datastar-compatible SSE events to a chunked Plug.Conn response. The Datastar JavaScript client running in the browser receives these events and applies DOM patches, signal updates, script executions, and redirects — all without a full-page reload and without any WebSocket or long-polling infrastructure.

See it in action on the live demo.


Installation

Add :datastar_plug to your dependencies in mix.exs:

def deps do
  [
    {:datastar_plug, "~> 0.2.0"}
  ]
end

Then run:

mix deps.get

No additional configuration is required. The package has two runtime dependencies: plug and jason, both of which are already present in virtually every Phoenix application.

Compatibility

This package is built for Datastar RC.8+. If you're using an earlier version, some functions or options may not work as expected.


What's New in v0.2.0


Quick Start

Phoenix controller

defmodule MyAppWeb.ItemController do
  use MyAppWeb, :controller

  alias Datastar
  alias MyApp.Items

  # GET /items/:id/edit — triggered by a Datastar `data-init` attribute
  def edit(conn, %{"id" => id} = params) do
    signals = Datastar.parse_signals(params)
    item = Items.get!(id)
    form_html = Phoenix.View.render_to_string(MyAppWeb.ItemView, "edit_form.html", item: item)

    conn
    |> Datastar.init_sse()
    |> Datastar.patch_fragment(form_html, selector: "#item-form")
    |> Datastar.patch_signals(%{editMode: true, itemId: id})
    |> Datastar.close_sse()
  end

  # PUT /items/:id — save changes and update the display
  def update(conn, %{"id" => id} = params) do
    signals = Datastar.parse_signals(params)
    item_attrs = Map.take(signals, ["title", "description"])
    {:ok, item} = Items.update(id, item_attrs)
    display_html = Phoenix.View.render_to_string(MyAppWeb.ItemView, "display.html", item: item)

    conn
    |> Datastar.init_sse()
    |> Datastar.patch_fragment(display_html, selector: "#item-display")
    |> Datastar.patch_signals(%{editMode: false})
    |> Datastar.close_sse()
  end
end

Plain Plug.Router

defmodule MyApp.Router do
  use Plug.Router

  plug :match
  plug Plug.Parsers, parsers: [:json], json_decoder: Jason
  plug :dispatch

  get "/updates" do
    conn
    |> Datastar.init_sse()
    |> Datastar.patch_fragment(~s(<div id="status">All systems go</div>))
    |> Datastar.patch_signals(%{ready: true})
    |> Datastar.close_sse()
  end
end

Long-running SSE streams with connection checking

def stream(conn, _params) do
  conn = Datastar.init_sse(conn)
  stream_items(conn, MyApp.Items.all())
end

defp stream_items(conn, []), do: conn

defp stream_items(conn, [item | rest]) do
  case Datastar.check_connection(conn) do
    {:ok, conn} ->
      conn
      |> Datastar.patch_fragment(render_item(item))
      |> stream_items(rest)

    {:error, _conn} ->
      # Client disconnected — stop streaming silently
      conn
  end
end

Removing signals

conn
|> Datastar.init_sse()
|> Datastar.remove_signals(["user.name", "user.email"])
|> Datastar.close_sse()

Reading signals from GET requests

Datastar serialises the entire client signal store as a JSON string in the ?datastar= query parameter on GET requests. Use parse_signals/1 to decode it:

def search(conn, params) do
  # params == %{"datastar" => "{\"query\":\"elixir\"}"}
  signals = Datastar.parse_signals(params)
  query   = Map.get(signals, "query", "")

  results_html = render_results(query)

  conn
  |> Datastar.init_sse()
  |> Datastar.patch_fragment(results_html)
  |> Datastar.close_sse()
end

Reading signals from POST / PUT / DELETE requests

For mutating requests Datastar sends signals as the JSON request body. The body parser decodes it, so params is already the signal map:

def create(conn, params) do
  # params == %{"title" => "Buy milk", "done" => false}
  signals = Datastar.parse_signals(params)
  title   = Map.get(signals, "title", "")

  {:ok, item} = MyApp.Items.create(%{title: title})

  conn
  |> Datastar.init_sse()
  |> Datastar.patch_fragment(render_item(item), selector: "#list", merge_mode: "append")
  |> Datastar.patch_signals(%{newTitle: ""})
  |> Datastar.close_sse()
end

Function Reference

Function Description
init_sse/1 Open a chunked SSE response. Call first.
patch_fragment/3 Patch HTML into the DOM (datastar-patch-elements).
remove_fragment/3 Remove a DOM element by CSS selector.
patch_signals/3 Merge values into the client signal store (datastar-patch-signals).
remove_signals/3 Remove one or more signals by dot-notated path.
execute_script/3 Execute JavaScript on the client (appends a <script> tag).
redirect_to/3 Redirect the browser via window.location.href.
check_connection/1 Verify the SSE connection is still alive.
close_sse/1 No-op pipeline terminator for readability.
parse_signals/1 Decode Datastar signals from GET or POST params.

SSE Protocol Overview

Each function emits one or more SSE events in the following wire format:

event: <event-type>\n
data: <key> <value>\n
[data: <key2> <value2>\n]
\n

The blank line (\n\n) terminates the event. Multi-line HTML values (from patch_fragment/3) are split into one data: elements line per source line.

datastar-patch-elements

Used by patch_fragment/3, execute_script/3, remove_fragment/3.

event: datastar-patch-elements
data: selector #my-div
data: mode inner
data: elements <p>Hello, world!</p>

datastar-patch-signals

Used by patch_signals/3.

event: datastar-patch-signals
data: signals {"count":42,"loading":false}

Merge Modes

The :merge_mode option of patch_fragment/3 controls how incoming HTML is merged into the DOM:

Mode Behaviour
"outer"(Default) Morphs the element in place. Without a :selector, matches top-level elements by id and morphs each one.
"inner" Replaces inner HTML of the target element using morphing.
"replace" Replaces the target element with replaceWith (no morphing diff).
"prepend" Inserts before the first child of the target.
"append" Inserts after the last child of the target.
"before" Inserts immediately before the target element.
"after" Inserts immediately after the target element.
"remove" Removes the target (use remove_fragment/3 instead).

Security

See the Datastar security reference for the full specification. Key points for this library:


Contributing

  1. Fork the repository.
  2. Create a feature branch: git checkout -b my-feature.
  3. Make your changes and add tests.
  4. Run the full quality suite:
    mix test.ci
  5. Open a pull request.

Bug reports and feature requests are welcome via GitHub Issues.


License

DatastarPlug is released under the MIT License.