[IMP] store: allow connecting to another store

Closes #58
This commit is contained in:
Aaron Bohy
2019-06-06 15:53:50 +02:00
committed by Géry Debongnie
parent 9a3e1ed99e
commit 1ff3c32fe4
5 changed files with 126 additions and 20 deletions
+57 -3
View File
@@ -28,7 +28,7 @@ Note: Owl's store is inspired by React Redux and VueX.
## Example ## Example
Here is what a simple store look like: Here is what a simple store looks like:
```js ```js
const actions = { const actions = {
@@ -68,7 +68,7 @@ state is changed. Note that these events are triggered only after a microtask
tick, so only one event will be triggered for any number of state changes in a tick, so only one event will be triggered for any number of state changes in a
call stack. call stack.
Also, it is important to mention that the state is observed (with a `owl.Observer`), Also, it is important to mention that the state is observed (with an `owl.Observer`),
which is the reason why it is able to know if it was changed. This implies that which is the reason why it is able to know if it was changed. This implies that
state changes need to be done carefully in some cases (adding a new key to an state changes need to be done carefully in some cases (adding a new key to an
object, or modifying an array with the `arr[i] = newValue` syntax). See the object, or modifying an array with the `arr[i] = newValue` syntax). See the
@@ -146,4 +146,58 @@ const post = store.getters.getPost(id);
### Connecting a component ### Connecting a component
Todo 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.
```javascript
const actions = {
increment({commit}) {
commit('increment', 1);
}
};
const mutations = {
increment({state}, val) {
state.counter += val;
}
};
const state = {
counter: 0,
};
const store = new owl.Store({state, actions, mutations});
class Counter extends owl.Component {
increment() {
this.env.store.dispatch('increment');
}
}
function mapStoreToProps(state) {
return {
value: state.counter
};
}
const ConnectedCounter = owl.connect(mapStoreToProps)(Counter);
const counter = new ConnectedCounter({ store, qweb });
```
```xml
<button t-name="Counter" t-on-click="increment">
Click Me! [<t t-esc="props.value"/>]
</button>
```
The arguments of `connect` are:
- `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 function that takes a `Component` as argument
and returns a sub Class of this `Component` that is connected to the `store`
+13 -10
View File
@@ -155,6 +155,7 @@ let nextID = 1;
export function connect(mapStateToProps, options: any = {}) { export function connect(mapStateToProps, options: any = {}) {
let hashFunction = options.hashFunction || null; let hashFunction = options.hashFunction || null;
const getStore = options.getStore || (env => env.store);
if (!hashFunction) { if (!hashFunction) {
let deep = "deep" in options ? options.deep : true; let deep = "deep" in options ? options.deep : true;
@@ -186,19 +187,21 @@ export function connect(mapStateToProps, options: any = {}) {
const Result = class extends Comp { const Result = class extends Comp {
constructor(parent, props?: any) { constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent; const env = parent instanceof Component ? parent.env : parent;
const store = getStore(env);
const ownProps = Object.assign({}, props || {}); const ownProps = Object.assign({}, props || {});
const storeProps = mapStateToProps( const storeProps = mapStateToProps(
env.store.state, store.state,
ownProps, ownProps,
env.store.getters store.getters
); );
const mergedProps = Object.assign({}, props || {}, storeProps); const mergedProps = Object.assign({}, props || {}, storeProps);
super(parent, mergedProps); super(parent, mergedProps);
(<any>this.__owl__).ownProps = ownProps; (<any>this.__owl__).ownProps = ownProps;
(<any>this.__owl__).currentStoreProps = storeProps; (<any>this.__owl__).currentStoreProps = storeProps;
(<any>this.__owl__).store = store;
(<any>this.__owl__).storeHash = hashFunction( (<any>this.__owl__).storeHash = hashFunction(
{ {
state: env.store.state, state: store.state,
storeProps: storeProps, storeProps: storeProps,
revNumber, revNumber,
deepRevNumber deepRevNumber
@@ -214,27 +217,27 @@ export function connect(mapStateToProps, options: any = {}) {
* if we use the mounted hook, this will be done in the reverse order. * if we use the mounted hook, this will be done in the reverse order.
*/ */
_callMounted() { _callMounted() {
this.env.store.on("update", this, this._checkUpdate); (<any>this.__owl__).store.on("update", this, this._checkUpdate);
super._callMounted(); super._callMounted();
} }
willUnmount() { willUnmount() {
this.env.store.off("update", this); (<any>this.__owl__).store.off("update", this);
super.willUnmount(); super.willUnmount();
} }
_checkUpdate() { _checkUpdate() {
const ownProps = (<any>this.__owl__).ownProps; const ownProps = (<any>this.__owl__).ownProps;
const storeProps = mapStateToProps( const storeProps = mapStateToProps(
this.env.store.state, (<any>this.__owl__).store.state,
ownProps, ownProps,
this.env.store.getters (<any>this.__owl__).store.getters
); );
const options: any = { const options: any = {
currentStoreProps: (<any>this.__owl__).currentStoreProps currentStoreProps: (<any>this.__owl__).currentStoreProps
}; };
const storeHash = hashFunction( const storeHash = hashFunction(
{ {
state: this.env.store.state, state: (<any>this.__owl__).store.state,
storeProps: storeProps, storeProps: storeProps,
revNumber, revNumber,
deepRevNumber deepRevNumber
@@ -254,9 +257,9 @@ export function connect(mapStateToProps, options: any = {}) {
_updateProps(nextProps, forceUpdate, patchQueue?: any[]) { _updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
if ((<any>this.__owl__).ownProps !== nextProps) { if ((<any>this.__owl__).ownProps !== nextProps) {
(<any>this.__owl__).currentStoreProps = mapStateToProps( (<any>this.__owl__).currentStoreProps = mapStateToProps(
this.env.store.state, (<any>this.__owl__).store.state,
nextProps, nextProps,
this.env.store.getters (<any>this.__owl__).store.getters
); );
} }
(<any>this.__owl__).ownProps = nextProps; (<any>this.__owl__).ownProps = nextProps;
+4
View File
@@ -1,5 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`connecting a component to store connecting a component to a local store 1`] = `"<div></div>"`;
exports[`connecting a component to store connecting a component to a local store 2`] = `"<div><span>hello</span></div>"`;
exports[`connecting a component to store connecting a component works 1`] = `"<div></div>"`; exports[`connecting a component to store connecting a component works 1`] = `"<div></div>"`;
exports[`connecting a component to store connecting a component works 2`] = `"<div><span>hello</span></div>"`; exports[`connecting a component to store connecting a component works 2`] = `"<div><span>hello</span></div>"`;
+44
View File
@@ -476,6 +476,50 @@ describe("connecting a component to store", () => {
expect(shallowFix.innerHTML).toMatchSnapshot(); expect(shallowFix.innerHTML).toMatchSnapshot();
}); });
test("connecting a component to a local store", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<t t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-widget="Todo" msg="todo.msg"/>
</t>
</div>
<span t-name="Todo"><t t-esc="props.msg"/></span>
</templates>
`);
class App extends Component<any, any, any> {
widgets = { Todo };
}
class Todo extends Component<any, any, any> {}
(<any>env).store = new Store({});
const store = new Store({
state: { todos: [] },
mutations: {
addTodo({ state }, msg) {
state.todos.push({ msg });
}
}
});
function mapStateToProps(s) {
return { todos: s.todos };
}
const TodoApp = connect(
mapStateToProps,
{
getStore: () => store
}
)(App);
const app = new TodoApp(env);
await app.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
(<any>app.__owl__).store.commit("addTodo", "hello");
await nextTick();
expect(fixture.innerHTML).toMatchSnapshot();
});
test("connected child components with custom hooks", async () => { test("connected child components with custom hooks", async () => {
let steps: any = []; let steps: any = [];
env.qweb.addTemplate("Child", `<div/>`); env.qweb.addTemplate("Child", `<div/>`);
+8 -7
View File
@@ -384,11 +384,11 @@ class TodoItem extends owl.Component {
state = { isEditing: false }; state = { isEditing: false };
removeTodo() { removeTodo() {
this.env.store.dispatch("removeTodo", this.props.id); this.env.dispatch("removeTodo", this.props.id);
} }
toggleTodo() { toggleTodo() {
this.env.store.dispatch("toggleTodo", this.props.id); this.env.dispatch("toggleTodo", this.props.id);
} }
async editTodo() { async editTodo() {
@@ -420,7 +420,7 @@ class TodoItem extends owl.Component {
if (!value) { if (!value) {
this.removeTodo(this.props.id); this.removeTodo(this.props.id);
} else { } else {
this.env.store.dispatch("editTodo", { this.env.dispatch("editTodo", {
id: this.props.id, id: this.props.id,
title: value title: value
}); });
@@ -470,18 +470,18 @@ class TodoApp extends owl.Component {
if (ev.keyCode === ENTER_KEY) { if (ev.keyCode === ENTER_KEY) {
const title = ev.target.value; const title = ev.target.value;
if (title.trim()) { if (title.trim()) {
this.env.store.dispatch("addTodo", title); this.env.dispatch("addTodo", title);
} }
ev.target.value = ""; ev.target.value = "";
} }
} }
clearCompleted() { clearCompleted() {
this.env.store.dispatch("clearCompleted"); this.env.dispatch("clearCompleted");
} }
toggleAll() { toggleAll() {
this.env.store.dispatch("toggleAll", !this.allChecked); this.env.dispatch("toggleAll", !this.allChecked);
} }
setFilter(filter) { setFilter(filter) {
@@ -498,7 +498,8 @@ const store = makeStore();
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb(TEMPLATES);
const env = { const env = {
qweb, qweb,
store store,
dispatch: store.dispatch.bind(store),
}; };
const app = new ConnectedTodoApp(env); const app = new ConnectedTodoApp(env);
app.mount(document.body); app.mount(document.body);