From 1ff3c32fe4307b38ad25830074ca992eb4641334 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 6 Jun 2019 15:53:50 +0200 Subject: [PATCH] [IMP] store: allow connecting to another store Closes #58 --- doc/store.md | 60 ++++++++++++++++++++++++-- src/store.ts | 23 +++++----- tests/__snapshots__/store.test.ts.snap | 4 ++ tests/store.test.ts | 44 +++++++++++++++++++ tools/playground/samples.js | 15 ++++--- 5 files changed, 126 insertions(+), 20 deletions(-) diff --git a/doc/store.md b/doc/store.md index ae15c2de..a4f58676 100644 --- a/doc/store.md +++ b/doc/store.md @@ -28,7 +28,7 @@ Note: Owl's store is inspired by React Redux and VueX. ## Example -Here is what a simple store look like: +Here is what a simple store looks like: ```js 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 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 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 @@ -146,4 +146,58 @@ const post = store.getters.getPost(id); ### Connecting a component -Todo \ No newline at end of file +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 + +``` + +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` diff --git a/src/store.ts b/src/store.ts index 853d0231..57cacbef 100644 --- a/src/store.ts +++ b/src/store.ts @@ -155,6 +155,7 @@ let nextID = 1; export function connect(mapStateToProps, options: any = {}) { let hashFunction = options.hashFunction || null; + const getStore = options.getStore || (env => env.store); if (!hashFunction) { let deep = "deep" in options ? options.deep : true; @@ -186,19 +187,21 @@ export function connect(mapStateToProps, options: any = {}) { 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 = mapStateToProps( - env.store.state, + store.state, ownProps, - env.store.getters + store.getters ); const mergedProps = Object.assign({}, props || {}, storeProps); super(parent, mergedProps); (this.__owl__).ownProps = ownProps; (this.__owl__).currentStoreProps = storeProps; + (this.__owl__).store = store; (this.__owl__).storeHash = hashFunction( { - state: env.store.state, + state: store.state, storeProps: storeProps, revNumber, 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. */ _callMounted() { - this.env.store.on("update", this, this._checkUpdate); + (this.__owl__).store.on("update", this, this._checkUpdate); super._callMounted(); } willUnmount() { - this.env.store.off("update", this); + (this.__owl__).store.off("update", this); super.willUnmount(); } _checkUpdate() { const ownProps = (this.__owl__).ownProps; const storeProps = mapStateToProps( - this.env.store.state, + (this.__owl__).store.state, ownProps, - this.env.store.getters + (this.__owl__).store.getters ); const options: any = { currentStoreProps: (this.__owl__).currentStoreProps }; const storeHash = hashFunction( { - state: this.env.store.state, + state: (this.__owl__).store.state, storeProps: storeProps, revNumber, deepRevNumber @@ -254,9 +257,9 @@ export function connect(mapStateToProps, options: any = {}) { _updateProps(nextProps, forceUpdate, patchQueue?: any[]) { if ((this.__owl__).ownProps !== nextProps) { (this.__owl__).currentStoreProps = mapStateToProps( - this.env.store.state, + (this.__owl__).store.state, nextProps, - this.env.store.getters + (this.__owl__).store.getters ); } (this.__owl__).ownProps = nextProps; diff --git a/tests/__snapshots__/store.test.ts.snap b/tests/__snapshots__/store.test.ts.snap index dc440c2d..1754f5d5 100644 --- a/tests/__snapshots__/store.test.ts.snap +++ b/tests/__snapshots__/store.test.ts.snap @@ -1,5 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`connecting a component to store connecting a component to a local store 1`] = `"
"`; + +exports[`connecting a component to store connecting a component to a local store 2`] = `"
hello
"`; + exports[`connecting a component to store connecting a component works 1`] = `"
"`; exports[`connecting a component to store connecting a component works 2`] = `"
hello
"`; diff --git a/tests/store.test.ts b/tests/store.test.ts index 88f12f82..05cecf02 100644 --- a/tests/store.test.ts +++ b/tests/store.test.ts @@ -476,6 +476,50 @@ describe("connecting a component to store", () => { expect(shallowFix.innerHTML).toMatchSnapshot(); }); + test("connecting a component to a local store", async () => { + env.qweb.addTemplates(` + +
+ + + +
+ +
+ `); + class App extends Component { + widgets = { Todo }; + } + class Todo extends Component {} + + (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(); + + (app.__owl__).store.commit("addTodo", "hello"); + await nextTick(); + expect(fixture.innerHTML).toMatchSnapshot(); + }); + test("connected child components with custom hooks", async () => { let steps: any = []; env.qweb.addTemplate("Child", `
`); diff --git a/tools/playground/samples.js b/tools/playground/samples.js index 8134210a..649b2e03 100644 --- a/tools/playground/samples.js +++ b/tools/playground/samples.js @@ -384,11 +384,11 @@ class TodoItem extends owl.Component { state = { isEditing: false }; removeTodo() { - this.env.store.dispatch("removeTodo", this.props.id); + this.env.dispatch("removeTodo", this.props.id); } toggleTodo() { - this.env.store.dispatch("toggleTodo", this.props.id); + this.env.dispatch("toggleTodo", this.props.id); } async editTodo() { @@ -420,7 +420,7 @@ class TodoItem extends owl.Component { if (!value) { this.removeTodo(this.props.id); } else { - this.env.store.dispatch("editTodo", { + this.env.dispatch("editTodo", { id: this.props.id, title: value }); @@ -470,18 +470,18 @@ class TodoApp extends owl.Component { if (ev.keyCode === ENTER_KEY) { const title = ev.target.value; if (title.trim()) { - this.env.store.dispatch("addTodo", title); + this.env.dispatch("addTodo", title); } ev.target.value = ""; } } clearCompleted() { - this.env.store.dispatch("clearCompleted"); + this.env.dispatch("clearCompleted"); } toggleAll() { - this.env.store.dispatch("toggleAll", !this.allChecked); + this.env.dispatch("toggleAll", !this.allChecked); } setFilter(filter) { @@ -498,7 +498,8 @@ const store = makeStore(); const qweb = new owl.QWeb(TEMPLATES); const env = { qweb, - store + store, + dispatch: store.dispatch.bind(store), }; const app = new ConnectedTodoApp(env); app.mount(document.body);