[REF] store: simplify connect API

Closes #158
This commit is contained in:
Aaron Bohy
2019-06-11 09:32:28 +02:00
committed by Géry Debongnie
parent 1ff3c32fe4
commit 22e48e3be0
4 changed files with 199 additions and 155 deletions
+3 -3
View File
@@ -176,7 +176,7 @@ function mapStoreToProps(state) {
value: state.counter value: state.counter
}; };
} }
const ConnectedCounter = owl.connect(mapStoreToProps)(Counter); const ConnectedCounter = owl.connect(Counter, mapStoreToProps);
const counter = new ConnectedCounter({ store, qweb }); const counter = new ConnectedCounter({ store, qweb });
``` ```
@@ -199,5 +199,5 @@ The arguments of `connect` are:
- `deep`: [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 function that takes a `Component` as argument The `connect` function returns a sub class of the given `Component` which is
and returns a sub Class of this `Component` that is connected to the `store` connected to the `store`.
+96 -94
View File
@@ -150,10 +150,20 @@ type Constructor<T> = new (...args: any[]) => T;
interface EnvWithStore extends Env { interface EnvWithStore extends Env {
store: Store; store: Store;
} }
type HashFunction = (a: any, b: any) => number;
interface StoreOptions {
getStore?(Env): Store;
hashFunction?: HashFunction;
deep?: boolean;
}
let nextID = 1; let nextID = 1;
export function connect(mapStateToProps, options: any = {}) { export function connect<E extends EnvWithStore, P, S>(
Comp: Constructor<Component<E, P, S>>,
mapStoreToProps,
options: StoreOptions = <StoreOptions>{}
) {
let hashFunction = options.hashFunction || null; let hashFunction = options.hashFunction || null;
const getStore = options.getStore || (env => env.store); const getStore = options.getStore || (env => env.store);
@@ -181,103 +191,95 @@ export function connect(mapStateToProps, options: any = {}) {
}; };
} }
return function<E extends EnvWithStore, P, S>( const Result = class extends Comp {
Comp: Constructor<Component<E, P, S>> constructor(parent, props?: any) {
) { const env = parent instanceof Component ? parent.env : parent;
const Result = class extends Comp { const store = getStore(env);
constructor(parent, props?: any) { const ownProps = Object.assign({}, props || {});
const env = parent instanceof Component ? parent.env : parent; const storeProps = mapStoreToProps(store.state, ownProps, store.getters);
const store = getStore(env); const mergedProps = Object.assign({}, props || {}, storeProps);
const ownProps = Object.assign({}, props || {}); super(parent, mergedProps);
const storeProps = mapStateToProps( (<any>this.__owl__).ownProps = ownProps;
store.state, (<any>this.__owl__).currentStoreProps = storeProps;
ownProps, (<any>this.__owl__).store = store;
store.getters (<any>this.__owl__).storeHash = (<HashFunction>hashFunction)(
); {
const mergedProps = Object.assign({}, props || {}, storeProps); state: store.state,
super(parent, mergedProps); storeProps: storeProps,
(<any>this.__owl__).ownProps = ownProps; revNumber,
(<any>this.__owl__).currentStoreProps = storeProps; deepRevNumber
(<any>this.__owl__).store = store; },
(<any>this.__owl__).storeHash = hashFunction( {
{ currentStoreProps: storeProps
state: store.state, }
storeProps: storeProps, );
revNumber, }
deepRevNumber /**
}, * 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,
currentStoreProps: storeProps * 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();
* 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, willUnmount() {
* if we use the mounted hook, this will be done in the reverse order. (<any>this.__owl__).store.off("update", this);
*/ super.willUnmount();
_callMounted() { }
(<any>this.__owl__).store.on("update", this, this._checkUpdate);
super._callMounted();
}
willUnmount() {
(<any>this.__owl__).store.off("update", this);
super.willUnmount();
}
_checkUpdate() { _checkUpdate() {
const ownProps = (<any>this.__owl__).ownProps; const ownProps = (<any>this.__owl__).ownProps;
const storeProps = mapStateToProps( 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;
this._updateProps(ownProps, false);
}
}
_updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
if ((<any>this.__owl__).ownProps !== nextProps) {
(<any>this.__owl__).currentStoreProps = mapStoreToProps(
(<any>this.__owl__).store.state, (<any>this.__owl__).store.state,
ownProps, nextProps,
(<any>this.__owl__).store.getters (<any>this.__owl__).store.getters
); );
const options: any = {
currentStoreProps: (<any>this.__owl__).currentStoreProps
};
const storeHash = 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;
this._updateProps(ownProps, false);
}
} }
_updateProps(nextProps, forceUpdate, patchQueue?: any[]) { (<any>this.__owl__).ownProps = nextProps;
if ((<any>this.__owl__).ownProps !== nextProps) { const mergedProps = Object.assign(
(<any>this.__owl__).currentStoreProps = mapStateToProps( {},
(<any>this.__owl__).store.state, nextProps,
nextProps, (<any>this.__owl__).currentStoreProps
(<any>this.__owl__).store.getters );
); return super._updateProps(mergedProps, forceUpdate, patchQueue);
} }
(<any>this.__owl__).ownProps = nextProps;
const mergedProps = Object.assign(
{},
nextProps,
(<any>this.__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 = `ConnectedComponent${nextID++}`;
Object.defineProperty(Result, "name", { value: name });
return Result;
}; };
// 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 = `ConnectedComponent${nextID++}`;
Object.defineProperty(Result, "name", { value: name });
return Result;
} }
+98 -56
View File
@@ -411,10 +411,13 @@ describe("connecting a component to store", () => {
state.todos.push({ msg }); state.todos.push({ msg });
} }
}; };
function mapStateToProps(s) { function mapStoreToProps(s) {
return { todos: s.todos }; return { todos: s.todos };
} }
const TodoApp = connect(mapStateToProps)(App); 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 TodoApp(env);
@@ -434,7 +437,7 @@ describe("connecting a component to store", () => {
state.todos[0].title = title; state.todos[0].title = title;
} }
}; };
function mapStateToProps(s) { function mapStoreToProps(s) {
return { todos: s.todos }; return { todos: s.todos };
} }
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
@@ -451,13 +454,15 @@ describe("connecting a component to store", () => {
class App extends Component<any, any, any> {} class App extends Component<any, any, any> {}
const DeepTodoApp = connect( const DeepTodoApp = connect(
mapStateToProps, App,
mapStoreToProps,
{ deep: true } { deep: true }
)(App); );
const ShallowTodoApp = connect( const ShallowTodoApp = connect(
mapStateToProps, App,
mapStoreToProps,
{ deep: false } { deep: false }
)(App); );
(<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);
@@ -501,15 +506,16 @@ describe("connecting a component to store", () => {
} }
} }
}); });
function mapStateToProps(s) { function mapStoreToProps(s) {
return { todos: s.todos }; return { todos: s.todos };
} }
const TodoApp = connect( const TodoApp = connect(
mapStateToProps, App,
mapStoreToProps,
{ {
getStore: () => store getStore: () => store
} }
)(App); );
const app = new TodoApp(env); const app = new TodoApp(env);
await app.mount(fixture); await app.mount(fixture);
@@ -532,7 +538,10 @@ describe("connecting a component to store", () => {
} }
} }
const ConnectedChild = connect(s => s)(Child); const ConnectedChild = connect(
Child,
s => s
);
env.qweb.addTemplate( env.qweb.addTemplate(
"Parent", "Parent",
@@ -574,10 +583,13 @@ describe("connecting a component to store", () => {
env.qweb.addTemplate("TodoItem", `<span><t t-esc="props.text"/></span>`); env.qweb.addTemplate("TodoItem", `<span><t t-esc="props.text"/></span>`);
class TodoItem extends Component<any, any, any> {} class TodoItem extends Component<any, any, any> {}
const ConnectedTodo = connect((state, props) => { const ConnectedTodo = connect(
const todo = state.todos.find(t => t.id === props.id); TodoItem,
return todo; (state, props) => {
})(TodoItem); const todo = state.todos.find(t => t.id === props.id);
return todo;
}
);
env.qweb.addTemplate( env.qweb.addTemplate(
"TodoList", "TodoList",
@@ -591,10 +603,13 @@ describe("connecting a component to store", () => {
widgets = { ConnectedTodo }; widgets = { ConnectedTodo };
} }
function mapStateToProps(state) { function mapStoreToProps(state) {
return { todos: state.todos }; return { todos: state.todos };
} }
const ConnectedTodoList = connect(mapStateToProps)(TodoList); const ConnectedTodoList = connect(
TodoList,
mapStoreToProps
);
(<any>env).store = store; (<any>env).store = store;
const app = new ConnectedTodoList(env); const app = new ConnectedTodoList(env);
@@ -632,13 +647,16 @@ describe("connecting a component to store", () => {
</div>` </div>`
); );
class TodoItem extends Component<any, any, any> {} class TodoItem extends Component<any, any, any> {}
const ConnectedTodo = connect((state, props, getters) => { const ConnectedTodo = connect(
const todo = state.todos.find(t => t.id === props.id); TodoItem,
return { (state, props, getters) => {
activeTodoText: getters.text(todo.id), const todo = state.todos.find(t => t.id === props.id);
importantTodoText: getters.importantTodoText() return {
}; activeTodoText: getters.text(todo.id),
})(TodoItem); importantTodoText: getters.importantTodoText()
};
}
);
env.qweb.addTemplate( env.qweb.addTemplate(
"TodoList", "TodoList",
@@ -652,10 +670,13 @@ describe("connecting a component to store", () => {
widgets = { ConnectedTodo }; widgets = { ConnectedTodo };
} }
function mapStateToProps(state) { function mapStoreToProps(state) {
return { todos: state.todos }; return { todos: state.todos };
} }
const ConnectedTodoList = connect(mapStateToProps)(TodoList); const ConnectedTodoList = connect(
TodoList,
mapStoreToProps
);
(<any>env).store = store; (<any>env).store = store;
const app = new ConnectedTodoList(env); const app = new ConnectedTodoList(env);
@@ -669,9 +690,12 @@ 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.addTemplate("Beer", `<span><t t-esc="props.name"/></span>`);
class Beer extends Component<any, any, any> {} class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect((state, props) => { const ConnectedBeer = connect(
return state.beers[props.id]; Beer,
})(Beer); (state, props) => {
return state.beers[props.id];
}
);
env.qweb.addTemplate( env.qweb.addTemplate(
"App", "App",
@@ -717,10 +741,13 @@ 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 mapStateToProps(state) { function mapStoreToProps(state) {
return { beers: state.beers, otherKey: 1 }; return { beers: state.beers, otherKey: 1 };
} }
const ConnectedApp = connect(mapStateToProps)(App); const ConnectedApp = connect(
App,
mapStoreToProps
);
const app = new ConnectedApp(env); const app = new ConnectedApp(env);
await app.mount(fixture); await app.mount(fixture);
@@ -743,13 +770,16 @@ describe("connecting a component to store", () => {
</div>` </div>`
); );
class Beer extends Component<any, any, any> {} class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect((state, props) => { const ConnectedBeer = connect(
return { Beer,
selected: state.beers[props.id], (state, props) => {
consumed: state.beers[state.consumedID] || null, return {
taster: state.taster selected: state.beers[props.id],
}; consumed: state.beers[state.consumedID] || null,
})(Beer); taster: state.taster
};
}
);
env.qweb.addTemplate( env.qweb.addTemplate(
"App", "App",
@@ -812,13 +842,16 @@ describe("connecting a component to store", () => {
</div>` </div>`
); );
class Beer extends Component<any, any, any> {} class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect((state, props) => { const ConnectedBeer = connect(
return { Beer,
selected: state.beers[props.id], (state, props) => {
consumed: state.beers[state.consumedID] || null, return {
taster: state.taster selected: state.beers[props.id],
}; consumed: state.beers[state.consumedID] || null,
})(Beer); taster: state.taster
};
}
);
env.qweb.addTemplate( env.qweb.addTemplate(
"App", "App",
@@ -909,18 +942,24 @@ describe("connecting a component to store", () => {
class Parent extends Component<any, any, any> { class Parent extends Component<any, any, any> {
widgets = { Child: ConnectedChild }; widgets = { Child: ConnectedChild };
} }
const ConnectedParent = connect(function(s) { const ConnectedParent = connect(
steps.push("parent"); Parent,
return { current: s.current, isvisible: s.isvisible }; function(s) {
})(Parent); steps.push("parent");
return { current: s.current, isvisible: s.isvisible };
}
);
env.qweb.addTemplate("Child", `<span><t t-esc="props.msg"/></span>`); env.qweb.addTemplate("Child", `<span><t t-esc="props.msg"/></span>`);
class Child extends Component<any, any, any> {} class Child extends Component<any, any, any> {}
const ConnectedChild = connect(function(s, props) { const ConnectedChild = connect(
steps.push("child"); Child,
return { msg: s.msg[props.key] }; function(s, props) {
})(Child); steps.push("child");
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 = {
@@ -953,9 +992,12 @@ describe("connecting a component to store", () => {
steps.push("patched"); steps.push("patched");
} }
} }
const ConnectedApp = connect(function(s) { const ConnectedApp = connect(
return { msg: s.msg }; App,
})(App); function(s) {
return { msg: s.msg };
}
);
const state = { msg: "a" }; const state = { msg: "a" };
const mutations = { const mutations = {
+2 -2
View File
@@ -432,7 +432,7 @@ class TodoItem extends owl.Component {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// TodoApp // TodoApp
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
function mapStateToProps(state) { function mapStoreToProps(state) {
return { return {
todos: state.todos todos: state.todos
}; };
@@ -489,7 +489,7 @@ class TodoApp extends owl.Component {
} }
} }
const ConnectedTodoApp = owl.connect(mapStateToProps)(TodoApp); const ConnectedTodoApp = owl.connect(TodoApp, mapStoreToProps);
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// App Initialization // App Initialization