@ou-imdt/utils
Version:
Utility library for interactive media development
94 lines (74 loc) • 3.24 kB
JavaScript
import { defaultState } from '../Base.js';
export const delegatedAttributePrefix = Symbol('delegatedAttributePrefix');
export const delegatedAttributes = Symbol('delegatedAttributes');
export const childAttributes = Symbol('childAttributes'); // better name?
/**
* Passes attributes to child components* where the attribute is observed on the child and
* unobserved on the parent. Child components must be declared within static components field.
* Removes attributes prefixed with [delegatedAttributePrefix] and stores/exposes as name/value
* pairs within [delegatedAttributes] hash for use/reflection internally.
* @mixin
*/
export default (superClass) => class DelegateAttributesMixin extends superClass {
static get [defaultState]() {
return {
...super[defaultState],
[delegatedAttributes]: {},
[childAttributes]: new Set()
};
}
static [delegatedAttributePrefix] = 'delegate-';
get unobservedAttributes() {
return this.getAttributeNames().filter((name) => !this.constructor.observedAttributes.includes(name));
}
#mutationObserver = new MutationObserver((mutationList) => {
for (const mutation of mutationList) {
const { attributeName, oldValue } = mutation;
const newValue = this.getAttribute(attributeName);
this.#delegateAttribute(attributeName, newValue, oldValue);
}
});
connectedCallback() {
super.connectedCallback();
for (const name of this.getAttributeNames()) {
this.#delegateAttribute(name, this.getAttribute(name));
}
this.#mutationObserver.observe(this, { attributes: true, attributeOldValue: true });
}
disconnectedCallback() {
super.disconnectedCallback();
this[delegatedAttributes] = {};
this[childAttributes].clear();
this.#mutationObserver.disconnect();
}
#delegateAttribute(name, newValue, oldValue = null) {
if (newValue === oldValue) return;
const prefix = this.constructor[delegatedAttributePrefix];
if (name.startsWith(prefix)) {
const key = name.replace(prefix, '');
this[delegatedAttributes] = { ...this[delegatedAttributes], [key]: newValue }; // new object in case shallow reactivity
return;
}
if (this.constructor.components === null) return false;
if (!this.unobservedAttributes.includes(name)) return false;
const isObserved = Object.values(this.constructor.components).map(component => {
if (!component.observedAttributes.includes(name)) return false;
// oldValue ??= component[defaultState][name]; // won't work as attr name
requestAnimationFrame(() => {
const elements = [
...this.querySelectorAll(component.tag), // keep these sync??
...this.shadowRoot.querySelectorAll(component.tag)
];
elements.forEach(el => {
// console.log(name, newValue, oldValue, el.getAttribute(name));
if (oldValue !== null && el.getAttribute(name) !== oldValue) return;
// TODO include check for initial value instead to prevent overwriting predefined values?
el.setAttribute(name, newValue);
});
});
return true;
});
if (!isObserved.includes(true)) return;
this[childAttributes].add(name);
}
}