Theme
On this page (4)

§The one runtime

core.js bundles defuss-morph + defuss-query + the shared component layer, and is the only thing that installs globalThis.df$. Component scripts bind to it - load core before them, or load all.js alone. A missing or mismatched core fails fast with one actionable load-order error, before anything renders.

How you load it depends on how you installed defuss-shadcn - <script type="module"> tags from the jsDelivr CDN in plain HTML, or side-effect imports through your bundler after npm install. The exact URLs, import paths and include order for both are on the Installation page; the rule is the same either way:

<!-- core first, then the components you use -->
<script type="module"
        src="https://cdn.jsdelivr.net/gh/kyr0/defuss-shadcn@latest/dist/components/core.min.js"></script>
<script type="module"
        src="https://cdn.jsdelivr.net/gh/kyr0/defuss-shadcn@latest/dist/components/dialog/dialog.min.js"></script>
<!-- …or everything at once (all.js embeds the same core payload) -->
<script type="module"
        src="https://cdn.jsdelivr.net/gh/kyr0/defuss-shadcn@latest/dist/components/all.min.js"></script>
// your app entry - the same order: core first, then the components you use
import 'defuss-shadcn/dist/components/core.js';
import 'defuss-shadcn/dist/components/dialog/dialog.js';
// …or everything at once
import 'defuss-shadcn/dist/components/all.js';
typeof df$;                  // 'function' - df$('button') selects
df$.queryVersion;             // the shipped query version
df$.morph(el, html, opts);    // the morph API surface
df$.shadcn.shared.abi;        // the release stamp components guard on
df$.shadcn.dialogApi;         // component registries (State API)
df$.shadcn.toast;             // imperative APIs (toast.show, …)

§Select, traverse, write scalars

Selections are snapshot arrays, not live queries: retain stable roots and re-query after structural changes. Scalar setters write flags, attributes, classes and values - exactly what native-state components need, without a subtree re-render:

// select (CSS selectors, scoped to a root)
const tabs = df$('.tab', root);            // DfQuery - an Array subclass
tabs.length;                                // snapshot size
// traverse - each hop returns a NEW selection
tabs.children('.tab-label');
df$('#trigger').closest('.field');
// scalar writes (mutators return the selection for chaining)
df$(trigger).prop('disabled', true);         // native boolean/IDL property
df$(opt).attr('aria-selected', 'false');     // explicit ARIA false string
df$(opt).attr('aria-expanded', null);        // null REMOVES the attribute
df$(input).val('');                          // live control value
df$(el).addClass('open').removeClass('closed');
df$(label).text('Saved.');                   // literal user-facing text
mode selection - which operation goes where
Required operationSanctioned path
Full next subtree, including deletions / reorderingdf$(container).morph(nextContent)
Addressed partial updates / upserts, retaining unmentioned siblingsdf$(container).morph(changeSet, { diff: true })
Exact insertion, move, or removaldf$(x).append() / .prepend() / .before() / .after() / .remove()
Native flag, ARIA attribute, class, or control valuedf$(x).prop() / .attr() / class methods / .val()
Literal user-facing textdf$(x).text(value)

§Reconciliation for structure

df$(container).morph(next) (aliased as .html(next)) replaces the container's children through a keyed DOM diff instead of innerHTML replacement - matched nodes are moved, never replaced, so listeners, focus, selection and live form values survive. Keys must represent identity (a date, an option value), not the current filtered position:

// full render: nodes missing from `next` are removed, order is reconciled
df$(grid).morph(nextGridHtml);            // stable ids: <td id="d2026-09-13">
// diff: upserts the addressed items, keeps unmentioned siblings
df$(list).morph({ tag: 'ul', children: [
  { tag: 'li', key: 'opt-2', attrs: { 'aria-selected': 'true' }, children: ['Second'] },
] }, { diff: true });

Two limits to code around: HTML strings cannot express explicitly unchecked or cleared uncontrolled inputs - write those as scalars (.prop('checked', false), .val('')) - and diff: true is an upsert, never a deletion or reorder operation. Without a transition, .morph() returns the selection synchronously; with one it returns a Promise - await it before dependent operations.

§Events, teardown, and safety

// element listeners live in morph's delegated registry - morphing never detaches them
df$(region).on('click', function () { /* this === the selected target */ });
df$(region).off('click', ownedHandler);     // remove owned handlers only
df$(el).trigger('df-custom');               // dispatches CustomEvent - not .click()
// exact lifecycle: mount / update / dismiss through query's structural adapter
df$(host).append(toastEl);                  // inserts the node itself (identity kept)
df$(toastEl).morph(nextToastHtml);          // reconciles owned content
df$(toastEl).remove();                      // removes after your own teardown

Treat every string input as an HTML sink, not a sanitizer: use .text() for untrusted labels, never pass user text where markup is parsed (including the markup form of the factory call - a string starting with a less-than sign). Keep native protocols native - showModal(), showPopover(), focus(), once/passive/signal listeners stay on the platform APIs; component teardown still owns its timers and observers. Full behavioral contract: dist/components/core.js + the State API guide.

Comments, ideas or improvements? Edit this page's source on GitHub