Compare commits

...

2 Commits

Author SHA1 Message Date
b2c6b8d398 Add on-off in router 2026-04-08 01:30:23 +02:00
4a9707819d Add Mount 2026-04-08 01:27:23 +02:00

View File

@@ -1,15 +1,15 @@
const isFn = (v) => typeof v === 'function'; const isFn = (v) => typeof v === 'function';
const isNode = (v) => v instanceof Node; const isNode = (v) => v instanceof Node;
let isScheduled = false; let isScheduled = false, activeEffect = null, context = null;
const queue = new Set(); const queue = new Set(), reactiveCache = new WeakMap();
const tick = () => { const tick = () => {
queue.forEach(fn => fn()); queue.forEach(fn => fn());
queue.clear(); queue.clear();
isScheduled = false; isScheduled = false;
} }
let activeEffect = null;
export const effect = (fn, is_scope = false) => { export const effect = (fn, is_scope = false) => {
let cleanup = null; let cleanup = null;
const run = () => { const run = () => {
@@ -22,10 +22,7 @@ export const effect = (fn, is_scope = false) => {
run.e.forEach(subs => subs.delete(run)); run.e.forEach(subs => subs.delete(run));
run.e.clear(); run.e.clear();
if (isFn(cleanup)) cleanup(); if (isFn(cleanup)) cleanup();
if (run.c) { if (run.c) { run.c.forEach(s => s()); run.c.length = 0; }
run.c.forEach(s => s());
run.c.length = 0;
}
} }
run.e = new Set(); run.e = new Set();
if (is_scope) run.c = []; if (is_scope) run.c = [];
@@ -34,8 +31,6 @@ export const effect = (fn, is_scope = false) => {
return stop; return stop;
} }
export const scope = f => effect(f, true);
const track = (subs) => { const track = (subs) => {
if (activeEffect && !activeEffect.c) { if (activeEffect && !activeEffect.c) {
subs.add(activeEffect); subs.add(activeEffect);
@@ -48,10 +43,10 @@ export const signal = (value) => {
return { return {
_isSig: true, _isSig: true,
get value() { track(subs); return value; }, get value() { track(subs); return value; },
set value(newValue) { set value(v) {
if (newValue === value) return; if (v === value) return;
value = newValue; value = v;
subs.forEach(fn => queue.add(fn)); subs.forEach(f => queue.add(f));
if (!isScheduled) { isScheduled = true; queueMicrotask(tick); } if (!isScheduled) { isScheduled = true; queueMicrotask(tick); }
} }
} }
@@ -60,9 +55,9 @@ export const signal = (value) => {
export const untrack = (fn) => { export const untrack = (fn) => {
const prev = activeEffect; const prev = activeEffect;
activeEffect = null; activeEffect = null;
const result = fn(); const res = fn();
activeEffect = prev; activeEffect = prev;
return result; return res;
} }
export const computed = (fn) => { export const computed = (fn) => {
@@ -71,21 +66,20 @@ export const computed = (fn) => {
return { get value() { return sig.value; } }; return { get value() { return sig.value; } };
} }
const reactiveCache = new WeakMap();
export const reactive = (obj) => { export const reactive = (obj) => {
if (reactiveCache.has(obj)) return reactiveCache.get(obj); if (reactiveCache.has(obj)) return reactiveCache.get(obj);
const subs = {}; const subs = {};
const proxy = new Proxy(obj, { const proxy = new Proxy(obj, {
get(t, key) { get(t, k) {
track(subs[key] ??= new Set()); track(subs[k] ??= new Set());
const val = t[key]; const v = t[k];
return (val && typeof val === 'object') ? reactive(val) : val; return (v && typeof v === 'object') ? reactive(v) : v;
}, },
set(t, key, val) { set(t, k, v) {
if (t[key] === val) return true; if (t[k] === v) return true;
t[key] = val; t[k] = v;
if (subs[key]) { if (subs[k]) {
subs[key].forEach(fn => queue.add(fn)); subs[k].forEach(f => queue.add(f));
if (!isScheduled) { isScheduled = true; queueMicrotask(tick); } if (!isScheduled) { isScheduled = true; queueMicrotask(tick); }
} }
return true; return true;
@@ -99,51 +93,42 @@ export const persist = (key, target) => {
const saved = localStorage.getItem(key); const saved = localStorage.getItem(key);
if (saved !== null) { if (saved !== null) {
const data = JSON.parse(saved); const data = JSON.parse(saved);
if (target._isSig) target.value = data; target._isSig ? (target.value = data) : Object.assign(target, data);
else Object.assign(target, data);
} }
effect(() => { effect(() => localStorage.setItem(key, JSON.stringify(target._isSig ? target.value : target)));
const val = target._isSig ? target.value : target;
localStorage.setItem(key, JSON.stringify(val));
});
return target; return target;
}; };
export const storage = (key, val) => persist(key, signal(val)); export const storage = (key, val) => persist(key, signal(val));
export const watch = (source, cb) => { export const watch = (source, cb) => {
let first = true, oldValue; let first = true, old;
return effect(() => { return effect(() => {
const newValue = isFn(source) ? source() : source.value; const val = isFn(source) ? source() : source.value;
if (!first) untrack(() => cb(newValue, oldValue)); if (!first) untrack(() => cb(val, old));
else first = false; first = false; old = val;
oldValue = newValue;
}); });
} }
let context = null; export const onMount = (f) => context?.m.push(f);
export const onMount = (fn) => context?.m.push(fn); export const onUnmount = (f) => context?.u.push(f);
export const onUnmount = (fn) => context?.u.push(fn); export const provide = (k, v) => context && (context.p[k] = v);
export const provide = (key, value) => context && (context.p[key] = value); export const inject = (k, d) => context && (k in context.p ? context.p[k] : d);
export const inject = (key, dft) => context && (key in context.p ? context.p[key] : dft);
const remove = async (node) => { const remove = async (n) => {
if (Array.isArray(node)) return Promise.all(node.map(remove)); if (Array.isArray(n)) return Promise.all(n.map(remove));
if (node.$off) await node.$off(node); if (n.$off) await n.$off(n);
else if (node.$l) await new Promise(res => node.$l(res)); else if (n.$l) await new Promise(r => n.$l(r));
node.$s?.(); n.$s?.();
if (node.$c) node.$c.u.forEach(f => f()); if (n.$c) n.$c.u.forEach(f => f());
node.remove(); n.remove();
} }
const render = (fn, ...data) => { const render = (fn, ...d) => {
let node; let n;
const stop = effect(() => { const s = effect(() => { n = fn(...d); if (isFn(n)) n = n(); }, true);
node = fn(...data); if (n) n.$s = s;
if (isFn(node)) node = node(); return n;
}, true);
if (node) node.$s = stop;
return node;
} }
export const h = (tag, props = {}, ...children) => { export const h = (tag, props = {}, ...children) => {
@@ -153,75 +138,74 @@ export const h = (tag, props = {}, ...children) => {
context = { m: [], u: [], p: { ...(prev?.p || {}) } }; context = { m: [], u: [], p: { ...(prev?.p || {}) } };
const ctx = context; const ctx = context;
let el; let el;
const stop = effect(() => { const s = effect(() => {
el = tag(props, { children, emit: (evt, ...args) => props[`on${evt[0].toUpperCase()}${evt.slice(1)}`]?.(...args) }); el = tag(props, { children, emit: (e, ...a) => props[`on${e[0].toUpperCase()}${e.slice(1)}`]?.(...a) });
return () => ctx.u.forEach(f => f()); return () => ctx.u.forEach(f => f());
}, true); }, true);
const out = isNode(el) ? el : document.createTextNode(String(el)); const out = isNode(el) ? el : document.createTextNode(String(el));
out.$c = ctx; out.$c = ctx; out.$s = s;
out.$s = stop;
if (props.on) out.$on = props.on; if (props.on) out.$on = props.on;
if (props.off) out.$off = props.off; if (props.off) out.$off = props.off;
context = prev; context = prev;
return out; return out;
} }
if (!tag) return children; if (!tag) return children;
const isSvg = tag === 'svg' || tag === 'path' || tag === 'circle'; const isSvg = /^(svg|path|circle|rect)$/.test(tag);
const el = isSvg ? document.createElementNS("http://www.w3.org/2000/svg", tag) : document.createElement(tag); const el = isSvg ? document.createElementNS("http://www.w3.org/2000/svg", tag) : document.createElement(tag);
for (const key in props) { for (const k in props) {
const val = props[key]; const v = props[k];
if (key.startsWith('on') && key !== 'on' && key !== 'off') el.addEventListener(key.slice(2).toLowerCase(), val); if (k.startsWith('on') && k !== 'on' && k !== 'off') el.addEventListener(k.slice(2).toLowerCase(), v);
else if (key === "ref") isFn(val) ? val(el) : val.value = el; else if (k === "ref") isFn(v) ? v(el) : v.value = el;
else if (key === "on") el.$on = val; else if (k === "on") el.$on = v;
else if (key === "off") el.$off = val; else if (k === "off") el.$off = v;
else if (isFn(val)) effect(() => el[key] = val()); else if (isFn(v)) effect(() => el[k] = v());
else el[key] = val; else el[k] = v;
} }
children.forEach(child => append(el, child)); children.forEach(c => append(el, c));
return el; return el;
} }
const append = (parent, child) => { const append = (p, c) => {
if (child == null) return; if (c == null) return;
if (isFn(child)) { if (isFn(c)) {
const anchor = document.createTextNode(''); const anchor = document.createTextNode('');
parent.appendChild(anchor); p.appendChild(anchor);
let nodes = []; let nodes = [];
effect(async () => { effect(async () => {
const raw = [child()].flat(Infinity).filter(n => n != null); const raw = [c()].flat(Infinity).filter(n => n != null);
const newNodes = raw.map(n => isNode(n) ? n : document.createTextNode(String(n))); const next = raw.map(n => isNode(n) ? n : document.createTextNode(String(n)));
for (const n of nodes) { if (!newNodes.includes(n)) await remove(n); } for (const n of nodes) { if (!next.includes(n)) await remove(n); }
newNodes.forEach((n, i) => { next.forEach((n, i) => {
if (!nodes.includes(n)) { if (!nodes.includes(n)) {
parent.insertBefore(n, newNodes[i+1] || anchor); p.insertBefore(n, next[i+1] || anchor);
if (n.$on) n.$on(n); if (n.$on) n.$on(n);
if (n.$c) n.$c.m.forEach(f => f()); if (n.$c) n.$c.m.forEach(f => f());
} }
}); });
nodes = newNodes; nodes = next;
}, true); }, true);
} else { } else {
const n = isNode(child) ? child : document.createTextNode(String(child)); const n = isNode(c) ? c : document.createTextNode(String(c));
parent.appendChild(n); p.appendChild(n);
if (n.$on) n.$on(n); if (n.$on) n.$on(n);
} }
} }
export const If = (cond, renderFn, fallback = null, transitions = {}) => { export const If = (cond, t, f = null, trans = {}) => {
let cached, current; let cached, current;
return () => { return () => {
const show = !!cond(); const show = !!cond();
if (show !== current) { if (show !== current) {
const update = async () => { const up = async () => {
if (cached) await remove(cached); if (cached) await remove(cached);
cached = show ? render(renderFn) : (isFn(fallback) ? render(fallback) : fallback); cached = show ? render(t) : (isFn(f) ? render(f) : f);
if (isNode(cached)) { if (isNode(cached)) {
if (transitions.on) cached.$on = transitions.on; if (trans.on) cached.$on = trans.on;
if (transitions.off) cached.$off = transitions.off; if (trans.off) cached.$off = trans.off;
} }
current = show; current = show;
}; };
update(); up();
} }
return cached; return cached;
} }
@@ -234,54 +218,49 @@ export const For = (list, key, renderFn) => {
const items = isFn(list) ? list() : (list.value || list); const items = isFn(list) ? list() : (list.value || list);
const res = items.map((item, i) => { const res = items.map((item, i) => {
const id = isFn(key) ? key(item, i) : (key ? item[id] : item); const id = isFn(key) ? key(item, i) : (key ? item[id] : item);
let node = cache.get(id); let n = cache.get(id);
if (!node) node = render(renderFn, item, i); if (!n) n = render(renderFn, item, i);
next.set(id, node); next.set(id, n);
return node; return n;
}); });
cache.forEach(async (node, id) => { if (!next.has(id)) await remove(node); }); cache.forEach(async (n, id) => { if (!next.has(id)) await remove(n); });
cache = next; cache = next;
return res; return res;
} }
} }
export const Router = (routes) => { export const Router = (routes, trans = {}) => {
const path = signal(window.location.hash.slice(1) || '/'); const path = signal(window.location.hash.slice(1) || '/');
window.onhashchange = () => path.value = window.location.hash.slice(1) || '/'; window.onhashchange = () => path.value = window.location.hash.slice(1) || '/';
return h('div', { class: 'router-view' }, () => { return h('div', { class: 'router-view' }, () => {
const route = routes.find(r => r.path === path.value) || routes.find(r => r.path === '*'); const p = path.value;
return route ? h(route.component) : null; const r = routes.find(x => x.path === p) || routes.find(x => x.path === '*');
return If(() => !!r, () => h(r.component, { path: p }), null, trans);
}); });
}; };
export const Component = ({ is, ...props }, { children }) => () => h(isFn(is) ? is() : is, props, children);
export const Transition = ({ enter: e, idle, leave: l }, { children: [c] }) => { export const Transition = ({ enter: e, idle, leave: l }, { children: [c] }) => {
const decorate = (el) => { const decorate = (el) => {
if (!isNode(el)) return el; if (!isNode(el)) return el;
const addClass = c => c && el.classList.add(...c.split(' ')); const add = (cl) => cl && el.classList.add(...cl.split(' '));
const removeClass = c => c && el.classList.remove(...c.split(' ')); const rem = (cl) => cl && el.classList.remove(...cl.split(' '));
el.$on = () => { el.$on = () => {
if (!e) return; if (!e) return;
requestAnimationFrame(() => { requestAnimationFrame(() => {
addClass(e[1]); add(e[1]);
requestAnimationFrame(() => { requestAnimationFrame(() => {
addClass(e[0]); removeClass(e[1]); addClass(e[2]); add(e[0]); rem(e[1]); add(e[2]);
el.addEventListener('transitionend', () => { el.addEventListener('transitionend', () => { rem(e[2]); rem(e[0]); add(idle); }, { once: true });
removeClass(e[2]); removeClass(e[0]); addClass(idle);
}, { once: true });
}); });
}); });
}; };
el.$off = (node) => { el.$off = () => {
if (!l) return node.remove(); if (!l) return el.remove();
return new Promise(res => { return new Promise(res => {
removeClass(idle); addClass(l[1]); rem(idle); add(l[1]);
requestAnimationFrame(() => { requestAnimationFrame(() => {
addClass(l[0]); removeClass(l[1]); addClass(l[2]); add(l[0]); rem(l[1]); add(l[2]);
el.addEventListener('transitionend', () => { el.addEventListener('transitionend', () => { rem(l[2]); rem(l[0]); res(); }, { once: true });
removeClass(l[2]); removeClass(l[0]); res();
}, { once: true });
}); });
}); });
}; };
@@ -290,10 +269,14 @@ export const Transition = ({ enter: e, idle, leave: l }, { children: [c] }) => {
return isFn(c) ? () => decorate(c()) : decorate(c); return isFn(c) ? () => decorate(c()) : decorate(c);
} }
export default (target, root, props) => { export const mount = (root, target, props = {}) => {
const container = typeof target === 'string' ? document.querySelector(target) : target;
if (container.firstElementChild) remove(container.firstElementChild);
const el = h(root, props); const el = h(root, props);
target.appendChild(el); container.appendChild(el);
if (el.$on) el.$on(el); if (el.$on) el.$on(el);
if (el.$c) el.$c.m.forEach(f => f()); if (el.$c) el.$c.m.forEach(f => f());
return () => remove(el); return () => remove(el);
} };
export default { signal, effect, reactive, computed, watch, persist, storage, h, mount, If, For, Router, Transition, onMount, onUnmount, provide, inject };