diff --git a/src/modules/esl-mixin-element/test/mixin.attributes.test.ts b/src/modules/esl-mixin-element/test/mixin.attributes.test.ts index cb22bae67..f6d62fda4 100644 --- a/src/modules/esl-mixin-element/test/mixin.attributes.test.ts +++ b/src/modules/esl-mixin-element/test/mixin.attributes.test.ts @@ -4,7 +4,6 @@ import MockedFn = jest.MockedFn; describe('ESLMixinElement: attribute observation', () => { class TestMixin extends ESLMixinElement { static override is = 'test-mixin-oattr'; - static override observedAttributes = [TestMixin.is]; override attributeChangedCallback: MockedFn<(name: string, oldV: string, newV: string) => void>; constructor(el: HTMLElement) { @@ -40,7 +39,7 @@ describe('ESLMixinElement: attribute observation', () => { expect(TestMixin.get($host)).toBeInstanceOf(TestMixin); }); - test('mixin initialized', async () => { + test('mixin new value handled correctly', async () => { const mixin = TestMixin.get($host)!; const oldValue = $host.getAttribute(TestMixin.is); mixin.attributeChangedCallback.mockReset(); diff --git a/src/modules/esl-mixin-element/ui/esl-mixin-attr.ts b/src/modules/esl-mixin-element/ui/esl-mixin-attr.ts index 9ab777bc9..281f9434b 100644 --- a/src/modules/esl-mixin-element/ui/esl-mixin-attr.ts +++ b/src/modules/esl-mixin-element/ui/esl-mixin-attr.ts @@ -1,7 +1,14 @@ import {ESLMixinRegistry} from './esl-mixin-registry'; import type {ESLMixinElement, ESLMixinElementInternal} from './esl-mixin-element'; +// Singleton cache for ESLMixinAttributesObserver instances const instances = new Map(); + +/** + * Internal {@link ESLMixinElement}s observedAttributes mutation listener. + * Creates a single instance per mixin type + * Ignores mixin primary attribute changes (they are observed by {@link ESLMixinRegistry} ootb) + */ export class ESLMixinAttributesObserver { protected observer = new MutationObserver( (records: MutationRecord[]) => records.forEach(this.handleRecord, this) @@ -12,6 +19,7 @@ export class ESLMixinAttributesObserver { instances.set(type, this); } + /** Process single mutation record */ protected handleRecord(record: MutationRecord): void { const name = record.attributeName; const target = record.target as HTMLElement; @@ -20,34 +28,40 @@ export class ESLMixinAttributesObserver { mixin && mixin.attributeChangedCallback(name, record.oldValue, target.getAttribute(name)); } + /** Subscribe to the {@link ESLMixinElement} host instance mutations */ public observe(mixin: ESLMixinElement): void { - const {observedAttributes} = mixin.constructor as typeof ESLMixinElement; + const {is, observedAttributes} = mixin.constructor as typeof ESLMixinElement; + const attributeFilter = observedAttributes.filter((name: string) => name !== is); + if (!attributeFilter.length) return; this.observer.observe(mixin.$host, { attributes: true, - attributeFilter: observedAttributes, + attributeFilter, attributeOldValue: true }); } + /** Unsubscribes from the {@link ESLMixinElement} host instance mutations */ public unobserve(mixin: ESLMixinElement): void { this.observer.observe(mixin.$host, { attributes: true, - attributeFilter: [], - attributeOldValue: true + attributeFilter: [] }); } private static instanceFor(mixin: ESLMixinElement): ESLMixinAttributesObserver | null { - const type = mixin.constructor as typeof ESLMixinElement; - if (!type.is || !type.observedAttributes || !type.observedAttributes.length) return null; - return new ESLMixinAttributesObserver(type.is); + const {is, observedAttributes} = mixin.constructor as typeof ESLMixinElement; + const attributes = (observedAttributes || []).filter((name: string) => name !== is); + if (!is || !attributes.length) return null; + return new ESLMixinAttributesObserver(is); } + /** Subscribe to the {@link ESLMixinElement} host instance mutations */ public static observe(mixin: ESLMixinElement): void { const observer = this.instanceFor(mixin); observer && observer.observe(mixin); } + /** Unsubscribes from the {@link ESLMixinElement} host instance mutations */ public static unobserve(mixin: ESLMixinElement): void { const observer = this.instanceFor(mixin); observer && observer.unobserve(mixin); diff --git a/src/modules/esl-mixin-element/ui/esl-mixin-registry.ts b/src/modules/esl-mixin-element/ui/esl-mixin-registry.ts index a4b6a9059..8c570a0cc 100644 --- a/src/modules/esl-mixin-element/ui/esl-mixin-registry.ts +++ b/src/modules/esl-mixin-element/ui/esl-mixin-registry.ts @@ -50,7 +50,8 @@ export class ESLMixinRegistry { subtree: true, childList: true, attributes: true, - attributeFilter: this.observedAttributes + attributeFilter: this.observedAttributes, + attributeOldValue: true }); } @@ -75,20 +76,23 @@ export class ESLMixinRegistry { } /** Invalidates passed mixin on the element */ - public invalidate(el: HTMLElement, mixin: string): void { - if (el.hasAttribute(mixin)) { - const mixinType = this.store.get(mixin); - mixinType && ESLMixinRegistry.init(el, mixinType); + public invalidate(el: HTMLElement, name: string, oldValue: string | null): void { + const newValue = el.getAttribute(name); + if (newValue === null) return ESLMixinRegistry.destroy(el, name); + const instance = ESLMixinRegistry.get(el, name) as ESLMixinElementInternal; + if (instance) { + instance.attributeChangedCallback(name, oldValue, newValue); } else { - ESLMixinRegistry.destroy(el, mixin); + const type = this.store.get(name); + type && ESLMixinRegistry.init(el, type); } } /** Handles DOM mutation list */ protected _onMutation(mutations: MutationRecord[]): void { mutations.forEach((record: MutationRecord) => { - if (record.type === 'attributes' && record.attributeName && this.store.has(record.attributeName)) { - this.invalidate(record.target as HTMLElement, record.attributeName); + if (record.type === 'attributes' && record.attributeName) { + this.invalidate(record.target as HTMLElement, record.attributeName, record.oldValue); } if (record.type === 'childList') { record.addedNodes.forEach((node) => {