import { DataObject, JsxChildren, VNode } from 'dom-renderer'; import { autorun, IReactionDisposer, IReactionPublic, reaction as watch } from 'mobx'; import { CustomElement, isHTMLElementClass, parseJSON, toCamelCase, toHyphenCase } from 'web-utility'; import { FunctionCell } from './Async'; import { getMobxData } from './utility'; import { ClassComponent, WebCell } from './WebCell'; export type PropsWithChildren

= P & { children?: JsxChildren; }; export type FunctionComponent

= (props: P) => VNode; export type AsyncFunctionComponent

= (props: P) => Promise; export type FC

= FunctionComponent

; export type AFC

= AsyncFunctionComponent

; interface ReactionItem { expression: ReactionExpression; effect: (...data: any[]) => any; } const reactionMap = new WeakMap(); function wrapClass(Component: T) { class ObserverComponent extends (Component as ClassComponent) implements CustomElement { static observedAttributes = []; protected disposers: IReactionDisposer[] = []; get props() { return getMobxData(this); } constructor() { super(); Promise.resolve().then(() => this.#boot()); } update = () => { const { update } = Object.getPrototypeOf(this); return new Promise(resolve => this.disposers.push(autorun(() => update.call(this).then(resolve))) ); }; #boot() { const names: string[] = this.constructor['observedAttributes'] || [], reactions = reactionMap.get(this) || []; this.disposers.push( ...names.map(name => autorun(() => this.syncPropAttr(name))), ...reactions.map(({ expression, effect }) => watch(reaction => expression(this, reaction), effect.bind(this)) ) ); } disconnectedCallback() { for (const disposer of this.disposers) disposer(); this.disposers.length = 0; super['disconnectedCallback']?.(); } setAttribute(name: string, value: string) { const old = super.getAttribute(name), names: string[] = this.constructor['observedAttributes']; super.setAttribute(name, value); if (names.includes(name)) this.attributeChangedCallback(name, old, value); } attributeChangedCallback(name: string, old: string, value: string) { this[toCamelCase(name)] = parseJSON(value); super['attributeChangedCallback']?.(name, old, value); } syncPropAttr(name: string) { let value = this[toCamelCase(name)]; if (!(value != null) || value === false) return this.removeAttribute(name); value = value === true ? name : value; if (typeof value === 'object') { value = value.toJSON?.(); value = typeof value === 'object' ? JSON.stringify(value) : value; } super.setAttribute(name, value); } } return ObserverComponent as unknown as T; } export type WebCellComponent = FunctionComponent | ClassComponent; export type ComponentType

= FC

| (WebCell

& ClassComponent); export type ComponentProps = C extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[C] : C extends FC ? P : C extends WebCell & ClassComponent ? P : never; export type ObservableComponent = WebCellComponent | AsyncFunctionComponent; export type AwaitedComponent = T extends ( props: infer P ) => Promise ? (props: P) => R : T; /** * `class` decorator of Web components for MobX */ export function observer( func: T, _: ClassDecoratorContext ): AwaitedComponent; export function observer(func: T): AwaitedComponent; export function observer(func: T, _?: ClassDecoratorContext) { return isHTMLElementClass(func) ? wrapClass(func) : (props: object) => func(props)) as FC | AFC} />; } /** * `accessor` decorator of MobX `@observable` for HTML attributes */ export function attribute( _: ClassAccessorDecoratorTarget, { name, addInitializer }: ClassAccessorDecoratorContext ) { addInitializer(function () { const names: string[] = this.constructor['observedAttributes'], attribute = toHyphenCase(name.toString()); if (!names.includes(attribute)) names.push(attribute); }); } export type ReactionExpression = (data: I, reaction: IReactionPublic) => O; export type ReactionEffect = (newValue: V, oldValue: V, reaction: IReactionPublic) => any; /** * Method decorator of [MobX `reaction()`](https://mobx.js.org/reactions.html#reaction) * * @example * ```tsx * import { observable } from 'mobx'; * import { component, observer, reaction } from 'web-cell'; * * @component({ tagName: 'my-tag' }) * @observer * export class MyTag extends HTMLElement { * @observable * accessor count = 0; * * @reaction(({ count }) => count) * handleCountChange(newValue: number, oldValue: number) { * console.log(`Count changed from ${oldValue} to ${newValue}`); * } * * render() { * return ( * * ); * } * } * ``` */ export const reaction = (expression: ReactionExpression) => (effect: ReactionEffect, { addInitializer }: ClassMethodDecoratorContext) => addInitializer(function () { const reactions = reactionMap.get(this) || []; reactions.push({ expression, effect }); reactionMap.set(this, reactions); });