2 * A mapping of active components keyed by name, with values being arrays of component
3 * instances since there can be multiple components of the same type.
4 * @type {Object<String, Component[]>}
9 * A mapping of component class models, keyed by name.
10 * @type {Object<String, Constructor<Component>>}
12 const componentModelMap = {};
15 * A mapping of active component maps, keyed by the element components are assigned to.
16 * @type {WeakMap<Element, Object<String, Component>>}
18 const elementComponentMap = new WeakMap();
21 * Initialize a component instance on the given dom element.
22 * @param {String} name
23 * @param {Element} element
25 function initComponent(name, element) {
26 /** @type {Function<Component>|undefined} **/
27 const componentModel = componentModelMap[name];
28 if (componentModel === undefined) return;
30 // Create our component instance
31 /** @type {Component} **/
34 instance = new componentModel();
35 instance.$name = name;
36 instance.$el = element;
37 const allRefs = parseRefs(name, element);
38 instance.$refs = allRefs.refs;
39 instance.$manyRefs = allRefs.manyRefs;
40 instance.$opts = parseOpts(name, element);
43 console.error('Failed to create component', e, name, element);
46 // Add to global listing
47 if (typeof components[name] === "undefined") {
48 components[name] = [];
50 components[name].push(instance);
52 // Add to element mapping
53 const elComponents = elementComponentMap.get(element) || {};
54 elComponents[name] = instance;
55 elementComponentMap.set(element, elComponents);
59 * Parse out the element references within the given element
60 * for the given component name.
61 * @param {String} name
62 * @param {Element} element
64 function parseRefs(name, element) {
68 const prefix = `${name}@`
69 const selector = `[refs*="${prefix}"]`;
70 const refElems = [...element.querySelectorAll(selector)];
71 if (element.matches(selector)) {
72 refElems.push(element);
75 for (const el of refElems) {
76 const refNames = el.getAttribute('refs')
78 .filter(str => str.startsWith(prefix))
79 .map(str => str.replace(prefix, ''))
81 for (const ref of refNames) {
83 if (typeof manyRefs[ref] === 'undefined') {
86 manyRefs[ref].push(el);
89 return {refs, manyRefs};
93 * Parse out the element component options.
94 * @param {String} name
95 * @param {Element} element
96 * @return {Object<String, String>}
98 function parseOpts(name, element) {
100 const prefix = `option:${name}:`;
101 for (const {name, value} of element.attributes) {
102 if (name.startsWith(prefix)) {
103 const optName = name.replace(prefix, '');
104 opts[kebabToCamel(optName)] = value || '';
111 * Convert a kebab-case string to camelCase
112 * @param {String} kebab
115 function kebabToCamel(kebab) {
116 const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
117 const words = kebab.split('-');
118 return words[0] + words.slice(1).map(ucFirst).join('');
122 * Initialize all components found within the given element.
123 * @param {Element|Document} parentElement
125 export function init(parentElement = document) {
126 const componentElems = parentElement.querySelectorAll(`[component],[components]`);
128 for (const el of componentElems) {
129 const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
130 for (const name of componentNames) {
131 initComponent(name, el);
137 * Register the given component mapping into the component system.
138 * @param {Object<String, ObjectConstructor<Component>>} mapping
140 export function register(mapping) {
141 const keys = Object.keys(mapping);
142 for (const key of keys) {
143 componentModelMap[camelToKebab(key)] = mapping[key];
148 * Get the first component of the given name.
149 * @param {String} name
150 * @returns {Component|null}
152 export function first(name) {
153 return (components[name] || [null])[0];
157 * Get all the components of the given name.
158 * @param {String} name
159 * @returns {Component[]}
161 export function get(name = '') {
162 return components[name] || [];
166 * Get the first component, of the given name, that's assigned to the given element.
167 * @param {Element} element
168 * @param {String} name
169 * @returns {Component|null}
171 export function firstOnElement(element, name) {
172 const elComponents = elementComponentMap.get(element) || {};
173 return elComponents[name] || null;
176 function camelToKebab(camelStr) {
177 return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase());