README.md

# Datastar for Elixir

An Elixir SDK for the [Datastar](https://data-star.dev) web framework, modeled after the [Go implementation](https://github.com/starfederation/datastar-go).

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`:

```elixir
def deps do
  [
    {:datastar_ex, "~> 0.1.0"}
  ]
end
```

## Quick Start

### Basic SSE Streaming

```elixir
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:

```elixir
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:

```elixir
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

```elixir
sse
|> Elements.remove("#temporary-message")
|> Elements.remove_by_id("old-content")
```

### JavaScript Execution

```elixir
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:

```elixir
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](https://github.com/starfederation/datastar-go), 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](LICENSE) file for details.

## Resources

- [Datastar Official Documentation](https://data-star.dev)
- [Datastar Go SDK](https://github.com/starfederation/datastar-go)
- [Server-Sent Events Specification](https://html.spec.whatwg.org/multipage/server-sent-events.html)

## Acknowledgments

This SDK is modeled after the excellent [Datastar Go SDK](https://github.com/starfederation/datastar-go) by the Star Federation team.