mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
d1094f647a
This rev. add a 'set' and 'delete' static functions to the Observer. They allow to set or delete (only for objects) properties on observed objects or arrays. With this, the 'set' functions in Store and Component are no longer necessary, so they have been removed. Closes #138
281 lines
8.1 KiB
TypeScript
281 lines
8.1 KiB
TypeScript
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, 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;
|
|
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 });
|
|
}
|
|
|
|
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> | 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,
|
|
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<T extends Object>(o: T): number {
|
|
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
|
|
return (<any>o).__owl__.rev;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function deepRevNumber<T extends Object>(o: T): number {
|
|
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
|
|
return (<any>o).__owl__.deepRev;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
type Constructor<T> = 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<E extends EnvWithStore, P, S>(
|
|
Comp: Constructor<Component<E, P, S>>
|
|
) {
|
|
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);
|
|
(<any>this.__owl__).ownProps = ownProps;
|
|
(<any>this.__owl__).currentStoreProps = storeProps;
|
|
(<any>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 = (<any>this.__owl__).ownProps;
|
|
const storeProps = mapStateToProps(
|
|
this.env.store.state,
|
|
ownProps,
|
|
this.env.store.getters
|
|
);
|
|
const options: any = {
|
|
currentStoreProps: (<any>this.__owl__).currentStoreProps
|
|
};
|
|
const storeHash = hashFunction(
|
|
{
|
|
state: this.env.store.state,
|
|
storeProps: storeProps,
|
|
revNumber,
|
|
deepRevNumber
|
|
},
|
|
options
|
|
);
|
|
let didChange = options.didChange;
|
|
if (storeHash !== (<any>this.__owl__).storeHash) {
|
|
didChange = true;
|
|
(<any>this.__owl__).storeHash = storeHash;
|
|
}
|
|
if (didChange) {
|
|
(<any>this.__owl__).currentStoreProps = storeProps;
|
|
this._updateProps(ownProps, false);
|
|
}
|
|
}
|
|
_updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
|
|
if ((<any>this.__owl__).ownProps !== nextProps) {
|
|
(<any>this.__owl__).currentStoreProps = mapStateToProps(
|
|
this.env.store.state,
|
|
nextProps,
|
|
this.env.store.getters
|
|
);
|
|
}
|
|
(<any>this.__owl__).ownProps = nextProps;
|
|
const mergedProps = Object.assign(
|
|
{},
|
|
nextProps,
|
|
(<any>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;
|
|
};
|
|
}
|