v0.21

reactivity

introduction

dframework provides two complementary reactivity systems that keep the user interface in sync with server state over the socket layer. no client side rendering framework is involved. the server renders html and the framework delivers it surgically to the correct dom target.

choosing a system

feature d-wire d-live
trigger an explicit user action a database write
best for counters, search, modals, forms feeds, dashboards, live lists
requires a named socket handler a d-live attribute, no handler
rendering the handler calls render the framework refetches automatically

use d-wire when the user expects a specific result from a deliberate action. use d-live when a region must stay in sync with data regardless of what caused it to change.

d-wire event driven reactivity

attach d-wire to any html element. when triggered, the element emits a named event over the socket to a socket handler. the handler renders a partial and the framework replaces the target container with the result.

1<button d-wire="counter:increment" d-target="#counter">+1</button>

passing data

use d-wire-data to send a payload with the event. it accepts key value pairs separated by commas or raw json.

1<button
2 d-wire="post:like"
3 d-wire-data="id:{{ post.id }}, type:'post'"
4 d-target="#likes-{{ post.id }}">
5 like
6</button>
7
8<button
9 d-wire="user:update"
10 d-wire-data='{"id": {{ user.id }}, "status": "active"}'
11 d-target="#user-status">
12 activate
13</button>

the payload is merged with any form data present and accessible as the request body in the handler.

backend handler

a d-wire handler must call render or json. the rendered html replaces the target element on the client. the framework hashes the rendered output and suppresses the update if the content has not changed, preventing redundant dom operations.

to prevent arbitrary dom injection, you must restrict which html elements a route can update by chaining targets() to your route definition.

1Socket.on('counter:increment', async (req, res) => {
2 const count = await Counter.increment(req.body.id);
3 return res.render('partials.counter', { count });
4}).targets(['#counter']);
5
6Socket.on('user:search', async (req, res) => {
7 const users = await User.where('name', 'LIKE', `%${req.body.q}%`).limit(10).get();
8 return res.render('partials.user-list', { users });
9}).targets(['#user-list']);

trigger types

d-wire binds to the natural event for each element type.

element default trigger
<button>, <a> click
<form> submit
<input>, <select> change

override with d-trigger.

1<!-- fire on keyup with debounce handled server side -->
2<input d-wire="user:search" d-target="#results" d-trigger="keyup" placeholder="search users">

special trigger values are available.

value behaviour
load fires once immediately when the element mounts
poll-{ms} fires continuously every n milliseconds, e.g. poll-5000

programmatic dispatch

emit a d-wire event from javascript or from inside a frontend component.

1Socket.emit('dashboard:refresh', {}, '#stats-panel');
2
3// from a component
4this.wire('notification:dismiss', { id: this.notificationId });

d-live state driven reactivity

d-live subscribes a dom element to a database table. whenever any model write touches that table, the framework evaluates active subscriptions, notifies matching clients, and the element refetches and replaces its own content. no backend handler is needed.

1<div d-live="posts">
2 @foreach(const post of await Post.where({ status: 'published' }).orderBy('created_at', 'DESC').get())
3 <article>
4 <h2>{{ post.title }}</h2>
5 <p>{{ post.excerpt }}</p>
6 </article>
7 @endforeach
8</div>

multiple elements can subscribe to the same or different tables simultaneously on a single page.

filtering

d-live-filter restricts updates to rows matching specific criteria. the server evaluates filters against the changed row before notifying clients. clients that do not match the filter receive no message and perform no refetch.

1<div d-live="orders" d-live-filter="user_id:{{ user.id }}">...</div>

supported operators include.

operator example meaning
= (default) status:published equals
!= status:!=archived not equal
> price:>100 greater than
< count:<5 less than
>= rating:>=4 greater than or equal
<= stock:<=10 less than or equal

multiple filters use and logic. use commas to separate them.

1<div d-live="messages" d-live-filter="room_id:{{ room.id }},status:visible">...</div>

how it works

  1. on mount, the element sends a subscribe message over the socket, registering itself for the table with any filter criteria.
  2. when the query builder executes a write, it notifies the socket router with the table name and the changed data.
  3. the router iterates active subscriptions, evaluates each filter against the changed data, and emits a notification only to clients whose filter matched.
  4. the subscribed element rerenders its own content. updates are debounced to one hundred milliseconds to handle bulk operations efficiently. updates are suppressed for two hundred and fifty milliseconds after a successful form submission to prevent double fetches.

surgical state sync

push state directly into a component's reactive memory without rerendering its template. this is useful for lightweight state updates that do not require a full html replacement.

1// backend, emit to a specific component by selector
2Socket.emit('d-sync:state', { unread: 14 }, '#notification-badge');

the targeted component calls its set state method automatically, triggering its render and effect loops.

error handling

spa and reactivity integrate with the abort helper.

form submissions use standard fetch requests. calling the abort method returns a structured json error object that the form renders inline next to the relevant inputs.

spa navigations use background requests. calling the abort method renders the full error page html, which the spa layer displays and reflects in the url. socket routing handlers also gracefully catch and log any rendering exceptions, falling back to a client safe error message when necessary.