rxflare-js 0.0.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/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # RxFlare
2
+
3
+ Fine-grained reactive framework inspired by:
4
+
5
+ - SolidJS
6
+ - Vue Vapor
7
+ - Svelte
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install rxflare
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```js
18
+ import {
19
+ state,
20
+ derived,
21
+ compile,
22
+ createComponent,
23
+ render
24
+ } from 'rxflare';
25
+
26
+ const count = state(0);
27
+
28
+ const App =
29
+ compile(`
30
+ <div>
31
+ {{ count() }}
32
+ </div>
33
+ `);
34
+
35
+ render(
36
+ () =>
37
+ createComponent(
38
+ App,
39
+ { count }
40
+ ),
41
+ document.body
42
+ );
43
+ ```
package/index.html ADDED
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+
3
+ <html>
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <title>RxFlare Demo</title>
7
+
8
+ <style>
9
+ body {
10
+ font-family: sans-serif;
11
+ padding: 40px;
12
+ }
13
+
14
+ .done {
15
+ text-decoration: line-through;
16
+ font-size: 50px;
17
+ opacity: 0.5;
18
+ }
19
+
20
+ button {
21
+ margin-left: 8px;
22
+ }
23
+ </style>
24
+
25
+ </head>
26
+ <body>
27
+
28
+ <div id="app"></div>
29
+
30
+ <script type="module" src="./main.js"></script>
31
+
32
+ </body>
33
+ </html>
package/main.js ADDED
@@ -0,0 +1,183 @@
1
+ import {
2
+ state,
3
+ derived,
4
+ render,
5
+ compile,
6
+ createComponent,
7
+ reconcile
8
+ } from './src/index.js';
9
+
10
+
11
+
12
+ // ======================================================
13
+ // STATE
14
+ // ======================================================
15
+
16
+ const count = state(0);
17
+
18
+ const todos = state([
19
+ {
20
+ id: 1,
21
+ text: 'Build compiler'
22
+ },
23
+ {
24
+ id: 2,
25
+ text: 'Build vapor'
26
+ }
27
+ ]);
28
+
29
+ const double = derived(() => {
30
+ return count() * 2;
31
+ });
32
+
33
+
34
+
35
+ // ======================================================
36
+ // TEMPLATES
37
+ // ======================================================
38
+
39
+ const AppTemplate =
40
+ compile(
41
+ `
42
+ <div class="app">
43
+
44
+ <h1>
45
+ Count:
46
+ {{ count() }}
47
+ </h1>
48
+
49
+ <p>
50
+ Double:
51
+ {{ double() }}
52
+ </p>
53
+
54
+ <button>
55
+ INC
56
+ </button>
57
+
58
+ <button :class="count() % 2 ? 'red' : 'blue'">
59
+ COLOR
60
+ </button>
61
+
62
+ <div id="list"></div>
63
+
64
+ </div>
65
+ `
66
+ );
67
+
68
+ const TodoTemplate =
69
+ compile(
70
+ `
71
+ <div class="todo">
72
+
73
+ {{ item.text }}
74
+
75
+ </div>
76
+ `
77
+ );
78
+
79
+
80
+
81
+ // ======================================================
82
+ // APP
83
+ // ======================================================
84
+
85
+ function App() {
86
+
87
+ const root =
88
+ createComponent(
89
+ AppTemplate,
90
+ {
91
+ count,
92
+ double
93
+ }
94
+ );
95
+
96
+
97
+
98
+ // ======================================================
99
+ // BUTTONS
100
+ // ======================================================
101
+
102
+ const buttons =
103
+ root.querySelectorAll('button');
104
+
105
+ buttons[0].onclick = () => {
106
+
107
+ count.set(
108
+ count() + 1
109
+ );
110
+ };
111
+
112
+ buttons[1].onclick = () => {
113
+
114
+ count.set(
115
+ count() + 1
116
+ );
117
+ };
118
+
119
+
120
+
121
+ // ======================================================
122
+ // LIST
123
+ // ======================================================
124
+
125
+ const list =
126
+ root.querySelector('#list');
127
+
128
+ reconcile(
129
+
130
+ list,
131
+
132
+ todos,
133
+
134
+ item => {
135
+
136
+ return createComponent(
137
+ TodoTemplate,
138
+ {
139
+ item
140
+ }
141
+ );
142
+ }
143
+ );
144
+
145
+
146
+
147
+ // ======================================================
148
+ // ADD TODO
149
+ // ======================================================
150
+
151
+ setInterval(() => {
152
+
153
+ todos.set([
154
+
155
+ ...todos(),
156
+
157
+ {
158
+ id: Date.now(),
159
+
160
+ text:
161
+ 'TODO ' +
162
+ todos().length
163
+ }
164
+
165
+ ]);
166
+
167
+ }, 3000);
168
+
169
+
170
+
171
+ return root;
172
+ }
173
+
174
+
175
+
176
+ // ======================================================
177
+ // RENDER
178
+ // ======================================================
179
+
180
+ render(
181
+ App,
182
+ document.getElementById('app')
183
+ );
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "rxflare-js",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "./src/index.js",
6
+ "module": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./core": "./src/core.js",
10
+ "./dom": "./src/dom.js",
11
+ "./compiler": "./src/compiler.js"
12
+ },
13
+ "keywords": [
14
+ "reactive",
15
+ "signals",
16
+ "solidjs",
17
+ "vue",
18
+ "compiler",
19
+ "framework"
20
+ ],
21
+ "author": "msfm2018",
22
+ "license": "MIT",
23
+ "description": "Fine-grained reactive UI framework",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/msfm2018/rxflare-js"
27
+ }
28
+ }
@@ -0,0 +1,322 @@
1
+ // Fixed version of rxflare-compiler.js
2
+ // Key fixes:
3
+ // 1. Path calculation bug during mutations (sibling index shifts)
4
+ // 2. Better handling using markers for dynamic parts
5
+ // 3. Attach instructions via a registry using node references (with clone mapping)
6
+
7
+ import { root } from './core.js';
8
+ import { insert, prop, show, reconcile } from './dom.js';
9
+
10
+ export function compile(htmlString) {
11
+ const parser = new DOMParser();
12
+ const doc = parser.parseFromString(htmlString, 'text/html');
13
+ const fragment = document.createDocumentFragment();
14
+
15
+ while (doc.body.firstChild) {
16
+ fragment.appendChild(doc.body.firstChild);
17
+ }
18
+
19
+ const instructions = [];
20
+ const nodeToIns = new Map(); // For elements/markers
21
+
22
+ // 深度优先遍历,处理指令并插入 marker
23
+ function traverse(node) {
24
+ if (!node) return;
25
+
26
+ if (node.nodeType === Node.ELEMENT_NODE) {
27
+ // 处理 :if
28
+ if (node.hasAttribute(':if')) {
29
+ const expr = node.getAttribute(':if');
30
+ const subNode = node.cloneNode(true);
31
+ subNode.removeAttribute(':if');
32
+
33
+ const marker = document.createComment(`rx-if:${expr}`);
34
+ node.parentNode.replaceChild(marker, node);
35
+
36
+ instructions.push({
37
+ type: 'if',
38
+ marker,
39
+ getter: compileExpression(expr),
40
+ subTemplate: compile(subNode.outerHTML)
41
+ });
42
+ nodeToIns.set(marker, instructions[instructions.length - 1]);
43
+ return;
44
+ }
45
+
46
+ // 处理 :for
47
+ if (node.hasAttribute(':for')) {
48
+ const rawExpr = node.getAttribute(':for');
49
+ const match = rawExpr.match(/^\s*(?:([\w\s,]+)|(?:\(([\w\s,]+)\)))\s+in\s+(.+)$/);
50
+ if (!match) throw new Error(`RxFlare 语法错误: :for 格式必须为 'item in list'`);
51
+
52
+ const iterators = (match[1] || match[2]).split(',').map(s => s.trim());
53
+ const itemParam = iterators[0];
54
+ const indexParam = iterators[1] || '$index';
55
+ const listExpr = match[3].trim();
56
+
57
+ const subNode = node.cloneNode(true);
58
+ subNode.removeAttribute(':for');
59
+
60
+ const marker = document.createComment(`rx-for:${listExpr}`);
61
+ node.parentNode.replaceChild(marker, node);
62
+
63
+ instructions.push({
64
+ type: 'for',
65
+ marker,
66
+ getter: compileExpression(listExpr),
67
+ itemParam,
68
+ indexParam,
69
+ subTemplate: compile(subNode.outerHTML)
70
+ });
71
+ nodeToIns.set(marker, instructions[instructions.length - 1]);
72
+ return;
73
+ }
74
+
75
+ // 处理属性绑定 (包括 :class, :style 等)
76
+ const attrs = Array.from(node.attributes);
77
+ attrs.forEach(attr => {
78
+ const name = attr.name;
79
+ const expr = attr.value;
80
+
81
+ if (name.startsWith('@') || name.startsWith(':on')) {
82
+ const eventName = name.startsWith('@') ? name.slice(1) : name.slice(3);
83
+ node.removeAttribute(name);
84
+ instructions.push({
85
+ type: 'event',
86
+ target: node, // element itself
87
+ eventName,
88
+ getter: compileExpression(expr)
89
+ });
90
+ nodeToIns.set(node, instructions[instructions.length - 1]); // last one, but better collect list
91
+ } else if (name === ':model') {
92
+ node.removeAttribute(name);
93
+ instructions.push({
94
+ type: 'model',
95
+ target: node,
96
+ propName: expr.trim()
97
+ });
98
+ nodeToIns.set(node, instructions[instructions.length - 1]);
99
+ } else if (name.startsWith(':')) {
100
+ const propName = name.slice(1);
101
+ node.removeAttribute(name);
102
+ instructions.push({
103
+ type: 'prop',
104
+ target: node,
105
+ propName,
106
+ getter: compileExpression(expr)
107
+ });
108
+ nodeToIns.set(node, instructions[instructions.length - 1]);
109
+ }
110
+ });
111
+ }
112
+ // 处理 TEXT 插值
113
+ else if (node.nodeType === Node.TEXT_NODE) {
114
+ const text = node.nodeValue;
115
+ const regex = /\{\{(.*?)\}\}/g;
116
+
117
+ if (regex.test(text)) {
118
+ const parent = node.parentNode;
119
+ const container = document.createDocumentFragment();
120
+ const segments = text.split(/\{\{(.*?)\}\}/g);
121
+
122
+ segments.forEach((seg, index) => {
123
+ if (index % 2 === 0) {
124
+ if (seg) container.appendChild(document.createTextNode(seg));
125
+ } else {
126
+ const marker = document.createComment('rx-text');
127
+ container.appendChild(marker);
128
+
129
+ instructions.push({
130
+ type: 'text',
131
+ marker,
132
+ getter: compileExpression(seg.trim())
133
+ });
134
+ nodeToIns.set(marker, instructions[instructions.length - 1]);
135
+ }
136
+ });
137
+
138
+ parent.replaceChild(container, node);
139
+ return;
140
+ }
141
+ }
142
+
143
+ // 递归子节点
144
+ let child = node.firstChild;
145
+ while (child) {
146
+ const next = child.nextSibling;
147
+ traverse(child);
148
+ child = next;
149
+ }
150
+ }
151
+
152
+ Array.from(fragment.childNodes).forEach(child => traverse(child));
153
+
154
+ return {
155
+ template: fragment,
156
+ instructions,
157
+ // nodeToIns not needed for runtime
158
+ };
159
+ }
160
+
161
+ function compileExpression(expr) {
162
+ try {
163
+ return new Function('scope', `
164
+ try {
165
+ with(scope) { return ${expr}; }
166
+ } catch(e) {
167
+ console.error('Expr error:', e);
168
+ return '';
169
+ }
170
+ `);
171
+ } catch (e) {
172
+ console.error(`RxFlare 表达式解析错误: ${expr}`);
173
+ return () => '';
174
+ }
175
+ }
176
+
177
+
178
+
179
+ // New createComponent using marker/target references + clone mapping
180
+ export function createComponent(compiledResult, scope = {}) {
181
+ return root(dispose => {
182
+ const clone = compiledResult.template.cloneNode(true);
183
+
184
+ // Create mapping from original nodes to cloned nodes by traversing both in parallel
185
+ const nodeMap = new Map();
186
+
187
+ function mapNodes(original, cloned) {
188
+ if (!original || !cloned) return;
189
+ nodeMap.set(original, cloned);
190
+
191
+ // Map children
192
+ let oChild = original.firstChild;
193
+ let cChild = cloned.firstChild;
194
+ while (oChild && cChild) {
195
+ mapNodes(oChild, cChild);
196
+ oChild = oChild.nextSibling;
197
+ cChild = cChild.nextSibling;
198
+ }
199
+ }
200
+
201
+ mapNodes(compiledResult.template, clone);
202
+
203
+ // Apply all instructions using mapped nodes
204
+ compiledResult.instructions.forEach(ins => {
205
+ let target;
206
+ if (ins.marker) {
207
+ target = nodeMap.get(ins.marker);
208
+ } else if (ins.target) {
209
+ target = nodeMap.get(ins.target);
210
+ }
211
+
212
+ if (!target) {
213
+ console.warn('Target not found for instruction', ins);
214
+ return;
215
+ }
216
+
217
+ switch (ins.type) {
218
+ case 'text':
219
+ insert(target, () => ins.getter(scope));
220
+ break;
221
+ case 'prop':
222
+ prop(target, ins.propName, () => ins.getter(scope));
223
+ break;
224
+ case 'event': {
225
+ prop(
226
+ target,
227
+ `on${ins.eventName}`,
228
+ e => {
229
+
230
+ const eventScope =
231
+ Object.create(scope);
232
+
233
+ eventScope.$event = e;
234
+
235
+ return ins.getter(eventScope);
236
+ }
237
+
238
+
239
+
240
+ );
241
+
242
+ break;
243
+ }
244
+
245
+ case 'model':
246
+ if (target.tagName === 'INPUT' && (target.type === 'checkbox' || target.type === 'radio')) {
247
+ prop(target, 'checked', () => scope[ins.propName]);
248
+ target.addEventListener('change', e => {
249
+ if (typeof scope[ins.propName] !== 'undefined') scope[ins.propName] = e.target.checked;
250
+ });
251
+ } else {
252
+ prop(target, 'value', () => scope[ins.propName]);
253
+ target.addEventListener('input', e => {
254
+ if (typeof scope[ins.propName] !== 'undefined') scope[ins.propName] = e.target.value;
255
+ });
256
+ }
257
+ break;
258
+ case 'if':
259
+ show(target, () => !!ins.getter(scope), () => {
260
+ return createComponent(ins.subTemplate, scope);
261
+ });
262
+ break;
263
+ case 'for': {
264
+ const endMarker =
265
+ document.createComment('rx-for-end');
266
+
267
+ target.parentNode.insertBefore(
268
+ endMarker,
269
+ target.nextSibling
270
+ );
271
+
272
+ reconcile(
273
+ target,
274
+ endMarker,
275
+ () => ins.getter(scope) || [],
276
+ (item, index) => {
277
+ const itemScope =
278
+ Object.create(scope);
279
+
280
+ itemScope[ins.itemParam] = item;
281
+ itemScope[ins.indexParam] = index;
282
+
283
+ return createComponent(
284
+ ins.subTemplate,
285
+ itemScope
286
+ );
287
+ }
288
+
289
+ );
290
+
291
+ break;
292
+ }
293
+
294
+ }
295
+ });
296
+
297
+
298
+ const first =
299
+ clone.firstElementChild ||
300
+ clone.firstChild;
301
+
302
+ if (first) {
303
+ first.dispose = dispose;
304
+ }
305
+
306
+ return first;
307
+
308
+
309
+ // const first = clone.firstElementChild || clone.firstChild;
310
+ // if (first) first.dispose = dispose;
311
+
312
+ // const wrapper =
313
+ // document.createElement('div');
314
+
315
+ // wrapper.appendChild(clone);
316
+
317
+ // wrapper.dispose = dispose;
318
+
319
+ // return wrapper;
320
+
321
+ });
322
+ }
package/src/core.js ADDED
@@ -0,0 +1,299 @@
1
+ // ======================================================
2
+ // RXFLARE CORE NEXT++
3
+ // ======================================================
4
+
5
+ let activeEffect = null;
6
+
7
+ const effectStack = [];
8
+
9
+ const scheduled = new Set();
10
+
11
+
12
+
13
+ // ======================================================
14
+ // OWNER TREE
15
+ // ======================================================
16
+
17
+ let currentOwner = null;
18
+
19
+ function createOwner(parent = currentOwner) {
20
+
21
+ const owner = {
22
+ parent,
23
+ cleanups: [],
24
+ effects: []
25
+ };
26
+
27
+ return owner;
28
+ }
29
+
30
+ export function onCleanup(fn) {
31
+
32
+ if (currentOwner) {
33
+ currentOwner.cleanups.push(fn);
34
+ }
35
+ }
36
+
37
+ function cleanupOwner(owner) {
38
+
39
+ owner.cleanups.forEach(fn => fn());
40
+
41
+ owner.cleanups.length = 0;
42
+
43
+ owner.effects.forEach(e => e.stop());
44
+
45
+ owner.effects.length = 0;
46
+ }
47
+
48
+
49
+
50
+ // ======================================================
51
+ // SIGNAL
52
+ // ======================================================
53
+
54
+ export function state(initial) {
55
+
56
+ let value = initial;
57
+
58
+ const subscribers = new Set();
59
+
60
+ function signal() {
61
+
62
+ if (activeEffect) {
63
+
64
+ subscribers.add(activeEffect);
65
+
66
+ activeEffect.deps.add(subscribers);
67
+ }
68
+
69
+ return value;
70
+ }
71
+
72
+ signal.set = next => {
73
+
74
+ if (Object.is(value, next)) return;
75
+
76
+ value = next;
77
+
78
+ subscribers.forEach(schedule);
79
+ };
80
+
81
+ signal.peek = () => value;
82
+
83
+ return signal;
84
+ }
85
+
86
+
87
+
88
+ // ======================================================
89
+ // EFFECT
90
+ // ======================================================
91
+
92
+ export function effect(fn) {
93
+
94
+ const owner = currentOwner;
95
+
96
+ const effectFn = {
97
+
98
+ active: true,
99
+
100
+ deps: new Set(),
101
+
102
+ cleanups: [],
103
+
104
+ run() {
105
+
106
+ if (!effectFn.active) return;
107
+
108
+ cleanup(effectFn);
109
+
110
+ effectStack.push(effectFn);
111
+
112
+ activeEffect = effectFn;
113
+
114
+ const prevOwner = currentOwner;
115
+
116
+ currentOwner = owner;
117
+
118
+ fn();
119
+
120
+ currentOwner = prevOwner;
121
+
122
+ effectStack.pop();
123
+
124
+ activeEffect =
125
+ effectStack[
126
+ effectStack.length - 1
127
+ ] || null;
128
+ },
129
+
130
+ stop() {
131
+
132
+ if (!effectFn.active) return;
133
+
134
+ effectFn.active = false;
135
+
136
+ cleanup(effectFn);
137
+ }
138
+ };
139
+
140
+ if (owner) {
141
+ owner.effects.push(effectFn);
142
+ }
143
+
144
+ effectFn.run();
145
+
146
+ return effectFn;
147
+ }
148
+
149
+ function cleanup(effectFn) {
150
+
151
+ effectFn.deps.forEach(dep => {
152
+ dep.delete(effectFn);
153
+ });
154
+
155
+ effectFn.deps.clear();
156
+
157
+ effectFn.cleanups.forEach(fn => fn());
158
+
159
+ effectFn.cleanups.length = 0;
160
+ }
161
+
162
+ function schedule(effectFn) {
163
+
164
+ if (
165
+ !effectFn.active ||
166
+ scheduled.has(effectFn)
167
+ ) return;
168
+
169
+ scheduled.add(effectFn);
170
+
171
+ queueMicrotask(() => {
172
+
173
+ scheduled.delete(effectFn);
174
+
175
+ effectFn.run();
176
+ });
177
+ }
178
+
179
+
180
+
181
+ // ======================================================
182
+ // ROOT
183
+ // ======================================================
184
+
185
+ export function root(fn) {
186
+
187
+ const prev = currentOwner;
188
+
189
+ const owner = createOwner(prev);
190
+
191
+ currentOwner = owner;
192
+
193
+ const result = fn(() => {
194
+ cleanupOwner(owner);
195
+ });
196
+
197
+ currentOwner = prev;
198
+
199
+ return result;
200
+ }
201
+
202
+
203
+
204
+ // ======================================================
205
+ // MEMO
206
+ // ======================================================
207
+
208
+ export function derived(fn) {
209
+
210
+ const s = state();
211
+
212
+ effect(() => {
213
+ s.set(fn());
214
+ });
215
+
216
+ return s;
217
+ }
218
+
219
+
220
+
221
+ // ======================================================
222
+ // TEMPLATE CACHE
223
+ // ======================================================
224
+
225
+ const templateCache = new Map();
226
+
227
+ export function template(html) {
228
+
229
+ if (templateCache.has(html)) {
230
+ return templateCache.get(html);
231
+ }
232
+
233
+ const tpl =
234
+ document.createElement('template');
235
+
236
+ tpl.innerHTML = html;
237
+
238
+ templateCache.set(html, tpl);
239
+
240
+ return tpl;
241
+ }
242
+
243
+ export function cloneTemplate(html) {
244
+
245
+ return template(html)
246
+ .content
247
+ .cloneNode(true);
248
+ }
249
+
250
+ // ======================================================
251
+ // RENDER
252
+ // ======================================================
253
+
254
+ export function render(
255
+ component,
256
+ root
257
+ ) {
258
+
259
+ const node = component();
260
+
261
+ root.appendChild(node);
262
+
263
+ return () => {
264
+
265
+ if (node.dispose) {
266
+ node.dispose();
267
+ }
268
+
269
+ node.remove();
270
+ };
271
+ }
272
+
273
+
274
+ export function store(obj) {
275
+ const signals = {};
276
+
277
+ return new Proxy(obj, {
278
+ get(target, key) {
279
+ if (!signals[key]) {
280
+ signals[key] = state(target[key]);
281
+ }
282
+
283
+ return signals[key]();
284
+ },
285
+
286
+ set(target, key, value) {
287
+ target[key] = value;
288
+
289
+ if (!signals[key]) {
290
+ signals[key] = state(value);
291
+ }
292
+
293
+ signals[key].set(value);
294
+
295
+ return true;
296
+ }
297
+
298
+ });
299
+ }
package/src/dom.js ADDED
@@ -0,0 +1,394 @@
1
+ // ======================================================
2
+ // RXFLARE DOM NEXT++
3
+ // ======================================================
4
+
5
+ import {
6
+ effect,
7
+ onCleanup
8
+ } from './core.js';
9
+
10
+
11
+
12
+ // ======================================================
13
+ // INSERT
14
+ // ======================================================
15
+
16
+ export function insert(marker, value) {
17
+ const parent = marker.parentNode;
18
+
19
+ let current = null;
20
+
21
+ effect(() => {
22
+ const next =
23
+ typeof value === 'function'
24
+ ? value()
25
+ : value;
26
+
27
+ patch(next);
28
+
29
+ });
30
+
31
+ function patch(v) {
32
+ if (
33
+ typeof v === 'string' ||
34
+ typeof v === 'number'
35
+ ) {
36
+ if (
37
+ current &&
38
+ current.nodeType === Node.TEXT_NODE
39
+ ) {
40
+ current.nodeValue = v;
41
+ } else {
42
+ clear();
43
+
44
+ current =
45
+ document.createTextNode(v);
46
+
47
+ parent.insertBefore(
48
+ current,
49
+ marker
50
+ );
51
+ }
52
+
53
+ return;
54
+ }
55
+
56
+ if (v instanceof Node) {
57
+ if (current !== v) {
58
+ clear();
59
+
60
+ current = v;
61
+
62
+ parent.insertBefore(v, marker);
63
+ }
64
+
65
+ return;
66
+ }
67
+
68
+ if (v == null || v === false) {
69
+ clear();
70
+ }
71
+
72
+ }
73
+
74
+ function clear() {
75
+ if (!current) return;
76
+
77
+
78
+ current.dispose?.();
79
+
80
+ current.remove?.();
81
+
82
+ current = null;
83
+
84
+ }
85
+ }
86
+
87
+
88
+
89
+
90
+ // ======================================================
91
+ // PROP
92
+ // ======================================================
93
+
94
+ export function prop(
95
+ el,
96
+ key,
97
+ value
98
+ ) {
99
+
100
+ // events
101
+ if (key.startsWith('on')) {
102
+ setProp(el, key, value);
103
+ return;
104
+ }
105
+
106
+ // reactive bindings
107
+ if (typeof value === 'function') {
108
+
109
+
110
+ effect(() => {
111
+ setProp(el, key, value());
112
+ });
113
+
114
+
115
+ } else {
116
+
117
+
118
+ setProp(el, key, value);
119
+
120
+
121
+ }
122
+ }
123
+
124
+
125
+ function setProp(
126
+ el,
127
+ key,
128
+ value
129
+ ) {
130
+
131
+ // event
132
+ if (key.startsWith('on')) {
133
+
134
+ const event =
135
+ key.slice(2).toLowerCase();
136
+
137
+ const store =
138
+ el._vei || (el._vei = {});
139
+
140
+ let invoker =
141
+ store[event];
142
+
143
+ if (!invoker) {
144
+
145
+ invoker = e => {
146
+ invoker.value?.(e);
147
+ };
148
+
149
+ store[event] = invoker;
150
+
151
+ el.addEventListener(
152
+ event,
153
+ invoker
154
+ );
155
+ }
156
+
157
+ invoker.value = value;
158
+
159
+ return;
160
+ }
161
+
162
+ if (
163
+ value == null ||
164
+ value === false
165
+ ) {
166
+
167
+ el.removeAttribute(key);
168
+
169
+ return;
170
+ }
171
+
172
+ if (key === 'class') {
173
+
174
+ el.className = value;
175
+
176
+ return;
177
+ }
178
+
179
+ if (key === 'style') {
180
+
181
+ Object.assign(el.style, value);
182
+
183
+ return;
184
+ }
185
+
186
+ if (key in el) {
187
+
188
+ el[key] = value;
189
+
190
+ } else {
191
+
192
+ el.setAttribute(key, value);
193
+ }
194
+ }
195
+
196
+
197
+
198
+ // ======================================================
199
+ // SHOW
200
+ // ======================================================
201
+
202
+ export function show(
203
+ marker,
204
+ condition,
205
+ render
206
+ ) {
207
+
208
+ insert(marker, () => {
209
+
210
+ return condition()
211
+ ? render()
212
+ : null;
213
+ });
214
+ }
215
+
216
+
217
+
218
+ // ======================================================
219
+ // EACH
220
+ // ======================================================
221
+
222
+ export function each(
223
+ marker,
224
+ list,
225
+ renderItem
226
+ ) {
227
+
228
+ insert(marker, () => {
229
+
230
+ return list().map(renderItem);
231
+ });
232
+ }
233
+
234
+ // ======================================================
235
+ // RECONCILE
236
+ // ======================================================
237
+
238
+ export function reconcile(
239
+ containerOrStart,
240
+ endMarkerOrList,
241
+ listOrRender,
242
+ renderItemOrKey,
243
+ keyFn = item => item?.id ?? item
244
+ ) {
245
+
246
+ // ======================================================
247
+ // COMPAT MODE
248
+ // reconcile(container, list, renderItem)
249
+ // ======================================================
250
+
251
+ if (
252
+ !(endMarkerOrList instanceof Node)
253
+ ) {
254
+
255
+ const container =
256
+ containerOrStart;
257
+
258
+ const list =
259
+ endMarkerOrList;
260
+
261
+ const renderItem =
262
+ listOrRender;
263
+
264
+ const startMarker =
265
+ document.createComment(
266
+ 'rx-start'
267
+ );
268
+
269
+ const endMarker =
270
+ document.createComment(
271
+ 'rx-end'
272
+ );
273
+
274
+ container.appendChild(
275
+ startMarker
276
+ );
277
+
278
+ container.appendChild(
279
+ endMarker
280
+ );
281
+
282
+ return reconcile(
283
+ startMarker,
284
+ endMarker,
285
+ list,
286
+ renderItem,
287
+ renderItemOrKey
288
+ );
289
+ }
290
+
291
+
292
+
293
+ // ======================================================
294
+ // NORMAL MODE
295
+ // reconcile(start, end, list, render)
296
+ // ======================================================
297
+
298
+ const startMarker =
299
+ containerOrStart;
300
+
301
+ const endMarker =
302
+ endMarkerOrList;
303
+
304
+ const list =
305
+ listOrRender;
306
+
307
+ const renderItem =
308
+ renderItemOrKey;
309
+
310
+ let oldMap = new Map();
311
+
312
+
313
+
314
+ effect(() => {
315
+
316
+ const items =
317
+ typeof list === 'function'
318
+ ? list() || []
319
+ : [];
320
+
321
+ const newMap = new Map();
322
+
323
+ const newNodes = [];
324
+
325
+
326
+
327
+ items.forEach((item, index) => {
328
+
329
+ const key =
330
+ keyFn(item);
331
+
332
+ let node =
333
+ oldMap.get(key);
334
+
335
+ if (!node) {
336
+
337
+ node =
338
+ renderItem(
339
+ item,
340
+ index
341
+ );
342
+ }
343
+
344
+ newMap.set(key, node);
345
+
346
+ newNodes.push(node);
347
+ });
348
+
349
+
350
+
351
+ // remove old
352
+ oldMap.forEach((node, key) => {
353
+
354
+ if (!newMap.has(key)) {
355
+
356
+ node.dispose?.();
357
+
358
+ node.remove?.();
359
+ }
360
+ });
361
+
362
+
363
+
364
+ // insert/reorder
365
+ let anchor =
366
+ endMarker;
367
+
368
+ for (
369
+ let i =
370
+ newNodes.length - 1;
371
+ i >= 0;
372
+ i--
373
+ ) {
374
+
375
+ const node =
376
+ newNodes[i];
377
+
378
+ if (
379
+ node.nextSibling !== anchor
380
+ ) {
381
+
382
+ endMarker.parentNode.insertBefore(
383
+ node,
384
+ anchor
385
+ );
386
+ }
387
+
388
+ anchor = node;
389
+ }
390
+
391
+ oldMap = newMap;
392
+
393
+ });
394
+ }
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './core.js';
2
+ export * from './dom.js';
3
+ export * from './compiler.js';