type StoreOptions = Record> = { setup: () => TState; }; const globalScope = new Map(); const getSymbol = Symbol('getValue'); const storeSymbol = Symbol('htmlStore'); const undefinedSymbol = Symbol('undefinedValue'); const proxyObject = >(init: T) => { const emitter = new EventTarget(); const wrap = >(obj: T) => new Proxy(obj, { get: (target, prop) => { if (prop === getSymbol) { return target; } const value = target[prop]; if (!value || typeof value !== 'object') { return Object.assign(value ?? undefinedSymbol, { [getSymbol]: () => target[prop], }); } return wrap(value as Record); }, set: (target: Record, prop, value) => { target[prop] = value; emitter.dispatchEvent(new Event('change')); return true; }, }); const state = wrap(init); return { state, emitter }; }; const store = >(options: StoreOptions) => { const create = () => { const emitter = new EventTarget(); const stateProxy = proxyObject(options.setup()); stateProxy.emitter.addEventListener('change', () => { emitter.dispatchEvent(new Event('change')); }); return { emitter, state: stateProxy.state, }; }; const get = (elm?: HTMLElement) => { while (elm) { if (storeSymbol in elm) { const localStore = elm[storeSymbol] as Map; if (!localStore.has(this)) { continue; } return localStore.get(this) as ReturnType; } elm = elm.parentElement || undefined; } if (!globalScope.has(this)) { globalScope.set(this, create()); } return globalScope.get(this) as ReturnType; }; return { create, get, }; }; type Store = Record> = ReturnType>; const htmlElementContext = (htmlElement: HTMLElement) => { const emitter = new EventTarget(); return { emitter, get: (store: TStore) => { const context = store.get(htmlElement); context.emitter.addEventListener('change', () => { emitter.dispatchEvent(new Event('change')); }); return context.state; }, }; }; type HtmlElementContext = ReturnType; const value = (init: T) => store({ setup: () => ({ value: init }), }); function getWithStore(value: T) { if (value && value !== null && typeof value === 'object' && getSymbol in value) { return (value[getSymbol] as () => unknown)(); } return value; } const expandTemplateStringFromStore = (strings: TemplateStringsArray, ...values: T[]) => { return strings.flatMap((current, i) => { return [current, getWithStore(values[i]) ?? '']; }); }; export type { HtmlElementContext, Store }; export { store, value, htmlElementContext, getWithStore, expandTemplateStringFromStore, storeSymbol };