diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e350005..620e3e45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,16 +9,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/). PRs should document their user-visible changes (if any) in the Unreleased section, uncommenting the header as necessary. --> + + + + ## Unreleased -### Fixed -* [Breaking] User defined accessors are now wrapped to enable better composition ([#286](https://github.com/Polymer/lit-element/issues/286)) - - +### Changed +* [Breaking] Changes property options to add `converter`. This option works the same as the previous `type` option except that the `converter` methods now also get `type` as the second argument. This effectively changes `type` to be a hint for the `converter`. A default `converter` is used if none is provided and it now supports `Boolean`, `String`, `Number`, `Object`, and `Array` ([#264](https://github.com/Polymer/lit-element/issues/264)). +* [Breaking] Numbers and strings now become null if their reflected attribute is removed (https://github.com/Polymer/lit-element/issues/264)). +* [Breaking] Previously, when an attribute changed as a result of a reflecting property changing, the property was prevented from mutating again as can happen when a custom +`converter` is used. Now, the oppose is also true. When a property changes as a result of an attribute changing, the attribute is prevented from mutating again (https://github.com/Polymer/lit-element/issues/264)) ### Fixed +* [Breaking] User defined accessors are now wrapped to enable better composition ([#286](https://github.com/Polymer/lit-element/issues/286)) * Type for `eventOptions` decorator now properly includes `passive` and `once` options ([#325](https://github.com/Polymer/lit-element/issues/325)) ## [0.6.5] - 2018-12-13 @@ -83,8 +89,3 @@ https://github.com/Polymer/lit-html/pull/486). * The `firstUpdated` method should now always be called the first time the element updates, even if `shouldUpdate` initially returned `false` (https://github.com/Polymer/lit-element/pull/173). - - - - - diff --git a/README.md b/README.md index 7a7b850c..7df56077 100644 --- a/README.md +++ b/README.md @@ -35,19 +35,22 @@ for additional information on how to create templates for lit-element. If the value is `false`, the property is not added to the static `observedAttributes` getter. If `true` or absent, the lowercased property name is observed (e.g. `fooBar` becomes `foobar`). If a string, the string value is observed (e.g `attribute: 'foo-bar'`). - * `type`: Indicates how to serialize and deserialize the attribute to/from a property. + * `converter`: Indicates how to convert the attribute to/from a property. The value can be a function used for both serialization and deserialization, or it can be an object with individual functions via the optional keys, `fromAttribute` and `toAttribute`. - `type` defaults to the `String` constructor, and so does the `toAttribute` and `fromAttribute` - keys. + A default `converter` is used if none is provided; it supports + `Boolean`, `String`, `Number`, `Object`, and `Array`. + * `type`: Indicates the type of the property. This is used only as a hint for the + `converter` to determine how to convert the attribute + to/from a property. Note, when a property changes and the converter is used + to update the attribute, the property is never updated again as a result of + the attribute changing, and vice versa. * `reflect`: Indicates whether the property should reflect to its associated - attribute (as determined by the attribute option). - If `true`, when the property is set, the attribute which name is determined - according to the rules for the `attribute` property option, will be set to the - value of the property serialized using the rules from the `type` property option. - Note, `type: Boolean` has special handling by default which means that truthy - values result in the presence of the attribute, whereas falsy values result - in the absence of the attribute. + attribute (as determined by the attribute option). If `true`, when the + property is set, the attribute which name is determined according to the + rules for the `attribute` property option will be set to the value of the + property converted using the rules from the `type` and `converter` + property options. * `hasChanged`: A function that indicates whether a property should be considered changed when it is set and thus result in an update. The function should take the `newValue` and `oldValue` and return `true` if an update should be requested. diff --git a/demo/lit-element.html b/demo/lit-element.html index 00659f49..e0764a4f 100644 --- a/demo/lit-element.html +++ b/demo/lit-element.html @@ -38,7 +38,7 @@ foo: {}, bar: {}, whales: {type: Number}, - fooBar: {type: {fromAttribute: parseInt, toAttribute: (value) => value + '-attr'}, reflect: true} + fooBar: {converter: {fromAttribute: parseInt, toAttribute: (value) => value + '-attr'}, reflect: true} } } diff --git a/src/demo/ts-element.ts b/src/demo/ts-element.ts index 4df4c2c5..3b1d7cbc 100644 --- a/src/demo/ts-element.ts +++ b/src/demo/ts-element.ts @@ -4,7 +4,8 @@ class TSElement extends LitElement { @property() message = 'Hi'; - @property({attribute : 'more-info', type: (value: string) => `[${value}]`}) + @property( + {attribute : 'more-info', converter: (value: string) => `[${value}]`}) extra = ''; render() { diff --git a/src/lib/updating-element.ts b/src/lib/updating-element.ts index 2d3d461b..8fe67785 100644 --- a/src/lib/updating-element.ts +++ b/src/lib/updating-element.ts @@ -33,27 +33,28 @@ const descriptorFromPrototype = (name: PropertyKey, proto: UpdatingElement) => { /** * Converts property values to and from attribute values. */ -export interface AttributeSerializer { +export interface ComplexAttributeConverter { /** - * Deserializing function called to convert an attribute value to a property + * Function called to convert an attribute value to a property * value. */ - fromAttribute?(value: string): T; + fromAttribute?(value: string, type?: TypeHint): Type; /** - * Serializing function called to convert a property value to an attribute + * Function called to convert a property value to an attribute * value. */ - toAttribute?(value: T): string|null; + toAttribute?(value: Type, type?: TypeHint): string|null; } -type AttributeType = AttributeSerializer|((value: string) => T); +type AttributeConverter = + ComplexAttributeConverter|((value: string, type?: TypeHint) => Type); /** * Defines options for a property accessor. */ -export interface PropertyDeclaration { +export interface PropertyDeclaration { /** * Indicates how and whether the property becomes an observed attribute. @@ -65,23 +66,32 @@ export interface PropertyDeclaration { attribute?: boolean|string; /** - * Indicates how to serialize and deserialize the attribute to/from a - * property. If this value is a function, it is used to deserialize the - * attribute value a the property value. If it's an object, it can have keys - * for `fromAttribute` and `toAttribute` where `fromAttribute` is the - * deserialize function and `toAttribute` is a serialize function used to set - * the property to an attribute. If no `toAttribute` function is provided and + * Indicates the type of the property. This is used only as a hint for the + * `converter` to determine how to convert the attribute + * to/from a property. + */ + type?: TypeHint; + + /** + * Indicates how to convert the attribute to/from a property. If this value + * is a function, it is used to convert the attribute value a the property + * value. If it's an object, it can have keys for `fromAttribute` and + * `toAttribute`. If no `toAttribute` function is provided and * `reflect` is set to `true`, the property value is set directly to the - * attribute. + * attribute. A default `converter` is used if none is provided; it supports + * `Boolean`, `String`, `Number`, `Object`, and `Array`. Note, + * when a property changes and the converter is used to update the attribute, + * the property is never updated again as a result of the attribute changing, + * and vice versa. */ - type?: AttributeType; + converter?: AttributeConverter; /** * Indicates if the property should reflect to an attribute. * If `true`, when the property is set, the attribute is set using the * attribute name determined according to the rules for the `attribute` - * property option and the value of the property serialized using the rules - * from the `type` property option. + * property option and the value of the property converted using the rules + * from the `converter` property option. */ reflect?: boolean; @@ -90,7 +100,7 @@ export interface PropertyDeclaration { * it is set. The function should take the `newValue` and `oldValue` and * return `true` if an update should be requested. */ - hasChanged?(value: T, oldValue: T): boolean; + hasChanged?(value: Type, oldValue: Type): boolean; /** * Indicates whether an accessor will be created for this property. By @@ -118,9 +128,35 @@ type AttributeMap = Map; export type PropertyValues = Map; -// serializer/deserializers for boolean attribute -const fromBooleanAttribute = (value: string) => value !== null; -const toBooleanAttribute = (value: string) => value ? '' : null; +export const defaultConverter: ComplexAttributeConverter = { + + toAttribute(value: any, type?: any) { + switch (type) { + case Boolean: + return value ? '' : null; + case Object: + case Array: + // if the value is `null` or `undefined` pass this through + // to allow removing/no change behavior. + return value == null ? value : JSON.stringify(value); + } + return value; + }, + + fromAttribute(value: any, type?: any) { + switch (type) { + case Boolean: + return value !== null; + case Number: + return value === null ? null : Number(value); + case Object: + case Array: + return JSON.parse(value); + } + return value; + } + +}; export interface HasChanged { (value: unknown, old: unknown): boolean; @@ -138,6 +174,7 @@ export const notEqual: HasChanged = (value: unknown, old: unknown): boolean => { const defaultPropertyDeclaration: PropertyDeclaration = { attribute : true, type : String, + converter : defaultConverter, reflect : false, hasChanged : notEqual }; @@ -146,9 +183,10 @@ const microtaskPromise = new Promise((resolve) => resolve(true)); const STATE_HAS_UPDATED = 1; const STATE_UPDATE_REQUESTED = 1 << 2; -const STATE_IS_REFLECTING = 1 << 3; +const STATE_IS_REFLECTING_TO_ATTRIBUTE = 1 << 3; +const STATE_IS_REFLECTING_TO_PROPERTY = 1 << 4; type UpdateState = typeof STATE_HAS_UPDATED|typeof STATE_UPDATE_REQUESTED| - typeof STATE_IS_REFLECTING; + typeof STATE_IS_REFLECTING_TO_ATTRIBUTE|typeof STATE_IS_REFLECTING_TO_PROPERTY; /** * Base element class which manages element properties and attributes. When @@ -286,8 +324,8 @@ export abstract class UpdatingElement extends HTMLElement { * Returns the property name for the given attribute `name`. */ private static _attributeNameForProperty(name: PropertyKey, - options?: PropertyDeclaration) { - const attribute = options !== undefined && options.attribute; + options: PropertyDeclaration) { + const attribute = options.attribute; return attribute === false ? undefined : (typeof attribute === 'string' @@ -308,21 +346,16 @@ export abstract class UpdatingElement extends HTMLElement { /** * Returns the property value for the given attribute value. - * Called via the `attributeChangedCallback` and uses the property's `type` - * or `type.fromAttribute` property option. + * Called via the `attributeChangedCallback` and uses the property's + * `converter` or `converter.fromAttribute` property option. */ private static _propertyValueFromAttribute(value: string, - options?: PropertyDeclaration) { - const type = options && options.type; - if (type === undefined) { - return value; - } - // Note: special case `Boolean` so users can use it as a `type`. + options: PropertyDeclaration) { + const type = options.type; + const converter = options.converter || defaultConverter; const fromAttribute = - type === Boolean - ? fromBooleanAttribute - : (typeof type === 'function' ? type : type.fromAttribute); - return fromAttribute ? fromAttribute(value) : value; + (typeof converter === 'function' ? converter : converter.fromAttribute); + return fromAttribute ? fromAttribute(value, type) : value; } /** @@ -333,18 +366,16 @@ export abstract class UpdatingElement extends HTMLElement { * This uses the property's `reflect` and `type.toAttribute` property options. */ private static _propertyValueToAttribute(value: unknown, - options?: PropertyDeclaration) { - if (options === undefined || options.reflect === undefined) { + options: PropertyDeclaration) { + if (options.reflect === undefined) { return; } - // Note: special case `Boolean` so users can use it as a `type`. + const type = options.type; + const converter = options.converter; const toAttribute = - options.type === Boolean - ? toBooleanAttribute - : (options.type && - (options.type as AttributeSerializer).toAttribute || - String); - return toAttribute(value); + converter && (converter as ComplexAttributeConverter).toAttribute || + defaultConverter.toAttribute; + return toAttribute!(value, type); } private _updateState: UpdateState = 0; @@ -464,41 +495,49 @@ export abstract class UpdatingElement extends HTMLElement { name: PropertyKey, value: unknown, options: PropertyDeclaration = defaultPropertyDeclaration) { const ctor = (this.constructor as typeof UpdatingElement); - const attrValue = ctor._propertyValueToAttribute(value, options); - if (attrValue !== undefined) { - const attr = ctor._attributeNameForProperty(name, options); - if (attr !== undefined) { - // Track if the property is being reflected to avoid - // setting the property again via `attributeChangedCallback`. Note: - // 1. this takes advantage of the fact that the callback is synchronous. - // 2. will behave incorrectly if multiple attributes are in the reaction - // stack at time of calling. However, since we process attributes - // in `update` this should not be possible (or an extreme corner case - // that we'd like to discover). - // mark state reflecting - this._updateState = this._updateState | STATE_IS_REFLECTING; - if (attrValue === null) { - this.removeAttribute(attr); - } else { - this.setAttribute(attr, attrValue); - } - // mark state not reflecting - this._updateState = this._updateState & ~STATE_IS_REFLECTING; + const attr = ctor._attributeNameForProperty(name, options); + if (attr !== undefined) { + const attrValue = ctor._propertyValueToAttribute(value, options); + // an undefined value does not change the attribute. + if (attrValue === undefined) { + return; } + // Track if the property is being reflected to avoid + // setting the property again via `attributeChangedCallback`. Note: + // 1. this takes advantage of the fact that the callback is synchronous. + // 2. will behave incorrectly if multiple attributes are in the reaction + // stack at time of calling. However, since we process attributes + // in `update` this should not be possible (or an extreme corner case + // that we'd like to discover). + // mark state reflecting + this._updateState = this._updateState | STATE_IS_REFLECTING_TO_ATTRIBUTE; + if (attrValue == null) { + this.removeAttribute(attr); + } else { + this.setAttribute(attr, attrValue); + } + // mark state not reflecting + this._updateState = this._updateState & ~STATE_IS_REFLECTING_TO_ATTRIBUTE; } } private _attributeToProperty(name: string, value: string) { // Use tracking info to avoid deserializing attribute value if it was // just set from a property setter. - if (!(this._updateState & STATE_IS_REFLECTING)) { - const ctor = (this.constructor as typeof UpdatingElement); - const propName = ctor._attributeToPropertyMap.get(name); - if (propName !== undefined) { - const options = ctor._classProperties.get(propName); - this[propName as keyof this] = - ctor._propertyValueFromAttribute(value, options); - } + if (this._updateState & STATE_IS_REFLECTING_TO_ATTRIBUTE) { + return; + } + const ctor = (this.constructor as typeof UpdatingElement); + const propName = ctor._attributeToPropertyMap.get(name); + if (propName !== undefined) { + const options = + ctor._classProperties.get(propName) || defaultPropertyDeclaration; + // mark state reflecting + this._updateState = this._updateState | STATE_IS_REFLECTING_TO_PROPERTY; + this[propName as keyof this] = + ctor._propertyValueFromAttribute(value, options); + // mark state not reflecting + this._updateState = this._updateState & ~STATE_IS_REFLECTING_TO_PROPERTY; } } @@ -526,8 +565,10 @@ export abstract class UpdatingElement extends HTMLElement { } // track old value when changing. this._changedProperties.set(name, oldValue); - // add to reflecting properties set - if (options.reflect === true) { + // Add to reflecting properties set if `reflect` is true and the property + // is not reflecting to the property from the attribute + if (options.reflect === true && + !(this._updateState & STATE_IS_REFLECTING_TO_PROPERTY)) { if (this._reflectingProperties === undefined) { this._reflectingProperties = new Map(); } diff --git a/src/test/lit-element_test.ts b/src/test/lit-element_test.ts index 60ceb6ef..f409f8b6 100644 --- a/src/test/lit-element_test.ts +++ b/src/test/lit-element_test.ts @@ -13,6 +13,7 @@ */ import { + ComplexAttributeConverter, html, LitElement, property, @@ -161,12 +162,12 @@ suite('LitElement', () => { atTr : {attribute : true}, customAttr : {attribute : 'custom', reflect : true}, hasChanged : {hasChanged}, - fromAttribute : {type : fromAttribute}, - toAttribute : {reflect : true, type : {toAttribute}}, + fromAttribute : {converter : fromAttribute}, + toAttribute : {reflect : true, converter : {toAttribute}}, all : { attribute : 'all-attr', hasChanged, - type : {fromAttribute, toAttribute}, + converter : {fromAttribute, toAttribute}, reflect : true }, }; @@ -246,6 +247,274 @@ suite('LitElement', () => { assert.equal(el.updateCount, 6); }); + test('property option `converter` can use `type` info', async () => { + const FooType = {name : 'FooType'}; + // Make test work on IE where these are undefined. + if (!('name' in String)) { + (String as any).name = (String as any).name || 'String'; + } + if (!('name' in Number)) { + (Number as any).name = (Number as any).name || 'Number'; + } + + const converter: ComplexAttributeConverter = { + fromAttribute : + (_value: any, + type: any) => { return `fromAttribute: ${String(type.name)}`; }, + toAttribute : + (_value: any, + type: any) => { return `toAttribute: ${String(type.name)}`; } + }; + + class E extends LitElement { + static get properties() { + return { + num : {type : Number, converter, reflect : true}, + str : {type : String, converter, reflect : true}, + foo : {type : FooType, converter, reflect : true} + }; + } + + num?: any; + str?: any; + foo?: any; + + render() { return html``; } + } + customElements.define(generateElementName(), E); + const el = new E(); + container.appendChild(el); + await el.updateComplete; + el.num = 5; + el.str = 'hi'; + el.foo = 'zoink'; + await el.updateComplete; + assert.equal(el.getAttribute('num'), 'toAttribute: Number'); + assert.equal(el.getAttribute('str'), 'toAttribute: String'); + assert.equal(el.getAttribute('foo'), 'toAttribute: FooType'); + el.removeAttribute('num'); + el.removeAttribute('str'); + el.removeAttribute('foo'); + await el.updateComplete; + assert.equal(el.num, 'fromAttribute: Number'); + assert.equal(el.str, 'fromAttribute: String'); + assert.equal(el.foo, 'fromAttribute: FooType'); + assert.equal(el.getAttribute('num'), null); + assert.equal(el.getAttribute('str'), null); + assert.equal(el.getAttribute('foo'), null); + el.num = 0; + el.str = ''; + el.foo = {}; + await el.updateComplete; + assert.equal(el.getAttribute('num'), 'toAttribute: Number'); + assert.equal(el.getAttribute('str'), 'toAttribute: String'); + assert.equal(el.getAttribute('foo'), 'toAttribute: FooType'); + }); + + test('property/attribute values when attributes removed', async () => { + class E extends LitElement { + static get properties() { + return { + bool : {type : Boolean}, + num : {type : Number}, + str : {type : String}, + obj : {type : Object}, + arr : {type : Array}, + reflectBool : {type : Boolean, reflect : true}, + reflectNum : {type : Number, reflect : true}, + reflectStr : {type : String, reflect : true}, + reflectObj : {type : Object, reflect : true}, + reflectArr : {type : Array, reflect : true}, + defaultBool : {type : Boolean}, + defaultNum : {type : Number}, + defaultStr : {type : String}, + defaultObj : {type : Object}, + defaultArr : {type : Array}, + defaultReflectBool : {type : Boolean, reflect : true}, + defaultReflectNum : {type : Number, reflect : true}, + defaultReflectStr : {type : String, reflect : true}, + defaultReflectObj : {type : Object, reflect : true}, + defaultReflectArr : {type : Array, reflect : true}, + }; + } + + bool?: any; + num?: any; + str?: any; + obj?: any; + arr?: any; + reflectBool?: any; + reflectNum?: any; + reflectStr?: any; + reflectObj?: any; + reflectArr?: any; + defaultBool = false; + defaultNum = 0; + defaultStr = ''; + defaultObj = {defaultObj : false}; + defaultArr = [ 1 ]; + defaultReflectBool = false; + defaultReflectNum = 0; + defaultReflectStr = 'defaultReflectStr'; + defaultReflectObj = {defaultReflectObj : true}; + defaultReflectArr = [ 1, 2 ]; + + render() { return html``; } + } + const name = generateElementName(); + customElements.define(name, E); + container.innerHTML = `<${name} bool num="2" str="str" obj='{"obj": true}' + arr='[1]' reflectBool reflectNum="3" reflectStr="reflectStr" + reflectObj ='{"reflectObj": true}' reflectArr="[1, 2]" + defaultBool defaultNum="4" defaultStr="defaultStr" + defaultObj='{"defaultObj": true}' defaultArr="[1, 2, 3]"> + `; + const el = container.firstChild as E; + await el.updateComplete; + assert.equal(el.bool, true); + assert.equal(el.num, 2); + assert.equal(el.str, 'str'); + assert.deepEqual(el.obj, {obj : true}); + assert.deepEqual(el.arr, [ 1 ]); + assert.equal(el.reflectBool, true); + assert.equal(el.reflectNum, 3); + assert.equal(el.reflectStr, 'reflectStr'); + assert.deepEqual(el.reflectObj, {reflectObj : true}); + assert.deepEqual(el.reflectArr, [ 1, 2 ]); + assert.equal(el.defaultBool, true); + assert.equal(el.defaultNum, 4); + assert.equal(el.defaultStr, 'defaultStr'); + assert.deepEqual(el.defaultObj, {defaultObj : true}); + assert.deepEqual(el.defaultArr, [ 1, 2, 3 ]); + assert.equal(el.defaultReflectBool, false); + assert.equal(el.defaultReflectNum, 0); + assert.equal(el.defaultReflectStr, 'defaultReflectStr'); + assert.deepEqual(el.defaultReflectObj, {defaultReflectObj : true}); + assert.deepEqual(el.defaultReflectArr, [ 1, 2 ]); + el.removeAttribute('bool'); + el.removeAttribute('num'); + el.removeAttribute('str'); + el.removeAttribute('obj'); + el.removeAttribute('arr'); + el.removeAttribute('reflectbool'); + el.removeAttribute('reflectnum'); + el.removeAttribute('reflectstr'); + el.removeAttribute('reflectobj'); + el.removeAttribute('reflectarr'); + el.removeAttribute('defaultbool'); + el.removeAttribute('defaultnum'); + el.removeAttribute('defaultstr'); + el.removeAttribute('defaultobj'); + el.removeAttribute('defaultarr'); + el.removeAttribute('defaultreflectbool'); + el.removeAttribute('defaultreflectnum'); + el.removeAttribute('defaultreflectstr'); + el.removeAttribute('defaultreflectobj'); + el.removeAttribute('defaultreflectarr'); + await el.updateComplete; + assert.equal(el.bool, false); + assert.equal(el.num, null); + assert.equal(el.str, null); + assert.deepEqual(el.obj, null); + assert.deepEqual(el.arr, null); + assert.equal(el.reflectBool, false); + assert.equal(el.reflectNum, null); + assert.equal(el.reflectStr, null); + assert.deepEqual(el.reflectObj, null); + assert.deepEqual(el.reflectArr, null); + assert.equal(el.defaultBool, false); + assert.equal(el.defaultNum, null); + assert.equal(el.defaultStr, null); + assert.deepEqual(el.defaultObj, null); + assert.deepEqual(el.defaultArr, null); + assert.equal(el.defaultReflectBool, false); + assert.equal(el.defaultReflectNum, null); + assert.equal(el.defaultReflectStr, null); + assert.deepEqual(el.defaultReflectObj, null); + assert.deepEqual(el.defaultReflectArr, null); + }); + + test('attributes removed when a reflecting property\'s value becomes null', async () => { + class E extends LitElement { + static get properties() { + return { + bool : {type : Boolean, reflect: true}, + num : {type : Number, reflect: true}, + str : {type : String, reflect: true}, + obj : {type : Object, reflect: true}, + arr : {type : Array, reflect: true} + }; + } + + bool?: any; + num?: any; + str?: any; + obj?: any; + arr?: any; + + render() { return html``; } + } + const name = generateElementName(); + customElements.define(name, E); + container.innerHTML = `<${name} bool num="2" str="str" obj='{"obj": true}' + arr='[1]'> + `; + const el = container.firstChild as E; + await el.updateComplete; + el.bool = false; + el.num = null; + el.str = null; + el.obj = null; + el.arr = null; + await el.updateComplete; + assert.isFalse(el.hasAttribute('bool')); + assert.isFalse(el.hasAttribute('num')); + assert.isFalse(el.hasAttribute('str')); + assert.isFalse(el.hasAttribute('obj')); + assert.isFalse(el.hasAttribute('arr')); + }); + + test('if a `reflect: true` returns `undefined`, the attribute does not change', async () => { + class E extends LitElement { + static get properties() { + return { + foo: {reflect: true}, + obj: {type: Object, reflect: true} + }; + } + + foo?: any; + obj?: any; + + render() { return html``; } + } + const name = generateElementName(); + customElements.define(name, E); + const el = new E(); + container.appendChild(el); + await el.updateComplete; + el.setAttribute('foo', 'foo'); + el.setAttribute('obj', '{"obj": 1}'); + assert.equal(el.foo, 'foo'); + assert.deepEqual(el.obj, {obj: 1}); + await el.updateComplete; + el.foo = 'foo2'; + el.obj = {obj: 2}; + await el.updateComplete; + assert.equal(el.getAttribute('foo'), 'foo2'); + assert.equal(el.getAttribute('obj'), '{"obj":2}'); + el.foo = undefined; + el.obj = undefined; + await el.updateComplete; + assert.equal(el.getAttribute('foo'), 'foo2'); + assert.equal(el.getAttribute('obj'), '{"obj":2}'); + el.foo = 'foo3'; + el.obj = {obj: 3}; + await el.updateComplete; + assert.equal(el.getAttribute('foo'), 'foo3'); + assert.equal(el.getAttribute('obj'), '{"obj":3}'); + }); + test('property options via decorator', async () => { const hasChanged = (value: any, old: any) => old === undefined || value > old; @@ -258,12 +527,12 @@ suite('LitElement', () => { @property({attribute : 'custom', reflect: true}) customAttr = 'customAttr'; @property({hasChanged}) hasChanged = 10; - @property({type : fromAttribute}) fromAttribute = 1; - @property({reflect : true, type: {toAttribute}}) toAttribute = 1; + @property({converter : fromAttribute}) fromAttribute = 1; + @property({reflect : true, converter: {toAttribute}}) toAttribute = 1; @property({ attribute : 'all-attr', hasChanged, - type: {fromAttribute, toAttribute}, + converter: {fromAttribute, toAttribute}, reflect: true }) all = 10; @@ -342,12 +611,12 @@ suite('LitElement', () => { class E extends LitElement { @property({hasChanged}) hasChanged = 10; - @property({type : fromAttribute}) fromAttribute = 1; - @property({reflect : true, type: {toAttribute}}) toAttribute = 1; + @property({converter : fromAttribute}) fromAttribute = 1; + @property({reflect : true, converter: {toAttribute}}) toAttribute = 1; @property({ attribute : 'all-attr', hasChanged, - type: {fromAttribute, toAttribute}, + converter: {fromAttribute, toAttribute}, reflect: true }) all = 10; @@ -450,14 +719,16 @@ suite('LitElement', () => { noAttr : {attribute : false}, atTr : {attribute : true}, customAttr : {attribute : 'custom', reflect : true}, - fromAttribute : {type : fromAttribute}, + fromAttribute : {converter : fromAttribute}, toAttribute : - {reflect : true, type : {toAttribute : toAttributeOnly}}, + {reflect : true, converter : {toAttribute : toAttributeOnly}}, all : { attribute : 'all-attr', - type : {fromAttribute, toAttribute}, + converter : {fromAttribute, toAttribute}, reflect : true }, + obj : {type : Object}, + arr : {type : Array} }; } @@ -467,6 +738,8 @@ suite('LitElement', () => { fromAttribute = 1; toAttribute: string|number = 1; all = 10; + obj?: any; + arr?: any; render() { return html``; } } @@ -478,7 +751,9 @@ suite('LitElement', () => { custom="3" fromAttribute="6-attr" toAttribute="7" - all-attr="11-attr">`; + all-attr="11-attr" + obj='{"foo": true, "bar": 5, "baz": "hi"}' + arr="[1, 2, 3, 4]">`; const el = container.firstChild as E; await el.updateComplete; assert.equal(el.noAttr, 'noAttr'); @@ -491,6 +766,8 @@ suite('LitElement', () => { assert.equal(el.getAttribute('toattribute'), '7-attr'); assert.equal(el.all, 11); assert.equal(el.getAttribute('all-attr'), '11-attr'); + assert.deepEqual(el.obj, {foo : true, bar : 5, baz : 'hi'}); + assert.deepEqual(el.arr, [ 1, 2, 3, 4 ]); }); if (Object.getOwnPropertySymbols) { @@ -540,7 +817,7 @@ suite('LitElement', () => { [zug] : { attribute : 'zug', reflect : true, - type : (value: string) => Number(value) + 100 + converter : (value: string) => Number(value) + 100 } }; } @@ -564,7 +841,7 @@ suite('LitElement', () => { assert.equal(el.getAttribute('zug'), '6'); el.setAttribute('zug', '7'); await el.updateComplete; - assert.equal(el.getAttribute('zug'), '107'); + assert.equal(el.getAttribute('zug'), '7'); assert.equal(el[zug], 107); }); } @@ -618,12 +895,12 @@ suite('LitElement', () => { class G extends F { static get properties(): PropertyDeclarations { return { - fromAttribute : {type : fromAttribute}, - toAttribute : {reflect : true, type : {toAttribute}}, + fromAttribute : {converter : fromAttribute}, + toAttribute : {reflect : true, converter : {toAttribute}}, all : { attribute : 'all-attr', hasChanged, - type : {fromAttribute, toAttribute}, + converter : {fromAttribute, toAttribute}, reflect : true }, }; @@ -746,7 +1023,7 @@ suite('LitElement', () => { return { foo : { reflect : true, - type : {toAttribute : (value: any) => `${value}${suffix}`} + converter : {toAttribute : (value: any) => `${value}${suffix}`} } }; } @@ -919,7 +1196,7 @@ suite('LitElement', () => { bar : { attribute : 'attr-bar', reflect : true, - type : {fromAttribute, toAttribute}, + converter : {fromAttribute, toAttribute}, hasChanged } }; @@ -956,12 +1233,12 @@ suite('LitElement', () => { await el.updateComplete; assert.equal(el.updateCount, 2); assert.equal(el.bar, 7); - assert.equal(el.getAttribute('attr-bar'), `7-attr`); + assert.equal(el.getAttribute('attr-bar'), `7`); el.bar = 4; await el.updateComplete; assert.equal(el.updateCount, 2); assert.equal(el.bar, 4); - assert.equal(el.getAttribute('attr-bar'), `7-attr`); + assert.equal(el.getAttribute('attr-bar'), `7`); el.setAttribute('attr-bar', '3'); await el.updateComplete; assert.equal(el.updateCount, 2);