mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
committed by
Géry Debongnie
parent
9a3e1ed99e
commit
1ff3c32fe4
+57
-3
@@ -28,7 +28,7 @@ Note: Owl's store is inspired by React Redux and VueX.
|
|||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
Here is what a simple store look like:
|
Here is what a simple store looks like:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const actions = {
|
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
|
tick, so only one event will be triggered for any number of state changes in a
|
||||||
call stack.
|
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
|
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
|
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
|
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
|
### Connecting a component
|
||||||
|
|
||||||
Todo
|
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
|
||||||
|
<button t-name="Counter" t-on-click="increment">
|
||||||
|
Click Me! [<t t-esc="props.value"/>]
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
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`
|
||||||
|
|||||||
+13
-10
@@ -155,6 +155,7 @@ let nextID = 1;
|
|||||||
|
|
||||||
export function connect(mapStateToProps, options: any = {}) {
|
export function connect(mapStateToProps, options: any = {}) {
|
||||||
let hashFunction = options.hashFunction || null;
|
let hashFunction = options.hashFunction || null;
|
||||||
|
const getStore = options.getStore || (env => env.store);
|
||||||
|
|
||||||
if (!hashFunction) {
|
if (!hashFunction) {
|
||||||
let deep = "deep" in options ? options.deep : true;
|
let deep = "deep" in options ? options.deep : true;
|
||||||
@@ -186,19 +187,21 @@ export function connect(mapStateToProps, options: any = {}) {
|
|||||||
const Result = class extends Comp {
|
const Result = class extends Comp {
|
||||||
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 store = getStore(env);
|
||||||
const ownProps = Object.assign({}, props || {});
|
const ownProps = Object.assign({}, props || {});
|
||||||
const storeProps = mapStateToProps(
|
const storeProps = mapStateToProps(
|
||||||
env.store.state,
|
store.state,
|
||||||
ownProps,
|
ownProps,
|
||||||
env.store.getters
|
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;
|
||||||
(<any>this.__owl__).currentStoreProps = storeProps;
|
(<any>this.__owl__).currentStoreProps = storeProps;
|
||||||
|
(<any>this.__owl__).store = store;
|
||||||
(<any>this.__owl__).storeHash = hashFunction(
|
(<any>this.__owl__).storeHash = hashFunction(
|
||||||
{
|
{
|
||||||
state: env.store.state,
|
state: store.state,
|
||||||
storeProps: storeProps,
|
storeProps: storeProps,
|
||||||
revNumber,
|
revNumber,
|
||||||
deepRevNumber
|
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.
|
* if we use the mounted hook, this will be done in the reverse order.
|
||||||
*/
|
*/
|
||||||
_callMounted() {
|
_callMounted() {
|
||||||
this.env.store.on("update", this, this._checkUpdate);
|
(<any>this.__owl__).store.on("update", this, this._checkUpdate);
|
||||||
super._callMounted();
|
super._callMounted();
|
||||||
}
|
}
|
||||||
willUnmount() {
|
willUnmount() {
|
||||||
this.env.store.off("update", this);
|
(<any>this.__owl__).store.off("update", this);
|
||||||
super.willUnmount();
|
super.willUnmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkUpdate() {
|
_checkUpdate() {
|
||||||
const ownProps = (<any>this.__owl__).ownProps;
|
const ownProps = (<any>this.__owl__).ownProps;
|
||||||
const storeProps = mapStateToProps(
|
const storeProps = mapStateToProps(
|
||||||
this.env.store.state,
|
(<any>this.__owl__).store.state,
|
||||||
ownProps,
|
ownProps,
|
||||||
this.env.store.getters
|
(<any>this.__owl__).store.getters
|
||||||
);
|
);
|
||||||
const options: any = {
|
const options: any = {
|
||||||
currentStoreProps: (<any>this.__owl__).currentStoreProps
|
currentStoreProps: (<any>this.__owl__).currentStoreProps
|
||||||
};
|
};
|
||||||
const storeHash = hashFunction(
|
const storeHash = hashFunction(
|
||||||
{
|
{
|
||||||
state: this.env.store.state,
|
state: (<any>this.__owl__).store.state,
|
||||||
storeProps: storeProps,
|
storeProps: storeProps,
|
||||||
revNumber,
|
revNumber,
|
||||||
deepRevNumber
|
deepRevNumber
|
||||||
@@ -254,9 +257,9 @@ export function connect(mapStateToProps, options: any = {}) {
|
|||||||
_updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
|
_updateProps(nextProps, forceUpdate, patchQueue?: 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,
|
(<any>this.__owl__).store.state,
|
||||||
nextProps,
|
nextProps,
|
||||||
this.env.store.getters
|
(<any>this.__owl__).store.getters
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
(<any>this.__owl__).ownProps = nextProps;
|
(<any>this.__owl__).ownProps = nextProps;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`connecting a component to store connecting a component to a local store 1`] = `"<div></div>"`;
|
||||||
|
|
||||||
|
exports[`connecting a component to store connecting a component to a local store 2`] = `"<div><span>hello</span></div>"`;
|
||||||
|
|
||||||
exports[`connecting a component to store connecting a component works 1`] = `"<div></div>"`;
|
exports[`connecting a component to store connecting a component works 1`] = `"<div></div>"`;
|
||||||
|
|
||||||
exports[`connecting a component to store connecting a component works 2`] = `"<div><span>hello</span></div>"`;
|
exports[`connecting a component to store connecting a component works 2`] = `"<div><span>hello</span></div>"`;
|
||||||
|
|||||||
@@ -476,6 +476,50 @@ describe("connecting a component to store", () => {
|
|||||||
expect(shallowFix.innerHTML).toMatchSnapshot();
|
expect(shallowFix.innerHTML).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("connecting a component to a local store", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="App">
|
||||||
|
<t t-foreach="props.todos" t-as="todo" t-key="todo">
|
||||||
|
<t t-widget="Todo" msg="todo.msg"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<span t-name="Todo"><t t-esc="props.msg"/></span>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
class App extends Component<any, any, any> {
|
||||||
|
widgets = { Todo };
|
||||||
|
}
|
||||||
|
class Todo extends Component<any, any, any> {}
|
||||||
|
|
||||||
|
(<any>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();
|
||||||
|
|
||||||
|
(<any>app.__owl__).store.commit("addTodo", "hello");
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
test("connected child components with custom hooks", async () => {
|
test("connected child components with custom hooks", async () => {
|
||||||
let steps: any = [];
|
let steps: any = [];
|
||||||
env.qweb.addTemplate("Child", `<div/>`);
|
env.qweb.addTemplate("Child", `<div/>`);
|
||||||
|
|||||||
@@ -384,11 +384,11 @@ class TodoItem extends owl.Component {
|
|||||||
state = { isEditing: false };
|
state = { isEditing: false };
|
||||||
|
|
||||||
removeTodo() {
|
removeTodo() {
|
||||||
this.env.store.dispatch("removeTodo", this.props.id);
|
this.env.dispatch("removeTodo", this.props.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTodo() {
|
toggleTodo() {
|
||||||
this.env.store.dispatch("toggleTodo", this.props.id);
|
this.env.dispatch("toggleTodo", this.props.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async editTodo() {
|
async editTodo() {
|
||||||
@@ -420,7 +420,7 @@ class TodoItem extends owl.Component {
|
|||||||
if (!value) {
|
if (!value) {
|
||||||
this.removeTodo(this.props.id);
|
this.removeTodo(this.props.id);
|
||||||
} else {
|
} else {
|
||||||
this.env.store.dispatch("editTodo", {
|
this.env.dispatch("editTodo", {
|
||||||
id: this.props.id,
|
id: this.props.id,
|
||||||
title: value
|
title: value
|
||||||
});
|
});
|
||||||
@@ -470,18 +470,18 @@ class TodoApp extends owl.Component {
|
|||||||
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.store.dispatch("addTodo", title);
|
this.env.dispatch("addTodo", title);
|
||||||
}
|
}
|
||||||
ev.target.value = "";
|
ev.target.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCompleted() {
|
clearCompleted() {
|
||||||
this.env.store.dispatch("clearCompleted");
|
this.env.dispatch("clearCompleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAll() {
|
toggleAll() {
|
||||||
this.env.store.dispatch("toggleAll", !this.allChecked);
|
this.env.dispatch("toggleAll", !this.allChecked);
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilter(filter) {
|
setFilter(filter) {
|
||||||
@@ -498,7 +498,8 @@ const store = makeStore();
|
|||||||
const qweb = new owl.QWeb(TEMPLATES);
|
const qweb = new owl.QWeb(TEMPLATES);
|
||||||
const env = {
|
const env = {
|
||||||
qweb,
|
qweb,
|
||||||
store
|
store,
|
||||||
|
dispatch: store.dispatch.bind(store),
|
||||||
};
|
};
|
||||||
const app = new ConnectedTodoApp(env);
|
const app = new ConnectedTodoApp(env);
|
||||||
app.mount(document.body);
|
app.mount(document.body);
|
||||||
|
|||||||
Reference in New Issue
Block a user