@ou-imdt/utils
Version:
Utility library for interactive media development
120 lines (101 loc) • 3.81 kB
JavaScript
/**
* @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();
}
}