/// Builders for `PatchElements`, `PatchSignals`, and `ExecuteScript` events.
///
/// This module is the primary way to construct Datastar events. Each builder
/// returns an intermediate record that you can customise with `with_*` helpers
/// before converting to a `DatastarEvent`.
import datastar_gleam.{type DatastarEvent, DatastarEvent}
import datastar_gleam/consts.{
type ElementPatchMode, Append, Outer,
default_elements_use_view_transitions, default_patch_signals_only_if_missing,
default_sse_retry_duration, elements_literal, mode_literal,
only_if_missing_literal, selector_literal, signals_literal,
use_view_transition_literal,
}
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/string
// -- PatchElements --
/// Configuration for a `PatchElements` event.
///
/// Use `new_elements` or `new_remove` to create a value, then customise it
/// with `with_*` functions and finally call `patch_elements_to_datastar_event`.
pub type PatchElements {
PatchElements(
id: Option(String),
retry: Int,
elements: Option(String),
selector: Option(String),
mode: ElementPatchMode,
use_view_transition: Bool,
)
}
/// Create a `PatchElements` builder that inserts the given HTML fragment.
///
/// ```gleam
/// let patch =
/// event.new_elements("<div id='message'>Hello!</div>")
/// |> event.with_selector("#target")
/// |> event.with_mode(event.Append)
/// ```
pub fn new_elements(html: String) -> PatchElements {
PatchElements(
id: None,
retry: default_sse_retry_duration,
elements: Some(html),
selector: None,
mode: Outer,
use_view_transition: default_elements_use_view_transitions,
)
}
/// Create a `PatchElements` builder that removes the element matched by
/// `selector`.
pub fn new_remove(selector: String) -> PatchElements {
PatchElements(
id: None,
retry: default_sse_retry_duration,
elements: None,
selector: Some(selector),
mode: consts.Remove,
use_view_transition: default_elements_use_view_transitions,
)
}
/// Set the SSE event ID.
pub fn with_id(p: PatchElements, id: String) -> PatchElements {
PatchElements(..p, id: Some(id))
}
/// Set the SSE retry duration in milliseconds.
pub fn with_retry(p: PatchElements, retry: Int) -> PatchElements {
PatchElements(..p, retry: retry)
}
/// Set the CSS selector that identifies the target element.
pub fn with_selector(p: PatchElements, selector: String) -> PatchElements {
PatchElements(..p, selector: Some(selector))
}
/// Set the insertion mode (default `Outer`).
pub fn with_mode(p: PatchElements, mode: ElementPatchMode) -> PatchElements {
PatchElements(..p, mode: mode)
}
/// Enable or disable view transitions.
pub fn with_use_view_transition(p: PatchElements, enabled: Bool) -> PatchElements {
PatchElements(..p, use_view_transition: enabled)
}
/// Convert a `PatchElements` builder into a `DatastarEvent`.
pub fn patch_elements_to_datastar_event(p: PatchElements) -> DatastarEvent {
let data = []
let data = case p.selector {
Some(sel) -> list.append(data, [selector_literal <> " " <> sel])
None -> data
}
let data = case p.mode {
Outer -> data
_ ->
list.append(data, [
mode_literal <> " " <> consts.element_patch_mode_string(p.mode),
])
}
let data = case p.use_view_transition {
True -> list.append(data, [use_view_transition_literal <> " True"])
False -> data
}
let data = case p.elements {
Some(el) -> {
string.split(el, on: "\n")
|> list.map(fn(line) { elements_literal <> " " <> line })
|> list.append(data, _)
}
None -> data
}
DatastarEvent(
event: consts.PatchElements,
id: p.id,
retry: p.retry,
data: data,
)
}
// -- PatchSignals --
/// Configuration for a `PatchSignals` event.
pub type PatchSignals {
PatchSignals(
id: Option(String),
retry: Int,
signals: String,
only_if_missing: Bool,
)
}
/// Create a `PatchSignals` builder with the given JSON-encoded signals.
///
/// ```gleam
/// let patch =
/// event.new_signals("{ \"count\": 42 }")
/// |> event.with_only_if_missing(True)
/// ```
pub fn new_signals(signals: String) -> PatchSignals {
PatchSignals(
id: None,
retry: default_sse_retry_duration,
signals: signals,
only_if_missing: default_patch_signals_only_if_missing,
)
}
/// Set the SSE event ID.
pub fn with_signals_id(p: PatchSignals, id: String) -> PatchSignals {
PatchSignals(..p, id: Some(id))
}
/// Set the SSE retry duration in milliseconds.
pub fn with_signals_retry(p: PatchSignals, retry: Int) -> PatchSignals {
PatchSignals(..p, retry: retry)
}
/// Only patch signals that are not already present in the frontend store.
pub fn with_only_if_missing(p: PatchSignals, only_if_missing: Bool) -> PatchSignals {
PatchSignals(..p, only_if_missing: only_if_missing)
}
/// Convert a `PatchSignals` builder into a `DatastarEvent`.
pub fn patch_signals_to_datastar_event(p: PatchSignals) -> DatastarEvent {
let data = []
let data = case p.only_if_missing {
True -> list.append(data, [only_if_missing_literal <> " True"])
False -> data
}
let data = {
string.split(p.signals, on: "\n")
|> list.map(fn(line) { signals_literal <> " " <> line })
|> list.append(data, _)
}
DatastarEvent(
event: consts.PatchSignals,
id: p.id,
retry: p.retry,
data: data,
)
}
// -- ExecuteScript --
/// Configuration for an `ExecuteScript` event.
///
/// This is a convenience wrapper around `PatchElements` that injects a
/// `<script>` tag into the page and optionally removes it after execution.
pub type ExecuteScript {
ExecuteScript(
id: Option(String),
retry: Int,
script: String,
auto_remove: Option(Bool),
attributes: List(String),
)
}
/// Create an `ExecuteScript` builder with the given JavaScript source.
///
/// ```gleam
/// let script =
/// event.new_script("console.log('Hello from Gleam!')")
/// |> event.with_auto_remove(True)
/// ```
pub fn new_script(script: String) -> ExecuteScript {
ExecuteScript(
id: None,
retry: default_sse_retry_duration,
script: script,
auto_remove: None,
attributes: [],
)
}
/// Set the SSE event ID.
pub fn with_script_id(s: ExecuteScript, id: String) -> ExecuteScript {
ExecuteScript(..s, id: Some(id))
}
/// Set the SSE retry duration in milliseconds.
pub fn with_script_retry(s: ExecuteScript, retry: Int) -> ExecuteScript {
ExecuteScript(..s, retry: retry)
}
/// Automatically remove the `<script>` tag after it executes.
pub fn with_auto_remove(s: ExecuteScript, auto_remove: Bool) -> ExecuteScript {
ExecuteScript(..s, auto_remove: Some(auto_remove))
}
/// Add extra attributes to the generated `<script>` tag.
pub fn with_attributes(
s: ExecuteScript,
attributes: List(String),
) -> ExecuteScript {
ExecuteScript(..s, attributes: attributes)
}
/// Convert an `ExecuteScript` builder into a `DatastarEvent`.
pub fn execute_script_to_datastar_event(s: ExecuteScript) -> DatastarEvent {
let data = [selector_literal <> " body"]
let data =
list.append(data, [
mode_literal <> " " <> consts.element_patch_mode_string(Append),
])
let script_tag = {
let auto_remove_attr = case s.auto_remove {
Some(False) -> ""
_ -> " data-effect=\"el.remove()\""
}
let attrs = string.join(s.attributes, with: " ")
let attr_str = case attrs {
"" -> ""
_ -> " " <> attrs
}
"<script" <> auto_remove_attr <> attr_str <> ">"
}
let lines = string.split(s.script, on: "\n")
let #(first, rest) = case lines {
[f, ..r] -> #(f, r)
[] -> #("", [])
}
let first_el = elements_literal <> " " <> script_tag <> first
let rest_els = list.map(rest, fn(l) { elements_literal <> " " <> l })
let all_els = [first_el, ..rest_els]
let all_els = append_to_last(all_els, "</script>")
let data = list.append(data, all_els)
DatastarEvent(
event: consts.PatchElements,
id: s.id,
retry: s.retry,
data: data,
)
}
fn append_to_last(items: List(String), suffix: String) -> List(String) {
case list.reverse(items) {
[last, ..front] -> list.reverse([last <> suffix, ..front])
[] -> []
}
}