[IMP] connected component: add dispatch method

This commit is contained in:
Géry Debongnie
2019-08-26 10:02:56 +02:00
parent bb4c2506c0
commit c59abef374
4 changed files with 79 additions and 26 deletions
+19
View File
@@ -227,6 +227,25 @@ The `ConnectedComponent` class can be configured with the following fields:
- `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch - `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch
for top level state changes (`true` by default) for top level state changes (`true` by default)
Note that the class `ConnectedComponent` has a `dispatch` method. This means
that the previous example could be simplified like this:
```javascript
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
}
```
```xml
<button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="props.value"/>]
</button>
```
### Semantics ### Semantics
The `Store` and the `ConnectedComponent` try to be smart and to optimize as much The `Store` and the `ConnectedComponent` try to be smart and to optimize as much
+14 -6
View File
@@ -39,10 +39,18 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
return {}; return {};
} }
dispatch() {
const [name, ...payload] = arguments;
(this.__owl__ as any).store.dispatch(name, ...payload);
}
/** /**
* Need to do this here so 'deep' can be overrided by subcomponent easily * Need to do this here so 'deep' can be overrided by subcomponent easily
*/ */
async __prepareAndRender(scope?: Object, vars?: any): ReturnType<Component<any,any,any>["__prepareAndRender"]> { async __prepareAndRender(
scope?: Object,
vars?: any
): ReturnType<Component<any, any, any>["__prepareAndRender"]> {
const store = this.getStore(this.env); const store = this.getStore(this.env);
const ownProps = this.props || {}; const ownProps = this.props || {};
this.storeProps = (<any>this.constructor).mapStoreToProps(store.state, ownProps, store.getters); this.storeProps = (<any>this.constructor).mapStoreToProps(store.state, ownProps, store.getters);
@@ -87,22 +95,22 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
this.storeProps = storeProps; this.storeProps = storeProps;
let didChange = options.didChange; let didChange = options.didChange;
if (storeHash !== (this.__owl__ as any).storeHash) { if (storeHash !== (this.__owl__ as any).storeHash) {
(this.__owl__ as any).storeHash = storeHash; (this.__owl__ as any).storeHash = storeHash;
didChange = true; didChange = true;
} }
(this.__owl__ as any).rev = store.observer.rev; (this.__owl__ as any).rev = store.observer.rev;
return didChange return didChange;
} }
async __checkUpdate() { async __checkUpdate() {
const observer = (this.__owl__ as any).store.observer; const observer = (this.__owl__ as any).store.observer;
if (observer.rev === (this.__owl__ as any).rev) { if (observer.rev === (this.__owl__ as any).rev) {
// update was already done by updateProps, from parent // update was already done by updateProps, from parent
return; return;
} }
const didChange = this.__updateStoreProps(this.props); const didChange = this.__updateStoreProps(this.props);
if (didChange) { if (didChange) {
this.render(); this.render();
} }
} }
} }
+37
View File
@@ -140,6 +140,43 @@ describe("connecting a component to store", () => {
expect(fixture.innerHTML).toMatchSnapshot(); expect(fixture.innerHTML).toMatchSnapshot();
}); });
test("can dispatch actions from a connected component", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<button t-on-click="dispatch('inc')">Inc</button>
<span><t t-esc="storeProps.value"/></span>
</div>
</templates>
`);
const store = new Store({
state: { value: 1 },
actions: {
inc({ state }) {
state.value++;
}
}
});
(<any>env).store = store;
class App extends ConnectedComponent<any, any, any> {
static mapStoreToProps(s) {
return { value: s.value };
}
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>Inc</button><span>1</span></div>");
const button = (<HTMLElement>app.el).getElementsByTagName("button")[0];
await button.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>Inc</button><span>2</span></div>");
});
test("connected child components with custom hooks", async () => { test("connected child components with custom hooks", async () => {
let steps: any = []; let steps: any = [];
env.qweb.addTemplates(` env.qweb.addTemplates(`
+9 -20
View File
@@ -275,13 +275,11 @@ const LOCALSTORAGE_KEY = "todos-odoo";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const actions = { const actions = {
addTodo({ state }, title) { addTodo({ state }, title) {
const id = state.nextId++; state.todos.push({
const todo = { id: state.nextId++,
id,
title, title,
completed: false completed: false
}; });
state.todos.push(todo);
}, },
removeTodo({ state }, id) { removeTodo({ state }, id) {
const index = state.todos.findIndex(t => t.id === id); const index = state.todos.findIndex(t => t.id === id);
@@ -298,7 +296,7 @@ const actions = {
dispatch("removeTodo", todo.id); dispatch("removeTodo", todo.id);
}); });
}, },
toggleAll({ state, commit }, completed) { toggleAll({ state, dispatch }, completed) {
state.todos.forEach(todo => { state.todos.forEach(todo => {
dispatch("editTodo", { dispatch("editTodo", {
id: todo.id, id: todo.id,
@@ -341,11 +339,11 @@ class TodoItem extends owl.Component {
state = { isEditing: false }; state = { isEditing: false };
removeTodo() { removeTodo() {
this.env.dispatch("removeTodo", this.props.id); this.env.store.dispatch("removeTodo", this.props.id);
} }
toggleTodo() { toggleTodo() {
this.env.dispatch("toggleTodo", this.props.id); this.env.store.dispatch("toggleTodo", this.props.id);
} }
async editTodo() { async editTodo() {
@@ -426,20 +424,12 @@ class TodoApp extends owl.store.ConnectedComponent {
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.dispatch("addTodo", title); this.dispatch("addTodo", title);
} }
ev.target.value = ""; ev.target.value = "";
} }
} }
clearCompleted() {
this.env.dispatch("clearCompleted");
}
toggleAll() {
this.env.dispatch("toggleAll", !this.allChecked);
}
setFilter(filter) { setFilter(filter) {
this.state.filter = filter; this.state.filter = filter;
} }
@@ -453,7 +443,6 @@ const qweb = new owl.QWeb(TEMPLATES);
const env = { const env = {
qweb, qweb,
store, store,
dispatch: store.dispatch.bind(store),
}; };
const app = new TodoApp(env); const app = new TodoApp(env);
app.mount(document.body); app.mount(document.body);
@@ -466,7 +455,7 @@ const TODO_APP_STORE_XML = `<templates>
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/> <input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
</header> </header>
<section class="main" t-if="storeProps.todos.length"> <section class="main" t-if="storeProps.todos.length">
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="toggleAll"/> <input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="dispatch('toggleAll', !allChecked)"/>
<label for="toggle-all"></label> <label for="toggle-all"></label>
<ul class="todo-list"> <ul class="todo-list">
<t t-foreach="visibleTodos" t-as="todo"> <t t-foreach="visibleTodos" t-as="todo">
@@ -492,7 +481,7 @@ const TODO_APP_STORE_XML = `<templates>
<a t-on-click="setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a> <a t-on-click="setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
</li> </li>
</ul> </ul>
<button class="clear-completed" t-if="storeProps.todos.length gt remaining" t-on-click="clearCompleted"> <button class="clear-completed" t-if="storeProps.todos.length gt remaining" t-on-click="dispatch('clearCompleted')">
Clear completed Clear completed
</button> </button>
</footer> </footer>