# Datastar BEAM SDK
The `datastar_beam` package provides the `datastar-beam` SDK core for working
with [Datastar](https://data-star.dev).
Datastar sends responses back to the browser using Server-Sent Events. This
allows a BEAM backend to send any number of events, from zero to infinity, in
response to a single request.
`datastar_beam` has helpers for creating those events, formatting Datastar
patches, reading signals from the frontend, executing scripts, redirecting the
browser, and generating Datastar frontend action expressions. It is written in
portable Erlang, so Erlang, Elixir, Gleam, LFE, and other BEAM languages can
call the same module.
This package is intentionally web-server-neutral. It does not own sockets,
request objects, response streams, routes, or framework adapters. Cowboy,
Phoenix, Plug, Mist, Elli, and other adapters can use the SDK core by writing
the returned iodata to an SSE response.
## License
This package is licensed for free under the [MIT License](LICENSE).
## Requirements
This package requires Erlang/OTP 27 or later. The current implementation uses
OTP's built-in `json` module.
Runtime dependencies are limited to `kernel` and `stdlib`.
## Installation
Rebar3:
```erlang
{deps, [
{datastar_beam, "0.1.0"}
]}.
```
Mix:
```elixir
def deps do
[
{:datastar_beam, "~> 0.1.0"}
]
end
```
Gleam:
```sh
gleam add datastar_beam
```
Hex, OTP application, and Erlang module identifiers use `datastar_beam`, the
BEAM-safe spelling of the `datastar-beam` project name.
## Usage
The SDK functions return iodata. Your web framework or adapter should set the
SSE response headers, then stream or write the events returned by
`datastar_beam`.
```erlang
handler(Method, QueryString, Body) ->
{ok, Signals} = datastar_beam:read_signals(Method, QueryString, Body),
Count = maps:get(<<"count">>, Signals, 0),
Headers = datastar_beam:sse_headers(),
Events = [
datastar_beam:patch_elements(
<<"<div id=\"message\">Hello from Datastar on the BEAM!</div>">>,
#{selector => <<"#message">>}
),
datastar_beam:remove_elements(<<"#temporary-element">>),
datastar_beam:patch_signals(#{
<<"message">> => <<"Updated message">>,
<<"count">> => Count + 1
}),
datastar_beam:execute_script(<<"console.log('Hello from server!')">>),
datastar_beam:redirect(<<"/new-page">>)
],
{200, Headers, Events}.
```
Elixir can call the Erlang module directly:
```elixir
event =
:datastar_beam.patch_signals(%{
"message" => "Hello from Elixir"
})
IO.puts(IO.iodata_to_binary(event))
```
Gleam can bind the Erlang module with `@external`:
```gleam
import gleam/dynamic.{type Dynamic}
import gleam/io
@external(erlang, "datastar_beam", "patch_signals")
fn patch_signals(signals: String) -> Dynamic
@external(erlang, "erlang", "iolist_to_binary")
fn iolist_to_binary(iodata: Dynamic) -> String
pub fn main() {
patch_signals("{\"message\":\"Hello from Gleam\"}")
|> iolist_to_binary
|> io.println
}
```
All event builders produce Datastar-compatible SSE bytes:
```text
event: datastar-patch-signals
data: signals {"message":"Hello from BEAM"}
```
## Event Generation Helpers
The core helper is `event/2,3`, which formats arbitrary SSE events. The
Datastar-specific helpers build on top of it:
```erlang
patch_elements/1,2
remove_elements/1,2
patch_signals/1,2
remove_signals/1,2
execute_script/1,2
redirect/1,2
console_log/1,2
```
HTML inputs may be iodata, binaries, strings, `{safe, Iodata}`, or
`{ok, Iodata}`. Options may be Erlang maps or proplists, which keeps Elixir
keyword lists and plain BEAM callers pleasant.
## Response Helpers
`sse_headers/0` returns the headers recommended for Datastar SSE responses:
```erlang
datastar_beam:sse_headers().
```
The SDK does not create a response object because each BEAM web stack streams
in its own way. Adapter packages should own request and response integration
while passing rendered HTML, JSON signals, and scripts through this module.
## Signal Helpers
The current state of Datastar signals is included in Datastar requests.
`read_signals/3` decodes those signals from the request parts supplied by your
adapter:
```erlang
datastar_beam:read_signals(
get,
<<"datastar=%7B%22count%22%3A41%7D">>,
<<>>
).
```
For `GET` and `DELETE` requests, signals are read from the `datastar` query
parameter. For other request methods, the body is decoded as JSON.
## Attribute And Action Helpers
Datastar allows server-rendered HTML to declare frontend behavior using
`data-*` attributes. This SDK provides action-expression helpers that can be
used from templates or component functions:
```erlang
datastar_beam:post(<<"/counter/increment">>).
datastar_beam:delete(<<"/items/42">>, #{
options => <<"{retryMaxCount: Infinity}">>
}).
```
The SDK stays template-engine-neutral. Render HTML with HEEx, Nakai, ErlyDTL,
Phoenix components, or another template library first; then pass the rendered
iodata to `patch_elements/1,2`.
## Examples
See the [examples directory](https://github.com/xs-and-10s/datastar-beam/tree/main/examples)
for working BEAM examples:
- `examples/elixir_smoke.exs`
- `examples/elixir_usage.exs`
- `examples/gleam_smoke/src/datastar_beam_gleam_smoke.gleam`
- `examples/gleam_smoke/src/datastar_beam_usage.gleam`
Additional notes are available in:
- [BEAM usage](docs/beam-usage.md)
- [API notes](docs/datastar-beam-api.md)
- [Optional language bindings](docs/language-bindings.md)
- [Templating notes](docs/templating.md)
## Testing
```sh
rebar3 eunit
rebar3 compile
./test.sh
```
`./test.sh` also runs the Elixir and Gleam examples when those tools are
available on `PATH`.
## Contributing
Contributions are welcome. Please keep the SDK core portable across BEAM
languages and free of web-server-specific dependencies.