Files
reference/DataSystem.md
2026-08-04 15:32:57 +06:30

24 KiB
Executable File

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 `
      <div>[[name.first]] [[name.last]]</div>
    `;
  }
}
customElements.define('name-card', NameCard);
  1. The <name-card> element has a name property that refers to a JavaScript object.

  2. The <name-card> 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 <name-card> 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.

<local-dom-child name="{{hostProperty.subProperty}}"></local-dom-child>

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 ({{ }}):

<my-input value="{{name}}"></my-input>

2 - One-way,

which only allows downwards data flow. Upward data flow is disabled. One-way bindings use double square brackets ( ).

<name-tag name="[[name]]"></name-tag>

Example of one-way binding (downward)

class XHost extends PolymerElement {
    static get template() {
      return html`
        <x-target some-prop="[[value]]"></x-target>
      `;
    }
  }

....................

...
  class XTarget extends PolymerElement {
    static get properties() {
      return {
        someProp: {
          type: String //no notify: true
        }
      }
    }
  }
  ...

  ...
  class XHost extends PolymerElement {
    static get template() {
      return html`
        <!-- changes to "value" propagate downward to "someProp" on target -->
        <!-- changes to "someProp" are not notified to host due to notify:falsey -->
        <x-target some-prop="{{value}}"></x-target>
      `;
    }
  }
  ...

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`
        <!-- changes to "value" are ignored by child because "someProp" is read-only -->
        <!-- changes to "someProp" propagate upward to "value" on host -->
        <x-target some-prop="{{value}}"></x-target>
      `;
    }
  }
  ...
  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`
         <!-- changes to "value" are ignored by child because "someProp" is read-only -->
         <!-- changes to "someProp" don't propagate upward because of the one-way binding -->
         <x-target some-prop="[[value]]"></x-target>
       `;
     }
   }
   ...
   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`
      <template is="dom-repeat" items="[[users]]"><p>{{item}}</p></template>
      <p><button on-click="addUser">add</button><button on-click="removeUser">remove</button><button on-click="_reset">reset</button></p>
    `;
  }
  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);
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`
      <!-- Sub-property is updated via property binding. -->
      <input value="{{user.name::input}}">
    `;
  }
  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<s.addedCount; i++) {
          var index = s.index + i;
          var newUser = s.object[index];
          console.log('User ' + newUser.name + ' added at index ' + index);
        }
      }, this);
    }
  }
  ready() {
    super.ready();
    this.push('users', {name: "Jack Aubrey"});
  }
}
customElements.define('x-custom', XCustom);

To call an observer when any (deep) sub-property of an object or array changes, specify a path with a wildcard (*).

When you specify a path with a wildcard, the argument passed to your observer is a change record object with the following properties:

  • path. Path to the property that changed. Use this to determine whether a property changed, a sub-property changed, or an array was mutated.
  • value. New value of the path that changed.
  • base. The object matching the non-wildcard portion of the path.

Example

import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';

class XCustom extends PolymerElement {
  static get template() {
    return html`
      <input value="{{user.name.first::input}}" placeholder="First Name">
      <input value="{{user.name.last::input}}" placeholder="Last Name">
    `;
  }
  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`
      <p>My name is <span>{{fullName}}</span></p>
    `;
  }
  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`
    <target-element target-property="{{hostProperty}}"></target-element>
  `;
}

Bind to a target property

<target-element name="{{myName}}"></target-element>

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`
      <div>[[name]]</div>
    `;
  }
}
customElements.define('user-view', UserView);
<!-- usage -->
<user-view name="Samuel"></user-view>

Bind to a target attribute

To bind to an attribute, add a dollar sign ($) after the attribute name:

<div style$="color: {{myColor}};">

Attribute binding results in a call to:

element.setAttribute(attr,value);

As opposed to:

element.property = value;

For example:

static get template() {
  return html`
    <!-- Attribute binding -->
    <my-element selected$="[[value]]"></my-element>
    <!-- results in <my-element>.setAttribute('selected', this.value); -->

    <!-- Property binding -->
    <my-element selected="{{value}}"></my-element>
    <!-- results in <my-element>.selected = this.value; -->
  `;
}

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`
      <user-view given="{{user.given}}" family="{{user.family}}"></user-view>
    `;
  }
}
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`
      <div>[[given]] [[family]]</div>
    `;
  }
}
customElements.define('user-view', UserView);

index.html

<main-view></main-view>

Logical not operator

static get template() {
  return html`
    <my-page show-login="[[!isLoggedIn]]"></my-page>
  `;
}

Computed bindings

<div>[[_formatName(first, last, title)]]</div>

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 <span>[[_formatName(given, family)]]</span>
    `;
  }
}
customElements.define('x-custom', XCustom);

Literal arguments to computed bindings

Example:

static get template() {
  return html`
    <span>{{translate('Hello\!\, nice to meet you', given, family)}}</span>
  `;
}

Compound bindings

You can combine string literals and bindings in a single property binding or text content binding. For example:

<img src$="https://www.example.com/profiles/[[userId]].jpg">

<span>Name: [[lastname]], [[firstname]]</span>

Binding to arrays and array items

<!-- Don't do this! This format doesn't work -->
<span>{{array[0]}}</span>
<!-- Don't do this! Data may display, but won't be updated correctly -->
 <span>{{array.0}}</span>

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`
        <div>[[arrayItem(myArray.*, 0, 'name')]]</div>
        <div>[[arrayItem(myArray.*, 1, 'name')]]</div>
      `;
    }
  }

Two-way binding to a non-Polymer element

Example

<!-- Listens for `input` event and sets hostValue to <input>.value -->
<input value="{{hostValue::input}}">

<!-- Listens for `change` event and sets hostChecked to <input>.checked -->
<input type="checkbox" checked="{{hostChecked::change}}">

<!-- Listens for `timeupdate ` event and sets hostTime to <video>.currentTime -->
<video url="..." current-time="{{hostTime::timeupdate}}">