/* Public surface: html/svg/attrs tag functions, defineWompo, lazy, createPortal, Suspense, * createContext, and the registries. */ import { IS_SERVER } from './constants.js'; import { _$wompo } from './component.js'; import { useEffect, useRef, useSelf, useExposed } from './hooks.js'; import { __generateSpecifcStyles } from './styles.js'; import type { AttrsBag, Context, ContextProviderExposed, ContextProviderProps, LazyCallbackResult, LazyResult, RenderHtml, SuspenseInstance, SuspenseProps, WompoComponent, WompoComponentOptions, WompoProps, } from './types.js'; /** Tagged-template for HTML interpolations. */ export function html(templateParts: TemplateStringsArray, ...values: any[]): RenderHtml { // Values for closing dynamic tags (``) are dropped so that `values` aligns 1:1 with // the `dependencies` produced by the parser. Multiple consecutive dynamic tags (e.g. // `<${a}>.. <${b}>..`) would otherwise read the closing `a` as the opening `b`. // Server-side serializers consume parts directly and don't rely on this scheme. const cleanValues = []; const length = templateParts.length - 1; if (!IS_SERVER) { for (let i = 0; i < length; i++) { if (!templateParts[i].endsWith(' } = {}; export function defineWompo( Component: WompoComponent, options?: WompoComponentOptions, ) { if (!Component.css) Component.css = ''; const componentOptions = { ...wompoDefaultOptions, ...(options || {}), }; if (!componentOptions.name) { let newName = Component.name .replace(/.[A-Z]/g, (letter) => `${letter[0]}-${letter[1].toLowerCase()}`) .toLowerCase(); if (!newName.includes('-')) newName += '-wompo'; componentOptions.name = newName; } Component.componentName = componentOptions.name; Component._$wompoF = true; const [generatedCSS, styles] = __generateSpecifcStyles( Component as unknown as WompoComponent, componentOptions, ); Component.css = generatedCSS; Component.options = { generatedCSS: generatedCSS, styles: styles, shadow: componentOptions.shadow, island: componentOptions.island, }; if (!IS_SERVER) { const existing = customElements.get(componentOptions.name); if (existing) { // Tag was already defined — most often a duplicate `wompo` module load (npm-link or // workspace setups). Reuse the existing class so subsequent renders still work, and // skip a second customElements.define call which would throw NotSupportedError. Component.class = existing as any; } else { const ComponentClass = _$wompo( Component as unknown as WompoComponent, componentOptions, ); Component.class = ComponentClass; customElements.define(componentOptions.name, ComponentClass); } } registeredComponents[componentOptions.name] = Component; return Component as WompoComponent; } export const lazy = (load: () => LazyCallbackResult): LazyResult => { let loaded: WompoComponent = null; async function LazyComponent() { if (!loaded) { try { const importedModule = await load(); loaded = importedModule.default; return loaded; } catch (err) { console.error(err); } } return loaded; } LazyComponent._$wompoLazy = true; return LazyComponent; }; export const unsafelyRenderString = (html: string): RenderHtml => { return { _$wompoHtml: true, parts: [html] as any, values: [], }; }; export const createPortal = (html: RenderHtml, node: HTMLElement): RenderHtml => { return { ...html, _$portal: node, }; }; const createContextMemo = () => { let contextIdentifier = 0; return (initialValue: S, providerName?: string): Context => { const name = providerName ?? `wompo-context-provider-${contextIdentifier}`; contextIdentifier++; const ProviderFunction = defineWompo( ({ children, value }: ContextProviderProps) => { const initialSubscribers = new Set(); const subscribers = useRef(initialSubscribers); useEffect(() => { subscribers.current.forEach((el: any) => { if (el.isConnected) el.requestRender(); }); }, [value]); useExposed({ subscribers: subscribers }); return html`${children}`; }, { name: name, cssModule: false, }, ); const Context = { name: name, Provider: ProviderFunction, default: initialValue, }; return Context; }; }; export const createContext = createContextMemo(); export function Suspense({ children, fallback }: SuspenseProps) { const self = useSelf() as unknown as SuspenseInstance; if (!self.loadingComponents) { self.loadingComponents = useRef(new Set()).current; } self.addSuspense = (node: Node) => { if (!self.loadingComponents.size) self.requestRender(); self.loadingComponents.add(node); }; self.removeSuspense = (node: Node, newNode: Node = null) => { self.loadingComponents.delete(node); if (newNode) { for (let i = 0; i < (children as any).nodes.length; i++) { if ((children as any).nodes[i] === node) { (children as any).nodes[i] = newNode; break; } } } if (!self.loadingComponents.size) self.requestRender(); }; if (self.loadingComponents.size && fallback) return html`${fallback}`; return html`${children}`; } defineWompo(Suspense, { name: 'wompo-suspense' });