[IMP] store: getters

This commit is contained in:
Alexandre Kühn
2019-05-08 17:10:12 +02:00
committed by Géry Debongnie
parent d928927da8
commit 147f6fced7
3 changed files with 225 additions and 19 deletions
+13 -11
View File
@@ -83,7 +83,7 @@ object, or modifying an array with the `arr[i] = newValue` syntax). See the
### Mutations ### Mutations
Mutations are the only way to modify the state. Changing the state outside a 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 ### Actions
@@ -93,7 +93,7 @@ in an action.
```js ```js
const actions = { const actions = {
login({commit}) { async login({commit}) {
commit('setLoginState', 'pending'); commit('setLoginState', 'pending');
try { try {
const loginInfo = await doSomeRPC('/login/', 'someinfo'); const loginInfo = await doSomeRPC('/login/', 'someinfo');
@@ -128,18 +128,20 @@ transform the data contained in the store.
```js ```js
const getters = { const getters = {
getPost({state}) { getPost({state}, id) {
return function (id) { const post = state.posts.find(p => p.id === id);
const post = state.posts.find(p => p.id === id); const author = state.authors.find(a => a.id = post.id);
const author = state.authors.find(a => a.id = post.id); return {
return { id,
id, author,
author, content: post.content
content: post.content
};
}; };
}, },
}; };
// somewhere else
const post = store.getters.getPost(id);
``` ```
### Connecting a component ### Connecting a component
+34 -8
View File
@@ -21,13 +21,15 @@ import { Observer } from "./observer";
// Store Definition // Store Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
type Mutation = ({state, commit, set}, payload: any) => void; type Mutation = ({ state, commit, set, getters }, payload: any) => void;
type Action = ({commit, state, dispatch, env}, payload: any) => void; type Action = ({ commit, state, dispatch, env, getters }, payload: any) => void;
type Getter = ({ state, getters }, payload) => any;
interface StoreConfig { interface StoreConfig {
env?: Env; env?: Env;
state?: any; state?: any;
actions?: {[name: string]: Action}; actions?: { [name: string]: Action };
getters?: { [name: string]: Getter };
mutations?: { [name: string]: Mutation }; mutations?: { [name: string]: Mutation };
} }
@@ -45,6 +47,7 @@ export class Store extends EventBus {
env: any; env: any;
observer: Observer; observer: Observer;
set: any; set: any;
getters: { [name: string]: Getter };
constructor(config: StoreConfig, options: StoreOption = {}) { constructor(config: StoreConfig, options: StoreOption = {}) {
super(); super();
@@ -57,11 +60,23 @@ export class Store extends EventBus {
this.observer.notifyCB = this.trigger.bind(this, "update"); this.observer.notifyCB = this.trigger.bind(this, "update");
this.observer.allowMutations = false; this.observer.allowMutations = false;
this.observer.observe(this.state); this.observer.observe(this.state);
this.getters = {};
if (this.debug) { if (this.debug) {
this.history.push({ state: this.state }); this.history.push({ state: this.state });
} }
this.set = this.observer.set.bind(this.observer); 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> | void { dispatch(action: string, payload?: any): Promise<void> | void {
@@ -73,7 +88,8 @@ export class Store extends EventBus {
commit: this.commit.bind(this), commit: this.commit.bind(this),
dispatch: this.dispatch.bind(this), dispatch: this.dispatch.bind(this),
env: this.env, env: this.env,
state: this.state state: this.state,
getters: this.getters
}, },
payload payload
); );
@@ -97,7 +113,8 @@ export class Store extends EventBus {
{ {
commit: this.commit.bind(this), commit: this.commit.bind(this),
state: this.state, state: this.state,
set: this.set set: this.set,
getters: this.getters
}, },
payload payload
); );
@@ -174,7 +191,11 @@ export function connect(mapStateToProps, options: any = {}) {
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 ownProps = Object.assign({}, props || {}); 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); const mergedProps = Object.assign({}, props || {}, storeProps);
super(parent, mergedProps); super(parent, mergedProps);
(<any>this.__owl__).ownProps = ownProps; (<any>this.__owl__).ownProps = ownProps;
@@ -207,7 +228,11 @@ export function connect(mapStateToProps, options: any = {}) {
_checkUpdate() { _checkUpdate() {
const ownProps = (<any>this.__owl__).ownProps; const ownProps = (<any>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 = { const options: any = {
currentStoreProps: (<any>this.__owl__).currentStoreProps currentStoreProps: (<any>this.__owl__).currentStoreProps
}; };
@@ -234,7 +259,8 @@ export function connect(mapStateToProps, options: 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, this.env.store.state,
nextProps nextProps,
this.env.store.getters
); );
} }
(<any>this.__owl__).ownProps = nextProps; (<any>this.__owl__).ownProps = nextProps;
+178
View File
@@ -169,6 +169,131 @@ describe("basic use", () => {
expect(updateCounter).toBe(1); expect(updateCounter).toBe(1);
expect(store.state).toEqual({ bertinchamps: "brune", chouffe: "blonde" }); 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((<any>store.getters).bestBeerName).toBe("bertinchamps");
expect((<any>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", () => { 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<any, any, any> {
inlineTemplate = `<div>
<span><t t-esc="props.activeTodoText"/></span>
<span><t t-esc="props.importantTodoText"/></span>
</div>`;
}
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<any, any, any> {
inlineTemplate = `<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" t-props="todo"/>
</t>
</div>`;
widgets = { ConnectedTodo };
}
function mapStateToProps(state) {
return { todos: state.todos };
}
const ConnectedTodoList = connect(mapStateToProps)(TodoList);
(<any>env).store = store;
const app = new ConnectedTodoList(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>jupiler</span><span>jupiler</span></div><div><span>bertinchamps</span><span>jupiler</span></div></div>");
});
test("connected component is updated when props are updated", async () => { test("connected component is updated when props are updated", async () => {
class Beer extends Component<any, any, any> { class Beer extends Component<any, any, any> {
inlineTemplate = `<span><t t-esc="props.name"/></span>`; inlineTemplate = `<span><t t-esc="props.name"/></span>`;