ba-web-components/src/components/ComposableElement.mjs
Alan Bridgeman d2e99cc280
All checks were successful
Build, Test, and Publish (to Private NPM Registry) UI Components Library / publish (push) Successful in 58s
Added tabs components and refactored stuff to work properly with composed / nested elements etc...
2026-05-18 13:56:31 -05:00

124 lines
No EOL
6.3 KiB
JavaScript

/**
* Abstract Base Class for Bridgeman Accessible Web Components
* Provides helper methods for easily initializing components, composing complex components, and managing nested custom elements.
*/
export class ComposableElement extends HTMLElement {
constructor() {
super();
}
/**
* Helper method to load and parse a JSON configuration ("Data Island") from a script tag within the Light DOM of the component.
* The script tag should have a `data-{name}-config` attribute where `{name}` is a unique identifier for the component (e.g., 'tablist' for 'ba-tablist').
*
* @param {string} name - The suffix of the component (e.g., 'tablist' for 'ba-tablist')
* @returns {Object|null} The parsed JSON configuration object, or null if not found
*/
loadDataIslandConfig(name) {
const configScript = this.querySelector(`script[data-${name}-config]`);
if (!configScript) {
console.error(`${name} configuration missing.`);
return null;
}
const config = JSON.parse(configScript.textContent);
return config;
}
/**
* Helper method to include the necessary stylesheets and scripts for a component based on its configuration.
* This method should be called within the `createTemplateInJS` of a component after loading it's "data island" configurations.
*
* @param {string} name - The suffix of the component (e.g., 'tablist' for 'ba-tablist')
* @param {Object} config - The JSON configuration object that includes the necessary information about styles and scripts to include
* @param {ShadowRoot} shadow - The shadow DOM root to append the link and script tags to
*/
includeStylesheetsAndScripts(name, config, shadow) {
if (config.componentsStyleHref) {
// Create and append the link tag for the components library CSS (including component specific styles)
const componentLibraryStyle = document.createElement('link');
componentLibraryStyle.rel = 'stylesheet';
componentLibraryStyle.type = 'text/css';
componentLibraryStyle.href = config.componentsStyleHref;
shadow.appendChild(componentLibraryStyle);
}
// Create additional link tags for any extra CSS files to include
const additionalStyles = config[`${name}ExtraStyles`] || [];
additionalStyles.forEach(style => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = `/css/${style}.css`;
shadow.appendChild(link);
});
// Create and append script tags for any extra JavaScript files to include
const additionalScripts = config[`${name}ExtraScripts`] || [];
additionalScripts.forEach(script => {
const scriptElem = document.createElement('script');
scriptElem.type = typeof script === 'object' && typeof script.module === 'boolean' && script.module ? 'module' : typeof script === 'string' && script.endsWith('.mjs') ? 'module' : 'application/javascript';
scriptElem.src = typeof script === 'object' ? script.script.startsWith('http') ? script.script : `/js/${script.script.endsWith('.mjs') ? script.script : script.script + '.js'}` : script.startsWith('http') ? script : `/js/${script.endsWith('.mjs') ? script : script + '.js'}`;
shadow.appendChild(scriptElem);
});
}
/**
* Helper method to initialize a component by loading its configuration and including necessary stylesheets and scripts.
* This method should be called at the beginning of the `createTemplateInJS` of a component to set up everything needed for the component to function properly.
*
* @param {string} name - The suffix of the component (e.g., 'tablist' for 'ba-tablist')
* @param {ShadowRoot} shadow - The shadow DOM root to append the link and script tags to
* @param {{ includeStylesAndScripts: boolean, stylesAndScriptsNameOverride?: string, subComponents?: Array<{ name: string, configKey: string, slotNames: Array<string> }> }} options - Options for initializing the component
* @returns {Object|null} The parsed JSON configuration object, or null if not found
*/
initializeComponent(name, shadow, { includeStylesAndScripts = true, stylesAndScriptsNameOverride, subComponents = [] } = {}) {
const config = this.loadDataIslandConfig(name);
if(config == null) {
return null;
}
if (includeStylesAndScripts) {
this.includeStylesheetsAndScripts(typeof stylesAndScriptsNameOverride !== 'undefined' ? stylesAndScriptsNameOverride : name, config, shadow);
}
subComponents.forEach(subComp => {
const subCompElem = this.createSubComponent(subComp.name, config[subComp.configKey], subComp.slotNames);
shadow.appendChild(subCompElem);
});
return config;
}
/**
* Generates a sub-component, attaches its configuration, and forwards requested slots.
*
* @param {string} name - The suffix of the component (e.g., 'tablist' for 'ba-tablist')
* @param {Object} config - The JSON configuration object to pass to the sub-component
* @param {Array<string>} slotNames - An array of exact slot names to forward down
* @returns {HTMLElement} The fully constructed sub-component ready to be appended
*/
createSubComponent(name, config, slotNames = []) {
const customElem = document.createElement(`ba-${name}`);
// Attach the Configuration Data Island
const customElemConfig = document.createElement('script');
customElemConfig.type = 'application/json';
customElemConfig.setAttribute(`data-${name}-config`, '');
customElemConfig.textContent = JSON.stringify(config);
customElem.appendChild(customElemConfig);
// Forward the explicit slots
slotNames.forEach(slotName => {
const slotForwarder = document.createElement('slot');
slotForwarder.name = slotName;
slotForwarder.slot = slotName;
customElem.appendChild(slotForwarder);
});
// Return the element so the child class can append it exactly where it belongs in its specific DOM structure
return customElem;
}
}