Skip to content

Commit 71d48e5

Browse files
43081j43081j
authored and
43081j
committed
add computed decorator
1 parent 9bcac25 commit 71d48e5

File tree

5 files changed

+112
-29
lines changed

5 files changed

+112
-29
lines changed

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,18 @@ class TestElement extends Polymer.Element {
126126
@property()
127127
bar: string = 'yes';
128128

129+
// @computed replaces the getter with a computed property
130+
@computed('foo')
131+
get computedExample() {
132+
return this.foo * 2;
133+
}
134+
135+
// @computed also takes multiple parameters
136+
@computed('foo', 'bar')
137+
get computedExampleTwo() {
138+
return `${this.bar}: ${this.foo}`;
139+
}
140+
129141
// @query replaces the property with a getter that querySelectors() in
130142
// the shadow root. Use this for type-safe access to internal nodes.
131143
@query('h1')

src/decorators.ts

+58-27
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,40 @@ export interface PropertyOptions {
4242
notify?: boolean;
4343
reflectToAttribute?: boolean;
4444
readOnly?: boolean;
45+
computed?: string;
46+
}
47+
48+
function createProperty(proto: any, name: string, options?: PropertyOptions): void {
49+
const notify: boolean = options && options.notify || false;
50+
const reflectToAttribute: boolean =
51+
options && options.reflectToAttribute || false;
52+
const readOnly: boolean = options && options.readOnly || false;
53+
const computed: string|undefined = options && options.computed || undefined;
54+
55+
let type;
56+
if (options && options.hasOwnProperty('type')) {
57+
type = options.type;
58+
} else if (
59+
(window as any).Reflect && Reflect.hasMetadata && Reflect.getMetadata &&
60+
Reflect.hasMetadata('design:type', proto, name)) {
61+
type = Reflect.getMetadata('design:type', proto, name);
62+
} else {
63+
console.error(
64+
'A type could not be found for ${propName}. ' +
65+
'Set a type or configure Metadata Reflection API support.');
66+
}
67+
68+
if (!proto.constructor.hasOwnProperty('properties')) {
69+
proto.constructor.properties = {};
70+
}
71+
72+
proto.constructor.properties[name] = {
73+
type,
74+
notify,
75+
reflectToAttribute,
76+
readOnly,
77+
computed
78+
};
4579
}
4680

4781
/**
@@ -52,33 +86,7 @@ export interface PropertyOptions {
5286
*/
5387
export function property(options?: PropertyOptions) {
5488
return (proto: any, propName: string): any => {
55-
const notify: boolean = options && options.notify || false;
56-
const reflectToAttribute: boolean =
57-
options && options.reflectToAttribute || false;
58-
const readOnly: boolean = options && options.readOnly || false;
59-
60-
let type;
61-
if (options && options.hasOwnProperty('type')) {
62-
type = options.type;
63-
} else if (
64-
(window as any).Reflect && Reflect.hasMetadata && Reflect.getMetadata &&
65-
Reflect.hasMetadata('design:type', proto, propName)) {
66-
type = Reflect.getMetadata('design:type', proto, propName);
67-
} else {
68-
console.error(
69-
'A type could not be found for ${propName}. ' +
70-
'Set a type or configure Metadata Reflection API support.');
71-
}
72-
73-
if (!proto.constructor.hasOwnProperty('properties')) {
74-
proto.constructor.properties = {};
75-
}
76-
proto.constructor.properties[propName] = {
77-
type,
78-
notify,
79-
reflectToAttribute,
80-
readOnly,
81-
};
89+
createProperty(proto, propName, options);
8290
}
8391
}
8492

@@ -100,6 +108,29 @@ export function observe(targets: string|string[]) {
100108
}
101109
}
102110

111+
/**
112+
* A TypeScript accessor decorator factory that causes the decorated accessor to
113+
* be called when a property changes. `targets` is either a single property
114+
* name, or a list of property names.
115+
*
116+
* This function must be invoked to return a decorator.
117+
*/
118+
export function computed<T = any>(...targets: (keyof T)[]) {
119+
return (proto: any, propName: string, descriptor: PropertyDescriptor): void => {
120+
const targetString = targets.join(',');
121+
const propNameCased = propName[0].toUpperCase() + propName.slice(1);
122+
const fnName = `__compute${propNameCased}`;
123+
124+
proto.constructor.prototype[fnName] = descriptor.get;
125+
126+
descriptor.get = undefined;
127+
128+
createProperty(proto, propName, {
129+
computed: `${fnName}(${targetString})`
130+
});
131+
};
132+
}
133+
103134
/**
104135
* A TypeScript property decorator factory that converts a class property into
105136
* 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
@@ -94,6 +94,32 @@ suite('TypeScript Decorators', function() {
9494

9595
});
9696

97+
suite('@computed', function() {
98+
99+
test('defines a computed property', function() {
100+
testElement.dependencyOne = 'foo';
101+
102+
const compDiv = testElement.shadowRoot.querySelector('#computed');
103+
chai.assert.equal(compDiv.textContent, 'foo');
104+
chai.assert.equal(testElement.computedOne, 'foo');
105+
});
106+
107+
test('defines a computed property with multiple arguments', function() {
108+
testElement.dependencyOne = 'foo';
109+
110+
const compDiv = testElement.shadowRoot.querySelector('#computedTwo');
111+
112+
chai.assert.equal(compDiv.textContent, 'foo');
113+
chai.assert.equal(testElement.computedTwo, 'foo');
114+
115+
testElement.dependencyTwo = 'bar';
116+
117+
chai.assert.equal(compDiv.textContent, 'foobar');
118+
chai.assert.equal(testElement.computedTwo, 'foobar');
119+
});
120+
121+
});
122+
97123
suite('@query', function() {
98124

99125
test('queries the shadow root', function() {
@@ -107,7 +133,7 @@ suite('TypeScript Decorators', function() {
107133

108134
test('queries the shadow root', function() {
109135
const divs = testElement.divs;
110-
chai.assert.equal(divs.length, 2);
136+
chai.assert.equal(divs.length, 4);
111137
});
112138

113139
});

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 {
@@ -33,6 +33,18 @@ class TestElement extends Polymer.Element {
3333
@property({readOnly:true})
3434
readOnlyString: string;
3535

36+
@property()
37+
dependencyOne: string = '';
38+
39+
@property()
40+
dependencyTwo: string = '';
41+
42+
@computed('dependencyOne')
43+
get computedOne() { return this.dependencyOne; }
44+
45+
@computed('dependencyOne', 'dependencyTwo')
46+
get computedTwo() { return this.dependencyOne + this.dependencyTwo; }
47+
3648
// stand-in for set function dynamically created by Polymer on read only properties
3749
_setReadOnlyString: (value: string) => void;
3850

0 commit comments

Comments
 (0)