jj 3.0.0-rc.4 → 3.0.0-rc.6
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 +487 -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.6",
|
|
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,487 @@
|
|
|
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
|
+
## 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
|
+
- Use `setText()` for user content and treat `setHTML(..., true)` as a trusted-content escape hatch.
|
|
20
|
+
- For repeated child interactions, prefer one delegated listener on a stable parent over one listener per child.
|
|
21
|
+
- Choose shadow DOM for self-contained widgets and light DOM for page-level content that should inherit global styles.
|
|
22
|
+
- 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.
|
|
23
|
+
- Prefer `'open'` shadow roots unless the user explicitly needs `'closed'`; open mode is easier to test and debug.
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
26
|
+
## Naming Conventions
|
|
27
|
+
|
|
28
|
+
In this repository, prefix variables that hold JJ wrapper instances with `jj`.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
const jjDoc = JJD.from(document)
|
|
32
|
+
const jjFruits = jjDoc.find('#fruits', true)
|
|
33
|
+
const jjSubmitBtn = jjDoc.find('button#submit', true)
|
|
34
|
+
const jjDialog = JJHE.create('dialog')
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Naming defaults:
|
|
38
|
+
|
|
39
|
+
- 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).
|
|
40
|
+
- Do not use `jj*` for plain data like `fruits`, `title`, `isOpen`, or `userName`.
|
|
41
|
+
- Do not use `jj*` for native DOM values; prefer names like `formEl`, `shadowRoot`, `inputRef`, or `styleSheet`.
|
|
42
|
+
- For promises, use normal names with `Promise`, like `templatePromise` or `stylePromise`.
|
|
43
|
+
- `h` is the main intentional exception: use it as the local alias for `JJHE.tree`.
|
|
44
|
+
|
|
45
|
+
## Wrapper Hierarchy
|
|
46
|
+
|
|
47
|
+
Each JJ wrapper exposes the native node via `.ref`.
|
|
48
|
+
|
|
49
|
+
| Class | Wraps | Key additions |
|
|
50
|
+
| ----- | ---------------- | ------------------------------------------------------ |
|
|
51
|
+
| JJET | EventTarget | `.on()`, `.off()`, `.trigger()`, `.run()` |
|
|
52
|
+
| JJN | Node | `.getParent()`, `.getChildren()`, `.rm()`, `.clone()` |
|
|
53
|
+
| JJD | Document | `.find()`, `.findAll()` |
|
|
54
|
+
| JJDF | DocumentFragment | `.addTemplate()`, `.setTemplate()`, batch child ops |
|
|
55
|
+
| JJE | Element | Attributes, classes, ARIA, visibility, HTML write |
|
|
56
|
+
| JJHE | HTMLElement | `.setText()`, `.setStyle()`, `.setShadow()`, `.tree()` |
|
|
57
|
+
| JJSE | SVGElement | SVG namespace factory |
|
|
58
|
+
| JJME | MathMLElement | MathML namespace factory |
|
|
59
|
+
| JJSR | ShadowRoot | `.find()`, `.findAll()`, `.addStyle()`, `.init()` |
|
|
60
|
+
| JJDF | DocumentFragment | Fragment operations |
|
|
61
|
+
| JJT | Text | `.getText()`, `.setText()` |
|
|
62
|
+
|
|
63
|
+
## Type-Safe Creation — Always Use Factory Methods
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// ✅ CORRECT — factory methods infer the precise generic type
|
|
67
|
+
const div = JJHE.create('div') // JJHE<HTMLDivElement>
|
|
68
|
+
const input = JJHE.create('input') // JJHE<HTMLInputElement>
|
|
69
|
+
const svg = JJSE.create('svg') // JJSE<SVGSVGElement>
|
|
70
|
+
const math = JJME.create('math') // JJME<MathMLElement>
|
|
71
|
+
const frag = JJDF.create() // JJDF
|
|
72
|
+
const btn = JJHE.fromId('my-btn') // JJHE<HTMLButtonElement>
|
|
73
|
+
|
|
74
|
+
// ❌ WRONG
|
|
75
|
+
JJHE.create('svg') // throws — use JJSE.create('svg')
|
|
76
|
+
new JJHE(element) // don't call constructors directly
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Chaining
|
|
80
|
+
|
|
81
|
+
All mutating methods return `this`. Chain as much as possible; access `.ref` only when a wrapper method does not exist.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
const btn = JJHE.create('button')
|
|
85
|
+
.addClass('btn', 'primary')
|
|
86
|
+
.setText('Save')
|
|
87
|
+
.setAttr('type', 'submit')
|
|
88
|
+
.setAriaAttr('label', 'Save changes')
|
|
89
|
+
.on('click', handleSave)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Tutorial Defaults — Prefer JJ Idioms Over Native DOM Steps
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// ✅ preferred: build a subtree once
|
|
98
|
+
const h = JJHE.tree
|
|
99
|
+
|
|
100
|
+
latestChatResponse.addChild(
|
|
101
|
+
h('section', null, h('h2', null, 'User'), h('div', null, userPrompt), h('h2', null, 'Assistant'), assistantMessage),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
// ✅ also fine for flat mapped children
|
|
105
|
+
const list = JJHE.create('ul').addChildMap(fruits, (fruit) => h('li', null, fruit))
|
|
106
|
+
|
|
107
|
+
// ❌ avoid native-style wrapper escape hatches when JJ already covers it
|
|
108
|
+
latestChatResponse.ref.appendChild(JJHE.create('h2').setText('User').ref)
|
|
109
|
+
latestChatResponse.ref.appendChild(JJHE.create('div').setText(userPrompt).ref)
|
|
110
|
+
latestChatResponse.ref.appendChild(JJHE.create('h2').setText('Assistant').ref)
|
|
111
|
+
latestChatResponse.ref.appendChild(assistantMessage.ref)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Default heuristics from the tutorial:
|
|
115
|
+
|
|
116
|
+
- Use `JJHE.tree` with a local `h` alias when creating multiple siblings or any nested subtree.
|
|
117
|
+
- Use `create()` for one-off elements; switch to `tree()` as soon as structure becomes non-trivial.
|
|
118
|
+
- Prefer `setChild()` or `setChildren()` when replacing content, not `.empty().addChild()`.
|
|
119
|
+
- Prefer `addChildMap()` or `setChildMap()` when rendering from arrays.
|
|
120
|
+
- Keep values wrapped. Reach for `.ref` only for native APIs JJ does not expose.
|
|
121
|
+
- Use JJ verb families consistently: `set*` replaces, `add*` appends, `pre*` prepends, `rm*` removes, `sw*` toggles.
|
|
122
|
+
|
|
123
|
+
## Document Queries
|
|
124
|
+
|
|
125
|
+
Wrap `document` with `JJD.from(document)` before querying.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const doc = JJD.from(document)
|
|
129
|
+
const app = doc.find('#app', true) // throws when absent
|
|
130
|
+
const card = doc.find('.card') // null when absent
|
|
131
|
+
const items = doc.findAll('.item') // always an array
|
|
132
|
+
|
|
133
|
+
// Inside a custom element's shadow root
|
|
134
|
+
const btn = this.getShadow(true).find('#submit')
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Querying defaults from the tutorial:
|
|
138
|
+
|
|
139
|
+
- Start from a wrapped container like `JJD.from(document)` or a `JJSR` shadow root.
|
|
140
|
+
- Prefer `find(selector, true)` when absence is a bug; it fails earlier and more clearly than a later null access.
|
|
141
|
+
- Prefer narrower selectors that encode expectations, like `button#submit`, instead of broad lookups plus manual type checks.
|
|
142
|
+
- Use `findAll()` for arrays of wrappers and keep operating on wrappers instead of unwrapping to native elements.
|
|
143
|
+
- Do not use `.ref.querySelector(...)` or `.ref.querySelectorAll(...)` when `find()` or `findAll()` already covers the case.
|
|
144
|
+
- Use `.closest()` on wrappers for event delegation and ancestor lookup.
|
|
145
|
+
- Use `JJHE.fromId('submit-btn')` for direct ID lookup when you already know the target is an HTML element.
|
|
146
|
+
|
|
147
|
+
## Attributes, Classes, Styles
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// Attribute — singular
|
|
151
|
+
el.setAttr('role', 'button')
|
|
152
|
+
el.getAttr('role')
|
|
153
|
+
el.rmAttr('hidden')
|
|
154
|
+
el.swAttr('disabled', !isReady) // sets disabled="" or removes it
|
|
155
|
+
|
|
156
|
+
// Attribute — batch (null/undefined skipped)
|
|
157
|
+
el.setAttrs({ type: 'text', placeholder: 'Search…' })
|
|
158
|
+
|
|
159
|
+
// Classes
|
|
160
|
+
el.addClass('active')
|
|
161
|
+
el.addClasses(['chip', 'selected'])
|
|
162
|
+
el.rmClass('disabled')
|
|
163
|
+
el.rmClasses(['pending', 'loading'])
|
|
164
|
+
el.swClass('expanded', isExpanded) // explicit: adds when truthy, removes when falsy
|
|
165
|
+
el.swClass('is-active') // auto: flips current state (adds if absent, removes if present)
|
|
166
|
+
el.swAttr('disabled', !isReady) // explicit: sets disabled="" or removes it
|
|
167
|
+
el.swAttr('readonly') // auto: flips current state
|
|
168
|
+
el.setClasses({ active: isActive, disabled: !isReady })
|
|
169
|
+
el.setClass('card card--featured') // replaces entire className
|
|
170
|
+
|
|
171
|
+
// Dataset
|
|
172
|
+
el.getDataAttr('userId')
|
|
173
|
+
el.hasDataAttr('userId')
|
|
174
|
+
el.setDataAttr('userId', '42')
|
|
175
|
+
el.setDataAttrs({ role: 'admin', team: 'ui' })
|
|
176
|
+
el.rmDataAttr('userId')
|
|
177
|
+
el.rmDataAttrs(['role', 'team'])
|
|
178
|
+
|
|
179
|
+
// ARIA
|
|
180
|
+
el.getAriaAttr('hidden')
|
|
181
|
+
el.hasAriaAttr('hidden')
|
|
182
|
+
el.setAriaAttr('hidden', 'true')
|
|
183
|
+
el.setAriaAttrs({ label: 'Dialog', modal: 'true' })
|
|
184
|
+
el.rmAriaAttr('hidden')
|
|
185
|
+
|
|
186
|
+
// ARIA is not presence-based like HTML boolean attributes
|
|
187
|
+
// Use explicit string states instead of swAttr()
|
|
188
|
+
el.setAriaAttr('disabled', 'true')
|
|
189
|
+
|
|
190
|
+
// Inline styles
|
|
191
|
+
el.setStyle('color', 'var(--color-brand)')
|
|
192
|
+
el.setStyles({ color: 'red', padding: '8px', border: null })
|
|
193
|
+
el.rmStyle('color', 'padding')
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Security — HTML Writes
|
|
197
|
+
|
|
198
|
+
Prefer `.setText()` for any user-supplied content. `.setHTML()` requires an explicit `true` flag when the string is non-empty.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
el.setText(userInput) // ✅ always safe
|
|
202
|
+
el.setHTML('<p>Trusted markup</p>', true) // ✅ explicit opt-in
|
|
203
|
+
el.setHTML('') // ✅ clearing is allowed without flag
|
|
204
|
+
el.setHTML('<p>content</p>') // ❌ THROWS — missing unsafe flag
|
|
205
|
+
el.ref.innerHTML = '…' // ❌ bypasses guard — avoid
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Events
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// Native events
|
|
212
|
+
el.on('click', handler)
|
|
213
|
+
el.off('click', handler)
|
|
214
|
+
el.trigger('click')
|
|
215
|
+
|
|
216
|
+
// Explicit event objects (equivalent to JJ helpers below)
|
|
217
|
+
el.trigger(new Event('click', { bubbles: true, composed: true }))
|
|
218
|
+
el.trigger(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true }))
|
|
219
|
+
|
|
220
|
+
// Custom events — JJ defaults: bubbles: true, composed: true
|
|
221
|
+
this.dispatchEvent(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true }))
|
|
222
|
+
|
|
223
|
+
// Fluent dispatch (same defaults)
|
|
224
|
+
el.triggerEvent('click') // equivalent to trigger(new Event('click', { bubbles: true, composed: true }))
|
|
225
|
+
JJHE.from(this).triggerCustomEvent('todo-toggle', { id: 1, done: true })
|
|
226
|
+
// equivalent to trigger(new CustomEvent('todo-toggle', { detail: { id: 1, done: true }, bubbles: true, composed: true }))
|
|
227
|
+
|
|
228
|
+
// Override defaults for internal-only events
|
|
229
|
+
new CustomEvent('panel-ready', { bubbles: false, composed: false })
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Event defaults from the tutorial:
|
|
233
|
+
|
|
234
|
+
- Prefer `.on()` and `.off()` on wrappers over native `addEventListener`/`removeEventListener` when already working with JJ values.
|
|
235
|
+
- Prefer `.triggerEvent()` and `.triggerCustomEvent()` for common JJ event dispatch; they default to `bubbles: true` and `composed: true`.
|
|
236
|
+
- Use `triggerCustomEvent(name, detail)` for component-to-parent communication instead of ad hoc callback plumbing.
|
|
237
|
+
- Use `bubbles: false` and `composed: false` only for intentionally internal events.
|
|
238
|
+
- Keep event code close to the wrapper it affects so later DOM updates stay targeted and local.
|
|
239
|
+
|
|
240
|
+
Guide defaults for event-heavy UI:
|
|
241
|
+
|
|
242
|
+
- Prefer event delegation on a common parent for repeated child actions instead of binding one listener per item.
|
|
243
|
+
- Use `.closest()` to recover the intended delegated target from `event.target`.
|
|
244
|
+
- When you need JJ's wrapper-bound `this` inside a listener, use `function` syntax, not an arrow.
|
|
245
|
+
- Native UI events like `click`, `input`, and `change` already cross shadow boundaries; custom events do not unless `composed: true`.
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
list.on('click', function (event) {
|
|
249
|
+
const item = JJHE.from(event.target as Node).closest('[data-item-id]')
|
|
250
|
+
if (!item) return
|
|
251
|
+
this.addClass('handled')
|
|
252
|
+
item.addClass('active')
|
|
253
|
+
})
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Custom Elements — Complete Pattern
|
|
257
|
+
|
|
258
|
+
Fetch template and style at **module scope** — loaded once, shared across all instances.
|
|
259
|
+
|
|
260
|
+
Guide defaults for component shape:
|
|
261
|
+
|
|
262
|
+
- 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.
|
|
263
|
+
- Prefer `'open'` shadow mode unless stricter encapsulation is a hard requirement.
|
|
264
|
+
- `attributeChangedCallback()` can run before `connectedCallback()` for parsed attributes, so setters and render paths must tolerate pre-mount state.
|
|
265
|
+
- 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.
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
import { attr2prop, defineComponent, fetchStyle, fetchTemplate, JJHE } from 'jj'
|
|
269
|
+
|
|
270
|
+
const templatePromise = fetchTemplate(import.meta.resolve('./my-card.html'))
|
|
271
|
+
const stylePromise = fetchStyle(import.meta.resolve('./my-card.css'))
|
|
272
|
+
|
|
273
|
+
export class MyCard extends HTMLElement {
|
|
274
|
+
static observedAttributes = ['user-name', 'count']
|
|
275
|
+
static defined = defineComponent('my-card', MyCard)
|
|
276
|
+
|
|
277
|
+
#userName = ''
|
|
278
|
+
#count = 0
|
|
279
|
+
#root = null // JJSR wrapper; attached in constructor
|
|
280
|
+
#isInitialized = false
|
|
281
|
+
|
|
282
|
+
constructor() {
|
|
283
|
+
super()
|
|
284
|
+
this.#root = JJHE.from(this).setShadow('open').getShadow(true)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
288
|
+
// Converts kebab-case → camelCase, then calls the matching setter
|
|
289
|
+
attr2prop(this, name, oldValue, newValue)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
get userName() {
|
|
293
|
+
return this.#userName
|
|
294
|
+
}
|
|
295
|
+
set userName(v) {
|
|
296
|
+
this.#userName = String(v ?? '')
|
|
297
|
+
this.#render()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
get count() {
|
|
301
|
+
return this.#count
|
|
302
|
+
}
|
|
303
|
+
set count(v) {
|
|
304
|
+
this.#count = Number(v) || 0
|
|
305
|
+
this.#render()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async connectedCallback() {
|
|
309
|
+
if (!this.#isInitialized) {
|
|
310
|
+
this.#root.init(await templatePromise, await stylePromise)
|
|
311
|
+
this.#isInitialized = true
|
|
312
|
+
}
|
|
313
|
+
this.#render()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#render() {
|
|
317
|
+
if (!this.#root) return // guard for attribute changes before mount
|
|
318
|
+
this.#root.find('[data-role="name"]')?.setText(this.#userName)
|
|
319
|
+
this.#root.find('[data-role="count"]')?.setText(String(this.#count))
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Caller must await before using the custom element tag
|
|
324
|
+
await MyCard.defined
|
|
325
|
+
// Or multiple in parallel
|
|
326
|
+
await Promise.all([MyCard.defined, OtherCard.defined])
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Template defaults from the tutorial:
|
|
330
|
+
|
|
331
|
+
- Prefer fetched `.html` templates for large static markup.
|
|
332
|
+
- Prefer `<template>` elements for reusable DOM snippets already present in the page.
|
|
333
|
+
- Prefer `JJHE.tree()` or `JJHE.create()` when you need live wrapper references for later updates.
|
|
334
|
+
- Keep template promises at module scope; for lazy loading, initialize them inside `connectedCallback()` with an `if (!templatePromise)` guard.
|
|
335
|
+
- Use one stable `#root` wrapper per component: `JJHE.from(this)` for light DOM or `JJHE.from(this).setShadow(...).getShadow(true)` for shadow DOM.
|
|
336
|
+
- Initialize template content once, then update specific nodes with `find(...).setText(...)` or other targeted wrapper operations.
|
|
337
|
+
|
|
338
|
+
Guide defaults for attributes and queries:
|
|
339
|
+
|
|
340
|
+
- Always coerce attribute-backed values in setters because HTML attributes arrive as strings.
|
|
341
|
+
- Query inside shadow DOM from the `JJSR` wrapper, never from `document`.
|
|
342
|
+
- Use specific selectors like `button#submit` or `[data-role="title"]` so the selector carries intent.
|
|
343
|
+
|
|
344
|
+
State defaults from the tutorial:
|
|
345
|
+
|
|
346
|
+
- Prefer plain objects or classes for state and update the exact affected wrappers in event handlers or setters.
|
|
347
|
+
- Prefer targeted updates like `value.setText(String(state.count))` over rebuilding an entire subtree for a small change.
|
|
348
|
+
- Use getters/setters or small helper methods when they make state transitions clearer, not because JJ requires a framework-style abstraction.
|
|
349
|
+
- Reach for external state libraries only when the application actually needs cross-cutting coordination beyond local JS state.
|
|
350
|
+
|
|
351
|
+
`defineComponent()` returns `Promise<boolean>`:
|
|
352
|
+
|
|
353
|
+
- `false` — newly defined by this call
|
|
354
|
+
- `true` — already defined with the same constructor
|
|
355
|
+
|
|
356
|
+
## Tree Builder
|
|
357
|
+
|
|
358
|
+
`JJHE.tree` is a factory for declarative element trees. Alias as `h` for brevity.
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
const h = JJHE.tree
|
|
362
|
+
|
|
363
|
+
const card = h(
|
|
364
|
+
'article',
|
|
365
|
+
{ class: 'card' },
|
|
366
|
+
h('h2', null, title),
|
|
367
|
+
h('p', { class: 'body' }, description),
|
|
368
|
+
h('footer', null, h('a', { href: url }, 'Read more')),
|
|
369
|
+
)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Children and Templates
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
// Clear children — internally uses replaceChildren()
|
|
376
|
+
el.empty()
|
|
377
|
+
|
|
378
|
+
// Replace all children in one call (prefer over .empty().addChild())
|
|
379
|
+
el.setChild(newChild)
|
|
380
|
+
el.setChildren([childA, childB])
|
|
381
|
+
el.setChildMap(items, (item) => JJHE.tree('li', null, item.label))
|
|
382
|
+
el.setTemplate(templateElement)
|
|
383
|
+
|
|
384
|
+
// Append
|
|
385
|
+
el.addChild(child)
|
|
386
|
+
el.addChildMap(items, (item) => JJHE.tree('li', null, item.label))
|
|
387
|
+
el.addTemplate(await templatePromise) // clones before appending
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
`addChild` / `preChild` / `setChild` and map variants ignore `null`/`undefined`; all other non-node values are coerced to Text nodes.
|
|
391
|
+
|
|
392
|
+
Guide defaults for template and fragment usage:
|
|
393
|
+
|
|
394
|
+
- `addTemplate()` and `setTemplate()` always clone the input before appending; reuse the same template value safely.
|
|
395
|
+
- Prefer `setTemplate()` over `empty().addTemplate()` when replacing all content.
|
|
396
|
+
- Prefer `addChildMap()` or `setChildMap()` over manually building a fragment when rendering arrays.
|
|
397
|
+
- Use `JJDF.create()` when you need to assemble multiple sibling nodes before one insertion.
|
|
398
|
+
|
|
399
|
+
## Node Traversal
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
const parent = el.getParent() // wrapped parent or null (detached)
|
|
403
|
+
const children = el.getChildren() // wrapped child array (always an array)
|
|
404
|
+
el.rm() // detach from parent (no-op if already detached)
|
|
405
|
+
const ancestor = el.closest('[data-section]') // null if not found
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Resource Loaders
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
import { JJHE, fetchStyle, fetchTemplate } from 'jj'
|
|
412
|
+
|
|
413
|
+
const h = JJHE.tree
|
|
414
|
+
|
|
415
|
+
// Hint browser to preload early with native <link>
|
|
416
|
+
document.head.append(
|
|
417
|
+
h('link', {
|
|
418
|
+
href: import.meta.resolve('./bundle.js'),
|
|
419
|
+
rel: 'modulepreload',
|
|
420
|
+
}).ref,
|
|
421
|
+
)
|
|
422
|
+
document.head.append(
|
|
423
|
+
h('link', {
|
|
424
|
+
href: import.meta.resolve('./main.css'),
|
|
425
|
+
rel: 'preload',
|
|
426
|
+
as: 'style',
|
|
427
|
+
}).ref,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
// Load a CSSStyleSheet for adoptedStyleSheets or setShadow
|
|
431
|
+
const sheet = await fetchStyle(import.meta.resolve('./theme.css'))
|
|
432
|
+
document.adoptedStyleSheets = [sheet]
|
|
433
|
+
|
|
434
|
+
// Load a DocumentFragment for addTemplate / setShadow
|
|
435
|
+
const fragment = await fetchTemplate(import.meta.resolve('./dialog.html'))
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Guide defaults for browser-native loading hints:
|
|
439
|
+
|
|
440
|
+
- Use native `<link>` hints built with `JJHE.tree` and appended to `head` for `preload`, `prefetch`, and `modulepreload`.
|
|
441
|
+
- Keep the `as` value explicit for `preload` instead of inferring it from file extensions.
|
|
442
|
+
- Use `preload` for current-page needs, `prefetch` for probable future navigation, and `modulepreload` for module graphs you want fetched early.
|
|
443
|
+
|
|
444
|
+
## String Casing
|
|
445
|
+
|
|
446
|
+
String case-conversion helpers are internal implementation details.
|
|
447
|
+
Use higher-level public APIs like `attr2prop` and `defineComponent` instead of importing low-level casing utilities.
|
|
448
|
+
|
|
449
|
+
## Common mistakes
|
|
450
|
+
|
|
451
|
+
1. **`.ts` extension in imports** — TypeScript source must use `.js` (`import { X } from './X.js'`).
|
|
452
|
+
2. **`JJHE.create('svg')`** — throws; use `JJSE.create('svg')`.
|
|
453
|
+
3. **`el.setHTML(html)` without `true`** — throws when html is non-empty.
|
|
454
|
+
4. **Fetching template/style inside `connectedCallback`** — fetch at module scope so the network request is shared.
|
|
455
|
+
5. **Not awaiting `Element.defined`** — markup may be parsed before the element is defined, causing flaky upgrades.
|
|
456
|
+
6. **Breaking the chain with `.ref` unnecessarily** — use wrapper methods first; reach for `.ref` only when no wrapper method exists.
|
|
457
|
+
|
|
458
|
+
## Pitfall Prevention Rules (Component Work)
|
|
459
|
+
|
|
460
|
+
Apply these rules whenever building or refactoring JJ-based custom elements:
|
|
461
|
+
|
|
462
|
+
1. **Template-first component UI** — if component markup is static, use `fetchTemplate` + `setTemplate` (or shadow `init`) rather than building the UI imperatively in JS.
|
|
463
|
+
2. **Keep routing/URL state outside UI components** — query params and history updates belong in page/controller code, not in reusable visual components.
|
|
464
|
+
3. **Query with wrappers, not native DOM first** — prefer `find` / `findAll` on JJ wrappers before dropping to `.ref`.
|
|
465
|
+
4. **Do not unwrap and re-wrap** — avoid `find(...).ref` followed by `JJHE.from(...)`; keep the wrapper value.
|
|
466
|
+
5. **Use specific selectors for required nodes** — prefer selectors like `button#save` and `progress#step-progress` so selector intent replaces manual `instanceof` checks.
|
|
467
|
+
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.
|
|
468
|
+
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.
|
|
469
|
+
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).
|
|
470
|
+
|
|
471
|
+
## Reference Docs
|
|
472
|
+
|
|
473
|
+
For framework migration or deep-dive patterns, load these on demand:
|
|
474
|
+
|
|
475
|
+
- `references/react-to-jj-translation.md`
|
|
476
|
+
- `references/vue-to-jj-translation.md`
|
|
477
|
+
- `references/svelte-to-jj-translation.md`
|
|
478
|
+
- `references/angular-to-jj-translation.md`
|
|
479
|
+
- `references/jquery-to-jj-translation.md`
|
|
480
|
+
- `references/lit-to-jj-translation.md`
|
|
481
|
+
- `references/web-components-patterns.md`
|
|
482
|
+
- `references/eventing-patterns.md`
|
|
483
|
+
- `references/querying-patterns.md`
|
|
484
|
+
- `references/css-improvements.md`
|
|
485
|
+
- `references/testing-with-jsdom.md`
|
|
486
|
+
- `references/security-and-html.md`
|
|
487
|
+
- `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
|