init
This commit is contained in:
31
lib/component.ts
Normal file
31
lib/component.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { Html } from './html.js';
|
||||
import { htmlElementContext, type HtmlElementContext } from './store.js';
|
||||
|
||||
type ComponentOptions<T> = (options: { props: T; context: HtmlElementContext['get'] }) => Html;
|
||||
|
||||
const component = <T>(options: ComponentOptions<T>) => {
|
||||
const render = (props: T) => {
|
||||
const toHtmlElement = () => {
|
||||
const root = document.createElement('div');
|
||||
root.style.display = 'content';
|
||||
const context = htmlElementContext(root);
|
||||
const api = { context: context.get, props };
|
||||
const view = options(api);
|
||||
context.emitter.addEventListener('change', () => {
|
||||
view(root);
|
||||
});
|
||||
view(root);
|
||||
return root;
|
||||
};
|
||||
|
||||
return {
|
||||
toHtmlElement,
|
||||
};
|
||||
};
|
||||
|
||||
return Object.assign(render, {
|
||||
toHtmlElement: () => render({} as T).toHtmlElement(),
|
||||
});
|
||||
};
|
||||
|
||||
export { component };
|
||||
3
lib/exports.ts
Normal file
3
lib/exports.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './component.js';
|
||||
export * from './store.js';
|
||||
export * from './html.js';
|
||||
82
lib/html.ts
Normal file
82
lib/html.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { expandTemplateStringFromStore, storeSymbol, type Store } from './store.js';
|
||||
|
||||
const toHtmlElement = (input: unknown): HTMLElement | Text => {
|
||||
if (input instanceof HTMLElement || input instanceof Text) {
|
||||
return input;
|
||||
}
|
||||
const toHtmlProperty = Object.getOwnPropertyDescriptor(input, 'toHtmlElement');
|
||||
if (toHtmlProperty && typeof toHtmlProperty.value === 'function') {
|
||||
const elm = toHtmlProperty.value();
|
||||
return elm;
|
||||
}
|
||||
if (typeof input === 'object') {
|
||||
return document.createTextNode(JSON.stringify(input));
|
||||
}
|
||||
if (input === undefined) {
|
||||
return document.createTextNode('');
|
||||
}
|
||||
return document.createTextNode(String(input));
|
||||
};
|
||||
|
||||
const html = (tag: string | (() => HTMLElement)) => {
|
||||
const render =
|
||||
(options: Record<string, unknown>) =>
|
||||
(strings: TemplateStringsArray, ...params: unknown[]) => {
|
||||
const fn = (elm: HTMLElement) => {
|
||||
for (const [event, listener] of Object.entries(options)) {
|
||||
if (typeof listener === 'function') {
|
||||
elm.addEventListener(event, () => listener());
|
||||
}
|
||||
}
|
||||
|
||||
if ('style' in options) {
|
||||
for (const [name, value] of Object.entries(options.style as any)) {
|
||||
(elm.style as any)[name] = value;
|
||||
}
|
||||
}
|
||||
const elements = expandTemplateStringFromStore(strings, ...params);
|
||||
const htmlElements = elements.map(toHtmlElement).filter(Boolean);
|
||||
|
||||
elm.innerHTML = '';
|
||||
for (const element of htmlElements) {
|
||||
elm.appendChild(element);
|
||||
}
|
||||
};
|
||||
|
||||
return Object.assign(fn, {
|
||||
toHtmlElement: () => {
|
||||
const root = typeof tag === 'function' ? tag() : document.createElement(tag);
|
||||
fn(root);
|
||||
return root;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return Object.assign((strings: TemplateStringsArray, ...params: unknown[]) => render({})(strings, ...params), {
|
||||
$:
|
||||
(props: Record<string, unknown>) =>
|
||||
(strings: TemplateStringsArray, ...params: unknown[]) =>
|
||||
render(props)(strings, ...params),
|
||||
});
|
||||
};
|
||||
|
||||
type Html = ReturnType<ReturnType<typeof html>>;
|
||||
|
||||
const div = html('div');
|
||||
const h1 = html('h1');
|
||||
const p = html('p');
|
||||
const button = html('button');
|
||||
const scope = (...stores: Store[]) =>
|
||||
html(() => {
|
||||
const elm = document.createElement('div');
|
||||
elm.style.display = 'content';
|
||||
const storeMap = new Map();
|
||||
(elm as any)[storeSymbol] = storeMap;
|
||||
for (const store of stores) {
|
||||
storeMap.set(store, store.create());
|
||||
}
|
||||
return elm;
|
||||
});
|
||||
|
||||
export type { Html };
|
||||
export { html, div, h1, p, button, scope };
|
||||
112
lib/store.ts
Normal file
112
lib/store.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user