// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type {Action, DeferredAction, Store} from '//resources/js/store.js';
import type {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {dedupingMixin} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
/**
* @fileoverview defines a helper function `makeStoreClientMixin` to create a
* Polymer mixin that binds Polymer elements to a specific instance of `Store`.
* The mixin provides utility functions for Polymer elements to dispatch actions
* that change state, and to react to Store state changes.
*/
/**
* A callback function that runs when the store state has been updated.
* Returning `undefined` will skip updating the local property.
* @see StoreClientInterface.watch
*/
export interface ValueGetter<S, V> {
(state: S): V|undefined;
}
export interface StoreClientInterface<S, A extends Action> {
/**
* Helper to dispatch an action to the store, which will update the store data
* and then (possibly) flow through to the UI.
*/
dispatch(action: A|null): void;
/**
* Helper to dispatch an asynchronous action to the store.
* TODO(b/296440261) remove `dispatchAsync` in favor of promises.
*/
dispatchAsync(action: DeferredAction<A>): void;
// Called when the store state has changed.
onStateChanged(state: S): void;
/**
* Call this when the element is connected and has called `watch` for its
* properties. This will populate the element with the initial
* data from the store if the store has been initialized.
*/
updateFromStore(): void;
/**
* Watches a particular part of the state tree, updating `localProperty` to
* the return value of `valueGetter` whenever the state changes.
*
* Note that object identity is used to determine if the value has changed
* before updating, rather than deep equality. If the getter function
* returns `undefined`, no changes will be propagated.
*/
watch<V>(localProperty: string, valueGetter: ValueGetter<S, V>): void;
// Get the current state from the store.
getState(): S;
// Get the store that this client is bound to.
getStore(): Store<S, A>;
}
type Constructor<T> = new (...args: any[]) => T;
/**
* Create a store client mixin for the store instance returned by
* `storeGetter()`. An app, such as Personalization App, will have one central
* store to bind to. Example:
*
* class MyStore extends Store {
* static getInstance(): MyStore {
* ....
* }
* }
*
* const MyStoreClientMixin = makeStoreClientMixin(MyStore.getInstance);
*
* const MyElement extends MyStoreClientMixin(PolymerElement) {
* ....
* }
*/
export function makeStoreClientMixin<S, A extends Action>(
storeGetter: () => Store<S, A>) {
function storeClientMixin<T extends Constructor<PolymerElement>>(
superClass: T): T&Constructor<StoreClientInterface<S, A>> {
class StoreClientMixin extends superClass implements
StoreClientInterface<S, A> {
private propertyWatches_: Map<string, ValueGetter<S, any>> = new Map();
override connectedCallback() {
super.connectedCallback();
this.getStore().addObserver(this);
}
override disconnectedCallback() {
super.disconnectedCallback();
this.getStore().removeObserver(this);
}
dispatch(action: A): void {
this.getStore().dispatch(action);
}
dispatchAsync(action: DeferredAction<A>): void {
this.getStore().dispatchAsync(action);
}
onStateChanged(state: S) {
this.propertyWatches_.forEach((valueGetter, localProperty) => {
const oldValue = this.get(localProperty);
const newValue = valueGetter(state);
// 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.set(localProperty, newValue);
});
}
updateFromStore(): void {
// TODO(b/296282541) assert that store is initialized instead of
// performing a runtime check.
if (this.getStore().isInitialized()) {
this.onStateChanged(this.getStore().data);
}
}
watch<V>(localProperty: string, valueGetter: ValueGetter<S, V>) {
if (this.propertyWatches_.has(localProperty)) {
console.warn(`Overwriting watch for property ${localProperty}`);
}
this.propertyWatches_.set(localProperty, valueGetter);
}
getState(): S {
return this.getStore().data;
}
getStore(): Store<S, A> {
return storeGetter();
}
}
return StoreClientMixin;
}
return dedupingMixin(storeClientMixin);
}