Datastar for Elixir
View SourceAn Elixir SDK for the Datastar web framework, modeled after the Go implementation.
Datastar enables real-time, server-driven UI updates using Server-Sent Events (SSE). This library provides a clean, idiomatic Elixir interface for streaming dynamic updates to your web applications.
Features
- 🔄 Server-Sent Events (SSE) - Stream real-time updates to connected clients
- 📊 Signal Management - Read and patch client-side reactive state
- 🎨 DOM Manipulation - Update, append, prepend, and remove HTML elements
- 🚀 JavaScript Execution - Execute scripts, log to console, and dispatch events
- 🔀 Navigation Control - Redirect and manipulate browser history
- 🎯 Type-Safe - Leverages Elixir's pattern matching and type specs
Installation
Add datastar_ex to your list of dependencies in mix.exs:
def deps do
[
{:datastar_ex, "~> 0.1.0"}
]
endQuick Start
Basic SSE Streaming
defmodule MyAppWeb.DatastarController do
use MyAppWeb, :controller
alias Datastar.{SSE, Elements, Signals}
def stream(conn, _params) do
conn
|> put_resp_content_type("text/event-stream")
|> send_chunked(200)
|> SSE.new()
|> Elements.patch("<div>Hello, Datastar!</div>", selector: "#content")
end
endReading Signals
Signals represent client-side state that can be synchronized with the server:
def handle_request(conn, _params) do
# Read signals from the request
signals = Datastar.Signals.read(conn)
count = signals["count"] || 0
# Update the UI based on signals
conn
|> put_resp_content_type("text/event-stream")
|> send_chunked(200)
|> SSE.new()
|> Signals.patch(%{count: count + 1})
|> Elements.patch("<div>Count: #{count + 1}</div>", selector: "#counter")
endPatching Elements
Update DOM elements with various merge strategies:
sse
# Replace entire element (outer HTML)
|> Elements.patch("<div id='box'>New content</div>", selector: "#box")
# Replace inner HTML only
|> Elements.patch("<p>Inner content</p>", selector: "#container", mode: :inner)
# Append to element
|> Elements.patch("<li>New item</li>", selector: "ul", mode: :append)
# Prepend to element
|> Elements.patch("<li>First item</li>", selector: "ul", mode: :prepend)
# Insert before element
|> Elements.patch("<div>Before</div>", selector: "#target", mode: :before)
# Insert after element
|> Elements.patch("<div>After</div>", selector: "#target", mode: :after)Removing Elements
sse
|> Elements.remove("#temporary-message")
|> Elements.remove_by_id("old-content")JavaScript Execution
sse
# Execute arbitrary JavaScript
|> Script.execute("alert('Hello from server!')")
# Console logging
|> Script.console_log("Debug message")
|> Script.console_error("Error occurred")
# Navigation
|> Script.redirect("/dashboard")
|> Script.replace_url("/new-path")
# Custom events
|> Script.dispatch_custom_event("user-updated", %{id: 123, name: "Alice"})
# Prefetch URLs
|> Script.prefetch(["/dashboard", "/profile"])API Reference
Datastar.SSE
Core module for Server-Sent Event streaming:
new(conn)- Create a new SSE generator from a Plug connectionsend_event(sse, event_type, data, opts)- Send a custom SSE eventsend_event!(sse, event_type, data, opts)- Send event, raising on errorclosed?(sse)- Check if the connection is closed
Datastar.Signals
Manage client-side reactive state:
read(conn)- Read signals from request (query params or body)read_as(conn, module)- Read signals into a structpatch(sse, signals, opts)- Update client-side signalspatch_raw(sse, json, opts)- Update with raw JSONpatch_if_missing(sse, signals, opts)- Update only missing signals
Options:
:only_if_missing- Only patch signals that don't exist on client:event_id- Event ID for client tracking:retry- Retry duration in milliseconds
Datastar.Elements
Manipulate DOM elements:
patch(sse, html, opts)- Update elements with HTMLpatchf(sse, format, values, opts)- Patch with formatted stringpatch_by_id(sse, id, html, opts)- Patch element by IDremove(sse, selector, opts)- Remove elements by selectorremove_by_id(sse, id, opts)- Remove element by ID
Convenience functions:
patch_outer/3,patch_inner/3,patch_prepend/3,patch_append/3patch_before/3,patch_after/3,patch_replace/3
Options:
:selector- CSS selector for target elements (required):mode- Patch mode (:outer,:inner,:append,:prepend,:before,:after,:replace):use_view_transitions- Enable View Transitions API:event_id- Event ID for client tracking:retry- Retry duration in milliseconds
Datastar.Script
Execute JavaScript and manage browser state:
execute(sse, script, opts)- Execute JavaScript codeexecutef(sse, format, args, opts)- Execute with formattingconsole_log(sse, message, opts)- Log to browser consoleconsole_error(sse, message, opts)- Log error to consoleredirect(sse, url, opts)- Navigate to URLredirectf(sse, format, args, opts)- Navigate with formattingreplace_url(sse, url, opts)- Update URL without navigationreplace_url_querystring(sse, qs, opts)- Update query stringdispatch_custom_event(sse, event, detail, opts)- Dispatch DOM eventprefetch(sse, urls, opts)- Prefetch URLs using Speculation Rules API
Options:
:auto_remove- Remove script element after execution (default: true):attributes- Additional script element attributes:event_id- Event ID for client tracking:retry- Retry duration in milliseconds
Complete Example
Here's a complete example of a Phoenix LiveView-style counter:
defmodule MyAppWeb.CounterController do
use MyAppWeb, :controller
alias Datastar.{SSE, Elements, Signals, Script}
def increment(conn, _params) do
# Read current count from client
signals = Signals.read(conn)
current_count = signals["count"] || 0
new_count = current_count + 1
# Stream updates back
conn
|> put_resp_content_type("text/event-stream")
|> send_chunked(200)
|> SSE.new()
|> Signals.patch(%{count: new_count})
|> Elements.patch(
"<div>Count: #{new_count}</div>",
selector: "#counter"
)
|> Script.console_log("Count updated to #{new_count}")
end
def reset(conn, _params) do
conn
|> put_resp_content_type("text/event-stream")
|> send_chunked(200)
|> SSE.new()
|> Signals.patch(%{count: 0})
|> Elements.patch("<div>Count: 0</div>", selector: "#counter")
|> Script.dispatch_custom_event("counter-reset", %{})
end
endComparison with Go SDK
This Elixir SDK closely follows the design of the Go implementation, with idiomatic Elixir adaptations:
- Functional API: Methods return updated SSE structs for easy piping
- Pattern Matching: Leverage Elixir's pattern matching for cleaner code
- Keyword Options: Use keyword lists instead of functional options
- Error Handling: Provide both safe (
{:ok, result}) and bang (result!) variants
Requirements
- Elixir 1.14 or later
- Plug (optional, but recommended for web applications)
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Resources
Acknowledgments
This SDK is modeled after the excellent Datastar Go SDK by the Star Federation team.