Template Output In, Datastar Event Out
View Sourcedatastar_beam does not need to know which template engine produced your
HTML. The template engine owns HTML generation and escaping. datastar_beam
owns Datastar's SSE event framing.
The shape is always:
- Render a complete HTML fragment with your template library.
- Pass that rendered iodata, binary, or string to
datastar_beam. - Send the returned iodata as a Datastar SSE event.
For a replacement patch, the event should look like this after encoding:
event: datastar-patch-elements
data: selector #results
data: elements <section id="results">...</section>Elixir HEEx
HEEx function components return Phoenix rendered structs. Convert that value
with Phoenix.HTML.Safe.to_iodata/1, then pass the iodata directly to the
Erlang SDK.
defmodule MyAppWeb.SearchFragments do
use Phoenix.Component
attr :rows, :list, required: true
def results(assigns) do
~H"""
<section id="results" data-signals={"{search: '', selected: null}"}>
<ul>
<li :for={row <- @rows}>
<button data-on-click={"$selected = #{row.id}"}>
{row.name}
</button>
</li>
</ul>
</section>
"""
end
def patch_results(rows) do
html =
%{rows: rows}
|> results()
|> Phoenix.HTML.Safe.to_iodata()
:datastar_beam.patch_elements(html, %{
selector: "#results",
mode: :outer
})
end
endpatch_results/1 returns iodata for a datastar-patch-elements SSE event.
Your adapter, controller, or Cowboy handler writes that iodata to the response.
Gleam Nakai
Nakai is a good fit for server-rendered Gleam fragments because it builds HTML
nodes in Gleam and can render snippets with nakai.to_inline_string/1. Bind the
portable Erlang SDK with @external and pass Nakai's string output to it.
import gleam/dynamic.{type Dynamic}
import gleam/list
import nakai
import nakai/attr
import nakai/html
@external(erlang, "datastar_beam", "patch_elements")
fn patch_elements(html: String) -> Dynamic
pub fn results(names: List(String)) -> String {
html.section(
[attr.id("results"), attr.data("signals", "{search: '', selected: null}")],
[
html.ul(
[],
list.map(names, fn(name) {
html.li_text([], name)
}),
),
],
)
|> nakai.to_inline_string()
}
pub fn patch_results(names: List(String)) -> Dynamic {
names
|> results()
|> patch_elements()
}That one-argument call emits the default Datastar element patch. If you want a
typed Gleam facade for selectors and modes, keep that facade in your Gleam app
or a thin Gleam helper package and let it call datastar_beam:patch_elements/2.
Erlang ErlyDTL
ErlyDTL compiles Django-style templates to Erlang modules. Rendering returns
{ok, IOList}, which can be handed to datastar_beam without flattening.
-module(my_search_fragments).
-export([compile/0, patch_results/1]).
compile() ->
Template =
<<"<section id=\"results\" data-signals=\"{search: '', selected: null}\">"
"<ul>"
"{% for name in names %}"
"<li>{{ name }}</li>"
"{% endfor %}"
"</ul>"
"</section>">>,
erlydtl:compile_template(Template, my_search_results_dtl, [
{out_dir, false}
]).
patch_results(Names) ->
{ok, Html} = my_search_results_dtl:render([{names, Names}]),
datastar_beam:patch_elements(Html, #{
selector => <<"#results">>,
mode => outer
}).In an OTP release you normally compile templates during build/startup rather
than per request. The request path should only call the compiled template's
render/1 or render/2 function, then pass the returned iodata into
datastar_beam.
Contract
- Template engines own escaping and HTML validity.
datastar_beam:patch_elements/1,2accepts rendered fragments as iodata, binaries, or strings.datastar_beamdoes not take dependencies on Phoenix, Nakai, ErlyDTL, Cowboy, Plug, Mist, Elli, or any other adapter/template package.- Adapters decide how to write the returned iodata to HTTP, SSE, HTTP/2, or HTTP/3 responses.