UNPKG

@joist/element

Version:

Intelligently apply styles to WebComponents

131 lines (106 loc) 3.14 kB
type Updater = () => void; class Updates extends Set<Updater> {} type TemplateValueGetter = (key: string) => string; export interface TemplateOpts { value?: TemplateValueGetter; tokenPrefix?: string; } export interface RenderOpts { refresh?: boolean; } export function template({ tokenPrefix = "#:", value }: TemplateOpts = {}) { // Track all nodes that can be updated and their associated property let updates: Updates | null = null; return function render<T extends HTMLElement>( this: T, opts?: RenderOpts, ): void { if (!updates || opts?.refresh) { updates = findUpdates(this, { tokenPrefix, value: value ?? ((key: string) => getTemplateValue(this, key)), }); } else { for (const update of updates) { update(); } } }; } function findUpdates(el: HTMLElement, opts: Required<TemplateOpts>): Updates { const iterator = document.createTreeWalker( el.shadowRoot ?? el, NodeFilter.SHOW_ELEMENT, ); const updates = new Updates(); while (iterator.nextNode()) { const res = trackElement(iterator.currentNode, updates, opts); if (res !== null) { iterator.currentNode = res; } } return updates; } /** * configures and tracks a given Node so that it can be updated in place later. */ function trackElement( node: Node, updates: Updates, opts: Required<TemplateOpts>, ): Node | null { const element = node as Element; const getter = opts.value; const tokenPrefix = opts.tokenPrefix; for (const attr of element.attributes) { const nodeValue = attr.value.trim(); const realAttributeName = attr.name.replace(tokenPrefix, ""); let update: Updater | null = null; if (attr.name.startsWith(`${tokenPrefix}bind`)) { update = () => { const value = getter(attr.value); if (element.textContent !== value) { element.textContent = getter(attr.value); } }; } else if (attr.name.startsWith(tokenPrefix)) { const isBooleanAttr = nodeValue.startsWith("!"); const isPositive = nodeValue.startsWith("!!"); const propertyKey = nodeValue.replaceAll("!", ""); if (isBooleanAttr) { update = () => { const value = isPositive ? !!getter(propertyKey) : !getter(propertyKey); if (value) { element.setAttribute(realAttributeName, ""); } else { element.removeAttribute(realAttributeName); } }; } else { const realAttribute = document.createAttribute(realAttributeName); element.setAttributeNode(realAttribute); update = () => { const value = getter(nodeValue); if (realAttribute.value !== value) { realAttribute.value = value; } }; } } if (update) { updates.add(update); update(); } } return null; } export function getTemplateValue(obj: object, key: string): any { const parsed = key.split("."); let pointer: any = obj; for (const part of parsed) { pointer = pointer[part]; } return pointer; }