UNPKG

@mr_hugo/boredom

Version:

Another boring JavaScript framework.

152 lines (140 loc) 4.83 kB
import { createAndRunCode, createEventsHandler, createRefsAccessor, createSlotsAccessor, createStateAccessor, proxify, runComponentsInitializer, } from "./bore"; import { Bored, createComponent, dynamicImportScripts, searchForComponents, } from "./dom"; import type { AppState, InitFunction } from "./types"; export { queryComponent } from "./dom"; /** * Queries all `<template>` elements that * have a `data-component` attribute defined and creates web components * with the tag name in that attribute. * * @param state An optional initial app state object. When provided this will * be proxified to allow for automatic updates of the dom whenever it * changes. * * @param componentsLogic An optional object that allows you to specify the * web components script code without having to place it in a separate file. * Its keys are the tag names and its value is the return type of * the `webComponent()` function. This overrides any external file * associated with the component. * * @returns The app initial state. */ export async function inflictBoreDOM<S>( state?: S, componentsLogic?: { [key: string]: ReturnType<typeof webComponent> }, ): Promise<AppState<S>["app"]> { const registeredNames = searchForComponents(); const componentsCode = await dynamicImportScripts(registeredNames); if (componentsLogic) { for (const tagName of Object.keys(componentsLogic)) { componentsCode.set(tagName, componentsLogic[tagName]); } } // Initial state for boreDOM: const initialState: AppState<S> = { app: state, internal: { customTags: registeredNames, components: componentsCode, updates: { path: [], value: [], raf: undefined, subscribers: new Map(), }, }, }; // Proxifies the `initialState.app`: const proxifiedState = proxify(initialState); // Call the code from the corresponding .js file of each component: runComponentsInitializer(proxifiedState); // When no initial state is provided, return undefined. This still // initializes components, event wiring, and subscriptions. return proxifiedState.app; } /** * Creates a Web Component render updater * * @param initFunction Initialization function that returns the render function * @return A curried function to use as callback for component initialization */ export function webComponent<S>( initFunction: InitFunction<S | undefined>, ): (appState: AppState<S>, detail?: any) => (c: Bored) => void { // Was it already initialized? let isInitialized: null | Bored = null; let renderFunction: (state?: S) => void; return (appState: AppState<S>, detail: any) => (c: Bored) => { const { internal, app } = appState; let log: string[] | string = []; const state = createStateAccessor(app, log); const refs = createRefsAccessor(c); const slots = createSlotsAccessor(c); const on = createEventsHandler(c, app, detail); if (isInitialized !== c) { // `updateSubscribers` is called right after the user defined renderer is called, // to ensure that the user defined renderer is in the subscribers list of // any state attribute being read. The execution might also change/update the // state attributes being read, and if so, calling this function also guarantees // that they are updated. const updateSubscribers = async () => { const subscribers = internal.updates.subscribers; for (let path of log) { /** * Get the functions that are subscribed to be called for * this access path */ const functions = subscribers.get(path); if (functions) { if (!functions.includes(renderFunction)) { // The function is not yet registered functions.push(renderFunction); } } else { subscribers.set(path, [renderFunction]); } } }; const userDefinedRenderer = initFunction({ detail, state, refs, on, self: c, }); // The render function is updated to ensure the `updatedSubscribers ` renderFunction = (state) => { userDefinedRenderer({ state, refs, slots, self: c, detail, makeComponent: (tag, opts) => { return createAndRunCode(tag, appState as any, opts?.detail); }, }); updateSubscribers(); }; } // Do the initial call right away: renderFunction(state); // Keep track of which component detail was initialized, this // allows the same component tag to be initialized multiple times. // This is a common scenario in lists of components, such as menu items, etc isInitialized = c; }; }