diff --git a/src/modules/esl-base-element/core.ts b/src/modules/esl-base-element/core.ts index ef57d58d5..e016353f6 100644 --- a/src/modules/esl-base-element/core.ts +++ b/src/modules/esl-base-element/core.ts @@ -3,3 +3,4 @@ export * from './core/esl-base-element'; export * from './decorators/attr'; export * from './decorators/bool-attr'; export * from './decorators/json-attr'; +export * from './decorators/override'; diff --git a/src/modules/esl-base-element/decorators/override.ts b/src/modules/esl-base-element/decorators/override.ts new file mode 100644 index 000000000..21c10ecd8 --- /dev/null +++ b/src/modules/esl-base-element/decorators/override.ts @@ -0,0 +1,16 @@ +/** + * `@override` is auxiliary decorator to override field that decorated in the parent class. + * + * Typically used to override {@link attr}, {@link boolAttr}, etc + * + * @param [value] - initial property value + * @param [readonly] - make a non writable constant + */ +export function override(value: any = undefined, readonly = false) { + 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, enumerable: true, writable: !readonly}); + }; +} diff --git a/src/modules/esl-base-element/test/override.test.ts b/src/modules/esl-base-element/test/override.test.ts new file mode 100644 index 000000000..e75a648bf --- /dev/null +++ b/src/modules/esl-base-element/test/override.test.ts @@ -0,0 +1,141 @@ +import '../../../polyfills/es5-target-shim'; +import {ESLBaseElement, attr, override, boolAttr, jsonAttr} from '../core'; + +describe('Decorator: override', () => { + class TestBaseElement extends ESLBaseElement { + @attr() + public field: string; + @boolAttr() + public field2: boolean; + @jsonAttr() + public field3: {a: number}; + @attr() + public field4?: string; + @attr({readonly: true}) + public readonlyField: string; + } + + describe('Overriding @attr', () => { + class TestElement extends TestBaseElement { + @override('test') + public field: string; + @override() + public field4?: string; + @override('test') + public readonlyField: string; + } + customElements.define('attr-override-1', TestElement); + + test('should override simple @attr decorator', () => { + const el = new TestElement(); + expect(el.field).toBe('test'); + }); + test('should override readonly @attr decorator', () => { + const el = new TestElement(); + expect(el.readonlyField).toBe('test'); + }); + test('override should be writeable', () => { + const el = new TestElement(); + el.field = el.readonlyField = 'hi'; + expect(el.field).toBe('hi'); + expect(el.readonlyField).toBe('hi'); + }); + test('original decorator should not be executed', () => { + const el = new TestElement(); + el.field = 'hi'; + expect(el.getAttribute('field')).toBe(null); + }); + + test('should have undefined as a default', () => { + const el = new TestElement(); + expect('field4' in el).toBe(true); + expect(el.field4).toBe(undefined); + }); + }); + + describe('Overriding @boolAttr', () => { + class TestElement extends TestBaseElement { + @override(true) + public field2: boolean; + } + customElements.define('bool-attr-override-1', TestElement); + + test('should override simple @boolAttr decorator', () => { + const el = new TestElement(); + expect(el.field2).toBe(true); + }); + test('override should be writeable', () => { + const el = new TestElement(); + el.field2 = false; + expect(el.field2).toBe(false); + }); + test('original decorator should not be executed', () => { + const el = new TestElement(); + expect(el.getAttribute('field2')).toBe(null); + }); + }); + + describe('Overriding @jsonAttr', () => { + class TestElement extends TestBaseElement { + @override({a: 2}) + public field3: {a: number}; + } + customElements.define('json-attr-override-1', TestElement); + + test('should override simple @jsonAttr decorator', () => { + const el = new TestElement(); + expect(el.field3).toEqual({a: 2}); + }); + test('override should be writeable', () => { + const el = new TestElement(); + el.field3 = {a: 4}; + expect(el.field3).toEqual({a: 4}); + }); + test('original decorator should not be executed', () => { + const el = new TestElement(); + expect(el.getAttribute('field3')).toBe(null); + }); + }); + + describe('Overriding @attr with readonly decorator', () => { + class TestElement extends TestBaseElement { + @override('test', true) + public field: string; + } + customElements.define('readonly-attr-override-1', TestElement); + + test('should override simple @attr decorator', () => { + const el = new TestElement(); + expect(el.field).toBe('test'); + }); + test('override should be writeable', () => { + const el = new TestElement(); + expect(() => el.field ='hi').toThrowError(); + expect(el.field).toBe('test'); + }); + }); + + describe('Overridden property can be defined through ES initial value ', () => { + class TestElement extends TestBaseElement { + @override() + public field: string = '123'; + } + customElements.define('es-initial-attr-override-1', TestElement); + + test('should override simple @attr decorator', () => { + const el = new TestElement(); + expect(el.field).toBe('123'); + }); + }); + + test('Overriding own property produce error', () => { + expect(() => { + class TestElement extends ESLBaseElement { + @override('') + @attr() + public field: string; + } + new TestElement(); + }).toThrowError(/own property/); + }); +});