From 545ceefc3dda42ad8b320f5d412f8d15cbf69d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 12 Jul 2019 15:22:14 +0200 Subject: [PATCH] [REF] store: remove connect function, add ConnectedComponent closes #238 closes #235 --- doc/store.md | 59 ++-- src/index.ts | 2 +- src/store.ts | 231 ++++++++-------- tests/store.test.ts | 529 +++++++++++++++--------------------- tools/playground/samples.js | 17 +- 5 files changed, 362 insertions(+), 476 deletions(-) diff --git a/doc/store.md b/doc/store.md index c4c7111b4..af487f50a 100644 --- a/doc/store.md +++ b/doc/store.md @@ -89,7 +89,7 @@ mutation is not allowed (and should throw an error). Mutations are synchronous. ```js const mutations = { setLoginState({ state }, loginState) { - state.loginState = loginState; + state.loginState = loginState; } }; ``` @@ -165,17 +165,16 @@ const getters = { 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 is a string or a number. ### Connecting a Component -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. +At some point, we need a way to access the state in the store from a component. +By default, an Owl `Component` is not connected to any store. To do that, we +need to create a component inheriting from `OwlComponent`: ```javascript const actions = { @@ -193,19 +192,18 @@ const state = { }; 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() { 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 @@ -214,45 +212,40 @@ const counter = new ConnectedCounter({ store, qweb }); ``` -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 - 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 sub class of the given `Component` which is -connected to the `store`. + from the `state` of the `Store` and returns them as a dict. +- `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` (boolean): [only useful if no hashFunction is given] if `false`, only watch + for top level state changes (`true` by default) ### Semantics -The `Store` and the `connect` function try to be smart and to optimize as much -as possible the rendering and update process. What is important to know is: +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: - components are always updated in the order of their creation (so, parent before children) - they are updated only if they are in the DOM - if a parent is asynchronous, the system will wait for it to complete its 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 a situation where some part of the UI is updated and other parts of the UI is not updated. ### 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. - 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 message - since the `mapStoreToProps` function is called for each connected component, for each state update, it is important to make sure that these functions are - as fast as possible. \ No newline at end of file + as fast as possible. diff --git a/src/index.ts b/src/index.ts index 65741f6c9..37b86e7cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,7 @@ import "./qweb_extensions"; import { QWeb } from "./qweb_core"; export { QWeb }; -export { connect, Store } from "./store"; +export { Store, ConnectedComponent } from "./store"; import * as _utils from "./utils"; export const __info__ = {}; diff --git a/src/store.ts b/src/store.ts index 73ef56dae..db39859d9 100644 --- a/src/store.ts +++ b/src/store.ts @@ -7,7 +7,7 @@ import { Observer } from "./observer"; * * We have here: * - a Store class - * - a connect function + * - the ConnectedComponent class * * 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 @@ -185,139 +185,122 @@ function deepRevNumber(o: T): number { return 0; } -type Constructor = new (...args: any[]) => T; -interface EnvWithStore extends Env { - store: Store; -} type HashFunction = (a: any, b: any) => number; -interface StoreOptions { - getStore?(Env): Store; - hashFunction?: HashFunction; - deep?: boolean; -} -export function connect( - Comp: Constructor>, - mapStoreToProps, - options: StoreOptions = {} -) { - let hashFunction = options.hashFunction || null; - const getStore = options.getStore || (env => env.store); +export class ConnectedComponent extends Component { + deep: boolean = true; + getStore(env) { + return 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; + hashFunction: HashFunction = ({ storeProps }, options) => { + let refFunction = this.deep ? deepRevNumber : revNumber; + if ("__owl__" in storeProps) { + return refFunction(storeProps); + } + const { currentStoreProps } = options; + let hash = 0; + for (let key in storeProps) { + const val = storeProps[key]; + const hashVal = refFunction(val); + if (hashVal === 0) { + if (val !== currentStoreProps[key]) { + options.didChange = true; } + } else { + hash += hashVal; } - return hash; - }; + } + return hash; + }; + + static mapStoreToProps(storeState, ownProps, getters) { + return {}; + } + constructor(parent, props?: any) { + super(parent, props); + const store = this.getStore(this.env); + const ownProps = Object.assign({}, props || {}); + const storeProps = (this.constructor).mapStoreToProps( + store.state, + ownProps, + store.getters + ); + const mergedProps = Object.assign({}, props || {}, storeProps); + this.props = mergedProps; + (this.__owl__).ownProps = ownProps; + (this.__owl__).currentStoreProps = storeProps; + (this.__owl__).store = store; + (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() { + (this.__owl__).store.on("update", this, this.__checkUpdate); + super.__callMounted(); + } + willUnmount() { + (this.__owl__).store.off("update", this); + super.willUnmount(); } - const Result = class extends Comp { - constructor(parent, props?: any) { - const env = parent instanceof Component ? parent.env : parent; - const store = getStore(env); - const ownProps = Object.assign({}, props || {}); - const storeProps = mapStoreToProps(store.state, ownProps, store.getters); - const mergedProps = Object.assign({}, props || {}, storeProps); - super(parent, mergedProps); - (this.__owl__).ownProps = ownProps; - (this.__owl__).currentStoreProps = storeProps; - (this.__owl__).store = store; - (this.__owl__).storeHash = (hashFunction)( - { - state: store.state, - storeProps: storeProps, - revNumber, - deepRevNumber - }, - { - currentStoreProps: storeProps - } - ); + async __checkUpdate(updateId) { + if (updateId === (this.__owl__).currentUpdateId) { + return; } - /** - * 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() { - (this.__owl__).store.on("update", this, this.__checkUpdate); - super.__callMounted(); + const ownProps = (this.__owl__).ownProps; + const storeProps = (this.constructor).mapStoreToProps( + (this.__owl__).store.state, + ownProps, + (this.__owl__).store.getters + ); + const options: any = { + currentStoreProps: (this.__owl__).currentStoreProps + }; + const storeHash = this.hashFunction( + { + state: (this.__owl__).store.state, + storeProps: storeProps, + revNumber, + deepRevNumber + }, + options + ); + let didChange = options.didChange; + if (storeHash !== (this.__owl__).storeHash) { + didChange = true; + (this.__owl__).storeHash = storeHash; } - willUnmount() { - (this.__owl__).store.off("update", this); - super.willUnmount(); + if (didChange) { + (this.__owl__).currentStoreProps = storeProps; + await this.__updateProps(ownProps, false); } - - async __checkUpdate(updateId) { - if (updateId === (this.__owl__).currentUpdateId) { - return; - } - const ownProps = (this.__owl__).ownProps; - const storeProps = mapStoreToProps( - (this.__owl__).store.state, - ownProps, - (this.__owl__).store.getters - ); - const options: any = { - currentStoreProps: (this.__owl__).currentStoreProps - }; - const storeHash = (hashFunction)( - { - state: (this.__owl__).store.state, - storeProps: storeProps, - revNumber, - deepRevNumber - }, - options + } + __updateProps(nextProps, forceUpdate, patchQueue?: any[]) { + const __owl__ = this.__owl__; + __owl__.currentUpdateId = __owl__.store._updateId; + if (__owl__.ownProps !== nextProps) { + __owl__.currentStoreProps = (this.constructor).mapStoreToProps( + __owl__.store.state, + nextProps, + __owl__.store.getters ); - let didChange = options.didChange; - if (storeHash !== (this.__owl__).storeHash) { - didChange = true; - (this.__owl__).storeHash = storeHash; - } - if (didChange) { - (this.__owl__).currentStoreProps = storeProps; - await this.__updateProps(ownProps, false); - } } - __updateProps(nextProps, forceUpdate, patchQueue?: any[]) { - const __owl__ = 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. - // 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; + __owl__.ownProps = nextProps; + const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps); + return super.__updateProps(mergedProps, forceUpdate, patchQueue); + } } diff --git a/tests/store.test.ts b/tests/store.test.ts index 35999ab56..04768d924 100644 --- a/tests/store.test.ts +++ b/tests/store.test.ts @@ -1,5 +1,5 @@ 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 { Observer } from "../src"; @@ -561,18 +561,21 @@ describe("connecting a component to store", () => { }); test("connecting a component works", async () => { - env.qweb.addTemplate( - "App", - ` -
+ env.qweb.addTemplates(` + +
-
` - ); - env.qweb.addTemplate("Todo", ``); - class App extends Component { +
+ + + `); + class App extends ConnectedComponent { components = { Todo }; + static mapStoreToProps(s) { + return { todos: s.todos }; + } } class Todo extends Component {} const state = { todos: [] }; @@ -581,16 +584,9 @@ describe("connecting a component to store", () => { state.todos.push({ msg }); } }; - function mapStoreToProps(s) { - return { todos: s.todos }; - } - const TodoApp = connect( - App, - mapStoreToProps - ); const store = new Store({ state, mutations }); (env).store = store; - const app = new TodoApp(env); + const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toMatchSnapshot(); @@ -601,38 +597,35 @@ describe("connecting a component to store", () => { }); test("deep and shallow connecting a component", async () => { + env.qweb.addTemplates(` + +
+ + + +
+
+ `); const state = { todos: [{ title: "Kasteel" }] }; const mutations = { edit({ state }, title) { state.todos[0].title = title; } }; - function mapStoreToProps(s) { - return { todos: s.todos }; - } const store = new Store({ state, mutations }); - env.qweb.addTemplate( - "App", - ` -
- - - -
` - ); - class App extends Component {} + class App extends ConnectedComponent { + static mapStoreToProps(s) { + return { todos: s.todos }; + } + } + class DeepTodoApp extends App { + deep = true; + } + class ShallowTodoApp extends App { + deep = false; + } - const DeepTodoApp = connect( - App, - mapStoreToProps, - { deep: true } - ); - const ShallowTodoApp = connect( - App, - mapStoreToProps, - { deep: false } - ); (env).store = store; const deepTodoApp = new DeepTodoApp(env); const shallowTodoApp = new ShallowTodoApp(env); @@ -662,9 +655,6 @@ describe("connecting a component to store", () => { `); - class App extends Component { - components = { Todo }; - } class Todo extends Component {} (env).store = new Store({}); @@ -676,17 +666,16 @@ describe("connecting a component to store", () => { } } }); - function mapStoreToProps(s) { - return { todos: s.todos }; - } - const TodoApp = connect( - App, - mapStoreToProps, - { - getStore: () => store + class App extends ConnectedComponent { + components = { Todo }; + static mapStoreToProps(s) { + return { todos: s.todos }; } - ); - const app = new TodoApp(env); + getStore() { + return store; + } + } + const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toMatchSnapshot(); @@ -698,8 +687,18 @@ describe("connecting a component to store", () => { test("connected child components with custom hooks", async () => { let steps: any = []; - env.qweb.addTemplate("Child", `
`); - class Child extends Component { + env.qweb.addTemplates(` + +
+ +
+
+ + `); + class Child extends ConnectedComponent { + static mapStoreToProps(s) { + return s; + } 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", - ` -
- -
` - ); class Parent extends Component { - components = { ConnectedChild }; + components = { Child }; constructor(env: Env) { super(env); @@ -741,7 +728,7 @@ describe("connecting a component to store", () => { 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" }] }; let nextId = 2; const mutations = { @@ -751,38 +738,32 @@ describe("connecting a component to store", () => { }; const store = new Store({ state, mutations }); - env.qweb.addTemplate("TodoItem", ``); - class TodoItem extends Component {} - const ConnectedTodo = connect( - TodoItem, - (state, props) => { + env.qweb.addTemplates(` + + +
+ + + +
+
+ `); + class TodoItem extends ConnectedComponent { + static mapStoreToProps(state, props) { const todo = state.todos.find(t => t.id === props.id); return todo; } - ); - - env.qweb.addTemplate( - "TodoList", - `
- - - -
` - ); - class TodoList extends Component { - components = { ConnectedTodo }; } - function mapStoreToProps(state) { - return { todos: state.todos }; + class TodoList extends ConnectedComponent { + components = { TodoItem }; + static mapStoreToProps(state) { + return { todos: state.todos }; + } } - const ConnectedTodoList = connect( - TodoList, - mapStoreToProps - ); (env).store = store; - const app = new ConnectedTodoList(env); + const app = new TodoList(env); await app.mount(fixture); expect(fixture.innerHTML).toBe("
jupiler
"); @@ -792,7 +773,7 @@ describe("connecting a component to store", () => { expect(fixture.innerHTML).toBe("
jupilerhoegaarden
"); }); - test("connect receives store getters as third argument", async () => { + test("mapStoreToProps receives store getters as third argument", async () => { const state = { importantID: 1, 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 }); - env.qweb.addTemplate( - "TodoItem", - `
- - -
` - ); - class TodoItem extends Component {} - const ConnectedTodo = connect( - TodoItem, - (state, props, getters) => { + env.qweb.addTemplates(` + +
+ + +
+
+ + + +
+
+ `); + + class TodoItem extends ConnectedComponent { + static mapStoreToProps(state, props, getters) { const todo = state.todos.find(t => t.id === props.id); return { activeTodoText: getters.text(todo.id), importantTodoText: getters.importantTodoText() }; } - ); - - env.qweb.addTemplate( - "TodoList", - `
- - - -
` - ); - class TodoList extends Component { - components = { ConnectedTodo }; } - function mapStoreToProps(state) { - return { todos: state.todos }; + class TodoList extends ConnectedComponent { + components = { TodoItem }; + static mapStoreToProps(state) { + return { todos: state.todos }; + } } - const ConnectedTodoList = connect( - TodoList, - mapStoreToProps - ); (env).store = store; - const app = new ConnectedTodoList(env); + const app = new TodoList(env); await app.mount(fixture); expect(fixture.innerHTML).toBe( @@ -856,23 +829,23 @@ describe("connecting a component to store", () => { }); test("connected component is updated when props are updated", async () => { - env.qweb.addTemplate("Beer", ``); - class Beer extends Component {} - const ConnectedBeer = connect( - Beer, - (state, props) => { + env.qweb.addTemplates(` + + +
+ +
+
+ `); + + class Beer extends ConnectedComponent { + static mapStoreToProps(state, props) { return state.beers[props.id]; } - ); + } - env.qweb.addTemplate( - "App", - `
- -
` - ); class App extends Component { - components = { ConnectedBeer }; + components = { Beer }; state = { beerId: 1 }; } @@ -890,14 +863,19 @@ describe("connecting a component to store", () => { }); test("connected component is updated when store is changed", async () => { - env.qweb.addTemplate( - "App", - ` -
+ env.qweb.addTemplates(` + +
-
` - ); - class App extends Component {} +
+ + `); + + class App extends ConnectedComponent { + static mapStoreToProps(state) { + return { beers: state.beers, otherKey: 1 }; + } + } const mutations = { addBeer({ state }, name) { @@ -909,14 +887,7 @@ describe("connecting a component to store", () => { const store = new Store({ state, mutations }); (env).store = store; - function mapStoreToProps(state) { - return { beers: state.beers, otherKey: 1 }; - } - const ConnectedApp = connect( - App, - mapStoreToProps - ); - const app = new ConnectedApp(env); + const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toBe("
jupiler
"); @@ -927,34 +898,31 @@ describe("connecting a component to store", () => { }); test("connected component with undefined, null and string props", async () => { - env.qweb.addTemplate( - "Beer", - `
- taster: - selected: - consumed: -
` - ); - class Beer extends Component {} - const ConnectedBeer = connect( - Beer, - (state, props) => { + env.qweb.addTemplates(` + +
+ taster: + selected: + consumed: +
+
+ +
+
+ `); + + class Beer extends ConnectedComponent { + static mapStoreToProps(state, props) { return { selected: state.beers[props.id], consumed: state.beers[state.consumedID] || null, taster: state.taster }; } - ); + } - env.qweb.addTemplate( - "App", - `
- -
` - ); class App extends Component { - components = { ConnectedBeer }; + components = { Beer }; 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 () => { - env.qweb.addTemplate( - "Beer", - `
- taster: - selected: - consumed: -
` - ); - class Beer extends Component {} - const ConnectedBeer = connect( - Beer, - (state, props) => { + env.qweb.addTemplates(` + +
+ taster: + selected: + consumed: +
+
+ +
+
+ `); + + class Beer extends ConnectedComponent { + static mapStoreToProps(storeState, props) { return { - selected: state.beers[props.id], - consumed: state.beers[state.consumedID] || null, - taster: state.taster + selected: storeState.beers[props.id], + consumed: storeState.beers[storeState.consumedID] || null, + taster: storeState.taster }; } - ); + } - env.qweb.addTemplate( - "App", - `
- -
` - ); class App extends Component { - components = { ConnectedBeer }; + components = { Beer }; state = { beerId: 0 }; } @@ -1093,35 +1058,29 @@ describe("connecting a component to store", () => { test("correct update order when parent/children are connected", async () => { const steps: string[] = []; - env.qweb.addTemplate( - "Parent", - ` -
- -
- ` - ); - class Parent extends Component { - components = { Child: ConnectedChild }; - } - const ConnectedParent = connect( - Parent, - function(s) { + env.qweb.addTemplates(` + +
+ +
+ +
+ `); + + class Parent extends ConnectedComponent { + components = { Child }; + static mapStoreToProps(s) { steps.push("parent"); return { current: s.current, isvisible: s.isvisible }; } - ); - - env.qweb.addTemplate("Child", ``); - class Child extends Component {} + } - const ConnectedChild = connect( - Child, - function(s, props) { + class Child extends ConnectedComponent { + static mapStoreToProps(s, props) { steps.push("child"); return { msg: s.msg[props.key] }; } - ); + } const state = { current: "a", msg: { a: "a", b: "b" } }; const mutations = { @@ -1132,7 +1091,7 @@ describe("connecting a component to store", () => { const store = new Store({ state, mutations }); (env).store = store; - const app = new ConnectedParent(env); + const app = new Parent(env); await app.mount(fixture); expect(fixture.innerHTML).toBe("
a
"); @@ -1163,7 +1122,7 @@ describe("connecting a component to store", () => {
- +
@@ -1174,33 +1133,26 @@ describe("connecting a component to store", () => {
`); - function mapStoreToPropsTodoApp(state) { - return { - todos: state.todos - }; - } - - class TodoApp extends Component { - components = { ConnectedTodoItem }; + class TodoApp extends ConnectedComponent { + components = { TodoItem }; + static mapStoreToProps(state) { + return { + todos: state.todos + }; + } } - const ConnectedTodoApp = connect( - TodoApp, - mapStoreToPropsTodoApp - ); - let renderCount = 0; let fCount = 0; - function mapStoreToPropsTodoItem(state, ownProps) { - fCount++; - return { - todo: state.todos[ownProps.id] - }; - } - - class TodoItem extends Component { + class TodoItem extends ConnectedComponent { state = { isEditing: false }; + static mapStoreToProps(state, ownProps) { + fCount++; + return { + todo: state.todos[ownProps.id] + }; + } editTodo() { this.env.store.commit("editTodo"); @@ -1211,13 +1163,8 @@ describe("connecting a component to store", () => { } } - const ConnectedTodoItem = connect( - TodoItem, - mapStoreToPropsTodoItem - ); - (env).store = store; - const app = new ConnectedTodoApp(env); + const app = new TodoApp(env); await app.mount(fixture); expect(fixture.innerHTML).toBe( @@ -1254,7 +1201,7 @@ describe("connecting a component to store", () => {
- +
@@ -1265,34 +1212,27 @@ describe("connecting a component to store", () => {
`); - function mapStoreToPropsTodoApp(state) { - return { - todos: state.todos - }; - } - - class TodoApp extends Component { - components = { ConnectedTodoItem }; + class TodoApp extends ConnectedComponent { + components = { TodoItem }; + static mapStoreToProps(state) { + return { + todos: state.todos + }; + } } - const ConnectedTodoApp = connect( - TodoApp, - mapStoreToPropsTodoApp - ); - let renderCount = 0; let fCount = 0; - function mapStoreToPropsTodoItem(state, ownProps) { - fCount++; - return { - todo: state.todos[ownProps.id] - }; - } - - class TodoItem extends Component { + class TodoItem extends ConnectedComponent { state = { isEditing: false }; + static mapStoreToProps(state, ownProps) { + fCount++; + return { + todo: state.todos[ownProps.id] + }; + } removeTodo() { this.env.store.commit("removeTodo"); } @@ -1302,13 +1242,8 @@ describe("connecting a component to store", () => { } } - const ConnectedTodoItem = connect( - TodoItem, - mapStoreToPropsTodoItem - ); - (env).store = store; - const app = new ConnectedTodoApp(env); + const app = new TodoApp(env); await app.mount(fixture); 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 () => { const steps: string[] = []; - env.qweb.addTemplate("App", `
`); - class App extends Component { + + env.qweb.addTemplates(` + +
+
+ `); + + class App extends ConnectedComponent { + static mapStoreToProps(s) { + return { msg: s.msg }; + } willPatch() { steps.push("willpatch"); } @@ -1335,12 +1279,6 @@ describe("connecting a component to store", () => { steps.push("patched"); } } - const ConnectedApp = connect( - App, - function(s) { - return { msg: s.msg }; - } - ); const state = { msg: "a" }; const mutations = { @@ -1351,7 +1289,7 @@ describe("connecting a component to store", () => { const store = new Store({ state, mutations }); (env).store = store; - const app = new ConnectedApp(env); + const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toBe("
a
"); @@ -1362,29 +1300,4 @@ describe("connecting a component to store", () => { expect(steps).toEqual(["willpatch", "patched"]); }); - test("connected component has its own name", () => { - function mapStoreToProps() {} - - class Named extends Component {} - const namedConnected = connect( - Named, - mapStoreToProps - ); - expect(namedConnected.name).toMatch("ConnectedNamed"); - - class ParentNamed extends Component {} - class ChildNamed extends ParentNamed {} - const childConnected = connect( - ChildNamed, - mapStoreToProps - ); - expect(childConnected.name).toMatch("ConnectedChildNamed"); - - const Anonymous = class extends Component {}; - const anonymousConnected = connect( - Anonymous, - mapStoreToProps - ); - expect(anonymousConnected.name).toMatch(/^Connectedclass_\d+/); - }); }); diff --git a/tools/playground/samples.js b/tools/playground/samples.js index d461b3c1f..5cc0cb50e 100644 --- a/tools/playground/samples.js +++ b/tools/playground/samples.js @@ -405,16 +405,15 @@ class TodoItem extends owl.Component { //------------------------------------------------------------------------------ // TodoApp //------------------------------------------------------------------------------ -function mapStoreToProps(state) { - return { - todos: state.todos - }; -} - -class TodoApp extends owl.Component { +class TodoApp extends owl.ConnectedComponent { components = { TodoItem }; state = { filter: "all" }; + static mapStoreToProps(state) { + return { + todos: state.todos + }; + } get visibleTodos() { let todos = this.props.todos; if (this.state.filter === "active") { @@ -462,8 +461,6 @@ class TodoApp extends owl.Component { } } -const ConnectedTodoApp = owl.connect(TodoApp, mapStoreToProps); - //------------------------------------------------------------------------------ // App Initialization //------------------------------------------------------------------------------ @@ -474,7 +471,7 @@ const env = { store, dispatch: store.dispatch.bind(store), }; -const app = new ConnectedTodoApp(env); +const app = new TodoApp(env); app.mount(document.body); `;