[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
+16 -23
View File
@@ -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 {
increment() { static mapStoreToProps(state) {
this.env.store.dispatch("increment");
}
}
function mapStoreToProps(state) {
return { return {
value: state.counter value: state.counter
}; };
} }
const ConnectedCounter = owl.connect(Counter, mapStoreToProps); increment() {
this.env.store.dispatch("increment");
}
}
const counter = new ConnectedCounter({ store, qweb }); const counter = new Counter({ store, qweb });
``` ```
```xml ```xml
@@ -214,26 +212,21 @@ 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`: [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)
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
+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__ = {};
+26 -43
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,37 +185,24 @@ 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; export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> {
hashFunction?: HashFunction; deep: boolean = true;
deep?: boolean; getStore(env) {
return env.store;
} }
export function connect<E extends EnvWithStore, P, S>( hashFunction: HashFunction = ({ storeProps }, options) => {
Comp: Constructor<Component<E, P, S>>, let refFunction = this.deep ? deepRevNumber : revNumber;
mapStoreToProps,
options: StoreOptions = <StoreOptions>{}
) {
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) { if ("__owl__" in storeProps) {
return defaultRevFunction(storeProps); return refFunction(storeProps);
} }
const { currentStoreProps } = options;
let hash = 0; let hash = 0;
for (let key in storeProps) { for (let key in storeProps) {
const val = storeProps[key]; const val = storeProps[key];
const hashVal = defaultRevFunction(val); const hashVal = refFunction(val);
if (hashVal === 0) { if (hashVal === 0) {
if (val !== currentStoreProps[key]) { if (val !== currentStoreProps[key]) {
options.didChange = true; options.didChange = true;
@@ -226,20 +213,25 @@ export function connect<E extends EnvWithStore, P, S>(
} }
return hash; return hash;
}; };
}
const Result = class extends Comp { static mapStoreToProps(storeState, ownProps, getters) {
return {};
}
constructor(parent, props?: any) { constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent; super(parent, props);
const store = getStore(env); const store = this.getStore(this.env);
const ownProps = Object.assign({}, props || {}); const ownProps = Object.assign({}, props || {});
const storeProps = mapStoreToProps(store.state, ownProps, store.getters); const storeProps = (<any>this.constructor).mapStoreToProps(
store.state,
ownProps,
store.getters
);
const mergedProps = Object.assign({}, props || {}, storeProps); const mergedProps = Object.assign({}, props || {}, storeProps);
super(parent, mergedProps); this.props = 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__).store = store;
(<any>this.__owl__).storeHash = (<HashFunction>hashFunction)( (<any>this.__owl__).storeHash = this.hashFunction(
{ {
state: store.state, state: store.state,
storeProps: storeProps, storeProps: storeProps,
@@ -270,7 +262,7 @@ export function connect<E extends EnvWithStore, P, S>(
return; return;
} }
const ownProps = (<any>this.__owl__).ownProps; const ownProps = (<any>this.__owl__).ownProps;
const storeProps = mapStoreToProps( const storeProps = (<any>this.constructor).mapStoreToProps(
(<any>this.__owl__).store.state, (<any>this.__owl__).store.state,
ownProps, ownProps,
(<any>this.__owl__).store.getters (<any>this.__owl__).store.getters
@@ -278,7 +270,7 @@ export function connect<E extends EnvWithStore, P, S>(
const options: any = { const options: any = {
currentStoreProps: (<any>this.__owl__).currentStoreProps currentStoreProps: (<any>this.__owl__).currentStoreProps
}; };
const storeHash = (<HashFunction>hashFunction)( const storeHash = this.hashFunction(
{ {
state: (<any>this.__owl__).store.state, state: (<any>this.__owl__).store.state,
storeProps: storeProps, storeProps: storeProps,
@@ -301,7 +293,7 @@ export function connect<E extends EnvWithStore, P, S>(
const __owl__ = <any>this.__owl__; const __owl__ = <any>this.__owl__;
__owl__.currentUpdateId = __owl__.store._updateId; __owl__.currentUpdateId = __owl__.store._updateId;
if (__owl__.ownProps !== nextProps) { if (__owl__.ownProps !== nextProps) {
__owl__.currentStoreProps = mapStoreToProps( __owl__.currentStoreProps = (<any>this.constructor).mapStoreToProps(
__owl__.store.state, __owl__.store.state,
nextProps, nextProps,
__owl__.store.getters __owl__.store.getters
@@ -311,13 +303,4 @@ export function connect<E extends EnvWithStore, P, S>(
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps); const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
return super.__updateProps(mergedProps, forceUpdate, patchQueue); return super.__updateProps(mergedProps, forceUpdate, patchQueue);
} }
};
// we assign here a unique name to the resulting anonymous class.
// this is necessary for Owl to be able to properly deduce templates.
// Otherwise, all connected components would have the same name, and then
// each component after the first will necessarily have the same template.
let name = `Connected${Comp.name}`;
Object.defineProperty(Result, "name", { value: name });
return Result;
} }
+192 -279
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> {
components = { Todo };
static mapStoreToProps(s) {
return { todos: s.todos }; return { todos: s.todos };
} }
const TodoApp = connect( getStore() {
App, return store;
mapStoreToProps,
{
getStore: () => 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();
@@ -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> {
components = { TodoItem };
static mapStoreToProps(state) {
return { todos: state.todos }; 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> {
components = { TodoItem };
static mapStoreToProps(state) {
return { todos: state.todos }; 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> {
const ConnectedParent = connect( components = { Child };
Parent, static mapStoreToProps(s) {
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,34 +1133,27 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
function mapStoreToPropsTodoApp(state) { class TodoApp extends ConnectedComponent<any, any, any> {
components = { TodoItem };
static mapStoreToProps(state) {
return { return {
todos: state.todos 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> {
state = { isEditing: false };
static mapStoreToProps(state, ownProps) {
fCount++; fCount++;
return { return {
todo: state.todos[ownProps.id] todo: state.todos[ownProps.id]
}; };
} }
class TodoItem extends Component<any, any, any> {
state = { isEditing: false };
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> {
components = { TodoItem };
static mapStoreToProps(state) {
return { return {
todos: state.todos 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> {
state = { isEditing: false };
static mapStoreToProps(state, ownProps) {
fCount++; fCount++;
return { return {
todo: state.todos[ownProps.id] todo: state.todos[ownProps.id]
}; };
} }
class TodoItem extends Component<any, any, any> {
state = { isEditing: false };
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+/);
});
}); });
+6 -9
View File
@@ -405,16 +405,15 @@ class TodoItem extends owl.Component {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// TodoApp // TodoApp
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
function mapStoreToProps(state) { class TodoApp extends owl.ConnectedComponent {
components = { TodoItem };
state = { filter: "all" };
static mapStoreToProps(state) {
return { return {
todos: state.todos todos: state.todos
}; };
} }
class TodoApp extends owl.Component {
components = { TodoItem };
state = { filter: "all" };
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);
`; `;