import { Component, Env } from "./component"; import { EventBus } from "./event_bus"; import { Observer } from "./observer"; /** * Owl Store * * We have here: * - a Store class * - a connect function * * 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/mutations, 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 //------------------------------------------------------------------------------ type Mutation = ({ state, commit, set, getters }, payload: any) => void; type Action = ({ commit, state, dispatch, env, getters }, payload: any) => void; type Getter = ({ state, getters }, payload) => any; interface StoreConfig { env?: Env; state?: any; actions?: { [name: string]: Action }; getters?: { [name: string]: Getter }; mutations?: { [name: string]: Mutation }; } interface StoreOption { debug?: boolean; } export class Store extends EventBus { state: any; actions: any; mutations: any; _commitLevel: number = 0; history: any[] = []; debug: boolean; env: any; observer: Observer; set: any; getters: { [name: string]: (payload?) => any }; constructor(config: StoreConfig, options: StoreOption = {}) { super(); this.debug = options.debug || false; this.state = config.state || {}; this.actions = config.actions; this.mutations = config.mutations; this.env = config.env; this.observer = new Observer(); this.observer.notifyCB = this.trigger.bind(this, "update"); this.observer.allowMutations = false; this.observer.observe(this.state); this.getters = {}; if (this.debug) { this.history.push({ state: this.state }); } this.set = this.observer.set.bind(this.observer); for (let entry of Object.entries(config.getters || {})) { const name: string = entry[0]; const func: (...any) => any = entry[1]; this.getters[name] = payload => { return func({ state: this.state, getters: this.getters }, payload); }; } } dispatch(action: string, payload?: any): Promise | void { if (!this.actions[action]) { throw new Error(`[Error] action ${action} is undefined`); } const result = this.actions[action]( { commit: this.commit.bind(this), dispatch: this.dispatch.bind(this), env: this.env, state: this.state, getters: this.getters }, payload ); if (result instanceof Promise) { return new Promise((resolve, reject) => { result.then(() => resolve()); result.catch(reject); }); } } commit(type: string, payload?: any): any { if (!this.mutations[type]) { throw new Error(`[Error] mutation ${type} is undefined`); } this._commitLevel++; this.observer.allowMutations = true; const res = this.mutations[type].call( null, { commit: this.commit.bind(this), state: this.state, set: this.set, getters: this.getters }, payload ); if (this._commitLevel === 1) { this.observer.allowMutations = false; if (this.debug) { this.history.push({ state: this.state, mutation: type, payload: payload }); } } this._commitLevel--; return res; } } //------------------------------------------------------------------------------ // Connect function //------------------------------------------------------------------------------ function revNumber(o: T): number { if (o !== null && typeof o === "object" && (o).__owl__) { return (o).__owl__.rev; } return 0; } function deepRevNumber(o: T): number { if (o !== null && typeof o === "object" && (o).__owl__) { return (o).__owl__.deepRev; } return 0; } type Constructor = new (...args: any[]) => T; interface EnvWithStore extends Env { store: Store; } let nextID = 1; export function connect(mapStateToProps, options: any = {}) { let hashFunction = options.hashFunction || null; if (!hashFunction) { let deep = "deep" in options ? options.deep : true; let defaultRevFunction = deep ? deepRevNumber : revNumber; hashFunction = function({ storeProps }, options) { const { currentStoreProps } = options; if ("__owl__" in storeProps) { return defaultRevFunction(storeProps); } let hash = 0; for (let key in storeProps) { const val = storeProps[key]; const hashVal = defaultRevFunction(val); if (hashVal === 0) { if (val !== currentStoreProps[key]) { options.didChange = true; } } else { hash += hashVal; } } return hash; }; } return function( Comp: Constructor> ) { const Result = class extends Comp { constructor(parent, props?: any) { const env = parent instanceof Component ? parent.env : parent; const ownProps = Object.assign({}, props || {}); const storeProps = mapStateToProps( env.store.state, ownProps, env.store.getters ); const mergedProps = Object.assign({}, props || {}, storeProps); super(parent, mergedProps); (this.__owl__).ownProps = ownProps; (this.__owl__).currentStoreProps = storeProps; (this.__owl__).storeHash = hashFunction( { state: env.store.state, storeProps: storeProps, revNumber, deepRevNumber }, { currentStoreProps: storeProps } ); } /** * We do not use the mounted hook here for a subtle reason: we want the * updates to be called for the parents before the children. However, * if we use the mounted hook, this will be done in the reverse order. */ _callMounted() { this.env.store.on("update", this, this._checkUpdate); super._callMounted(); } willUnmount() { this.env.store.off("update", this); super.willUnmount(); } _checkUpdate() { const ownProps = (this.__owl__).ownProps; const storeProps = mapStateToProps( this.env.store.state, ownProps, this.env.store.getters ); const options: any = { currentStoreProps: (this.__owl__).currentStoreProps }; const storeHash = hashFunction( { state: this.env.store.state, storeProps: storeProps, revNumber, deepRevNumber }, options ); let didChange = options.didChange; if (storeHash !== (this.__owl__).storeHash) { didChange = true; (this.__owl__).storeHash = storeHash; } if (didChange) { (this.__owl__).currentStoreProps = storeProps; this._updateProps(ownProps, false); } } _updateProps(nextProps, forceUpdate, patchQueue?: any[]) { if ((this.__owl__).ownProps !== nextProps) { (this.__owl__).currentStoreProps = mapStateToProps( this.env.store.state, nextProps, this.env.store.getters ); } (this.__owl__).ownProps = nextProps; const mergedProps = Object.assign( {}, nextProps, (this.__owl__).currentStoreProps ); return super._updateProps(mergedProps, forceUpdate, patchQueue); } }; // we assign here a unique name to the resulting anonymous class. // this is necessary for Owl to be able to properly deduce templates. // Otherwise, all connected components would have the same name, and then // each component after the first will necessarily have the same template. let name = `ConnectedComponent${nextID++}`; Object.defineProperty(Result, "name", { value: name }); return Result; }; }