jj 3.0.0-rc.4 → 3.0.0-rc.5
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 +2 -2
- package/skills/SKILL.md +345 -0
- package/skills/references/angular-to-jj-translation.md +76 -0
- package/skills/references/css-improvements.md +91 -0
- package/skills/references/error-handling-patterns.md +79 -0
- package/skills/references/eventing-patterns.md +78 -0
- package/skills/references/jquery-to-jj-translation.md +78 -0
- package/skills/references/lit-to-jj-translation.md +103 -0
- package/skills/references/querying-patterns.md +93 -0
- package/skills/references/react-to-jj-translation.md +83 -0
- package/skills/references/security-and-html.md +65 -0
- package/skills/references/svelte-to-jj-translation.md +70 -0
- package/skills/references/testing-with-jsdom.md +93 -0
- package/skills/references/vue-to-jj-translation.md +72 -0
- package/skills/references/web-components-patterns.md +95 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jj",
|
|
3
|
-
"version": "3.0.0-rc.
|
|
3
|
+
"version": "3.0.0-rc.5",
|
|
4
4
|
"description": "A minimal DOM manipulation library with web components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"javascript",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"lib",
|
|
31
|
-
"
|
|
31
|
+
"skills"
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
34
|
"fmt": "prettier --write .",
|
package/skills/SKILL.md
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jj
|
|
3
|
+
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# JJ DOM Library
|
|
7
|
+
|
|
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
|
+
|
|
10
|
+
## Wrapper Hierarchy
|
|
11
|
+
|
|
12
|
+
Each JJ wrapper exposes the native node via `.ref`.
|
|
13
|
+
|
|
14
|
+
| Class | Wraps | Key additions |
|
|
15
|
+
| ----- | ---------------- | ------------------------------------------------------ |
|
|
16
|
+
| JJET | EventTarget | `.on()`, `.off()`, `.trigger()`, `.run()` |
|
|
17
|
+
| JJN | Node | `.getParent()`, `.getChildren()`, `.rm()`, `.clone()` |
|
|
18
|
+
| JJD | Document | `.find()`, `.findAll()` |
|
|
19
|
+
| JJDF | DocumentFragment | `.addTemplate()`, `.setTemplate()`, batch child ops |
|
|
20
|
+
| JJE | Element | Attributes, classes, ARIA, visibility, HTML write |
|
|
21
|
+
| JJHE | HTMLElement | `.setText()`, `.setStyle()`, `.setShadow()`, `.tree()` |
|
|
22
|
+
| JJSE | SVGElement | SVG namespace factory |
|
|
23
|
+
| JJME | MathMLElement | MathML namespace factory |
|
|
24
|
+
| JJSR | ShadowRoot | `.find()`, `.findAll()`, `.addStyle()`, `.init()` |
|
|
25
|
+
| JJDF | DocumentFragment | Fragment operations |
|
|
26
|
+
| JJT | Text | `.getText()`, `.setText()` |
|
|
27
|
+
|
|
28
|
+
## Type-Safe Creation — Always Use Factory Methods
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// ✅ 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>
|
|
38
|
+
|
|
39
|
+
// ❌ WRONG
|
|
40
|
+
JJHE.create('svg') // throws — use JJSE.create('svg')
|
|
41
|
+
new JJHE(element) // don't call constructors directly
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Chaining
|
|
45
|
+
|
|
46
|
+
All mutating methods return `this`. Chain as much as possible; access `.ref` only when a wrapper method does not exist.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
const btn = JJHE.create('button')
|
|
50
|
+
.addClass('btn', 'primary')
|
|
51
|
+
.setText('Save')
|
|
52
|
+
.setAttr('type', 'submit')
|
|
53
|
+
.setAriaAttr('label', 'Save changes')
|
|
54
|
+
.on('click', handleSave)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Document Queries
|
|
58
|
+
|
|
59
|
+
Wrap `document` with `JJD.from(document)` before querying.
|
|
60
|
+
|
|
61
|
+
```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
|
|
66
|
+
|
|
67
|
+
// Inside a custom element's shadow root
|
|
68
|
+
const btn = this.getShadow(true).find('#submit')
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Attributes, Classes, Styles
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// 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
|
|
79
|
+
|
|
80
|
+
// Attribute — batch (null/undefined skipped)
|
|
81
|
+
el.setAttrs({ type: 'text', placeholder: 'Search…' })
|
|
82
|
+
|
|
83
|
+
// 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
|
|
94
|
+
|
|
95
|
+
// 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'])
|
|
102
|
+
|
|
103
|
+
// 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')
|
|
109
|
+
|
|
110
|
+
// ARIA is not presence-based like HTML boolean attributes
|
|
111
|
+
// Use explicit string states instead of swAttr()
|
|
112
|
+
el.setAriaAttr('disabled', 'true')
|
|
113
|
+
|
|
114
|
+
// Inline styles
|
|
115
|
+
el.setStyle('color', 'var(--color-brand)')
|
|
116
|
+
el.setStyles({ color: 'red', padding: '8px', border: null })
|
|
117
|
+
el.rmStyle('color', 'padding')
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Security — HTML Writes
|
|
121
|
+
|
|
122
|
+
Prefer `.setText()` for any user-supplied content. `.setHTML()` requires an explicit `true` flag when the string is non-empty.
|
|
123
|
+
|
|
124
|
+
```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
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Events
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// Native events
|
|
136
|
+
el.on('click', handler)
|
|
137
|
+
el.off('click', handler)
|
|
138
|
+
el.trigger('click')
|
|
139
|
+
|
|
140
|
+
// 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 }))
|
|
143
|
+
|
|
144
|
+
// Custom events — JJ defaults: bubbles: true, composed: true
|
|
145
|
+
this.dispatchEvent(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true }))
|
|
146
|
+
|
|
147
|
+
// Fluent dispatch (same defaults)
|
|
148
|
+
el.triggerEvent('click') // equivalent to trigger(new Event('click', { bubbles: true, composed: true }))
|
|
149
|
+
JJHE.from(this).triggerCustomEvent('todo-toggle', { id: 1, done: true })
|
|
150
|
+
// equivalent to trigger(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true }))
|
|
151
|
+
|
|
152
|
+
// Override defaults for internal-only events
|
|
153
|
+
new CustomEvent('panel-ready', { bubbles: false, composed: false })
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Custom Elements — Complete Pattern
|
|
157
|
+
|
|
158
|
+
Fetch template and style at **module scope** — loaded once, shared across all instances.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { attr2prop, defineComponent, fetchStyle, fetchTemplate, JJHE } from 'jj'
|
|
162
|
+
|
|
163
|
+
const templatePromise = fetchTemplate(import.meta.resolve('./my-card.html'))
|
|
164
|
+
const stylePromise = fetchStyle(import.meta.resolve('./my-card.css'))
|
|
165
|
+
|
|
166
|
+
export class MyCard extends HTMLElement {
|
|
167
|
+
static observedAttributes = ['user-name', 'count']
|
|
168
|
+
static defined = defineComponent('my-card', MyCard)
|
|
169
|
+
|
|
170
|
+
#userName = ''
|
|
171
|
+
#count = 0
|
|
172
|
+
#root = null // JJSR wrapper; attached in constructor
|
|
173
|
+
#isInitialized = false
|
|
174
|
+
|
|
175
|
+
constructor() {
|
|
176
|
+
super()
|
|
177
|
+
this.#root = JJHE.from(this).setShadow('open').getShadow(true)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
181
|
+
// Converts kebab-case → camelCase, then calls the matching setter
|
|
182
|
+
attr2prop(this, name, oldValue, newValue)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
get userName() {
|
|
186
|
+
return this.#userName
|
|
187
|
+
}
|
|
188
|
+
set userName(v) {
|
|
189
|
+
this.#userName = String(v ?? '')
|
|
190
|
+
this.#render()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
get count() {
|
|
194
|
+
return this.#count
|
|
195
|
+
}
|
|
196
|
+
set count(v) {
|
|
197
|
+
this.#count = Number(v) || 0
|
|
198
|
+
this.#render()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async connectedCallback() {
|
|
202
|
+
if (!this.#isInitialized) {
|
|
203
|
+
this.#root.init(await templatePromise, await stylePromise)
|
|
204
|
+
this.#isInitialized = true
|
|
205
|
+
}
|
|
206
|
+
this.#render()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#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))
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Caller must await before using the custom element tag
|
|
217
|
+
await MyCard.defined
|
|
218
|
+
// Or multiple in parallel
|
|
219
|
+
await Promise.all([MyCard.defined, OtherCard.defined])
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`defineComponent()` returns `Promise<boolean>`:
|
|
223
|
+
|
|
224
|
+
- `false` — newly defined by this call
|
|
225
|
+
- `true` — already defined with the same constructor
|
|
226
|
+
|
|
227
|
+
## Tree Builder
|
|
228
|
+
|
|
229
|
+
`JJHE.tree` is a factory for declarative element trees. Alias as `h` for brevity.
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
const h = JJHE.tree
|
|
233
|
+
|
|
234
|
+
const card = h(
|
|
235
|
+
'article',
|
|
236
|
+
{ class: 'card' },
|
|
237
|
+
h('h2', null, title),
|
|
238
|
+
h('p', { class: 'body' }, description),
|
|
239
|
+
h('footer', null, h('a', { href: url }, 'Read more')),
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Children and Templates
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// Clear children — internally uses replaceChildren()
|
|
247
|
+
el.empty()
|
|
248
|
+
|
|
249
|
+
// 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)
|
|
254
|
+
|
|
255
|
+
// Append
|
|
256
|
+
el.addChild(child)
|
|
257
|
+
el.addChildMap(items, (item) => JJHE.tree('li', null, item.label))
|
|
258
|
+
el.addTemplate(await templatePromise) // clones before appending
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`addChild` / `preChild` / `setChild` and map variants ignore `null`/`undefined`; all other non-node values are coerced to Text nodes.
|
|
262
|
+
|
|
263
|
+
## Node Traversal
|
|
264
|
+
|
|
265
|
+
```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
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Resource Loaders
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { JJHE, fetchStyle, fetchTemplate } from 'jj'
|
|
276
|
+
|
|
277
|
+
const h = JJHE.tree
|
|
278
|
+
|
|
279
|
+
// Hint browser to preload early with native <link>
|
|
280
|
+
document.head.append(
|
|
281
|
+
h('link', {
|
|
282
|
+
href: import.meta.resolve('./bundle.js'),
|
|
283
|
+
rel: 'modulepreload',
|
|
284
|
+
}).ref,
|
|
285
|
+
)
|
|
286
|
+
document.head.append(
|
|
287
|
+
h('link', {
|
|
288
|
+
href: import.meta.resolve('./main.css'),
|
|
289
|
+
rel: 'preload',
|
|
290
|
+
as: 'style',
|
|
291
|
+
}).ref,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
// Load a CSSStyleSheet for adoptedStyleSheets or setShadow
|
|
295
|
+
const sheet = await fetchStyle(import.meta.resolve('./theme.css'))
|
|
296
|
+
document.adoptedStyleSheets = [sheet]
|
|
297
|
+
|
|
298
|
+
// Load a DocumentFragment for addTemplate / setShadow
|
|
299
|
+
const fragment = await fetchTemplate(import.meta.resolve('./dialog.html'))
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## String Casing
|
|
303
|
+
|
|
304
|
+
String case-conversion helpers are internal implementation details.
|
|
305
|
+
Use higher-level public APIs like `attr2prop` and `defineComponent` instead of importing low-level casing utilities.
|
|
306
|
+
|
|
307
|
+
## Common mistakes
|
|
308
|
+
|
|
309
|
+
1. **`.ts` extension in imports** — TypeScript source must use `.js` (`import { X } from './X.js'`).
|
|
310
|
+
2. **`JJHE.create('svg')`** — throws; use `JJSE.create('svg')`.
|
|
311
|
+
3. **`el.setHTML(html)` without `true`** — throws when html is non-empty.
|
|
312
|
+
4. **Fetching template/style inside `connectedCallback`** — fetch at module scope so the network request is shared.
|
|
313
|
+
5. **Not awaiting `Element.defined`** — markup may be parsed before the element is defined, causing flaky upgrades.
|
|
314
|
+
6. **Breaking the chain with `.ref` unnecessarily** — use wrapper methods first; reach for `.ref` only when no wrapper method exists.
|
|
315
|
+
|
|
316
|
+
## Pitfall Prevention Rules (Component Work)
|
|
317
|
+
|
|
318
|
+
Apply these rules whenever building or refactoring JJ-based custom elements:
|
|
319
|
+
|
|
320
|
+
1. **Template-first component UI** — if component markup is static, use `fetchTemplate` + `setTemplate` (or shadow `init`) rather than building the UI imperatively in JS.
|
|
321
|
+
2. **Keep routing/URL state outside UI components** — query params and history updates belong in page/controller code, not in reusable visual components.
|
|
322
|
+
3. **Query with wrappers, not native DOM first** — prefer `find` / `findAll` on JJ wrappers before dropping to `.ref`.
|
|
323
|
+
4. **Do not unwrap and re-wrap** — avoid `find(...).ref` followed by `JJHE.from(...)`; keep the wrapper value.
|
|
324
|
+
5. **Use specific selectors for required nodes** — prefer selectors like `button#save` and `progress#step-progress` so selector intent replaces manual `instanceof` checks.
|
|
325
|
+
6. **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.
|
|
326
|
+
7. **Use one initialization invariant** — if `isInitialized` exists and guarantees bound refs are ready, guard on that flag instead of repeating null checks for every bound field.
|
|
327
|
+
8. **Prefer fluent assignment when binding handlers** — in setup code, chain `.on(...)` directly on `find(...)` where readable (for example assigning a button wrapper and click handler in one line).
|
|
328
|
+
|
|
329
|
+
## Reference Docs
|
|
330
|
+
|
|
331
|
+
For framework migration or deep-dive patterns, load these on demand:
|
|
332
|
+
|
|
333
|
+
- `references/react-to-jj-translation.md`
|
|
334
|
+
- `references/vue-to-jj-translation.md`
|
|
335
|
+
- `references/svelte-to-jj-translation.md`
|
|
336
|
+
- `references/angular-to-jj-translation.md`
|
|
337
|
+
- `references/jquery-to-jj-translation.md`
|
|
338
|
+
- `references/lit-to-jj-translation.md`
|
|
339
|
+
- `references/web-components-patterns.md`
|
|
340
|
+
- `references/eventing-patterns.md`
|
|
341
|
+
- `references/querying-patterns.md`
|
|
342
|
+
- `references/css-improvements.md`
|
|
343
|
+
- `references/testing-with-jsdom.md`
|
|
344
|
+
- `references/security-and-html.md`
|
|
345
|
+
- `references/error-handling-patterns.md`
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Angular to JJ Translation
|
|
2
|
+
|
|
3
|
+
Angular uses a DI-driven component architecture with templates and decorators. JJ is library-level, not framework-level.
|
|
4
|
+
|
|
5
|
+
## Mental mapping
|
|
6
|
+
|
|
7
|
+
| Angular concept | JJ equivalent |
|
|
8
|
+
| ---------------------------- | --------------------------------------------------------------- |
|
|
9
|
+
| `@Component` template | `JJHE.tree()` or external HTML with `fetchTemplate` |
|
|
10
|
+
| `@Input()` | `observedAttributes` + `attr2prop` + property setter |
|
|
11
|
+
| `@Output()` / `EventEmitter` | `triggerCustomEvent()` dispatching a `CustomEvent` |
|
|
12
|
+
| `ngOnInit` | `connectedCallback` |
|
|
13
|
+
| `ngOnDestroy` | `disconnectedCallback` |
|
|
14
|
+
| `ChangeDetectionRef` | Explicit render call in property setters |
|
|
15
|
+
| `*ngFor` | `el.addChildMap(items, fn)` |
|
|
16
|
+
| `*ngIf` | `el.hide()` / `el.show()` or conditional class via `setClasses` |
|
|
17
|
+
| DI services | Plain ES modules exporting shared state or event buses |
|
|
18
|
+
|
|
19
|
+
## Input binding example
|
|
20
|
+
|
|
21
|
+
Angular:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
@Input() userName: string = ''
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
JJ:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
static observedAttributes = ['user-name']
|
|
31
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
32
|
+
attr2prop(this, name, oldValue, newValue)
|
|
33
|
+
}
|
|
34
|
+
get userName() { return this.#userName }
|
|
35
|
+
set userName(v) { this.#userName = String(v ?? ''); this.#render() }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Output binding example
|
|
39
|
+
|
|
40
|
+
Angular:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
@Output() selected = new EventEmitter<string>()
|
|
44
|
+
this.selected.emit(itemId)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
JJ:
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
JJHE.from(this).triggerCustomEvent('selected', itemId)
|
|
51
|
+
// JJ default: bubbles: true, composed: true — parent receives through shadow boundary
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## No DI — use ES modules instead
|
|
55
|
+
|
|
56
|
+
Angular service:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
@Injectable({ providedIn: 'root' })
|
|
60
|
+
class AuthService { … }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
JJ — just a module:
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
// auth.js
|
|
67
|
+
export const authState = { user: null }
|
|
68
|
+
export function login(user) {
|
|
69
|
+
authState.user = user
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Browser references
|
|
74
|
+
|
|
75
|
+
- Custom element lifecycle: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks
|
|
76
|
+
- ES Modules: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# CSS Improvements
|
|
2
|
+
|
|
3
|
+
Use JJ for DOM wiring and rely on CSS standards for the styling system.
|
|
4
|
+
|
|
5
|
+
## JJ style helpers with examples
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
// Single property
|
|
9
|
+
el.setStyle('color', 'var(--color-brand)')
|
|
10
|
+
el.setStyle('padding', '8px 16px')
|
|
11
|
+
|
|
12
|
+
// Batch — null/false removes the property
|
|
13
|
+
el.setStyles({
|
|
14
|
+
color: 'red',
|
|
15
|
+
padding: '8px',
|
|
16
|
+
border: null, // removes border
|
|
17
|
+
background: false, // removes background
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Remove properties
|
|
21
|
+
el.rmStyle('color', 'padding')
|
|
22
|
+
|
|
23
|
+
// Read a property
|
|
24
|
+
const val = el.getStyle('color') // '' when not set
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Class-driven styling (preferred)
|
|
28
|
+
|
|
29
|
+
Modify classes and keep visual logic in CSS:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
el.addClass('is-active')
|
|
33
|
+
el.rmClass('is-loading')
|
|
34
|
+
el.swClass('is-expanded', isExpanded) // explicit: condition drives add/remove
|
|
35
|
+
el.swClass('is-expanded') // auto: flips current state
|
|
36
|
+
el.setClasses({ 'is-active': isActive, 'is-error': hasError })
|
|
37
|
+
el.setClass('card card--featured') // replaces entire className
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Text direction and advanced DOM state
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
el.setAttr('dir', 'rtl')
|
|
44
|
+
el.setAttr('lang', 'ar')
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## CSS custom properties pierce Shadow DOM
|
|
48
|
+
|
|
49
|
+
Custom properties cascade through shadow boundaries. Define variables on the host or `:root`:
|
|
50
|
+
|
|
51
|
+
```css
|
|
52
|
+
:root {
|
|
53
|
+
--btn-color: #0070f3;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Inside shadow styles:
|
|
58
|
+
|
|
59
|
+
```css
|
|
60
|
+
button {
|
|
61
|
+
background: var(--btn-color);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Loading stylesheets
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { JJHE, fetchStyle } from 'jj'
|
|
69
|
+
|
|
70
|
+
const h = JJHE.tree
|
|
71
|
+
|
|
72
|
+
// Hint browser to start loading early
|
|
73
|
+
document.head.addChild(
|
|
74
|
+
h('link', {
|
|
75
|
+
href: import.meta.resolve('./theme.css'),
|
|
76
|
+
rel: 'preload',
|
|
77
|
+
as: 'style',
|
|
78
|
+
}).ref,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// Load as a CSSStyleSheet for adoptedStyleSheets
|
|
82
|
+
const sheet = await fetchStyle(import.meta.resolve('./theme.css'))
|
|
83
|
+
document.adoptedStyleSheets = [sheet]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Browser references
|
|
87
|
+
|
|
88
|
+
- CSSStyleDeclaration: https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration
|
|
89
|
+
- CSS nesting: https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Nesting
|
|
90
|
+
- CSS custom properties: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties
|
|
91
|
+
- adoptedStyleSheets: https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Error Handling Patterns
|
|
2
|
+
|
|
3
|
+
JJ follows four principles for errors: specific, actionable, proximate, and runtime-verified.
|
|
4
|
+
|
|
5
|
+
## Principles
|
|
6
|
+
|
|
7
|
+
1. **Specific** — Use the most precise error type: `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`.
|
|
8
|
+
2. **Actionable** — Include a fix hint when the stack trace alone is insufficient.
|
|
9
|
+
3. **Proximate** — Throw where the invalid value is _consumed_, not where it is received.
|
|
10
|
+
4. **Runtime-verified** — Don't rely on TypeScript types alone; validate at library boundaries because callers may be plain JavaScript.
|
|
11
|
+
|
|
12
|
+
## Internal helpers
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { typeErr, errMsg } from './internal.js'
|
|
16
|
+
|
|
17
|
+
// TypeError with standardized message
|
|
18
|
+
throw typeErr('name', 'a string', name)
|
|
19
|
+
// → TypeError: Expected name to be a string, but got number
|
|
20
|
+
|
|
21
|
+
// RangeError using errMsg for the message
|
|
22
|
+
throw new RangeError(
|
|
23
|
+
errMsg('as', "'fetch', 'style', or 'script'", as, 'Use a valid value or omit it to auto-detect from the URL.'),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
// With extra fix hint when context is ambiguous
|
|
27
|
+
throw typeErr('ref', 'a Text node', ref, "Create a Text node with JJT.create() or document.createTextNode('text').")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`errMsg(varName, expected, received, extra?)` — generates the standard message string.
|
|
31
|
+
`typeErr(varName, expected, received, extra?)` — creates and returns a `TypeError` using that message.
|
|
32
|
+
|
|
33
|
+
## When to add an `extra` hint
|
|
34
|
+
|
|
35
|
+
Add it when:
|
|
36
|
+
|
|
37
|
+
- The API has overloads and the caller might not know which to use.
|
|
38
|
+
- The wrapper constructor / factory method accepts multiple input forms.
|
|
39
|
+
- The correct alternative is not obvious from the stack trace.
|
|
40
|
+
|
|
41
|
+
Skip it when:
|
|
42
|
+
|
|
43
|
+
- The failure is a simple scalar type check and the stack trace already pinpoints the misuse.
|
|
44
|
+
|
|
45
|
+
## Practical validation example
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
if (!isStr(name)) {
|
|
49
|
+
throw typeErr('name', 'a string', name)
|
|
50
|
+
}
|
|
51
|
+
if (!isObj(attrs)) {
|
|
52
|
+
throw typeErr(
|
|
53
|
+
'attrs',
|
|
54
|
+
'a plain object or null/undefined',
|
|
55
|
+
attrs,
|
|
56
|
+
'Pass an object like { class: "btn" } or omit the argument.',
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
if (val < 0 || val > 100) {
|
|
60
|
+
throw new RangeError(errMsg('val', 'a number between 0 and 100', val))
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Error types reference
|
|
65
|
+
|
|
66
|
+
| Error type | When to use |
|
|
67
|
+
| ---------------- | ------------------------------------------------- |
|
|
68
|
+
| `TypeError` | Wrong type or shape |
|
|
69
|
+
| `RangeError` | Value out of acceptable range |
|
|
70
|
+
| `SyntaxError` | Malformed string input (selector, expression) |
|
|
71
|
+
| `ReferenceError` | Reference to undefined name |
|
|
72
|
+
| `AggregateError` | Multiple simultaneous failures (e.g., map-reduce) |
|
|
73
|
+
|
|
74
|
+
## Browser references
|
|
75
|
+
|
|
76
|
+
- TypeError: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
|
|
77
|
+
- RangeError: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RangeError
|
|
78
|
+
- SyntaxError: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SyntaxError
|
|
79
|
+
- AggregateError: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Eventing Patterns
|
|
2
|
+
|
|
3
|
+
## Native event listeners
|
|
4
|
+
|
|
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'))
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The handler `this` inside `.on()` is bound to the JJ wrapper instance. Use `this.ref` to access the native element:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
btn.on('click', function () {
|
|
15
|
+
// this → the JJHE wrapper
|
|
16
|
+
// this.ref → the HTMLButtonElement
|
|
17
|
+
console.log(this.ref.textContent)
|
|
18
|
+
})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Custom events with payloads
|
|
22
|
+
|
|
23
|
+
`trigger()` accepts a full event object. JJ also provides two convenience constructors that keep dispatch fluent:
|
|
24
|
+
|
|
25
|
+
- `triggerEvent(name, options?)` is equivalent to `trigger(new Event(name, { bubbles: true, composed: true, ...options }))`
|
|
26
|
+
- `triggerCustomEvent(name, detail?, options?)` is equivalent to `trigger(new CustomEvent(name, { bubbles: true, composed: true, ...options, detail }))`
|
|
27
|
+
|
|
28
|
+
Use the native `CustomEvent` constructor when dispatching directly:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
this.dispatchEvent(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true }))
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`triggerCustomEvent(name, detail?, options?)` — fluent wrapper dispatch:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
JJHE.from(this).triggerCustomEvent('todo-toggle', { id: 1, done: true })
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Shadow DOM event rules
|
|
41
|
+
|
|
42
|
+
| Situation | propagates past shadow root? |
|
|
43
|
+
| --------------------------------------- | ----------------------------------------- |
|
|
44
|
+
| Native UI events (click, input, change) | Yes — already `composed: true` |
|
|
45
|
+
| Native `CustomEvent` (no options) | No — `composed` defaults to `false` |
|
|
46
|
+
| `triggerCustomEvent()` | Yes — JJ sets `composed: true` by default |
|
|
47
|
+
|
|
48
|
+
Override defaults explicitly when the event is internal:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
new CustomEvent('internal-ready', { bubbles: false, composed: false })
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Event delegation
|
|
55
|
+
|
|
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)
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## One-time listener via AbortController
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
const ctrl = new AbortController()
|
|
68
|
+
el.ref.addEventListener('click', handler, { signal: ctrl.signal })
|
|
69
|
+
// later:
|
|
70
|
+
ctrl.abort() // removes listener
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Browser references
|
|
74
|
+
|
|
75
|
+
- EventTarget.addEventListener: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
|
|
76
|
+
- CustomEvent: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
|
|
77
|
+
- Event.composed: https://developer.mozilla.org/en-US/docs/Web/API/Event/composed
|
|
78
|
+
- Event bubbling: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Event_bubbling
|