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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jj",
3
- "version": "3.0.0-rc.4",
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
- "SKILL.md"
31
+ "skills"
32
32
  ],
33
33
  "scripts": {
34
34
  "fmt": "prettier --write .",
@@ -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