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
.refonly for native APIs JJ does not provide. - Use
JJHE.treewith a localhalias for concise element creation, especially mapped/list children and nested UI. - Use
setChild()/setChildren()to replace content andaddChildMap()/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 nativequerySelector*when JJ already covers the case. - For form-like value elements (
input,select,textarea,progress, etc.), prefergetValue()/setValue(...)over.ref.value. - Use
setText()for user content and treatsetHTML(..., 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.
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:#jjHostforJJHE.from(this)(the wrapped host element) and#jjShadowforthis.#jjHost.getShadow()(the wrapped shadow root). - Do not use
jj*for plain data likefruits,title,isOpen, oruserName. - Do not use
jj*for native DOM values; prefer names likeformEl,shadowRoot,inputRef, orstyleSheet. - For promises, use normal names with
Promise, liketemplatePromiseorstylePromise. his the main intentional exception: use it as the local alias forJJHE.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.
const jjRootEl = JJE.from(document.documentElement)
const fullPageText = jjRootEl.getText()
Type-Safe Creation — Always Use Factory Methods
// ✅ CORRECT — factory methods infer the precise generic type
const jjDiv = JJHE.create('div') // JJHE<HTMLDivElement>
const jjInput = JJHE.create('input') // JJHE<HTMLInputElement>
const jjSvg = JJSE.create('svg') // JJSE<SVGSVGElement>
const jjMath = JJME.create('math') // JJME<MathMLElement>
const jjFrag = JJDF.create() // JJDF
const jjBtn = JJHE.fromId('my-btn') // JJHE<HTMLButtonElement>
// ❌ 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.
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.
// ✅ 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.treewith a localhalias when creating multiple siblings or any nested subtree. - Prefer
h(tag, attrs, ...children)over verbosecreate(...).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 ofx => { return h(...) }. - Use
create()for one-off elements; switch totree()as soon as structure becomes non-trivial. - Prefer
setChild()orsetChildren()when replacing content, not.empty().addChild(). - Prefer
addChildMap()orsetChildMap()when rendering from arrays. - Keep values wrapped. Reach for
.refonly for native APIs JJ does not expose. - Use JJ verb families consistently:
set*replaces,add*appends,pre*prepends,rm*removes,sw*toggles.
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.
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 aJJSRshadow 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(...)whenfind()orfindAll()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
// 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.
jjEl.setText(userInput) // ✅ always safe
jjEl.setHTML('<p>Trusted markup</p>', true) // ✅ explicit opt-in
jjEl.setHTML('') // ✅ clearing is allowed without flag
jjEl.setHTML('<p>content</p>') // ❌ THROWS — missing unsafe flag
jjEl.ref.innerHTML = '…' // ❌ bypasses guard — avoid
Events
// 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 nativeaddEventListener/removeEventListenerwhen already working with JJ values. - Prefer
.triggerEvent()and.triggerCustomEvent()for common JJ event dispatch; they default tobubbles: trueandcomposed: true. - Use
triggerCustomEvent(name, detail)for component-to-parent communication instead of ad hoc callback plumbing. - Use
bubbles: falseandcomposed: falseonly 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 fromevent.target. - When you need JJ's wrapper-bound
thisinside a listener, usefunctionsyntax, not an arrow. - Native UI events like
click,input, andchangealready cross shadow boundaries; custom events do not unlesscomposed: true.
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 beforeconnectedCallback()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.
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
.htmltemplates for large static markup. - Prefer
<template>elements for reusable DOM snippets already present in the page. - Prefer
JJHE.tree()orJJHE.create()when you need live wrapper references for later updates. - Keep template promises at module scope; for lazy loading, initialize them inside
connectedCallback()with anif (!templatePromise)guard. - Use one stable wrapper per component:
#jjHostwithJJHE.from(this)for light DOM, or#jjShadowwithJJHE.from(this).setShadow(...).getShadow(true)for shadow DOM. - Initialize template content once, then update specific nodes with
find(...).setText(...)or other targeted wrapper operations.
Guide defaults for attributes and queries:
- Always coerce attribute-backed values in setters because HTML attributes arrive as strings.
- Query inside shadow DOM from the
JJSRwrapper, never fromdocument. - Use specific selectors like
button#submitor[data-role="title"]so the selector carries intent.
State defaults from the tutorial:
- Prefer plain objects or classes for state and update the exact affected wrappers in event handlers or setters.
- Prefer targeted updates like
value.setText(String(state.count))over rebuilding an entire subtree for a small change. - Use getters/setters or small helper methods when they make state transitions clearer, not because JJ requires a framework-style abstraction.
- Reach for external state libraries only when the application actually needs cross-cutting coordination beyond local JS state.
defineComponent() returns Promise<boolean>:
false— newly defined by this calltrue— already defined with the same constructor
Tree Builder
JJHE.tree is a factory for declarative element trees. Alias as h for brevity.
const h = JJHE.tree
const card = h(
'article',
{ class: 'card' },
h('h2', null, title),
h('p', { class: 'body' }, description),
h('footer', null, h('a', { href: url }, 'Read more')),
)
Children and Templates
// Clear children — internally uses replaceChildren()
jjEl.empty()
// Replace all children in one call (prefer over .empty().addChild())
jjEl.setChild(newChild)
jjEl.setChildren([childA, childB])
jjEl.setChildMap(items, (item) => JJHE.tree('li', null, item.label))
jjEl.setTemplate(templateElement)
// Append
jjEl.addChild(child)
jjEl.addChildMap(items, (item) => JJHE.tree('li', null, item.label))
jjEl.addTemplate(await templatePromise) // clones before appending
addChild / preChild / setChild and map variants ignore null/undefined; all other non-node values are coerced to Text nodes.
Guide defaults for template and fragment usage:
addTemplate()andsetTemplate()always clone the input before appending; reuse the same template value safely.- Prefer
setTemplate()overempty().addTemplate()when replacing all content. - Prefer
addChildMap()orsetChildMap()over manually building a fragment when rendering arrays. - Use
JJDF.create()when you need to assemble multiple sibling nodes before one insertion.
Node Traversal
const parent = jjEl.getParent() // wrapped parent or null (detached)
const children = jjEl.getChildren() // wrapped child array (always an array)
jjEl.rm() // detach from parent (no-op if already detached)
const ancestor = jjEl.closest('[data-section]') // null if not found
Resource Loaders
import { JJHE, fetchStyle, fetchTemplate } from 'jj'
const h = JJHE.tree
// Hint browser to preload early with native <link>
document.head.append(
h('link', {
href: import.meta.resolve('./bundle.js'),
rel: 'modulepreload',
}).ref,
)
document.head.append(
h('link', {
href: import.meta.resolve('./main.css'),
rel: 'preload',
as: 'style',
}).ref,
)
// Load a CSSStyleSheet for adoptedStyleSheets or setShadow
const sheet = await fetchStyle(import.meta.resolve('./theme.css'))
document.adoptedStyleSheets = [sheet]
// Load a DocumentFragment for addTemplate / setShadow
const fragment = await fetchTemplate(import.meta.resolve('./dialog.html'))
Guide defaults for browser-native loading hints:
- Use native
<link>hints built withJJHE.treeand appended toheadforpreload,prefetch, andmodulepreload. - Keep the
asvalue explicit forpreloadinstead of inferring it from file extensions. - Use
preloadfor current-page needs,prefetchfor probable future navigation, andmodulepreloadfor module graphs you want fetched early.
String Casing
String case-conversion helpers are internal implementation details.
Use higher-level public APIs like attr2prop and defineComponent instead of importing low-level casing utilities.
Common mistakes
.tsextension in imports — TypeScript source must use.js(import { X } from './X.js').JJHE.create('svg')— throws; useJJSE.create('svg').jjEl.setHTML(html)withouttrue— throws when html is non-empty.- Fetching template/style inside
connectedCallback— fetch at module scope so the network request is shared. - Not awaiting
Element.defined— markup may be parsed before the element is defined, causing flaky upgrades. - Breaking the chain with
.refunnecessarily — use wrapper methods first; reach for.refonly when no wrapper method exists. - Using
.ref.valuefor common value updates — prefergetValue()/setValue(...)for wrapper-level reads/writes.
Pitfall Prevention Rules (Component Work)
Apply these rules whenever building or refactoring JJ-based custom elements:
- Template-first component UI — if component markup is static, use
fetchTemplate+setTemplate(or shadowinit) rather than building the UI imperatively in JS. - Keep routing/URL state outside UI components — query params and history updates belong in page/controller code, not in reusable visual components.
- Query with wrappers, not native DOM first — prefer
find/findAllon JJ wrappers before dropping to.ref. - Do not unwrap and re-wrap — avoid
find(...).reffollowed byJJHE.from(...); keep the wrapper value. - Use specific selectors for required nodes — prefer selectors like
button#saveandprogress#step-progressso selector intent replaces manualinstanceofchecks. - Keep one canonical state-update path — for state like
step, centralize validation/clamping/render/event dispatch in one code path; avoid split setter/private-method duplication unless clearly justified. - Use one initialization invariant — if
isInitializedexists and guarantees bound refs are ready, guard on that flag instead of repeating null checks for every bound field. - Prefer fluent assignment when binding handlers — in setup code, chain
.on(...)directly onfind(...)where readable (for example assigning a button wrapper and click handler in one line).
Reference Docs
For framework migration or deep-dive patterns, load these on demand:
references/react-to-jj-translation.mdreferences/vue-to-jj-translation.mdreferences/svelte-to-jj-translation.mdreferences/angular-to-jj-translation.mdreferences/jquery-to-jj-translation.mdreferences/lit-to-jj-translation.mdreferences/dom-to-jj-translation.mdreferences/web-components-patterns.mdreferences/eventing-patterns.mdreferences/querying-patterns.mdreferences/css-improvements.mdreferences/testing-with-jsdom.mdreferences/security-and-html.mdreferences/error-handling-patterns.md