defuss-shadcn / Guides / dom-querying
DOM Querying & Morphing
Every interactive component runs on one runtime: the callable df$, installed once by core.js (or embedded first inside all.js). It is defuss-query - a small jQuery-shaped, native-DOM facade - over defuss-morph, a key/id-aware DOM reconciler. Select and mutate through it; keep native browser protocols native.
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 useimport 'defuss-shadcn/dist/components/core.js';import 'defuss-shadcn/dist/components/dialog/dialog.js';// …or everything at onceimport 'defuss-shadcn/dist/components/all.js';typeof df$; // 'function' - df$('button') selectsdf$.queryVersion; // the shipped query versiondf$.morph(el, html, opts); // the morph API surfacedf$.shadcn.shared.abi; // the release stamp components guard ondf$.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 subclasstabs.length; // snapshot size// traverse - each hop returns a NEW selectiontabs.children('.tab-label');df$('#trigger').closest('.field');// scalar writes (mutators return the selection for chaining)df$(trigger).prop('disabled', true); // native boolean/IDL propertydf$(opt).attr('aria-selected', 'false'); // explicit ARIA false stringdf$(opt).attr('aria-expanded', null); // null REMOVES the attributedf$(input).val(''); // live control valuedf$(el).addClass('open').removeClass('closed');df$(label).text('Saved.'); // literal user-facing text| Required operation | Sanctioned path |
|---|---|
| Full next subtree, including deletions / reordering | df$(container).morph(nextContent) |
| Addressed partial updates / upserts, retaining unmentioned siblings | df$(container).morph(changeSet, { diff: true }) |
| Exact insertion, move, or removal | df$(x).append() / .prepend() / .before() / .after() / .remove() |
| Native flag, ARIA attribute, class, or control value | df$(x).prop() / .attr() / class methods / .val() |
| Literal user-facing text | df$(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 reconcileddf$(grid).morph(nextGridHtml); // stable ids: <td id="d2026-09-13">// diff: upserts the addressed items, keeps unmentioned siblingsdf$(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 themdf$(region).on('click', function () { /* this === the selected target */ });df$(region).off('click', ownedHandler); // remove owned handlers onlydf$(el).trigger('df-custom'); // dispatches CustomEvent - not .click()// exact lifecycle: mount / update / dismiss through query's structural adapterdf$(host).append(toastEl); // inserts the node itself (identity kept)df$(toastEl).morph(nextToastHtml); // reconciles owned contentdf$(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