--- name: jj description: Expert guide for the JJ DOM manipulation library. Load this skill whenever you need to write, debug, or review JJ code; create native web components using JJ's defineComponent, setShadow, fetchTemplate, or fetchStyle; translate React, Vue, Svelte, Angular, jQuery, or Lit patterns to JJ idioms; work with JJHE, JJD, JJSE, JJME, JJDF, JJSR, JJET, JJN, or JJT wrappers; or write JJ tests with jsdom. If any JJ class name or helper function appears in the conversation, always load this skill. --- # JJ DOM Library JJ is a minimal, zero-dependency TypeScript library that wraps browser DOM interfaces in fluent, type-safe classes. It complements native browser APIs rather than replacing them. ## Translation Checklist When converting native DOM code, framework code, or vague UI requests into JJ, default to this order of thought: - Start from wrappers, not native nodes: `JJD.from(document)`, `JJE.from(document.documentElement)`, `JJHE.create()`, `JJHE.tree()`, `JJET.from(window)`, `getShadow(true)`. - Keep values wrapped and chain operations; use `.ref` only for native APIs JJ does not provide. - Use `JJHE.tree` with a local `h` alias for concise element creation, especially mapped/list children and nested UI. - Use `setChild()`/`setChildren()` to replace content and `addChildMap()`/`setChildMap()` for array rendering. - Prefer batch object-dictionary helpers (`setAttrs`, `setAriaAttrs`, `setDataAttrs`, `setStyles`, `setClasses`) over repeated singular setter chains when updating multiple keys. - Query with `find()`/`findAll()`/`closest()` instead of native `querySelector*` when JJ already covers the case. - For form-like value elements (`input`, `select`, `textarea`, `progress`, etc.), prefer `getValue()` / `setValue(...)` over `.ref.value`. - Use `setText()` for user content and treat `setHTML(..., true)` as a trusted-content escape hatch. - For repeated child interactions, prefer one delegated listener on a stable parent over one listener per child. - Choose shadow DOM for self-contained widgets and light DOM for page-level content that should inherit global styles. - For components, keep fetched templates/styles at module scope, attach shadow root in the constructor, initialize once, then update targeted nodes instead of rebuilding the whole tree. - Prefer `'open'` shadow roots unless the user explicitly needs `'closed'`; open mode is easier to test and debug. - Use plain JS state plus targeted wrapper updates by default; do not invent a virtual DOM style rerender loop unless the user explicitly wants one. ## Naming Conventions In this repository, prefix variables that hold JJ wrapper instances with `jj`. ```typescript const jjDoc = JJD.from(document) const jjFruits = jjDoc.find('#fruits', true) const jjSubmitBtn = jjDoc.find('button#submit', true) const jjDialog = JJHE.create('dialog') ``` Naming defaults: - Use `jj*` for JJ wrappers, including private fields: `#jjHost` for `JJHE.from(this)` (the wrapped host element) and `#jjShadow` for `this.#jjHost.getShadow()` (the wrapped shadow root). - Do not use `jj*` for plain data like `fruits`, `title`, `isOpen`, or `userName`. - Do not use `jj*` for native DOM values; prefer names like `formEl`, `shadowRoot`, `inputRef`, or `styleSheet`. - For promises, use normal names with `Promise`, like `templatePromise` or `stylePromise`. - `h` is the main intentional exception: use it as the local alias for `JJHE.tree`. ## Wrapper Hierarchy Each JJ wrapper exposes the native node via `.ref`. | Class | Wraps | Key additions | | ----- | ---------------- | ----------------------------------------------------------------------------- | | JJET | EventTarget | `.on()`, `.off()`, `.trigger()`, `.run()` | | JJN | Node | `.getParent()`, `.getChildren()`, `.rm()`, `.clone()` | | JJD | Document | `.find()`, `.findAll()` | | JJDF | DocumentFragment | `.addTemplate()`, `.setTemplate()`, batch child ops | | JJE | Element | Attributes, classes, ARIA, visibility, HTML write, `.getText()`, `.setText()` | | JJHE | HTMLElement | `.setStyle()`, `.setShadow()`, `.tree()` | | JJSE | SVGElement | SVG namespace factory, `.tree()` | | JJME | MathMLElement | MathML namespace factory, `.tree()` | | JJSR | ShadowRoot | `.find()`, `.findAll()`, `.addStyle()`, `.init()` | | JJDF | DocumentFragment | Fragment operations | | JJT | Text | `.getText()`, `.setText()` | Text semantics note: `JJE.getText()` and `JJE.setText()` use `textContent` only and are inherited by `JJHE`, `JJSE`, and `JJME`. For HTML-specific rendering-aware behavior (for example `innerText` line-break handling), use `jjEl.ref.innerText` on `JJHE` wrappers. Per MDN, `Document.textContent` and `DocumentType.textContent` are `null`; use `document.documentElement.textContent` or `jjDoc.ref.documentElement.textContent` for whole-document text. ```typescript const jjRootEl = JJE.from(document.documentElement) const fullPageText = jjRootEl.getText() ``` ## Type-Safe Creation — Always Use Factory Methods ```typescript // ✅ CORRECT — factory methods infer the precise generic type const jjDiv = JJHE.create('div') // JJHE const jjInput = JJHE.create('input') // JJHE const jjSvg = JJSE.create('svg') // JJSE const jjMath = JJME.create('math') // JJME const jjFrag = JJDF.create() // JJDF const jjBtn = JJHE.fromId('my-btn') // JJHE // ❌ WRONG JJHE.create('svg') // throws — use JJSE.create('svg') new JJHE(element) // don't call constructors directly ``` ## Chaining All mutating methods return `this`. Chain as much as possible; access `.ref` only when a wrapper method does not exist. ```typescript const jjBtn = JJHE.create('button') .addClass('btn', 'primary') .setText('Save') .setAttr('type', 'submit') .setAriaAttr('label', 'Save changes') .on('click', handleSave) ``` ## Tutorial Defaults — Prefer JJ Idioms Over Native DOM Steps When translating browser DOM code into JJ, do not mechanically keep native patterns like repeated `appendChild`, `querySelector`, or unwrap/re-wrap flows. Prefer the JJ equivalent that keeps work inside wrappers. ```typescript // ✅ preferred: build a subtree once const h = JJHE.tree latestChatResponse.addChild( h('section', null, h('h2', null, 'User'), h('div', null, userPrompt), h('h2', null, 'Assistant'), assistantMessage), ) // ✅ also fine for flat mapped children const jjList = JJHE.create('ul').addChildMap(fruits, (fruit) => h('li', null, fruit)) // ❌ avoid native-style wrapper escape hatches when JJ already covers it latestChatResponse.ref.appendChild(JJHE.create('h2').setText('User').ref) latestChatResponse.ref.appendChild(JJHE.create('div').setText(userPrompt).ref) latestChatResponse.ref.appendChild(JJHE.create('h2').setText('Assistant').ref) latestChatResponse.ref.appendChild(assistantMessage.ref) ``` Default heuristics from the tutorial: - Use `JJHE.tree` with a local `h` alias when creating multiple siblings or any nested subtree. - Prefer `h(tag, attrs, ...children)` over verbose `create(...).set*()` chains when the element can be expressed as structure (for example options, list items, rows, and cards). - For single-expression callbacks, prefer concise arrows: `x => h(...)` instead of `x => { return h(...) }`. - Use `create()` for one-off elements; switch to `tree()` as soon as structure becomes non-trivial. - Prefer `setChild()` or `setChildren()` when replacing content, not `.empty().addChild()`. - Prefer `addChildMap()` or `setChildMap()` when rendering from arrays. - Keep values wrapped. Reach for `.ref` only for native APIs JJ does not expose. - Use JJ verb families consistently: `set*` replaces, `add*` appends, `pre*` prepends, `rm*` removes, `sw*` toggles. ```typescript const h = JJHE.tree // ✅ preferred for mapped options const jjSelect = JJHE.create('select').addChildMap(items, ({ value, title }) => h('option', { value }, title)) // ✅ preferred for single-expression callbacks const jjList = JJHE.create('ul').addChildMap(items, (item) => h('li', null, item.title)) // ⚠️ avoid unnecessary block + return for a single h(...) expression const jjListVerbose = JJHE.create('ul').addChildMap(items, (item) => { return h('li', null, item.title) }) // ⚠️ avoid verbose chain for simple structure const jjSelectVerbose = JJHE.create('select').addChildMap(items, ({ value, title }) => JJHE.create('option').setValue(value).setText(title), ) ``` ## Document Queries Wrap `document` with `JJD.from(document)` before querying. ```typescript const jjDoc = JJD.from(document) const jjApp = jjDoc.find('#app', true) // throws when absent const jjCard = jjDoc.find('.card') // null when absent const jjItems = jjDoc.findAll('.item') // always an array // Inside a custom element's shadow root const jjBtn = this.getShadow(true).find('#submit') ``` Querying defaults from the tutorial: - Start from a wrapped container like `JJD.from(document)` or a `JJSR` shadow root. - Prefer `find(selector, true)` when absence is a bug; it fails earlier and more clearly than a later null access. - Prefer narrower selectors that encode expectations, like `button#submit`, instead of broad lookups plus manual type checks. - Use `findAll()` for arrays of wrappers and keep operating on wrappers instead of unwrapping to native elements. - Do not use `.ref.querySelector(...)` or `.ref.querySelectorAll(...)` when `find()` or `findAll()` already covers the case. - Use `.closest()` on wrappers for event delegation and ancestor lookup. - Use `JJHE.fromId('submit-btn')` for direct ID lookup when you already know the target is an HTML element. ## Attributes, Classes, Styles ```typescript // Attribute — singular jjEl.setAttr('role', 'button') jjEl.getAttr('role') jjEl.rmAttr('hidden') jjEl.swAttr('readonly') // auto: flips current state of the "readonly" attribute jjEl.swAttr('disabled', !isReady) // sets disabled="" or removes it // Attribute — batch (null/undefined skipped) jjEl.setAttrs({ type: 'text', placeholder: 'Search…' }) // Prefer batch updates for multiple keys on the same wrapper jjDoc .find('#source-url', true) .setAttrs({ href: sourceUrl, target: '_blank', rel: 'noopener noreferrer' }) .setText(sourceUrl) // Avoid repetitive singular setter chains for the same object-like update jjDoc .find('#source-url', true) .setAttr('href', sourceUrl) .setAttr('target', '_blank') .setAttr('rel', 'noopener noreferrer') .setText(sourceUrl) // Classes jjEl.addClass('active') // Multiple classes via varargs jjEl.addClass('active', 'selected') jjEl.rmClass('active', 'loading') // Multiple classes via array jjEl.addClasses(['chip', 'selected']) jjEl.rmClasses(['pending', 'loading']) // Explicit mode: truthy adds, falsy removes jjEl.swClass('expanded', isExpanded) // Auto mode: flips current state jjEl.swClass('is-active') // Batch conditional class updates jjEl.setClasses({ active: isActive, disabled: !isReady }) // Replace the entire className jjEl.setClass('card card--featured') // Dataset jjEl.getDataAttr('userId') jjEl.hasDataAttr('userId') jjEl.setDataAttr('userId', '42') jjEl.setDataAttrs({ role: 'admin', team: 'ui' }) // batch set jjEl.rmDataAttr('userId') jjEl.rmDataAttr('role', 'team') // batch remove, varargs syntax jjEl.rmDataAttrs(['role', 'team']) // batch remove, array syntax // ARIA jjEl.getAriaAttr('hidden') jjEl.hasAriaAttr('hidden') jjEl.setAriaAttr('hidden', 'true') jjEl.setAriaAttrs({ label: 'Dialog', modal: 'true' }) jjEl.rmAriaAttr('hidden') // ARIA is not presence-based like HTML boolean attributes // Use explicit string states instead of swAttr() jjEl.setAriaAttr('disabled', 'true') // Inline styles jjEl.setStyle('color', 'var(--color-brand)') jjEl.setStyles({ color: 'red', padding: '8px', border: null }) jjEl.rmStyle('color', 'padding') // Value helpers (prefer over .ref.value) jjEl.getValue() jjEl.setValue('next') ``` Use `.ref.value` only when a JJ value helper is unavailable for your exact use case. ## Security — HTML Writes Prefer `.setText()` for any user-supplied content. `.setHTML()` requires an explicit `true` flag when the string is non-empty. ```typescript jjEl.setText(userInput) // ✅ always safe jjEl.setHTML('

Trusted markup

', true) // ✅ explicit opt-in jjEl.setHTML('') // ✅ clearing is allowed without flag jjEl.setHTML('

content

') // ❌ THROWS — missing unsafe flag jjEl.ref.innerHTML = '…' // ❌ bypasses guard — avoid ``` ## Events ```typescript // Native events jjEl.on('click', handler) jjEl.off('click', handler) jjEl.triggerEvent('click') // Explicit event objects (equivalent to JJ helpers below) jjEl.trigger(new Event('click', { bubbles: true, composed: true })) jjEl.trigger(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true })) // Custom events — JJ defaults: bubbles: true, composed: true this.dispatchEvent(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true })) // Fluent dispatch (same defaults) jjEl.triggerEvent('click') // equivalent to trigger(new Event('click', { bubbles: true, composed: true })) JJHE.from(this).triggerCustomEvent('todo-toggle', { id: 1, done: true }) // equivalent to trigger(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true })) // Override defaults for internal-only events new CustomEvent('panel-ready', { bubbles: false, composed: false }) ``` Event defaults from the tutorial: - Prefer `.on()` and `.off()` on wrappers over native `addEventListener`/`removeEventListener` when already working with JJ values. - Prefer `.triggerEvent()` and `.triggerCustomEvent()` for common JJ event dispatch; they default to `bubbles: true` and `composed: true`. - Use `triggerCustomEvent(name, detail)` for component-to-parent communication instead of ad hoc callback plumbing. - Use `bubbles: false` and `composed: false` only for intentionally internal events. - Keep event code close to the wrapper it affects so later DOM updates stay targeted and local. Guide defaults for event-heavy UI: - Prefer event delegation on a common parent for repeated child actions instead of binding one listener per item. - Use `.closest()` to recover the intended delegated target from `event.target`. - When you need JJ's wrapper-bound `this` inside a listener, use `function` syntax, not an arrow. - Native UI events like `click`, `input`, and `change` already cross shadow boundaries; custom events do not unless `composed: true`. ```typescript list.on('click', function (event) { const jjItem = JJHE.from(event.target as Node).closest('[data-item-id]') if (!jjItem) return this.addClass('handled') jjItem.addClass('active') }) ``` ## Custom Elements — Complete Pattern Fetch template and style at **module scope** — loaded once, shared across all instances. Guide defaults for component shape: - Use shadow DOM for self-contained widgets and design-system components; use light DOM for sections that should inherit page styling and normal document flow. - Prefer `'open'` shadow mode unless stricter encapsulation is a hard requirement. - `attributeChangedCallback()` can run before `connectedCallback()` for parsed attributes, so setters and render paths must tolerate pre-mount state. - Use `disconnectedCallback()` only to clean up external side effects like document listeners, timers, observers, or subscriptions; do not tear down the shadow root just because the element was detached. ```typescript import { attr2prop, defineComponent, fetchStyle, fetchTemplate, JJHE } from 'jj' const templatePromise = fetchTemplate(import.meta.resolve('./my-card.html')) const stylePromise = fetchStyle(import.meta.resolve('./my-card.css')) export class MyCard extends HTMLElement { static observedAttributes = ['user-name', 'count'] static defined = defineComponent('my-card', MyCard) #userName = '' #count = 0 #jjShadow = null // JJSR wrapper; attached in constructor #isInitialized = false constructor() { super() this.#jjShadow = JJHE.from(this).setShadow('open').getShadow(true) } attributeChangedCallback(name, oldValue, newValue) { // Converts kebab-case → camelCase, then calls the matching setter attr2prop(this, name, oldValue, newValue) } get userName() { return this.#userName } set userName(v) { this.#userName = String(v ?? '') this.#render() } get count() { return this.#count } set count(v) { this.#count = Number(v) || 0 this.#render() } async connectedCallback() { if (!this.#isInitialized) { this.#jjShadow.init(await templatePromise, await stylePromise) this.#isInitialized = true } this.#render() } #render() { if (!this.#jjShadow) return // guard for attribute changes before mount this.#jjShadow.find('[data-role="name"]')?.setText(this.#userName) this.#jjShadow.find('[data-role="count"]')?.setText(String(this.#count)) } } // Caller must await before using the custom element tag await MyCard.defined // Or multiple in parallel await Promise.all([MyCard.defined, OtherCard.defined]) ``` Template defaults from the tutorial: - Prefer fetched `.html` templates for large static markup. - Prefer `