Zotonic JavaScript
First Pass
- Inspect nearby templates, JavaScript modules, workers, actions, and Erlang
event/2 handlers before editing; preserve local patterns.
- Prefer Zotonic declarative behavior (
{% wire %}, actions, Cotonic data attributes, workers, and do_... widgets) over one-off DOM scripts.
- Put reusable JavaScript under the module or site
priv/lib/js tree and include it with {% lib %} from the relevant include template.
- Use plain JavaScript and small functional helpers; keep jQuery usage only where existing Zotonic widgets/actions require it.
- For source documentation, Erlang actions, scomps, models, and modules should have
-moduledoc; use those docs as the local source of truth.
Template JavaScript
- Include JavaScript libraries with
{% lib "js/file.js" %} or multi-file {% lib %} blocks. Options include minify, nocache, async, and defer; {% lib ... minify %} can force minification.
- Add module/site JS includes through local include templates such as
_js_include.tpl, _admin_js_include.tpl, _html_head.tpl, or _html_body.tpl instead of duplicating script tags in pages.
- Put page-specific inline JavaScript inside
{% javascript %}...{% endjavascript %}. It runs after jQuery is initialized; for dynamic content it runs after the DOM update that inserted the template.
- Ensure the base template has exactly one
{% script %}, normally near the end of <body>. It emits collected JavaScript from {% javascript %}, {% wire %}, actions, and related scomps.
- Do not put generated JavaScript after
{% script %} in a page; it will not be included in that page render.
{% script nostartup %} omits startup code, and format="html" | "escapejs" | "js" controls output format. Use these only when the caller expects a nonstandard script output.
- Direct
<script> tags must carry the CSP nonce: <script nonce="{{ m.req.csp_nonce }}">. Prefer {% javascript %} or {% lib %} when possible because they fit Zotonic's collection/minification flow.
Wires And Actions
- Use
{% wire %} to bind browser events to actions and optional server postbacks. The default event is click.
{% wire id="show" action={show target="message"} %}
<button id="show" type="button">{_ Show _}</button>
- Use
type="submit" to wire form submission. The target form should have an id and usually method="post" action="postback".
{% wire id="edit-form" type="submit" postback={save id=id} delegate=`mod_example` %}
<form id="edit-form" method="post" action="postback">
...
</form>
- A click wire with
postback=... sends a #postback{} to the delegate. A submit wire sends a #submit{}.
- Use
delegate=`mod_example` when the event/2 handler is not in the controller or current module.
- Use repeated
action={...} arguments for client-side effects before/after a postback; keep user-visible text translated.
- Named wires can be triggered from JavaScript with
z_event("name"): {% wire name="refresh-list" action={update target="list" template="_list.tpl"} %}.
- MQTT wires can subscribe client actions to topics when
mod_mqtt is enabled, for example {% wire type={mqtt topic="~site/public/hello"} action={growl text="hello"} %}.
- Actions live under
src/actions/ as action_<module>_<name>.erl. They normally implement render_action/4 and should document arguments, generated JavaScript, postbacks, and security assumptions in -moduledoc.
Erlang Event Handlers
- Browser wire events are received by
event/2; include the relevant records via zotonic.hrl or zotonic_wired.hrl.
#postback{message, trigger, target} is sent for normal postbacks from clicks and explicit postback actions.
#submit{message, form, target} is sent for wired form submits; access fields with z_context:get_q/2, z_context:get_q_all/1, or z_context:get_q_validated/2.
#postback_notify{message, trigger, target, data} is a notification-style event used by JavaScript postback handlers; see zotonic_notifications.hrl and nearby module event/2 clauses for exact payloads.
- Return the updated
Context. Use z_render:update/3, replace/3, insert_*, dialog/4, dialog_close/1, growl/2, and z_render:wire/2 to queue browser responses.
Client Postback Notify
- Send a
#postback_notify{} from browser JavaScript with z_notify(message, params), defined in apps/zotonic_mod_wires/priv/lib/js/apps/zotonic-wired.js.
- Without
z_delegate, z_notify sends to the server-side postback_notify observer chain via the notify delegate. With z_delegate: 'mod_name', it calls mod_name:event(#postback_notify{}, Context).
- Use
z_target_id for the element that should receive possible updates and z_trigger_id for the triggering element. Other params are available as request/query values in Context.
z_notify automatically sends the current CSP nonce and any stored postback data.
z_notify("update", {
z_delegate: "mod_admin",
z_target_id: targetId,
z_trigger_id: triggerId,
id: resourceId
});
event(#postback_notify{message = <<"update">>, target = TargetId}, Context) ->
Id = z_context:get_q(<<"id">>, Context),
Html = z_template:render("_rsc_item.tpl", [{id, Id}], Context),
z_render:update(TargetId, Html, Context).
Postback Client State
- Zotonic wires can attach client-side state to every postback, submit, and
postback_notify sent by zotonic-wired.js.
- Page-scoped state is stored as JSON in the
<body data-wired-postback="..."> attribute.
- Tab-scoped state is stored under
sessionStorage.postbackData; persistent browser/site state is stored under localStorage.postbackData.
z_postback_data() merges these three stores and sends the result as a query parameter named z_postback_data.
- Merge precedence is body attribute over sessionStorage over localStorage. Use page-scoped state for current-page UI state, sessionStorage for per-tab state, and localStorage only for state that should survive reloads and new tabs.
z_notify(...) adds this data to #postback_notify{data = #{ q := ... }}. Normal postback and submit events add the same z_postback_data value to the #postback_event{data = #{ q := ... }} payload before it becomes #postback{} or #submit{}.
- On the server, read it with
z_context:get_q(<<"z_postback_data">>, Context) and validate it like any other client-provided value.
case z_context:get_q(<<"z_postback_data">>, Context) of
#{ <<"z_edit_language">> := Lang } ->
handle_language(Lang, Context);
_ ->
Context
end.
- Set page-scoped state from client JavaScript with
z_postback_data_set(Name, Value). Read it with z_postback_data_get(Name).
- Set tab-scoped state with
z_postback_data_set_session(Name, Value), which publishes to model/sessionStorage/post/postbackData/<Name>.
- Set persistent state with
z_postback_data_set_local(Name, Value), which publishes to model/localStorage/post/postbackData/<Name>.
- Delete a key from all three stores with
z_postback_data_delete(Name).
z_postback_data_set("z_edit_language", "nl");
z_postback_data_set_session("wizard_step", 3);
z_postback_data_set_local("preferred_panel", "advanced");
z_postback_data_delete("wizard_step");
- Set initial page-scoped state from a template by rendering the JSON-encoded map on
<body data-wired-postback="...">; escape it as an HTML attribute.
- Set or change state from a server response by emitting JavaScript with a
{script} action, {% javascript %} in rendered HTML, or z_render:add_script/2. Escape any values placed into generated JavaScript.
- From Erlang code running in a page/client context, persistent client stores can also be updated by publishing to the current client bridge:
z_mqtt:publish(
[<<"~client">>, <<"model">>, <<"sessionStorage">>, <<"post">>, <<"postbackData">>, <<"wizard_step">>],
3,
Context),
z_mqtt:publish(
[<<"~client">>, <<"model">>, <<"localStorage">>, <<"post">>, <<"postbackData">>, <<"preferred_panel">>],
<<"advanced">>,
Context).
-spec event(#submit{} | #postback{}, z:context()) -> z:context().
event(#submit{message = {save, Args}, form = FormId}, Context0) ->
Title = z_context:get_q(<<"title">>, Context0),
Context = save_title(Args, Title, Context0),
z_render:growl(?__("Saved.", Context), z_render:update(FormId, <<>>, Context));
event(#postback{message = refresh, target = TargetId}, Context) ->
Html = z_template:render("_list.tpl", [], Context),
z_render:update(TargetId, Html, Context).
Security
- Always use a nonce on direct script tags:
nonce="{{ m.req.csp_nonce }}".
- Treat query arguments, form fields, postback payload data, MQTT payloads, and Cotonic data attribute values as untrusted. Validate in
event/2 and server model callbacks.
- Do not interpolate untrusted template values directly into JavaScript. Use JSON/JS escaping filters appropriate to the local code, and prefer passing structured data via data attributes or MQTT payloads.
- Signed postbacks protect the postback command, not arbitrary form/query data. Validate ids through
m_rsc, ACL checks, or model functions before modifying state.
- Client-side MQTT topics are subject to bridge/server authorization, but handlers must still validate payload shape, ids, and permissions.
Client Server Communication
- Zotonic uses Cotonic in the browser and MQTT-style messaging between browser and server.
_html_head_cotonic.tpl creates cotonic.ready, pre-connects cotonic.bridgeSocket to the mqtt_transport WebSocket with the mqtt subprotocol, and buffers early click/submit data-attribute events.
_js_include.tpl loads cotonic/cotonic.js, js/apps/zotonic-wired.js, js/apps/z.widgetmanager.js, and other base modules. Include _html_head.tpl/_html_head_admin.tpl and _js_include.tpl through the normal base template flow.
controller_mqtt_transport.erl handles MQTT over WebSocket and authenticated HTTP fallback/post traffic. Authentication can use the z.auth cookie or MQTT username/password.
- Add connection status HTML with
_bridge_warning.tpl where the site wants to show “Connecting...” and a connection-test link.
- MQTT topics are slash-separated and support
+ and # wildcards. Server z_mqtt supports QoS 0, 1, and 2, and options such as retain; most browser communication uses QoS 0 unless a call explicitly asks otherwise.
- Do not assume exactly-once delivery for JavaScript relay traffic. The browser/server bridge queues while reconnecting, and the server page process buffers until the browser connects, but persistent semantics depend on the server topic, retain flag, and QoS path being used.
- There are two topic trees: the browser's local Cotonic broker and Zotonic's server broker.
bridge/origin/... on the client publishes/calls the server origin tree. Server topics under bridge/<client-id>/... route to the browser tree.
- Server shorthand topics include
~client for the current client bridge and ~user for the current user topic. Core server topic roots include public, test, user, user/<id>, and bridge/<client-id>.
- Server models are reachable through topics such as
bridge/origin/model/<model>/get/..., bridge/origin/model/<model>/post/..., and bridge/origin/model/<model>/delete/...; server-side mod_mqtt dispatches them through z_model:callback/5.
- Client-routing topics on the server are the
bridge/... topics; use them for page-specific browser communication, not for durable global state.
Client Publish Subscribe
- Wait for
cotonic.ready before browser code depends on Cotonic startup.
cotonic.ready.then(() => {
const sub = cotonic.broker.subscribe("bridge/origin/test/#", (msg, bindings, options) => {
console.log(msg, bindings, options);
});
cotonic.broker.publish("bridge/origin/test/hello", { text: "Hello" });
cotonic.broker.call(
"bridge/origin/model/template/get/render/_item.tpl",
{ id: 123 },
{ qos: 1 }
).then((resp) => cotonic.broker.publish("model/ui/replace/item", resp.payload.result));
});
- Use
cotonic.broker.publish(topic, payload, options) for fire-and-forget messages, subscribe(filter, callback, options) for subscriptions, and call(topic, payload, options) when a response topic is expected.
- The
m_template model adds the call payload as query arguments before rendering. In this example _item.tpl reads q.id; the payload does not create a top-level id template variable.
Server Publish Subscribe
- Use
z_mqtt for Erlang-side MQTT. Prefer binary topic segments or the helper mapping functions when topic parts are dynamic.
z_mqtt:subscribe([<<"my">>, <<"topic">>, '#'], Context),
z_mqtt:publish([<<"my">>, <<"topic">>], #{status => ok}, #{qos => 1, retain => true}, Context).
- A subscribed Erlang process receives
{mqtt_msg, Msg} when using process subscriptions.
- Modules can export quoted
mqtt: callback functions. mod_mqtt scans active modules and subscribes these with a sudo context.
-export(['mqtt:test/#'/2]).
'mqtt:test/#'(#{payload := Payload, topic := Topic}, Context) ->
handle_test_message(Topic, Payload, Context).
Cotonic
- Cotonic is the browser-side runtime for isolated workers, models, topic routing, and interactive DOM updates. See cotonic.org for the upstream concepts and use local Zotonic sources for Zotonic-specific topics.
- Workers are spawned by Cotonic (
cotonic.spawn, cotonic.spawn_named, or Zotonic template worker tags). Worker code uses self.subscribe, self.publish, and self.call and declares provides/depends so startup can order services.
- The service worker coordinates cross-tab/browser features. Zotonic uses topics such as
model/serviceWorker/post/broadcast/+channel and model/serviceWorker/event/broadcast/+channel for browser-window synchronization, including auth state sync.
- Common client models include
model/localStorage, model/sessionStorage, model/sessionId, model/document, model/location, model/window, model/ui, model/serviceWorker, model/lifecycle, model/autofocus, model/dedup, model/auth, model/auth-ui, model/oauth, model/loadmore, and module-specific models such as model/fileuploader.
- Use local/client models directly from JavaScript (
model/localStorage/get/key) and server models via the origin bridge (bridge/origin/model/rsc/get/...). Server code can target client models by publishing to the current client bridge (~client or bridge/<client-id>/...).
- Cotonic data attributes publish DOM events to topics:
data-onclick-topic, data-onsubmit-topic, data-oninput-topic, with matching data-on...-cancel attributes for cancellation behavior.
- Add
data-cotonic-pathname-search="{% cotonic_pathname_search %}" to <body> in normal pages so Cotonic location/UI logic has the routed pathname/search value.
- The interactive DOM is updated by publishing to UI topics such as
model/ui/insert/<key>, model/ui/update/<key>, model/ui/replace/<key>, model/ui/delete/<key>, and model/ui/render-template/<key>. Listen for DOM update events when follow-up initialization is needed.
- Check Zotonic Cotonic workers and models under
apps/*/priv/lib/js/**/*.worker.js, apps/*/priv/lib/js/models/*.js, and base files such as apps/zotonic_mod_wires/priv/lib/js/apps/zotonic-wired.js.
Authentication
zotonic.auth.worker.js owns browser auth state. It checks, refreshes, logs on/off, resets, changes, and switches users by calling /zotonic-auth and publishing auth model events.
- Important auth topics include
model/auth/post/check, model/auth/post/logon, model/auth/post/logoff, model/auth/post/refresh, model/auth/post/form/logon, model/auth/post/onetime-token, model/auth/event/auth, model/auth/event/auth-user-id, model/auth/event/auth-error, and model/auth/event/ui-status.
- The
z.auth cookie is the browser auth cookie managed by server authentication token code and refreshed/reset via /zotonic-auth. Client code should go through model/auth topics instead of editing this cookie directly.
zotonic.auth-ui.worker.js owns auth UI flows such as login views, reminders, verification messages, reset, change, and confirmation. It listens to model/auth-ui/post/... and calls server models via bridge/origin/model/authentication/....
zotonic.oauth.worker.js coordinates OAuth authorize/redirect flows, stores temporary OAuth data through model/localStorage/model/sessionStorage, calls bridge/origin/model/oauth2_service/post/oauth-redirect, and publishes auth onetime-token or UI status topics as needed.
do Widgets
z.widgetmanager.js initializes classes starting with do_. The class do_clickable maps to the jQuery widget/plugin clickable; do_dialog maps to show_dialog.
- Widget options are read from metadata/data attributes such as
data-adminwidget='{"minifiedOnInit": true}', merged with widget defaults, and passed to the plugin.
- Run widgets by adding the class and including the widget JavaScript through
{% lib %}. The widget manager initializes existing DOM on page startup and new nodes after IncrementalDOM/Cotonic updates.
{% lib "js/modules/z.clickable.js" %}
<div class="do_clickable" data-clickable='{"url":"/example"}'>...</div>
- Define widgets as normal jQuery UI/Zotonic widgets in
priv/lib/js/modules/ and set defaults on the widget, for example $.ui.clickable.defaults = {...}.
- Core Zotonic widgets under
apps/ include base widgets do_clickable, do_smiley, do_feedback, do_timesince, do_tooltip, do_inputoverlay, do_autocomplete, do_zeditor, do_datepicker, do_formdirty, do_popupwindow, do_filepreview, do_forminit, and do_dialog.
- Additional core module widgets include
do_live (mod_mqtt), do_adminwidget (mod_admin), do_menuedit/do_trash/Superfish menu behavior (mod_menu), do_cookie_consent, do_survey_test_feedback, do_gaq_track, and do_make_diff.
- Before adding a new widget, run
rg "do_<name>|\$\.widget|\.defaults" apps/*/priv/lib/js to avoid duplicating an existing core widget.
Live Search With do_feedback
- Prefer the existing
do_feedback widget for a debounced server-rendered live search. Ensure js/modules/z.feedback.js is included by the active page or admin JavaScript bundle before relying on the class.
- Put
class="do_feedback" and a JSON data-feedback attribute on the result container. trigger is the id of a form or input; the widget listens for keyup and change, with a default debounce of 600 ms that can be overridden with timeout.
- A form trigger serializes all named form fields into the request payload. A single input trigger sends its value as
triggervalue. Prefer a form when the result template needs multiple values, including hidden context fields.
- With a
template option, the widget calls bridge/origin/model/template/get/render/<template> and replaces the result container with resp.payload.result. Payload fields are query arguments in the rendered template, so read them through q.*.
{% wire id=#search_form type="submit" action={script script=""} %}
<form id="{{ #search_form }}" role="search">
<input type="search" name="text" autocomplete="off">
</form>
<div class="do_feedback"
data-feedback='{ "trigger": "{{ #search_form }}", "template": "_search_results.tpl" }'>
{% include "_search_results.tpl" text=text %}
</div>
- Use
text|default:q.text in a result partial that is both included normally and rendered dynamically. Keep an initial include in the result container when useful, because do_feedback only updates after the trigger changes.
- Keep a live-search form separate from a surrounding action form. Otherwise pressing Enter in the search field can submit an unrelated default button. Wire the search form to an empty script action, as above, to suppress native submission.
- If dynamically rendered selection buttons must submit another form, pass that form's generated id through a hidden search field and set the button's HTML
form="{{ form_id|escape }}" attribute.
- Without a
template option, do_feedback sends z_notify("feedback", ...) to the configured delegate as a #postback_notify{}. The handler must validate inputs and permissions, update the target, and remove its loading class. Use this delegate mode when the search needs an explicit server-side ACL check or other application logic.
- Direct template rendering retains the caller context and the called models' ACL checks, but does not add permission checks. Treat all live-search payload fields as untrusted and escape them at output boundaries.
1---2name: zotonic-javascript3description: Use when creating, refactoring, or reviewing Zotonic JavaScript, template JavaScript tags, wires, actions, Erlang event/2 browser handlers, Cotonic workers/models, MQTT client-server communication, authentication workers, and do_ widgets in Zotonic sites or modules.4---56# Zotonic JavaScript78## First Pass910- Inspect nearby templates, JavaScript modules, workers, actions, and Erlang `event/2` handlers before editing; preserve local patterns.11- Prefer Zotonic declarative behavior (`{% wire %}`, actions, Cotonic data attributes, workers, and `do_...` widgets) over one-off DOM scripts.12- Put reusable JavaScript under the module or site `priv/lib/js` tree and include it with `{% lib %}` from the relevant include template.13- Use plain JavaScript and small functional helpers; keep jQuery usage only where existing Zotonic widgets/actions require it.14- For source documentation, Erlang actions, scomps, models, and modules should have `-moduledoc`; use those docs as the local source of truth.1516## Template JavaScript1718- Include JavaScript libraries with `{% lib "js/file.js" %}` or multi-file `{% lib %}` blocks. Options include `minify`, `nocache`, `async`, and `defer`; `{% lib ... minify %}` can force minification.19- Add module/site JS includes through local include templates such as `_js_include.tpl`, `_admin_js_include.tpl`, `_html_head.tpl`, or `_html_body.tpl` instead of duplicating script tags in pages.20- Put page-specific inline JavaScript inside `{% javascript %}...{% endjavascript %}`. It runs after jQuery is initialized; for dynamic content it runs after the DOM update that inserted the template.21- Ensure the base template has exactly one `{% script %}`, normally near the end of `<body>`. It emits collected JavaScript from `{% javascript %}`, `{% wire %}`, actions, and related scomps.22- Do not put generated JavaScript after `{% script %}` in a page; it will not be included in that page render.23- `{% script nostartup %}` omits startup code, and `format="html" | "escapejs" | "js"` controls output format. Use these only when the caller expects a nonstandard script output.24- Direct `<script>` tags must carry the CSP nonce: `<script nonce="{{ m.req.csp_nonce }}">`. Prefer `{% javascript %}` or `{% lib %}` when possible because they fit Zotonic's collection/minification flow.2526## Wires And Actions2728- Use `{% wire %}` to bind browser events to actions and optional server postbacks. The default event is click.2930```django31{% wire id="show" action={show target="message"} %}32<button id="show" type="button">{_ Show _}</button>33```3435- Use `type="submit"` to wire form submission. The target form should have an id and usually `method="post" action="postback"`.3637```django38{% wire id="edit-form" type="submit" postback={save id=id} delegate=`mod_example` %}39<form id="edit-form" method="post" action="postback">40 ...41</form>42```4344- A click wire with `postback=...` sends a `#postback{}` to the delegate. A submit wire sends a `#submit{}`.45- Use ``delegate=`mod_example` `` when the `event/2` handler is not in the controller or current module.46- Use repeated `action={...}` arguments for client-side effects before/after a postback; keep user-visible text translated.47- Named wires can be triggered from JavaScript with `z_event("name")`: `{% wire name="refresh-list" action={update target="list" template="_list.tpl"} %}`.48- MQTT wires can subscribe client actions to topics when `mod_mqtt` is enabled, for example `{% wire type={mqtt topic="~site/public/hello"} action={growl text="hello"} %}`.49- Actions live under `src/actions/` as `action_<module>_<name>.erl`. They normally implement `render_action/4` and should document arguments, generated JavaScript, postbacks, and security assumptions in `-moduledoc`.5051## Erlang Event Handlers5253- Browser wire events are received by `event/2`; include the relevant records via `zotonic.hrl` or `zotonic_wired.hrl`.54- `#postback{message, trigger, target}` is sent for normal postbacks from clicks and explicit postback actions.55- `#submit{message, form, target}` is sent for wired form submits; access fields with `z_context:get_q/2`, `z_context:get_q_all/1`, or `z_context:get_q_validated/2`.56- `#postback_notify{message, trigger, target, data}` is a notification-style event used by JavaScript postback handlers; see `zotonic_notifications.hrl` and nearby module `event/2` clauses for exact payloads.57- Return the updated `Context`. Use `z_render:update/3`, `replace/3`, `insert_*`, `dialog/4`, `dialog_close/1`, `growl/2`, and `z_render:wire/2` to queue browser responses.5859## Client Postback Notify6061- Send a `#postback_notify{}` from browser JavaScript with `z_notify(message, params)`, defined in `apps/zotonic_mod_wires/priv/lib/js/apps/zotonic-wired.js`.62- Without `z_delegate`, `z_notify` sends to the server-side `postback_notify` observer chain via the `notify` delegate. With `z_delegate: 'mod_name'`, it calls `mod_name:event(#postback_notify{}, Context)`.63- Use `z_target_id` for the element that should receive possible updates and `z_trigger_id` for the triggering element. Other params are available as request/query values in `Context`.64- `z_notify` automatically sends the current CSP nonce and any stored postback data.6566```javascript67z_notify("update", {68 z_delegate: "mod_admin",69 z_target_id: targetId,70 z_trigger_id: triggerId,71 id: resourceId72});73```7475```erlang76event(#postback_notify{message = <<"update">>, target = TargetId}, Context) ->77 Id = z_context:get_q(<<"id">>, Context),78 Html = z_template:render("_rsc_item.tpl", [{id, Id}], Context),79 z_render:update(TargetId, Html, Context).80```8182## Postback Client State8384- Zotonic wires can attach client-side state to every postback, submit, and `postback_notify` sent by `zotonic-wired.js`.85- Page-scoped state is stored as JSON in the `<body data-wired-postback="...">` attribute.86- Tab-scoped state is stored under `sessionStorage.postbackData`; persistent browser/site state is stored under `localStorage.postbackData`.87- `z_postback_data()` merges these three stores and sends the result as a query parameter named `z_postback_data`.88- Merge precedence is body attribute over sessionStorage over localStorage. Use page-scoped state for current-page UI state, sessionStorage for per-tab state, and localStorage only for state that should survive reloads and new tabs.89- `z_notify(...)` adds this data to `#postback_notify{data = #{ q := ... }}`. Normal postback and submit events add the same `z_postback_data` value to the `#postback_event{data = #{ q := ... }}` payload before it becomes `#postback{}` or `#submit{}`.90- On the server, read it with `z_context:get_q(<<"z_postback_data">>, Context)` and validate it like any other client-provided value.9192```erlang93case z_context:get_q(<<"z_postback_data">>, Context) of94 #{ <<"z_edit_language">> := Lang } ->95 handle_language(Lang, Context);96 _ ->97 Context98end.99```100101- Set page-scoped state from client JavaScript with `z_postback_data_set(Name, Value)`. Read it with `z_postback_data_get(Name)`.102- Set tab-scoped state with `z_postback_data_set_session(Name, Value)`, which publishes to `model/sessionStorage/post/postbackData/<Name>`.103- Set persistent state with `z_postback_data_set_local(Name, Value)`, which publishes to `model/localStorage/post/postbackData/<Name>`.104- Delete a key from all three stores with `z_postback_data_delete(Name)`.105106```javascript107z_postback_data_set("z_edit_language", "nl");108z_postback_data_set_session("wizard_step", 3);109z_postback_data_set_local("preferred_panel", "advanced");110z_postback_data_delete("wizard_step");111```112113- Set initial page-scoped state from a template by rendering the JSON-encoded map on `<body data-wired-postback="...">`; escape it as an HTML attribute.114- Set or change state from a server response by emitting JavaScript with a `{script}` action, `{% javascript %}` in rendered HTML, or `z_render:add_script/2`. Escape any values placed into generated JavaScript.115- From Erlang code running in a page/client context, persistent client stores can also be updated by publishing to the current client bridge:116117```erlang118z_mqtt:publish(119 [<<"~client">>, <<"model">>, <<"sessionStorage">>, <<"post">>, <<"postbackData">>, <<"wizard_step">>],120 3,121 Context),122z_mqtt:publish(123 [<<"~client">>, <<"model">>, <<"localStorage">>, <<"post">>, <<"postbackData">>, <<"preferred_panel">>],124 <<"advanced">>,125 Context).126```127128```erlang129-spec event(#submit{} | #postback{}, z:context()) -> z:context().130event(#submit{message = {save, Args}, form = FormId}, Context0) ->131 Title = z_context:get_q(<<"title">>, Context0),132 Context = save_title(Args, Title, Context0),133 z_render:growl(?__("Saved.", Context), z_render:update(FormId, <<>>, Context));134event(#postback{message = refresh, target = TargetId}, Context) ->135 Html = z_template:render("_list.tpl", [], Context),136 z_render:update(TargetId, Html, Context).137```138139## Security140141- Always use a nonce on direct script tags: `nonce="{{ m.req.csp_nonce }}"`.142- Treat query arguments, form fields, postback payload data, MQTT payloads, and Cotonic data attribute values as untrusted. Validate in `event/2` and server model callbacks.143- Do not interpolate untrusted template values directly into JavaScript. Use JSON/JS escaping filters appropriate to the local code, and prefer passing structured data via data attributes or MQTT payloads.144- Signed postbacks protect the postback command, not arbitrary form/query data. Validate ids through `m_rsc`, ACL checks, or model functions before modifying state.145- Client-side MQTT topics are subject to bridge/server authorization, but handlers must still validate payload shape, ids, and permissions.146147## Client Server Communication148149- Zotonic uses Cotonic in the browser and MQTT-style messaging between browser and server.150- `_html_head_cotonic.tpl` creates `cotonic.ready`, pre-connects `cotonic.bridgeSocket` to the `mqtt_transport` WebSocket with the `mqtt` subprotocol, and buffers early click/submit data-attribute events.151- `_js_include.tpl` loads `cotonic/cotonic.js`, `js/apps/zotonic-wired.js`, `js/apps/z.widgetmanager.js`, and other base modules. Include `_html_head.tpl`/`_html_head_admin.tpl` and `_js_include.tpl` through the normal base template flow.152- `controller_mqtt_transport.erl` handles MQTT over WebSocket and authenticated HTTP fallback/post traffic. Authentication can use the `z.auth` cookie or MQTT username/password.153- Add connection status HTML with `_bridge_warning.tpl` where the site wants to show “Connecting...” and a connection-test link.154- MQTT topics are slash-separated and support `+` and `#` wildcards. Server `z_mqtt` supports QoS `0`, `1`, and `2`, and options such as `retain`; most browser communication uses QoS 0 unless a call explicitly asks otherwise.155- Do not assume exactly-once delivery for JavaScript relay traffic. The browser/server bridge queues while reconnecting, and the server page process buffers until the browser connects, but persistent semantics depend on the server topic, retain flag, and QoS path being used.156- There are two topic trees: the browser's local Cotonic broker and Zotonic's server broker. `bridge/origin/...` on the client publishes/calls the server origin tree. Server topics under `bridge/<client-id>/...` route to the browser tree.157- Server shorthand topics include `~client` for the current client bridge and `~user` for the current user topic. Core server topic roots include `public`, `test`, `user`, `user/<id>`, and `bridge/<client-id>`.158- Server models are reachable through topics such as `bridge/origin/model/<model>/get/...`, `bridge/origin/model/<model>/post/...`, and `bridge/origin/model/<model>/delete/...`; server-side `mod_mqtt` dispatches them through `z_model:callback/5`.159- Client-routing topics on the server are the `bridge/...` topics; use them for page-specific browser communication, not for durable global state.160161## Client Publish Subscribe162163- Wait for `cotonic.ready` before browser code depends on Cotonic startup.164165```javascript166cotonic.ready.then(() => {167 const sub = cotonic.broker.subscribe("bridge/origin/test/#", (msg, bindings, options) => {168 console.log(msg, bindings, options);169 });170171 cotonic.broker.publish("bridge/origin/test/hello", { text: "Hello" });172173 cotonic.broker.call(174 "bridge/origin/model/template/get/render/_item.tpl",175 { id: 123 },176 { qos: 1 }177 ).then((resp) => cotonic.broker.publish("model/ui/replace/item", resp.payload.result));178});179```180181- Use `cotonic.broker.publish(topic, payload, options)` for fire-and-forget messages, `subscribe(filter, callback, options)` for subscriptions, and `call(topic, payload, options)` when a response topic is expected.182- The `m_template` model adds the call payload as query arguments before rendering. In this example `_item.tpl` reads `q.id`; the payload does not create a top-level `id` template variable.183184## Server Publish Subscribe185186- Use `z_mqtt` for Erlang-side MQTT. Prefer binary topic segments or the helper mapping functions when topic parts are dynamic.187188```erlang189z_mqtt:subscribe([<<"my">>, <<"topic">>, '#'], Context),190z_mqtt:publish([<<"my">>, <<"topic">>], #{status => ok}, #{qos => 1, retain => true}, Context).191```192193- A subscribed Erlang process receives `{mqtt_msg, Msg}` when using process subscriptions.194- Modules can export quoted `mqtt:` callback functions. `mod_mqtt` scans active modules and subscribes these with a sudo context.195196```erlang197-export(['mqtt:test/#'/2]).198199'mqtt:test/#'(#{payload := Payload, topic := Topic}, Context) ->200 handle_test_message(Topic, Payload, Context).201```202203## Cotonic204205- Cotonic is the browser-side runtime for isolated workers, models, topic routing, and interactive DOM updates. See [cotonic.org](https://cotonic.org/) for the upstream concepts and use local Zotonic sources for Zotonic-specific topics.206- Workers are spawned by Cotonic (`cotonic.spawn`, `cotonic.spawn_named`, or Zotonic template worker tags). Worker code uses `self.subscribe`, `self.publish`, and `self.call` and declares `provides`/`depends` so startup can order services.207- The service worker coordinates cross-tab/browser features. Zotonic uses topics such as `model/serviceWorker/post/broadcast/+channel` and `model/serviceWorker/event/broadcast/+channel` for browser-window synchronization, including auth state sync.208- Common client models include `model/localStorage`, `model/sessionStorage`, `model/sessionId`, `model/document`, `model/location`, `model/window`, `model/ui`, `model/serviceWorker`, `model/lifecycle`, `model/autofocus`, `model/dedup`, `model/auth`, `model/auth-ui`, `model/oauth`, `model/loadmore`, and module-specific models such as `model/fileuploader`.209- Use local/client models directly from JavaScript (`model/localStorage/get/key`) and server models via the origin bridge (`bridge/origin/model/rsc/get/...`). Server code can target client models by publishing to the current client bridge (`~client` or `bridge/<client-id>/...`).210- Cotonic data attributes publish DOM events to topics: `data-onclick-topic`, `data-onsubmit-topic`, `data-oninput-topic`, with matching `data-on...-cancel` attributes for cancellation behavior.211- Add `data-cotonic-pathname-search="{% cotonic_pathname_search %}"` to `<body>` in normal pages so Cotonic location/UI logic has the routed pathname/search value.212- The interactive DOM is updated by publishing to UI topics such as `model/ui/insert/<key>`, `model/ui/update/<key>`, `model/ui/replace/<key>`, `model/ui/delete/<key>`, and `model/ui/render-template/<key>`. Listen for DOM update events when follow-up initialization is needed.213- Check Zotonic Cotonic workers and models under `apps/*/priv/lib/js/**/*.worker.js`, `apps/*/priv/lib/js/models/*.js`, and base files such as `apps/zotonic_mod_wires/priv/lib/js/apps/zotonic-wired.js`.214215## Authentication216217- `zotonic.auth.worker.js` owns browser auth state. It checks, refreshes, logs on/off, resets, changes, and switches users by calling `/zotonic-auth` and publishing auth model events.218- Important auth topics include `model/auth/post/check`, `model/auth/post/logon`, `model/auth/post/logoff`, `model/auth/post/refresh`, `model/auth/post/form/logon`, `model/auth/post/onetime-token`, `model/auth/event/auth`, `model/auth/event/auth-user-id`, `model/auth/event/auth-error`, and `model/auth/event/ui-status`.219- The `z.auth` cookie is the browser auth cookie managed by server authentication token code and refreshed/reset via `/zotonic-auth`. Client code should go through `model/auth` topics instead of editing this cookie directly.220- `zotonic.auth-ui.worker.js` owns auth UI flows such as login views, reminders, verification messages, reset, change, and confirmation. It listens to `model/auth-ui/post/...` and calls server models via `bridge/origin/model/authentication/...`.221- `zotonic.oauth.worker.js` coordinates OAuth authorize/redirect flows, stores temporary OAuth data through `model/localStorage`/`model/sessionStorage`, calls `bridge/origin/model/oauth2_service/post/oauth-redirect`, and publishes auth onetime-token or UI status topics as needed.222223## do Widgets224225- `z.widgetmanager.js` initializes classes starting with `do_`. The class `do_clickable` maps to the jQuery widget/plugin `clickable`; `do_dialog` maps to `show_dialog`.226- Widget options are read from metadata/data attributes such as `data-adminwidget='{"minifiedOnInit": true}'`, merged with widget defaults, and passed to the plugin.227- Run widgets by adding the class and including the widget JavaScript through `{% lib %}`. The widget manager initializes existing DOM on page startup and new nodes after IncrementalDOM/Cotonic updates.228229```django230{% lib "js/modules/z.clickable.js" %}231<div class="do_clickable" data-clickable='{"url":"/example"}'>...</div>232```233234- Define widgets as normal jQuery UI/Zotonic widgets in `priv/lib/js/modules/` and set defaults on the widget, for example `$.ui.clickable.defaults = {...}`.235- Core Zotonic widgets under `apps/` include base widgets `do_clickable`, `do_smiley`, `do_feedback`, `do_timesince`, `do_tooltip`, `do_inputoverlay`, `do_autocomplete`, `do_zeditor`, `do_datepicker`, `do_formdirty`, `do_popupwindow`, `do_filepreview`, `do_forminit`, and `do_dialog`.236- Additional core module widgets include `do_live` (`mod_mqtt`), `do_adminwidget` (`mod_admin`), `do_menuedit`/`do_trash`/Superfish menu behavior (`mod_menu`), `do_cookie_consent`, `do_survey_test_feedback`, `do_gaq_track`, and `do_make_diff`.237- Before adding a new widget, run `rg "do_<name>|\$\.widget|\.defaults" apps/*/priv/lib/js` to avoid duplicating an existing core widget.238239## Live Search With `do_feedback`240241- Prefer the existing `do_feedback` widget for a debounced server-rendered live search. Ensure `js/modules/z.feedback.js` is included by the active page or admin JavaScript bundle before relying on the class.242- Put `class="do_feedback"` and a JSON `data-feedback` attribute on the result container. `trigger` is the id of a form or input; the widget listens for `keyup` and `change`, with a default debounce of 600 ms that can be overridden with `timeout`.243- A form trigger serializes all named form fields into the request payload. A single input trigger sends its value as `triggervalue`. Prefer a form when the result template needs multiple values, including hidden context fields.244- With a `template` option, the widget calls `bridge/origin/model/template/get/render/<template>` and replaces the result container with `resp.payload.result`. Payload fields are query arguments in the rendered template, so read them through `q.*`.245246```django247{% wire id=#search_form type="submit" action={script script=""} %}248<form id="{{ #search_form }}" role="search">249 <input type="search" name="text" autocomplete="off">250</form>251<div class="do_feedback"252 data-feedback='{ "trigger": "{{ #search_form }}", "template": "_search_results.tpl" }'>253 {% include "_search_results.tpl" text=text %}254</div>255```256257- Use `text|default:q.text` in a result partial that is both included normally and rendered dynamically. Keep an initial include in the result container when useful, because `do_feedback` only updates after the trigger changes.258- Keep a live-search form separate from a surrounding action form. Otherwise pressing Enter in the search field can submit an unrelated default button. Wire the search form to an empty script action, as above, to suppress native submission.259- If dynamically rendered selection buttons must submit another form, pass that form's generated id through a hidden search field and set the button's HTML `form="{{ form_id|escape }}"` attribute.260- Without a `template` option, `do_feedback` sends `z_notify("feedback", ...)` to the configured `delegate` as a `#postback_notify{}`. The handler must validate inputs and permissions, update the target, and remove its `loading` class. Use this delegate mode when the search needs an explicit server-side ACL check or other application logic.261- Direct template rendering retains the caller context and the called models' ACL checks, but does not add permission checks. Treat all live-search payload fields as untrusted and escape them at output boundaries.