jj 3.0.0-rc.5 → 3.0.0-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jj",
3
- "version": "3.0.0-rc.5",
3
+ "version": "3.0.0-rc.7",
4
4
  "description": "A minimal DOM manipulation library with web components",
5
5
  "keywords": [
6
6
  "javascript",
package/skills/SKILL.md CHANGED
@@ -7,6 +7,42 @@ description: Expert guide for the JJ DOM manipulation library. Load this skill w
7
7
 
8
8
  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.
9
9
 
10
+ ## Translation Checklist
11
+
12
+ When converting native DOM code, framework code, or vague UI requests into JJ, default to this order of thought:
13
+
14
+ - Start from wrappers, not native nodes: `JJD.from(document)`, `JJHE.create()`, `JJHE.tree()`, `JJET.from(window)`, `getShadow(true)`.
15
+ - Keep values wrapped and chain operations; use `.ref` only for native APIs JJ does not provide.
16
+ - Use `JJHE.tree` with a local `h` alias for multi-node or nested UI.
17
+ - Use `setChild()`/`setChildren()` to replace content and `addChildMap()`/`setChildMap()` for array rendering.
18
+ - Query with `find()`/`findAll()`/`closest()` instead of native `querySelector*` when JJ already covers the case.
19
+ - For form-like value elements (`input`, `select`, `textarea`, `progress`, etc.), prefer `getValue()` / `setValue(...)` over `.ref.value`.
20
+ - Use `setText()` for user content and treat `setHTML(..., true)` as a trusted-content escape hatch.
21
+ - For repeated child interactions, prefer one delegated listener on a stable parent over one listener per child.
22
+ - Choose shadow DOM for self-contained widgets and light DOM for page-level content that should inherit global styles.
23
+ - 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.
24
+ - Prefer `'open'` shadow roots unless the user explicitly needs `'closed'`; open mode is easier to test and debug.
25
+ - 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.
26
+
27
+ ## Naming Conventions
28
+
29
+ In this repository, prefix variables that hold JJ wrapper instances with `jj`.
30
+
31
+ ```typescript
32
+ const jjDoc = JJD.from(document)
33
+ const jjFruits = jjDoc.find('#fruits', true)
34
+ const jjSubmitBtn = jjDoc.find('button#submit', true)
35
+ const jjDialog = JJHE.create('dialog')
36
+ ```
37
+
38
+ Naming defaults:
39
+
40
+ - 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).
41
+ - Do not use `jj*` for plain data like `fruits`, `title`, `isOpen`, or `userName`.
42
+ - Do not use `jj*` for native DOM values; prefer names like `formEl`, `shadowRoot`, `inputRef`, or `styleSheet`.
43
+ - For promises, use normal names with `Promise`, like `templatePromise` or `stylePromise`.
44
+ - `h` is the main intentional exception: use it as the local alias for `JJHE.tree`.
45
+
10
46
  ## Wrapper Hierarchy
11
47
 
12
48
  Each JJ wrapper exposes the native node via `.ref`.
@@ -29,12 +65,12 @@ Each JJ wrapper exposes the native node via `.ref`.
29
65
 
30
66
  ```typescript
31
67
  // ✅ CORRECT — factory methods infer the precise generic type
32
- const div = JJHE.create('div') // JJHE<HTMLDivElement>
33
- const input = JJHE.create('input') // JJHE<HTMLInputElement>
34
- const svg = JJSE.create('svg') // JJSE<SVGSVGElement>
35
- const math = JJME.create('math') // JJME<MathMLElement>
36
- const frag = JJDF.create() // JJDF
37
- const btn = JJHE.fromId('my-btn') // JJHE<HTMLButtonElement>
68
+ const jjDiv = JJHE.create('div') // JJHE<HTMLDivElement>
69
+ const jjInput = JJHE.create('input') // JJHE<HTMLInputElement>
70
+ const jjSvg = JJSE.create('svg') // JJSE<SVGSVGElement>
71
+ const jjMath = JJME.create('math') // JJME<MathMLElement>
72
+ const jjFrag = JJDF.create() // JJDF
73
+ const jjBtn = JJHE.fromId('my-btn') // JJHE<HTMLButtonElement>
38
74
 
39
75
  // ❌ WRONG
40
76
  JJHE.create('svg') // throws — use JJSE.create('svg')
@@ -46,7 +82,7 @@ new JJHE(element) // don't call constructors directly
46
82
  All mutating methods return `this`. Chain as much as possible; access `.ref` only when a wrapper method does not exist.
47
83
 
48
84
  ```typescript
49
- const btn = JJHE.create('button')
85
+ const jjBtn = JJHE.create('button')
50
86
  .addClass('btn', 'primary')
51
87
  .setText('Save')
52
88
  .setAttr('type', 'submit')
@@ -54,98 +90,153 @@ const btn = JJHE.create('button')
54
90
  .on('click', handleSave)
55
91
  ```
56
92
 
93
+ ## Tutorial Defaults — Prefer JJ Idioms Over Native DOM Steps
94
+
95
+ 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.
96
+
97
+ ```typescript
98
+ // ✅ preferred: build a subtree once
99
+ const h = JJHE.tree
100
+
101
+ latestChatResponse.addChild(
102
+ h('section', null, h('h2', null, 'User'), h('div', null, userPrompt), h('h2', null, 'Assistant'), assistantMessage),
103
+ )
104
+
105
+ // ✅ also fine for flat mapped children
106
+ const jjList = JJHE.create('ul').addChildMap(fruits, (fruit) => h('li', null, fruit))
107
+
108
+ // ❌ avoid native-style wrapper escape hatches when JJ already covers it
109
+ latestChatResponse.ref.appendChild(JJHE.create('h2').setText('User').ref)
110
+ latestChatResponse.ref.appendChild(JJHE.create('div').setText(userPrompt).ref)
111
+ latestChatResponse.ref.appendChild(JJHE.create('h2').setText('Assistant').ref)
112
+ latestChatResponse.ref.appendChild(assistantMessage.ref)
113
+ ```
114
+
115
+ Default heuristics from the tutorial:
116
+
117
+ - Use `JJHE.tree` with a local `h` alias when creating multiple siblings or any nested subtree.
118
+ - Use `create()` for one-off elements; switch to `tree()` as soon as structure becomes non-trivial.
119
+ - Prefer `setChild()` or `setChildren()` when replacing content, not `.empty().addChild()`.
120
+ - Prefer `addChildMap()` or `setChildMap()` when rendering from arrays.
121
+ - Keep values wrapped. Reach for `.ref` only for native APIs JJ does not expose.
122
+ - Use JJ verb families consistently: `set*` replaces, `add*` appends, `pre*` prepends, `rm*` removes, `sw*` toggles.
123
+
57
124
  ## Document Queries
58
125
 
59
126
  Wrap `document` with `JJD.from(document)` before querying.
60
127
 
61
128
  ```typescript
62
- const doc = JJD.from(document)
63
- const app = doc.find('#app', true) // throws when absent
64
- const card = doc.find('.card') // null when absent
65
- const items = doc.findAll('.item') // always an array
129
+ const jjDoc = JJD.from(document)
130
+ const jjApp = jjDoc.find('#app', true) // throws when absent
131
+ const jjCard = jjDoc.find('.card') // null when absent
132
+ const jjItems = jjDoc.findAll('.item') // always an array
66
133
 
67
134
  // Inside a custom element's shadow root
68
- const btn = this.getShadow(true).find('#submit')
135
+ const jjBtn = this.getShadow(true).find('#submit')
69
136
  ```
70
137
 
138
+ Querying defaults from the tutorial:
139
+
140
+ - Start from a wrapped container like `JJD.from(document)` or a `JJSR` shadow root.
141
+ - Prefer `find(selector, true)` when absence is a bug; it fails earlier and more clearly than a later null access.
142
+ - Prefer narrower selectors that encode expectations, like `button#submit`, instead of broad lookups plus manual type checks.
143
+ - Use `findAll()` for arrays of wrappers and keep operating on wrappers instead of unwrapping to native elements.
144
+ - Do not use `.ref.querySelector(...)` or `.ref.querySelectorAll(...)` when `find()` or `findAll()` already covers the case.
145
+ - Use `.closest()` on wrappers for event delegation and ancestor lookup.
146
+ - Use `JJHE.fromId('submit-btn')` for direct ID lookup when you already know the target is an HTML element.
147
+
71
148
  ## Attributes, Classes, Styles
72
149
 
73
150
  ```typescript
74
151
  // Attribute — singular
75
- el.setAttr('role', 'button')
76
- el.getAttr('role')
77
- el.rmAttr('hidden')
78
- el.swAttr('disabled', !isReady) // sets disabled="" or removes it
152
+ jjEl.setAttr('role', 'button')
153
+ jjEl.getAttr('role')
154
+ jjEl.rmAttr('hidden')
155
+ jjEl.swAttr('readonly') // auto: flips current state of the "readonly" attribute
156
+ jjEl.swAttr('disabled', !isReady) // sets disabled="" or removes it
79
157
 
80
158
  // Attribute — batch (null/undefined skipped)
81
- el.setAttrs({ type: 'text', placeholder: 'Search…' })
159
+ jjEl.setAttrs({ type: 'text', placeholder: 'Search…' })
82
160
 
83
161
  // Classes
84
- el.addClass('active')
85
- el.addClasses(['chip', 'selected'])
86
- el.rmClass('disabled')
87
- el.rmClasses(['pending', 'loading'])
88
- el.swClass('expanded', isExpanded) // explicit: adds when truthy, removes when falsy
89
- el.swClass('is-active') // auto: flips current state (adds if absent, removes if present)
90
- el.swAttr('disabled', !isReady) // explicit: sets disabled="" or removes it
91
- el.swAttr('readonly') // auto: flips current state
92
- el.setClasses({ active: isActive, disabled: !isReady })
93
- el.setClass('card card--featured') // replaces entire className
162
+ jjEl.addClass('active')
163
+ // Multiple classes via varargs
164
+ jjEl.addClass('active', 'selected')
165
+ jjEl.rmClass('active', 'loading')
166
+ // Multiple classes via array
167
+ jjEl.addClasses(['chip', 'selected'])
168
+ jjEl.rmClasses(['pending', 'loading'])
169
+ // Explicit mode: truthy adds, falsy removes
170
+ jjEl.swClass('expanded', isExpanded)
171
+ // Auto mode: flips current state
172
+ jjEl.swClass('is-active')
173
+
174
+ // Batch conditional class updates
175
+ jjEl.setClasses({ active: isActive, disabled: !isReady })
176
+ // Replace the entire className
177
+ jjEl.setClass('card card--featured')
94
178
 
95
179
  // Dataset
96
- el.getDataAttr('userId')
97
- el.hasDataAttr('userId')
98
- el.setDataAttr('userId', '42')
99
- el.setDataAttrs({ role: 'admin', team: 'ui' })
100
- el.rmDataAttr('userId')
101
- el.rmDataAttrs(['role', 'team'])
180
+ jjEl.getDataAttr('userId')
181
+ jjEl.hasDataAttr('userId')
182
+ jjEl.setDataAttr('userId', '42')
183
+ jjEl.setDataAttrs({ role: 'admin', team: 'ui' }) // batch set
184
+ jjEl.rmDataAttr('userId')
185
+ jjEl.rmDataAttr('role', 'team') // batch remove, varargs syntax
186
+ jjEl.rmDataAttrs(['role', 'team']) // batch remove, array syntax
102
187
 
103
188
  // ARIA
104
- el.getAriaAttr('hidden')
105
- el.hasAriaAttr('hidden')
106
- el.setAriaAttr('hidden', 'true')
107
- el.setAriaAttrs({ label: 'Dialog', modal: 'true' })
108
- el.rmAriaAttr('hidden')
189
+ jjEl.getAriaAttr('hidden')
190
+ jjEl.hasAriaAttr('hidden')
191
+ jjEl.setAriaAttr('hidden', 'true')
192
+ jjEl.setAriaAttrs({ label: 'Dialog', modal: 'true' })
193
+ jjEl.rmAriaAttr('hidden')
109
194
 
110
195
  // ARIA is not presence-based like HTML boolean attributes
111
196
  // Use explicit string states instead of swAttr()
112
- el.setAriaAttr('disabled', 'true')
197
+ jjEl.setAriaAttr('disabled', 'true')
113
198
 
114
199
  // Inline styles
115
- el.setStyle('color', 'var(--color-brand)')
116
- el.setStyles({ color: 'red', padding: '8px', border: null })
117
- el.rmStyle('color', 'padding')
200
+ jjEl.setStyle('color', 'var(--color-brand)')
201
+ jjEl.setStyles({ color: 'red', padding: '8px', border: null })
202
+ jjEl.rmStyle('color', 'padding')
203
+
204
+ // Value helpers (prefer over .ref.value)
205
+ jjEl.getValue()
206
+ jjEl.setValue('next')
118
207
  ```
119
208
 
209
+ Use `.ref.value` only when a JJ value helper is unavailable for your exact use case.
210
+
120
211
  ## Security — HTML Writes
121
212
 
122
213
  Prefer `.setText()` for any user-supplied content. `.setHTML()` requires an explicit `true` flag when the string is non-empty.
123
214
 
124
215
  ```typescript
125
- el.setText(userInput) // ✅ always safe
126
- el.setHTML('<p>Trusted markup</p>', true) // ✅ explicit opt-in
127
- el.setHTML('') // ✅ clearing is allowed without flag
128
- el.setHTML('<p>content</p>') // ❌ THROWS — missing unsafe flag
129
- el.ref.innerHTML = '…' // ❌ bypasses guard — avoid
216
+ jjEl.setText(userInput) // ✅ always safe
217
+ jjEl.setHTML('<p>Trusted markup</p>', true) // ✅ explicit opt-in
218
+ jjEl.setHTML('') // ✅ clearing is allowed without flag
219
+ jjEl.setHTML('<p>content</p>') // ❌ THROWS — missing unsafe flag
220
+ jjEl.ref.innerHTML = '…' // ❌ bypasses guard — avoid
130
221
  ```
131
222
 
132
223
  ## Events
133
224
 
134
225
  ```typescript
135
226
  // Native events
136
- el.on('click', handler)
137
- el.off('click', handler)
138
- el.trigger('click')
227
+ jjEl.on('click', handler)
228
+ jjEl.off('click', handler)
229
+ jjEl.triggerEvent('click')
139
230
 
140
231
  // Explicit event objects (equivalent to JJ helpers below)
141
- el.trigger(new Event('click', { bubbles: true, composed: true }))
142
- el.trigger(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true }))
232
+ jjEl.trigger(new Event('click', { bubbles: true, composed: true }))
233
+ jjEl.trigger(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true }))
143
234
 
144
235
  // Custom events — JJ defaults: bubbles: true, composed: true
145
236
  this.dispatchEvent(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true }))
146
237
 
147
238
  // Fluent dispatch (same defaults)
148
- el.triggerEvent('click') // equivalent to trigger(new Event('click', { bubbles: true, composed: true }))
239
+ jjEl.triggerEvent('click') // equivalent to trigger(new Event('click', { bubbles: true, composed: true }))
149
240
  JJHE.from(this).triggerCustomEvent('todo-toggle', { id: 1, done: true })
150
241
  // equivalent to trigger(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true }))
151
242
 
@@ -153,10 +244,41 @@ JJHE.from(this).triggerCustomEvent('todo-toggle', { id: 1, done: true })
153
244
  new CustomEvent('panel-ready', { bubbles: false, composed: false })
154
245
  ```
155
246
 
247
+ Event defaults from the tutorial:
248
+
249
+ - Prefer `.on()` and `.off()` on wrappers over native `addEventListener`/`removeEventListener` when already working with JJ values.
250
+ - Prefer `.triggerEvent()` and `.triggerCustomEvent()` for common JJ event dispatch; they default to `bubbles: true` and `composed: true`.
251
+ - Use `triggerCustomEvent(name, detail)` for component-to-parent communication instead of ad hoc callback plumbing.
252
+ - Use `bubbles: false` and `composed: false` only for intentionally internal events.
253
+ - Keep event code close to the wrapper it affects so later DOM updates stay targeted and local.
254
+
255
+ Guide defaults for event-heavy UI:
256
+
257
+ - Prefer event delegation on a common parent for repeated child actions instead of binding one listener per item.
258
+ - Use `.closest()` to recover the intended delegated target from `event.target`.
259
+ - When you need JJ's wrapper-bound `this` inside a listener, use `function` syntax, not an arrow.
260
+ - Native UI events like `click`, `input`, and `change` already cross shadow boundaries; custom events do not unless `composed: true`.
261
+
262
+ ```typescript
263
+ list.on('click', function (event) {
264
+ const jjItem = JJHE.from(event.target as Node).closest('[data-item-id]')
265
+ if (!jjItem) return
266
+ this.addClass('handled')
267
+ jjItem.addClass('active')
268
+ })
269
+ ```
270
+
156
271
  ## Custom Elements — Complete Pattern
157
272
 
158
273
  Fetch template and style at **module scope** — loaded once, shared across all instances.
159
274
 
275
+ Guide defaults for component shape:
276
+
277
+ - 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.
278
+ - Prefer `'open'` shadow mode unless stricter encapsulation is a hard requirement.
279
+ - `attributeChangedCallback()` can run before `connectedCallback()` for parsed attributes, so setters and render paths must tolerate pre-mount state.
280
+ - 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.
281
+
160
282
  ```typescript
161
283
  import { attr2prop, defineComponent, fetchStyle, fetchTemplate, JJHE } from 'jj'
162
284
 
@@ -169,12 +291,12 @@ export class MyCard extends HTMLElement {
169
291
 
170
292
  #userName = ''
171
293
  #count = 0
172
- #root = null // JJSR wrapper; attached in constructor
294
+ #jjShadow = null // JJSR wrapper; attached in constructor
173
295
  #isInitialized = false
174
296
 
175
297
  constructor() {
176
298
  super()
177
- this.#root = JJHE.from(this).setShadow('open').getShadow(true)
299
+ this.#jjShadow = JJHE.from(this).setShadow('open').getShadow(true)
178
300
  }
179
301
 
180
302
  attributeChangedCallback(name, oldValue, newValue) {
@@ -200,16 +322,16 @@ export class MyCard extends HTMLElement {
200
322
 
201
323
  async connectedCallback() {
202
324
  if (!this.#isInitialized) {
203
- this.#root.init(await templatePromise, await stylePromise)
325
+ this.#jjShadow.init(await templatePromise, await stylePromise)
204
326
  this.#isInitialized = true
205
327
  }
206
328
  this.#render()
207
329
  }
208
330
 
209
331
  #render() {
210
- if (!this.#root) return // guard for attribute changes before mount
211
- this.#root.find('[data-role="name"]')?.setText(this.#userName)
212
- this.#root.find('[data-role="count"]')?.setText(String(this.#count))
332
+ if (!this.#jjShadow) return // guard for attribute changes before mount
333
+ this.#jjShadow.find('[data-role="name"]')?.setText(this.#userName)
334
+ this.#jjShadow.find('[data-role="count"]')?.setText(String(this.#count))
213
335
  }
214
336
  }
215
337
 
@@ -219,6 +341,28 @@ await MyCard.defined
219
341
  await Promise.all([MyCard.defined, OtherCard.defined])
220
342
  ```
221
343
 
344
+ Template defaults from the tutorial:
345
+
346
+ - Prefer fetched `.html` templates for large static markup.
347
+ - Prefer `<template>` elements for reusable DOM snippets already present in the page.
348
+ - Prefer `JJHE.tree()` or `JJHE.create()` when you need live wrapper references for later updates.
349
+ - Keep template promises at module scope; for lazy loading, initialize them inside `connectedCallback()` with an `if (!templatePromise)` guard.
350
+ - Use one stable wrapper per component: `#jjHost` with `JJHE.from(this)` for light DOM, or `#jjShadow` with `JJHE.from(this).setShadow(...).getShadow(true)` for shadow DOM.
351
+ - Initialize template content once, then update specific nodes with `find(...).setText(...)` or other targeted wrapper operations.
352
+
353
+ Guide defaults for attributes and queries:
354
+
355
+ - Always coerce attribute-backed values in setters because HTML attributes arrive as strings.
356
+ - Query inside shadow DOM from the `JJSR` wrapper, never from `document`.
357
+ - Use specific selectors like `button#submit` or `[data-role="title"]` so the selector carries intent.
358
+
359
+ State defaults from the tutorial:
360
+
361
+ - Prefer plain objects or classes for state and update the exact affected wrappers in event handlers or setters.
362
+ - Prefer targeted updates like `value.setText(String(state.count))` over rebuilding an entire subtree for a small change.
363
+ - Use getters/setters or small helper methods when they make state transitions clearer, not because JJ requires a framework-style abstraction.
364
+ - Reach for external state libraries only when the application actually needs cross-cutting coordination beyond local JS state.
365
+
222
366
  `defineComponent()` returns `Promise<boolean>`:
223
367
 
224
368
  - `false` — newly defined by this call
@@ -244,29 +388,36 @@ const card = h(
244
388
 
245
389
  ```typescript
246
390
  // Clear children — internally uses replaceChildren()
247
- el.empty()
391
+ jjEl.empty()
248
392
 
249
393
  // Replace all children in one call (prefer over .empty().addChild())
250
- el.setChild(newChild)
251
- el.setChildren([childA, childB])
252
- el.setChildMap(items, (item) => JJHE.tree('li', null, item.label))
253
- el.setTemplate(templateElement)
394
+ jjEl.setChild(newChild)
395
+ jjEl.setChildren([childA, childB])
396
+ jjEl.setChildMap(items, (item) => JJHE.tree('li', null, item.label))
397
+ jjEl.setTemplate(templateElement)
254
398
 
255
399
  // Append
256
- el.addChild(child)
257
- el.addChildMap(items, (item) => JJHE.tree('li', null, item.label))
258
- el.addTemplate(await templatePromise) // clones before appending
400
+ jjEl.addChild(child)
401
+ jjEl.addChildMap(items, (item) => JJHE.tree('li', null, item.label))
402
+ jjEl.addTemplate(await templatePromise) // clones before appending
259
403
  ```
260
404
 
261
405
  `addChild` / `preChild` / `setChild` and map variants ignore `null`/`undefined`; all other non-node values are coerced to Text nodes.
262
406
 
407
+ Guide defaults for template and fragment usage:
408
+
409
+ - `addTemplate()` and `setTemplate()` always clone the input before appending; reuse the same template value safely.
410
+ - Prefer `setTemplate()` over `empty().addTemplate()` when replacing all content.
411
+ - Prefer `addChildMap()` or `setChildMap()` over manually building a fragment when rendering arrays.
412
+ - Use `JJDF.create()` when you need to assemble multiple sibling nodes before one insertion.
413
+
263
414
  ## Node Traversal
264
415
 
265
416
  ```typescript
266
- const parent = el.getParent() // wrapped parent or null (detached)
267
- const children = el.getChildren() // wrapped child array (always an array)
268
- el.rm() // detach from parent (no-op if already detached)
269
- const ancestor = el.closest('[data-section]') // null if not found
417
+ const parent = jjEl.getParent() // wrapped parent or null (detached)
418
+ const children = jjEl.getChildren() // wrapped child array (always an array)
419
+ jjEl.rm() // detach from parent (no-op if already detached)
420
+ const ancestor = jjEl.closest('[data-section]') // null if not found
270
421
  ```
271
422
 
272
423
  ## Resource Loaders
@@ -299,6 +450,12 @@ document.adoptedStyleSheets = [sheet]
299
450
  const fragment = await fetchTemplate(import.meta.resolve('./dialog.html'))
300
451
  ```
301
452
 
453
+ Guide defaults for browser-native loading hints:
454
+
455
+ - Use native `<link>` hints built with `JJHE.tree` and appended to `head` for `preload`, `prefetch`, and `modulepreload`.
456
+ - Keep the `as` value explicit for `preload` instead of inferring it from file extensions.
457
+ - Use `preload` for current-page needs, `prefetch` for probable future navigation, and `modulepreload` for module graphs you want fetched early.
458
+
302
459
  ## String Casing
303
460
 
304
461
  String case-conversion helpers are internal implementation details.
@@ -308,10 +465,11 @@ Use higher-level public APIs like `attr2prop` and `defineComponent` instead of i
308
465
 
309
466
  1. **`.ts` extension in imports** — TypeScript source must use `.js` (`import { X } from './X.js'`).
310
467
  2. **`JJHE.create('svg')`** — throws; use `JJSE.create('svg')`.
311
- 3. **`el.setHTML(html)` without `true`** — throws when html is non-empty.
468
+ 3. **`jjEl.setHTML(html)` without `true`** — throws when html is non-empty.
312
469
  4. **Fetching template/style inside `connectedCallback`** — fetch at module scope so the network request is shared.
313
470
  5. **Not awaiting `Element.defined`** — markup may be parsed before the element is defined, causing flaky upgrades.
314
471
  6. **Breaking the chain with `.ref` unnecessarily** — use wrapper methods first; reach for `.ref` only when no wrapper method exists.
472
+ 7. **Using `.ref.value` for common value updates** — prefer `getValue()` / `setValue(...)` for wrapper-level reads/writes.
315
473
 
316
474
  ## Pitfall Prevention Rules (Component Work)
317
475
 
@@ -62,15 +62,16 @@ button {
62
62
  }
63
63
  ```
64
64
 
65
- ## Loading stylesheets
65
+ ## Loading css files
66
66
 
67
67
  ```js
68
- import { JJHE, fetchStyle } from 'jj'
68
+ import { JJD, JJHE, fetchStyle } from 'jj'
69
69
 
70
70
  const h = JJHE.tree
71
+ const jjDoc = JJD.from(document)
71
72
 
72
73
  // Hint browser to start loading early
73
- document.head.addChild(
74
+ jjDoc.find('head', true).addChild(
74
75
  h('link', {
75
76
  href: import.meta.resolve('./theme.css'),
76
77
  rel: 'preload',
@@ -3,15 +3,15 @@
3
3
  ## Native event listeners
4
4
 
5
5
  ```js
6
- el.on('click', handler) // addEventListener
7
- el.off('click', handler) // removeEventListener (same function reference)
8
- el.trigger('click') // dispatchEvent(new Event('click'))
6
+ jjEl.on('click', handler) // addEventListener
7
+ jjEl.off('click', handler) // removeEventListener (same function reference)
8
+ jjEl.triggerEvent('click') // creates and dispatches Event('click')
9
9
  ```
10
10
 
11
11
  The handler `this` inside `.on()` is bound to the JJ wrapper instance. Use `this.ref` to access the native element:
12
12
 
13
13
  ```js
14
- btn.on('click', function () {
14
+ jjBtn.on('click', function () {
15
15
  // this → the JJHE wrapper
16
16
  // this.ref → the HTMLButtonElement
17
17
  console.log(this.ref.textContent)
@@ -54,10 +54,16 @@ new CustomEvent('internal-ready', { bubbles: false, composed: false })
54
54
  ## Event delegation
55
55
 
56
56
  ```js
57
- const list = doc.find('#list', true)
58
- list.on('click', (e) => {
59
- const item = e.target.closest('[data-id]')
60
- if (item) handleClick(item.dataset.id)
57
+ import { JJD } from 'jj'
58
+
59
+ const jjDoc = JJD.from(document)
60
+ const jjList = jjDoc.find('#list', true)
61
+ jjList.on('click', (e) => {
62
+ if (!(e.target instanceof Element)) {
63
+ return
64
+ }
65
+ const itemEl = e.target.closest('[data-id]')
66
+ if (itemEl) handleClick(itemEl.dataset.id)
61
67
  })
62
68
  ```
63
69
 
@@ -65,7 +71,7 @@ list.on('click', (e) => {
65
71
 
66
72
  ```js
67
73
  const ctrl = new AbortController()
68
- el.ref.addEventListener('click', handler, { signal: ctrl.signal })
74
+ jjEl.ref.addEventListener('click', handler, { signal: ctrl.signal })
69
75
  // later:
70
76
  ctrl.abort() // removes listener
71
77
  ```
@@ -7,8 +7,8 @@ jQuery provides chainable selection and DOM operations. JJ is similar in API fee
7
7
  | jQuery | JJ equivalent |
8
8
  | ------------------------------ | --------------------------------------------------------------------------------- |
9
9
  | `$('#id')` | `JJD.from(document).find('#id')` |
10
- | `$('.cls')` (first match) | `doc.find('.cls')` |
11
- | `$('.cls')` (all matches) | `doc.findAll('.cls')` |
10
+ | `$('.cls')` (first match) | `jjDoc.find('.cls')` |
11
+ | `$('.cls')` (all matches) | `jjDoc.findAll('.cls')` |
12
12
  | `.addClass()` / `.removeClass` | `.addClass()` / `.rmClass()` |
13
13
  | `.swClass(cls, bool?)` | `.swClass(cls, force?)` — explicit when force is provided, auto-flip when omitted |
14
14
  | `.attr(name, val)` (write) | `.setAttr(name, val)` |
@@ -36,8 +36,8 @@ $('.card').addClass('active').css('color', 'red')
36
36
  JJ:
37
37
 
38
38
  ```js
39
- const doc = JJD.from(document)
40
- doc.findAll('.card').forEach((card) => card.addClass('active').setStyle('color', 'red'))
39
+ const jjDoc = JJD.from(document)
40
+ jjDoc.findAll('.card').forEach((jjCard) => jjCard.addClass('active').setStyle('color', 'red'))
41
41
  ```
42
42
 
43
43
  ## HTML write safety
@@ -51,8 +51,8 @@ $('#msg').html(userInput) // XSS risk
51
51
  JJ requires explicit acknowledgement:
52
52
 
53
53
  ```js
54
- doc.find('#msg').setHTML(trustedMarkup, true) // must pass true
55
- doc.find('#msg').setText(userInput) // safe for user content
54
+ jjDoc.find('#msg').setHTML(trustedMarkup, true) // must pass true
55
+ jjDoc.find('#msg').setText(userInput) // safe for user content
56
56
  ```
57
57
 
58
58
  ## Event delegation
@@ -66,7 +66,10 @@ $(document).on('click', '.item', handler)
66
66
  JJ — use native event delegation via `matches`:
67
67
 
68
68
  ```js
69
- doc.on('click', (e) => {
69
+ jjDoc.on('click', (e) => {
70
+ if (!(e.target instanceof Element)) {
71
+ return
72
+ }
70
73
  if (e.target.matches('.item')) handler(e)
71
74
  })
72
75
  ```
@@ -13,7 +13,7 @@ Lit adds reactive properties, template literals, and scoped CSS on top of native
13
13
  | `html\`<div>\`` | `JJHE.tree('div', …)` or `JJHE.create('div')` |
14
14
  | `@event="${handler}"` | `.on('event', handler)` in `connectedCallback` |
15
15
  | `updated()` hook | Call `#render()` from setters that change visible state |
16
- | `this.renderRoot.query…` | `this.#root.find(selector)` |
16
+ | `this.renderRoot.query…` | `this.#jjShadow.find(selector)` |
17
17
 
18
18
  ## Component example
19
19
 
@@ -46,12 +46,12 @@ export class MyCounter extends HTMLElement {
46
46
  static defined = defineComponent('my-counter', MyCounter)
47
47
 
48
48
  #count = 0
49
- #root = null
49
+ #jjShadow = null
50
50
  #isInitialized = false
51
51
 
52
52
  constructor() {
53
53
  super()
54
- this.#root = JJHE.from(this).setShadow('open').getShadow(true)
54
+ this.#jjShadow = JJHE.from(this).setShadow('open').getShadow(true)
55
55
  }
56
56
 
57
57
  attributeChangedCallback(name, oldValue, newValue) {
@@ -67,7 +67,7 @@ export class MyCounter extends HTMLElement {
67
67
 
68
68
  async connectedCallback() {
69
69
  if (!this.#isInitialized) {
70
- this.#root
70
+ this.#jjShadow
71
71
  .init('<button id="btn">0</button>', await stylePromise)
72
72
  .find('#btn', true)
73
73
  .on('click', () => {
@@ -79,7 +79,7 @@ export class MyCounter extends HTMLElement {
79
79
  }
80
80
 
81
81
  #render() {
82
- this.#root?.find('#btn')?.setText(String(this.#count))
82
+ this.#jjShadow?.find('#btn')?.setText(String(this.#count))
83
83
  }
84
84
  }
85
85
  ```
@@ -5,16 +5,16 @@
5
5
  Always start from a wrapped container — most commonly the document or a shadow root:
6
6
 
7
7
  ```js
8
- const doc = JJD.from(document)
9
- const shadow = this.#root // JJSR inside a custom element
8
+ const jjDoc = JJD.from(document)
9
+ const jjShadow = this.#jjShadow // JJSR inside a custom element
10
10
  ```
11
11
 
12
12
  ## find — first match
13
13
 
14
14
  ```js
15
- const card = doc.find('.card') // null when absent
16
- const app = doc.find('#app', true) // throws TypeError when absent
17
- const btn = shadow.find('#submit') // scoped to shadow root
15
+ const jjCard = jjDoc.find('.card') // null when absent
16
+ const jjApp = jjDoc.find('#app', true) // throws ReferenceError when absent
17
+ const jjBtn = jjShadow.find('#submit') // scoped to shadow root
18
18
  ```
19
19
 
20
20
  Pass `true` as the second argument when the element is required. This produces a clearer error than a null-access crash later.
@@ -22,9 +22,9 @@ Pass `true` as the second argument when the element is required. This produces a
22
22
  You can use it in combination with a more specific query to simultaneously assert your expectations. For example, if you want a reference to a button with the id `submit`, you could write:
23
23
 
24
24
  ```js
25
- const submitBtn = doc.find('#submit-btn', true)
25
+ const jjSubmitBtn = jjDoc.find('#submit-btn', true)
26
26
 
27
- if (!(submitBtn instanceof HTMLButtonElement)) {
27
+ if (!(jjSubmitBtn.ref instanceof HTMLButtonElement)) {
28
28
  throw new Error('Expected #submit-btn to be an HTMLButtonElement.')
29
29
  }
30
30
  ```
@@ -34,7 +34,7 @@ But a shorter and more expressive way to write this is:
34
34
  ```js
35
35
  // This will throw an exception if an element with id is not found
36
36
  // OR if it's found but not a button
37
- const submitBtn = doc.find('button#submit-btn', true)
37
+ const jjSubmitBtn = jjDoc.find('button#submit-btn', true)
38
38
  ```
39
39
 
40
40
  This helps catch errors early and narrow down troubleshooting.
@@ -43,19 +43,19 @@ When `find` returns a wrapper, keep the wrapper unless you need a native API tha
43
43
 
44
44
  ```js
45
45
  // ✅ keep wrapper value
46
- const jjSubmitBtn = doc.find('button#submit-btn', true)
46
+ const jjSubmitBtn = jjDoc.find('button#submit-btn', true)
47
47
  jjSubmitBtn.on('click', onSubmit)
48
48
 
49
49
  // ❌ avoid unwrap + re-wrap noise
50
- const submitRef = doc.find('button#submit-btn', true).ref
50
+ const submitRef = jjDoc.find('button#submit-btn', true).ref
51
51
  const jjSubmitBtnAgain = JJHE.from(submitRef)
52
52
  ```
53
53
 
54
54
  ## findAll — all matches
55
55
 
56
56
  ```js
57
- const items = doc.findAll('li.item') // always an array (may be empty)
58
- items.forEach((item) => item.addClass('loaded'))
57
+ const jjItems = jjDoc.findAll('li.item') // always an array (may be empty)
58
+ jjItems.forEach((jjItem) => jjItem.addClass('loaded'))
59
59
  ```
60
60
 
61
61
  ## closest — ancestor lookup
@@ -63,16 +63,19 @@ items.forEach((item) => item.addClass('loaded'))
63
63
  Use `.closest()` on element wrappers for event delegation or tree navigation:
64
64
 
65
65
  ```js
66
- doc.on('click', (e) => {
67
- const item = JJHE.from(e.target).closest('[data-item-id]')
68
- if (item) handleItemClick(item.getAttr('data-item-id'))
66
+ jjDoc.on('click', (e) => {
67
+ if (!(e.target instanceof Node)) {
68
+ return
69
+ }
70
+ const jjItem = JJHE.from(e.target).closest('[data-item-id]')
71
+ if (jjItem) handleItemClick(jjItem.getAttr('data-item-id'))
69
72
  })
70
73
  ```
71
74
 
72
75
  ## fromId — direct ID lookup
73
76
 
74
77
  ```js
75
- const btn = JJHE.fromId('submit-btn') // typed as JJHE<HTMLButtonElement>
78
+ const jjBtn = JJHE.fromId('submit-btn') // typed as JJHE<HTMLButtonElement>
76
79
  ```
77
80
 
78
81
  ## When to use .ref for queries
@@ -31,12 +31,12 @@ JJ:
31
31
  import { JJHE } from 'jj'
32
32
 
33
33
  let count = 0
34
- const btn = JJHE.create('button').setText('0')
35
- btn.on('click', () => {
34
+ const jjBtn = JJHE.create('button').setText('0')
35
+ jjBtn.on('click', () => {
36
36
  count++
37
- btn.setText(String(count))
37
+ jjBtn.setText(String(count))
38
38
  })
39
- document.body.appendChild(btn.ref)
39
+ document.body.appendChild(jjBtn.ref)
40
40
  ```
41
41
 
42
42
  ## Component props → observed attributes
@@ -73,7 +73,7 @@ JJ — child dispatches, parent listens:
73
73
  JJHE.from(this).triggerCustomEvent('todo-toggle', { id: this.#id })
74
74
 
75
75
  // parent
76
- container.on('todo-toggle', (e) => handleToggle(e.detail.id))
76
+ jjContainer.on('todo-toggle', (e) => handleToggle(e.detail.id))
77
77
  ```
78
78
 
79
79
  ## Browser references
@@ -32,9 +32,9 @@ JJ:
32
32
  import { JJHE } from 'jj'
33
33
 
34
34
  let count = 0
35
- const btn = JJHE.create('button').setText('0')
36
- btn.on('click', () => btn.setText(String(++count)))
37
- document.body.appendChild(btn.ref)
35
+ const jjBtn = JJHE.create('button').setText('0')
36
+ jjBtn.on('click', () => jjBtn.setText(String(++count)))
37
+ document.body.appendChild(jjBtn.ref)
38
38
  ```
39
39
 
40
40
  ## Outbound events
@@ -33,9 +33,9 @@ import { JJHE } from '../src/index.js'
33
33
  describe('JJHE', () => {
34
34
  describe('static create()', () => {
35
35
  it('creates element from tag name', () => {
36
- const div = JJHE.create('div')
37
- assert.ok(div instanceof JJHE)
38
- assert.strictEqual(div.ref.tagName, 'DIV')
36
+ const jjDiv = JJHE.create('div')
37
+ assert.ok(jjDiv instanceof JJHE)
38
+ assert.strictEqual(jjDiv.ref.tagName, 'DIV')
39
39
  })
40
40
 
41
41
  it('throws TypeError for non-string tagName', () => {
@@ -68,12 +68,12 @@ it('renders title in shadow', async () => {
68
68
 
69
69
  ```js
70
70
  it('dispatches todo-toggle with detail', () => {
71
- const el = JJHE.create('div')
71
+ const jjEl = JJHE.create('div')
72
72
  let captured = null
73
- el.on('todo-toggle', (e) => {
73
+ jjEl.on('todo-toggle', (e) => {
74
74
  captured = e.detail
75
75
  })
76
- el.triggerCustomEvent('todo-toggle', { id: 1, done: true })
76
+ jjEl.triggerCustomEvent('todo-toggle', { id: 1, done: true })
77
77
  assert.deepStrictEqual(captured, { id: 1, done: true })
78
78
  })
79
79
  ```
@@ -30,12 +30,12 @@ JJ:
30
30
  import { JJHE } from 'jj'
31
31
 
32
32
  let message = ''
33
- const p = JJHE.create('p').setText('')
34
- const input = JJHE.create('input')
33
+ const jjMessage = JJHE.create('p').setText('')
34
+ const jjInput = JJHE.create('input')
35
35
  .setAttr('type', 'text')
36
- .on('input', (e) => {
37
- message = e.target.value
38
- p.setText(message)
36
+ .on('input', () => {
37
+ message = jjInput.getValue()
38
+ jjMessage.setText(message)
39
39
  })
40
40
  ```
41
41
 
@@ -62,7 +62,7 @@ set count(v) {
62
62
  ## v-for replacement
63
63
 
64
64
  ```js
65
- list.addChildMap(items, (item) => JJHE.tree('li', { class: 'item' }, item.label))
65
+ jjList.addChildMap(items, (item) => JJHE.tree('li', { class: 'item' }, item.label))
66
66
  ```
67
67
 
68
68
  ## Browser references
@@ -37,10 +37,16 @@ use `defineComponent()` which also calls `customElements.whenDefined()`.
37
37
  const templatePromise = fetchTemplate(import.meta.resolve('./my-component.html'))
38
38
  const stylePromise = fetchStyle(import.meta.resolve('./my-component.css'))
39
39
 
40
+ constructor() {
41
+ super()
42
+ this.#jjHost = JJHE.from(this).setShadow('open')
43
+ this.#jjShadow = this.#jjHost.getShadow(true)
44
+ }
45
+
40
46
  async connectedCallback() {
41
- // setShadow(mode) in the constructor, then initShadow(template, ...styles) here
42
- JJHE.from(this).initShadow(await templatePromise, await stylePromise)
43
- this.#root.find('#btn').on('click', this.#handleClick)
47
+ // setShadow(mode) in the constructor, then initialize once here
48
+ this.#jjHost.initShadow(await templatePromise, await stylePromise)
49
+ this.#jjShadow.find('#btn', true).on('click', this.#handleClick)
44
50
  this.#render()
45
51
  }
46
52
  ```
@@ -63,8 +69,8 @@ attributeChangedCallback(name, oldValue, newValue) {
63
69
  // attr2prop fires attributeChangedCallback BEFORE connectedCallback on initial parse.
64
70
  // Guard renders until shadow is ready:
65
71
  #render() {
66
- if (!this.#root) return
67
- this.#root.find('[data-role="name"]')?.setText(this.#userName)
72
+ if (!this.#jjShadow) return
73
+ this.#jjShadow.find('[data-role="name"]')?.setText(this.#userName)
68
74
  }
69
75
  ```
70
76
 
@@ -74,8 +80,8 @@ Skip `setShadow` and update children directly when page-level styling should app
74
80
 
75
81
  ```js
76
82
  async connectedCallback() {
77
- this.#root = JJHE.from(this)
78
- this.#root.setTemplate(await templatePromise)
83
+ this.#jjHost = JJHE.from(this)
84
+ this.#jjHost.setTemplate(await templatePromise)
79
85
  this.#render()
80
86
  }
81
87
  ```