Repository: exadel-inc/esl
Pull request #1261 (2022-10-02 01:05:30)

fix(esl-utils): fix `@boolAttr` incompatible with mixin element


Patch:
diff --git a/src/modules/esl-utils/decorators/bool-attr.ts b/src/modules/esl-utils/decorators/bool-attr.ts
index 6acfeadc6..d02484526 100644
--- a/src/modules/esl-utils/decorators/bool-attr.ts
+++ b/src/modules/esl-utils/decorators/bool-attr.ts
@@ -1,4 +1,4 @@
-import {setAttr} from '../dom/attr';
+import {hasAttr, setAttr} from '../dom/attr';
 import {toKebabCase} from '../misc/format';
 import type {AttributeDecorator, AttributeTarget} from '../dom/attr';
 
@@ -14,7 +14,7 @@ type BoolAttrDescriptor = {
 
 function buildConditionalDescriptor(attrName: string, readOnly: boolean): PropertyDescriptor {
   function get(): boolean {
-    return this.hasAttribute(attrName);
+    return hasAttr(this, attrName);
   }
   function set(value: unknown): void {
     setAttr(this, attrName, !!value);
diff --git a/src/modules/esl-utils/dom/attr.ts b/src/modules/esl-utils/dom/attr.ts
index f9cb02799..ec3be96bf 100644
--- a/src/modules/esl-utils/dom/attr.ts
+++ b/src/modules/esl-utils/dom/attr.ts
@@ -1,6 +1,12 @@
 export type AttributeTarget = {$host: Element};
 export type AttributeDecorator = (target: Element | AttributeTarget, propName: string) => void;
 
+/** @returns if attribute presented */
+export function hasAttr($el: Element | AttributeTarget, name: string): boolean {
+  if ('$host' in $el) $el = $el.$host;
+  return $el.hasAttribute(name);
+}
+
 /** @returns attribute or passed fallback value. Identical to getAttribute by default */
 export function getAttr($el: Element | AttributeTarget, name: string): string | null;
 export function getAttr<T>($el: Element | AttributeTarget, name: string, fallback: T): string | T;


Test patch:
diff --git a/src/modules/esl-mixin-element/test/mixin.attr-helpers.test.ts b/src/modules/esl-mixin-element/test/mixin.attr-helpers.test.ts
new file mode 100644
index 000000000..6b4d4303c
--- /dev/null
+++ b/src/modules/esl-mixin-element/test/mixin.attr-helpers.test.ts
@@ -0,0 +1,52 @@
+import {ESLMixinElement} from '../ui/esl-mixin-element';
+import {attr, boolAttr, jsonAttr} from '../../esl-utils/decorators';
+
+describe('ESLMixinElement: attribute mixins correctly reflect to the $host', () => {
+  class TestMixin extends ESLMixinElement {
+    static is = 'test-mixin-2';
+
+    @attr() public val: string | null;
+
+    @boolAttr() public bool: boolean;
+
+    @jsonAttr() public json: object;
+  }
+  TestMixin.register();
+
+  const $el = document.createElement('div');
+  $el.toggleAttribute(TestMixin.is, true);
+
+  beforeAll(() => {
+    document.body.appendChild($el);
+  });
+
+  test('@attr', async () => {
+    const mixin = TestMixin.get($el) as TestMixin;
+    expect(mixin.val).toBe('');
+    mixin.val = 'a';
+    expect($el.getAttribute('val')).toBe('a');
+    mixin.val = null;
+    expect($el.hasAttribute('val')).toBe(false);
+  });
+
+  test('@boolAttr', async () => {
+    const mixin = TestMixin.get($el) as TestMixin;
+    expect(mixin.bool).toBe(false);
+    mixin.bool = true;
+    expect($el.hasAttribute('bool')).toBe(true);
+    mixin.bool = false;
+    expect($el.hasAttribute('bool')).toBe(false);
+  });
+
+  test('@jsonAttr', async () => {
+    const mixin = TestMixin.get($el) as TestMixin;
+    expect(mixin.json).toEqual({});
+
+    mixin.json = {a: 1};
+    expect($el.getAttribute('json')).toBe(JSON.stringify({a: 1}));
+  });
+
+  afterAll(() => {
+    while (document.body.lastElementChild) document.body.removeChild(document.body.lastElementChild);
+  });
+});
diff --git a/src/modules/esl-mixin-element/test/mixin.test.ts b/src/modules/esl-mixin-element/test/mixin.test.ts
index 6697c59d6..0fc1f349c 100644
--- a/src/modules/esl-mixin-element/test/mixin.test.ts
+++ b/src/modules/esl-mixin-element/test/mixin.test.ts
@@ -1,4 +1,4 @@
-import {attr, ESLMixinElement} from '../core';
+import {ESLMixinElement} from '../core';
 
 // TODO: exited with more cases
 describe('ESLMixinElement', () => {
@@ -39,32 +39,4 @@ describe('ESLMixinElement', () => {
       while (document.body.lastElementChild) document.body.removeChild(document.body.lastElementChild);
     });
   });
-
-  describe('instance proxy', () => {
-    class TestMixin extends ESLMixinElement {
-      static is = 'test-mixin-2';
-
-      @attr() public val: string;
-    }
-
-    const $el = document.createElement('div');
-    $el.toggleAttribute(TestMixin.is, true);
-
-    beforeAll(() => {
-      document.body.appendChild($el);
-      TestMixin.register();
-    });
-
-    test('attr', async () => {
-      const mixin = TestMixin.get($el) as TestMixin;
-      expect($el.getAttribute('val')).toBe(null);
-
-      mixin.val = 'a';
-      expect($el.getAttribute('val')).toBe('a');
-    });
-
-    afterAll(() => {
-      while (document.body.lastElementChild) document.body.removeChild(document.body.lastElementChild);
-    });
-  });
 });