Repository: exadel-inc/esl
Pull request #142 (2021-04-13 12:53:07)

feat: `@prop` decorator


Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 8c2769897..a75e8540f 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -11,7 +11,5 @@ updates:
     directory: "/"
     schedule:
       interval: "daily"
-    labels:
-      - "dependencies update"
     reviewers:
       - "@exadel-inc/esl-core-team"
diff --git a/src/modules/esl-forms/esl-select/core/esl-select-dropdown.ts b/src/modules/esl-forms/esl-select/core/esl-select-dropdown.ts
index a683ce303..271397796 100644
--- a/src/modules/esl-forms/esl-select/core/esl-select-dropdown.ts
+++ b/src/modules/esl-forms/esl-select/core/esl-select-dropdown.ts
@@ -1,6 +1,6 @@
 import {ESLToggleable, ToggleableActionParams} from '../../../esl-toggleable/core/esl-toggleable';
 import {bind} from '../../../esl-utils/decorators/bind';
-import {override} from '../../../esl-utils/decorators/override';
+import {prop} from '../../../esl-utils/decorators/prop';
 import {TAB} from '../../../esl-utils/dom/keys';
 import {rafDecorator} from '../../../esl-utils/async/raf';
 import {ESLSelectList} from '../../esl-select-list/core';
@@ -29,10 +29,8 @@ export class ESLSelectDropdown extends ESLToggleable {
   protected _disposeTimeout: number;
   protected _deferredUpdatePosition = rafDecorator(() => this.updatePosition());
 
-  @override()
-  public closeOnEsc = true;
-  @override()
-  public closeOnOutsideAction = true;
+  @prop() public closeOnEsc = true;
+  @prop() public closeOnOutsideAction = true;
 
   constructor() {
     super();
diff --git a/src/modules/esl-utils/README.md b/src/modules/esl-utils/README.md
index a79bf37cd..d49d85a15 100644
--- a/src/modules/esl-utils/README.md
+++ b/src/modules/esl-utils/README.md
@@ -40,9 +40,7 @@ Even if you use the whole utils module, it is still tiny.
 
     - #### [@memoize](./decorators/memoize.ts) - TS decorator to make method or get accessor memoized.
 
-    - #### [@override](./decorators/override.ts) - TS decorator to define a writable field on the prototype level.
-    
-    - #### [@constant](./decorators/readonly.ts) - TS decorator to create o readonly field on the prototype level.
+    - #### [@prop](src/modules/esl-utils/decorators/prop.ts) - TS decorator to define a field on the prototype level.
 
 - ### DOM Helpers
   
diff --git a/src/modules/esl-utils/all.ts b/src/modules/esl-utils/all.ts
index a91c814a3..5b7c35595 100644
--- a/src/modules/esl-utils/all.ts
+++ b/src/modules/esl-utils/all.ts
@@ -13,8 +13,7 @@ export * from './abstract/observable';
 // Decorators
 export * from './decorators/bind';
 export * from './decorators/memoize';
-export * from './decorators/override';
-export * from './decorators/readonly';
+export * from './decorators/prop';
 
 // Function
 export * from './async/promise';
diff --git a/src/modules/esl-utils/decorators/override.ts b/src/modules/esl-utils/decorators/override.ts
index 59c50763f..9ca64510b 100644
--- a/src/modules/esl-utils/decorators/override.ts
+++ b/src/modules/esl-utils/decorators/override.ts
@@ -1,4 +1,6 @@
 /**
+ * @deprecated This decorator is deprecated use {@link property} decorator instead.
+ *
  * `@override` is auxiliary decorator to override a field that is decorated in the parent class.
  * The override _value_ is always defined on the object prototype level.
  *
diff --git a/src/modules/esl-utils/decorators/prop.ts b/src/modules/esl-utils/decorators/prop.ts
new file mode 100644
index 000000000..8242e7c1f
--- /dev/null
+++ b/src/modules/esl-utils/decorators/prop.ts
@@ -0,0 +1,38 @@
+/** Property configuration */
+export type OverrideDecoratorConfig = {
+  /** Value to set in the prototype */
+  value?: any;
+  /** To define mutable property */
+  readonly?: false;
+} | {
+  /** Value to set in the prototype */
+  value: any;
+  /** To define readonly property */
+  readonly: true;
+};
+
+/**
+ * `@prop` is auxiliary decorator to define a field on the prototype level.
+ *` @prop` can be used to override decorated property from the parent level
+ *
+ * You can also use an @override decorator in combination with ECMA Script class property definition:
+ * `@prop() public field: any = initial value;`
+ *
+ * The class property initial value is a part of object creation, so it goes to the object itself,
+ * while the @override value is defined on the prototype level.
+ *
+ *  @param [prototypeConfig] - prototype property configuration
+ */
+export function prop(prototypeConfig: OverrideDecoratorConfig = {}) {
+  return function (obj: any, name: string): void {
+    if (Object.hasOwnProperty.call(obj, name)) {
+      throw new TypeError('Can\'t override own property');
+    }
+    Object.defineProperty(obj, name, {
+      value: prototypeConfig.value,
+      writable: !prototypeConfig.readonly,
+      enumerable: true,
+      configurable: true
+    });
+  };
+}
diff --git a/src/modules/esl-utils/decorators/readonly.ts b/src/modules/esl-utils/decorators/readonly.ts
index 6205b0a4c..9c32e27bb 100644
--- a/src/modules/esl-utils/decorators/readonly.ts
+++ b/src/modules/esl-utils/decorators/readonly.ts
@@ -1,8 +1,10 @@
 /**
+ * @deprecated This property is deprecated use @property instead
+ *
  * `@readonly` is auxiliary decorator to create a readonly field on prototype level.
  *
- *  @param value - property value
- *  @param [silent] - to not throw error on setting value
+ * @param value - property value
+ * @param [silent] - to not throw error on setting value
  */
 export function readonly(value: any, silent = false) {
   return function (obj: any, name: string): void {


Test patch:
diff --git a/src/modules/esl-utils/decorators/test/override.test.ts b/src/modules/esl-utils/decorators/test/prop.test.ts
similarity index 72%
rename from src/modules/esl-utils/decorators/test/override.test.ts
rename to src/modules/esl-utils/decorators/test/prop.test.ts
index 5e41acd5f..8986cce55 100644
--- a/src/modules/esl-utils/decorators/test/override.test.ts
+++ b/src/modules/esl-utils/decorators/test/prop.test.ts
@@ -1,8 +1,8 @@
 import '../../../../polyfills/es5-target-shim';
 import {ESLBaseElement, attr, boolAttr, jsonAttr} from '../../../esl-base-element/core';
-import {override} from '../override';
+import {prop} from '../prop';
 
-describe('Decorator: override', () => {
+describe('Decorator: @prop', () => {
   class TestBaseElement extends ESLBaseElement {
     @attr()
     public field: string;
@@ -18,11 +18,11 @@ describe('Decorator: override', () => {
 
   describe('Overriding @attr', () => {
     class TestElement extends TestBaseElement {
-      @override('test')
+      @prop({ value: 'test' })
       public field: string;
-      @override()
+      @prop()
       public field4?: string;
-      @override('test')
+      @prop({ value: 'test' })
       public readonlyField: string;
     }
     customElements.define('attr-override-1', TestElement);
@@ -54,9 +54,38 @@ describe('Decorator: override', () => {
     });
   });
 
+  describe('Overriding with a non writable', () => {
+    class TestElement extends TestBaseElement {
+      @prop({ value: 'test', readonly: true }) public field: string;
+      @prop({ value: true, readonly: true }) public field2: boolean;
+    }
+    class TestElement2 extends TestElement {
+      @prop() public field: string = 'test2';
+      @prop({ value: false }) public field2: boolean;
+    }
+    customElements.define('attr-writable-override-1', TestElement);
+    customElements.define('attr-writable-override-2', TestElement2);
+
+    test('should override @attr and @boolAttr decorator', () => {
+      const el = new TestElement();
+      expect(el.field).toBe('test');
+      expect(el.field2).toBe(true);
+    });
+    test('override should not be writeable', () => {
+      const el = new TestElement();
+      expect(() => el.field = 'hi').toThrowError();
+      expect(() => el.field2 = false).toThrowError();
+    });
+    test('should have undefined as a default', () => {
+      const el = new TestElement2();
+      expect(el.field).toBe('test2');
+      expect(el.field2).toBe(false);
+    });
+  });
+
   describe('Overriding @boolAttr', () => {
     class TestElement extends TestBaseElement {
-      @override(true)
+      @prop({ value: true })
       public field2: boolean;
     }
     customElements.define('bool-attr-override-1', TestElement);
@@ -78,7 +107,7 @@ describe('Decorator: override', () => {
 
   describe('Overriding @jsonAttr', () => {
     class TestElement extends TestBaseElement {
-      @override({a: 2})
+      @prop({ value: {a: 2} })
       public field3: {a: number};
     }
     customElements.define('json-attr-override-1', TestElement);
@@ -100,7 +129,7 @@ describe('Decorator: override', () => {
 
   describe('Overridden property can be defined through ES initial value ', () => {
     class TestElement extends TestBaseElement {
-      @override()
+      @prop()
       public field: string = '123';
     }
     customElements.define('es-initial-attr-override-1', TestElement);
@@ -114,7 +143,7 @@ describe('Decorator: override', () => {
   test('Overriding own property produce error', () => {
     expect(() => {
       class TestElement extends ESLBaseElement {
-        @override('')
+        @prop({ value: '' })
         @attr()
         public field: string;
       }
diff --git a/src/modules/esl-utils/decorators/test/readonly.test.ts b/src/modules/esl-utils/decorators/test/readonly.test.ts
deleted file mode 100644
index 05e0d49b7..000000000
--- a/src/modules/esl-utils/decorators/test/readonly.test.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import '../../../../polyfills/es5-target-shim';
-import { ESLBaseElement, attr } from '../../../esl-base-element/core';
-import { readonly } from '../readonly';
-
-describe('Decorator: constant', () => {
-  class TestBaseElement extends ESLBaseElement {
-    @attr()
-    public field: string;
-    @attr({readonly: true})
-    public readonlyField: string;
-  }
-
-  test('Silent readonly field is not writeable and not throws error', () => {
-    class TestElement {
-      @readonly('test', true)
-      public field: string;
-    }
-
-    const el = new TestElement();
-    expect(() => { el.field = 'hey'; }).not.toThrowError();
-    expect(el.field).toBe('test');
-  });
-
-  test('Non silent readonly field is not writeable and throws error', () => {
-    class TestElement {
-      @readonly('test')
-      public field: string;
-    }
-
-    const el = new TestElement();
-    expect(() => { el.field = 'hey'; }).toThrowError();
-    expect(el.field).toBe('test');
-  });
-
-  test('Non silent readonly field is not throwing error when setting original value', () => {
-    class TestElement {
-      @readonly('test')
-      public field: string;
-    }
-
-    const el = new TestElement();
-    expect(() => { el.field = 'test'; }).not.toThrowError();
-    expect(el.field).toBe('test');
-  });
-
-  test('Overriding own property produce error', () => {
-    expect(() => {
-      class TestElement extends ESLBaseElement {
-        @readonly('')
-        @attr()
-        public field: string;
-      }
-      new TestElement();
-    }).toThrowError(/own property/);
-  });
-
-  describe('Overriding @attr', () => {
-    class TestElement extends TestBaseElement {
-      @readonly('test', true)
-      public field: string;
-
-      @readonly('test')
-      public readonlyField: string;
-    }
-    customElements.define('attr-constant-1', TestElement);
-
-    test('should override simple @attr decorator', () => {
-      const el = new TestElement();
-      expect(el.field).toBe('test');
-      expect(el.getAttribute('field')).toBe(null);
-    });
-    test('should override readonly @attr decorator', () => {
-      const el = new TestElement();
-      expect(el.readonlyField).toBe('test');
-      expect(el.getAttribute('readonly-field')).toBe(null);
-    });
-    test('overwritten property is not writable', () => {
-      const el = new TestElement();
-      el.field = '123';
-      expect(el.field).toBe('test');
-      expect(el.getAttribute('field')).toBe(null);
-    });
-  });
-});