blob: 503e86d7eecb3bb1bd12410956080cef73aca262 [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() {
/** @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,
};
});