jj 3.0.0-rc.6 → 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 +1 -1
- package/skills/SKILL.md +93 -77
- package/skills/references/css-improvements.md +4 -3
- package/skills/references/eventing-patterns.md +15 -9
- package/skills/references/jquery-to-jj-translation.md +10 -7
- package/skills/references/lit-to-jj-translation.md +5 -5
- package/skills/references/querying-patterns.md +19 -16
- package/skills/references/react-to-jj-translation.md +5 -5
- package/skills/references/svelte-to-jj-translation.md +3 -3
- package/skills/references/testing-with-jsdom.md +6 -6
- package/skills/references/vue-to-jj-translation.md +6 -6
- package/skills/references/web-components-patterns.md +13 -7
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -16,6 +16,7 @@ When converting native DOM code, framework code, or vague UI requests into JJ, d
|
|
|
16
16
|
- Use `JJHE.tree` with a local `h` alias for multi-node or nested UI.
|
|
17
17
|
- Use `setChild()`/`setChildren()` to replace content and `addChildMap()`/`setChildMap()` for array rendering.
|
|
18
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`.
|
|
19
20
|
- Use `setText()` for user content and treat `setHTML(..., true)` as a trusted-content escape hatch.
|
|
20
21
|
- For repeated child interactions, prefer one delegated listener on a stable parent over one listener per child.
|
|
21
22
|
- Choose shadow DOM for self-contained widgets and light DOM for page-level content that should inherit global styles.
|
|
@@ -64,12 +65,12 @@ Each JJ wrapper exposes the native node via `.ref`.
|
|
|
64
65
|
|
|
65
66
|
```typescript
|
|
66
67
|
// ✅ CORRECT — factory methods infer the precise generic type
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
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>
|
|
73
74
|
|
|
74
75
|
// ❌ WRONG
|
|
75
76
|
JJHE.create('svg') // throws — use JJSE.create('svg')
|
|
@@ -81,7 +82,7 @@ new JJHE(element) // don't call constructors directly
|
|
|
81
82
|
All mutating methods return `this`. Chain as much as possible; access `.ref` only when a wrapper method does not exist.
|
|
82
83
|
|
|
83
84
|
```typescript
|
|
84
|
-
const
|
|
85
|
+
const jjBtn = JJHE.create('button')
|
|
85
86
|
.addClass('btn', 'primary')
|
|
86
87
|
.setText('Save')
|
|
87
88
|
.setAttr('type', 'submit')
|
|
@@ -102,7 +103,7 @@ latestChatResponse.addChild(
|
|
|
102
103
|
)
|
|
103
104
|
|
|
104
105
|
// ✅ also fine for flat mapped children
|
|
105
|
-
const
|
|
106
|
+
const jjList = JJHE.create('ul').addChildMap(fruits, (fruit) => h('li', null, fruit))
|
|
106
107
|
|
|
107
108
|
// ❌ avoid native-style wrapper escape hatches when JJ already covers it
|
|
108
109
|
latestChatResponse.ref.appendChild(JJHE.create('h2').setText('User').ref)
|
|
@@ -125,13 +126,13 @@ Default heuristics from the tutorial:
|
|
|
125
126
|
Wrap `document` with `JJD.from(document)` before querying.
|
|
126
127
|
|
|
127
128
|
```typescript
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
const
|
|
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
|
|
132
133
|
|
|
133
134
|
// Inside a custom element's shadow root
|
|
134
|
-
const
|
|
135
|
+
const jjBtn = this.getShadow(true).find('#submit')
|
|
135
136
|
```
|
|
136
137
|
|
|
137
138
|
Querying defaults from the tutorial:
|
|
@@ -148,80 +149,94 @@ Querying defaults from the tutorial:
|
|
|
148
149
|
|
|
149
150
|
```typescript
|
|
150
151
|
// Attribute — singular
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
155
157
|
|
|
156
158
|
// Attribute — batch (null/undefined skipped)
|
|
157
|
-
|
|
159
|
+
jjEl.setAttrs({ type: 'text', placeholder: 'Search…' })
|
|
158
160
|
|
|
159
161
|
// Classes
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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')
|
|
170
178
|
|
|
171
179
|
// Dataset
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
178
187
|
|
|
179
188
|
// ARIA
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
189
|
+
jjEl.getAriaAttr('hidden')
|
|
190
|
+
jjEl.hasAriaAttr('hidden')
|
|
191
|
+
jjEl.setAriaAttr('hidden', 'true')
|
|
192
|
+
jjEl.setAriaAttrs({ label: 'Dialog', modal: 'true' })
|
|
193
|
+
jjEl.rmAriaAttr('hidden')
|
|
185
194
|
|
|
186
195
|
// ARIA is not presence-based like HTML boolean attributes
|
|
187
196
|
// Use explicit string states instead of swAttr()
|
|
188
|
-
|
|
197
|
+
jjEl.setAriaAttr('disabled', 'true')
|
|
189
198
|
|
|
190
199
|
// Inline styles
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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')
|
|
194
207
|
```
|
|
195
208
|
|
|
209
|
+
Use `.ref.value` only when a JJ value helper is unavailable for your exact use case.
|
|
210
|
+
|
|
196
211
|
## Security — HTML Writes
|
|
197
212
|
|
|
198
213
|
Prefer `.setText()` for any user-supplied content. `.setHTML()` requires an explicit `true` flag when the string is non-empty.
|
|
199
214
|
|
|
200
215
|
```typescript
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
206
221
|
```
|
|
207
222
|
|
|
208
223
|
## Events
|
|
209
224
|
|
|
210
225
|
```typescript
|
|
211
226
|
// Native events
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
227
|
+
jjEl.on('click', handler)
|
|
228
|
+
jjEl.off('click', handler)
|
|
229
|
+
jjEl.triggerEvent('click')
|
|
215
230
|
|
|
216
231
|
// Explicit event objects (equivalent to JJ helpers below)
|
|
217
|
-
|
|
218
|
-
|
|
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 }))
|
|
219
234
|
|
|
220
235
|
// Custom events — JJ defaults: bubbles: true, composed: true
|
|
221
236
|
this.dispatchEvent(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true }))
|
|
222
237
|
|
|
223
238
|
// Fluent dispatch (same defaults)
|
|
224
|
-
|
|
239
|
+
jjEl.triggerEvent('click') // equivalent to trigger(new Event('click', { bubbles: true, composed: true }))
|
|
225
240
|
JJHE.from(this).triggerCustomEvent('todo-toggle', { id: 1, done: true })
|
|
226
241
|
// equivalent to trigger(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true }))
|
|
227
242
|
|
|
@@ -246,10 +261,10 @@ Guide defaults for event-heavy UI:
|
|
|
246
261
|
|
|
247
262
|
```typescript
|
|
248
263
|
list.on('click', function (event) {
|
|
249
|
-
const
|
|
250
|
-
if (!
|
|
264
|
+
const jjItem = JJHE.from(event.target as Node).closest('[data-item-id]')
|
|
265
|
+
if (!jjItem) return
|
|
251
266
|
this.addClass('handled')
|
|
252
|
-
|
|
267
|
+
jjItem.addClass('active')
|
|
253
268
|
})
|
|
254
269
|
```
|
|
255
270
|
|
|
@@ -276,12 +291,12 @@ export class MyCard extends HTMLElement {
|
|
|
276
291
|
|
|
277
292
|
#userName = ''
|
|
278
293
|
#count = 0
|
|
279
|
-
#
|
|
294
|
+
#jjShadow = null // JJSR wrapper; attached in constructor
|
|
280
295
|
#isInitialized = false
|
|
281
296
|
|
|
282
297
|
constructor() {
|
|
283
298
|
super()
|
|
284
|
-
this.#
|
|
299
|
+
this.#jjShadow = JJHE.from(this).setShadow('open').getShadow(true)
|
|
285
300
|
}
|
|
286
301
|
|
|
287
302
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
@@ -307,16 +322,16 @@ export class MyCard extends HTMLElement {
|
|
|
307
322
|
|
|
308
323
|
async connectedCallback() {
|
|
309
324
|
if (!this.#isInitialized) {
|
|
310
|
-
this.#
|
|
325
|
+
this.#jjShadow.init(await templatePromise, await stylePromise)
|
|
311
326
|
this.#isInitialized = true
|
|
312
327
|
}
|
|
313
328
|
this.#render()
|
|
314
329
|
}
|
|
315
330
|
|
|
316
331
|
#render() {
|
|
317
|
-
if (!this.#
|
|
318
|
-
this.#
|
|
319
|
-
this.#
|
|
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))
|
|
320
335
|
}
|
|
321
336
|
}
|
|
322
337
|
|
|
@@ -332,7 +347,7 @@ Template defaults from the tutorial:
|
|
|
332
347
|
- Prefer `<template>` elements for reusable DOM snippets already present in the page.
|
|
333
348
|
- Prefer `JJHE.tree()` or `JJHE.create()` when you need live wrapper references for later updates.
|
|
334
349
|
- Keep template promises at module scope; for lazy loading, initialize them inside `connectedCallback()` with an `if (!templatePromise)` guard.
|
|
335
|
-
- Use one stable
|
|
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.
|
|
336
351
|
- Initialize template content once, then update specific nodes with `find(...).setText(...)` or other targeted wrapper operations.
|
|
337
352
|
|
|
338
353
|
Guide defaults for attributes and queries:
|
|
@@ -373,18 +388,18 @@ const card = h(
|
|
|
373
388
|
|
|
374
389
|
```typescript
|
|
375
390
|
// Clear children — internally uses replaceChildren()
|
|
376
|
-
|
|
391
|
+
jjEl.empty()
|
|
377
392
|
|
|
378
393
|
// Replace all children in one call (prefer over .empty().addChild())
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
394
|
+
jjEl.setChild(newChild)
|
|
395
|
+
jjEl.setChildren([childA, childB])
|
|
396
|
+
jjEl.setChildMap(items, (item) => JJHE.tree('li', null, item.label))
|
|
397
|
+
jjEl.setTemplate(templateElement)
|
|
383
398
|
|
|
384
399
|
// Append
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
400
|
+
jjEl.addChild(child)
|
|
401
|
+
jjEl.addChildMap(items, (item) => JJHE.tree('li', null, item.label))
|
|
402
|
+
jjEl.addTemplate(await templatePromise) // clones before appending
|
|
388
403
|
```
|
|
389
404
|
|
|
390
405
|
`addChild` / `preChild` / `setChild` and map variants ignore `null`/`undefined`; all other non-node values are coerced to Text nodes.
|
|
@@ -399,10 +414,10 @@ Guide defaults for template and fragment usage:
|
|
|
399
414
|
## Node Traversal
|
|
400
415
|
|
|
401
416
|
```typescript
|
|
402
|
-
const parent =
|
|
403
|
-
const children =
|
|
404
|
-
|
|
405
|
-
const ancestor =
|
|
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
|
|
406
421
|
```
|
|
407
422
|
|
|
408
423
|
## Resource Loaders
|
|
@@ -450,10 +465,11 @@ Use higher-level public APIs like `attr2prop` and `defineComponent` instead of i
|
|
|
450
465
|
|
|
451
466
|
1. **`.ts` extension in imports** — TypeScript source must use `.js` (`import { X } from './X.js'`).
|
|
452
467
|
2. **`JJHE.create('svg')`** — throws; use `JJSE.create('svg')`.
|
|
453
|
-
3. **`
|
|
468
|
+
3. **`jjEl.setHTML(html)` without `true`** — throws when html is non-empty.
|
|
454
469
|
4. **Fetching template/style inside `connectedCallback`** — fetch at module scope so the network request is shared.
|
|
455
470
|
5. **Not awaiting `Element.defined`** — markup may be parsed before the element is defined, causing flaky upgrades.
|
|
456
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.
|
|
457
473
|
|
|
458
474
|
## Pitfall Prevention Rules (Component Work)
|
|
459
475
|
|
|
@@ -62,15 +62,16 @@ button {
|
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
## Loading
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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) | `
|
|
11
|
-
| `$('.cls')` (all matches) | `
|
|
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
|
|
40
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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.#
|
|
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
|
-
#
|
|
49
|
+
#jjShadow = null
|
|
50
50
|
#isInitialized = false
|
|
51
51
|
|
|
52
52
|
constructor() {
|
|
53
53
|
super()
|
|
54
|
-
this.#
|
|
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.#
|
|
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.#
|
|
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
|
|
9
|
-
const
|
|
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
|
|
16
|
-
const
|
|
17
|
-
const
|
|
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
|
|
25
|
+
const jjSubmitBtn = jjDoc.find('#submit-btn', true)
|
|
26
26
|
|
|
27
|
-
if (!(
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
58
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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
|
|
35
|
-
|
|
34
|
+
const jjBtn = JJHE.create('button').setText('0')
|
|
35
|
+
jjBtn.on('click', () => {
|
|
36
36
|
count++
|
|
37
|
-
|
|
37
|
+
jjBtn.setText(String(count))
|
|
38
38
|
})
|
|
39
|
-
document.body.appendChild(
|
|
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
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
document.body.appendChild(
|
|
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
|
|
37
|
-
assert.ok(
|
|
38
|
-
assert.strictEqual(
|
|
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
|
|
71
|
+
const jjEl = JJHE.create('div')
|
|
72
72
|
let captured = null
|
|
73
|
-
|
|
73
|
+
jjEl.on('todo-toggle', (e) => {
|
|
74
74
|
captured = e.detail
|
|
75
75
|
})
|
|
76
|
-
|
|
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
|
|
34
|
-
const
|
|
33
|
+
const jjMessage = JJHE.create('p').setText('')
|
|
34
|
+
const jjInput = JJHE.create('input')
|
|
35
35
|
.setAttr('type', 'text')
|
|
36
|
-
.on('input', (
|
|
37
|
-
message =
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
this.#
|
|
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.#
|
|
67
|
-
this.#
|
|
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.#
|
|
78
|
-
this.#
|
|
83
|
+
this.#jjHost = JJHE.from(this)
|
|
84
|
+
this.#jjHost.setTemplate(await templatePromise)
|
|
79
85
|
this.#render()
|
|
80
86
|
}
|
|
81
87
|
```
|