# Data system concepts 1. Observers- Callbacks invoked when data changes. 2. Computed properties- Virtual properties computed based on other properties, and recomputed when the input data changes. 3. Data bindings- Annotations that update the properties, attributes, or text content of a DOM node when data changes. Simple element: class NameCard extends PolymerElement { constructor() { super(); this.name = {first: 'Kai', last: 'Li'}; } static get template() { return html `
[[name.first]] [[name.last]]
`; } } customElements.define('name-card', NameCard); 1. The `` element has a name property that refers to a JavaScript object. 2. The `` element hosts a local DOM tree, that contains a single
element. 3. Data bindings in the template link the JavaScript object to the
element. For example, the `` element has data bindings for the paths "name.first" and "name.last" ## Observable changes -Setting a direct property of the element. this.owner = 'Jane'; -Setting a subproperty of the element using a two way data binding. ## Unobservable changes -Setting a subproperty of an object. // unobservable subproperty change this.address.street = 'Elm Street'; -Mutating an array: // unobservable change using native Array.push this.users.push({ name: 'Maturin'}); ## Mutating objects and arrays observably Polymer provides methods for making observable changes to subproperties and arrays: // mutate an object observably this.set('address.street', 'Half Moon Street'); // mutate an array observably this.push('users', { name: 'Maturin'}); #### Batched property changes You can atomically set multiple properties using the setProperties method. this.setProperties({item: 'Orange', count: 12 }); For example, // observer fires twice this.a = 10; this.b = 20; // observer fires once this.setProperties({a: 10, b: 20}); The two types of data binding annotations are: ### 1 - Automatic, which allows upward (target to host) and downwards (host to target) data flow. Automatic bindings use double curly brackets ({{ }}): ### 2 - One-way, which only allows downwards data flow. Upward data flow is disabled. One-way bindings use double square brackets ([[ ]]). #### Example of one-way binding (downward) class XHost extends PolymerElement { static get template() { return html` `; } } .................... ... class XTarget extends PolymerElement { static get properties() { return { someProp: { type: String //no notify: true } } } } ... ... class XHost extends PolymerElement { static get template() { return html` `; } } ... #### Example of one-way binding (upward, child-to-host) ... class XTarget extends PolymerElement { static get properties() { return { someProp: { type: String, notify: true, readOnly: true } } } } ... customElements.define('x-target', XTarget); ... class XHost extends PolymerElement { static get template() { return html` `; } } ... customElements.define('x-host', XHost); #### Example of no data flow / nonsensical state ... class XTarget extends PolymerElement { static get properties() { return { someProp: { type: String, notify: true, readOnly: true } } } } ... customElements.define('x-target', XTarget); ... class XHost extends PolymerElement { static get template() { return html` `; } } ... customElements.define('x-host', XHost); ## Property effects 1. Computed properties. 2. Data bindings. 3. Reflected values. 4. Observers. 5. Change notification events. #### Data bindings Two-way property binding: target-property="{{hostProperty}}" One-way property binding: target-property="[[hostProperty]]" Attribute binding: target-attribute$="[[hostProperty]]" # Work with object and array data Get a value by path Use the get method to retrieve a value based on its path. // retrieve a subproperty by path var value = this.get('myProp.subProp'); // Retrieve the 11th item in myArray var item = this.get(['myArray', 11]) ### Set a property or subproperty by path Use the set method to make an observable change to a subproperty. // clear an array this.set('group.members', []); // set a subproperty this.set('profile.name', 'Alex'); ### Working with arrays Example import { PolymerElement, html } from '@polymer/polymer/polymer-element.js'; import '@polymer/polymer/lib/elements/dom-repeat.js'; class XCustom extends PolymerElement { static get template() { return html`

`; } constructor() { super(); this._reset(); } addUser() { var user = "wing sang"; this.push('users', user); } removeUser() { var user = "wing sang"; var index = this.users.indexOf(user); this.splice('users', index, 1); } _reset() { this.users=["wing sang", "deshaun", "kelley"]; } } customElements.define('x-custom', XCustom); ### Batch multiple property changes Use **setProperties** method to make a batch change to a set of properties. This ensures the property changes run as a coherent set. this.setProperties({ date: 'Jan 17, 2017', verified: true }); **setProperties** supports an optional **setReadOnly** flag as the second parameter. If you need to set read-only properties as part of a batch change, pass true for the second parameter: this.setProperties({ date: 'Jan 17, 2017', verified: true }, true); #### Link two paths to the same object linkPaths('selectedUser', 'users.1'); To remove a path linkage, call **unlinkPaths**, passing in the first path you passed to linkPaths: unlinkPaths('selectedUser'); # Observers and computed properties - Simple observers observe a single property. - Complex observers can observe one or more properties or paths. ## Simple observers Simple observers are declared in the properties object, and always observe a single property. Simple observers only fire when the property itself changes. They don't fire on subproperty changes, or array mutation. If you need these changes, use a complex observer with a wildcard path ### Observe a property Define a simple observer by adding an observer key to the property's declaration, identifying the observer method by name. Example import { PolymerElement } from '@polymer/polymer/polymer-element.js'; class XCustom extends PolymerElement { static get properties() { return { active: { type: Boolean, // Observer method identified by name observer: '_activeChanged' } } } // Observer method defined as a class method _activeChanged(newValue, oldValue) { this.toggleClass('highlight', newValue); } } customElements.define('x-custom', XCustom); The observer method is usually defined on the class itself, although an observer method can also be defined by a superclass, subclass, or a class mixin, as long as the named method exists on the element. ## Complex observers Complex observers are declared in the observers array. Complex observers can monitor one or more paths. These paths are called the observer's dependencies. static get observers() { return [ // Observer method name, followed by a list of dependencies, in parenthesis 'userListChanged(users.*, filter)' ] } Each dependency represents: - A specific property (for example, firstName). - A specific subproperty (for example, address.street). - Mutations on a specific array (for example, users.splices). - All subproperty changes and array mutations below a given path (for example, users.*). ### Observe changes to multiple properties To observe changes to a set of properties, use the observers array. Observers do not receive old values as arguments, only new values. Only single-property observers defined in the properties object receive both old and new values. Example import { PolymerElement } from '@polymer/polymer/polymer-element.js'; class XCustom extends PolymerElement { static get properties() { return { preload: Boolean, src: String, size: String } } // Each item of observers array is a method name followed by // a comma-separated list of one or more dependencies. static get observers() { return [ 'updateImage(preload, src, size)' ] } // Each method referenced in observers must be defined in // element prototype. The arguments to the method are new value // of each dependency, and may be undefined. updateImage(preload, src, size) { // ... do work using dependent values } } customElements.define('x-custom', XCustom); ## Observe sub-property changes - Define an observers array. - Add an item to the observers array. The item must be a method name followed by a comma-separated list of one or more paths. For example, **onNameChange(dog.name) for one path, or onNameChange(dog.name, cat.name) for multiple paths**. **Each path is a sub-property** that you want to observe. - Define the method in your element prototype. When the method is called, the argument to the method is the new value of the sub-property. Example import { PolymerElement, html } from '@polymer/polymer/polymer-element.js'; class XCustom extends PolymerElement { static get template () { return html` `; } static get properties() { return { user: { type: Object, value: function() { return {}; } } } } // Observe the name sub-property on the user object static get observers() { return [ 'userNameChanged(user.name)' ] } // For a property or sub-property dependency, the corresponding // argument is the new value of the property or sub-property userNameChanged: function(name) { if (name) { console.log('new name: ' + name); } else { console.log('user name is undefined'); } } } customElements.define('x-custom', XCustom); ## Observe array mutations static get observers() { return [ 'usersAddedOrRemoved(users.splices)' ] } - indexSplices. The set of changes that occurred to the array, in terms of array indexes. Each indexSplices record contains the following properties: - index. Position where the splice started. - removed. Array of removed items. - addedCount. Number of new items inserted at index. - object: A reference to the array in question. - type: The string literal 'splice'. Example import { PolymerElement } from '@polymer/polymer/polymer-element.js'; class XCustom extends PolymerElement { static get properties() { return { users: { type: Array, value: function() { return []; } } } } // Observe changes to the users array static get observers() { return [ 'usersAddedOrRemoved(users.splices)' ]; } // For an array mutation dependency, the corresponding argument is a change record usersAddedOrRemoved(changeRecord) { if (changeRecord) { changeRecord.indexSplices.forEach(function(s) { s.removed.forEach(function(user) { console.log(user.name + ' was removed'); }); for (var i=0; i `; } static get properties() { return { user: { type: Object, value: function() { return {'name':{}}; } } } } static get observers() { return [ 'userNameChanged(user.name.*)' ] } userNameChanged(changeRecord) { console.log('path: ' + changeRecord.path); console.log('value: ' + changeRecord.value); } } customElements.define('x-custom', XCustom); static get properties() { return { firstName: { type: String }, lastName: { type: String } } } static get observers() { return [ 'nameChanged(firstName, lastName)' ] } nameChanged: function(firstName, lastName) { console.log('new name:', firstName, lastName); } ## Computed properties Note: The definition of a computing function looks like **the definition of a multi-property observer**, and the two act almost identically. The only difference is that the computed property function returns a value that's exposed as a virtual property. import { PolymerElement, html } from '@polymer/polymer/polymer-element.js'; class XCustom extends PolymerElement { static get template() { return html`

My name is {{fullName}}

`; } static get properties() { return { first: String, last: String, fullName: { type: String, // when `first` or `last` changes `computeFullName` is called once // and the value it returns is stored as `fullName` computed: 'computeFullName(first, last)' } } } computeFullName(first, last) { return first + ' ' + last; } } customElements.define('x-custom', XCustom); ## Dynamic observer methods For example, import { PolymerElement } from '@polymer/polymer/polymer-element.js'; class NameCard extends PolymerElement { static get properties() { return { // Override default format by assigning a new formatter // function formatter: { type: Function }, formattedName: { computed: 'formatter(name.title, name.first, name.last)' }, name: { type: Object, value() { return { title: "", first: "", last: "" }; } } } } constructor() { super(); this.formatter = this.defaultFormatter; } defaultFormatter(title, first, last) { return `${title} ${first} ${last}` } } customElements.define('name-card', NameCard); Setting a new value for formatter causes the formattedName property to update, even if the name property doesn't change: nameCard.name = { title: 'Admiral', first: 'Grace', last: 'Hopper'} console.log(nameCard.formattedName); // Admiral Grace Hopper nameCard.formatter = function(title, first, last) { return `${last}, ${first}` } console.log(nameCard.formattedName); // Hopper, Grace ### Add a simple observer dynamically You can create a simple observer dynamically using the _createPropertyObserver instance method. For example: this._observedPropertyChanged = (newVal) => { console.log('observedProperty changed to ' + newVal); }; this._createPropertyObserver('observedProperty', '_observedPropertyChanged', true); ### Add a complex observer dynamically You can create a computed property dynamically using the _createMethodObserver instance method. For example: this._createMethodObserver('_observeSeveralProperties(prop1,prop2,prop3)', true); ### Add a computed property dynamically You can create a computed property dynamically using the _createComputedProperty instance method. For example: this._createComputedProperty('newProperty', '_computeNewProperty(prop1,prop2)', true); # Data Bindings A data binding connects data from a custom element (the host element) to a property or attribute of an element in its local DOM (the child or target element). static get template() { return html` `; } ### Bind to a target property This example binds the target element's name property to the host element's myName property. ### Bind to text content To bind to a target element's textContent, you can simply include the annotation or compound binding inside the target element. import { PolymerElement, html } from '@polymer/polymer/polymer-element.js'; class UserView extends PolymerElement { static get properties() { return { name: String }; } static get template() { return html`
[[name]]
`; } } customElements.define('user-view', UserView); ### Bind to a target attribute To bind to an attribute, add a dollar sign ($) after the attribute name:
Attribute binding results in a call to: element.setAttribute(attr,value); As opposed to: element.property = value; For example: static get template() { return html` `; } ### Bind to a host sub-property main-view.js import { PolymerElement, html } from '@polymer/polymer/polymer-element.js'; import './user-view.js'; class MainView extends PolymerElement { static get properties() { return { user: { type: Object, value: function () { return { given: "San", family: "Zhang" }; } } }; } static get template() { return html` `; } } customElements.define('main-view', MainView); user-view.js import { PolymerElement, html } from '@polymer/polymer/polymer-element.js'; class UserView extends PolymerElement { static get properties() { return { given: String, family: String }; } static get template() { return html`
[[given]] [[family]]
`; } } customElements.define('user-view', UserView); index.html ## Logical not operator static get template() { return html` `; } ### Computed bindings
[[_formatName(first, last, title)]]
A computed binding is useful if you don't need to expose a computed property as part of the element's API, or use it elsewhere in the element. Computed bindings are also useful for filtering or transforming values for display. Example class XCustom extends PolymerElement { static get properties() { return { given: String, family: String }; } _formatName(given, family) { return `${family}, ${given}`; } static get template() { return html` My name is [[_formatName(given, family)]] `; } } customElements.define('x-custom', XCustom); ### Literal arguments to computed bindings Example: static get template() { return html` {{translate('Hello\!\, nice to meet you', given, family)}} `; } ### Compound bindings You can combine string literals and bindings in a single property binding or text content binding. For example: Name: [[lastname]], [[firstname]] ## Binding to arrays and array items {{array[0]}} {{array.0}} ### Bind to an array item You can use a computed binding to bind to a specific array item, or to a subproperty of an array item, like array[index].name. class XCustom extends PolymerElement { static get properties() { return { myArray: { type: Array, value: [{ name: 'Bob' }, { name: 'Wing Sang' }] } }; } // first argument is the change record for the array change, // change.base is the array specified in the binding arrayItem(change, index, path) { // this.get(path, root) returns a value for a path // relative to a root object. return this.get(path, change.base[index]); } ready() { super.ready(); // mutate the array this.unshift('myArray', { name: 'Fatma' }); // change a subproperty this.set('myArray.1.name', 'Rupert'); } static get template() { return html`
[[arrayItem(myArray.*, 0, 'name')]]
[[arrayItem(myArray.*, 1, 'name')]]
`; } } ## Two-way binding to a non-Polymer element Example