import {kebabToCamel, camelToKebab} from './text'; import {Component} from "../components/component"; /** * Parse out the element references within the given element * for the given component name. */ function parseRefs(name: string, element: HTMLElement): {refs: Record, manyRefs: Record} { const refs: Record = {}; const manyRefs: Record = {}; const prefix = `${name}@`; const selector = `[refs*="${prefix}"]`; const refElems = [...element.querySelectorAll(selector)]; if (element.matches(selector)) { refElems.push(element); } for (const el of refElems as HTMLElement[]) { const refNames = (el.getAttribute('refs') || '') .split(' ') .filter(str => str.startsWith(prefix)) .map(str => str.replace(prefix, '')) .map(kebabToCamel); for (const ref of refNames) { refs[ref] = el; if (typeof manyRefs[ref] === 'undefined') { manyRefs[ref] = []; } manyRefs[ref].push(el); } } return {refs, manyRefs}; } /** * Parse out the element component options. */ function parseOpts(componentName: string, element: HTMLElement): Record { const opts: Record = {}; const prefix = `option:${componentName}:`; for (const {name, value} of element.attributes) { if (name.startsWith(prefix)) { const optName = name.replace(prefix, ''); opts[kebabToCamel(optName)] = value || ''; } } return opts; } export class ComponentStore { /** * A mapping of active components keyed by name, with values being arrays of component * instances since there can be multiple components of the same type. */ protected components: Record = {}; /** * A mapping of component class models, keyed by name. */ protected componentModelMap: Record = {}; /** * A mapping of active component maps, keyed by the element components are assigned to. */ protected elementComponentMap: WeakMap> = new WeakMap(); /** * Initialize a component instance on the given dom element. */ protected initComponent(name: string, element: HTMLElement): void { const ComponentModel = this.componentModelMap[name]; if (ComponentModel === undefined) return; // Create our component instance let instance: Component|null = null; try { instance = new ComponentModel(); instance.$name = name; instance.$el = element; const allRefs = parseRefs(name, element); instance.$refs = allRefs.refs; instance.$manyRefs = allRefs.manyRefs; instance.$opts = parseOpts(name, element); instance.setup(); } catch (e) { console.error('Failed to create component', e, name, element); } if (!instance) { return; } // Add to global listing if (typeof this.components[name] === 'undefined') { this.components[name] = []; } this.components[name].push(instance); // Add to element mapping const elComponents = this.elementComponentMap.get(element) || {}; elComponents[name] = instance; this.elementComponentMap.set(element, elComponents); } /** * Initialize all components found within the given element. */ public init(parentElement: Document|HTMLElement = document) { const componentElems = parentElement.querySelectorAll('[component],[components]'); for (const el of componentElems) { const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean); for (const name of componentNames) { this.initComponent(name, el as HTMLElement); } } } /** * Register the given component mapping into the component system. * @param {Object>} mapping */ public register(mapping: Record) { const keys = Object.keys(mapping); for (const key of keys) { this.componentModelMap[camelToKebab(key)] = mapping[key]; } } /** * Get the first component of the given name. */ public first(name: string): Component|null { return (this.components[name] || [null])[0]; } /** * Get all the components of the given name. */ public get(name: string): Component[] { return this.components[name] || []; } /** * Get the first component, of the given name, that's assigned to the given element. */ public firstOnElement(element: HTMLElement, name: string): Component|null { const elComponents = this.elementComponentMap.get(element) || {}; return elComponents[name] || null; } }