[REF] store: remove connect function, add ConnectedComponent

closes #238
closes #235
This commit is contained in:
Géry Debongnie
2019-07-12 15:22:14 +02:00
parent 591508e769
commit 545ceefc3d
5 changed files with 365 additions and 479 deletions
+26 -33
View File
@@ -89,7 +89,7 @@ mutation is not allowed (and should throw an error). Mutations are synchronous.
```js ```js
const mutations = { const mutations = {
setLoginState({ state }, loginState) { setLoginState({ state }, loginState) {
state.loginState = loginState; state.loginState = loginState;
} }
}; };
``` ```
@@ -165,17 +165,16 @@ const getters = {
const post = store.getters.getPost(id); const post = store.getters.getPost(id);
``` ```
Getters take *at most* one argument. Getters take _at most_ one argument.
Note that getters are cached if they don't take any argument, or their argument Note that getters are cached if they don't take any argument, or their argument
is a string or a number. is a string or a number.
### Connecting a Component ### Connecting a Component
By default, an Owl `Component` is not connected to any store. The `connect` At some point, we need a way to access the state in the store from a component.
function is there to create sub Components that are connected versions of By default, an Owl `Component` is not connected to any store. To do that, we
Components. need to create a component inheriting from `OwlComponent`:
```javascript ```javascript
const actions = { const actions = {
@@ -193,19 +192,18 @@ const state = {
}; };
const store = new owl.Store({ state, actions, mutations }); const store = new owl.Store({ state, actions, mutations });
class Counter extends owl.Component { class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
increment() { increment() {
this.env.store.dispatch("increment"); this.env.store.dispatch("increment");
} }
} }
function mapStoreToProps(state) {
return {
value: state.counter
};
}
const ConnectedCounter = owl.connect(Counter, mapStoreToProps);
const counter = new ConnectedCounter({ store, qweb }); const counter = new Counter({ store, qweb });
``` ```
```xml ```xml
@@ -214,45 +212,40 @@ const counter = new ConnectedCounter({ store, qweb });
</button> </button>
``` ```
The arguments of `connect` are: The `ConnectedComponent` class can be configured with the following fields:
- `Counter`: an owl `Component` to connect
- `mapStoreToProps`: a function that extracts the `props` of the Component - `mapStoreToProps`: a function that extracts the `props` of the Component
from the `state` of the `Store` and returns them as a dict 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
- `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`)
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
- `hashFunction`: the function to use to detect changes in the state (if not given, generates a function that uses revision numbers, incremented at
given, generates a function that uses revision numbers, incremented at each state change)
each state change) - `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch
- `deep`: [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)
The `connect` function returns a sub class of the given `Component` which is
connected to the `store`.
### Semantics ### Semantics
The `Store` and the `connect` function try to be smart and to optimize as much The `Store` and the `ConnectedComponent` try to be smart and to optimize as much
as possible the rendering and update process. What is important to know is: as possible the rendering and update process. What is important to know is:
- components are always updated in the order of their creation (so, parent - components are always updated in the order of their creation (so, parent
before children) before children)
- they are updated only if they are in the DOM - they are updated only if they are in the DOM
- if a parent is asynchronous, the system will wait for it to complete its - if a parent is asynchronous, the system will wait for it to complete its
update before updating other components. update before updating other components.
- in general, updates are not coordinated. This is not a problem for synchronous - in general, updates are not coordinated. This is not a problem for synchronous
components, but if there are many asynchronous components, this could lead to components, but if there are many asynchronous components, this could lead to
a situation where some part of the UI is updated and other parts of the UI is a situation where some part of the UI is updated and other parts of the UI is
not updated. not updated.
### Good Practices ### Good Practices
- avoid asynchronous components as much as possible. Asynchronous components - avoid asynchronous components as much as possible. Asynchronous components
lead to situations where parts of the UI is not updated immediately. lead to situations where parts of the UI is not updated immediately.
- do not be afraid to connect many components, parent or children if needed. For - do not be afraid to connect many components, parent or children if needed. For
example, a `MessageList` component could get a list of ids in its `mapStoreToProps` and a `Message` component could get the data of its own example, a `MessageList` component could get a list of ids in its `mapStoreToProps` and a `Message` component could get the data of its own
message message
- since the `mapStoreToProps` function is called for each connected component, - since the `mapStoreToProps` function is called for each connected component,
for each state update, it is important to make sure that these functions are for each state update, it is important to make sure that these functions are
as fast as possible. as fast as possible.
+1 -1
View File
@@ -15,7 +15,7 @@ import "./qweb_extensions";
import { QWeb } from "./qweb_core"; import { QWeb } from "./qweb_core";
export { QWeb }; export { QWeb };
export { connect, Store } from "./store"; export { Store, ConnectedComponent } from "./store";
import * as _utils from "./utils"; import * as _utils from "./utils";
export const __info__ = {}; export const __info__ = {};
+110 -127
View File
@@ -7,7 +7,7 @@ import { Observer } from "./observer";
* *
* We have here: * We have here:
* - a Store class * - a Store class
* - a connect function * - the ConnectedComponent class
* *
* The Owl store is our answer to the problem of managing complex state across * The Owl store is our answer to the problem of managing complex state across
* components. The main idea is that the store owns some state, allow external * components. The main idea is that the store owns some state, allow external
@@ -185,139 +185,122 @@ function deepRevNumber<T extends Object>(o: T): number {
return 0; return 0;
} }
type Constructor<T> = new (...args: any[]) => T;
interface EnvWithStore extends Env {
store: Store;
}
type HashFunction = (a: any, b: any) => number; type HashFunction = (a: any, b: any) => number;
interface StoreOptions {
getStore?(Env): Store;
hashFunction?: HashFunction;
deep?: boolean;
}
export function connect<E extends EnvWithStore, P, S>( export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> {
Comp: Constructor<Component<E, P, S>>, deep: boolean = true;
mapStoreToProps, getStore(env) {
options: StoreOptions = <StoreOptions>{} return env.store;
) {
let hashFunction = options.hashFunction || null;
const getStore = options.getStore || (env => env.store);
if (!hashFunction) {
let deep = "deep" in options ? options.deep : true;
let defaultRevFunction = deep ? deepRevNumber : revNumber;
hashFunction = function({ storeProps }, options) {
const { currentStoreProps } = options;
if ("__owl__" in storeProps) {
return defaultRevFunction(storeProps);
}
let hash = 0;
for (let key in storeProps) {
const val = storeProps[key];
const hashVal = defaultRevFunction(val);
if (hashVal === 0) {
if (val !== currentStoreProps[key]) {
options.didChange = true;
}
} else {
hash += hashVal;
}
}
return hash;
};
} }
const Result = class extends Comp { hashFunction: HashFunction = ({ storeProps }, options) => {
constructor(parent, props?: any) { let refFunction = this.deep ? deepRevNumber : revNumber;
const env = parent instanceof Component ? parent.env : parent; if ("__owl__" in storeProps) {
const store = getStore(env); return refFunction(storeProps);
const ownProps = Object.assign({}, props || {}); }
const storeProps = mapStoreToProps(store.state, ownProps, store.getters); const { currentStoreProps } = options;
const mergedProps = Object.assign({}, props || {}, storeProps); let hash = 0;
super(parent, mergedProps); for (let key in storeProps) {
(<any>this.__owl__).ownProps = ownProps; const val = storeProps[key];
(<any>this.__owl__).currentStoreProps = storeProps; const hashVal = refFunction(val);
(<any>this.__owl__).store = store; if (hashVal === 0) {
(<any>this.__owl__).storeHash = (<HashFunction>hashFunction)( if (val !== currentStoreProps[key]) {
{ options.didChange = true;
state: store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
{
currentStoreProps: storeProps
} }
); } else {
} hash += hashVal;
/**
* We do not use the mounted hook here for a subtle reason: we want the
* updates to be called for the parents before the children. However,
* if we use the mounted hook, this will be done in the reverse order.
*/
__callMounted() {
(<any>this.__owl__).store.on("update", this, this.__checkUpdate);
super.__callMounted();
}
willUnmount() {
(<any>this.__owl__).store.off("update", this);
super.willUnmount();
}
async __checkUpdate(updateId) {
if (updateId === (<any>this.__owl__).currentUpdateId) {
return;
}
const ownProps = (<any>this.__owl__).ownProps;
const storeProps = mapStoreToProps(
(<any>this.__owl__).store.state,
ownProps,
(<any>this.__owl__).store.getters
);
const options: any = {
currentStoreProps: (<any>this.__owl__).currentStoreProps
};
const storeHash = (<HashFunction>hashFunction)(
{
state: (<any>this.__owl__).store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
options
);
let didChange = options.didChange;
if (storeHash !== (<any>this.__owl__).storeHash) {
didChange = true;
(<any>this.__owl__).storeHash = storeHash;
}
if (didChange) {
(<any>this.__owl__).currentStoreProps = storeProps;
await this.__updateProps(ownProps, false);
} }
} }
__updateProps(nextProps, forceUpdate, patchQueue?: any[]) { return hash;
const __owl__ = <any>this.__owl__;
__owl__.currentUpdateId = __owl__.store._updateId;
if (__owl__.ownProps !== nextProps) {
__owl__.currentStoreProps = mapStoreToProps(
__owl__.store.state,
nextProps,
__owl__.store.getters
);
}
__owl__.ownProps = nextProps;
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
return super.__updateProps(mergedProps, forceUpdate, patchQueue);
}
}; };
// we assign here a unique name to the resulting anonymous class. static mapStoreToProps(storeState, ownProps, getters) {
// this is necessary for Owl to be able to properly deduce templates. return {};
// Otherwise, all connected components would have the same name, and then }
// each component after the first will necessarily have the same template. constructor(parent, props?: any) {
let name = `Connected${Comp.name}`; super(parent, props);
Object.defineProperty(Result, "name", { value: name }); const store = this.getStore(this.env);
return Result; const ownProps = Object.assign({}, props || {});
const storeProps = (<any>this.constructor).mapStoreToProps(
store.state,
ownProps,
store.getters
);
const mergedProps = Object.assign({}, props || {}, storeProps);
this.props = mergedProps;
(<any>this.__owl__).ownProps = ownProps;
(<any>this.__owl__).currentStoreProps = storeProps;
(<any>this.__owl__).store = store;
(<any>this.__owl__).storeHash = this.hashFunction(
{
state: store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
{
currentStoreProps: storeProps
}
);
}
/**
* We do not use the mounted hook here for a subtle reason: we want the
* updates to be called for the parents before the children. However,
* if we use the mounted hook, this will be done in the reverse order.
*/
__callMounted() {
(<any>this.__owl__).store.on("update", this, this.__checkUpdate);
super.__callMounted();
}
willUnmount() {
(<any>this.__owl__).store.off("update", this);
super.willUnmount();
}
async __checkUpdate(updateId) {
if (updateId === (<any>this.__owl__).currentUpdateId) {
return;
}
const ownProps = (<any>this.__owl__).ownProps;
const storeProps = (<any>this.constructor).mapStoreToProps(
(<any>this.__owl__).store.state,
ownProps,
(<any>this.__owl__).store.getters
);
const options: any = {
currentStoreProps: (<any>this.__owl__).currentStoreProps
};
const storeHash = this.hashFunction(
{
state: (<any>this.__owl__).store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
options
);
let didChange = options.didChange;
if (storeHash !== (<any>this.__owl__).storeHash) {
didChange = true;
(<any>this.__owl__).storeHash = storeHash;
}
if (didChange) {
(<any>this.__owl__).currentStoreProps = storeProps;
await this.__updateProps(ownProps, false);
}
}
__updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
const __owl__ = <any>this.__owl__;
__owl__.currentUpdateId = __owl__.store._updateId;
if (__owl__.ownProps !== nextProps) {
__owl__.currentStoreProps = (<any>this.constructor).mapStoreToProps(
__owl__.store.state,
nextProps,
__owl__.store.getters
);
}
__owl__.ownProps = nextProps;
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
return super.__updateProps(mergedProps, forceUpdate, patchQueue);
}
} }
+221 -308
View File
@@ -1,5 +1,5 @@
import { Component, Env } from "../src/component"; import { Component, Env } from "../src/component";
import { connect, Store } from "../src/store"; import { Store, ConnectedComponent } from "../src/store";
import { makeTestFixture, makeTestEnv, nextMicroTick, nextTick } from "./helpers"; import { makeTestFixture, makeTestEnv, nextMicroTick, nextTick } from "./helpers";
import { Observer } from "../src"; import { Observer } from "../src";
@@ -561,18 +561,21 @@ describe("connecting a component to store", () => {
}); });
test("connecting a component works", async () => { test("connecting a component works", async () => {
env.qweb.addTemplate( env.qweb.addTemplates(`
"App", <templates>
` <div t-name="App">
<div>
<t t-foreach="props.todos" t-as="todo" > <t t-foreach="props.todos" t-as="todo" >
<Todo msg="todo.msg" t-key="todo"/> <Todo msg="todo.msg" t-key="todo"/>
</t> </t>
</div>` </div>
); <span t-name="Todo"><t t-esc="props.msg"/></span>
env.qweb.addTemplate("Todo", `<span><t t-esc="props.msg"/></span>`); </templates>
class App extends Component<any, any, any> { `);
class App extends ConnectedComponent<any, any, any> {
components = { Todo }; components = { Todo };
static mapStoreToProps(s) {
return { todos: s.todos };
}
} }
class Todo extends Component<any, any, any> {} class Todo extends Component<any, any, any> {}
const state = { todos: [] }; const state = { todos: [] };
@@ -581,16 +584,9 @@ describe("connecting a component to store", () => {
state.todos.push({ msg }); state.todos.push({ msg });
} }
}; };
function mapStoreToProps(s) {
return { todos: s.todos };
}
const TodoApp = connect(
App,
mapStoreToProps
);
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
(<any>env).store = store; (<any>env).store = store;
const app = new TodoApp(env); const app = new App(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot(); expect(fixture.innerHTML).toMatchSnapshot();
@@ -601,38 +597,35 @@ describe("connecting a component to store", () => {
}); });
test("deep and shallow connecting a component", async () => { test("deep and shallow connecting a component", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<span t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-esc="todo.title"/>
</span>
</div>
</templates>
`);
const state = { todos: [{ title: "Kasteel" }] }; const state = { todos: [{ title: "Kasteel" }] };
const mutations = { const mutations = {
edit({ state }, title) { edit({ state }, title) {
state.todos[0].title = title; state.todos[0].title = title;
} }
}; };
function mapStoreToProps(s) {
return { todos: s.todos };
}
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
env.qweb.addTemplate( class App extends ConnectedComponent<any, any, any> {
"App", static mapStoreToProps(s) {
` return { todos: s.todos };
<div> }
<span t-foreach="props.todos" t-as="todo" t-key="todo"> }
<t t-esc="todo.title"/> class DeepTodoApp extends App {
</span> deep = true;
</div>` }
); class ShallowTodoApp extends App {
class App extends Component<any, any, any> {} deep = false;
}
const DeepTodoApp = connect(
App,
mapStoreToProps,
{ deep: true }
);
const ShallowTodoApp = connect(
App,
mapStoreToProps,
{ deep: false }
);
(<any>env).store = store; (<any>env).store = store;
const deepTodoApp = new DeepTodoApp(env); const deepTodoApp = new DeepTodoApp(env);
const shallowTodoApp = new ShallowTodoApp(env); const shallowTodoApp = new ShallowTodoApp(env);
@@ -662,9 +655,6 @@ describe("connecting a component to store", () => {
<span t-name="Todo"><t t-esc="props.msg"/></span> <span t-name="Todo"><t t-esc="props.msg"/></span>
</templates> </templates>
`); `);
class App extends Component<any, any, any> {
components = { Todo };
}
class Todo extends Component<any, any, any> {} class Todo extends Component<any, any, any> {}
(<any>env).store = new Store({}); (<any>env).store = new Store({});
@@ -676,17 +666,16 @@ describe("connecting a component to store", () => {
} }
} }
}); });
function mapStoreToProps(s) { class App extends ConnectedComponent<any, any, any> {
return { todos: s.todos }; components = { Todo };
} static mapStoreToProps(s) {
const TodoApp = connect( return { todos: s.todos };
App,
mapStoreToProps,
{
getStore: () => store
} }
); getStore() {
const app = new TodoApp(env); return store;
}
}
const app = new App(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot(); expect(fixture.innerHTML).toMatchSnapshot();
@@ -698,8 +687,18 @@ describe("connecting a component to store", () => {
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.addTemplates(`
class Child extends Component<any, any, any> { <templates>
<div t-name="Parent">
<Child t-if="state.child" />
</div>
<div t-name="Child"/>
</templates>
`);
class Child extends ConnectedComponent<any, any, any> {
static mapStoreToProps(s) {
return s;
}
mounted() { mounted() {
steps.push("child:mounted"); steps.push("child:mounted");
} }
@@ -708,20 +707,8 @@ describe("connecting a component to store", () => {
} }
} }
const ConnectedChild = connect(
Child,
s => s
);
env.qweb.addTemplate(
"Parent",
`
<div>
<t t-if="state.child" t-component="ConnectedChild"/>
</div>`
);
class Parent extends Component<any, any, any> { class Parent extends Component<any, any, any> {
components = { ConnectedChild }; components = { Child };
constructor(env: Env) { constructor(env: Env) {
super(env); super(env);
@@ -741,7 +728,7 @@ describe("connecting a component to store", () => {
expect(steps).toEqual(["child:mounted", "child:willUnmount"]); expect(steps).toEqual(["child:mounted", "child:willUnmount"]);
}); });
test("connect receives ownprops as second argument", async () => { test("mapStoreToProps receives ownprops as second argument", async () => {
const state = { todos: [{ id: 1, text: "jupiler" }] }; const state = { todos: [{ id: 1, text: "jupiler" }] };
let nextId = 2; let nextId = 2;
const mutations = { const mutations = {
@@ -751,38 +738,32 @@ describe("connecting a component to store", () => {
}; };
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
env.qweb.addTemplate("TodoItem", `<span><t t-esc="props.text"/></span>`); env.qweb.addTemplates(`
class TodoItem extends Component<any, any, any> {} <templates>
const ConnectedTodo = connect( <span t-name="TodoItem"><t t-esc="props.text"/></span>
TodoItem, <div t-name="TodoList">
(state, props) => { <t t-foreach="props.todos" t-as="todo">
<TodoItem id="todo.id" t-key="todo.id"/>
</t>
</div>
</templates>
`);
class TodoItem extends ConnectedComponent<any, any, any> {
static mapStoreToProps(state, props) {
const todo = state.todos.find(t => t.id === props.id); const todo = state.todos.find(t => t.id === props.id);
return todo; return todo;
} }
);
env.qweb.addTemplate(
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<ConnectedTodo id="todo.id" t-key="todo.id"/>
</t>
</div>`
);
class TodoList extends Component<any, any, any> {
components = { ConnectedTodo };
} }
function mapStoreToProps(state) { class TodoList extends ConnectedComponent<any, any, any> {
return { todos: state.todos }; components = { TodoItem };
static mapStoreToProps(state) {
return { todos: state.todos };
}
} }
const ConnectedTodoList = connect(
TodoList,
mapStoreToProps
);
(<any>env).store = store; (<any>env).store = store;
const app = new ConnectedTodoList(env); const app = new TodoList(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>"); expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
@@ -792,7 +773,7 @@ describe("connecting a component to store", () => {
expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>hoegaarden</span></div>"); expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>hoegaarden</span></div>");
}); });
test("connect receives store getters as third argument", async () => { test("mapStoreToProps receives store getters as third argument", async () => {
const state = { const state = {
importantID: 1, importantID: 1,
todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }] todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }]
@@ -807,47 +788,39 @@ describe("connecting a component to store", () => {
}; };
const store = new Store({ state, getters }); const store = new Store({ state, getters });
env.qweb.addTemplate( env.qweb.addTemplates(`
"TodoItem", <templates>
`<div> <div t-name="TodoItem">
<span><t t-esc="props.activeTodoText"/></span> <span><t t-esc="props.activeTodoText"/></span>
<span><t t-esc="props.importantTodoText"/></span> <span><t t-esc="props.importantTodoText"/></span>
</div>` </div>
); <div t-name="TodoList">
class TodoItem extends Component<any, any, any> {} <t t-foreach="props.todos" t-as="todo">
const ConnectedTodo = connect( <TodoItem id="todo.id" t-key="todo.id"/>
TodoItem, </t>
(state, props, getters) => { </div>
</templates>
`);
class TodoItem extends ConnectedComponent<any, any, any> {
static mapStoreToProps(state, props, getters) {
const todo = state.todos.find(t => t.id === props.id); const todo = state.todos.find(t => t.id === props.id);
return { return {
activeTodoText: getters.text(todo.id), activeTodoText: getters.text(todo.id),
importantTodoText: getters.importantTodoText() importantTodoText: getters.importantTodoText()
}; };
} }
);
env.qweb.addTemplate(
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<ConnectedTodo id="todo.id" t-key="todo.id"/>
</t>
</div>`
);
class TodoList extends Component<any, any, any> {
components = { ConnectedTodo };
} }
function mapStoreToProps(state) { class TodoList extends ConnectedComponent<any, any, any> {
return { todos: state.todos }; components = { TodoItem };
static mapStoreToProps(state) {
return { todos: state.todos };
}
} }
const ConnectedTodoList = connect(
TodoList,
mapStoreToProps
);
(<any>env).store = store; (<any>env).store = store;
const app = new ConnectedTodoList(env); const app = new TodoList(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
@@ -856,23 +829,23 @@ describe("connecting a component to store", () => {
}); });
test("connected component is updated when props are updated", async () => { test("connected component is updated when props are updated", async () => {
env.qweb.addTemplate("Beer", `<span><t t-esc="props.name"/></span>`); env.qweb.addTemplates(`
class Beer extends Component<any, any, any> {} <templates>
const ConnectedBeer = connect( <span t-name="Beer"><t t-esc="props.name"/></span>
Beer, <div t-name="App">
(state, props) => { <Beer id="state.beerId"/>
</div>
</templates>
`);
class Beer extends ConnectedComponent<any, any, any> {
static mapStoreToProps(state, props) {
return state.beers[props.id]; return state.beers[props.id];
} }
); }
env.qweb.addTemplate(
"App",
`<div>
<ConnectedBeer id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> { class App extends Component<any, any, any> {
components = { ConnectedBeer }; components = { Beer };
state = { beerId: 1 }; state = { beerId: 1 };
} }
@@ -890,14 +863,19 @@ describe("connecting a component to store", () => {
}); });
test("connected component is updated when store is changed", async () => { test("connected component is updated when store is changed", async () => {
env.qweb.addTemplate( env.qweb.addTemplates(`
"App", <templates>
` <div t-name="App">
<div>
<span t-foreach="props.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span> <span t-foreach="props.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span>
</div>` </div>
); </templates>
class App extends Component<any, any, any> {} `);
class App extends ConnectedComponent<any, any, any> {
static mapStoreToProps(state) {
return { beers: state.beers, otherKey: 1 };
}
}
const mutations = { const mutations = {
addBeer({ state }, name) { addBeer({ state }, name) {
@@ -909,14 +887,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
(<any>env).store = store; (<any>env).store = store;
function mapStoreToProps(state) { const app = new App(env);
return { beers: state.beers, otherKey: 1 };
}
const ConnectedApp = connect(
App,
mapStoreToProps
);
const app = new ConnectedApp(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>"); expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
@@ -927,34 +898,31 @@ describe("connecting a component to store", () => {
}); });
test("connected component with undefined, null and string props", async () => { test("connected component with undefined, null and string props", async () => {
env.qweb.addTemplate( env.qweb.addTemplates(`
"Beer", <templates>
`<div> <div t-name="Beer">
<span>taster:<t t-esc="props.taster"/></span> <span>taster:<t t-esc="props.taster"/></span>
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span> <span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span> <span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>` </div>
); <div t-name="App">
class Beer extends Component<any, any, any> {} <Beer id="state.beerId"/>
const ConnectedBeer = connect( </div>
Beer, </templates>
(state, props) => { `);
class Beer extends ConnectedComponent<any, any, any> {
static mapStoreToProps(state, props) {
return { return {
selected: state.beers[props.id], selected: state.beers[props.id],
consumed: state.beers[state.consumedID] || null, consumed: state.beers[state.consumedID] || null,
taster: state.taster taster: state.taster
}; };
} }
); }
env.qweb.addTemplate(
"App",
`<div>
<ConnectedBeer id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> { class App extends Component<any, any, any> {
components = { ConnectedBeer }; components = { Beer };
state = { beerId: 0 }; state = { beerId: 0 };
} }
@@ -997,34 +965,31 @@ describe("connecting a component to store", () => {
}); });
test("connected component deeply reactive with undefined, null and string props", async () => { test("connected component deeply reactive with undefined, null and string props", async () => {
env.qweb.addTemplate( env.qweb.addTemplates(`
"Beer", <templates>
`<div> <div t-name="Beer">
<span>taster:<t t-esc="props.taster"/></span> <span>taster:<t t-esc="props.taster"/></span>
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span> <span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span> <span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>` </div>
); <div t-name="App">
class Beer extends Component<any, any, any> {} <Beer id="state.beerId"/>
const ConnectedBeer = connect( </div>
Beer, </templates>
(state, props) => { `);
class Beer extends ConnectedComponent<any, any, any> {
static mapStoreToProps(storeState, props) {
return { return {
selected: state.beers[props.id], selected: storeState.beers[props.id],
consumed: state.beers[state.consumedID] || null, consumed: storeState.beers[storeState.consumedID] || null,
taster: state.taster taster: storeState.taster
}; };
} }
); }
env.qweb.addTemplate(
"App",
`<div>
<ConnectedBeer id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> { class App extends Component<any, any, any> {
components = { ConnectedBeer }; components = { Beer };
state = { beerId: 0 }; state = { beerId: 0 };
} }
@@ -1093,35 +1058,29 @@ describe("connecting a component to store", () => {
test("correct update order when parent/children are connected", async () => { test("correct update order when parent/children are connected", async () => {
const steps: string[] = []; const steps: string[] = [];
env.qweb.addTemplate( env.qweb.addTemplates(`
"Parent", <templates>
` <div t-name="Parent">
<div> <Child key="props.current"/>
<Child key="props.current"/> </div>
</div> <span t-name="Child"><t t-esc="props.msg"/></span>
` </templates>
); `);
class Parent extends Component<any, any, any> {
components = { Child: ConnectedChild }; class Parent extends ConnectedComponent<any, any, any> {
} components = { Child };
const ConnectedParent = connect( static mapStoreToProps(s) {
Parent,
function(s) {
steps.push("parent"); steps.push("parent");
return { current: s.current, isvisible: s.isvisible }; return { current: s.current, isvisible: s.isvisible };
} }
); }
env.qweb.addTemplate("Child", `<span><t t-esc="props.msg"/></span>`); class Child extends ConnectedComponent<any, any, any> {
class Child extends Component<any, any, any> {} static mapStoreToProps(s, props) {
const ConnectedChild = connect(
Child,
function(s, props) {
steps.push("child"); steps.push("child");
return { msg: s.msg[props.key] }; return { msg: s.msg[props.key] };
} }
); }
const state = { current: "a", msg: { a: "a", b: "b" } }; const state = { current: "a", msg: { a: "a", b: "b" } };
const mutations = { const mutations = {
@@ -1132,7 +1091,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
(<any>env).store = store; (<any>env).store = store;
const app = new ConnectedParent(env); const app = new Parent(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>a</span></div>"); expect(fixture.innerHTML).toBe("<div><span>a</span></div>");
@@ -1163,7 +1122,7 @@ describe("connecting a component to store", () => {
<templates> <templates>
<div t-name="TodoApp" class="todoapp"> <div t-name="TodoApp" class="todoapp">
<t t-foreach="Object.values(props.todos)" t-as="todo"> <t t-foreach="Object.values(props.todos)" t-as="todo">
<ConnectedTodoItem t-key="todo.id" id="todo.id"/> <TodoItem t-key="todo.id" id="todo.id"/>
</t> </t>
</div> </div>
@@ -1174,33 +1133,26 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
function mapStoreToPropsTodoApp(state) { class TodoApp extends ConnectedComponent<any, any, any> {
return { components = { TodoItem };
todos: state.todos static mapStoreToProps(state) {
}; return {
todos: state.todos
};
}
} }
class TodoApp extends Component<any, any, any> {
components = { ConnectedTodoItem };
}
const ConnectedTodoApp = connect(
TodoApp,
mapStoreToPropsTodoApp
);
let renderCount = 0; let renderCount = 0;
let fCount = 0; let fCount = 0;
function mapStoreToPropsTodoItem(state, ownProps) { class TodoItem extends ConnectedComponent<any, any, any> {
fCount++;
return {
todo: state.todos[ownProps.id]
};
}
class TodoItem extends Component<any, any, any> {
state = { isEditing: false }; state = { isEditing: false };
static mapStoreToProps(state, ownProps) {
fCount++;
return {
todo: state.todos[ownProps.id]
};
}
editTodo() { editTodo() {
this.env.store.commit("editTodo"); this.env.store.commit("editTodo");
@@ -1211,13 +1163,8 @@ describe("connecting a component to store", () => {
} }
} }
const ConnectedTodoItem = connect(
TodoItem,
mapStoreToPropsTodoItem
);
(<any>env).store = store; (<any>env).store = store;
const app = new ConnectedTodoApp(env); const app = new TodoApp(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
@@ -1254,7 +1201,7 @@ describe("connecting a component to store", () => {
<templates> <templates>
<div t-name="TodoApp" class="todoapp"> <div t-name="TodoApp" class="todoapp">
<t t-foreach="Object.values(props.todos)" t-as="todo"> <t t-foreach="Object.values(props.todos)" t-as="todo">
<ConnectedTodoItem t-key="todo.id" id="todo.id"/> <TodoItem t-key="todo.id" id="todo.id"/>
</t> </t>
</div> </div>
@@ -1265,34 +1212,27 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
function mapStoreToPropsTodoApp(state) { class TodoApp extends ConnectedComponent<any, any, any> {
return { components = { TodoItem };
todos: state.todos static mapStoreToProps(state) {
}; return {
todos: state.todos
};
}
} }
class TodoApp extends Component<any, any, any> {
components = { ConnectedTodoItem };
}
const ConnectedTodoApp = connect(
TodoApp,
mapStoreToPropsTodoApp
);
let renderCount = 0; let renderCount = 0;
let fCount = 0; let fCount = 0;
function mapStoreToPropsTodoItem(state, ownProps) { class TodoItem extends ConnectedComponent<any, any, any> {
fCount++;
return {
todo: state.todos[ownProps.id]
};
}
class TodoItem extends Component<any, any, any> {
state = { isEditing: false }; state = { isEditing: false };
static mapStoreToProps(state, ownProps) {
fCount++;
return {
todo: state.todos[ownProps.id]
};
}
removeTodo() { removeTodo() {
this.env.store.commit("removeTodo"); this.env.store.commit("removeTodo");
} }
@@ -1302,13 +1242,8 @@ describe("connecting a component to store", () => {
} }
} }
const ConnectedTodoItem = connect(
TodoItem,
mapStoreToPropsTodoItem
);
(<any>env).store = store; (<any>env).store = store;
const app = new ConnectedTodoApp(env); const app = new TodoApp(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
@@ -1326,8 +1261,17 @@ describe("connecting a component to store", () => {
test("connected component willpatch/patch hooks are called on store updates", async () => { test("connected component willpatch/patch hooks are called on store updates", async () => {
const steps: string[] = []; const steps: string[] = [];
env.qweb.addTemplate("App", `<div><t t-esc="props.msg"/></div>`);
class App extends Component<any, any, any> { env.qweb.addTemplates(`
<templates>
<div t-name="App"><t t-esc="props.msg"/></div>
</templates>
`);
class App extends ConnectedComponent<any, any, any> {
static mapStoreToProps(s) {
return { msg: s.msg };
}
willPatch() { willPatch() {
steps.push("willpatch"); steps.push("willpatch");
} }
@@ -1335,12 +1279,6 @@ describe("connecting a component to store", () => {
steps.push("patched"); steps.push("patched");
} }
} }
const ConnectedApp = connect(
App,
function(s) {
return { msg: s.msg };
}
);
const state = { msg: "a" }; const state = { msg: "a" };
const mutations = { const mutations = {
@@ -1351,7 +1289,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
(<any>env).store = store; (<any>env).store = store;
const app = new ConnectedApp(env); const app = new App(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>a</div>"); expect(fixture.innerHTML).toBe("<div>a</div>");
@@ -1362,29 +1300,4 @@ describe("connecting a component to store", () => {
expect(steps).toEqual(["willpatch", "patched"]); expect(steps).toEqual(["willpatch", "patched"]);
}); });
test("connected component has its own name", () => {
function mapStoreToProps() {}
class Named extends Component<any, any, any> {}
const namedConnected = connect(
Named,
mapStoreToProps
);
expect(namedConnected.name).toMatch("ConnectedNamed");
class ParentNamed extends Component<any, any, any> {}
class ChildNamed extends ParentNamed {}
const childConnected = connect(
ChildNamed,
mapStoreToProps
);
expect(childConnected.name).toMatch("ConnectedChildNamed");
const Anonymous = class extends Component<any, any, any> {};
const anonymousConnected = connect(
Anonymous,
mapStoreToProps
);
expect(anonymousConnected.name).toMatch(/^Connectedclass_\d+/);
});
}); });
+7 -10
View File
@@ -405,16 +405,15 @@ class TodoItem extends owl.Component {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// TodoApp // TodoApp
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
function mapStoreToProps(state) { class TodoApp extends owl.ConnectedComponent {
return {
todos: state.todos
};
}
class TodoApp extends owl.Component {
components = { TodoItem }; components = { TodoItem };
state = { filter: "all" }; state = { filter: "all" };
static mapStoreToProps(state) {
return {
todos: state.todos
};
}
get visibleTodos() { get visibleTodos() {
let todos = this.props.todos; let todos = this.props.todos;
if (this.state.filter === "active") { if (this.state.filter === "active") {
@@ -462,8 +461,6 @@ class TodoApp extends owl.Component {
} }
} }
const ConnectedTodoApp = owl.connect(TodoApp, mapStoreToProps);
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// App Initialization // App Initialization
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -474,7 +471,7 @@ const env = {
store, store,
dispatch: store.dispatch.bind(store), dispatch: store.dispatch.bind(store),
}; };
const app = new ConnectedTodoApp(env); const app = new TodoApp(env);
app.mount(document.body); app.mount(document.body);
`; `;