Datastar for Elixir

View Source

An 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"}
  ]
end

Quick 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
end

Reading 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")
end

Patching 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 connection
  • send_event(sse, event_type, data, opts) - Send a custom SSE event
  • send_event!(sse, event_type, data, opts) - Send event, raising on error
  • closed?(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 struct
  • patch(sse, signals, opts) - Update client-side signals
  • patch_raw(sse, json, opts) - Update with raw JSON
  • patch_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 HTML
  • patchf(sse, format, values, opts) - Patch with formatted string
  • patch_by_id(sse, id, html, opts) - Patch element by ID
  • remove(sse, selector, opts) - Remove elements by selector
  • remove_by_id(sse, id, opts) - Remove element by ID

Convenience functions:

  • patch_outer/3, patch_inner/3, patch_prepend/3, patch_append/3
  • patch_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 code
  • executef(sse, format, args, opts) - Execute with formatting
  • console_log(sse, message, opts) - Log to browser console
  • console_error(sse, message, opts) - Log error to console
  • redirect(sse, url, opts) - Navigate to URL
  • redirectf(sse, format, args, opts) - Navigate with formatting
  • replace_url(sse, url, opts) - Update URL without navigation
  • replace_url_querystring(sse, qs, opts) - Update query string
  • dispatch_custom_event(sse, event, detail, opts) - Dispatch DOM event
  • prefetch(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
end

Comparison 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.