Skip to content

Commit f3fd7fd

Browse files
43081jjustinfagnani
authored andcommitted
add @computed decorator (#20)
1 parent 8fbb6f3 commit f3fd7fd

File tree

5 files changed

+109
-27
lines changed

5 files changed

+109
-27
lines changed

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ class TestElement extends Polymer.Element {
134134
private onBazChanged(newValue: string, oldValue: string) {
135135
}
136136

137+
// @computed replaces the getter with a computed property
138+
@computed('foo')
139+
get computedExample() {
140+
return this.foo * 2;
141+
}
142+
143+
// @computed also takes multiple parameters
144+
@computed('foo', 'bar')
145+
get computedExampleTwo() {
146+
return `${this.bar}: ${this.foo}`;
147+
}
148+
137149
// @query replaces the property with a getter that querySelectors() in
138150
// the shadow root. Use this for type-safe access to internal nodes.
139151
@query('h1')

src/decorators.ts

+55-25
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,35 @@ export interface PropertyOptions {
4646
observer?: string|((val: any, old: any) => void);
4747
}
4848

49+
function createProperty(proto: any, name: string, options?: PropertyOptions): void {
50+
const notify = options && options.notify || false;
51+
const reflectToAttribute = options && options.reflectToAttribute || false;
52+
const readOnly = options && options.readOnly || false;
53+
const computed = options && options.computed || '';
54+
const observer = options && options.observer || '';
55+
56+
let type;
57+
if (options && options.hasOwnProperty('type')) {
58+
type = options.type;
59+
} else if (
60+
(window as any).Reflect && Reflect.hasMetadata && Reflect.getMetadata &&
61+
Reflect.hasMetadata('design:type', proto, name)) {
62+
type = Reflect.getMetadata('design:type', proto, name);
63+
} else {
64+
console.error(
65+
'A type could not be found for ${propName}. ' +
66+
'Set a type or configure Metadata Reflection API support.');
67+
}
68+
69+
if (!proto.constructor.hasOwnProperty('properties')) {
70+
proto.constructor.properties = {};
71+
}
72+
73+
const finalOpts: PropertyOptions =
74+
{type, notify, reflectToAttribute, readOnly, computed, observer};
75+
proto.constructor.properties[name] = finalOpts;
76+
}
77+
4978
/**
5079
* A TypeScript property decorator factory that defines this as a Polymer
5180
* property.
@@ -54,31 +83,7 @@ export interface PropertyOptions {
5483
*/
5584
export function property(options?: PropertyOptions) {
5685
return (proto: any, propName: string): any => {
57-
const notify = options && options.notify || false;
58-
const reflectToAttribute = options && options.reflectToAttribute || false;
59-
const readOnly = options && options.readOnly || false;
60-
const computed = options && options.computed || '';
61-
const observer = options && options.observer || '';
62-
63-
let type;
64-
if (options && options.hasOwnProperty('type')) {
65-
type = options.type;
66-
} else if (
67-
(window as any).Reflect && Reflect.hasMetadata && Reflect.getMetadata &&
68-
Reflect.hasMetadata('design:type', proto, propName)) {
69-
type = Reflect.getMetadata('design:type', proto, propName);
70-
} else {
71-
console.error(
72-
'A type could not be found for ${propName}. ' +
73-
'Set a type or configure Metadata Reflection API support.');
74-
}
75-
76-
if (!proto.constructor.hasOwnProperty('properties')) {
77-
proto.constructor.properties = {};
78-
}
79-
const finalOpts: PropertyOptions =
80-
{type, notify, reflectToAttribute, readOnly, computed, observer};
81-
proto.constructor.properties[propName] = finalOpts;
86+
createProperty(proto, propName, options);
8287
}
8388
}
8489

@@ -100,6 +105,31 @@ export function observe(targets: string|string[]) {
100105
}
101106
}
102107

108+
/**
109+
* A TypeScript accessor decorator factory that causes the decorated getter to
110+
* be called when a set of dependencies change. The arguments of this decorator
111+
* should be paths of the data dependencies as described
112+
* [here](https://www.polymer-project.org/2.0/docs/devguide/observers#define-a-computed-property)
113+
* The decorated getter should not have an associated setter.
114+
*
115+
* This function must be invoked to return a decorator.
116+
*/
117+
export function computed<T = any>(...targets: (keyof T)[]) {
118+
return (proto: any, propName: string, descriptor: PropertyDescriptor): void => {
119+
const fnName = `__compute${propName}`;
120+
121+
Object.defineProperty(proto, fnName, {
122+
value: descriptor.get
123+
});
124+
125+
descriptor.get = undefined;
126+
127+
createProperty(proto, propName, {
128+
computed: `${fnName}(${targets.join(',')})`
129+
});
130+
};
131+
}
132+
103133
/**
104134
* A TypeScript property decorator factory that converts a class property into
105135
* a getter that executes a querySelector on the element's shadow root.

test/integration/decorators.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,32 @@ suite('TypeScript Decorators', function() {
106106

107107
});
108108

109+
suite('@computed', function() {
110+
111+
test('defines a computed property', function() {
112+
testElement.dependencyOne = 'foo';
113+
114+
const compDiv = testElement.shadowRoot.querySelector('#computed');
115+
chai.assert.equal(compDiv.textContent, 'foo');
116+
chai.assert.equal(testElement.computedOne, 'foo');
117+
});
118+
119+
test('defines a computed property with multiple arguments', function() {
120+
testElement.dependencyOne = 'foo';
121+
122+
const compDiv = testElement.shadowRoot.querySelector('#computedTwo');
123+
124+
chai.assert.equal(compDiv.textContent, 'foo');
125+
chai.assert.equal(testElement.computedTwo, 'foo');
126+
127+
testElement.dependencyTwo = 'bar';
128+
129+
chai.assert.equal(compDiv.textContent, 'foobar');
130+
chai.assert.equal(testElement.computedTwo, 'foobar');
131+
});
132+
133+
});
134+
109135
suite('@query', function() {
110136

111137
test('queries the shadow root', function() {
@@ -119,7 +145,7 @@ suite('TypeScript Decorators', function() {
119145

120146
test('queries the shadow root', function() {
121147
const divs = testElement.divs;
122-
chai.assert.equal(divs.length, 2);
148+
chai.assert.equal(divs.length, 4);
123149
});
124150

125151
});

test/integration/elements/test-element.html

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
<template>
66
<div id="num">{{aNum}}</div>
77
<div id="string">{{aString}}</div>
8+
<div id="computed">{{computedOne}}</div>
9+
<div id="computedTwo">{{computedTwo}}</div>
810
</template>
911
<script src="./test-element.js"></script>
1012
</dom-module>

test/integration/elements/test-element.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
/// <reference path="../bower_components/polymer-decorators/global.d.ts" />
1313

14-
const {customElement, property, query, queryAll, observe} = Polymer.decorators;
14+
const {customElement, property, query, queryAll, observe, computed} = Polymer.decorators;
1515

1616
@customElement('test-element')
1717
class TestElement extends Polymer.Element {
@@ -39,6 +39,18 @@ class TestElement extends Polymer.Element {
3939
@property({observer:'observeString'})
4040
observedString: string;
4141

42+
@property()
43+
dependencyOne: string = '';
44+
45+
@property()
46+
dependencyTwo: string = '';
47+
48+
@computed('dependencyOne')
49+
get computedOne() { return this.dependencyOne; }
50+
51+
@computed('dependencyOne', 'dependencyTwo')
52+
get computedTwo() { return this.dependencyOne + this.dependencyTwo; }
53+
4254
// stand-in for set function dynamically created by Polymer on read only properties
4355
_setReadOnlyString: (value: string) => void;
4456

0 commit comments

Comments
 (0)