micra.js 2.2.1 → 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/src/dom/scan.ts CHANGED
@@ -5,12 +5,13 @@
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
- * its children are processed by each.ts on every render via scanFragment.
13
+ * its children are processed by each.ts on every render fresh rows
14
+ * are wrapped in a per-row element and scanned via scanComponent.
14
15
  *
15
16
  * Hot-path notes:
16
17
  * - We read `el.attributes` once and switch by suffix. No allocations per
@@ -166,24 +167,3 @@ export function scanComponent(root: Element): ScanIndex {
166
167
  return scan;
167
168
  }
168
169
 
169
- /**
170
- * Scan a DocumentFragment (no-key each clone). Not cached — these fragments
171
- * are temporary and re-cloned every render.
172
- */
173
- export function scanFragment(frag: DocumentFragment): ScanIndex {
174
- const scan = emptyScan();
175
-
176
- const walker = document.createTreeWalker(
177
- frag,
178
- NodeFilter.SHOW_ELEMENT,
179
- NESTED_COMPONENT_FILTER,
180
- );
181
-
182
- let node: Element | null = walker.nextNode() as Element | null;
183
- while (node) {
184
- classify(node, scan);
185
- node = walker.nextNode() as Element | null;
186
- }
187
-
188
- return scan;
189
- }
package/src/index.ts CHANGED
@@ -27,6 +27,9 @@ export type {
27
27
  StateRecord,
28
28
  UnsubFn,
29
29
  EventHandler,
30
+ EventPayload,
31
+ EmitArgs,
32
+ MicraEvents,
30
33
  FetchOptions,
31
34
  ComponentMethods,
32
35
  ComponentBuiltins,
package/src/types.ts CHANGED
@@ -22,6 +22,53 @@ export type UnsubFn = () => void
22
22
  /** Event bus handler. Generic `T` types the payload. */
23
23
  export type EventHandler<T = unknown> = (payload: T) => void
24
24
 
25
+ /**
26
+ * Type-safe event bus registry. Empty by default — augment it via
27
+ * declaration merging to type your application's events.
28
+ *
29
+ * @example
30
+ * declare module 'micra.js' {
31
+ * interface MicraEvents {
32
+ * 'cart:updated': { count: number }
33
+ * 'user:login': { id: number; name: string }
34
+ * 'modal:close': void
35
+ * }
36
+ * }
37
+ *
38
+ * Micra.emit('cart:updated', { count: 3 }) // ✓ typed
39
+ * Micra.emit('cart:updated', { count: '3' }) // ✗ type error
40
+ * Micra.on('user:login', user => user.id) // user: { id, name }
41
+ *
42
+ * Events not present in the interface fall back to `unknown` payload —
43
+ * fully backward-compatible with untyped usage.
44
+ */
45
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
46
+ export interface MicraEvents {}
47
+
48
+ /**
49
+ * Resolves the payload type for an event key. For keys registered in
50
+ * `MicraEvents` returns the declared payload; for any other string key
51
+ * returns `unknown` (preserving backward compatibility).
52
+ */
53
+ export type EventPayload<K extends string> = K extends keyof MicraEvents
54
+ ? MicraEvents[K]
55
+ : unknown
56
+
57
+ /**
58
+ * Tuple of arguments passed to `emit` after the event name. When the
59
+ * payload type for a known event includes `undefined` (or the payload is
60
+ * declared as `void`), the argument is optional. For known events with a
61
+ * required payload, the argument is required. Unknown events accept any
62
+ * optional payload (backward compat).
63
+ */
64
+ export type EmitArgs<K extends string> = K extends keyof MicraEvents
65
+ ? [MicraEvents[K]] extends [void]
66
+ ? [payload?: undefined]
67
+ : undefined extends MicraEvents[K]
68
+ ? [payload?: MicraEvents[K]]
69
+ : [payload: MicraEvents[K]]
70
+ : [payload?: unknown]
71
+
25
72
  /** Options for `this.fetch()`. For GET/HEAD extra keys become query params. */
26
73
  export interface FetchOptions {
27
74
  method?: string
@@ -71,10 +118,16 @@ export interface ComponentBuiltins<S extends StateRecord = StateRecord> {
71
118
  prop<T>(name: string, defaultVal: T): T
72
119
  /** Fetch helper: CSRF header, JSON body, query params, typed errors. */
73
120
  fetch(url: string, options?: FetchOptions): Promise<unknown>
74
- /** Publish an event on the global bus. */
75
- emit(event: string, payload?: unknown): void
76
- /** Subscribe to the global bus. Subscription is auto-removed on destroy(). */
77
- on<T = unknown>(event: string, handler: EventHandler<T>): UnsubFn
121
+ /**
122
+ * Publish an event on the global bus.
123
+ * Payload is typed via the `MicraEvents` interface (augmentable).
124
+ */
125
+ emit<K extends string>(event: K, ...args: EmitArgs<K>): void
126
+ /**
127
+ * Subscribe to the global bus. Subscription is auto-removed on destroy().
128
+ * Handler payload is typed via the `MicraEvents` interface (augmentable).
129
+ */
130
+ on<K extends string>(event: K, handler: (payload: EventPayload<K>) => void): UnsubFn
78
131
  }
79
132
 
80
133
  /**
@@ -146,8 +199,6 @@ export interface MicraElement extends HTMLElement {
146
199
  __micraModel?: true // data-model listener bound
147
200
  __micraEvents?: true // data-on listeners bound
148
201
  __micraAtBound?: true // @event shorthand bound (per-element)
149
- __micraKey?: unknown // keyed-diff key
150
- __micraEach?: true // belongs to a no-key each list
151
202
  __micraScan?: ScanIndex // single-pass scan result (cached after 1st render)
152
203
  __micraItem?: StateRecord // keyed row: last-rendered item ref (for skip check)
153
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
- }