113 lines
3.2 KiB
TypeScript
113 lines
3.2 KiB
TypeScript
type StoreOptions<TState extends Record<string, unknown> = Record<string, unknown>> = {
|
|
setup: () => TState;
|
|
};
|
|
|
|
const globalScope = new Map<unknown, unknown>();
|
|
|
|
const getSymbol = Symbol('getValue');
|
|
const storeSymbol = Symbol('htmlStore');
|
|
const undefinedSymbol = Symbol('undefinedValue');
|
|
|
|
const proxyObject = <T extends Record<string, unknown>>(init: T) => {
|
|
const emitter = new EventTarget();
|
|
|
|
const wrap = <T extends Record<string | number | symbol, unknown>>(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<string, unknown>);
|
|
},
|
|
set: (target: Record<string | number | symbol, unknown>, prop, value) => {
|
|
target[prop] = value;
|
|
emitter.dispatchEvent(new Event('change'));
|
|
return true;
|
|
},
|
|
});
|
|
|
|
const state = wrap(init);
|
|
return { state, emitter };
|
|
};
|
|
|
|
const store = <TState extends Record<string, unknown>>(options: StoreOptions<TState>) => {
|
|
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<unknown, unknown>;
|
|
if (!localStore.has(this)) {
|
|
continue;
|
|
}
|
|
return localStore.get(this) as ReturnType<typeof create>;
|
|
}
|
|
elm = elm.parentElement || undefined;
|
|
}
|
|
if (!globalScope.has(this)) {
|
|
globalScope.set(this, create());
|
|
}
|
|
return globalScope.get(this) as ReturnType<typeof create>;
|
|
};
|
|
|
|
return {
|
|
create,
|
|
get,
|
|
};
|
|
};
|
|
|
|
type Store<TState extends Record<string, unknown> = Record<string, unknown>> = ReturnType<typeof store<TState>>;
|
|
|
|
const htmlElementContext = (htmlElement: HTMLElement) => {
|
|
const emitter = new EventTarget();
|
|
return {
|
|
emitter,
|
|
get: <TStore extends Store>(store: TStore) => {
|
|
const context = store.get(htmlElement);
|
|
context.emitter.addEventListener('change', () => {
|
|
emitter.dispatchEvent(new Event('change'));
|
|
});
|
|
return context.state;
|
|
},
|
|
};
|
|
};
|
|
|
|
type HtmlElementContext = ReturnType<typeof htmlElementContext>;
|
|
|
|
const value = <T>(init: T) =>
|
|
store({
|
|
setup: () => ({ value: init }),
|
|
});
|
|
|
|
function getWithStore<T>(value: T) {
|
|
if (value && value !== null && typeof value === 'object' && getSymbol in value) {
|
|
return (value[getSymbol] as () => unknown)();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
const expandTemplateStringFromStore = <T = unknown>(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 };
|