import type { ReactiveControllerHost, ReactiveController } from 'lit'; import { Ref, RefObject } from 'preact'; import { intercept, Context, ComponentCtrl, Props, ComponentCtrlGetter } from 'js-widgets'; // === types ========================================================= type Action = (...args: A) => R; type ContextType> = C extends Context ? T : never; // === exports ======================================================= export { afterMount, consume, create, createMemo, createTicker, effect, getRefresher, mutable, optimizeUpdates, preset, stateFn, stateObj, stateObj as state, // not sure about the final name stateRef, stateVal, stateVal as atom, // not sure about the final name interval, handleMethods, handlePromise, withDefaults }; // === types ========================================================= type Getter = () => T; type Updater = Exclude | ((value: T) => U); type Setter = (updater: Updater) => void; type StateObjSetter> = { (updater: Updater>): void; } & { [K in keyof T]: (updater: Updater) => void; }; // === local data ==================================================== let getCurrCtrl: ComponentCtrlGetter | null = null; // === interception logic ============================================ function getCtrl() { if (!getCurrCtrl) { throw Error('Extension has been called outside of component function'); } return getCurrCtrl(2); } intercept({ onInit(next, id, getCtrl) { try { getCurrCtrl = getCtrl; next(); } finally { getCurrCtrl = null; } } }); // === extensions ==================================================== // --- withDefaults -------------------------------------------------- function withDefaults

>( props: P, defaults: D | (() => D) ): P & D { const ret: any = {}; const preactClass = (props as any)?.constructor?.__preactClass; if (typeof preactClass !== 'function') { throw new TypeError('Illegal first argument for function `preset`'); } let defaultValues: D | null = null; const ctrl = getCtrl(); if (typeof defaults !== 'function') { defaultValues = defaults; } else { defaultValues = preactClass.__defaults; if (!defaultValues) { defaultValues = defaults(); preactClass.__defaults = defaultValues; } } const reassign = () => { for (const key in ret) { delete ret[key]; } Object.assign(ret, defaultValues, props); }; ctrl.beforeUpdate(reassign); reassign(); return ret; } // --- preset -------------------------------------------------------- function preset

>( props: P, defaults: D | (() => D) ): asserts props is P & D { const preactClass = (props as any)?.constructor?.__preactClass; if (typeof preactClass !== 'function') { throw new TypeError('Illegal first argument for function `setDefaults`'); } let defaultValues: D | null = null; const ctrl = getCtrl(); if (typeof defaults !== 'function') { defaultValues = defaults; } else { defaultValues = preactClass.__defaults; if (!defaultValues) { defaultValues = defaults(); preactClass.__defaults = defaultValues; } } (props.constructor as any).__defaults = defaultValues; for (const key in defaultValues) { if (!props.hasOwnProperty(key)) { (props as any)[key] = defaultValues[key]; } } } // --- optimizeUpdates ----------------------------------------------- function optimizeUpdates(pred?: () => boolean): void { const ctrl = getCtrl(); if (!pred) { ctrl.shouldUpdate((prevProps, nextProps) => { for (const key in prevProps) if (!(key in nextProps)) return true; for (const key in nextProps) if (prevProps[key] !== nextProps[key]) return true; return false; }); } else { ctrl.shouldUpdate(() => pred!()); } } // --- getRefresher -------------------------------------------------- function getRefresher(): (force?: boolean) => void { return getCtrl().getUpdater(); } // --- stateVal ------------------------------------------------------ function stateVal(value: T): [Getter, Setter] { const ctrl = getCtrl(); const update = ctrl.getUpdater(); let currVal: T = value; let nextVal: T = value; const getter = () => currVal; const setter: Setter = (valueOrMapper): void => { nextVal = typeof valueOrMapper === 'function' ? (valueOrMapper as any)(nextVal) : valueOrMapper; update(); }; ctrl.beforeUpdate(() => void (currVal = nextVal)); return [getter, setter]; } function stateObj>( values: T ): [T, StateObjSetter] { const ctrl = getCtrl(); const update = ctrl.getUpdater(); const obj = { ...values }; const clone = { ...values }; let merge = false; ctrl.beforeUpdate(() => { if (merge) { Object.assign(obj, clone); merge = false; } }); const setter: StateObjSetter = ((updater: Updater>) => { const values = typeof updater === 'function' ? (updater as any)(clone) : updater; Object.assign(clone, values); merge = true; update(); }) as any; for (const key of Object.keys(obj)) { (setter as any)[key] = (updater: Updater) => { (clone as any)[key] = typeof updater === 'function' ? (updater as any)(clone[key]) : updater; merge = true; update(); }; } return [obj, setter]; } // --- stateFn ------------------------------------------------------- function stateFn(initialValue: T): { (): T; (updater: Updater): void; } { let current = initialValue; let next = initialValue; const ctrl = getCtrl(); const update = ctrl.getUpdater(); ctrl.beforeUpdate(() => { current = next; }); return function (updater?: Updater) { if (arguments.length === 0) { return current; } else { next = typeof updater === 'function' ? (updater as any)(current) : updater; update(); } } as any; } // --- stateRef ------------------------------------------------------ function stateRef(initialValue: T): { current: T; set(value: T): void; map(mapper: (value: T) => T): void; } { let current = initialValue; let next = initialValue; const ctrl = getCtrl(); const update = ctrl.getUpdater(); ctrl.beforeUpdate(() => { current = next; }); return { get current() { return current; }, set(value) { next = value; update(); }, map(mapper) { next = mapper(next); update(); } }; } // --- mutable ------------------------------------------------------- function mutable>(initialState: T): T { const ret = {} as T; const values = { ...initialState }; const update = getCtrl().getUpdater(); for (const key of Object.keys(initialState)) { Object.defineProperty(ret, key, { get() { return values[key]; }, set(value: any) { (values as any)[key] = value; update(); } }); } return ret; } // --- createMemo ---------------------------------------------------- // TODO - this is not really optimized, is it? function createMemo A>( getValue: (...args: ReturnType) => T, getDeps: G ) { const ctrl = getCtrl(); let oldDeps: any[], value: T; const memo = { get value() { const newDeps = getDeps(); if (!oldDeps || !isEqualArray(oldDeps, newDeps)) { value = getValue.apply(null, newDeps as any); // TODO } oldDeps = newDeps; return value; } }; return memo; } // --- afterMount ---------------------------------------------------- function afterMount(action: () => void | (() => void)): void { const ctrl = getCtrl(); let cleanup: null | (() => void) = null; ctrl.afterMount(() => { const result = action(); if (typeof result === 'function') { cleanup = result; } }); ctrl.beforeUnmount(() => { if (cleanup) { cleanup(); cleanup = null; } }); } // --- effect -------------------------------------------------------- function effect( action: () => void | undefined | null | (() => void), getDeps?: null | (() => any[]) ): void { const ctrl = getCtrl(); let oldDeps: any[] | null = null, cleanup: Action | null | undefined | void; if (getDeps === null) { ctrl.afterMount(() => { cleanup = action(); }); ctrl.beforeUnmount(() => { cleanup && cleanup(); }); } else if (getDeps === undefined || typeof getDeps === 'function') { const callback = () => { let needsAction = getDeps === undefined; if (!needsAction) { const newDeps = getDeps!(); needsAction = oldDeps === null || newDeps === null || !isEqualArray(oldDeps, newDeps); oldDeps = newDeps; } if (needsAction) { cleanup && cleanup(); cleanup = action(); } }; ctrl.afterMount(callback); ctrl.afterUpdate(callback); } else { throw new TypeError( '[effect] Third argument must either be undefined, null or a function' ); } } // --- consume ------------------------------------------------------- function consume(context: Context): () => T; function consume>>( contexts: T ): { [K in keyof T]: ContextType }; function consume(ctx: any): any { const ctrl = getCtrl(); if (ctx && ctx.constructor && ctx.constructor.__preactCtx) { return ctrl.consumeContext(ctx); } const ret = {}; for (const key of Object.keys(ctx)) { const get = ctrl.consumeContext(ctx[key]); Object.defineProperty(ctx, key, { get }); } return ret; } // --- handleMethods ------------------------------------------------- function updateRef(ref: Ref | undefined, value: T | null): void { if (ref) { if (typeof ref === 'function') { ref(value); } else { ref.current = value; } } } function handleMethods>( getMethodsRef: () => RefObject | undefined, methods: M ) { const ctrl = getCtrl(); let ref: RefObject | undefined = getMethodsRef(); updateRef(ref, methods); ctrl.beforeUpdate(() => { const newRef = getMethodsRef(); if (newRef !== ref) { updateRef(ref, null); ref = newRef; updateRef(ref, methods); } }); ctrl.beforeUnmount(() => { updateRef(ref, null); }); } // --- interval ------------------------------------------------------ function interval( callbackOrRef: (() => void) | { current: () => void }, delayOrGetDelay: number | (() => number) ) { const getCallback = typeof callbackOrRef === 'function' ? () => callbackOrRef : () => callbackOrRef.current; const getDelay = typeof delayOrGetDelay === 'function' ? delayOrGetDelay : () => delayOrGetDelay; effect( () => { const id = setInterval(getCallback(), getDelay()); return () => clearInterval(id); }, () => [getCallback(), getDelay()] ); } // --- handlePromise --------------------------------------------------- type PromiseRes = | { result: null; error: null; state: 'pending'; } | { result: T; error: null; state: 'resolved'; } | { result: null; error: Error; state: 'rejected'; }; const initialState: PromiseRes = { result: null, error: null, state: 'pending' }; function handlePromise( getPromise: () => Promise, getDeps?: () => any[] ): PromiseRes { const ctrl = getCtrl(); const [getState, setState] = stateVal>(initialState); let promiseIdx = -1; effect( () => { ++promiseIdx; if (getState().state !== 'pending') { setState(initialState); } const myPromiseIdx = promiseIdx; getPromise() .then((result) => { if (promiseIdx === myPromiseIdx) { setState({ result, error: null, state: 'resolved' }); } }) .catch((error) => { if (promiseIdx === myPromiseIdx) { setState({ result: null, error: error instanceof Error ? error : new Error(String(error)), state: 'rejected' }); } }); }, typeof getDeps === 'function' ? getDeps : null ); return getState(); } // --- ticker -------------------------------------------------------- function createTicker(): () => Date; function createTicker(mapper: (time: Date) => T): () => T; function createTicker(mapper?: (time: Date) => any): any { const now = () => new Date(); const [getTime, setTime] = stateVal(now()); interval(() => { setTime(now()); }, 1000); return mapper ? () => mapper(getTime()) : getTime(); } // === create ======================================================== type ControllerClass = { new (host: ReactiveControllerHost, ...args: A): T; }; function create, A extends any[]>( controllerClass: C, ...args: A ): InstanceType { const ctrl = getCtrl(); const host = new Host(ctrl); return new controllerClass(host, ...args); } class Host implements ReactiveControllerHost { #update: () => void; #controllers = new Set(); constructor(ctrl: ComponentCtrl) { this.#update = ctrl.getUpdater(); ctrl.afterMount(() => { this.#controllers.forEach((it) => it.hostConnected && it.hostConnected()); }); ctrl.beforeUnmount(() => { this.#controllers.forEach( (it) => it.hostDisconnected && it.hostDisconnected() ); }); ctrl.beforeUpdate(() => { this.#controllers.forEach((it) => it.hostUpdate && it.hostUpdate()); }); ctrl.afterUpdate(() => { this.#controllers.forEach((it) => it.hostUpdated && it.hostUpdated()); }); } addController(controller: ReactiveController) { this.#controllers.add(controller); } removeController(controller: ReactiveController) { this.#controllers.delete(controller); } requestUpdate(): void { this.#update(); } get updateComplete() { return Promise.resolve(true); // TODO!!! } } // --- locals -------------------------------------------------------- function isEqualArray(arr1: any[], arr2: any[]) { let ret = Array.isArray(arr1) && Array.isArray(arr2) && arr1.length === arr2.length; if (ret) { for (let i = 0; i < arr1.length; ++i) { if (arr1[i] !== arr2[i]) { ret = false; break; } } } return ret; }