[IMP] component: observe the state and rerender if it changes

closes #35
This commit is contained in:
Géry Debongnie
2019-04-16 22:59:54 +02:00
parent 94e7d9aa39
commit 619e455dd5
3 changed files with 45 additions and 9 deletions
+1
View File
@@ -3,6 +3,7 @@ module.exports = {
transform: { transform: {
"^.+\\.ts?$": "ts-jest" "^.+\\.ts?$": "ts-jest"
}, },
verbose: false,
testRegex: "(/tests/.*(test|spec))\\.ts?$", testRegex: "(/tests/.*(test|spec))\\.ts?$",
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"] moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"]
}; };
+16 -4
View File
@@ -5,6 +5,7 @@ import sdListeners from "../libs/snabbdom/src/modules/eventlisteners";
import { init } from "../libs/snabbdom/src/snabbdom"; import { init } from "../libs/snabbdom/src/snabbdom";
import { VNode } from "../libs/snabbdom/src/vnode"; import { VNode } from "../libs/snabbdom/src/vnode";
import { EventBus } from "./event_bus"; import { EventBus } from "./event_bus";
import { Observer } from "./observer";
import { QWeb } from "./qweb"; import { QWeb } from "./qweb";
import { idGenerator } from "./utils"; import { idGenerator } from "./utils";
@@ -34,6 +35,7 @@ export interface Meta<T extends Env, Props> {
renderProps: Props | null; renderProps: Props | null;
renderPromise: Promise<VNode> | null; renderPromise: Promise<VNode> | null;
boundHandlers: { [key: number]: any }; boundHandlers: { [key: number]: any };
observer: Observer;
} }
const patch = init([sdListeners, sdAttrs, sdProps]); const patch = init([sdListeners, sdAttrs, sdProps]);
@@ -118,7 +120,8 @@ export class Component<
renderId: 1, renderId: 1,
renderPromise: null, renderPromise: null,
renderProps: props || null, renderProps: props || null,
boundHandlers: {} boundHandlers: {},
observer: new Observer()
}; };
} }
@@ -232,6 +235,7 @@ export class Component<
this._visitSubTree(w => { this._visitSubTree(w => {
if (!w.__owl__.isMounted && this.el!.contains(w.el)) { if (!w.__owl__.isMounted && this.el!.contains(w.el)) {
w.__owl__.isMounted = true; w.__owl__.isMounted = true;
this._observeState();
w.mounted(); w.mounted();
return true; return true;
} }
@@ -312,7 +316,6 @@ export class Component<
if (this.__owl__.isMounted) { if (this.__owl__.isMounted) {
await this.render(true); await this.render(true);
} }
this.patched();
} }
async updateProps( async updateProps(
@@ -340,7 +343,6 @@ export class Component<
if (this.__owl__.isStarted) { if (this.__owl__.isStarted) {
await this.render(); await this.render();
} }
this.patched();
} }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -351,7 +353,6 @@ export class Component<
await this.willUpdateProps(nextProps); await this.willUpdateProps(nextProps);
this.props = nextProps; this.props = nextProps;
await this.render(); await this.render();
this.patched();
} }
_patch(vnode) { _patch(vnode) {
@@ -359,6 +360,7 @@ export class Component<
if (this.__owl__.vnode) { if (this.__owl__.vnode) {
this.willPatch(); this.willPatch();
this.__owl__.vnode = patch(this.__owl__.vnode, vnode); this.__owl__.vnode = patch(this.__owl__.vnode, vnode);
this.patched();
} else { } else {
this.__owl__.vnode = patch(document.createElement(vnode.sel!), vnode); this.__owl__.vnode = patch(document.createElement(vnode.sel!), vnode);
} }
@@ -419,6 +421,7 @@ export class Component<
if (this.__owl__.parent) { if (this.__owl__.parent) {
if (this.__owl__.parent!.__owl__.isMounted) { if (this.__owl__.parent!.__owl__.isMounted) {
this.__owl__.isMounted = true; this.__owl__.isMounted = true;
this._observeState();
this.mounted(); this.mounted();
const children = this.__owl__.children; const children = this.__owl__.children;
for (let id in children) { for (let id in children) {
@@ -437,4 +440,13 @@ export class Component<
} }
} }
} }
_observeState() {
if (Object.keys(this.state).length) {
this.__owl__.observer.observe(this.state);
this.__owl__.observer.notifyCB = () => {
this.render();
};
}
}
} }
+28 -5
View File
@@ -259,7 +259,8 @@ describe("lifecycle hooks", () => {
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
expect(hookCounter).toBe(0); // sub widget not created yet expect(hookCounter).toBe(0); // sub widget not created yet
await widget.updateState({ ok: true }); widget.state.ok = true;
await nextTick();
expect(hookCounter).toBe(2); expect(hookCounter).toBe(2);
}); });
@@ -566,7 +567,8 @@ describe("lifecycle hooks", () => {
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
expect(steps).toEqual([]); expect(steps).toEqual([]);
await widget.updateState({ n: 2 }); widget.state.n = 2;
await nextTick();
// Not sure about this order. If you disagree, feel free to open an issue... // Not sure about this order. If you disagree, feel free to open an issue...
expect(steps).toEqual([ expect(steps).toEqual([
@@ -1181,10 +1183,12 @@ describe("async rendering", () => {
const w = new W(env); const w = new W(env);
await w.mount(fixture); await w.mount(fixture);
expect(n).toBe(0); expect(n).toBe(0);
w.updateState({ val: 2 }); w.state.val = 2;
await nextMicroTick();
expect(n).toBe(1); expect(n).toBe(1);
await nextTick(); w.state.val = 3;
w.updateState({ val: 3 }); await nextMicroTick();
expect(n).toBe(2); expect(n).toBe(2);
def.resolve(); def.resolve();
await nextTick(); await nextTick();
@@ -1391,3 +1395,22 @@ describe("updating environment", () => {
expect(fixture.innerHTML).toBe("<div><div>rerendered</div></div>"); expect(fixture.innerHTML).toBe("<div><div>rerendered</div></div>");
}); });
}); });
describe("widget and observable state", () => {
test("widget is rerendered when its state is changed", async () => {
class TestWidget extends Widget {
state = { drink: "water" };
inlineTemplate = `<div><t t-esc="state.drink"/></div>`;
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>water</div>");
widget.state.drink = "beer";
// 2 microtask ticks: one for observer, one for rendering
await nextMicroTick();
await nextMicroTick();
expect(fixture.innerHTML).toBe("<div>beer</div>");
});
});