diff --git a/src/events.js b/src/events.js index 966d4fd..3511114 100644 --- a/src/events.js +++ b/src/events.js @@ -1,6 +1,6 @@ -import { isSignal } from './signals.js'; +import { isSignal, addSignalListener } from './signals.js'; export function on(event, listener, options){ - if(isSignal(event)) return event.listeners.add(listener); + if(isSignal(event)) return addSignalListener(event, listener); return element=> element.addEventListener(event, listener, options); } export function dispatch(event, detail){ diff --git a/src/signals.js b/src/signals.js index 1420926..6d17d44 100644 --- a/src/signals.js +++ b/src/signals.js @@ -3,15 +3,41 @@ export function isSignal(candidate){ try{ return Reflect.has(candidate, mark); } catch(e){ return false; } } -export function S(signal){ - if(typeof signal!=="function") - return create(signal); - if(isSignal(signal)) return signal; +export function addSignalListener(signal, listener){ + return signal[mark].listeners.add(listener); +} +export function S(value){ + if(typeof value!=="function") + return create(value); + if(isSignal(value)) return value; const out= create(); - watch(()=> out(signal())); + watch(()=> out(value())); return out; } +export function reactive(data){ + if(isSignal(data)) + return data; + if(typeof data!=="object" || data===null) + return create(data); + + let type; + if(Array.isArray(data)){ + type= "array"; + data= data.map(v=> reactive(v)); + } else if(data.toString()!=="[object Object]"){ + return create(data); + } else { + type= "object"; + data= Object.fromEntries( + Object.entries(data) + .map(([ key, value ])=> [ key, reactive(value) ]) + ); + } + const signal= (...value)=> + value.length ? write(signal, reactive(value[0])) : read(signal[mark]); + return createWrapObject(type, toSignal(signal, data)); +} const stack= []; export function watch(context){ stack.push(context); @@ -24,28 +50,57 @@ function currentContext(){ } function create(value){ if(isSignal(value)) return value; - - if(typeof value==="object" && value!==null) - //TODO Array? - return Object.fromEntries( - Object.entries(value) - .map(([ key, value ])=> [ key, create(value) ]) - ); - const signal= (...value)=> - value.length ? write(signal, value[0]) : read(signal); - Object.assign(signal, { - [mark]: true, + const signal= (...value)=> + value.length ? write(signal, value[0]) : read(signal[mark]); + return toSignal(signal, value); +} +function toSignal(signal, value){ + signal[mark]= { value, listeners: new Set() - }); + }; return signal; } +function createWrapObject(type, signal){ + return new Proxy(signal, { + set(_, p, newValue){ + const s= signal[mark]; + if(p in s.value){ + const v= s.value[p]; + if(isSignal(v)) return v(newValue); + return (s.value[p]= newValue); + } + const v= reactive(newValue); + s.value[p]= v; + s.listeners.forEach(fn=> fn(s.value)); + return v; + }, + deleteProperty(_, p){ + const s= signal[mark]; + Reflect.deleteProperty(s.value, p); + s.listeners.forEach(fn=> fn(s.value)); + }, + get(_, p){ + if(mark===p) return signal[mark]; + if("array"!==type || !(p in Array.prototype) || p==="length") + return Reflect.get(signal[mark].value, p); + return (...a)=> { + const s= signal[mark]; + const result= Array.prototype[p].call(s.value, ...a); + //TODO optimize! + s.value.forEach((v, i)=> Reflect.set(s.value, i, reactive(v))); + s.listeners.forEach(fn=> fn(s.value)); + return result; + }; + } + }); +} function read({ value, listeners }){ const context= currentContext(); if(context) listeners.add(context); return value; } function write(signal, value){ - signal.value= value; - signal.listeners.forEach(fn=> fn(value)) + signal[mark].value= value; + signal[mark].listeners.forEach(fn=> fn(value)) } diff --git a/test/index.js b/test/index.js index 9e26d95..7c83861 100644 --- a/test/index.js +++ b/test/index.js @@ -1,5 +1,5 @@ -import { S, watch, el, namespace, assign, on, dispatch } from "../index.js"; -Object.assign(globalThis, { S, watch, el, namespace, assign, on, dispatch }); +import { S, reactive, watch, el, namespace, assign, on, dispatch } from "../index.js"; +Object.assign(globalThis, { S, reactive, watch, el, namespace, assign, on, dispatch }); const { style, css }= createStyle(); globalThis.test= console.log; @@ -21,7 +21,7 @@ function component({ name= "World", surname= "" }= {}){ margin-inline-start: .5em; } `; - const store= S({ name, surname }); + const store= reactive({ name, surname }); const full_name= S(()=> store.name()+" "+store.surname()); on(full_name, console.log);