blob: 0e29bfc67b4174673e1e88cf9d89accc8c634f66 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.define('cr.ui', function() {
/**
* StoreClient is a Polymer behavior which ties front-end elements to
* back-end data from an associated Store. An element using this behavior
* can use the watch method to tie one of its properties to a specific piece
* of data from the Store.
*
* This StoreClient is generic, and needs to be combined with a
* page-specific implementation, as in
* chrome/browser/resources/md_bookmarks/store_client.js.
* This implementation should override the watch, getState and getStore
* methods.
*
* These methods need to be overridden to allow type-checking on them,
* since they all use the page state type associated with the specific page,
* and templates cannot be applied to Polymer behaviors. Polymer 2 Mixins
* may solve these type-checking problems.
*
* @polymerBehavior
*/
const StoreClient = {
created: function() {
/**
* @type {!Array<{
* localProperty: string,
* valueGetter: function(!Object)
* }>}
*/
this.watches_ = [];
},
attached: function() {
this.getStore().addObserver(this);
},
detached: function() {
this.getStore().removeObserver(this);
},
/**
* Watches a particular part of the state tree, updating |localProperty|
* to the return value of |valueGetter| whenever the state changes. Eg, to
* keep |this.item| updated with the value of a node:
* watch('item', (state) => state.nodes[this.itemId]);
*
* Note that object identity is used to determine if the value has changed
* before updating the UI, rather than Polymer-style deep equality. If the
* getter function returns |undefined|, no changes will propagate to the UI.
*
* @param {string} localProperty
* @param {function(!Object)} valueGetter
*/
watch_: function(localProperty, valueGetter) {
this.watches_.push({
localProperty: localProperty,
valueGetter: valueGetter,
});
},
/**
* Helper to dispatch an action to the store, which will update the store
* data and then (possibly) flow through to the UI.
* @param {?cr.ui.Action} action
*/
dispatch: function(action) {
this.getStore().dispatch(action);
},
/**
* Helper to dispatch a DeferredAction to the store, which will
* asynchronously perform updates to the store data and UI.
* @param {cr.ui.DeferredAction} action
*/
dispatchAsync: function(action) {
this.getStore().dispatchAsync(action);
},
/** @param {string} newState */
onStateChanged: function(newState) {
this.watches_.forEach((watch) => {
const oldValue = this[watch.localProperty];
const newValue = watch.valueGetter(newState);
// Avoid poking Polymer unless something has actually changed. Reducers
// must return new objects rather than mutating existing objects, so
// any real changes will pass through correctly.
if (oldValue === newValue || newValue === undefined)
return;
this[watch.localProperty] = newValue;
});
},
updateFromStore: function() {
if (this.getStore().isInitialized())
this.onStateChanged(this.getStore().data);
},
/**
* Should be overridden by a function which calls the private watch_
* with the given arguments. Needs to be overridden to allow
* type-checking on the valueGetter parameter.
*/
watch: function(localProperty, valueGetter) {
assertNotReached();
},
/**
* Should be overridden by a function which returns the data from the
* associated Store. Needs to be overridden to allow type-checking on the
* return value, which will be a page state type specific to each page.
*/
getState: function() {
assertNotReached();
},
/**
* Should be overridden by a function which returns the specific Store
* instance associated with the StoreClient. Needs to be overridden to
* allow type-checking on the return value, which will be a
* page-specific subclass of Store.
*/
getStore: function() {
assertNotReached();
},
};
return {
StoreClient: StoreClient,
};
});