micra.js 2.3.0 → 2.3.1
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/CHANGELOG.md +43 -0
- package/README.md +43 -0
- package/dist/core/reactive.d.ts +4 -0
- package/dist/dom/directives.d.ts +2 -2
- package/dist/dom/scan.d.ts +3 -3
- package/dist/micra.cjs.js +11 -12
- package/dist/micra.cjs.js.map +2 -2
- package/dist/micra.esm.js +11 -12
- package/dist/micra.esm.js.map +2 -2
- package/dist/micra.js +11 -12
- package/dist/micra.js.map +2 -2
- package/dist/micra.min.js +2 -2
- package/dist/types.d.ts +0 -2
- package/llms-full.txt +110 -15
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/src/core/mount.ts +1 -1
- package/src/core/reactive.ts +6 -1
- package/src/dom/directives.ts +1 -3
- package/src/dom/each.ts +3 -5
- package/src/dom/scan.ts +3 -3
- package/src/types.ts +0 -2
- package/src/dom/query.ts +0 -50
package/llms-full.txt
CHANGED
|
@@ -7,7 +7,7 @@ This file follows the llmstxt.org "expanded" convention: it inlines code recipes
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install micra.js@^2.3.
|
|
10
|
+
npm install micra.js@^2.3.1
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
```ts
|
|
@@ -17,7 +17,7 @@ import * as Micra from 'micra.js'
|
|
|
17
17
|
Or CDN (no build step):
|
|
18
18
|
|
|
19
19
|
```html
|
|
20
|
-
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.
|
|
20
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
This exposes a global `Micra` object.
|
|
@@ -146,7 +146,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
|
|
|
146
146
|
<button @click="inc">+</button>
|
|
147
147
|
</div>
|
|
148
148
|
|
|
149
|
-
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.
|
|
149
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
150
150
|
<script>
|
|
151
151
|
Micra.define('counter', {
|
|
152
152
|
state: { count: 0 },
|
|
@@ -190,7 +190,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
|
|
|
190
190
|
</footer>
|
|
191
191
|
</div>
|
|
192
192
|
|
|
193
|
-
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.
|
|
193
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
194
194
|
<script>
|
|
195
195
|
Micra.define('todo-app', {
|
|
196
196
|
state: {
|
|
@@ -262,7 +262,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
|
|
|
262
262
|
<p data-if="filtered().length === 0">No matches.</p>
|
|
263
263
|
</div>
|
|
264
264
|
|
|
265
|
-
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.
|
|
265
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
266
266
|
<script>
|
|
267
267
|
Micra.define('users-table', {
|
|
268
268
|
state: {
|
|
@@ -303,7 +303,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
|
|
|
303
303
|
<p data-if="success">Invitation sent ✓</p>
|
|
304
304
|
</form>
|
|
305
305
|
|
|
306
|
-
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.
|
|
306
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
307
307
|
<script>
|
|
308
308
|
Micra.define('invite-form', {
|
|
309
309
|
state: { email: '', loading: false, error: '', success: false },
|
|
@@ -345,7 +345,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
|
|
|
345
345
|
</div>
|
|
346
346
|
</div>
|
|
347
347
|
|
|
348
|
-
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.
|
|
348
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
349
349
|
<script>
|
|
350
350
|
Micra.define('open-modal-btn', {
|
|
351
351
|
open() {
|
|
@@ -394,7 +394,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
|
|
|
394
394
|
<section data-if="tab === 'security'">Security content</section>
|
|
395
395
|
</div>
|
|
396
396
|
|
|
397
|
-
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.
|
|
397
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
398
398
|
<script>
|
|
399
399
|
Micra.define('tabs', {
|
|
400
400
|
state: { tab: 'overview' },
|
|
@@ -414,7 +414,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
|
|
|
414
414
|
<button @click="upgrade" data-if="plan !== 'enterprise'">Upgrade</button>
|
|
415
415
|
</div>
|
|
416
416
|
|
|
417
|
-
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.
|
|
417
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
418
418
|
<script>
|
|
419
419
|
Micra.define('user-card', {
|
|
420
420
|
state: { name: '', plan: '' },
|
|
@@ -445,7 +445,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
|
|
|
445
445
|
<p data-if="!loading && results.length === 0 && query">No results.</p>
|
|
446
446
|
</div>
|
|
447
447
|
|
|
448
|
-
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.
|
|
448
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
449
449
|
<script>
|
|
450
450
|
Micra.define('search', {
|
|
451
451
|
state: { query: '', results: [], loading: false },
|
|
@@ -479,7 +479,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
|
|
|
479
479
|
<p data-if="loading">Loading chart…</p>
|
|
480
480
|
</div>
|
|
481
481
|
|
|
482
|
-
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.
|
|
482
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
483
483
|
<script>
|
|
484
484
|
Micra.define('revenue-chart', {
|
|
485
485
|
state: { loading: true },
|
|
@@ -511,7 +511,7 @@ Everything else (`window`, `document`, `fetch`, `eval`, `constructor`, `setTimeo
|
|
|
511
511
|
<p data-if="!loading && rows.length === 0">No results.</p>
|
|
512
512
|
</div>
|
|
513
513
|
|
|
514
|
-
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.
|
|
514
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
515
515
|
<script>
|
|
516
516
|
Micra.define('search-bar', {
|
|
517
517
|
state: { query: '' },
|
|
@@ -542,7 +542,7 @@ the same page. Twelve lines of glue, written once.
|
|
|
542
542
|
<main hx-get="/page/home" hx-trigger="load" hx-swap="innerHTML"></main>
|
|
543
543
|
|
|
544
544
|
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"></script>
|
|
545
|
-
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.
|
|
545
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
546
546
|
<script>
|
|
547
547
|
Micra.define('counter', {
|
|
548
548
|
state: { count: 0 },
|
|
@@ -586,6 +586,101 @@ Full reference with state-survival patterns, `hx-vals`/`hx-include`
|
|
|
586
586
|
bridging, and per-component loading state lives in
|
|
587
587
|
`docs/recipes/htmx.md`.
|
|
588
588
|
|
|
589
|
+
## Recipe 12 — Rails + Micra (importmap, CSRF, Turbo Drive)
|
|
590
|
+
|
|
591
|
+
Pin Micra in `config/importmap.rb`, boot it once in the layout — that's
|
|
592
|
+
the whole integration. CSRF works automatically (Micra reads
|
|
593
|
+
`<meta name="csrf-token">` that Rails already ships). Turbo Drive needs
|
|
594
|
+
a `turbo:load` mirror so the second navigation doesn't ghost.
|
|
595
|
+
|
|
596
|
+
```ruby
|
|
597
|
+
# config/importmap.rb
|
|
598
|
+
pin "micra",
|
|
599
|
+
to: "https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.esm.js",
|
|
600
|
+
preload: true
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
```erb
|
|
604
|
+
<%# app/views/layouts/application.html.erb — inside <head> %>
|
|
605
|
+
<%= csrf_meta_tags %>
|
|
606
|
+
<%= javascript_importmap_tags %>
|
|
607
|
+
<script type="module">
|
|
608
|
+
import * as Micra from "micra"
|
|
609
|
+
window.Micra = Micra
|
|
610
|
+
document.addEventListener("DOMContentLoaded", () => Micra.start())
|
|
611
|
+
document.addEventListener("turbo:load", () => Micra.start())
|
|
612
|
+
</script>
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
```erb
|
|
616
|
+
<%# app/views/tasks/index.html.erb %>
|
|
617
|
+
<section data-component="tasks-board"
|
|
618
|
+
data-initial-tasks='<%= @tasks.as_json(only: %i[id title done]).to_json %>'>
|
|
619
|
+
<form @submit.prevent="add">
|
|
620
|
+
<input data-model="draft" placeholder="New task…" />
|
|
621
|
+
<button data-bind="disabled:!draft.trim()">Add</button>
|
|
622
|
+
</form>
|
|
623
|
+
<template data-each="tasks" data-key="id">
|
|
624
|
+
<li data-class="done:item.done">
|
|
625
|
+
<input type="checkbox" data-bind="checked:item.done" @change="toggle" />
|
|
626
|
+
<span data-text="item.title"></span>
|
|
627
|
+
<button @click="remove" data-bind="data-id:item.id">×</button>
|
|
628
|
+
</li>
|
|
629
|
+
</template>
|
|
630
|
+
</section>
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
```js
|
|
634
|
+
// app/javascript/application.js
|
|
635
|
+
import * as Micra from "micra"
|
|
636
|
+
|
|
637
|
+
Micra.define("tasks-board", {
|
|
638
|
+
state: { tasks: [], draft: "" },
|
|
639
|
+
onCreate() { this.state.tasks = JSON.parse(this.prop("initialTasks") || "[]") },
|
|
640
|
+
async add() {
|
|
641
|
+
const task = await this.fetch("/tasks", { method: "POST", body: { title: this.state.draft.trim() } })
|
|
642
|
+
this.state.tasks = [...this.state.tasks, task]
|
|
643
|
+
this.state.draft = ""
|
|
644
|
+
},
|
|
645
|
+
async toggle(e) {
|
|
646
|
+
const id = Number(e.currentTarget.closest("li").querySelector("[data-id]").dataset.id)
|
|
647
|
+
const next = !this.state.tasks.find(t => t.id === id).done
|
|
648
|
+
const task = await this.fetch(`/tasks/${id}`, { method: "PATCH", body: { done: next } })
|
|
649
|
+
this.state.tasks = this.state.tasks.map(t => t.id === id ? task : t)
|
|
650
|
+
},
|
|
651
|
+
async remove(e) {
|
|
652
|
+
const id = Number(e.currentTarget.dataset.id)
|
|
653
|
+
await this.fetch(`/tasks/${id}`, { method: "DELETE" })
|
|
654
|
+
this.state.tasks = this.state.tasks.filter(t => t.id !== id)
|
|
655
|
+
},
|
|
656
|
+
})
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Rules of thumb:
|
|
660
|
+
|
|
661
|
+
- **CSRF is automatic** — keep `<%= csrf_meta_tags %>` in the layout.
|
|
662
|
+
`this.fetch()` sends `X-CSRF-Token` on every non-GET request.
|
|
663
|
+
- **Add `turbo:load`** alongside `DOMContentLoaded` if you have Turbo
|
|
664
|
+
Drive enabled (default since Rails 7). Without it, only the first
|
|
665
|
+
page load wires up.
|
|
666
|
+
- **JSON in `data-*` attributes does NOT auto-parse.** Non-primitive
|
|
667
|
+
props (`@tasks.as_json.to_json`) are strings client-side — call
|
|
668
|
+
`JSON.parse(this.prop('initialTasks'))` in `onCreate`. Primitives
|
|
669
|
+
(numbers, booleans) auto-cast through `this.prop()`.
|
|
670
|
+
- **Turbo Frames** that swap their own `innerHTML` and contain a
|
|
671
|
+
`[data-component]` will leave the cached scan pointing at gone DOM.
|
|
672
|
+
Same footgun as htmx — keep `data-component` outside the frame, or
|
|
673
|
+
destroy+remount on `turbo:frame-render`.
|
|
674
|
+
- The optional **`micra-rails`** gem adds an ERB helper
|
|
675
|
+
(`micra_component`) and a one-shot installer. Its importmap pin can lag
|
|
676
|
+
the latest Micra.js release between gem versions (override it in your
|
|
677
|
+
own `config/importmap.rb`), and the `micra_state.to_html` example in
|
|
678
|
+
its README doesn't compile — see `docs/recipes/rails.md` §2 for
|
|
679
|
+
workarounds.
|
|
680
|
+
|
|
681
|
+
Full reference with Turbo Streams cleanup, no-flicker hydration pattern,
|
|
682
|
+
and the Stimulus-vs-Micra split is in `docs/recipes/rails.md`.
|
|
683
|
+
|
|
589
684
|
---
|
|
590
685
|
|
|
591
686
|
# Anti-pattern reference (what LLMs gravitate to — DO NOT)
|
|
@@ -622,9 +717,9 @@ import { ref, computed } from 'vue'
|
|
|
622
717
|
import Alpine from 'alpinejs'
|
|
623
718
|
|
|
624
719
|
// ❌ unpkg CDN — blocked by Claude artifacts and most AI sandbox CSPs
|
|
625
|
-
<script src="https://unpkg.com/micra.js@2.3.
|
|
720
|
+
<script src="https://unpkg.com/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
626
721
|
// ✅ Use jsDelivr instead — it auto-mirrors npm and is CSP-allowlisted everywhere
|
|
627
|
-
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.
|
|
722
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
628
723
|
```
|
|
629
724
|
|
|
630
725
|
# Final checklist
|
package/llms.txt
CHANGED
|
@@ -25,7 +25,7 @@ import * as Micra from 'micra.js'
|
|
|
25
25
|
Or via CDN (no build step):
|
|
26
26
|
|
|
27
27
|
```html
|
|
28
|
-
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.
|
|
28
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.1/dist/micra.min.js"></script>
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
This exposes a global `Micra` object.
|
package/package.json
CHANGED
package/src/core/mount.ts
CHANGED
|
@@ -175,7 +175,7 @@ export function mount<S extends StateRecord, M>(
|
|
|
175
175
|
const mRoot = root as MicraElement;
|
|
176
176
|
const scan =
|
|
177
177
|
mRoot.__micraScan ?? (mRoot.__micraScan = scanComponent(root));
|
|
178
|
-
applyDirectives(scan, exprState, rawState
|
|
178
|
+
applyDirectives(scan, exprState, rawState);
|
|
179
179
|
renderList(scan.each, exprState, rawState, instance, triggerKey);
|
|
180
180
|
bindDataOn(scan.on, instance);
|
|
181
181
|
bindAtEvents(scan.atEvents, instance);
|
package/src/core/reactive.ts
CHANGED
|
@@ -36,6 +36,10 @@ export function createReactiveState<S extends StateRecord>(obj: S, schedule: ()
|
|
|
36
36
|
* Return a debounce function that defers `render` to the next microtask.
|
|
37
37
|
* Multiple calls within the same tick collapse to a single render.
|
|
38
38
|
*
|
|
39
|
+
* Uses `queueMicrotask` so each batch enqueues a single microtask instead of
|
|
40
|
+
* allocating a Promise + reaction job. `flush` is hoisted out of the hot path
|
|
41
|
+
* so it isn't re-created on every schedule() call.
|
|
42
|
+
*
|
|
39
43
|
* @example
|
|
40
44
|
* const schedule = createScheduler(render)
|
|
41
45
|
* schedule() // defers render
|
|
@@ -43,9 +47,10 @@ export function createReactiveState<S extends StateRecord>(obj: S, schedule: ()
|
|
|
43
47
|
*/
|
|
44
48
|
export function createScheduler(render: () => void): () => void {
|
|
45
49
|
let pending = false
|
|
50
|
+
const flush = () => { pending = false; render() }
|
|
46
51
|
return function schedule() {
|
|
47
52
|
if (pending) return
|
|
48
53
|
pending = true
|
|
49
|
-
|
|
54
|
+
queueMicrotask(flush)
|
|
50
55
|
}
|
|
51
56
|
}
|
package/src/dom/directives.ts
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
|
|
15
15
|
import type {
|
|
16
16
|
CachedIfBinding,
|
|
17
|
-
InternalInstance,
|
|
18
17
|
ScanIndex,
|
|
19
18
|
StateRecord,
|
|
20
19
|
} from '../types'
|
|
@@ -147,11 +146,10 @@ function applyModel(
|
|
|
147
146
|
* @param state - Expression state (may include item/index for each rows)
|
|
148
147
|
* @param rawState - Raw (non-proxy) state for model sync
|
|
149
148
|
*/
|
|
150
|
-
export function applyDirectives
|
|
149
|
+
export function applyDirectives(
|
|
151
150
|
scan: ScanIndex,
|
|
152
151
|
state: StateRecord,
|
|
153
152
|
rawState: StateRecord,
|
|
154
|
-
_instance: InternalInstance<S>,
|
|
155
153
|
): void {
|
|
156
154
|
// data-if runs first so subsequent directives don't write into a tree that's
|
|
157
155
|
// about to be detached this tick.
|
package/src/dom/each.ts
CHANGED
|
@@ -156,7 +156,6 @@ function renderKeyed<S extends StateRecord>(
|
|
|
156
156
|
|
|
157
157
|
if (!node) {
|
|
158
158
|
node = createRowNode(tmpl, state, instance)
|
|
159
|
-
node.__micraKey = key
|
|
160
159
|
keyMap.set(key, node)
|
|
161
160
|
} else if (canSkipUnchanged && node.__micraItem === item && node.__micraIndex === index) {
|
|
162
161
|
// Item reference and index are unchanged, and no other state key changed
|
|
@@ -177,7 +176,7 @@ function renderKeyed<S extends StateRecord>(
|
|
|
177
176
|
// Use the cached scan if present (created above on first sight of this key);
|
|
178
177
|
// older paths may pass a node we haven't scanned yet.
|
|
179
178
|
const rowScan = node.__micraScan ?? (node.__micraScan = scanComponent(node))
|
|
180
|
-
applyDirectives(rowScan, itemState, rawState
|
|
179
|
+
applyDirectives(rowScan, itemState, rawState)
|
|
181
180
|
nextNodes.push(node)
|
|
182
181
|
}
|
|
183
182
|
|
|
@@ -292,7 +291,7 @@ function renderNoKey<S extends StateRecord>(
|
|
|
292
291
|
itemState.item = item
|
|
293
292
|
itemState.index = i
|
|
294
293
|
itemState.$index = i
|
|
295
|
-
applyDirectives(node.__micraScan!, itemState, rawState
|
|
294
|
+
applyDirectives(node.__micraScan!, itemState, rawState)
|
|
296
295
|
nextList[i] = node
|
|
297
296
|
}
|
|
298
297
|
|
|
@@ -311,10 +310,9 @@ function renderNoKey<S extends StateRecord>(
|
|
|
311
310
|
itemState.item = item
|
|
312
311
|
itemState.index = i
|
|
313
312
|
itemState.$index = i
|
|
314
|
-
node.__micraEach = true
|
|
315
313
|
node.__micraItem = item
|
|
316
314
|
node.__micraIndex = i
|
|
317
|
-
applyDirectives(node.__micraScan!, itemState, rawState
|
|
315
|
+
applyDirectives(node.__micraScan!, itemState, rawState)
|
|
318
316
|
nextList[i] = node
|
|
319
317
|
frag.append(node)
|
|
320
318
|
}
|
package/src/dom/scan.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* traversal that classifies every directive attribute in a single visit.
|
|
6
6
|
*
|
|
7
7
|
* Boundaries:
|
|
8
|
-
* - REJECT (skip subtree) on nested [data-component] —
|
|
9
|
-
*
|
|
10
|
-
* even *visit* those nodes.
|
|
8
|
+
* - REJECT (skip subtree) on nested [data-component] — a parent component
|
|
9
|
+
* never processes directives owned by a nested child. Applied during the
|
|
10
|
+
* walk so we don't even *visit* those nodes.
|
|
11
11
|
* - <template> contents are not visited (browser TreeWalker default).
|
|
12
12
|
* `<template data-each>` itself IS visited and classified into scan.each;
|
|
13
13
|
* its children are processed by each.ts on every render — fresh rows
|
package/src/types.ts
CHANGED
|
@@ -199,8 +199,6 @@ export interface MicraElement extends HTMLElement {
|
|
|
199
199
|
__micraModel?: true // data-model listener bound
|
|
200
200
|
__micraEvents?: true // data-on listeners bound
|
|
201
201
|
__micraAtBound?: true // @event shorthand bound (per-element)
|
|
202
|
-
__micraKey?: unknown // keyed-diff key
|
|
203
|
-
__micraEach?: true // belongs to a no-key each list
|
|
204
202
|
__micraScan?: ScanIndex // single-pass scan result (cached after 1st render)
|
|
205
203
|
__micraItem?: StateRecord // keyed row: last-rendered item ref (for skip check)
|
|
206
204
|
__micraIndex?: number // keyed row: last-rendered index (for skip check)
|
package/src/dom/query.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* src/dom/query.ts — DOM query helpers.
|
|
3
|
-
*
|
|
4
|
-
* LLM NOTE: These are utility functions with no side effects.
|
|
5
|
-
* queryOwn is the critical function that prevents a parent component from
|
|
6
|
-
* accidentally processing directives belonging to a nested child component.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* querySelectorAll wrapper — returns a typed array.
|
|
11
|
-
*/
|
|
12
|
-
export function queryAll(root: ParentNode, sel: string): Element[] {
|
|
13
|
-
return Array.from(root.querySelectorAll(sel))
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Like querySelectorAll, but EXCLUDES elements that live inside a nested
|
|
18
|
-
* `[data-component]` subtree.
|
|
19
|
-
*
|
|
20
|
-
* This is what prevents a parent component's render() from clobbering
|
|
21
|
-
* the DOM managed by a child component.
|
|
22
|
-
*
|
|
23
|
-
* LLM NOTE: The walk goes up parentElement until it hits `root` or null.
|
|
24
|
-
* If any ancestor (between el and root) has data-component, the element is
|
|
25
|
-
* owned by that nested component, not by root's component — so we skip it.
|
|
26
|
-
*/
|
|
27
|
-
export function queryOwn(root: Element, attr: string): Element[] {
|
|
28
|
-
return filterOwn(root, queryAll(root, `[${attr}]`))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Like queryOwn but accepts an arbitrary CSS selector. Used by bindAtEvents
|
|
33
|
-
* which scans `*` for `@`-prefixed attribute names (no attribute selector exists
|
|
34
|
-
* for those).
|
|
35
|
-
*/
|
|
36
|
-
export function queryOwnAll(root: Element, sel: string): Element[] {
|
|
37
|
-
return filterOwn(root, queryAll(root, sel))
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** @internal Shared subtree-ownership filter. */
|
|
41
|
-
function filterOwn(root: Element, els: Element[]): Element[] {
|
|
42
|
-
return els.filter(el => {
|
|
43
|
-
let node: Element | null = el.parentElement
|
|
44
|
-
while (node && node !== root) {
|
|
45
|
-
if (node.hasAttribute('data-component')) return false
|
|
46
|
-
node = node.parentElement
|
|
47
|
-
}
|
|
48
|
-
return true
|
|
49
|
-
})
|
|
50
|
-
}
|