micra.js 2.1.0 → 2.2.0
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 +231 -0
- package/README.md +39 -14
- package/dist/core/mount.d.ts +8 -2
- package/dist/core/registry.d.ts +13 -4
- package/dist/dom/directives.d.ts +11 -15
- package/dist/dom/each.d.ts +6 -4
- package/dist/dom/events.d.ts +15 -5
- package/dist/dom/refs.d.ts +5 -5
- package/dist/dom/scan.d.ts +34 -0
- package/dist/index.d.ts +1 -1
- package/dist/micra.cjs.js +196 -143
- package/dist/micra.cjs.js.map +4 -4
- package/dist/micra.esm.js +196 -143
- package/dist/micra.esm.js.map +4 -4
- package/dist/micra.js +196 -143
- package/dist/micra.js.map +4 -4
- package/dist/micra.min.js +2 -2
- package/dist/types.d.ts +63 -21
- package/llms-full.txt +600 -0
- package/llms.txt +148 -0
- package/package.json +11 -3
- package/src/core/mount.ts +126 -98
- package/src/core/registry.ts +19 -9
- package/src/dom/directives.ts +34 -120
- package/src/dom/each.ts +36 -19
- package/src/dom/events.ts +23 -31
- package/src/dom/refs.ts +6 -7
- package/src/dom/scan.ts +189 -0
- package/src/index.ts +2 -0
- package/src/types.ts +76 -21
package/llms.txt
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Micra.js
|
|
2
|
+
|
|
3
|
+
> Lightweight reactive TypeScript framework for server-rendered apps and small SaaS frontends. ~5 KB gzip. No build step required.
|
|
4
|
+
>
|
|
5
|
+
> **For LLM code generation:** read the expanded version at https://github.com/denisfl/micra.js/blob/master/llms-full.txt — it includes 10 full inline recipes and an anti-pattern reference. The recipes are the canonical answers to common requests like "build a todo app" or "build a search-with-debounce".
|
|
6
|
+
|
|
7
|
+
## When to use Micra.js instead of React/Vue
|
|
8
|
+
|
|
9
|
+
- You have a server-rendered page (Laravel, Rails, Django, etc.) and need a small amount of reactivity
|
|
10
|
+
- Bundle size matters (~5 KB gzip vs ~45 KB React)
|
|
11
|
+
- You don't need a full SPA or client-side routing
|
|
12
|
+
- You want to drop a `<script>` tag and go
|
|
13
|
+
- You need reactive directives on existing HTML without rewriting templates
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install micra.js
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import * as Micra from 'micra.js'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or via CDN (no build step):
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js@2.2.0/dist/micra.min.js"></script>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This exposes a global `Micra` object.
|
|
32
|
+
|
|
33
|
+
> **CDN choice matters for AI sandboxes.** Use `cdn.jsdelivr.net` — it is in the
|
|
34
|
+
> Content Security Policy allowlist of Claude artifacts, ChatGPT canvas, and most
|
|
35
|
+
> other AI runtime environments. `unpkg.com` is NOT in those allowlists and will
|
|
36
|
+
> fail to load. jsDelivr auto-mirrors every npm package.
|
|
37
|
+
|
|
38
|
+
## Core pattern
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<div data-component="counter">
|
|
42
|
+
<span data-text="count"></span>
|
|
43
|
+
<button @click="increment">+</button>
|
|
44
|
+
</div>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
Micra.define('counter', {
|
|
49
|
+
state: { count: 0 },
|
|
50
|
+
increment() { this.state.count++ },
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
Micra.start()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Directives
|
|
57
|
+
|
|
58
|
+
| Directive | Example | Description |
|
|
59
|
+
|------------------|--------------------------------------|--------------------------------|
|
|
60
|
+
| `data-text` | `data-text="name"` | Set `textContent` |
|
|
61
|
+
| `data-html` | `data-html="content"` | Set `innerHTML` |
|
|
62
|
+
| `data-if` | `data-if="count > 0"` | Mount/**unmount** from DOM |
|
|
63
|
+
| `data-show` | `data-show="loaded"` | Toggle `style.display` only |
|
|
64
|
+
| `data-bind` | `data-bind="href:url, disabled:loading"` | Bind attributes |
|
|
65
|
+
| `data-model` | `data-model="search"` | Two-way input binding |
|
|
66
|
+
| `data-each` | `data-each="items" data-key="id"` | Keyed list rendering |
|
|
67
|
+
| `data-ref` | `data-ref="chart"` | DOM ref via `this.refs` |
|
|
68
|
+
| `data-class` | `data-class="active:isActive"` | Additive class toggling |
|
|
69
|
+
| `data-on` | `data-on="click:save"` | Bind DOM events |
|
|
70
|
+
| `@event` | `@click="increment"` | Shorthand event binding |
|
|
71
|
+
|
|
72
|
+
## API
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// Register & mount
|
|
76
|
+
Micra.define(name, definition)
|
|
77
|
+
Micra.mount(selector, definition)
|
|
78
|
+
Micra.start(root?) // scans DOM for [data-component] and mounts all
|
|
79
|
+
|
|
80
|
+
// Event bus
|
|
81
|
+
Micra.on(event, handler)
|
|
82
|
+
Micra.emit(event, payload?)
|
|
83
|
+
Micra.off(event, handler)
|
|
84
|
+
|
|
85
|
+
// DevTools
|
|
86
|
+
Micra.instances() // ReadonlyMap of live components
|
|
87
|
+
Micra.registry() // ReadonlyMap of registered definitions
|
|
88
|
+
Micra.debug() // prints all live components to console
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Inside component methods, `this` exposes:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
this.$el // root HTMLElement
|
|
95
|
+
this.state // reactive state proxy
|
|
96
|
+
this.refs // { [name]: HTMLElement } from data-ref
|
|
97
|
+
this.render() // force re-render
|
|
98
|
+
this.destroy() // unmount
|
|
99
|
+
this.prop(name, default?) // read data-* attributes (SSR props)
|
|
100
|
+
this.fetch(url, options?) // fetch helper
|
|
101
|
+
this.emit(event, payload?) // component-scoped event emit
|
|
102
|
+
this.on(event, handler) // component-scoped event subscribe
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Lifecycle hooks
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
Micra.define('app', {
|
|
109
|
+
state: {},
|
|
110
|
+
onCreate() { /* mounted, refs available */ },
|
|
111
|
+
onDestroy() { /* cleanup */ },
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Rules for code generation
|
|
116
|
+
|
|
117
|
+
**Rule 0 — Artifact type override.** When working inside Claude artifacts, ChatGPT
|
|
118
|
+
canvas, v0, Replit AI, or similar "framework-aware" runtimes: those tools default
|
|
119
|
+
to React. When Micra.js is the requested library, **switch the artifact/canvas type
|
|
120
|
+
to plain HTML** (Claude: `application/vnd.ant.html`). Do NOT translate Micra recipes
|
|
121
|
+
into React "because the artifact is React" — change the artifact type instead.
|
|
122
|
+
Micra is a standalone-HTML library; React artifacts cannot host it.
|
|
123
|
+
|
|
124
|
+
When generating Micra.js code, DO NOT:
|
|
125
|
+
|
|
126
|
+
1. Use `getElementById`/`querySelector`/`innerHTML` for component output. Lists go through `<template data-each>` with `data-key`.
|
|
127
|
+
2. Store derived values (counts, totals, filtered subsets) as state fields. They must be methods called from directives.
|
|
128
|
+
3. Use `addEventListener` inside methods — it leaks past `destroy()`. Use `@event` / `data-on`.
|
|
129
|
+
4. Call `this.renderList()` / `this.refresh()` / `this.update()` after mutations — Micra batches a microtask render.
|
|
130
|
+
5. Write `state.user.name = x` — shallow proxy. Replace top-level: `state.user = {...state.user, name: x}`.
|
|
131
|
+
6. Use `@keydown.enter` — branch on `e.key === 'Enter'` in the method.
|
|
132
|
+
7. Use `data-model="filters.search"` — writes literal flat key, not nested. Keep models top-level.
|
|
133
|
+
|
|
134
|
+
Full anti-pattern list with side-by-side examples: [docs/llm-guide.md](https://github.com/denisfl/micra.js/blob/master/docs/llm-guide.md#anti-patterns-llms-gravitate-to-do-not)
|
|
135
|
+
|
|
136
|
+
## Docs
|
|
137
|
+
|
|
138
|
+
- LLM expanded context (read first for code generation): https://github.com/denisfl/micra.js/blob/master/llms-full.txt
|
|
139
|
+
- Full LLM guide: https://github.com/denisfl/micra.js/blob/master/docs/llm-guide.md
|
|
140
|
+
- Recipes (canonical full-app examples): https://github.com/denisfl/micra.js/tree/master/docs/recipes
|
|
141
|
+
- Live demo docs: https://denisfl.github.io/micra.js/
|
|
142
|
+
- Getting started: https://github.com/denisfl/micra.js/blob/master/docs/getting-started.md
|
|
143
|
+
- Directives: https://github.com/denisfl/micra.js/blob/master/docs/directives.md
|
|
144
|
+
- API reference: https://github.com/denisfl/micra.js/blob/master/docs/api-reference.md
|
|
145
|
+
- Examples: https://github.com/denisfl/micra.js/blob/master/docs/examples.md
|
|
146
|
+
- SSR: https://github.com/denisfl/micra.js/blob/master/docs/ssr.md
|
|
147
|
+
- npm: https://www.npmjs.com/package/micra.js
|
|
148
|
+
- GitHub: https://github.com/denisfl/micra.js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "micra.js",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
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",
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
|
-
"src"
|
|
19
|
+
"src",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"CHANGELOG.md",
|
|
23
|
+
"llms.txt",
|
|
24
|
+
"llms-full.txt"
|
|
20
25
|
],
|
|
21
26
|
"scripts": {
|
|
22
27
|
"build": "node build.mjs",
|
|
@@ -24,7 +29,10 @@
|
|
|
24
29
|
"dev": "node build.mjs --watch",
|
|
25
30
|
"test": "vitest run",
|
|
26
31
|
"test:watch": "vitest",
|
|
27
|
-
"test:coverage": "vitest run --coverage"
|
|
32
|
+
"test:coverage": "vitest run --coverage",
|
|
33
|
+
"docs:sync": "rm -rf site/dist && mkdir -p site/dist && cp -R dist/* site/dist/",
|
|
34
|
+
"docs:build": "npm run build && npm run docs:sync",
|
|
35
|
+
"docs:dev": "npm run docs:sync && npx serve -p 4321 site"
|
|
28
36
|
},
|
|
29
37
|
"devDependencies": {
|
|
30
38
|
"@vitest/coverage-v8": "^4.1.7",
|
package/src/core/mount.ts
CHANGED
|
@@ -15,21 +15,23 @@
|
|
|
15
15
|
import type {
|
|
16
16
|
ComponentDefinition,
|
|
17
17
|
ComponentInstance,
|
|
18
|
+
ComponentMethods,
|
|
18
19
|
EventHandler,
|
|
19
20
|
InternalInstance,
|
|
20
21
|
MicraElement,
|
|
21
22
|
StateRecord,
|
|
22
23
|
UnsubFn,
|
|
23
|
-
} from
|
|
24
|
-
import { warn } from
|
|
25
|
-
import { micraFetch } from
|
|
26
|
-
import { on as busOn, emit as busEmit } from
|
|
27
|
-
import { createReactiveState, createScheduler } from
|
|
28
|
-
import { applyDirectives, validateDirectives } from
|
|
29
|
-
import { renderList } from
|
|
30
|
-
import { bindDataOn, bindAtEvents, bindModels } from
|
|
31
|
-
import { collectRefs } from
|
|
32
|
-
import {
|
|
24
|
+
} from "../types";
|
|
25
|
+
import { warn } from "../utils/expr";
|
|
26
|
+
import { micraFetch } from "../utils/fetch";
|
|
27
|
+
import { on as busOn, emit as busEmit } from "../core/bus";
|
|
28
|
+
import { createReactiveState, createScheduler } from "../core/reactive";
|
|
29
|
+
import { applyDirectives, validateDirectives } from "../dom/directives";
|
|
30
|
+
import { renderList } from "../dom/each";
|
|
31
|
+
import { bindDataOn, bindAtEvents, bindModels } from "../dom/events";
|
|
32
|
+
import { collectRefs } from "../dom/refs";
|
|
33
|
+
import { scanComponent } from "../dom/scan";
|
|
34
|
+
import { _instances } from "../core/registry";
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* Mount a component definition onto a DOM element.
|
|
@@ -37,64 +39,76 @@ import { _instances } from '../core/registry'
|
|
|
37
39
|
*
|
|
38
40
|
* Already-mounted elements return the existing instance.
|
|
39
41
|
*
|
|
42
|
+
* Both `S` (state) and `M` (methods) are inferred from the literal — the
|
|
43
|
+
* returned instance is fully typed: `inst.state.X` and `inst.someMethod()`
|
|
44
|
+
* are checked.
|
|
45
|
+
*
|
|
40
46
|
* @example
|
|
41
47
|
* const instance = Micra.mount('#counter', {
|
|
42
48
|
* state: { count: 0 },
|
|
43
49
|
* inc() { this.state.count++ },
|
|
44
50
|
* })
|
|
51
|
+
* instance?.inc()
|
|
52
|
+
* instance?.state.count
|
|
45
53
|
*/
|
|
46
|
-
export function mount<S extends StateRecord>(
|
|
54
|
+
export function mount<S extends StateRecord, M>(
|
|
47
55
|
selector: string | HTMLElement,
|
|
48
|
-
definition: ComponentDefinition<S>,
|
|
49
|
-
): ComponentInstance<S> | null {
|
|
56
|
+
definition: ComponentDefinition<S, M>,
|
|
57
|
+
): ComponentInstance<S, M> | null {
|
|
50
58
|
const root =
|
|
51
|
-
typeof selector ===
|
|
59
|
+
typeof selector === "string"
|
|
52
60
|
? document.querySelector<HTMLElement>(selector)
|
|
53
|
-
: selector
|
|
61
|
+
: selector;
|
|
54
62
|
|
|
55
63
|
if (!root) {
|
|
56
|
-
warn(`"${selector}" not found`)
|
|
57
|
-
return null
|
|
64
|
+
warn(`"${selector}" not found`);
|
|
65
|
+
return null;
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
// Already mounted — return existing instance without re-mounting
|
|
61
|
-
if (_instances.has(root))
|
|
69
|
+
if (_instances.has(root))
|
|
70
|
+
return _instances.get(root) as unknown as ComponentInstance<S, M>;
|
|
62
71
|
|
|
63
|
-
const rawState: StateRecord = { ...(definition.state ?? {}) }
|
|
64
|
-
const instance = { $el: root, refs: {} } as InternalInstance<S
|
|
72
|
+
const rawState: StateRecord = { ...(definition.state ?? {}) };
|
|
73
|
+
const instance = { $el: root, refs: {} } as InternalInstance<S>;
|
|
65
74
|
|
|
66
75
|
// Copy user-defined methods from definition to instance
|
|
67
|
-
for (const [key, val] of Object.entries(
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
for (const [key, val] of Object.entries(
|
|
77
|
+
definition as Record<string, unknown>,
|
|
78
|
+
)) {
|
|
79
|
+
if (key === "state" || key === "onCreate" || key === "onDestroy") continue;
|
|
80
|
+
if (typeof val === "function") instance[key] = val;
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
// ── prop() ────────────────────────────────────────────────────────────────
|
|
73
84
|
// Read data-* attributes from the root element with auto-cast.
|
|
74
85
|
instance.prop = function <T>(name: string, defaultVal?: T): T | undefined {
|
|
75
|
-
const val = root.dataset[name]
|
|
76
|
-
if (val === undefined) return defaultVal
|
|
77
|
-
if (val ===
|
|
78
|
-
if (val ===
|
|
79
|
-
if (val !==
|
|
80
|
-
return val as unknown as T
|
|
81
|
-
} as ComponentInstance<S>[
|
|
86
|
+
const val = root.dataset[name];
|
|
87
|
+
if (val === undefined) return defaultVal;
|
|
88
|
+
if (val === "true") return true as unknown as T;
|
|
89
|
+
if (val === "false") return false as unknown as T;
|
|
90
|
+
if (val !== "" && !isNaN(Number(val))) return Number(val) as unknown as T;
|
|
91
|
+
return val as unknown as T;
|
|
92
|
+
} as ComponentInstance<S>["prop"];
|
|
82
93
|
|
|
83
94
|
// ── fetch(), emit(), on() ─────────────────────────────────────────────────
|
|
84
|
-
instance.fetch = micraFetch
|
|
85
|
-
instance.emit
|
|
86
|
-
|
|
87
|
-
instance.on = <T = unknown>(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
instance.fetch = micraFetch;
|
|
96
|
+
instance.emit = busEmit;
|
|
97
|
+
|
|
98
|
+
instance.on = <T = unknown>(
|
|
99
|
+
event: string,
|
|
100
|
+
handler: EventHandler<T>,
|
|
101
|
+
): UnsubFn => {
|
|
102
|
+
const unsub = busOn(event, handler);
|
|
103
|
+
if (!instance.__micraSubs) instance.__micraSubs = [];
|
|
104
|
+
instance.__micraSubs.push(unsub);
|
|
105
|
+
return unsub;
|
|
106
|
+
};
|
|
93
107
|
|
|
94
108
|
// ── Render ────────────────────────────────────────────────────────────────
|
|
95
|
-
let isRendering = false
|
|
96
|
-
const schedule
|
|
97
|
-
instance.state
|
|
109
|
+
let isRendering = false;
|
|
110
|
+
const schedule = createScheduler(() => instance.render());
|
|
111
|
+
instance.state = createReactiveState(rawState, schedule) as S;
|
|
98
112
|
|
|
99
113
|
// Expression state: proxy that falls back to instance methods so expressions
|
|
100
114
|
// like `data-text="formatDate(item.date)"` can call component methods.
|
|
@@ -106,90 +120,104 @@ export function mount<S extends StateRecord>(
|
|
|
106
120
|
// Both traps reject Object.prototype names ('constructor', 'toString', ...) —
|
|
107
121
|
// accessing them via a directive expression returns undefined instead of
|
|
108
122
|
// leaking the prototype.
|
|
109
|
-
const boundMethods = new Map<string, Function>()
|
|
123
|
+
const boundMethods = new Map<string, Function>();
|
|
110
124
|
const exprState = new Proxy(rawState, {
|
|
111
125
|
get(target, key: string) {
|
|
112
|
-
if (Object.prototype.hasOwnProperty.call(target, key)) return target[key]
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
126
|
+
if (Object.prototype.hasOwnProperty.call(target, key)) return target[key];
|
|
127
|
+
if (
|
|
128
|
+
Object.prototype.hasOwnProperty.call(instance, key) &&
|
|
129
|
+
typeof instance[key] === "function"
|
|
130
|
+
) {
|
|
131
|
+
const cached = boundMethods.get(key);
|
|
132
|
+
if (cached) return cached;
|
|
133
|
+
const bound = (instance[key] as Function).bind(instance);
|
|
134
|
+
boundMethods.set(key, bound);
|
|
135
|
+
return bound;
|
|
120
136
|
}
|
|
121
|
-
return undefined
|
|
137
|
+
return undefined;
|
|
122
138
|
},
|
|
123
139
|
has(target, key: string) {
|
|
124
|
-
if (typeof key !==
|
|
125
|
-
if (Object.prototype.hasOwnProperty.call(target, key)) return true
|
|
126
|
-
return
|
|
127
|
-
|
|
140
|
+
if (typeof key !== "string") return false;
|
|
141
|
+
if (Object.prototype.hasOwnProperty.call(target, key)) return true;
|
|
142
|
+
return (
|
|
143
|
+
Object.prototype.hasOwnProperty.call(instance, key) &&
|
|
144
|
+
typeof instance[key] === "function"
|
|
145
|
+
);
|
|
128
146
|
},
|
|
129
|
-
})
|
|
147
|
+
});
|
|
130
148
|
|
|
131
|
-
let warnedReentry = false
|
|
149
|
+
let warnedReentry = false;
|
|
132
150
|
instance.render = function () {
|
|
133
|
-
if (instance.__micraDestroyed) return
|
|
151
|
+
if (instance.__micraDestroyed) return;
|
|
134
152
|
if (isRendering) {
|
|
135
153
|
if (!warnedReentry) {
|
|
136
|
-
warn(
|
|
137
|
-
|
|
154
|
+
warn(
|
|
155
|
+
"render() re-entry detected — mutation inside a directive expression is ignored. Move state writes to a method.",
|
|
156
|
+
);
|
|
157
|
+
warnedReentry = true;
|
|
138
158
|
}
|
|
139
|
-
return
|
|
159
|
+
return;
|
|
140
160
|
}
|
|
141
|
-
isRendering = true
|
|
161
|
+
isRendering = true;
|
|
142
162
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
163
|
+
// Single-pass scan, cached on the root for re-renders. Replaces what
|
|
164
|
+
// used to be ~10 separate querySelectorAll passes per render.
|
|
165
|
+
const mRoot = root as MicraElement;
|
|
166
|
+
const scan =
|
|
167
|
+
mRoot.__micraScan ?? (mRoot.__micraScan = scanComponent(root));
|
|
168
|
+
applyDirectives(scan, exprState, rawState, instance);
|
|
169
|
+
renderList(scan.each, exprState, rawState, instance);
|
|
170
|
+
bindDataOn(scan.on, instance);
|
|
171
|
+
bindAtEvents(scan.atEvents, instance);
|
|
172
|
+
bindModels(scan.model, instance);
|
|
173
|
+
collectRefs(scan.refs, instance);
|
|
149
174
|
} finally {
|
|
150
|
-
isRendering = false
|
|
175
|
+
isRendering = false;
|
|
151
176
|
}
|
|
152
|
-
}
|
|
177
|
+
};
|
|
153
178
|
|
|
154
179
|
// ── Destroy ───────────────────────────────────────────────────────────────
|
|
155
180
|
instance.destroy = function () {
|
|
156
|
-
if (instance.__micraDestroyed) return
|
|
157
|
-
instance.__micraDestroyed = true
|
|
181
|
+
if (instance.__micraDestroyed) return;
|
|
182
|
+
instance.__micraDestroyed = true;
|
|
158
183
|
|
|
159
184
|
// Remove every DOM listener attached by bindDataOn / bindAtEvents / bindModels.
|
|
160
|
-
instance.__micraListeners?.forEach(({ el, type, fn }) =>
|
|
161
|
-
|
|
185
|
+
instance.__micraListeners?.forEach(({ el, type, fn }) =>
|
|
186
|
+
el.removeEventListener(type, fn),
|
|
187
|
+
);
|
|
188
|
+
instance.__micraListeners = [];
|
|
162
189
|
|
|
163
190
|
// Clear per-element flags & cached directive scan so a future re-mount of the same DOM works.
|
|
164
191
|
const clearFlags = (el: Element) => {
|
|
165
|
-
const m = el as MicraElement
|
|
166
|
-
delete m.__micraEvents
|
|
167
|
-
delete m.__micraAtBound
|
|
168
|
-
delete m.__micraModel
|
|
169
|
-
delete m.
|
|
170
|
-
}
|
|
171
|
-
clearFlags(root)
|
|
172
|
-
root.querySelectorAll(
|
|
173
|
-
|
|
174
|
-
instance.__micraSubs?.forEach(unsub => unsub())
|
|
175
|
-
instance.__micraSubs = []
|
|
176
|
-
|
|
177
|
-
if (typeof (definition as Record<string, unknown>).onDestroy ===
|
|
178
|
-
(definition.onDestroy as () => void).call(instance)
|
|
179
|
-
_instances.delete(root)
|
|
180
|
-
}
|
|
192
|
+
const m = el as MicraElement;
|
|
193
|
+
delete m.__micraEvents;
|
|
194
|
+
delete m.__micraAtBound;
|
|
195
|
+
delete m.__micraModel;
|
|
196
|
+
delete m.__micraScan;
|
|
197
|
+
};
|
|
198
|
+
clearFlags(root);
|
|
199
|
+
root.querySelectorAll("*").forEach(clearFlags);
|
|
200
|
+
|
|
201
|
+
instance.__micraSubs?.forEach((unsub) => unsub());
|
|
202
|
+
instance.__micraSubs = [];
|
|
203
|
+
|
|
204
|
+
if (typeof (definition as Record<string, unknown>).onDestroy === "function")
|
|
205
|
+
(definition.onDestroy as () => void).call(instance);
|
|
206
|
+
_instances.delete(root);
|
|
207
|
+
};
|
|
181
208
|
|
|
182
209
|
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
|
183
|
-
_instances.set(root, instance as InternalInstance)
|
|
184
|
-
instance.render()
|
|
210
|
+
_instances.set(root, instance as InternalInstance);
|
|
211
|
+
instance.render();
|
|
185
212
|
|
|
186
|
-
// Validate directive usage and emit dev warnings
|
|
187
|
-
|
|
213
|
+
// Validate directive usage and emit dev warnings — reuses the same scan.
|
|
214
|
+
const mRoot = root as MicraElement;
|
|
215
|
+
if (mRoot.__micraScan) validateDirectives(mRoot.__micraScan);
|
|
188
216
|
|
|
189
|
-
if (typeof (definition as Record<string, unknown>).onCreate ===
|
|
217
|
+
if (typeof (definition as Record<string, unknown>).onCreate === "function")
|
|
190
218
|
Promise.resolve().then(() =>
|
|
191
219
|
(definition.onCreate as () => void | Promise<void>).call(instance),
|
|
192
|
-
)
|
|
220
|
+
);
|
|
193
221
|
|
|
194
|
-
return instance
|
|
222
|
+
return instance as unknown as ComponentInstance<S, M>;
|
|
195
223
|
}
|
package/src/core/registry.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type {
|
|
13
13
|
ComponentDefinition,
|
|
14
14
|
ComponentInstance,
|
|
15
|
+
ComponentMethods,
|
|
15
16
|
InternalInstance,
|
|
16
17
|
StateRecord,
|
|
17
18
|
} from '../types'
|
|
@@ -27,19 +28,28 @@ export const _instances = new Map<HTMLElement, InternalInstance>()
|
|
|
27
28
|
/**
|
|
28
29
|
* Register a component definition under `name`.
|
|
29
30
|
*
|
|
31
|
+
* Both state shape (`S`) and method set (`M`) are inferred from the literal,
|
|
32
|
+
* so `this.state.X` and `this.someMethod()` are fully typed inside the
|
|
33
|
+
* method bodies and lifecycle hooks.
|
|
34
|
+
*
|
|
30
35
|
* @example
|
|
31
|
-
* define('counter', {
|
|
36
|
+
* define('counter', {
|
|
37
|
+
* state: { count: 0 },
|
|
38
|
+
* inc() { this.state.count++ }, // this.state.count: number ✓
|
|
39
|
+
* reset() { this.state.count = 0 }, // this.reset() is also typed ✓
|
|
40
|
+
* onCreate() { this.inc() }, // ✓
|
|
41
|
+
* })
|
|
32
42
|
*/
|
|
33
|
-
export function define<S extends StateRecord>(
|
|
43
|
+
export function define<S extends StateRecord, M>(
|
|
34
44
|
name: string,
|
|
35
|
-
definition: ComponentDefinition<S>,
|
|
45
|
+
definition: ComponentDefinition<S, M>,
|
|
36
46
|
): void {
|
|
37
|
-
_registry.set(name, definition as ComponentDefinition)
|
|
47
|
+
_registry.set(name, definition as unknown as ComponentDefinition)
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
/**
|
|
41
51
|
* Type-helper — returns `definition` unchanged but lets TypeScript infer `S`
|
|
42
|
-
* from the
|
|
52
|
+
* and `M` from the literal so all methods are typed with the correct `this`.
|
|
43
53
|
*
|
|
44
54
|
* Use this when defining a component outside a `define()` call.
|
|
45
55
|
*
|
|
@@ -50,9 +60,9 @@ export function define<S extends StateRecord>(
|
|
|
50
60
|
* })
|
|
51
61
|
* Micra.define('counter', counter)
|
|
52
62
|
*/
|
|
53
|
-
export function defineComponent<S extends StateRecord>(
|
|
54
|
-
definition: ComponentDefinition<S>,
|
|
55
|
-
): ComponentDefinition<S> {
|
|
63
|
+
export function defineComponent<S extends StateRecord, M>(
|
|
64
|
+
definition: ComponentDefinition<S, M>,
|
|
65
|
+
): ComponentDefinition<S, M> {
|
|
56
66
|
return definition
|
|
57
67
|
}
|
|
58
68
|
|
|
@@ -61,7 +71,7 @@ export function defineComponent<S extends StateRecord>(
|
|
|
61
71
|
* Useful for DevTools / debugging.
|
|
62
72
|
*/
|
|
63
73
|
export function instances(): ReadonlyMap<HTMLElement, ComponentInstance> {
|
|
64
|
-
return _instances
|
|
74
|
+
return _instances as unknown as ReadonlyMap<HTMLElement, ComponentInstance>
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
/**
|