Commit 9bbb0ffe authored by Reka Norman's avatar Reka Norman Committed by Commit Bot

[App Management] Make bookmarks store generic

This CL moves the data store and store client from bookmarks into cr.ui
and makes them generic so that they can be used by other pages.

Bug: 906508
Change-Id: I578a3e0b308b1d21ad412cf8be7e08db3b402469
Reviewed-on: https://chromium-review.googlesource.com/c/1369444
Commit-Queue: Reka Norman <rekanorman@google.com>
Reviewed-by: default avatarEric Willigers <ericwilligers@chromium.org>
Reviewed-by: default avatarcalamity <calamity@chromium.org>
Cr-Commit-Position: refs/heads/master@{#616101}
parent 47adbb5d
<link rel="import" href="chrome://resources/html/polymer.html"> <link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/cr_elements/cr_toolbar/cr_toolbar.html"> <link rel="import" href="chrome://resources/cr_elements/cr_toolbar/cr_toolbar.html">
<link rel="import" href="chrome://resources/cr_elements/shared_vars_css.html"> <link rel="import" href="chrome://resources/cr_elements/shared_vars_css.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-collapse/iron-collapse.html"> <link rel="import" href="chrome://resources/polymer/v1_0/iron-collapse/iron-collapse.html">
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
# Use of this source code is governed by a BSD-style license that can be # Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file. # found in the LICENSE file.
import("../optimize_webui.gni")
import("//third_party/closure_compiler/compile_js.gni") import("//third_party/closure_compiler/compile_js.gni")
import("../optimize_webui.gni")
optimize_webui("build") { optimize_webui("build") {
host = "bookmarks" host = "bookmarks"
...@@ -48,6 +48,7 @@ js_library("actions") { ...@@ -48,6 +48,7 @@ js_library("actions") {
":types", ":types",
":util", ":util",
"//ui/webui/resources/js:cr", "//ui/webui/resources/js:cr",
"//ui/webui/resources/js/cr/ui:store",
] ]
externs_list = [ "$externs_path/chrome_extensions.js" ] externs_list = [ "$externs_path/chrome_extensions.js" ]
} }
...@@ -59,6 +60,7 @@ js_library("api_listener") { ...@@ -59,6 +60,7 @@ js_library("api_listener") {
":store", ":store",
":util", ":util",
"//ui/webui/resources/js:cr", "//ui/webui/resources/js:cr",
"//ui/webui/resources/js/cr/ui:store",
] ]
externs_list = [ "$externs_path/chrome_extensions.js" ] externs_list = [ "$externs_path/chrome_extensions.js" ]
} }
...@@ -196,6 +198,7 @@ js_library("store") { ...@@ -196,6 +198,7 @@ js_library("store") {
":reducers", ":reducers",
":types", ":types",
"//ui/webui/resources/js:cr", "//ui/webui/resources/js:cr",
"//ui/webui/resources/js/cr/ui:store",
] ]
externs_list = [ "$externs_path/chrome_extensions.js" ] externs_list = [ "$externs_path/chrome_extensions.js" ]
} }
...@@ -205,6 +208,7 @@ js_library("store_client") { ...@@ -205,6 +208,7 @@ js_library("store_client") {
":store", ":store",
":types", ":types",
"//ui/webui/resources/js:cr", "//ui/webui/resources/js:cr",
"//ui/webui/resources/js/cr/ui:store_client",
] ]
} }
......
...@@ -25,7 +25,7 @@ cr.define('bookmarks.actions', function() { ...@@ -25,7 +25,7 @@ cr.define('bookmarks.actions', function() {
/** /**
* @param {string} id * @param {string} id
* @param {{title: string, url: (string|undefined)}} changeInfo * @param {{title: string, url: (string|undefined)}} changeInfo
* @return {!Action} * @return {!cr.ui.Action}
*/ */
function editBookmark(id, changeInfo) { function editBookmark(id, changeInfo) {
return { return {
...@@ -41,7 +41,7 @@ cr.define('bookmarks.actions', function() { ...@@ -41,7 +41,7 @@ cr.define('bookmarks.actions', function() {
* @param {number} index * @param {number} index
* @param {string} oldParentId * @param {string} oldParentId
* @param {number} oldIndex * @param {number} oldIndex
* @return {!Action} * @return {!cr.ui.Action}
*/ */
function moveBookmark(id, parentId, index, oldParentId, oldIndex) { function moveBookmark(id, parentId, index, oldParentId, oldIndex) {
return { return {
...@@ -71,7 +71,7 @@ cr.define('bookmarks.actions', function() { ...@@ -71,7 +71,7 @@ cr.define('bookmarks.actions', function() {
* @param {string} parentId * @param {string} parentId
* @param {number} index * @param {number} index
* @param {NodeMap} nodes * @param {NodeMap} nodes
* @return {!Action} * @return {!cr.ui.Action}
*/ */
function removeBookmark(id, parentId, index, nodes) { function removeBookmark(id, parentId, index, nodes) {
const descendants = bookmarks.util.getDescendants(nodes, id); const descendants = bookmarks.util.getDescendants(nodes, id);
...@@ -86,7 +86,7 @@ cr.define('bookmarks.actions', function() { ...@@ -86,7 +86,7 @@ cr.define('bookmarks.actions', function() {
/** /**
* @param {NodeMap} nodeMap * @param {NodeMap} nodeMap
* @return {!Action} * @return {!cr.ui.Action}
*/ */
function refreshNodes(nodeMap) { function refreshNodes(nodeMap) {
return { return {
...@@ -98,7 +98,7 @@ cr.define('bookmarks.actions', function() { ...@@ -98,7 +98,7 @@ cr.define('bookmarks.actions', function() {
/** /**
* @param {string} id * @param {string} id
* @param {NodeMap} nodes Current node state. Can be ommitted in tests. * @param {NodeMap} nodes Current node state. Can be ommitted in tests.
* @return {?Action} * @return {?cr.ui.Action}
*/ */
function selectFolder(id, nodes) { function selectFolder(id, nodes) {
if (nodes && (id == ROOT_NODE_ID || !nodes[id] || nodes[id].url)) { if (nodes && (id == ROOT_NODE_ID || !nodes[id] || nodes[id].url)) {
...@@ -115,7 +115,7 @@ cr.define('bookmarks.actions', function() { ...@@ -115,7 +115,7 @@ cr.define('bookmarks.actions', function() {
/** /**
* @param {string} id * @param {string} id
* @param {boolean} open * @param {boolean} open
* @return {!Action} * @return {!cr.ui.Action}
*/ */
function changeFolderOpen(id, open) { function changeFolderOpen(id, open) {
return { return {
...@@ -125,14 +125,14 @@ cr.define('bookmarks.actions', function() { ...@@ -125,14 +125,14 @@ cr.define('bookmarks.actions', function() {
}; };
} }
/** @return {!Action} */ /** @return {!cr.ui.Action} */
function clearSearch() { function clearSearch() {
return { return {
name: 'clear-search', name: 'clear-search',
}; };
} }
/** @return {!Action} */ /** @return {!cr.ui.Action} */
function deselectItems() { function deselectItems() {
return { return {
name: 'deselect-items', name: 'deselect-items',
...@@ -150,7 +150,7 @@ cr.define('bookmarks.actions', function() { ...@@ -150,7 +150,7 @@ cr.define('bookmarks.actions', function() {
* - range: If true, selects all items from the anchor to this item * - range: If true, selects all items from the anchor to this item
* - toggle: If true, toggles the selection state of the item. Cannot be * - toggle: If true, toggles the selection state of the item. Cannot be
* used with clear or range. * used with clear or range.
* @return {!Action} * @return {!cr.ui.Action}
*/ */
function selectItem(id, state, config) { function selectItem(id, state, config) {
assert(!config.toggle || !config.range); assert(!config.toggle || !config.range);
...@@ -194,7 +194,7 @@ cr.define('bookmarks.actions', function() { ...@@ -194,7 +194,7 @@ cr.define('bookmarks.actions', function() {
* @param {Array<string>} ids * @param {Array<string>} ids
* @param {BookmarksPageState} state * @param {BookmarksPageState} state
* @param {string=} anchor * @param {string=} anchor
* @return {!Action} * @return {!cr.ui.Action}
*/ */
function selectAll(ids, state, anchor) { function selectAll(ids, state, anchor) {
return { return {
...@@ -208,7 +208,7 @@ cr.define('bookmarks.actions', function() { ...@@ -208,7 +208,7 @@ cr.define('bookmarks.actions', function() {
/** /**
* @param {string} id * @param {string} id
* @return {!Action} * @return {!cr.ui.Action}
*/ */
function updateAnchor(id) { function updateAnchor(id) {
return { return {
...@@ -219,7 +219,7 @@ cr.define('bookmarks.actions', function() { ...@@ -219,7 +219,7 @@ cr.define('bookmarks.actions', function() {
/** /**
* @param {string} term * @param {string} term
* @return {!Action} * @return {!cr.ui.Action}
*/ */
function setSearchTerm(term) { function setSearchTerm(term) {
if (!term) if (!term)
...@@ -233,7 +233,7 @@ cr.define('bookmarks.actions', function() { ...@@ -233,7 +233,7 @@ cr.define('bookmarks.actions', function() {
/** /**
* @param {!Array<string>} ids * @param {!Array<string>} ids
* @return {!Action} * @return {!cr.ui.Action}
*/ */
function setSearchResults(ids) { function setSearchResults(ids) {
return { return {
...@@ -244,7 +244,7 @@ cr.define('bookmarks.actions', function() { ...@@ -244,7 +244,7 @@ cr.define('bookmarks.actions', function() {
/** /**
* @param {IncognitoAvailability} availability * @param {IncognitoAvailability} availability
* @return {!Action} * @return {!cr.ui.Action}
*/ */
function setIncognitoAvailability(availability) { function setIncognitoAvailability(availability) {
assert(availability != IncognitoAvailability.FORCED); assert(availability != IncognitoAvailability.FORCED);
...@@ -256,7 +256,7 @@ cr.define('bookmarks.actions', function() { ...@@ -256,7 +256,7 @@ cr.define('bookmarks.actions', function() {
/** /**
* @param {boolean} canEdit * @param {boolean} canEdit
* @return {!Action} * @return {!cr.ui.Action}
*/ */
function setCanEditBookmarks(canEdit) { function setCanEditBookmarks(canEdit) {
return { return {
......
...@@ -65,7 +65,7 @@ cr.define('bookmarks.ApiListener', function() { ...@@ -65,7 +65,7 @@ cr.define('bookmarks.ApiListener', function() {
debouncer.promise.then(highlightUpdatedItemsImpl); debouncer.promise.then(highlightUpdatedItemsImpl);
} }
/** @param {Action} action */ /** @param {cr.ui.Action} action */
function dispatch(action) { function dispatch(action) {
bookmarks.Store.getInstance().dispatch(action); bookmarks.Store.getInstance().dispatch(action);
} }
......
<link rel="import" href="chrome://resources/html/cr.html"> <link rel="import" href="chrome://resources/html/cr.html">
<link rel="import" href="chrome://resources/html/cr/ui/store.html">
<link rel="import" href="chrome://bookmarks/reducers.html"> <link rel="import" href="chrome://bookmarks/reducers.html">
<link rel="import" href="chrome://bookmarks/util.html"> <link rel="import" href="chrome://bookmarks/util.html">
<script src="chrome://bookmarks/store.js"></script> <script src="chrome://bookmarks/store.js"></script>
...@@ -9,133 +9,10 @@ ...@@ -9,133 +9,10 @@
*/ */
cr.define('bookmarks', function() { cr.define('bookmarks', function() {
class Store { class Store extends cr.ui.Store {
/** @extends {cr.ui.Store<BookmarksPageState>} */
constructor() { constructor() {
/** @type {!BookmarksPageState} */ super(bookmarks.util.createEmptyState(), bookmarks.reduceAction);
this.data_ = bookmarks.util.createEmptyState();
/** @type {boolean} */
this.initialized_ = false;
/** @type {!Array<DeferredAction>} */
this.queuedActions_ = [];
/** @type {!Array<!StoreObserver>} */
this.observers_ = [];
/** @private {boolean} */
this.batchMode_ = false;
}
/**
* @param {!BookmarksPageState} initialState
*/
init(initialState) {
this.data_ = initialState;
this.queuedActions_.forEach((action) => {
this.dispatchInternal_(action);
});
this.initialized_ = true;
this.notifyObservers_(this.data_);
}
/** @return {!BookmarksPageState} */
get data() {
return this.data_;
}
/** @return {boolean} */
isInitialized() {
return this.initialized_;
}
/** @param {!StoreObserver} observer */
addObserver(observer) {
this.observers_.push(observer);
}
/** @param {!StoreObserver} observer */
removeObserver(observer) {
const index = this.observers_.indexOf(observer);
this.observers_.splice(index, 1);
}
/**
* Begin a batch update to store data, which will disable updates to the
* UI until `endBatchUpdate` is called. This is useful when a single UI
* operation is likely to cause many sequential model updates (eg, deleting
* 100 bookmarks).
*/
beginBatchUpdate() {
this.batchMode_ = true;
}
/**
* End a batch update to the store data, notifying the UI of any changes
* which occurred while batch mode was enabled.
*/
endBatchUpdate() {
this.batchMode_ = false;
this.notifyObservers_(this.data);
}
/**
* Handles a 'deferred' action, which can asynchronously dispatch actions
* to the Store in order to reach a new UI state. DeferredActions have the
* form `dispatchAsync(function(dispatch) { ... })`). Inside that function,
* the |dispatch| callback can be called asynchronously to dispatch Actions
* directly to the Store.
* @param {DeferredAction} action
*/
dispatchAsync(action) {
if (!this.initialized_) {
this.queuedActions_.push(action);
return;
}
this.dispatchInternal_(action);
}
/**
* Transition to a new UI state based on the supplied |action|, and notify
* observers of the change. If the Store has not yet been initialized, the
* action will be queued and performed upon initialization.
* @param {?Action} action
*/
dispatch(action) {
this.dispatchAsync(function(dispatch) {
dispatch(action);
});
}
/**
* @param {DeferredAction} action
*/
dispatchInternal_(action) {
action(this.reduce_.bind(this));
}
/**
* @param {?Action} action
* @private
*/
reduce_(action) {
if (!action)
return;
this.data_ = bookmarks.reduceAction(this.data_, action);
// Batch notifications until after all initialization queuedActions are
// resolved.
if (this.isInitialized() && !this.batchMode_)
this.notifyObservers_(this.data_);
}
/**
* @param {!BookmarksPageState} state
* @private
*/
notifyObservers_(state) {
this.observers_.forEach(function(o) {
o.onStateChanged(state);
});
} }
} }
......
<link rel="import" href="chrome://resources/html/cr.html"> <link rel="import" href="chrome://resources/html/cr.html">
<link rel="import" href="chrome://resources/html/cr/ui/store_client.html">
<link rel="import" href="chrome://bookmarks/store.html"> <link rel="import" href="chrome://bookmarks/store.html">
<script src="chrome://bookmarks/store_client.js"></script> <script src="chrome://bookmarks/store_client.js"></script>
...@@ -10,97 +10,39 @@ ...@@ -10,97 +10,39 @@
cr.define('bookmarks', function() { cr.define('bookmarks', function() {
/** /**
* @polymerBehavior * @polymerBehavior
* @implements {StoreObserver}
*/ */
const StoreClient = { const BookmarksStoreClientImpl = {
created: function() {
/**
* @type {!Array<{
* localProperty: string,
* valueGetter: function(!BookmarksPageState)
* }>}
*/
this.watches_ = [];
},
attached: function() {
bookmarks.Store.getInstance().addObserver(this);
},
detached: function() {
bookmarks.Store.getInstance().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.
*
* Typechecking is supressed because this conflicts with
* Object.prototype.watch, which is a Gecko-only method that is recognized
* by Closure.
* @suppress {checkTypes}
* @param {string} localProperty * @param {string} localProperty
* @param {function(!BookmarksPageState)} valueGetter * @param {function(!BookmarksPageState)} valueGetter
*/ */
watch: function(localProperty, valueGetter) { watch: function(localProperty, valueGetter) {
this.watches_.push({ this.watch_(localProperty, valueGetter);
localProperty: localProperty,
valueGetter: valueGetter,
});
}, },
/** /**
* Helper to dispatch an action to the store, which will update the store * @return {BookmarksPageState}
* data and then (possibly) flow through to the UI.
* @param {?Action} action
*/ */
dispatch: function(action) { getState: function() {
bookmarks.Store.getInstance().dispatch(action); return this.getStore().data;
}, },
/** /**
* Helper to dispatch a DeferredAction to the store, which will * @return {bookmarks.Store}
* asynchronously perform updates to the store data and UI.
* @param {DeferredAction} action
*/ */
dispatchAsync: function(action) { getStore: function() {
bookmarks.Store.getInstance().dispatchAsync(action); return bookmarks.Store.getInstance();
},
/** @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 (bookmarks.Store.getInstance().isInitialized())
this.onStateChanged(bookmarks.Store.getInstance().data);
},
/** @return {!BookmarksPageState} */
getState: function() {
return bookmarks.Store.getInstance().data;
}, },
}; };
/**
* @polymerBehavior
* @implements {cr.ui.StoreObserver<BookmarksPageState>}
*/
const StoreClient = [cr.ui.StoreClient, BookmarksStoreClientImpl];
return { return {
BookmarksStoreClientImpl: BookmarksStoreClientImpl,
StoreClient: StoreClient, StoreClient: StoreClient,
}; };
}); });
...@@ -78,12 +78,6 @@ let PreferencesState; ...@@ -78,12 +78,6 @@ let PreferencesState;
*/ */
let BookmarksPageState; let BookmarksPageState;
/** @typedef {{name: string}} */
let Action;
/** @typedef {function(function(?Action))} */
let DeferredAction;
/** @typedef {{element: BookmarkElement, position: DropPosition}} */ /** @typedef {{element: BookmarkElement, position: DropPosition}} */
let DropDestination; let DropDestination;
...@@ -106,9 +100,3 @@ function DragData() { ...@@ -106,9 +100,3 @@ function DragData() {
/** @type {boolean} */ /** @type {boolean} */
this.sameProfile = false; this.sameProfile = false;
} }
/** @interface */
function StoreObserver() {}
/** @param {!BookmarksPageState} newState */
StoreObserver.prototype.onStateChanged = function(newState) {};
...@@ -7,7 +7,7 @@ suiteSetup(function() { ...@@ -7,7 +7,7 @@ suiteSetup(function() {
class TestStore extends bookmarks.Store { class TestStore extends bookmarks.Store {
constructor(data) { constructor(data) {
super(); super();
this.data_ = Object.assign(bookmarks.util.createEmptyState(), data); this.data = Object.assign(bookmarks.util.createEmptyState(), data);
this.initialized_ = true; this.initialized_ = true;
this.lastAction_ = null; this.lastAction_ = null;
...@@ -34,14 +34,6 @@ suiteSetup(function() { ...@@ -34,14 +34,6 @@ suiteSetup(function() {
this.lastAction_ = null; this.lastAction_ = null;
} }
get data() {
return this.data_;
}
set data(newData) {
this.data_ = newData;
}
/** Replace the global store instance with this TestStore. */ /** Replace the global store instance with this TestStore. */
replaceSingleton() { replaceSingleton() {
bookmarks.Store.instance_ = this; bookmarks.Store.instance_ = this;
......
<script src="../../../js/cr/ui/store.js"></script>
<script src="../../../js/cr/ui/store_client.js"></script>
...@@ -32,6 +32,8 @@ js_type_check("closure_compile") { ...@@ -32,6 +32,8 @@ js_type_check("closure_compile") {
":overlay", ":overlay",
":position_util", ":position_util",
":splitter", ":splitter",
":store",
":store_client",
":table", ":table",
":tree", ":tree",
] ]
...@@ -230,6 +232,19 @@ js_library("splitter") { ...@@ -230,6 +232,19 @@ js_library("splitter") {
] ]
} }
js_library("store") {
deps = [
"../..:cr",
]
}
js_library("store_client") {
deps = [
":store",
"../..:cr",
]
}
js_library("table") { js_library("table") {
deps = [ deps = [
":list", ":list",
......
// 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() {
/** @typedef {{name: string}} */
let Action;
/** @typedef {function(function(?cr.ui.Action))} */
let DeferredAction;
/**
* @interface
* @template T
*/
class StoreObserver {
/** @param {!T} newState */
onStateChanged(newState) {}
}
/**
* A generic datastore for the state of a page, where the state is publicly
* readable but can only be modified by dispatching an Action.
* The Store should be extended by specifying T, the page state type
* associated with the store.
* @template T
*/
class Store {
/**
* @param {T} emptyState
* @param {function(T, cr.ui.Action):T} reducer
*/
constructor(emptyState, reducer) {
/** @type {!T} */
this.data = emptyState;
/** @type {function(T, cr.ui.Action):T} */
this.reducer_ = reducer;
/** @type {boolean} */
this.initialized_ = false;
/** @type {!Array<cr.ui.DeferredAction>} */
this.queuedActions_ = [];
/** @type {!Array<!cr.ui.StoreObserver>} */
this.observers_ = [];
/** @private {boolean} */
this.batchMode_ = false;
}
/**
* @param {!T} initialState
*/
init(initialState) {
this.data = initialState;
this.queuedActions_.forEach((action) => {
this.dispatchInternal_(action);
});
this.initialized_ = true;
this.notifyObservers_(this.data);
}
/** @return {boolean} */
isInitialized() {
return this.initialized_;
}
/** @param {!cr.ui.StoreObserver} observer */
addObserver(observer) {
this.observers_.push(observer);
}
/** @param {!cr.ui.StoreObserver} observer */
removeObserver(observer) {
const index = this.observers_.indexOf(observer);
this.observers_.splice(index, 1);
}
/**
* Begin a batch update to store data, which will disable updates to the
* UI until `endBatchUpdate` is called. This is useful when a single UI
* operation is likely to cause many sequential model updates (eg, deleting
* 100 bookmarks).
*/
beginBatchUpdate() {
this.batchMode_ = true;
}
/**
* End a batch update to the store data, notifying the UI of any changes
* which occurred while batch mode was enabled.
*/
endBatchUpdate() {
this.batchMode_ = false;
this.notifyObservers_(this.data);
}
/**
* Handles a 'deferred' action, which can asynchronously dispatch actions
* to the Store in order to reach a new UI state. DeferredActions have the
* form `dispatchAsync(function(dispatch) { ... })`). Inside that function,
* the |dispatch| callback can be called asynchronously to dispatch Actions
* directly to the Store.
* @param {cr.ui.DeferredAction} action
*/
dispatchAsync(action) {
if (!this.initialized_) {
this.queuedActions_.push(action);
return;
}
this.dispatchInternal_(action);
}
/**
* Transition to a new UI state based on the supplied |action|, and notify
* observers of the change. If the Store has not yet been initialized, the
* action will be queued and performed upon initialization.
* @param {?cr.ui.Action} action
*/
dispatch(action) {
this.dispatchAsync(function(dispatch) {
dispatch(action);
});
}
/**
* @param {cr.ui.DeferredAction} action
*/
dispatchInternal_(action) {
action(this.reduce_.bind(this));
}
/**
* @param {?cr.ui.Action} action
* @private
*/
reduce_(action) {
if (!action)
return;
this.data = this.reducer_(this.data, action);
// Batch notifications until after all initialization queuedActions are
// resolved.
if (this.isInitialized() && !this.batchMode_)
this.notifyObservers_(this.data);
}
/**
* @param {!T} state
* @private
*/
notifyObservers_(state) {
this.observers_.forEach(function(o) {
o.onStateChanged(state);
});
}
}
return {
Action: Action,
DeferredAction: DeferredAction,
Store: Store,
StoreObserver: StoreObserver,
};
});
// 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,
};
});
...@@ -347,6 +347,12 @@ without changes to the corresponding grd file. --> ...@@ -347,6 +347,12 @@ without changes to the corresponding grd file. -->
<structure name="IDR_WEBUI_HTML_CR_UI_SPLITTER" <structure name="IDR_WEBUI_HTML_CR_UI_SPLITTER"
file="html/cr/ui/splitter.html" type="chrome_html" file="html/cr/ui/splitter.html" type="chrome_html"
compress="gzip" /> compress="gzip" />
<structure name="IDR_WEBUI_HTML_CR_UI_STORE"
file="html/cr/ui/store.html"
type="chrome_html" compress="gzip" />
<structure name="IDR_WEBUI_HTML_CR_UI_STORE_CLIENT"
file="html/cr/ui/store_client.html"
type="chrome_html" compress="gzip" />
<structure name="IDR_WEBUI_HTML_DARK_MODE" <structure name="IDR_WEBUI_HTML_DARK_MODE"
file="html/dark_mode.html" type="chrome_html" compress="gzip" /> file="html/dark_mode.html" type="chrome_html" compress="gzip" />
<structure name="IDR_WEBUI_HTML_EVENT_TRACKER" <structure name="IDR_WEBUI_HTML_EVENT_TRACKER"
...@@ -474,6 +480,12 @@ without changes to the corresponding grd file. --> ...@@ -474,6 +480,12 @@ without changes to the corresponding grd file. -->
<structure name="IDR_WEBUI_JS_CR_UI_SPLITTER" <structure name="IDR_WEBUI_JS_CR_UI_SPLITTER"
file="js/cr/ui/splitter.js" type="chrome_html" file="js/cr/ui/splitter.js" type="chrome_html"
compress="gzip" /> compress="gzip" />
<structure name="IDR_WEBUI_JS_CR_UI_STORE"
file="js/cr/ui/store.js"
type="chrome_html" compress="gzip" />
<structure name="IDR_WEBUI_JS_CR_UI_STORE_CLENT"
file="js/cr/ui/store_client.js"
type="chrome_html" compress="gzip" />
<structure name="IDR_WEBUI_JS_CR_UI_GRID" <structure name="IDR_WEBUI_JS_CR_UI_GRID"
file="js/cr/ui/grid.js" type="chrome_html" compress="gzip" /> file="js/cr/ui/grid.js" type="chrome_html" compress="gzip" />
<structure name="IDR_WEBUI_JS_CR_UI_REPEATING_BUTTON" <structure name="IDR_WEBUI_JS_CR_UI_REPEATING_BUTTON"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment