arizona_js (arizona v0.4.0)

View Source

Builders for web (browser/JavaScript) client effects.

Each function returns an arizona_effect:cmd/0 tuple {arizona_effect, [OpCode | Args]} -- the shared wire format every client command module produces (op codes in include/arizona_effect.hrl), encoded by arizona_effect:encode/1. Use them inside web event attributes like az-click or as effects from handle_event/3.

Most of these are browser/DOM-only (toggle, the class ops, set_attr, dispatch_event, ...): they address the DOM by CSS selector or touch document/window state. push_event and navigate have portable counterparts in the per-platform modules (e.g. arizona_android) for ?native views.

Example

{'button', [{az_click, [
    arizona_js:push_event(~"inc"),
    arizona_js:toggle(~"#modal")
]}], [~"Both"]}

Two lifetimes -- attribute vs handler effect

The same builder means different things depending on where it is placed:

  • In an event attribute (az_click, az_submit, ...) it is markup. It renders at SSR and sits in the DOM, so a purely client-side command (toggle, the class ops, set/2,3) runs on its trigger with no round trip at all. push_event and fetch are the ones that do reach the server.
  • Returned from a handler callback it is a command sent over the WebSocket, so it cannot exist at SSR -- an SSR render is an HTTP response with no client attached yet. Which callbacks can return effects, and why mount/1 cannot, is covered by "Resets and effects" in arizona_stateful.

Appearance is rendered, not commanded

A class or attribute derived from state belongs in the template, where the diff engine owns it:

{'div', [{id, ?get(id)}, {class, [~"app ", ?get(tint)]}], [...]}

Driving that same class from a handler with add_class/2 paints the wrong value on a cold load and only corrects itself once connected -- never, if no event ever fires. Effects are for imperative one-shots the DOM cannot derive from state: focus a field, set the title, start a transition, copy to the clipboard. It is the same reasoning behind there being deliberately no set_text/set_html -- writing content from an effect would fight the diff engine, which is authoritative for what a view shows.

Summary

Functions

Adds a CSS class to all elements matching the selector.

Blurs (removes focus from) the first element matching the selector.

Closes the first <dialog> matching the selector via HTMLDialogElement.close. A non-dialog match is a safe no-op.

Copies to the clipboard the text of the first element matching the selector: the matched element's value (a form control) or, failing that, its textContent, written via navigator.clipboard.writeText.

Dispatches a custom DOM event with a payload.

Closes the floating Picture-in-Picture window for ViewId, moving the view back inline. No-op if the view isn't popped out.

Issues an HTTP request to Url via the browser fetch() API, without a page reload, and applies the response. Unlike push_event (WebSocket, can't set cookies) and a plain form POST (full reload), a fetch response can carry a real Set-Cookie -- HttpOnly honored, applied natively by the browser -- while the page stays put. Use it for flows that must rotate a session cookie yet keep the typed form fields and show inline validation (password change, login, logout).

Focuses the first element matching the selector.

Hides all elements matching the CSS selector.

Triggers a SPA navigation to Path.

Triggers a SPA navigation to Path with options.

Wraps a command (or list of commands) so it only fires when a key matching Key is pressed. Key can be an atom (e.g. enter), a list of atoms (matches any), or a regex pattern as a binary.

Triggers an in-place SPA navigation (patch) to Path: keeps the current live view mounted and re-renders it via handle_update/3 instead of replacing it. The counterpart of navigate/1; use it between routes that share a root view so live chrome survives the navigation.

Triggers an in-place SPA navigation (patch) to Path with options. Accepts the same flash opt as navigate/2 (carried in-process to the patched route's flash binding, stripped from the client effect) and its replace opt.

Pushes a named event to the server. The payload is auto-collected from the triggering element itself, not its siblings: a form trigger contributes its fields (a form submit passes the form), an input/select/textarea trigger its value; any other trigger (e.g. a plain button inside a form) sends an empty payload. Use push_event/2 to attach data explicitly.

Pushes a named event with an explicit payload, merged on top of any auto-collected form data.

Reloads the current page.

Removes an attribute from all elements matching the selector.

Removes a CSS class from all elements matching the selector.

Moves the view identified by ViewId into a floating Document Picture-in-Picture window, kept live by the server diff stream. Use inside a web event attribute (e.g. az-click) -- Document PiP requires a user gesture, so it won't open from a server-pushed handler effect. The window opens at the browser's default size; use request_pip/2 to choose a size.

Like request_pip/1 but with window options. width/height set the floating window's initial size in pixels; when omitted the browser picks a default size.

Resets all forms matching the selector, clearing the fields the user typed back to their initial values (HTMLFormElement.reset).

Scrolls smoothly to the first element matching the selector.

Scrolls to the first element matching the selector with scrollIntoView options (behavior, block, inline).

Selects (highlights) the text of the first element matching the selector, an <input> or <textarea>, via HTMLInputElement.select -- so a subsequent copy or overtype acts on the whole field.

Sets a client-owned slot (?local) to Value in the closest view of the triggering element, updating every slot with Key locally with no server round-trip. Use inside web event attributes like az-click.

Like set/2 but targets the slot in the view identified by ViewId.

Like set/2 but updates the slot in every view on the page (document-wide).

Sets an attribute on all elements matching the selector.

Sets the document title.

Shows all elements matching the CSS selector.

Opens the first <dialog> matching the selector as a true modal via HTMLDialogElement.showModal -- placed in the top layer, with a ::backdrop and ESC-to-close -- unlike merely setting the open attribute (a non-modal dialog). A non-dialog match is a safe no-op.

Toggles the visibility of all elements matching the CSS selector.

Toggles the presence of a bare boolean attribute on all elements matching the selector: removes it if present, otherwise sets it (e.g. disabled, readonly, hidden, open). For toggling between two values, use toggle_attr/4.

Toggles an attribute between two values on all elements matching the selector: sets ValueB when the current value is ValueA, otherwise sets ValueA (so any other current value resolves to ValueA). Useful for one-button toggles like a password field's type between password and text.

Toggles a CSS class on all elements matching the selector.

Wraps a command (or list of commands) so the DOM change it causes plays inside a view transition (document.startViewTransition). Works for a client-side effect (e.g. toggle/1, animated immediately), a navigate/1,2 (the page swap animates), or a push_event (the resulting server diff animates). A view transition is not tied to navigation -- it wraps any DOM change. Equivalent to transition/2 with no options.

Like transition/1 but with options. types is a list of view-transition type names activated for the transition (matched by :active-view-transition-type(...) CSS), letting one stylesheet pick a different animation per call.

Functions

add_class(Selector, Class)

-spec add_class(Selector, Class) -> arizona_effect:cmd() when Selector :: binary(), Class :: binary().

Adds a CSS class to all elements matching the selector.

blur(Selector)

-spec blur(Selector) -> arizona_effect:cmd() when Selector :: binary().

Blurs (removes focus from) the first element matching the selector.

close_modal(Selector)

-spec close_modal(Selector) -> arizona_effect:cmd() when Selector :: binary().

Closes the first <dialog> matching the selector via HTMLDialogElement.close. A non-dialog match is a safe no-op.

Like focus/1/blur/1/scroll_to/1 (first match only). Usable both as a web event command (e.g. {az_click, arizona_js:close_modal(~"#confirm")}) and as a handler effect from handle_event/3.

copy_to_clipboard(Selector)

-spec copy_to_clipboard(Selector) -> arizona_effect:cmd() when Selector :: binary().

Copies to the clipboard the text of the first element matching the selector: the matched element's value (a form control) or, failing that, its textContent, written via navigator.clipboard.writeText.

Like focus/1/blur/1/scroll_to/1 (first match only). The Clipboard API requires a secure context and a user gesture, so this is meaningful as an event command only (e.g. {az_click, arizona_js:copy_to_clipboard(~"#token")}); a server-pushed handler effect can't satisfy the gesture requirement. A missing/blocked clipboard is a safe no-op.

dispatch_event(Name, Payload)

-spec dispatch_event(Name, Payload) -> arizona_effect:cmd() when Name :: binary(), Payload :: map().

Dispatches a custom DOM event with a payload.

exit_pip(ViewId)

-spec exit_pip(ViewId) -> arizona_effect:cmd() when ViewId :: binary().

Closes the floating Picture-in-Picture window for ViewId, moving the view back inline. No-op if the view isn't popped out.

fetch(Url, Opts)

-spec fetch(Url, Opts) -> arizona_effect:cmd()
               when
                   Url :: binary(),
                   Opts ::
                       #{method => get | post | put | patch | delete,
                         body => term(),
                         headers => #{binary() => binary()},
                         credentials => same_origin | include | omit,
                         on_error => arizona_effect:cmd() | arizona_effect:cmds(),
                         keep_alive => boolean()}.

Issues an HTTP request to Url via the browser fetch() API, without a page reload, and applies the response. Unlike push_event (WebSocket, can't set cookies) and a plain form POST (full reload), a fetch response can carry a real Set-Cookie -- HttpOnly honored, applied natively by the browser -- while the page stays put. Use it for flows that must rotate a session cookie yet keep the typed form fields and show inline validation (password change, login, logout).

Composes as a command: as an az_submit command the trigger form's fields are the request body; as an az_click command on a non-form element, body is sent instead.

The endpoint is a controller route (e.g. {post, ...}) returning the {"e": [...]} effects wire payload -- arizona_controller:reply_effects/1 for the success leg (200; a form with az-form-reset clears) or reply_effects/2 with a non-2xx (e.g. 422) for a validation error (the typed fields survive). The effects are applied on any status, against the submitting view's element. on_error (and an arizona:fetch-error DOM event) runs only when there is no usable effects body -- a non-JSON page, an empty non-2xx, or a network failure.

To re-render the live view, return an arizona_js:push_event in the response: the client relays it over the existing WebSocket and the view re-renders through its normal handle_event/3 (no subscription; it targets the submitting view and does not echo the form fields). Use arizona_pubsub from the controller instead to reach other views (broadcast). To send the user elsewhere, return arizona_js:navigate/1 (e.g. via reply_redirect/1), not an HTTP 3xx -- a fetch-followed redirect can't drive a SPA navigation.

Identity changes need a reload. The WebSocket is not re-handshaked when the cookie changes, so a fetch that changes who the user is (login/logout) must arizona_js:reload/0 (or do a real navigation) for the socket to pick up the new session -- push_event/navigate would re-render over the stale one. Rotating the cookie for the same identity (e.g. password change) needs no reload.

Opts:

  • method -- HTTP method. Default: the trigger form's method, else post.
  • body -- request body for a non-form trigger (JSON-encoded); it wins over a trigger form's fields. Default: the trigger form's fields as application/x-www-form-urlencoded. A GET/HEAD has no request body, so either payload goes in the query string instead, ahead of any #fragment: the form's fields as they are, a body map one parameter per key (a list repeats its key, null is left out, a nested map is JSON-encoded). Only the atom null is left out: any other atom is sent as its name, so undefined arrives as the text "undefined". A GET body that is not a map is not sent, with a console warning.
  • headers -- extra request headers, merged on top.
  • credentials -- cookie/credential mode. Default same_origin.
  • on_error -- a command (or list) run on a non-2xx with no effects body, or a network error.
  • keep_alive -- when true, the request completes even if a navigation starts right after it (use case: a POST fired just before navigating away); maps to the browser's fetch(url, { keepalive: true }). Absent or false is the current behaviour (the navigation cancels an in-flight fetch). The browser caps a keepalive request's inflight body at ~64KB.

To send the user elsewhere, return an arizona_js:navigate/1 effect from the controller (e.g. via arizona_controller:reply_redirect/1) rather than an HTTP 3xx -- a fetch-followed redirect can't drive a SPA navigation.

focus(Selector)

-spec focus(Selector) -> arizona_effect:cmd() when Selector :: binary().

Focuses the first element matching the selector.

hide(Selector)

-spec hide(Selector) -> arizona_effect:cmd() when Selector :: binary().

Hides all elements matching the CSS selector.

on_key(Key, Cmd)

-spec on_key(Key, Cmd) -> arizona_effect:cmd()
                when
                    Key :: atom() | [atom()] | binary(),
                    Cmd :: arizona_effect:cmd() | arizona_effect:cmds().

Wraps a command (or list of commands) so it only fires when a key matching Key is pressed. Key can be an atom (e.g. enter), a list of atoms (matches any), or a regex pattern as a binary.

Matching is case-insensitive: the client lowercases the browser's event.key before comparing, so give keys and patterns in lowercase (enter, escape, arrowdown). An uppercase name or pattern ('Enter', ~"^[A-Z]$") never matches.

patch(Path)

-spec patch(Path) -> arizona_effect:cmd() when Path :: binary().

Triggers an in-place SPA navigation (patch) to Path: keeps the current live view mounted and re-renders it via handle_update/3 instead of replacing it. The counterpart of navigate/1; use it between routes that share a root view so live chrome survives the navigation.

patch(Path, Opts)

-spec patch(Path, Opts) -> arizona_effect:cmd() when Path :: binary(), Opts :: map().

Triggers an in-place SPA navigation (patch) to Path with options. Accepts the same flash opt as navigate/2 (carried in-process to the patched route's flash binding, stripped from the client effect) and its replace opt.

An http/https Path on another origin cannot be patched in place: the client loads it as a page (location.assign, or location.replace with replace). Any other scheme is dropped with a warning, as for navigate/2.

push_event(Event)

-spec push_event(Event) -> arizona_effect:cmd() when Event :: binary().

Pushes a named event to the server. The payload is auto-collected from the triggering element itself, not its siblings: a form trigger contributes its fields (a form submit passes the form), an input/select/textarea trigger its value; any other trigger (e.g. a plain button inside a form) sends an empty payload. Use push_event/2 to attach data explicitly.

push_event(Event, Payload)

-spec push_event(Event, Payload) -> arizona_effect:cmd() when Event :: binary(), Payload :: map().

Pushes a named event with an explicit payload, merged on top of any auto-collected form data.

reload()

-spec reload() -> arizona_effect:cmd().

Reloads the current page.

remove_attr(Selector, Attr)

-spec remove_attr(Selector, Attr) -> arizona_effect:cmd() when Selector :: binary(), Attr :: binary().

Removes an attribute from all elements matching the selector.

remove_class(Selector, Class)

-spec remove_class(Selector, Class) -> arizona_effect:cmd() when Selector :: binary(), Class :: binary().

Removes a CSS class from all elements matching the selector.

request_pip(ViewId)

-spec request_pip(ViewId) -> arizona_effect:cmd() when ViewId :: binary().

Moves the view identified by ViewId into a floating Document Picture-in-Picture window, kept live by the server diff stream. Use inside a web event attribute (e.g. az-click) -- Document PiP requires a user gesture, so it won't open from a server-pushed handler effect. The window opens at the browser's default size; use request_pip/2 to choose a size.

request_pip(ViewId, Opts)

-spec request_pip(ViewId, Opts) -> arizona_effect:cmd()
                     when ViewId :: binary(), Opts :: #{width => pos_integer(), height => pos_integer()}.

Like request_pip/1 but with window options. width/height set the floating window's initial size in pixels; when omitted the browser picks a default size.

reset_form(Selector)

-spec reset_form(Selector) -> arizona_effect:cmd() when Selector :: binary().

Resets all forms matching the selector, clearing the fields the user typed back to their initial values (HTMLFormElement.reset).

Unlike focus/1/blur/1 (first match only), this broadcasts to every match, like the DOM-mutating selector effects (toggle, the class ops, set_attr). A match without a reset() method (a non-form element) is a safe no-op.

Where set_attr(value, ~"") only rewrites the value attribute (leaving the user-typed value property untouched, and doing nothing at all for a <textarea> that has no value attribute), this drives the native form reset, so it clears text inputs and textareas alike. Unlike az-form-reset (which fires only on a successful submit/fetch), it's imperative: usable both as a web event command (e.g. {az_click, arizona_js:reset_form(~"#signup")}) and as a handler effect from handle_event/3.

scroll_to(Selector)

-spec scroll_to(Selector) -> arizona_effect:cmd() when Selector :: binary().

Scrolls smoothly to the first element matching the selector.

scroll_to(Selector, Opts)

-spec scroll_to(Selector, Opts) -> arizona_effect:cmd() when Selector :: binary(), Opts :: map().

Scrolls to the first element matching the selector with scrollIntoView options (behavior, block, inline).

Opts merges onto the #{behavior => smooth} default of scroll_to/1, so tuning only the alignment keeps the smooth scroll; pass #{behavior => auto} for an instant jump.

select(Selector)

-spec select(Selector) -> arizona_effect:cmd() when Selector :: binary().

Selects (highlights) the text of the first element matching the selector, an <input> or <textarea>, via HTMLInputElement.select -- so a subsequent copy or overtype acts on the whole field.

Like focus/1/blur/1/scroll_to/1 (first match only). A match without a select() method is a safe no-op. Usable both as a web event command (e.g. {az_click, arizona_js:select(~"#token")}) and as a handler effect from handle_event/3.

set(Key, Value)

-spec set(Key, Value) -> arizona_effect:cmd()
             when Key :: binary() | atom(), Value :: binary() | boolean() | number().

Sets a client-owned slot (?local) to Value in the closest view of the triggering element, updating every slot with Key locally with no server round-trip. Use inside web event attributes like az-click.

set(ViewId, Key, Value)

-spec set(ViewId, Key, Value) -> arizona_effect:cmd()
             when ViewId :: binary(), Key :: binary() | atom(), Value :: binary() | boolean() | number().

Like set/2 but targets the slot in the view identified by ViewId.

set_all(Key, Value)

-spec set_all(Key, Value) -> arizona_effect:cmd()
                 when Key :: binary() | atom(), Value :: binary() | boolean() | number().

Like set/2 but updates the slot in every view on the page (document-wide).

set_attr(Selector, Attr, Value)

-spec set_attr(Selector, Attr, Value) -> arizona_effect:cmd()
                  when Selector :: binary(), Attr :: binary(), Value :: binary().

Sets an attribute on all elements matching the selector.

set_title(Title)

-spec set_title(Title) -> arizona_effect:cmd() when Title :: binary().

Sets the document title.

show(Selector)

-spec show(Selector) -> arizona_effect:cmd() when Selector :: binary().

Shows all elements matching the CSS selector.

show_modal(Selector)

-spec show_modal(Selector) -> arizona_effect:cmd() when Selector :: binary().

Opens the first <dialog> matching the selector as a true modal via HTMLDialogElement.showModal -- placed in the top layer, with a ::backdrop and ESC-to-close -- unlike merely setting the open attribute (a non-modal dialog). A non-dialog match is a safe no-op.

Like focus/1/blur/1/scroll_to/1 (first match only). Usable both as a web event command (e.g. {az_click, arizona_js:show_modal(~"#confirm")}) and as a handler effect from handle_event/3.

toggle(Selector)

-spec toggle(Selector) -> arizona_effect:cmd() when Selector :: binary().

Toggles the visibility of all elements matching the CSS selector.

toggle_attr(Selector, Attr)

-spec toggle_attr(Selector, Attr) -> arizona_effect:cmd() when Selector :: binary(), Attr :: binary().

Toggles the presence of a bare boolean attribute on all elements matching the selector: removes it if present, otherwise sets it (e.g. disabled, readonly, hidden, open). For toggling between two values, use toggle_attr/4.

toggle_attr(Selector, Attr, ValueA, ValueB)

-spec toggle_attr(Selector, Attr, ValueA, ValueB) -> arizona_effect:cmd()
                     when Selector :: binary(), Attr :: binary(), ValueA :: binary(), ValueB :: binary().

Toggles an attribute between two values on all elements matching the selector: sets ValueB when the current value is ValueA, otherwise sets ValueA (so any other current value resolves to ValueA). Useful for one-button toggles like a password field's type between password and text.

toggle_class(Selector, Class)

-spec toggle_class(Selector, Class) -> arizona_effect:cmd() when Selector :: binary(), Class :: binary().

Toggles a CSS class on all elements matching the selector.

transition(Cmd)

-spec transition(Cmd) -> arizona_effect:cmd() when Cmd :: arizona_effect:cmd() | arizona_effect:cmds().

Wraps a command (or list of commands) so the DOM change it causes plays inside a view transition (document.startViewTransition). Works for a client-side effect (e.g. toggle/1, animated immediately), a navigate/1,2 (the page swap animates), or a push_event (the resulting server diff animates). A view transition is not tied to navigation -- it wraps any DOM change. Equivalent to transition/2 with no options.

transition(Cmd, Opts)

-spec transition(Cmd, Opts) -> arizona_effect:cmd()
                    when
                        Cmd :: arizona_effect:cmd() | arizona_effect:cmds(),
                        Opts :: #{types => [binary()]}.

Like transition/1 but with options. types is a list of view-transition type names activated for the transition (matched by :active-view-transition-type(...) CSS), letting one stylesheet pick a different animation per call.