UNPKG

@ou-imdt/utils

Version:

Utility library for interactive media development

120 lines (101 loc) 3.81 kB
/** * @mixin TemplateEngineMixin - adds slot support for passing template elements * (jsdoc tag here) TemplateEngineModule */ import { defaultState } from '../Base.js'; import TemplateEngineModule from '../../modules/TemplateEngineModule.js'; export { subber } from '../../modules/TemplateEngineModule.js'; export const engine = Symbol('TemplateEngineModule'); export const assignedTemplateOptions = Symbol('assignedTemplateOptions'); export const templateOptions = Symbol('templateOptions'); export const templateData = Symbol('templateData'); export const templateSlot = Symbol('templateSlot'); /** * gets a subset of assigned elements filtered by selector, from slot and all decendant slots * @param {Element} slot - slot element * @param {string} selector - CSS selector * @returns {array} */ export function getAssignedElementsMatching(slot, selector) { // console.log(slot, selector) return slot.assignedElements({ flatten: true }).flatMap(el => { return [...(el.matches(selector) ? [el] : []), ...el.querySelectorAll(selector)]; }); } export default (superClass) => class TemplateEngineMixin extends superClass { static get [defaultState]() { return { ...super[defaultState], [assignedTemplateOptions]: null, [templateSlot]: 'slot:not([name])', [engine]: new TemplateEngineModule() }; } get [templateOptions]() { return this[assignedTemplateOptions]; } get [templateData]() { return {}; } get #templateSlot() { return this.shadowRoot.querySelector(this[templateSlot]); } /** * gets all templates within target slot (and descendant slots) */ get #assignedTemplates() { // slottedTemplates?? return getAssignedElementsMatching(this.#templateSlot, 'template'); } // get #assignedTemplateOptions() { // return this.#assignedTemplates.map(el => { // const name = el.getAttribute('data-name'); // const slot = el.getAttribute('data-slot'); // const content = el.innerHTML; // const target = el.parentNode; // return { name, content, slot, target }; // }); // } get #assignedTemplateOptions() { const map = new Map(); for (const el of this.#assignedTemplates) { const [name, part = ''] = el.getAttribute('data-name')?.split(':') ?? ''; const options = map.get(name) ?? { name, partials: {} }; options.slot ??= el.getAttribute('data-slot'); options.target ??= el.parentNode; if (part.length) { const content = {}; if (typeof options.content === 'string') { content.default = options.content; delete options.content; } options.content ??= {}; content[part] = el.innerHTML; Object.assign(options.content, content); } else { options.content = el.innerHTML; } map.set(name, options); } // console.log('assigned templates', map); return [...map.values()]; } connectedCallback() { super.connectedCallback(); // slotchange only fires when the slotted nodes change, not descendants of the nodes // https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/slotchange_event // nested slots will trigger change on default slot during first update, as well as themselves // subsequent change events will only trigger on the nested slots themselves this.shadowRoot.addEventListener('slotchange', (e) => { if (e.target !== this.#templateSlot) return; this[assignedTemplateOptions] = this.#assignedTemplateOptions; }); } updateTemplates(data, templates) { const state = { templateOptions: templates ?? this[templateOptions], templateData: data ?? this[templateData] }; this[engine].state = state; this[engine].update(); } }