Skip to content

Commit

Permalink
Changes property options to support converter
Browse files Browse the repository at this point in the history
Fixes #264

Changes `type` to be only a hint to the `converter` option which has the previous `type` functionality, an object with `toAttribute` and `fromAttribute` or just a function which is `fromAttribute`. In addition to the `value` these functions now also get the property's `type`.

Also provides a default converter that supports `Boolean`, `String`, `Number`, `Object`, and `Array` out of the box.

In addition, numbers and strings now become `null` if their reflected attribute is removed.
  • Loading branch information
Steven Orvell committed Dec 13, 2018
1 parent 87e3fc8 commit 421acd2
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 73 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
* Types for the `property` and `customElement` decorators updated ([#288](https://github.com/Polymer/lit-element/issues/288) and [#291](https://github.com/Polymer/lit-element/issues/291)).

<!-- ### Changed -->
### Changed
* 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`. In addition, numbers and strings now become null if their reflected attribute is removed. ([#264](https://github.com/Polymer/lit-element/issues/264)).


<!-- ### Added -->
<!-- ### Removed -->
<!-- ### Fixed -->
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ 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 serialize and deserialize 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.
* `type`: Indicates the type of the property. This is used only as a hint for the
`converter` to determine how to serialize and deserialize the attribute
to/from a property.
* `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
Expand Down
2 changes: 1 addition & 1 deletion demo/lit-element.html
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/demo/ts-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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() {
Expand Down
116 changes: 68 additions & 48 deletions src/lib/updating-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@
/**
* Converts property values to and from attribute values.
*/
export interface AttributeSerializer<T = any> {
export interface ComplexAttributeConverter<T = any> {

/**
* Deserializing function called to convert an attribute value to a property
* value.
*/
fromAttribute?(value: string): T;
fromAttribute?(value: string, type?: any): T;

/**
* Serializing function called to convert a property value to an attribute
* value.
*/
toAttribute?(value: T): string|null;
toAttribute?(value: T, type?: any): string|null;
}

type AttributeType<T = any> = AttributeSerializer<T>|((value: string) => T);
type AttributeConverter<T = any> = ComplexAttributeConverter<T>|((value: string, type?: any) => T);

/**
* Defines options for a property accessor.
Expand All @@ -46,6 +46,13 @@ export interface PropertyDeclaration<T = any> {
*/
attribute?: boolean|string;

/**
* Indicates the type of the property. This is used only as a hint for the
* converter to determine how to serialize and deserialize the attribute
* to/from a property.
*/
type?: T;

/**
* Indicates how to serialize and deserialize the attribute to/from a
* property. If this value is a function, it is used to deserialize the
Expand All @@ -56,14 +63,14 @@ export interface PropertyDeclaration<T = any> {
* `reflect` is set to `true`, the property value is set directly to the
* attribute.
*/
type?: AttributeType<T>;
converter?: AttributeConverter<T>;

/**
* 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.
* from the `converter` property option.
*/
reflect?: boolean;

Expand All @@ -90,9 +97,33 @@ type AttributeMap = Map<string, PropertyKey>;

export type PropertyValues = Map<PropertyKey, unknown>;

// 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:
return 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;
Expand All @@ -110,6 +141,7 @@ export const notEqual: HasChanged = (value: unknown, old: unknown): boolean => {
const defaultPropertyDeclaration: PropertyDeclaration = {
attribute : true,
type : String,
converter: defaultConverter,
reflect : false,
hasChanged : notEqual
};
Expand Down Expand Up @@ -260,21 +292,15 @@ 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`.
const fromAttribute =
type === Boolean
? fromBooleanAttribute
: (typeof type === 'function' ? type : type.fromAttribute);
return fromAttribute ? fromAttribute(value) : value;
const converter = options && options.converter || defaultConverter;
const fromAttribute = (typeof converter === 'function' ? converter : converter.fromAttribute);
return fromAttribute ? fromAttribute(value, type) : value;
}

/**
Expand All @@ -289,14 +315,10 @@ export abstract class UpdatingElement extends HTMLElement {
if (options === undefined || options.reflect === undefined) {
return;
}
// Note: special case `Boolean` so users can use it as a `type`.
const toAttribute =
options.type === Boolean
? toBooleanAttribute
: (options.type &&
(options.type as AttributeSerializer).toAttribute ||
String);
return toAttribute(value);
const type = options && options.type;
const converter = options && options.converter;
const toAttribute = converter && (converter as ComplexAttributeConverter).toAttribute || defaultConverter.toAttribute;
return toAttribute!(value, type);
}

private _updateState: UpdateState = 0;
Expand Down Expand Up @@ -416,27 +438,25 @@ 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);
// 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;
}
}

Expand Down
Loading

0 comments on commit 421acd2

Please sign in to comment.