1 import {kebabToCamel, camelToKebab} from './text';
2 import {Component} from "../components/component";
5 * Parse out the element references within the given element
6 * for the given component name.
8 function parseRefs(name: string, element: HTMLElement):
9 {refs: Record<string, HTMLElement>, manyRefs: Record<string, HTMLElement[]>} {
10 const refs: Record<string, HTMLElement> = {};
11 const manyRefs: Record<string, HTMLElement[]> = {};
13 const prefix = `${name}@`;
14 const selector = `[refs*="${prefix}"]`;
15 const refElems = [...element.querySelectorAll(selector)];
16 if (element.matches(selector)) {
17 refElems.push(element);
20 for (const el of refElems as HTMLElement[]) {
21 const refNames = (el.getAttribute('refs') || '')
23 .filter(str => str.startsWith(prefix))
24 .map(str => str.replace(prefix, ''))
26 for (const ref of refNames) {
28 if (typeof manyRefs[ref] === 'undefined') {
31 manyRefs[ref].push(el);
34 return {refs, manyRefs};
38 * Parse out the element component options.
40 function parseOpts(componentName: string, element: HTMLElement): Record<string, string> {
41 const opts: Record<string, string> = {};
42 const prefix = `option:${componentName}:`;
43 for (const {name, value} of element.attributes) {
44 if (name.startsWith(prefix)) {
45 const optName = name.replace(prefix, '');
46 opts[kebabToCamel(optName)] = value || '';
52 export class ComponentStore {
54 * A mapping of active components keyed by name, with values being arrays of component
55 * instances since there can be multiple components of the same type.
57 protected components: Record<string, Component[]> = {};
60 * A mapping of component class models, keyed by name.
62 protected componentModelMap: Record<string, typeof Component> = {};
65 * A mapping of active component maps, keyed by the element components are assigned to.
67 protected elementComponentMap: WeakMap<HTMLElement, Record<string, Component>> = new WeakMap();
70 * Initialize a component instance on the given dom element.
72 protected initComponent(name: string, element: HTMLElement): void {
73 const ComponentModel = this.componentModelMap[name];
74 if (ComponentModel === undefined) return;
76 // Create our component instance
77 let instance: Component|null = null;
79 instance = new ComponentModel();
80 instance.$name = name;
81 instance.$el = element;
82 const allRefs = parseRefs(name, element);
83 instance.$refs = allRefs.refs;
84 instance.$manyRefs = allRefs.manyRefs;
85 instance.$opts = parseOpts(name, element);
88 console.error('Failed to create component', e, name, element);
95 // Add to global listing
96 if (typeof this.components[name] === 'undefined') {
97 this.components[name] = [];
99 this.components[name].push(instance);
101 // Add to element mapping
102 const elComponents = this.elementComponentMap.get(element) || {};
103 elComponents[name] = instance;
104 this.elementComponentMap.set(element, elComponents);
108 * Initialize all components found within the given element.
110 public init(parentElement: Document|HTMLElement = document) {
111 const componentElems = parentElement.querySelectorAll('[component],[components]');
113 for (const el of componentElems) {
114 const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
115 for (const name of componentNames) {
116 this.initComponent(name, el as HTMLElement);
122 * Register the given component mapping into the component system.
123 * @param {Object<String, ObjectConstructor<Component>>} mapping
125 public register(mapping: Record<string, typeof Component>) {
126 const keys = Object.keys(mapping);
127 for (const key of keys) {
128 this.componentModelMap[camelToKebab(key)] = mapping[key];
133 * Get the first component of the given name.
135 public first(name: string): Component|null {
136 return (this.components[name] || [null])[0];
140 * Get all the components of the given name.
142 public get<T extends Component>(name: string): T[] {
143 return (this.components[name] || []) as T[];
147 * Get the first component, of the given name, that's assigned to the given element.
149 public firstOnElement(element: HTMLElement, name: string): Component|null {
150 const elComponents = this.elementComponentMap.get(element) || {};
151 return elComponents[name] || null;
154 public allWithinElement<T extends Component>(element: HTMLElement, name: string): T[] {
155 const components = this.get<T>(name);
156 return components.filter(c => element.contains(c.$el));