Events & Interactivity

Arizona's event system connects user interactions in the browser to server-side Erlang functions. Every click, form submission, and key press travels over the WebSocket to your handle_event/3 callback.

Event handling

The handle_event/3 callback receives three arguments:

handle_event(EventName, Params, View) ->
    {Actions, UpdatedView}.

State is managed through the arizona_stateful and arizona_view APIs — get the state from the view, update bindings, and put it back.

Return values

The return is always {Actions, View} where Actions is a list:

ActionEffect
[]No actions — just update state and re-render
[{redirect, Path}]Navigate to a new page
[{dispatch, Event, Payload}]Dispatch an event to another component

Form handling

Forms are the most common interactive pattern:

render(Bindings) ->
    CS = arizona_template:get_binding(changeset, Bindings),
    Errors = arizona_template:get_binding(errors, Bindings),
    arizona_template:from_html(~"""
    <form az-submit="save" az-change="validate">
        <input type="text" name="title"
               value="{maps:get(title, kura_changeset:apply_changes(CS))}" />
        {render_error(Errors, title)}

        <textarea name="body">{maps:get(body, kura_changeset:apply_changes(CS))}</textarea>
        {render_error(Errors, body)}

        <button type="submit">Save</button>
    </form>
    """).

handle_event(<<"validate">>, Params, View) ->
    State = arizona_view:get_state(View),
    CS = post:changeset(#{}, Params),
    Errors = changeset_errors_to_json(CS),
    S1 = arizona_stateful:put_binding(changeset, CS, State),
    S2 = arizona_stateful:put_binding(errors, Errors, S1),
    {[], arizona_view:update_state(S2, View)};

handle_event(<<"save">>, Params, View) ->
    CS = post:changeset(#{}, Params),
    case blog_repo:insert(CS) of
        {ok, Post} ->
            {[{redirect, "/posts/" ++ integer_to_list(maps:get(id, Post))}], View};
        {error, CS1} ->
            State = arizona_view:get_state(View),
            S1 = arizona_stateful:put_binding(changeset, CS1, State),
            S2 = arizona_stateful:put_binding(errors, changeset_errors_to_json(CS1), S1),
            {[], arizona_view:update_state(S2, View)}
    end.

The render_error/2 helper formats a field's error for display:

render_error(Errors, Field) ->
    case maps:get(atom_to_binary(Field), Errors, []) of
        [] -> <<>>;
        [Msg | _] -> arizona_template:from_html(~"<span class=\"error\">{Msg}</span>")
    end.

The az-change attribute triggers validation on every input change — giving users instant feedback without a form submission.

Passing values with events

Use az-value-* attributes to send data with events:

<button az-click="delete" az-value-id="42" az-value-type="post">Delete</button>

In handle_event:

handle_event(<<"delete">>, #{<<"id">> := Id, <<"type">> := Type}, View) ->
    ...

Key events

<input type="text" az-keydown="search" az-debounce="300" />

The az-debounce attribute delays the event by the specified milliseconds — useful for search-as-you-type to avoid flooding the server.

handle_event(<<"search">>, #{<<"value">> := Query}, View) ->
    Q = kura_query:from(post),
    Q1 = kura_query:where(Q, {title, ilike, <<"%", Query/binary, "%">>}),
    {ok, Results} = blog_repo:all(Q1),
    State = arizona_view:get_state(View),
    NewState = arizona_stateful:put_binding(results, Results, State),
    {[], arizona_view:update_state(NewState, View)}.

Client-side JavaScript hooks

Arizona exposes a JavaScript API for pushing events from custom JS code:

// Push an event to the live view
arizona.pushEvent("my_event", {key: "value"});

// Push to a specific component by ID
arizona.pushEventTo("#comment-form", "submit", {body: "Hello"});

// Call an event and get a reply
const result = await arizona.callEvent("get_data", {id: 42});

// Call on a specific component
const result = await arizona.callEventFrom("#search", "search", {q: "nova"});

On the server:

handle_event(<<"get_data">>, #{<<"id">> := Id}, View) ->
    {ok, Post} = blog_repo:get(post, binary_to_integer(Id)),
    {[{dispatch, <<"get_data_reply">>, #{title => maps:get(title, Post)}}], View}.

Actions

Actions let you trigger side effects alongside state updates:

handle_event(<<"publish">>, _Params, View) ->
    State = arizona_view:get_state(View),
    Post = arizona_stateful:get_binding(post, State),
    CS = post:changeset(Post, #{<<"status">> => <<"published">>}),
    {ok, Updated} = blog_repo:update(CS),
    NewState = arizona_stateful:put_binding(post, Updated, State),
    Actions = [
        {dispatch, <<"post_published">>, #{id => maps:get(id, Updated)}},
        {redirect, "/posts/" ++ integer_to_list(maps:get(id, Updated))}
    ],
    {Actions, arizona_view:update_state(NewState, View)}.

Next: Live Navigation — navigating between live views without full page reloads.