Skip to content

Commit 8055b93

Browse files
author
Steve Orvell
authored
Merge pull request #914 from Polymer/customize-properties
Makes `createProperty` easier to use to customize properties
2 parents 8084b9f + 90c85a5 commit 8055b93

File tree

4 files changed

+205
-12
lines changed

4 files changed

+205
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1919
## Unreleased
2020

2121
### Changed
22-
* The value returned by `render` is always rendered, even if it isn't a `TemplateResult`. ([#712](https://github.com/Polymer/lit-element/issues/712)
22+
* Added a static `getPropertyDescriptor` method to allow easier customization of property accessors. This method should return a a `PropertyDescriptor` to install on the property. If no descriptor is returned, a property accessor is not be created. ([#911](https://github.com/Polymer/lit-element/issues/911))
23+
* The value returned by `render` is always rendered, even if it isn't a `TemplateResult`. ([#712](https://github.com/Polymer/lit-element/issues/712))
2324

2425
### Added
2526
* Added `@queryAsync(selector)` decorator which returns a Promise that resolves to the result of querying for the given selector after the element's `updateComplete` Promise resolves ([#903](https://github.com/Polymer/lit-element/issues/903)).

src/demo/ts-element.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class TSElement extends LitElement {
44
@property() message = 'Hi';
55

66
@property(
7-
{attribute: 'more-info', converter: (value: string) => `[${value}]`})
7+
{attribute: 'more-info', converter: (value: string|null) => `[${value}]`})
88
extra = '';
99

1010
render() {

src/lib/updating-element.ts

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export interface ComplexAttributeConverter<Type = unknown, TypeHint = unknown> {
5151
}
5252

5353
type AttributeConverter<Type = unknown, TypeHint = unknown> =
54-
ComplexAttributeConverter<Type>|((value: string, type?: TypeHint) => Type);
54+
ComplexAttributeConverter<Type>|((value: string|null, type?: TypeHint) => Type);
5555

5656
/**
5757
* Defines options for a property accessor.
@@ -112,6 +112,10 @@ export interface PropertyDeclaration<Type = unknown, TypeHint = unknown> {
112112
* the property changes.
113113
*/
114114
readonly noAccessor?: boolean;
115+
116+
// Allows extension while preserving the ability to use the
117+
// @property decorator.
118+
[index: string]: unknown;
115119
}
116120

117121
/**
@@ -281,10 +285,25 @@ export abstract class UpdatingElement extends HTMLElement {
281285
}
282286

283287
/**
284-
* Creates a property accessor on the element prototype if one does not exist.
288+
* Creates a property accessor on the element prototype if one does not exist
289+
* and stores a PropertyDeclaration for the property with the given options.
285290
* The property setter calls the property's `hasChanged` property option
286291
* or uses a strict identity check to determine whether or not to request
287292
* an update.
293+
*
294+
* This method may be overridden to customize properties; however,
295+
* when doing so, it's important to call `super.createProperty` to ensure
296+
* the property is setup correctly. This method calls
297+
* `getPropertyDescriptor` internally to get a descriptor to install.
298+
* To customize what properties do when they are get or set, override
299+
* `getPropertyDescriptor`. To customize the options for a property,
300+
* implement `createProperty` like this:
301+
*
302+
* static createProperty(name, options) {
303+
* options = Object.assign(options, {myOption: true});
304+
* super.createProperty(name, options);
305+
* }
306+
*
288307
* @nocollapse
289308
*/
290309
static createProperty(
@@ -304,7 +323,38 @@ export abstract class UpdatingElement extends HTMLElement {
304323
return;
305324
}
306325
const key = typeof name === 'symbol' ? Symbol() : `__${name}`;
307-
Object.defineProperty(this.prototype, name, {
326+
const descriptor = this.getPropertyDescriptor(name, key, options);
327+
if (descriptor !== undefined) {
328+
Object.defineProperty(this.prototype, name, descriptor);
329+
}
330+
}
331+
332+
/**
333+
* Returns a property descriptor to be defined on the given named property.
334+
* If no descriptor is returned, the property will not become an accessor.
335+
* For example,
336+
*
337+
* class MyElement extends LitElement {
338+
* static getPropertyDescriptor(name, key, options) {
339+
* const defaultDescriptor = super.getPropertyDescriptor(name, key, options);
340+
* const setter = defaultDescriptor.set;
341+
* return {
342+
* get: defaultDescriptor.get,
343+
* set(value) {
344+
* setter.call(this, value);
345+
* // custom action.
346+
* },
347+
* configurable: true,
348+
* enumerable: true
349+
* }
350+
* }
351+
* }
352+
*
353+
* @nocollapse
354+
*/
355+
protected static getPropertyDescriptor(name: PropertyKey,
356+
key: string|symbol, _options: PropertyDeclaration) {
357+
return {
308358
// tslint:disable-next-line:no-any no symbol in index
309359
get(): any {
310360
return (this as {[key: string]: unknown})[key as string];
@@ -317,7 +367,23 @@ export abstract class UpdatingElement extends HTMLElement {
317367
},
318368
configurable: true,
319369
enumerable: true
320-
});
370+
};
371+
}
372+
373+
/**
374+
* Returns the property options associated with the given property.
375+
* These options are defined with a PropertyDeclaration via the `properties`
376+
* object or the `@property` decorator and are registered in
377+
* `createProperty(...)`.
378+
*
379+
* Note, this method should be considered "final" and not overridden. To
380+
* customize the options for a given property, override `createProperty`.
381+
*
382+
* @final
383+
*/
384+
protected static getPropertyOptions(name: PropertyKey) {
385+
return this._classProperties && this._classProperties.get(name) ||
386+
defaultPropertyDeclaration;
321387
}
322388

323389
/**
@@ -563,8 +629,7 @@ export abstract class UpdatingElement extends HTMLElement {
563629
const ctor = (this.constructor as typeof UpdatingElement);
564630
const propName = ctor._attributeToPropertyMap.get(name);
565631
if (propName !== undefined) {
566-
const options =
567-
ctor._classProperties!.get(propName) || defaultPropertyDeclaration;
632+
const options = ctor.getPropertyOptions(propName);
568633
// mark state reflecting
569634
this._updateState = this._updateState | STATE_IS_REFLECTING_TO_PROPERTY;
570635
this[propName as keyof this] =
@@ -585,8 +650,7 @@ export abstract class UpdatingElement extends HTMLElement {
585650
// If we have a property key, perform property update steps.
586651
if (name !== undefined) {
587652
const ctor = this.constructor as typeof UpdatingElement;
588-
const options =
589-
ctor._classProperties!.get(name) || defaultPropertyDeclaration;
653+
const options = ctor.getPropertyOptions(name);
590654
if (ctor._valueHasChanged(
591655
this[name as keyof this], oldValue, options.hasChanged)) {
592656
if (!this._changedProperties.has(name)) {

src/test/lib/updating-element_test.ts

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
* http://polymer.github.io/PATENTS.txt
1313
*/
1414

15-
import {property} from '../../lib/decorators.js';
16-
import {ComplexAttributeConverter, PropertyDeclarations, PropertyValues, UpdatingElement} from '../../lib/updating-element.js';
15+
import {property, customElement} from '../../lib/decorators.js';
16+
import {ComplexAttributeConverter, PropertyDeclarations, PropertyValues, UpdatingElement, PropertyDeclaration, defaultConverter} from '../../lib/updating-element.js';
1717
import {generateElementName} from '../test-helpers.js';
1818

1919
// tslint:disable:no-any ok in tests
@@ -1802,6 +1802,134 @@ suite('UpdatingElement', () => {
18021802
assert.equal(sub.getAttribute('foo'), '5');
18031803
});
18041804

1805+
test('can provide a default property declaration', async () => {
1806+
1807+
const SpecialNumber = {};
1808+
1809+
const myPropertyDeclaration = {
1810+
type: SpecialNumber,
1811+
reflect: true,
1812+
converter: {
1813+
toAttribute: function(value: unknown, type?: unknown): unknown {
1814+
switch (type) {
1815+
case String:
1816+
return value === undefined ? null : value;
1817+
default:
1818+
return defaultConverter.toAttribute!(value, type);
1819+
}
1820+
},
1821+
fromAttribute: function(value: string|null, type?: unknown) {
1822+
switch (type) {
1823+
case SpecialNumber:
1824+
return Number(value) + 10;
1825+
default:
1826+
return defaultConverter.fromAttribute!(value, type);
1827+
}
1828+
}
1829+
}
1830+
};
1831+
1832+
@customElement(generateElementName())
1833+
class E extends UpdatingElement {
1834+
1835+
static createProperty(
1836+
name: PropertyKey,
1837+
options: PropertyDeclaration) {
1838+
// Always mix into defaults to preserve custom converter.
1839+
options = Object.assign(Object.create(myPropertyDeclaration), options);
1840+
super.createProperty(name, options);
1841+
}
1842+
1843+
@property()
1844+
foo = 5;
1845+
1846+
@property({type: String})
1847+
bar?: string = 'bar';
1848+
}
1849+
1850+
const el = new E();
1851+
container.appendChild(el);
1852+
el.setAttribute('foo', '10');
1853+
el.setAttribute('bar', 'attrBar');
1854+
await el.updateComplete;
1855+
assert.equal(el.foo, 20);
1856+
assert.equal(el.bar, 'attrBar');
1857+
el.foo = 5;
1858+
el.bar = undefined;
1859+
await el.updateComplete;
1860+
assert.equal(el.getAttribute('foo'), '5');
1861+
assert.isFalse(el.hasAttribute('bar'));
1862+
});
1863+
1864+
test('can customize property options and accessor creation', async () => {
1865+
1866+
interface MyPropertyDeclaration<TypeHint = unknown> extends PropertyDeclaration {
1867+
validator?: (value: any) => TypeHint;
1868+
observer?: (oldValue: TypeHint) => void;
1869+
}
1870+
1871+
@customElement(generateElementName())
1872+
class E extends UpdatingElement {
1873+
1874+
static getPropertyDescriptor(name: PropertyKey, key: string|symbol, options: MyPropertyDeclaration) {
1875+
const defaultDescriptor = super.getPropertyDescriptor(name, key, options);
1876+
return {
1877+
get: defaultDescriptor.get,
1878+
set(this: E, value: unknown) {
1879+
const oldValue =
1880+
(this as unknown as {[key: string]: unknown})[name as string];
1881+
if (options.validator) {
1882+
value = options.validator(value);
1883+
}
1884+
(this as unknown as {[key: string]: unknown})[key as string] = value;
1885+
(this as unknown as UpdatingElement).requestUpdate(name, oldValue);
1886+
},
1887+
1888+
configurable: defaultDescriptor.configurable,
1889+
enumerable: defaultDescriptor.enumerable
1890+
};
1891+
}
1892+
1893+
updated(changedProperties: PropertyValues) {
1894+
super.updated(changedProperties);
1895+
changedProperties.forEach((value: unknown, key: PropertyKey) => {
1896+
const options = (this.constructor as typeof UpdatingElement)
1897+
.getPropertyOptions(key) as MyPropertyDeclaration;
1898+
const observer = options.observer;
1899+
if (typeof observer === 'function') {
1900+
observer.call(this, value);
1901+
}
1902+
});
1903+
}
1904+
1905+
@property({type: Number, validator: (value: number) => Math.min(10, Math.max(value, 0))})
1906+
foo = 5;
1907+
1908+
@property({})
1909+
bar = 'bar';
1910+
1911+
// tslint:disable-next-line:no-any
1912+
_observedZot?: any;
1913+
1914+
@property({observer: function(this: E, oldValue: string) { this._observedZot = {value: this.zot, oldValue}; } })
1915+
zot = '';
1916+
}
1917+
1918+
const el = new E();
1919+
container.appendChild(el);
1920+
await el.updateComplete;
1921+
el.foo = 20;
1922+
assert.equal(el.foo, 10);
1923+
assert.deepEqual(el._observedZot, {value: '', oldValue: undefined});
1924+
el.foo = -5;
1925+
assert.equal(el.foo, 0);
1926+
el.bar = 'bar2';
1927+
assert.equal(el.bar, 'bar2');
1928+
el.zot = 'zot';
1929+
await el.updateComplete;
1930+
assert.deepEqual(el._observedZot, {value: 'zot', oldValue: ''});
1931+
});
1932+
18051933
test('attribute-based property storage', async () => {
18061934
class E extends UpdatingElement {
18071935
_updateCount = 0;

0 commit comments

Comments
 (0)