diff --git a/doc/hooks.md b/doc/hooks.md index c166ae0b..8f02a3d0 100644 --- a/doc/hooks.md +++ b/doc/hooks.md @@ -13,6 +13,7 @@ - [`onWillPatch`](#onwillpatch) - [`onPatched`](#onpatched) - [`useRef`](#useref) + - [`useSubEnv`](#useSubEnv) ## Overview @@ -246,3 +247,29 @@ this.ref2 = useRef("component_2"); References are only guaranteed to be active while the parent component is mounted. If this is not the case, accessing `el` or `comp` on it will return `null`. + +### `useSubEnv` + +The environment is sometimes useful to share some common information between +all components. But sometimes, we want to *scope* that knowledge to a subtree. + +For example, if we have a form view component, maybe we would like to make some +`model` object available to all sub component, but not to the whole application. +This is where the `useSubEnv` hook may be useful: it let a component add some +information to the environment in a way that only the component and its children +can access it: + +```js +class FormComponent extends Component { + constructor(...args) { + super(...args); + const model = makeModel(); + useSubEnv({ model }); + } +} +``` + +The `useSubEnv` takes one argument: an object which contains some key/value that +will be added to the parent environment. Note that it will extend, not replace +the parent environment. And of course, the parent environment will not be +affected. \ No newline at end of file diff --git a/doc/readme.md b/doc/readme.md index 00aac193..1434a24c 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -20,6 +20,7 @@ owl onPatched useState useRef + useSubEnv router Link RouteComponent diff --git a/src/component/component.ts b/src/component/component.ts index c93c764d..3e9e1537 100644 --- a/src/component/component.ts +++ b/src/component/component.ts @@ -380,26 +380,6 @@ export class Component { return true; } - /** - * This method is the correct way to update the environment of a component. Doing - * this will cause a full rerender of the component and its children, so this is - * an operation that should not be done frequently. - * - * A good usecase for updating the environment would be to update some mostly - * static config keys, such as a boolean to determine if we are in mobile - * mode or not. - */ - async updateEnv(nextEnv: Partial): Promise { - const __owl__ = this.__owl__; - if (__owl__.parent && __owl__.parent.env === this.env) { - this.env = Object.create(this.env); - } - Object.assign(this.env, nextEnv); - if (__owl__.isMounted) { - await this.render(true); - } - } - /** * Emit a custom event of type 'eventType' with the given 'payload' on the * component's el, if it exists. However, note that the event will only bubble diff --git a/src/hooks.ts b/src/hooks.ts index 8e3e5a37..adc1cd50 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -12,9 +12,11 @@ import { Observer } from "./core/observer"; * - useRef */ +// ----------------------------------------------------------------------------- +// useState +// ----------------------------------------------------------------------------- + /** - * useState hook - * * This is the main way a component can be made reactive. The useState hook * will return an observed object (or array). Changes to that value will then * trigger a rerendering of the current component. @@ -29,6 +31,9 @@ export function useState(state: T): T { return __owl__.observer.observe(state); } +// ----------------------------------------------------------------------------- +// Life cycle hooks +// ----------------------------------------------------------------------------- function makeLifecycleHook(method: string, reverse: boolean = false) { return function(cb) { const component: Component = Component._current; @@ -51,17 +56,16 @@ function makeLifecycleHook(method: string, reverse: boolean = false) { }; } -/** - * willUnmount hook. The callback will be called when the current component is - * willUnmounted. Note that the component mounted method is called last. - */ export const onMounted = makeLifecycleHook("mountedCB", true); export const onWillUnmount = makeLifecycleHook("willUnmountCB"); export const onWillPatch = makeLifecycleHook("willPatchCB"); export const onPatched = makeLifecycleHook("patchedCB", true); + +// ----------------------------------------------------------------------------- +// useRef +// ----------------------------------------------------------------------------- + /** - * useRef hook - * * The purpose of this hook is to allow components to get a reference to a sub * html node or component. */ @@ -83,3 +87,18 @@ export function useRef(name: string): Ref { } }; } + +// ----------------------------------------------------------------------------- +// useSubEnv +// ----------------------------------------------------------------------------- + + +/** + * This hook is a simple way to let components use a sub environment. Note that + * like for all hooks, it is important that this is only called in the + * constructor method. + */ +export function useSubEnv(nextEnv) { + const component = Component._current; + component.env = Object.assign(Object.create(component.env), nextEnv); +} \ No newline at end of file diff --git a/tests/component/component.test.ts b/tests/component/component.test.ts index f8b68b9e..34467977 100644 --- a/tests/component/component.test.ts +++ b/tests/component/component.test.ts @@ -625,24 +625,6 @@ describe("lifecycle hooks", () => { expect(n).toBe(1); }); - test("patched hook is called after updateEnv", async () => { - let n = 0; - - class TestWidget extends Widget { - state = useState({ a: 1 }); - - patched() { - n++; - } - } - const widget = new TestWidget(env); - await widget.mount(fixture); - expect(n).toBe(0); - - await widget.updateEnv({ isMobile: true }); - expect(n).toBe(1); - }); - test("shouldUpdate hook prevent rerendering", async () => { let shouldUpdate = false; class TestWidget extends Widget { @@ -2815,107 +2797,6 @@ describe("async rendering", () => { }); }); -describe("updating environment", () => { - test("can update widget env", async () => { - const widget = new Widget(env); - expect(widget.env).toBe(env); - await widget.updateEnv({ somekey: 4 }); - expect(widget.env).toBe(env); - expect((widget).env.somekey).toBe(4); - }); - - test("updating widget env does not render widget (if not mounted)", async () => { - let n = 0; - class TestWidget extends Widget { - __render(f) { - n++; - return super.__render(f); - } - } - - const widget = new TestWidget(env); - expect(n).toBe(0); - await widget.updateEnv({ somekey: 4 }); - expect(n).toBe(0); - await widget.mount(fixture); - expect(n).toBe(1); - await widget.updateEnv({ somekey: 5 }); - expect(n).toBe(2); - widget.unmount(); - expect(n).toBe(2); - await widget.updateEnv({ somekey: 5 }); - expect(n).toBe(2); - }); - - test("updating child env does not modify parent env", async () => { - env.qweb.addTemplate("ParentWidget", `
`); - class ParentWidget extends Widget { - static components = { child: Widget }; - } - const parent = new ParentWidget(env); - await parent.mount(fixture); - const child = children(parent)[0]; - expect(child.env).toBe(parent.env); - await child.updateEnv({ somekey: 4 }); - expect(child.env).not.toBe(parent.env); - expect((parent).env.somekey).toBeUndefined(); - }); - - test("updating parent env does modify child env", async () => { - env.qweb.addTemplate("ParentWidget", `
`); - class ParentWidget extends Widget { - static components = { child: Widget }; - } - const parent = new ParentWidget(env); - await parent.mount(fixture); - const child = children(parent)[0]; - expect(child.env.somekey).toBeUndefined(); - await parent.updateEnv({ somekey: 4 }); - expect(child.env.somekey).toBe(4); - }); - - test("updating parent env does modify child env, part 2", async () => { - env.qweb.addTemplate("ParentWidget", `
`); - class ParentWidget extends Widget { - static components = { Child: Widget }; - } - const parent = new ParentWidget(env); - await parent.mount(fixture); - const child = children(parent)[0]; - expect(child.env.somekey).toBeUndefined(); - await child.updateEnv({ somekey: 4 }); - await parent.updateEnv({ someotherkey: 4 }); - expect(child.env.someotherkey).toBe(4); - }); - - test("updating env force a rerender", async () => { - env.qweb.addTemplate("TestWidget", `
`); - class TestWidget extends Widget {} - (env).someKey = "hey"; - const widget = new TestWidget(env); - await widget.mount(fixture); - expect(fixture.innerHTML).toBe("
hey
"); - await widget.updateEnv({ someKey: "rerendered" }); - expect(fixture.innerHTML).toBe("
rerendered
"); - }); - - test("updating env force rerendering children", async () => { - env.qweb.addTemplate("Parent", `
`); - class Child extends Widget {} - class Parent extends Widget { - static components = { Child }; - } - env.qweb.addTemplate("Child", `
`); - (env).someKey = "hey"; - const widget = new Parent(env); - await widget.mount(fixture); - expect(fixture.innerHTML).toBe("
hey
"); - await widget.updateEnv({ someKey: "rerendered" }); - await nextTick(); - expect(fixture.innerHTML).toBe("
rerendered
"); - }); -}); - describe("widget and observable state", () => { test("widget is rerendered when its state is changed", async () => { env.qweb.addTemplate("TestWidget", `
`); diff --git a/tests/hooks.test.ts b/tests/hooks.test.ts index 0aee8876..d149a4fc 100644 --- a/tests/hooks.test.ts +++ b/tests/hooks.test.ts @@ -1,6 +1,14 @@ import { makeTestEnv, makeTestFixture, nextTick } from "./helpers"; import { Component, Env } from "../src/component/component"; -import { useState, onMounted, onWillUnmount, useRef, onPatched, onWillPatch } from "../src/hooks"; +import { + useState, + onMounted, + onWillUnmount, + useRef, + onPatched, + onWillPatch, + useSubEnv +} from "../src/hooks"; import { xml } from "../src/tags"; //------------------------------------------------------------------------------ @@ -245,12 +253,7 @@ describe("hooks", () => { await nextTick(); expect(fixture.innerHTML).toBe("
hey2
"); - expect(steps).toEqual([ - "hook:willPatch2", - "hook:willPatch1", - "hook:patched1", - "hook:patched2" - ]); + expect(steps).toEqual(["hook:willPatch2", "hook:willPatch1", "hook:patched1", "hook:patched2"]); }); describe("autofocus hook", () => { @@ -316,4 +319,41 @@ describe("hooks", () => { expect(input2).toBe(document.activeElement); }); }); + + test("can use sub env", async () => { + class TestComponent extends Component { + static template = xml`
`; + constructor(env) { + super(env); + useSubEnv({ val: 3 }); + } + } + const component = new TestComponent(env); + await component.mount(fixture); + expect(fixture.innerHTML).toBe("
3
"); + expect(env).not.toHaveProperty("val"); + expect(component.env).toHaveProperty("val"); + }); + + test("parent and child env", async () => { + class Child extends Component { + static template = xml`
`; + constructor(env) { + super(env); + useSubEnv({ val: 5 }); + } + } + + class Parent extends Component { + static template = xml`
`; + static components = { Child} + constructor(env) { + super(env); + useSubEnv({ val: 3 }); + } + } + const component = new Parent(env); + await component.mount(fixture); + expect(fixture.innerHTML).toBe( "
3
5
"); + }); });