Files
owl/src/store.ts
T
2019-11-26 22:17:05 +01:00

152 lines
4.3 KiB
TypeScript

import { Component } from "./component/component";
import { Env } from "./component/component";
import { Context, useContextWithCB } from "./context";
import { onWillUpdateProps } from "./hooks";
/**
* Owl Store
*
* We have here:
* - a Store class
* - useStore hook
* - useDispatch hook
* - useGetters hook
*
* The Owl store is our answer to the problem of managing complex state across
* components. The main idea is that the store owns some state, allow external
* code to modify it through actions, and for each state changes,
* connected component will be notified, and updated if necessary.
*
* Note that this code is partly inspired by VueX and React/Redux
*/
//------------------------------------------------------------------------------
// Store Definition
//------------------------------------------------------------------------------
export type Action = ({ state, dispatch, env, getters }, ...payload: any) => any;
export type Getter = ({ state: any, getters }, payload?) => any;
interface StoreConfig {
env?: Env;
state?: any;
actions?: { [name: string]: Action };
getters?: { [name: string]: Getter };
}
export class Store extends Context {
actions: any;
env: any;
getters: { [name: string]: (payload?) => any };
updateFunctions: { [key: number]: (() => boolean)[] };
constructor(config: StoreConfig) {
super(config.state);
this.actions = config.actions;
this.env = config.env;
this.getters = {};
this.updateFunctions = [];
if (config.getters) {
const firstArg = {
state: this.state,
getters: this.getters
};
for (let g in config.getters) {
this.getters[g] = config.getters[g].bind(this, firstArg);
}
}
}
dispatch(action: string, ...payload: any): Promise<void> | void {
if (!this.actions[action]) {
throw new Error(`[Error] action ${action} is undefined`);
}
const result = this.actions[action](
{
dispatch: this.dispatch.bind(this),
env: this.env,
state: this.state,
getters: this.getters
},
...payload
);
return result;
}
}
interface SelectorOptions {
store?: Store;
isEqual?: (a: any, b: any) => boolean;
onUpdate?: (result: any) => any;
}
const isStrictEqual = (a, b) => a === b;
export function useStore(selector, options: SelectorOptions = {}): any {
const component: Component<any, any> = Component.current!;
const store = options.store || (component.env.store as Store);
if (!(store instanceof Store)) {
throw new Error(`No store found when connecting '${component.constructor.name}'`);
}
let result = selector(store.state, component.props);
const hashFn = store.observer.revNumber.bind(store.observer);
let revNumber = hashFn(result);
const isEqual = options.isEqual || isStrictEqual;
if (!store.updateFunctions[component.__owl__.id]) {
store.updateFunctions[component.__owl__.id] = [];
}
const updateFunctions = store.updateFunctions[component.__owl__.id];
updateFunctions.push(function(): boolean {
const oldResult = result;
result = selector(store!.state, component.props);
const newRevNumber = hashFn(result);
if (
(newRevNumber > 0 && revNumber !== newRevNumber) ||
(newRevNumber === 0 && !isEqual(oldResult, result))
) {
revNumber = newRevNumber;
if (options.onUpdate) {
options.onUpdate(result);
}
return true;
}
return false;
});
useContextWithCB(store, component, function(): Promise<void> | void {
let shouldRender = false;
for (let fn of updateFunctions) {
shouldRender = fn() || shouldRender;
}
if (shouldRender) {
return component.render();
}
});
onWillUpdateProps(props => {
delete store.updateFunctions[component.__owl__.id];
result = selector(store.state, props);
});
if (typeof result !== "object") {
return result;
}
return new Proxy(result, {
get(target, k) {
return result[k];
},
set(target, k, v) {
result[k] = v;
return true;
}
});
}
export function useDispatch(store?: Store): Store["dispatch"] {
store = store || (Component.current!.env.store as Store);
return store.dispatch.bind(store);
}
export function useGetters(store?: Store): Store["getters"] {
store = store || (Component.current!.env.store as Store);
return store.getters;
}