mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] connected component: add dispatch method
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(`
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user