Skip to content

Commit f37ba69

Browse files
committed
feat: add reactive insert, setAttr, and on functions
1 parent 612911c commit f37ba69

File tree

4 files changed

+182
-11
lines changed

4 files changed

+182
-11
lines changed

examples/10k-todos-stress.jsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Direct JSX file, no framework — runs with your runtime
2+
import { signal, effect } from "../runtime/signals.js"
3+
import { insert, createElement, setProp } from "../runtime/dom.js"
4+
5+
// Core signals
6+
const todos = signal([])
7+
const filter = signal("all")
8+
const input = signal("")
9+
10+
// Add 10,000 todos initially
11+
for (let i = 0; i < 10000; i++) {
12+
todos.value.push({ id: i, text: `Task #${i}`, done: false })
13+
}
14+
15+
// Reactive rendering
16+
effect(() => {
17+
const root = document.getElementById("app")
18+
root.innerHTML = ""
19+
20+
const visible = todos.value.filter(todo => {
21+
if (filter.value === "done") return todo.done
22+
if (filter.value === "active") return !todo.done
23+
return true
24+
})
25+
26+
for (const todo of visible) {
27+
const div = createElement("div")
28+
div.className = "todo"
29+
div.style = `padding:2px;border-bottom:1px solid #eee`
30+
div.onclick = () => {
31+
todo.done = !todo.done
32+
todos.value = [...todos.value] // trigger update
33+
}
34+
div.textContent = `${todo.done ? "✅" : "⬜️"} ${todo.text}`
35+
insert(root, div)
36+
}
37+
})
38+
39+
// Input UI
40+
window.onload = () => {
41+
const inputBox = document.getElementById("input")
42+
const filterAll = document.getElementById("filter-all")
43+
const filterDone = document.getElementById("filter-done")
44+
const filterActive = document.getElementById("filter-active")
45+
const addButton = document.getElementById("add")
46+
47+
inputBox.oninput = (e) => input.value = e.target.value
48+
addButton.onclick = () => {
49+
if (!input.value.trim()) return
50+
todos.value = [
51+
...todos.value,
52+
{ id: todos.value.length, text: input.value, done: false }
53+
]
54+
input.value = ""
55+
inputBox.value = ""
56+
}
57+
58+
filterAll.onclick = () => filter.value = "all"
59+
filterDone.onclick = () => filter.value = "done"
60+
filterActive.onclick = () => filter.value = "active"
61+
}

runtime/dom.js

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import { effect } from './signals.js';
1+
import { effect, onCleanup } from './signals.js';
22

3-
// TEXT NODE POOL TO REUSE NODES
3+
// Text node reuse pool
44
const pool = [];
55

6-
/**
7-
* Insert reactive or static content.
8-
*/
96
export function insert(parent, value) {
107
if (typeof value === 'function') {
118
const node = pool.pop() || document.createTextNode('');
@@ -19,14 +16,47 @@ export function insert(parent, value) {
1916
node.textContent = next == null ? '' : next;
2017
}
2118
});
19+
} else if (Array.isArray(value)) {
20+
insertArray(parent, value);
21+
} else if (value instanceof Node) {
22+
parent.appendChild(value);
2223
} else {
2324
parent.appendChild(document.createTextNode(value ?? ''));
2425
}
2526
}
2627

27-
/**
28-
* Set attribute reactively.
29-
*/
28+
function insertArray(parent, items) {
29+
const nodes = [];
30+
31+
for (let item of items) {
32+
if (typeof item === 'function') {
33+
const node = document.createTextNode('');
34+
parent.appendChild(node);
35+
nodes.push(node);
36+
let prev = null;
37+
38+
effect(() => {
39+
const next = item();
40+
if (next !== prev) {
41+
prev = next;
42+
node.textContent = next == null ? '' : next;
43+
}
44+
});
45+
} else if (item instanceof Node) {
46+
parent.appendChild(item);
47+
nodes.push(item);
48+
} else {
49+
const node = document.createTextNode(item ?? '');
50+
parent.appendChild(node);
51+
nodes.push(node);
52+
}
53+
}
54+
55+
onCleanup(() => {
56+
for (const node of nodes) node.remove();
57+
});
58+
}
59+
3060
export function setAttr(el, name, value) {
3161
if (typeof value === 'function') {
3262
let prev = null;
@@ -42,9 +72,38 @@ export function setAttr(el, name, value) {
4272
}
4373
}
4474

45-
/**
46-
* Set event listeners.
47-
*/
4875
export function on(el, event, handler) {
4976
el.addEventListener(event, handler);
77+
onCleanup(() => el.removeEventListener(event, handler));
78+
}
79+
80+
export function insertHTML(parent, html) {
81+
parent.innerHTML = html;
82+
}
83+
84+
export function clear(parent) {
85+
while (parent.firstChild) {
86+
parent.removeChild(parent.firstChild);
87+
}
88+
}
89+
90+
// 🔥 JSX helper: auto wraps elements with props and children
91+
export function createElement(tag, props, ...children) {
92+
const el = typeof tag === 'function' ? tag(props || {}) : document.createElement(tag);
93+
94+
if (props) {
95+
for (let [key, value] of Object.entries(props)) {
96+
if (key.startsWith('on')) {
97+
on(el, key.slice(2).toLowerCase(), value);
98+
} else {
99+
setAttr(el, key, value);
100+
}
101+
}
102+
}
103+
104+
for (let child of children) {
105+
insert(el, child);
106+
}
107+
108+
return el;
50109
}

runtime/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// runtime/index.js
2+
import * as dom from './dom.js';
3+
import * as signals from './signals.js';
4+
5+
export {
6+
dom,
7+
signals
8+
};

runtime/signals.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// runtime/signals.js
2+
3+
export function signal(initial) {
4+
let value = initial;
5+
const subs = new Set();
6+
7+
const read = () => {
8+
if (currentSubscriber) subs.add(currentSubscriber);
9+
return value;
10+
};
11+
12+
const write = newValue => {
13+
if (newValue === value) return;
14+
value = newValue;
15+
for (const sub of subs) sub();
16+
};
17+
18+
return [read, write];
19+
}
20+
21+
let currentSubscriber = null;
22+
23+
/**
24+
* Tracks a reactive function.
25+
* @param {Function} fn - effect that uses signals
26+
*/
27+
export function effect(fn) {
28+
const wrapped = () => {
29+
cleanup(wrapped);
30+
currentSubscriber = wrapped;
31+
fn();
32+
currentSubscriber = null;
33+
};
34+
wrapped.deps = [];
35+
wrapped();
36+
}
37+
38+
function cleanup(effectFn) {
39+
for (const dep of effectFn.deps || []) {
40+
dep.delete(effectFn);
41+
}
42+
effectFn.deps = [];
43+
}

0 commit comments

Comments
 (0)