Skip to content

Commit

Permalink
[REF] store: remove connect function, add ConnectedComponent
Browse files Browse the repository at this point in the history
closes #238
closes #235
  • Loading branch information
ged-odoo committed Jul 13, 2019
1 parent 591508e commit 545ceef
Show file tree
Hide file tree
Showing 5 changed files with 362 additions and 476 deletions.
59 changes: 26 additions & 33 deletions doc/store.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ mutation is not allowed (and should throw an error). Mutations are synchronous.
```js
const mutations = {
setLoginState({ state }, loginState) {
state.loginState = loginState;
state.loginState = loginState;
}
};
```
Expand Down Expand Up @@ -165,17 +165,16 @@ const getters = {
const post = store.getters.getPost(id);
```

Getters take *at most* one argument.

Getters take _at most_ one argument.

Note that getters are cached if they don't take any argument, or their argument
is a string or a number.

### Connecting a Component

By default, an Owl `Component` is not connected to any store. The `connect`
function is there to create sub Components that are connected versions of
Components.
At some point, we need a way to access the state in the store from a component.
By default, an Owl `Component` is not connected to any store. To do that, we
need to create a component inheriting from `OwlComponent`:

```javascript
const actions = {
Expand All @@ -193,19 +192,18 @@ const state = {
};
const store = new owl.Store({ state, actions, mutations });

class Counter extends owl.Component {
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
increment() {
this.env.store.dispatch("increment");
}
}
function mapStoreToProps(state) {
return {
value: state.counter
};
}
const ConnectedCounter = owl.connect(Counter, mapStoreToProps);

const counter = new ConnectedCounter({ store, qweb });
const counter = new Counter({ store, qweb });
```

```xml
Expand All @@ -214,45 +212,40 @@ const counter = new ConnectedCounter({ store, qweb });
</button>
```

The arguments of `connect` are:
The `ConnectedComponent` class can be configured with the following fields:

- `Counter`: an owl `Component` to connect
- `mapStoreToProps`: a function that extracts the `props` of the Component
from the `state` of the `Store` and returns them as a dict
- `options`: dictionary of optional parameters that may contain
- `getStore`: a function that takes the `env` in arguments and returns an
instance of `Store` to connect to (if not given, connects to `env.store`)
- `hashFunction`: the function to use to detect changes in the state (if not
given, generates a function that uses revision numbers, incremented at
each state change)
- `deep`: [only useful if no hashFunction is given] if false, only watch
for top level state changes (true by default)

The `connect` function returns a sub class of the given `Component` which is
connected to the `store`.
from the `state` of the `Store` and returns them as a dict.
- `getStore`: a function that takes the `env` in arguments and returns an
instance of `Store` to connect to (if not given, connects to `env.store`)
- `hashFunction`: the function to use to detect changes in the state (if not
given, generates a function that uses revision numbers, incremented at
each state change)
- `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch
for top level state changes (`true` by default)

### Semantics

The `Store` and the `connect` function try to be smart and to optimize as much
as possible the rendering and update process. What is important to know is:
The `Store` and the `ConnectedComponent` try to be smart and to optimize as much
as possible the rendering and update process. What is important to know is:

- components are always updated in the order of their creation (so, parent
before children)
- they are updated only if they are in the DOM
- if a parent is asynchronous, the system will wait for it to complete its
update before updating other components.
- in general, updates are not coordinated. This is not a problem for synchronous
- in general, updates are not coordinated. This is not a problem for synchronous
components, but if there are many asynchronous components, this could lead to
a situation where some part of the UI is updated and other parts of the UI is
not updated.

### Good Practices

- avoid asynchronous components as much as possible. Asynchronous components
- avoid asynchronous components as much as possible. Asynchronous components
lead to situations where parts of the UI is not updated immediately.
- do not be afraid to connect many components, parent or children if needed. For
example, a `MessageList` component could get a list of ids in its `mapStoreToProps` and a `Message` component could get the data of its own
message
- since the `mapStoreToProps` function is called for each connected component,
for each state update, it is important to make sure that these functions are
as fast as possible.
as fast as possible.
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import "./qweb_extensions";
import { QWeb } from "./qweb_core";
export { QWeb };

export { connect, Store } from "./store";
export { Store, ConnectedComponent } from "./store";
import * as _utils from "./utils";

export const __info__ = {};
Expand Down
231 changes: 107 additions & 124 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Observer } from "./observer";
*
* We have here:
* - a Store class
* - a connect function
* - the ConnectedComponent class
*
* 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
Expand Down Expand Up @@ -185,139 +185,122 @@ function deepRevNumber<T extends Object>(o: T): number {
return 0;
}

type Constructor<T> = new (...args: any[]) => T;
interface EnvWithStore extends Env {
store: Store;
}
type HashFunction = (a: any, b: any) => number;
interface StoreOptions {
getStore?(Env): Store;
hashFunction?: HashFunction;
deep?: boolean;
}

export function connect<E extends EnvWithStore, P, S>(
Comp: Constructor<Component<E, P, S>>,
mapStoreToProps,
options: StoreOptions = <StoreOptions>{}
) {
let hashFunction = options.hashFunction || null;
const getStore = options.getStore || (env => env.store);
export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> {
deep: boolean = true;
getStore(env) {
return env.store;
}

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;
hashFunction: HashFunction = ({ storeProps }, options) => {
let refFunction = this.deep ? deepRevNumber : revNumber;
if ("__owl__" in storeProps) {
return refFunction(storeProps);
}
const { currentStoreProps } = options;
let hash = 0;
for (let key in storeProps) {
const val = storeProps[key];
const hashVal = refFunction(val);
if (hashVal === 0) {
if (val !== currentStoreProps[key]) {
options.didChange = true;
}
} else {
hash += hashVal;
}
return hash;
};
}
return hash;
};

static mapStoreToProps(storeState, ownProps, getters) {
return {};
}
constructor(parent, props?: any) {
super(parent, props);
const store = this.getStore(this.env);
const ownProps = Object.assign({}, props || {});
const storeProps = (<any>this.constructor).mapStoreToProps(
store.state,
ownProps,
store.getters
);
const mergedProps = Object.assign({}, props || {}, storeProps);
this.props = mergedProps;
(<any>this.__owl__).ownProps = ownProps;
(<any>this.__owl__).currentStoreProps = storeProps;
(<any>this.__owl__).store = store;
(<any>this.__owl__).storeHash = this.hashFunction(
{
state: 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() {
(<any>this.__owl__).store.on("update", this, this.__checkUpdate);
super.__callMounted();
}
willUnmount() {
(<any>this.__owl__).store.off("update", this);
super.willUnmount();
}

const Result = class extends Comp {
constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent;
const store = getStore(env);
const ownProps = Object.assign({}, props || {});
const storeProps = mapStoreToProps(store.state, ownProps, store.getters);
const mergedProps = Object.assign({}, props || {}, storeProps);
super(parent, mergedProps);
(<any>this.__owl__).ownProps = ownProps;
(<any>this.__owl__).currentStoreProps = storeProps;
(<any>this.__owl__).store = store;
(<any>this.__owl__).storeHash = (<HashFunction>hashFunction)(
{
state: store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
{
currentStoreProps: storeProps
}
);
async __checkUpdate(updateId) {
if (updateId === (<any>this.__owl__).currentUpdateId) {
return;
}
/**
* 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() {
(<any>this.__owl__).store.on("update", this, this.__checkUpdate);
super.__callMounted();
const ownProps = (<any>this.__owl__).ownProps;
const storeProps = (<any>this.constructor).mapStoreToProps(
(<any>this.__owl__).store.state,
ownProps,
(<any>this.__owl__).store.getters
);
const options: any = {
currentStoreProps: (<any>this.__owl__).currentStoreProps
};
const storeHash = this.hashFunction(
{
state: (<any>this.__owl__).store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
options
);
let didChange = options.didChange;
if (storeHash !== (<any>this.__owl__).storeHash) {
didChange = true;
(<any>this.__owl__).storeHash = storeHash;
}
willUnmount() {
(<any>this.__owl__).store.off("update", this);
super.willUnmount();
if (didChange) {
(<any>this.__owl__).currentStoreProps = storeProps;
await this.__updateProps(ownProps, false);
}

async __checkUpdate(updateId) {
if (updateId === (<any>this.__owl__).currentUpdateId) {
return;
}
const ownProps = (<any>this.__owl__).ownProps;
const storeProps = mapStoreToProps(
(<any>this.__owl__).store.state,
ownProps,
(<any>this.__owl__).store.getters
);
const options: any = {
currentStoreProps: (<any>this.__owl__).currentStoreProps
};
const storeHash = (<HashFunction>hashFunction)(
{
state: (<any>this.__owl__).store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
options
}
__updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
const __owl__ = <any>this.__owl__;
__owl__.currentUpdateId = __owl__.store._updateId;
if (__owl__.ownProps !== nextProps) {
__owl__.currentStoreProps = (<any>this.constructor).mapStoreToProps(
__owl__.store.state,
nextProps,
__owl__.store.getters
);
let didChange = options.didChange;
if (storeHash !== (<any>this.__owl__).storeHash) {
didChange = true;
(<any>this.__owl__).storeHash = storeHash;
}
if (didChange) {
(<any>this.__owl__).currentStoreProps = storeProps;
await this.__updateProps(ownProps, false);
}
}
__updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
const __owl__ = <any>this.__owl__;
__owl__.currentUpdateId = __owl__.store._updateId;
if (__owl__.ownProps !== nextProps) {
__owl__.currentStoreProps = mapStoreToProps(
__owl__.store.state,
nextProps,
__owl__.store.getters
);
}
__owl__.ownProps = nextProps;
const mergedProps = Object.assign({}, nextProps, __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 = `Connected${Comp.name}`;
Object.defineProperty(Result, "name", { value: name });
return Result;
__owl__.ownProps = nextProps;
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
return super.__updateProps(mergedProps, forceUpdate, patchQueue);
}
}
Loading

0 comments on commit 545ceef

Please sign in to comment.