/** * 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. * @type {Object} */ const components = {}; /** * A mapping of component class models, keyed by name. * @type {Object>} */ const componentModelMap = {}; /** * A mapping of active component maps, keyed by the element components are assigned to. * @type {WeakMap>} */ const elementComponentMap = new WeakMap(); /** * Initialize a component instance on the given dom element. * @param {String} name * @param {Element} element */ function initComponent(name, element) { /** @type {Function|undefined} **/ const componentModel = componentModelMap[name]; if (componentModel === undefined) return; // Create our component instance /** @type {Component} **/ let instance; 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); } // Add to global listing if (typeof components[name] === "undefined") { components[name] = []; } components[name].push(instance); // Add to element mapping const elComponents = elementComponentMap.get(element) || {}; elComponents[name] = instance; elementComponentMap.set(element, elComponents); } /** * Parse out the element references within the given element * for the given component name. * @param {String} name * @param {Element} element */ function parseRefs(name, element) { const refs = {}; const manyRefs = {}; const prefix = `${name}@` const selector = `[refs*="${prefix}"]`; const refElems = [...element.querySelectorAll(selector)]; if (element.matches(selector)) { refElems.push(element); } for (const el of refElems) { 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. * @param {String} name * @param {Element} element * @return {Object} */ function parseOpts(name, element) { const opts = {}; const prefix = `option:${name}:`; for (const {name, value} of element.attributes) { if (name.startsWith(prefix)) { const optName = name.replace(prefix, ''); opts[kebabToCamel(optName)] = value || ''; } } return opts; } /** * Convert a kebab-case string to camelCase * @param {String} kebab * @returns {string} */ function kebabToCamel(kebab) { const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1); const words = kebab.split('-'); return words[0] + words.slice(1).map(ucFirst).join(''); } /** * Initialize all components found within the given element. * @param {Element|Document} parentElement */ export function init(parentElement = 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) { initComponent(name, el); } } } /** * Register the given component mapping into the component system. * @param {Object>} mapping */ export function register(mapping) { const keys = Object.keys(mapping); for (const key of keys) { componentModelMap[camelToKebab(key)] = mapping[key]; } } /** * Get the first component of the given name. * @param {String} name * @returns {Component|null} */ export function first(name) { return (components[name] || [null])[0]; } /** * Get all the components of the given name. * @param {String} name * @returns {Component[]} */ export function get(name) { return components[name] || []; } /** * Get the first component, of the given name, that's assigned to the given element. * @param {Element} element * @param {String} name * @returns {Component|null} */ export function firstOnElement(element, name) { const elComponents = elementComponentMap.get(element) || {}; return elComponents[name] || null; } function camelToKebab(camelStr) { return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase()); }