Skip to content

Commit

Permalink
refactor(esl-mixin-element): fix observation provider, cover by funct…
Browse files Browse the repository at this point in the history
…ionality by unit tests
  • Loading branch information
ala-n committed May 10, 2023
1 parent e7ed2ab commit fdb2f6a
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 15 deletions.
90 changes: 90 additions & 0 deletions src/modules/esl-mixin-element/test/mixin.attributes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {ESLMixinElement} from '../ui/esl-mixin-element';
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) {
super(el);
this.attributeChangedCallback = jest.fn();
}
}
TestMixin.register();
class TestMixin2 extends TestMixin {
static override is = 'test-mixin-oattr-2';
static override observedAttributes = [TestMixin2.is, 'a', 'b'];
}
TestMixin2.register();
class TestMixin3 extends TestMixin {
static override is = 'test-mixin-oattr-3';
static override observedAttributes = [TestMixin3.is, 'a', 'c'];
}
TestMixin3.register();

describe('mixin `is` observed correctly', () => {
const $host = document.createElement('div');

beforeAll(() => {
$host.setAttribute(TestMixin.is, '');
document.body.appendChild($host);
return Promise.resolve();
});
afterAll(() => {
document.body.removeChild($host);
});

test('mixin initialized', () => {
expect(TestMixin.get($host)).toBeInstanceOf(TestMixin);
});

test('mixin initialized', async () => {
const mixin = TestMixin.get($host)!;
const oldValue = $host.getAttribute(TestMixin.is);
mixin.attributeChangedCallback.mockReset();

const newValue = 'test1';
$host.setAttribute(TestMixin.is, newValue);
await Promise.resolve();

expect(mixin.attributeChangedCallback).toBeCalledTimes(1);
expect(mixin.attributeChangedCallback).toBeCalledWith(TestMixin.is, oldValue, newValue);
});
});

describe('observed attributes handled correctly', () => {
const $host = document.createElement('div');

beforeAll(() => {
$host.setAttribute(TestMixin2.is, '');
$host.setAttribute(TestMixin3.is, '');
document.body.appendChild($host);
return Promise.resolve();
});
afterAll(() => {
document.body.removeChild($host);
});

test.each([
['a', TestMixin2],
['b', TestMixin2],
['a', TestMixin3],
['c', TestMixin3],
[TestMixin2.is, TestMixin2],
[TestMixin3.is, TestMixin3],
])('attr %s handled by mixin %o', async (attrName: string, Mixin: typeof TestMixin) => {
const mixin = Mixin.get($host)!;
const oldValue = $host.getAttribute(attrName);
mixin.attributeChangedCallback.mockReset();

const newValue = `test-${attrName}-${performance.now()}`;
$host.setAttribute(attrName, newValue);
await Promise.resolve();

expect(mixin.attributeChangedCallback).toBeCalledTimes(1);
expect(mixin.attributeChangedCallback).toBeCalledWith(attrName, oldValue, newValue);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {attr, boolAttr, jsonAttr} from '../../esl-utils/decorators';

describe('ESLMixinElement: attribute mixins correctly reflect to the $host', () => {
class TestMixin extends ESLMixinElement {
static override is = 'test-mixin-2';
static override is = 'test-mixin-attr-decorators';

@attr() public val: string | null;

Expand Down
34 changes: 22 additions & 12 deletions src/modules/esl-mixin-element/ui/esl-mixin-attr.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
import {ESLMixinRegistry} from './esl-mixin-registry';
import type {ESLMixinElement, ESLMixinElementInternal} from './esl-mixin-element';

let instance: ESLMixinAttributesObserver;
const instances = new Map<string, ESLMixinAttributesObserver>();
export class ESLMixinAttributesObserver {
protected observer = new MutationObserver(
(records: MutationRecord[]) => records.forEach(this.handleRecord, this)
);

constructor() {
if (instance) return instance;
// eslint-disable-next-line @typescript-eslint/no-this-alias
instance = this;
private constructor(protected readonly type: string) {
if (instances.has(type)) return instances.get(type)!;
instances.set(type, this);
}

protected handleRecord(record: MutationRecord): void {
const name = record.attributeName;
const target = record.target as HTMLElement;
if (!name || !target) return;
const mixins = ESLMixinRegistry.getAll(target) as ESLMixinElementInternal[];
for (const mixin of mixins) {
const observed = (mixin.constructor as typeof ESLMixinElement).observedAttributes;
if (!observed.includes(record.attributeName)) return;
mixin.attributeChangedCallback(name, record.oldValue, target.getAttribute(name));
}
const mixin = ESLMixinRegistry.get(target, this.type) as ESLMixinElementInternal;
mixin && mixin.attributeChangedCallback(name, record.oldValue, target.getAttribute(name));
}

public observe(mixin: ESLMixinElement): void {
const {observedAttributes} = mixin.constructor as typeof ESLMixinElement;
if (!observedAttributes.length) return;
this.observer.observe(mixin.$host, {
attributes: true,
attributeFilter: observedAttributes,
Expand All @@ -42,4 +36,20 @@ export class ESLMixinAttributesObserver {
attributeOldValue: true
});
}

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);
}

public static observe(mixin: ESLMixinElement): void {
const observer = this.instanceFor(mixin);
observer && observer.observe(mixin);
}

public static unobserve(mixin: ESLMixinElement): void {
const observer = this.instanceFor(mixin);
observer && observer.unobserve(mixin);
}
}
4 changes: 2 additions & 2 deletions src/modules/esl-mixin-element/ui/esl-mixin-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ export class ESLMixinElement implements ESLBaseComponent, ESLDomElementRelated {

/** Callback of mixin instance initialization */
protected connectedCallback(): void {
(new ESLMixinAttributesObserver()).observe(this);
ESLMixinAttributesObserver.observe(this);
ESLEventUtils.subscribe(this);
}

/** Callback to execute on mixin instance destroy */
protected disconnectedCallback(): void {
(new ESLMixinAttributesObserver()).unobserve(this);
ESLMixinAttributesObserver.unobserve(this);
ESLEventUtils.unsubscribe(this);
}

Expand Down

0 comments on commit fdb2f6a

Please # to comment.