imp: work on store, refactor, improve todoapp

This commit is contained in:
Géry Debongnie
2019-03-22 09:52:48 +01:00
parent a236ae396a
commit d48cb5aae4
9 changed files with 150 additions and 24 deletions
+11 -9
View File
@@ -1,20 +1,20 @@
import { TodoItem } from "./TodoItem.js";
const { StoreMixin, Component } = odoo.core;
const { connect, Component } = odoo.core;
const ENTER_KEY = 13;
export class TodoApp extends StoreMixin(Component) {
function mapStateToProps(state) {
return { todos: state.todos };
}
class TodoApp extends Component {
template = "todoapp";
widgets = { TodoItem };
state = { filter: "all" };
get todos() {
return this.env.store.state.todos;
}
get visibleTodos() {
let todos = this.todos;
let todos = this.props.todos;
if (this.state.filter === "active") {
todos = todos.filter(t => !t.completed);
}
@@ -25,11 +25,11 @@ export class TodoApp extends StoreMixin(Component) {
}
get allChecked() {
return this.todos.every(todo => todo.completed);
return this.props.todos.every(todo => todo.completed);
}
get remaining() {
return this.todos.filter(todo => !todo.completed).length;
return this.props.todos.filter(todo => !todo.completed).length;
}
get remainingText() {
@@ -54,3 +54,5 @@ export class TodoApp extends StoreMixin(Component) {
this.env.store.dispatch("toggleAll", !this.allChecked);
}
}
export default connect(mapStateToProps)(TodoApp);
+1 -1
View File
@@ -1,4 +1,4 @@
import { TodoApp } from "./components/TodoApp.js";
import TodoApp from "./components/TodoApp.js";
import { makeStore } from "./store.js";
async function makeEnv() {
+3 -3
View File
@@ -8,7 +8,7 @@
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
</header>
<!-- main section -->
<section class="main" t-if="todos.length">
<section class="main" t-if="props.todos.length">
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="toggleAll"/>
<label for="toggle-all"></label>
<ul class="todo-list">
@@ -18,7 +18,7 @@
</ul>
</section>
<!-- footer -->
<footer class="footer" t-if="todos.length">
<footer class="footer" t-if="props.todos.length">
<span class="todo-count">
<strong>
<t t-esc="remaining"/>
@@ -36,7 +36,7 @@
<a href="#/completed" t-on-click="updateState({filter:'completed'})" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
</li>
</ul>
<button class="clear-completed" t-if="todos.length gt remaining" t-on-click="clearCompleted">
<button class="clear-completed" t-if="props.todos.length gt remaining" t-on-click="clearCompleted">
Clear completed
</button>
</footer>
+2 -2
View File
@@ -1,7 +1,7 @@
import { QWeb } from "./qweb";
import { EventBus } from "./event_bus";
import { Component, PureComponent } from "./component";
import { Store, StoreMixin } from "./store";
import { Store, connect } from "./store";
export const core = {
QWeb,
@@ -9,5 +9,5 @@ export const core = {
Component,
PureComponent,
Store,
StoreMixin
connect
};
+37 -6
View File
@@ -1,10 +1,41 @@
import { EventBus } from "./event_bus";
import { Component } from "./component";
import { shallowEqual } from "./utils";
export function StoreMixin(Component) {
return class extends Component {
mounted() {
this.env.store.on("update", this, this.render);
}
export function connect(mapStateToProps) {
return function(Comp) {
return class extends Comp {
constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent;
const storeProps = mapStateToProps(env.store.state);
props = Object.assign(props || {}, storeProps);
super(parent, props);
this.__widget__.currentStoreProps = storeProps;
}
mounted() {
this.env.store.on("update", this, () => {
const storeProps = mapStateToProps(this.env.store.state);
if (!shallowEqual(storeProps, this.__widget__.currentStoreProps)) {
this.__widget__.currentStoreProps = storeProps;
// probably not optimal, will do 2 object.assign, one here and
// one in updateProps.
const nextProps = Object.assign(
{},
this.props,
this.__widget__.currentStoreProps
);
this.updateProps(nextProps, false);
}
});
}
willUnmount() {
this.env.store.off("update", this);
}
updateProps(nextProps, forceUpdate) {
nextProps = Object.assign(nextProps, this.__widget__.currentStoreProps);
return super.updateProps(nextProps, forceUpdate);
}
};
};
}
@@ -53,7 +84,7 @@ export class Store extends EventBus {
await Promise.resolve();
if (this._isMutating) {
this._isMutating = false;
this.trigger("update");
this.trigger("update", this.state);
}
}
+13
View File
@@ -112,3 +112,16 @@ export function findInTree<T extends Tree<T>>(
}
return null;
}
export function shallowEqual(objA, objB) {
if (objA === objB) {
return true;
}
const keysA = Object.keys(objA);
for (let key of keysA) {
if (!(key in objB) || objA[key] !== objB[key]) {
return false;
}
}
return true;
}
+15
View File
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`connecting a component to store connecting a component works 1`] = `
"<div>
</div>"
`;
exports[`connecting a component to store connecting a component works 2`] = `
"<div>
<span>hello</span>
</div>"
`;
+55 -2
View File
@@ -1,5 +1,11 @@
import { Store } from "../src/store";
import { nextMicroTick } from "./helpers";
import { Store, connect } from "../src/store";
import { Component, Env } from "../src/component";
import {
nextMicroTick,
makeTestWEnv,
makeTestFixture,
nextTick
} from "./helpers";
describe("basic use", () => {
test("commit a mutation", () => {
@@ -54,3 +60,50 @@ describe("basic use", () => {
expect(updateCounter).toBe(1);
});
});
describe("connecting a component to store", () => {
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestWEnv();
});
afterEach(() => {
fixture.remove();
});
class App extends Component<any, any, any> {
inlineTemplate = `
<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="Todo" t-props="todo"/>
</t>
</div>`;
widgets = { Todo };
}
class Todo extends Component<any, any, any> {
inlineTemplate = `<span><t t-esc="props.msg"/></span>`;
}
test("connecting a component works", async () => {
const state = { todos: [] };
const mutations = {
addTodo(state, msg) {
state.todos.push({ msg });
}
};
const TodoApp = connect(s => s)(App);
const store = new Store({ state, mutations });
(<any>env).store = store;
const app = new TodoApp(env);
await app.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
store.commit("addTodo", "hello");
await nextTick();
expect(fixture.innerHTML).toMatchSnapshot();
});
});
+13 -1
View File
@@ -4,7 +4,8 @@ import {
idGenerator,
memoize,
debounce,
findInTree
findInTree,
shallowEqual
} from "../src/utils";
describe("escape", () => {
@@ -91,3 +92,14 @@ describe("findInTree", () => {
expect(match2).toBe(null);
});
});
describe("shallowEqual", () => {
test("simple comparisons", () => {
const obj1 = {};
expect(shallowEqual(obj1, obj1)).toBe(true);
expect(shallowEqual({}, {})).toBe(true);
expect(shallowEqual({ a: 1 }, {})).toBe(false);
expect(shallowEqual({ a: 1 }, { a: 1 })).toBe(true);
expect(shallowEqual({ a: 1 }, ["a"])).toBe(false);
});
});