micra.js 2.3.0 → 2.3.2

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/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.0
10
+ npm install micra.js@^2.3.2
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.0/dist/micra.min.js"></script>
20
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/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.0/dist/micra.min.js"></script>
149
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/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.0/dist/micra.min.js"></script>
193
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/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.0/dist/micra.min.js"></script>
265
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/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.0/dist/micra.min.js"></script>
306
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/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.0/dist/micra.min.js"></script>
348
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/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.0/dist/micra.min.js"></script>
397
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/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.0/dist/micra.min.js"></script>
417
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/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.0/dist/micra.min.js"></script>
448
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/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.0/dist/micra.min.js"></script>
482
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/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.0/dist/micra.min.js"></script>
514
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/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.0/dist/micra.min.js"></script>
545
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/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.2/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.0/dist/micra.min.js"></script>
720
+ <script src="https://unpkg.com/micra.js@2.3.2/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.0/dist/micra.min.js"></script>
722
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/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.0/dist/micra.min.js"></script>
28
+ <script src="https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.min.js"></script>
29
29
  ```
30
30
 
31
31
  This exposes a global `Micra` object.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micra.js",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "Lightweight reactive UI framework for server-rendered pages — reactive state, directives, event bus. < 5 KB gzip.",
5
5
  "type": "module",
6
6
  "main": "./dist/micra.cjs.js",
@@ -30,7 +30,7 @@
30
30
  "test": "vitest run",
31
31
  "test:watch": "vitest",
32
32
  "test:coverage": "vitest run --coverage",
33
- "docs:sync": "rm -rf site/dist && mkdir -p site/dist && cp -R dist/* site/dist/",
33
+ "docs:sync": "rm -rf site/dist && mkdir -p site/dist && cp -R dist/* site/dist/ && cp llms.txt llms-full.txt site/",
34
34
  "docs:build": "npm run build && npm run docs:sync",
35
35
  "docs:dev": "npm run docs:sync && npx serve -p 4321 site"
36
36
  },
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, instance);
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);
@@ -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
- Promise.resolve().then(() => { pending = false; render() })
54
+ queueMicrotask(flush)
50
55
  }
51
56
  }
@@ -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<S extends StateRecord>(
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
@@ -106,8 +106,25 @@ function createRowNode<S extends StateRecord>(
106
106
  ): MicraElement {
107
107
  const frag = tmpl.content.cloneNode(true) as DocumentFragment
108
108
  let node: MicraElement
109
- if (frag.childNodes.length === 1) {
110
- node = frag.firstElementChild as MicraElement
109
+ // Single-root detection must ignore whitespace-only text nodes — a
110
+ // pretty-printed `<template>\n <tr>…</tr>\n</template>` is still one root.
111
+ // Wrapping a lone <tr> in <micra-each-item> would put invalid content
112
+ // inside <tbody> and break `tbody > tr` selectors. Only TOP-LEVEL child
113
+ // nodes are scanned (O(1-ish), not O(subtree)); NBSP counts as meaningful
114
+ // (it renders), so it keeps the wrapper. Comments beside the root are
115
+ // dropped — they don't render and aren't worth a wrapper in <tbody>.
116
+ const first = frag.firstElementChild as MicraElement | null
117
+ // meaningful = any char with code > 32 (NBSP included; \t \n \f \r and
118
+ // space excluded) in a top-level text node
119
+ const single =
120
+ !!first &&
121
+ !first.nextElementSibling &&
122
+ !Array.prototype.some.call(
123
+ frag.childNodes,
124
+ (c: Node) => c.nodeType === 3 && /[^\x00- ]/.test(c.textContent!),
125
+ )
126
+ if (single) {
127
+ node = first!
111
128
  } else {
112
129
  node = document.createElement('micra-each-item') as MicraElement
113
130
  node.style.display = 'contents'
@@ -156,7 +173,6 @@ function renderKeyed<S extends StateRecord>(
156
173
 
157
174
  if (!node) {
158
175
  node = createRowNode(tmpl, state, instance)
159
- node.__micraKey = key
160
176
  keyMap.set(key, node)
161
177
  } else if (canSkipUnchanged && node.__micraItem === item && node.__micraIndex === index) {
162
178
  // Item reference and index are unchanged, and no other state key changed
@@ -177,7 +193,7 @@ function renderKeyed<S extends StateRecord>(
177
193
  // Use the cached scan if present (created above on first sight of this key);
178
194
  // older paths may pass a node we haven't scanned yet.
179
195
  const rowScan = node.__micraScan ?? (node.__micraScan = scanComponent(node))
180
- applyDirectives(rowScan, itemState, rawState, instance)
196
+ applyDirectives(rowScan, itemState, rawState)
181
197
  nextNodes.push(node)
182
198
  }
183
199
 
@@ -292,7 +308,7 @@ function renderNoKey<S extends StateRecord>(
292
308
  itemState.item = item
293
309
  itemState.index = i
294
310
  itemState.$index = i
295
- applyDirectives(node.__micraScan!, itemState, rawState, instance)
311
+ applyDirectives(node.__micraScan!, itemState, rawState)
296
312
  nextList[i] = node
297
313
  }
298
314
 
@@ -311,10 +327,9 @@ function renderNoKey<S extends StateRecord>(
311
327
  itemState.item = item
312
328
  itemState.index = i
313
329
  itemState.$index = i
314
- node.__micraEach = true
315
330
  node.__micraItem = item
316
331
  node.__micraIndex = i
317
- applyDirectives(node.__micraScan!, itemState, rawState, instance)
332
+ applyDirectives(node.__micraScan!, itemState, rawState)
318
333
  nextList[i] = node
319
334
  frag.append(node)
320
335
  }
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] — same semantics as
9
- * the old `filterOwn` helper, but applied during the walk so we don't
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/utils/expr.ts CHANGED
@@ -52,10 +52,9 @@ const SIMPLE_PATH = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/
52
52
  * Globals reachable from directive expressions. Anything else (window, fetch,
53
53
  * constructor, eval, ...) is shadowed by SAFE_OUTER and resolves to undefined.
54
54
  */
55
- const ALLOWED_GLOBALS = new Set<string>([
56
- 'Math', 'JSON', 'Date', 'String', 'Number', 'Boolean', 'Array', 'Object',
57
- 'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'NaN', 'Infinity', 'undefined',
58
- ])
55
+ const ALLOWED_GLOBALS = new Set<string>(
56
+ 'Math,JSON,Date,String,Number,Boolean,Array,Object,parseInt,parseFloat,isNaN,isFinite,NaN,Infinity,undefined'.split(','),
57
+ )
59
58
 
60
59
  /**
61
60
  * Outer `with()` scope. Its `has` trap claims every non-whitelisted identifier
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
- }