# DatastarPlug
[](https://hex.pm/packages/datastar_plug)
[](https://hexdocs.pm/datastar_plug)
[](https://github.com/rskinnerc/datastar_plug/actions/workflows/ci.yml)
[](https://opensource.org/licenses/MIT)
Stateless [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
helpers for [Datastar](https://data-star.dev) 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](https://data-star.dev) 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.
---
## Installation
Add `:datastar_plug` to your dependencies in `mix.exs`:
```elixir
def deps do
[
{:datastar_plug, "~> 0.1.0"}
]
end
```
Then run:
```shell
mix deps.get
```
No additional configuration is required. The package has two runtime
dependencies: [`plug`](https://hex.pm/packages/plug) and
[`jason`](https://hex.pm/packages/jason), both of which are already present
in virtually every Phoenix application.
---
## Quick Start
### Phoenix controller
```elixir
defmodule MyAppWeb.ItemController do
use MyAppWeb, :controller
alias Datastar
alias MyApp.Items
# GET /items/:id/refresh — triggered by a Datastar `data-on-load` attribute
def refresh(conn, params) do
signals = Datastar.parse_signals(params)
item = Items.get!(signals["itemId"])
html = Phoenix.View.render_to_string(MyAppWeb.ItemView, "card.html", item: item)
conn
|> Datastar.init_sse()
|> Datastar.patch_fragment(html)
|> Datastar.patch_signals(%{itemLoaded: true})
|> Datastar.close_sse()
end
# DELETE /items/:id — delete and remove the card from the DOM
def delete(conn, %{"id" => id}) do
Items.delete!(id)
conn
|> Datastar.init_sse()
|> Datastar.remove_fragment("#item-#{id}")
|> Datastar.patch_signals(%{count: Items.count()})
|> Datastar.close_sse()
end
end
```
### Plain `Plug.Router`
```elixir
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
```
### 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:
```elixir
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:
```elixir
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`](https://hexdocs.pm/datastar_plug/Datastar.html#init_sse/1) | Open a chunked SSE response. **Call first.** |
| [`patch_fragment/3`](https://hexdocs.pm/datastar_plug/Datastar.html#patch_fragment/3) | Patch HTML into the DOM (`datastar-patch-elements`). |
| [`patch_signals/2`](https://hexdocs.pm/datastar_plug/Datastar.html#patch_signals/2) | Merge values into the client signal store (`datastar-patch-signals`). |
| [`execute_script/2`](https://hexdocs.pm/datastar_plug/Datastar.html#execute_script/2) | Execute JavaScript on the client (appends a `<script>` tag). |
| [`redirect_to/2`](https://hexdocs.pm/datastar_plug/Datastar.html#redirect_to/2) | Redirect the browser via `window.location.href`. |
| [`remove_fragment/2`](https://hexdocs.pm/datastar_plug/Datastar.html#remove_fragment/2) | Remove a DOM element by CSS selector. |
| [`close_sse/1`](https://hexdocs.pm/datastar_plug/Datastar.html#close_sse/1) | No-op pipeline terminator for readability. |
| [`parse_signals/1`](https://hexdocs.pm/datastar_plug/Datastar.html#parse_signals/1) | Decode Datastar signals from GET or POST params. |
---
## SSE Protocol Overview
Each function emits one or more
[SSE events](https://data-star.dev/reference/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/2`, `remove_fragment/2`.
```
event: datastar-patch-elements
data: selector #my-div
data: mode inner
data: elements <p>Hello, world!</p>
```
### `datastar-patch-signals`
Used by `patch_signals/2`.
```
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 |
|------|-----------|
| `"morph"` | ID-based morphing diff **(default)**. Matches top-level elements by `id`. |
| `"inner"` | Replaces inner HTML of the target element. |
| `"outer"` | Replaces outer HTML (element itself included). |
| `"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/2` instead). |
---
## Security
See the [Datastar security reference](https://data-star.dev/reference/security)
for the full specification. Key points for this library:
- **`patch_fragment/3`** — HTML is written verbatim to the SSE stream. If any
part of the HTML originates from user input, **sanitise it first** to prevent
XSS. Use `Phoenix.HTML.html_escape/1` or a dedicated HTML sanitiser.
- **`execute_script/2`** — Executes arbitrary JavaScript on the client. Only
pass **server-controlled** strings. Never interpolate user input into the
script.
- **`redirect_to/2`** — The URL is `Jason.encode!/1`-encoded before embedding,
preventing injection via single-quotes, backslashes, or `</script>` in the
URL string.
- **`parse_signals/1`** — Signal data originates from the browser and must be
treated as **untrusted user input**. Validate and sanitise all values before
using them in queries, HTML rendering, or downstream business logic.
---
## 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:
```shell
mix test.ci
```
5. Open a pull request.
Bug reports and feature requests are welcome via
[GitHub Issues](https://github.com/rskinnerc/datastar_plug/issues).
---
## License
DatastarPlug is released under the [MIT License](LICENSE).