Skip to content

Commit

Permalink
feat(esl-mixin-element): mixin primary attribute observed uncondition…
Browse files Browse the repository at this point in the history
…ally by mixin manager
  • Loading branch information
ala-n committed May 12, 2023
1 parent fdb2f6a commit c6741a4
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 17 deletions.
3 changes: 1 addition & 2 deletions src/modules/esl-mixin-element/test/mixin.attributes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
28 changes: 21 additions & 7 deletions src/modules/esl-mixin-element/ui/esl-mixin-attr.ts
Original file line number Diff line number Diff line change
@@ -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<string, ESLMixinAttributesObserver>();

/**
* 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)
Expand All @@ -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;
Expand All @@ -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);
Expand Down
20 changes: 12 additions & 8 deletions src/modules/esl-mixin-element/ui/esl-mixin-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export class ESLMixinRegistry {
subtree: true,
childList: true,
attributes: true,
attributeFilter: this.observedAttributes
attributeFilter: this.observedAttributes,
attributeOldValue: true
});
}

Expand All @@ -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) => {
Expand Down

0 comments on commit c6741a4

Please # to comment.