1 import {kebabToCamel, camelToKebab} from './text';
4 * A mapping of active components keyed by name, with values being arrays of component
5 * instances since there can be multiple components of the same type.
6 * @type {Object<String, Component[]>}
11 * A mapping of component class models, keyed by name.
12 * @type {Object<String, Constructor<Component>>}
14 const componentModelMap = {};
17 * A mapping of active component maps, keyed by the element components are assigned to.
18 * @type {WeakMap<Element, Object<String, Component>>}
20 const elementComponentMap = new WeakMap();
23 * Parse out the element references within the given element
24 * for the given component name.
25 * @param {String} name
26 * @param {Element} element
28 function parseRefs(name, element) {
32 const prefix = `${name}@`;
33 const selector = `[refs*="${prefix}"]`;
34 const refElems = [...element.querySelectorAll(selector)];
35 if (element.matches(selector)) {
36 refElems.push(element);
39 for (const el of refElems) {
40 const refNames = el.getAttribute('refs')
42 .filter(str => str.startsWith(prefix))
43 .map(str => str.replace(prefix, ''))
45 for (const ref of refNames) {
47 if (typeof manyRefs[ref] === 'undefined') {
50 manyRefs[ref].push(el);
53 return {refs, manyRefs};
57 * Parse out the element component options.
58 * @param {String} componentName
59 * @param {Element} element
60 * @return {Object<String, String>}
62 function parseOpts(componentName, element) {
64 const prefix = `option:${componentName}:`;
65 for (const {name, value} of element.attributes) {
66 if (name.startsWith(prefix)) {
67 const optName = name.replace(prefix, '');
68 opts[kebabToCamel(optName)] = value || '';
75 * Initialize a component instance on the given dom element.
76 * @param {String} name
77 * @param {Element} element
79 function initComponent(name, element) {
80 /** @type {Function<Component>|undefined} * */
81 const ComponentModel = componentModelMap[name];
82 if (ComponentModel === undefined) return;
84 // Create our component instance
85 /** @type {Component} * */
88 instance = new ComponentModel();
89 instance.$name = name;
90 instance.$el = element;
91 const allRefs = parseRefs(name, element);
92 instance.$refs = allRefs.refs;
93 instance.$manyRefs = allRefs.manyRefs;
94 instance.$opts = parseOpts(name, element);
97 console.error('Failed to create component', e, name, element);
100 // Add to global listing
101 if (typeof components[name] === 'undefined') {
102 components[name] = [];
104 components[name].push(instance);
106 // Add to element mapping
107 const elComponents = elementComponentMap.get(element) || {};
108 elComponents[name] = instance;
109 elementComponentMap.set(element, elComponents);
113 * Initialize all components found within the given element.
114 * @param {Element|Document} parentElement
116 export function init(parentElement = document) {
117 const componentElems = parentElement.querySelectorAll('[component],[components]');
119 for (const el of componentElems) {
120 const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
121 for (const name of componentNames) {
122 initComponent(name, el);
128 * Register the given component mapping into the component system.
129 * @param {Object<String, ObjectConstructor<Component>>} mapping
131 export function register(mapping) {
132 const keys = Object.keys(mapping);
133 for (const key of keys) {
134 componentModelMap[camelToKebab(key)] = mapping[key];
139 * Get the first component of the given name.
140 * @param {String} name
141 * @returns {Component|null}
143 export function first(name) {
144 return (components[name] || [null])[0];
148 * Get all the components of the given name.
149 * @param {String} name
150 * @returns {Component[]}
152 export function get(name) {
153 return components[name] || [];
157 * Get the first component, of the given name, that's assigned to the given element.
158 * @param {Element} element
159 * @param {String} name
160 * @returns {Component|null}
162 export function firstOnElement(element, name) {
163 const elComponents = elementComponentMap.get(element) || {};
164 return elComponents[name] || null;