Skip to content

Commit

Permalink
Support new decorators proposal as implemented in Babel 7
Browse files Browse the repository at this point in the history
* Moves @Property tests to decorators_test.ts
* Adds a Babel build step to compile decorators_test.ts to decorators-babel_test.js
* Updates decorators to detect if they’re called in legacy or modern decorator mode

Fixes #156
  • Loading branch information
justinfagnani committed Jan 9, 2019
1 parent 85fe270 commit 4ae3cdc
Show file tree
Hide file tree
Showing 8 changed files with 1,284 additions and 313 deletions.
6 changes: 6 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"plugins":[
["@babel/plugin-proposal-decorators", {"decoratorsBeforeExport": true}],
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-typescript"]
}
1,074 changes: 971 additions & 103 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
],
"scripts": {
"build": "tsc",
"build:babel-test": "babel src/test/lib/decorators_test.ts --out-file test/lib/decrators-babel_test.js",
"gen-docs": "typedoc --readme docs/_api/api-readme.md --tsconfig tsconfig_apidoc.json --mode modules --theme docs/_api/theme --excludeNotExported --excludePrivate --ignoreCompilerErrors --exclude '{**/*test*,**/node_modules/**,**/test/**}' --out ./docs/api src/**/*.ts",
"test": "npm run build && wct",
"test": "npm run build && npm run build:babel-test && wct",
"checksize": "rollup -c ; rm lit-element.bundled.js",
"format": "find src test | grep '\\.js$\\|\\.ts$' | xargs clang-format --style=file -i",
"lint": "tslint --project ./",
Expand All @@ -31,6 +32,10 @@
},
"author": "The Polymer Authors",
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/plugin-proposal-class-properties": "^7.2.3",
"@babel/plugin-proposal-decorators": "^7.2.3",
"@babel/plugin-transform-typescript": "^7.2.0",
"@types/chai": "^4.0.1",
"@types/mocha": "^5.2.4",
"@webcomponents/shadycss": "^1.5.2",
Expand Down
16 changes: 16 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,19 @@ interface Window {
ShadyCSS?: ShadyCSS;
ShadyDOM?: ShadyDOM;
}

// From the TC39 Decorators proposal
interface ClassDescriptor {
kind: 'class';
elements: ClassElement[];
}

// From the TC39 Decorators proposal
interface ClassElement {
kind: 'field'|'method';
key: PropertyKey;
placement: 'static'|'prototype'|'own';
initializer?: Function;
extras?;
finisher?;
}
106 changes: 89 additions & 17 deletions src/lib/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,73 @@ export type Constructor<T> = {
*
*/
export const customElement = (tagName: string) =>
(clazz: Constructor<HTMLElement>) => {
window.customElements.define(tagName, clazz);
// Cast as any because TS doesn't recognize the return type as being a
// subtype of the decorated class when clazz is typed as
// `Constructor<HTMLElement>` for some reason. `Constructor<HTMLElement>`
// is helpful to make sure the decorator is applied to elements however.
return clazz as any;
(classOrDescriptor: Constructor<HTMLElement>|ClassDescriptor) => {
if (typeof classOrDescriptor === 'function') {
const clazz = classOrDescriptor as Constructor<HTMLElement>;
// Legacy decorator
window.customElements.define(tagName, clazz);
// Cast as any because TS doesn't recognize the return type as being a
// subtype of the decorated class when clazz is typed as
// `Constructor<HTMLElement>` for some reason.
// `Constructor<HTMLElement>` is helpful to make sure the decorator is
// applied to elements however.
return clazz as any;
}
const {kind, elements} = classOrDescriptor;
console.assert(kind === 'class');
return {
kind,
elements,
// This callback is called once the class is otherwise fully defined
finisher(clazz: Constructor<HTMLElement>) {
window.customElements.define(tagName, clazz);
}
};
};

/**
* A property decorator which creates a LitElement property which reflects a
* corresponding attribute value. A `PropertyDeclaration` may optionally be
* supplied to configure property features.
*/
export const property = (options?: PropertyDeclaration) => (
proto: Object, name: PropertyKey) => {
(proto.constructor as typeof UpdatingElement).createProperty(name, options);
};
export const property = (options?: PropertyDeclaration) =>
(protoOrDescriptor: Object|ClassElement, name?: PropertyKey): any => {
if (name !== undefined) {
// Legacy decorator
(protoOrDescriptor.constructor as typeof UpdatingElement)
.createProperty(name!, options);
return;
}
const element = protoOrDescriptor as ClassElement;
console.assert(element.kind === 'field');
// createProperty() takes care of defining the property, but we still must
// return some kind of descriptor, so return a descriptor for an unused
// prototype field. The finisher calls createProperty().
return {
kind : 'field',
key : Symbol(),
placement : 'own',
descriptor : {},
// When @babel/plugin-proposal-decorators implements initializers, do
// this instead of the initializer below.
// See: https://github.com/babel/babel/issues/9260
// extras: [
// {
// kind: 'initializer',
// placement: 'own',
// initializer: descriptor.initializer,
// }
// ],
initializer(this: any) {
if (typeof element.initializer === 'function') {
this[element.key] = element.initializer!.call(this);
}
},
finisher(clazz: typeof UpdatingElement) {
clazz.createProperty(element.key, options);
}
};
};

/**
* A property decorator that converts a class property into a getter that
Expand All @@ -81,12 +130,24 @@ export const queryAll = _query((target: NodeSelector, selector: string) =>
* against `target`.
*/
function _query<T>(queryFn: (target: NodeSelector, selector: string) => T) {
return (selector: string) => (proto: any, propName: string) => {
Object.defineProperty(proto, propName, {
return (selector: string) => (protoOrDescriptor: any, name?: string): any => {
const descriptor = {
get(this: LitElement) { return queryFn(this.renderRoot!, selector); },
enumerable : true,
configurable : true,
});
};
if (name !== undefined) {
// Legacy decorator
Object.defineProperty(protoOrDescriptor, name, descriptor);
} else {
const element = protoOrDescriptor as ClassElement;
return {
kind : 'method',
placement : 'prototype',
key : element.key,
descriptor,
};
}
};
}

Expand Down Expand Up @@ -117,7 +178,18 @@ function _query<T>(queryFn: (target: NodeSelector, selector: string) => T) {
* }
*/
export const eventOptions = (options: AddEventListenerOptions) =>
(proto: any, name: string) => {
// This comment is here to fix a disagreement between formatter and linter
Object.assign(proto[name], options);
(protoOrDescriptor: any, name?: string) => {
if (name !== undefined) {
// Legacy decorator
Object.assign(protoOrDescriptor[name], options);
} else {
return {
...protoOrDescriptor,
finisher(clazz: typeof UpdatingElement) {
Object.assign(
clazz.prototype[protoOrDescriptor.key as keyof UpdatingElement],
options);
},
};
}
};
196 changes: 195 additions & 1 deletion src/test/lib/decorators_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
* http://polymer.github.io/PATENTS.txt
*/

import {eventOptions} from '../../lib/decorators.js';
import {eventOptions, property} from '../../lib/decorators.js';
import {
customElement,
html,
LitElement,
PropertyValues,
query,
queryAll
} from '../../lit-element.js';
Expand Down Expand Up @@ -95,6 +96,199 @@ suite('decorators', () => {
});
});

suite('@property', () => {
test('property options via decorator', async () => {
const hasChanged = (value: any, old: any) =>
old === undefined || value > old;
const fromAttribute = (value: any) => parseInt(value);
const toAttribute = (value: any) => `${value}-attr`;
class E extends LitElement {

@property({attribute : false}) noAttr = 'noAttr';
@property({attribute : true}) atTr = 'attr';
@property({attribute : 'custom', reflect: true})
customAttr = 'customAttr';
@property({hasChanged}) hasChanged = 10;
@property({converter : fromAttribute}) fromAttribute = 1;
@property({reflect : true, converter: {toAttribute}}) toAttribute = 1;
@property({
attribute : 'all-attr',
hasChanged,
converter: {fromAttribute, toAttribute},
reflect: true
})
all = 10;

updateCount = 0;

update(changed: PropertyValues) {
this.updateCount++;
super.update(changed);
}

render() { return html``; }
}
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.equal(el.updateCount, 1);
assert.equal(el.noAttr, 'noAttr');
assert.equal(el.atTr, 'attr');
assert.equal(el.customAttr, 'customAttr');
assert.equal(el.hasChanged, 10);
assert.equal(el.fromAttribute, 1);
assert.equal(el.toAttribute, 1);
assert.equal(el.getAttribute('toattribute'), '1-attr');
assert.equal(el.all, 10);
assert.equal(el.getAttribute('all-attr'), '10-attr');
el.setAttribute('noattr', 'noAttr2');
el.setAttribute('attr', 'attr2');
el.setAttribute('custom', 'customAttr2');
el.setAttribute('fromattribute', '2attr');
el.toAttribute = 2;
el.all = 5;
await el.updateComplete;
assert.equal(el.updateCount, 2);
assert.equal(el.noAttr, 'noAttr');
assert.equal(el.atTr, 'attr2');
assert.equal(el.customAttr, 'customAttr2');
assert.equal(el.fromAttribute, 2);
assert.equal(el.toAttribute, 2);
assert.equal(el.getAttribute('toattribute'), '2-attr');
assert.equal(el.all, 5);
el.all = 15;
await el.updateComplete;
assert.equal(el.updateCount, 3);
assert.equal(el.all, 15);
assert.equal(el.getAttribute('all-attr'), '15-attr');
el.setAttribute('all-attr', '16-attr');
await el.updateComplete;
assert.equal(el.updateCount, 4);
assert.equal(el.getAttribute('all-attr'), '16-attr');
assert.equal(el.all, 16);
el.hasChanged = 5;
await el.updateComplete;
assert.equal(el.hasChanged, 5);
assert.equal(el.updateCount, 4);
el.hasChanged = 15;
await el.updateComplete;
assert.equal(el.hasChanged, 15);
assert.equal(el.updateCount, 5);
el.setAttribute('all-attr', '5-attr');
await el.updateComplete;
assert.equal(el.all, 5);
assert.equal(el.updateCount, 5);
el.all = 15;
await el.updateComplete;
assert.equal(el.all, 15);
assert.equal(el.updateCount, 6);
});

test('can mix property options via decorator and via getter', async () => {
const hasChanged = (value: any, old: any) =>
old === undefined || value > old;
const fromAttribute = (value: any) => parseInt(value);
const toAttribute = (value: any) => `${value}-attr`;
class E extends LitElement {

@property({hasChanged}) hasChanged = 10;
@property({converter : fromAttribute}) fromAttribute = 1;
@property({reflect : true, converter: {toAttribute}}) toAttribute = 1;
@property({
attribute : 'all-attr',
hasChanged,
converter: {fromAttribute, toAttribute},
reflect: true
})
all = 10;

updateCount = 0;

static get properties() {
return {
noAttr : {attribute : false},
atTr : {attribute : true},
customAttr : {attribute : 'custom', reflect : true},
};
}

noAttr: string|undefined;
atTr: string|undefined;
customAttr: string|undefined;

constructor() {
super();
this.noAttr = 'noAttr';
this.atTr = 'attr';
this.customAttr = 'customAttr';
}

update(changed: PropertyValues) {
this.updateCount++;
super.update(changed);
}

render() { return html``; }
}
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.equal(el.updateCount, 1);
assert.equal(el.noAttr, 'noAttr');
assert.equal(el.atTr, 'attr');
assert.equal(el.customAttr, 'customAttr');
assert.equal(el.hasChanged, 10);
assert.equal(el.fromAttribute, 1);
assert.equal(el.toAttribute, 1);
assert.equal(el.getAttribute('toattribute'), '1-attr');
assert.equal(el.all, 10);
assert.equal(el.getAttribute('all-attr'), '10-attr');
el.setAttribute('noattr', 'noAttr2');
el.setAttribute('attr', 'attr2');
el.setAttribute('custom', 'customAttr2');
el.setAttribute('fromattribute', '2attr');
el.toAttribute = 2;
el.all = 5;
await el.updateComplete;
assert.equal(el.updateCount, 2);
assert.equal(el.noAttr, 'noAttr');
assert.equal(el.atTr, 'attr2');
assert.equal(el.customAttr, 'customAttr2');
assert.equal(el.fromAttribute, 2);
assert.equal(el.toAttribute, 2);
assert.equal(el.getAttribute('toattribute'), '2-attr');
assert.equal(el.all, 5);
el.all = 15;
await el.updateComplete;
assert.equal(el.updateCount, 3);
assert.equal(el.all, 15);
assert.equal(el.getAttribute('all-attr'), '15-attr');
el.setAttribute('all-attr', '16-attr');
await el.updateComplete;
assert.equal(el.updateCount, 4);
assert.equal(el.getAttribute('all-attr'), '16-attr');
assert.equal(el.all, 16);
el.hasChanged = 5;
await el.updateComplete;
assert.equal(el.hasChanged, 5);
assert.equal(el.updateCount, 4);
el.hasChanged = 15;
await el.updateComplete;
assert.equal(el.hasChanged, 15);
assert.equal(el.updateCount, 5);
el.setAttribute('all-attr', '5-attr');
await el.updateComplete;
assert.equal(el.all, 5);
assert.equal(el.updateCount, 5);
el.all = 15;
await el.updateComplete;
assert.equal(el.all, 15);
assert.equal(el.updateCount, 6);
});
});

suite('@query', () => {
@customElement(generateElementName() as keyof HTMLElementTagNameMap)
class C extends LitElement {
Expand Down
Loading

0 comments on commit 4ae3cdc

Please sign in to comment.