PhoenixTestDatastar.Signals (PhoenixTestDatastar v0.0.2)

Copy Markdown

Handles Datastar signal extraction from HTML and signal state management.

Datastar signals are declared in HTML via attributes:

  • data-signals:count="0" → individual signal named 'count' with value 0
  • data-signals:name="'hello'" → string value (JS-style single quotes)
  • data-signals="{foo: 1, bar: 2}" → object of signals
  • data-signals:_csrfToken="'abc123'" → underscore prefix = client-only signal

Summary

Functions

Merges new signals into existing state.

Extracts all Datastar signals from HTML.

Converts a kebab-case string to camelCase.

Converts a JavaScript-like expression to valid JSON.

Parses a JavaScript-like value string into an Elixir term.

Filters out client-only signals (prefixed with underscore).

Encodes signals as datastar query parameter, excluding client-only signals.

Encodes signals as JSON for POST body, excluding client-only signals.

Functions

apply_patch(state, new_signals, opts \\ [])

@spec apply_patch(map(), map(), keyword()) :: map()

Merges new signals into existing state.

Options

  • :only_if_missing - When true, only add signals not already present (default: false)

When a value is nil, that signal is removed from the state.

Examples

iex> PhoenixTestDatastar.Signals.apply_patch(%{"count" => 1}, %{"name" => "test"})
%{"count" => 1, "name" => "test"}

iex> PhoenixTestDatastar.Signals.apply_patch(%{"count" => 1}, %{"count" => 2}, only_if_missing: true)
%{"count" => 1}

iex> PhoenixTestDatastar.Signals.apply_patch(%{"count" => 1}, %{"count" => nil})
%{}

extract_from_html(html)

@spec extract_from_html(String.t()) :: map()

Extracts all Datastar signals from HTML.

Parses the HTML, finds all data-signals and data-signals:* attributes, and returns a merged map of all signals.

Examples

iex> html = ~s(<div data-signals:count="0" data-signals:name="'hello'"></div>)
iex> PhoenixTestDatastar.Signals.extract_from_html(html)
%{"count" => 0, "name" => "hello"}

iex> html = ~s(<div data-signals="{foo: 1, bar: 2}"></div>)
iex> PhoenixTestDatastar.Signals.extract_from_html(html)
%{"foo" => 1, "bar" => 2}

kebab_to_camel(str)

@spec kebab_to_camel(String.t()) :: String.t()

Converts a kebab-case string to camelCase.

HTML attributes are case-insensitive, so Datastar uses kebab-case in attribute suffixes (e.g., data-signals:_csrf-token) and converts to camelCase signal names (_csrfToken) on the client.

Only converts dashes followed by a letter (actual kebab-case). Dashes between hex digits (e.g., UUIDs) are preserved.

Handles leading underscores (preserved) and already-camelCase input.

Examples

iex> PhoenixTestDatastar.Signals.kebab_to_camel("_csrf-token")
"_csrfToken"

iex> PhoenixTestDatastar.Signals.kebab_to_camel("my-signal-name")
"mySignalName"

iex> PhoenixTestDatastar.Signals.kebab_to_camel("count")
"count"

iex> PhoenixTestDatastar.Signals.kebab_to_camel("_dstar_module")
"_dstar_module"

iex> PhoenixTestDatastar.Signals.kebab_to_camel("item_da576dd7-7f1f-4917-8dfa-05fe84a95779")
"item_da576dd7-7f1f-4917-8dfa-05fe84a95779"

normalize_to_json(js_value)

@spec normalize_to_json(String.t()) :: String.t()

Converts a JavaScript-like expression to valid JSON.

  • Converts single quotes to double quotes
  • Adds quotes around unquoted object keys
  • Handles numbers, booleans, null, arrays, and objects

Examples

iex> PhoenixTestDatastar.Signals.normalize_to_json("'hello'")
~s("hello")

iex> PhoenixTestDatastar.Signals.normalize_to_json("{foo: 1, bar: 2}")
~s({"foo": 1,"bar": 2})

parse_js_value(value)

@spec parse_js_value(String.t()) :: term()

Parses a JavaScript-like value string into an Elixir term.

Handles JS-style syntax including single quotes, unquoted object keys, and standard JSON primitives.

Examples

iex> PhoenixTestDatastar.Signals.parse_js_value("0")
0

iex> PhoenixTestDatastar.Signals.parse_js_value("'hello'")
"hello"

iex> PhoenixTestDatastar.Signals.parse_js_value("true")
true

iex> PhoenixTestDatastar.Signals.parse_js_value("{foo: 1}")
%{"foo" => 1}

public_signals(signals)

@spec public_signals(map()) :: map()

Filters out client-only signals (prefixed with underscore).

Examples

iex> PhoenixTestDatastar.Signals.public_signals(%{"count" => 1, "_csrfToken" => "abc"})
%{"count" => 1}

to_get_query(signals)

@spec to_get_query(map()) :: String.t()

Encodes signals as datastar query parameter, excluding client-only signals.

Returns a query string in the format: datastar=<json>

Examples

iex> PhoenixTestDatastar.Signals.to_get_query(%{"count" => 1, "name" => "test"})
"datastar=%7B%22count%22%3A1%2C%22name%22%3A%22test%22%7D"

to_post_body(signals)

@spec to_post_body(map()) :: String.t()

Encodes signals as JSON for POST body, excluding client-only signals.

Client-only signals (prefixed with underscore) are excluded.

Examples

iex> PhoenixTestDatastar.Signals.to_post_body(%{"count" => 1, "_csrfToken" => "abc"})
~s({"count":1})