From 147f6fced73f2c8552838abb5942aad6d42c2b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Wed, 8 May 2019 17:10:12 +0200 Subject: [PATCH] [IMP] store: getters --- doc/store.md | 24 +++--- src/store.ts | 42 +++++++++-- tests/store.test.ts | 178 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+), 19 deletions(-) diff --git a/doc/store.md b/doc/store.md index 8b1ab806..8dc0011e 100644 --- a/doc/store.md +++ b/doc/store.md @@ -83,7 +83,7 @@ object, or modifying an array with the `arr[i] = newValue` syntax). See the ### Mutations Mutations are the only way to modify the state. Changing the state outside a -mutation is not allowed (and should throw an error). Mutations as synchronous. +mutation is not allowed (and should throw an error). Mutations are synchronous. ### Actions @@ -93,7 +93,7 @@ in an action. ```js const actions = { - login({commit}) { + async login({commit}) { commit('setLoginState', 'pending'); try { const loginInfo = await doSomeRPC('/login/', 'someinfo'); @@ -128,18 +128,20 @@ transform the data contained in the store. ```js const getters = { - getPost({state}) { - return function (id) { - const post = state.posts.find(p => p.id === id); - const author = state.authors.find(a => a.id = post.id); - return { - id, - author, - content: post.content - }; + getPost({state}, id) { + const post = state.posts.find(p => p.id === id); + const author = state.authors.find(a => a.id = post.id); + return { + id, + author, + content: post.content }; }, }; + +// somewhere else +const post = store.getters.getPost(id); + ``` ### Connecting a component diff --git a/src/store.ts b/src/store.ts index e92ac0be..78a8cfd8 100644 --- a/src/store.ts +++ b/src/store.ts @@ -21,13 +21,15 @@ import { Observer } from "./observer"; // Store Definition //------------------------------------------------------------------------------ -type Mutation = ({state, commit, set}, payload: any) => void; -type Action = ({commit, state, dispatch, env}, payload: any) => void; +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}; + actions?: { [name: string]: Action }; + getters?: { [name: string]: Getter }; mutations?: { [name: string]: Mutation }; } @@ -45,6 +47,7 @@ export class Store extends EventBus { env: any; observer: Observer; set: any; + getters: { [name: string]: Getter }; constructor(config: StoreConfig, options: StoreOption = {}) { super(); @@ -57,11 +60,23 @@ export class Store extends EventBus { 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]; + Object.defineProperty(this.getters, name, { + get: func.bind(this, { + state: this.state, + getters: this.getters + }) + }); + } } dispatch(action: string, payload?: any): Promise | void { @@ -73,7 +88,8 @@ export class Store extends EventBus { commit: this.commit.bind(this), dispatch: this.dispatch.bind(this), env: this.env, - state: this.state + state: this.state, + getters: this.getters }, payload ); @@ -97,7 +113,8 @@ export class Store extends EventBus { { commit: this.commit.bind(this), state: this.state, - set: this.set + set: this.set, + getters: this.getters }, payload ); @@ -174,7 +191,11 @@ export function connect(mapStateToProps, options: any = {}) { constructor(parent, props?: any) { const env = parent instanceof Component ? parent.env : parent; const ownProps = Object.assign({}, props || {}); - const storeProps = mapStateToProps(env.store.state, ownProps); + const storeProps = mapStateToProps( + env.store.state, + ownProps, + env.store.getters + ); const mergedProps = Object.assign({}, props || {}, storeProps); super(parent, mergedProps); (this.__owl__).ownProps = ownProps; @@ -207,7 +228,11 @@ export function connect(mapStateToProps, options: any = {}) { _checkUpdate() { const ownProps = (this.__owl__).ownProps; - const storeProps = mapStateToProps(this.env.store.state, ownProps); + const storeProps = mapStateToProps( + this.env.store.state, + ownProps, + this.env.store.getters + ); const options: any = { currentStoreProps: (this.__owl__).currentStoreProps }; @@ -234,7 +259,8 @@ export function connect(mapStateToProps, options: any = {}) { if ((this.__owl__).ownProps !== nextProps) { (this.__owl__).currentStoreProps = mapStateToProps( this.env.store.state, - nextProps + nextProps, + this.env.store.getters ); } (this.__owl__).ownProps = nextProps; diff --git a/tests/store.test.ts b/tests/store.test.ts index cdaaec00..f4b2d108 100644 --- a/tests/store.test.ts +++ b/tests/store.test.ts @@ -169,6 +169,131 @@ describe("basic use", () => { expect(updateCounter).toBe(1); expect(store.state).toEqual({ bertinchamps: "brune", chouffe: "blonde" }); }); + + test("can have getters from store", async () => { + const state = { + beers: { + 1: { + id: 1, + name: "bertinchamps", + tasterID: 1, + }, + }, + tasters: { + 1: { + id: 1, + name: 'aaron', + } + }, + }; + const getters = { + beerTasterName({ state }) { + return beerID => { + return state.tasters[state.beers[beerID].tasterID].name; + } + }, + bestBeerName({ state }) { + return state.beers[1].name; + } + }; + const store = new Store({ state, mutations: {}, actions: {}, getters }); + expect(store.getters).toBeDefined(); + expect((store.getters).bestBeerName).toBe("bertinchamps"); + expect((store.getters).beerTasterName(1)).toBe("aaron"); + }); + + test("getters given to actions", async () => { + expect.assertions(3); + const state = { + beers: { + 1: { + id: 1, + name: "bertinchamps", + tasterID: 1, + }, + }, + tasters: { + 1: { + id: 1, + name: 'aaron', + } + }, + }; + const getters = { + beerTasterName({ state }) { + return beerID => { + return state.tasters[state.beers[beerID].tasterID].name; + } + }, + bestBeerName({ state }) { + return state.beers[1].name; + } + }; + const actions = { + action({ getters }) { + expect(getters).toBeDefined(); + expect(getters.bestBeerName).toBe("bertinchamps"); + expect(getters.beerTasterName(1)).toBe("aaron"); + } + }; + const store = new Store({ state, mutations: {}, actions, getters }); + store.dispatch("action"); + }); + + test("getters given to mutations", async () => { + expect.assertions(3); + const state = { + beers: { + 1: { + id: 1, + name: "bertinchamps", + tasterID: 1, + }, + }, + tasters: { + 1: { + id: 1, + name: 'aaron', + } + }, + }; + const getters = { + beerTasterName({ state }) { + return beerID => { + return state.tasters[state.beers[beerID].tasterID].name; + } + }, + bestBeerName({ state }) { + return state.beers[1].name; + } + }; + const mutations = { + mutation({ getters }) { + expect(getters).toBeDefined(); + expect(getters.bestBeerName).toBe("bertinchamps"); + expect(getters.beerTasterName(1)).toBe("aaron"); + } + }; + const store = new Store({ state, mutations, actions: {}, getters }); + store.commit("mutation"); + }); + + test("can use getters inside a getter", () => { + const getters = { + a({ getters }) { + return `${getters.b}${getters.c(1)}`; + }, + b() { + return 'b'; + }, + c() { + return i => `c${i}`; + }, + }; + const store = new Store({ getters }); + + expect(store.getters.a).toBe('bc1'); + }); }); describe("advanced state properties", () => { @@ -456,6 +581,59 @@ describe("connecting a component to store", () => { ); }); + test("connect receives store getters as third argument", async () => { + const state = { + importantID: 1, + todos: [ + { id: 1, text: "jupiler" }, + { id: 2, text: "bertinchamps" }, + ], + }; + const getters = { + importantTodoText({ state }) { + return state.todos.find(todo => todo.id === state.importantID).text; + }, + text({ state }) { + return id => state.todos.find(todo => todo.id === id).text; + }, + }; + const store = new Store({ state, getters }); + + class TodoItem extends Component { + inlineTemplate = `
+ + +
`; + } + const ConnectedTodo = connect((state, props, getters) => { + const todo = state.todos.find(t => t.id === props.id); + return { + activeTodoText: getters.text(todo.id), + importantTodoText: getters.importantTodoText, + }; + })(TodoItem); + + class TodoList extends Component { + inlineTemplate = `
+ + + +
`; + widgets = { ConnectedTodo }; + } + + function mapStateToProps(state) { + return { todos: state.todos }; + } + const ConnectedTodoList = connect(mapStateToProps)(TodoList); + + (env).store = store; + const app = new ConnectedTodoList(env); + + await app.mount(fixture); + expect(fixture.innerHTML).toBe("
jupilerjupiler
bertinchampsjupiler
"); + }); + test("connected component is updated when props are updated", async () => { class Beer extends Component { inlineTemplate = ``;