diff --git a/src/app/app.ts b/src/app/app.ts index 3dd4f76b..0ec70068 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -6,9 +6,13 @@ import { TemplateSet } from "./template_set"; // reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f +export interface Env { + [key: string]: any; +} + interface Config { dev?: boolean; - env?: { [key: string]: any }; + env?: Env; translatableAttributes?: string[]; translateFn?: (s: string) => string; } @@ -21,7 +25,7 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor export class App extends TemplateSet { Root: T; props: any; - env: any = {}; + env: Env = Object.freeze({}); scheduler = new Scheduler(window.requestAnimationFrame.bind(window)); root: ComponentNode | null = null; @@ -36,9 +40,8 @@ export class App extends TemplateSet { this.dev = config.dev; console.info(DEV_MSG); } - if (config.env) { - this.env = config.env; + this.env = Object.freeze(Object.assign({}, config.env)); } if (config.translateFn) { this.translateFn = config.translateFn; diff --git a/src/component/component.ts b/src/component/component.ts index 5b717481..e5726ff0 100644 --- a/src/component/component.ts +++ b/src/component/component.ts @@ -1,3 +1,4 @@ +import type { Env } from "../app/app"; import type { ComponentNode } from "./component_node"; // ----------------------------------------------------------------------------- @@ -9,10 +10,10 @@ export class Component { static style: string = ""; props: any; - env: any; + env: Env; __owl__: ComponentNode; - constructor(props: any, env: any, node: ComponentNode) { + constructor(props: any, env: Env, node: ComponentNode) { this.props = props; this.env = env; this.__owl__ = node; diff --git a/src/component/component_node.ts b/src/component/component_node.ts index b34e261c..d8fcbcc2 100644 --- a/src/component/component_node.ts +++ b/src/component/component_node.ts @@ -1,4 +1,4 @@ -import type { App } from "../app/app"; +import type { App, Env } from "../app/app"; import { BDom, VNode } from "../blockdom"; import { Component } from "./component"; import { @@ -83,6 +83,7 @@ export class ComponentNode implements VNode implements VNode(name: string): { el: T | null } { + const node = getCurrent()!; + return { + get el(): T | null { + return node.refs[name] || null; + }, + }; +} + +// ----------------------------------------------------------------------------- +// useEnv and useSubEnv +// ----------------------------------------------------------------------------- + +/** + * This hook is useful as a building block for some customized hooks, that may + * need a reference to the env of the component calling them. + */ +export function useEnv(): E { + return getCurrent()!.component.env as any; +} + +/** + * 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(envExtension: Env) { + const node = getCurrent()!; + node.childEnv = Object.freeze(Object.assign({}, node.childEnv, envExtension)); +} diff --git a/src/index.ts b/src/index.ts index 72d41ca2..4d3a3a37 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,11 +31,9 @@ export const blockDom = { html, }; -// import { makeBlockClass } from "./_old_bdom/element"; import { App } from "./app/app"; import { Component } from "./component/component"; import { getCurrent } from "./component/component_node"; -// import { getCurrent } from "./b_node"; export { App, Component }; @@ -57,7 +55,7 @@ export { Portal } from "./misc/portal"; export { Memo } from "./misc/memo"; export { css, xml } from "./tags"; export { useState } from "./reactivity"; -export { useRef } from "./refs"; +export { useRef, useEnv, useSubEnv } from "./hooks"; export const utils = { EventBus, whenReady, loadFile }; export { diff --git a/src/refs.ts b/src/refs.ts deleted file mode 100644 index ac1fc44f..00000000 --- a/src/refs.ts +++ /dev/null @@ -1,18 +0,0 @@ -// ----------------------------------------------------------------------------- -// useRef -// ----------------------------------------------------------------------------- - -import { getCurrent } from "./component/component_node"; - -/** - * The purpose of this hook is to allow components to get a reference to a sub - * html node or component. - */ -export function useRef(name: string): { el: T | null } { - const node = getCurrent()!; - return { - get el(): T | null { - return node.refs[name] || null; - }, - }; -} diff --git a/tests/components/__snapshots__/concurrency.test.ts.snap b/tests/components/__snapshots__/concurrency.test.ts.snap index 931f5fb2..e96638c9 100644 --- a/tests/components/__snapshots__/concurrency.test.ts.snap +++ b/tests/components/__snapshots__/concurrency.test.ts.snap @@ -1132,7 +1132,7 @@ exports[`rendering component again in next microtick 2`] = ` let b2; const v1 = ctx['onClick']; let d1 = [v1, ctx]; - if (ctx['env'].flag) { + if (ctx['env'].config.flag) { b2 = component(\`Child\`, {}, key + \`__2\`, node, ctx); } return block1([d1], [b2]); diff --git a/tests/components/__snapshots__/higher_order_component.test.ts.snap b/tests/components/__snapshots__/higher_order_component.test.ts.snap index 8ce47ed9..42051371 100644 --- a/tests/components/__snapshots__/higher_order_component.test.ts.snap +++ b/tests/components/__snapshots__/higher_order_component.test.ts.snap @@ -63,10 +63,10 @@ exports[`basics can select a sub widget 3`] = ` return function template(ctx, node, key = \\"\\") { let b2,b3; - if (ctx['env'].flag) { + if (ctx['env'].options.flag) { b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx); } - if (!ctx['env'].flag) { + if (!ctx['env'].options.flag) { b3 = component(\`OtherChild\`, {}, key + \`__2\`, node, ctx); } return multi([b2, b3]); diff --git a/tests/components/__snapshots__/hooks.test.ts.snap b/tests/components/__snapshots__/hooks.test.ts.snap index 060da9c8..b829c6d3 100644 --- a/tests/components/__snapshots__/hooks.test.ts.snap +++ b/tests/components/__snapshots__/hooks.test.ts.snap @@ -66,6 +66,21 @@ exports[`hooks can use onWillStart, onWillUpdateProps 2`] = ` }" `; +exports[`hooks can use sub env 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['env'].val; + return block1([d1]); + } +}" +`; + exports[`hooks can use useComponent 1`] = ` "function anonymous(bdom, helpers ) { @@ -80,6 +95,21 @@ exports[`hooks can use useComponent 1`] = ` }" `; +exports[`hooks can use useEnv 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['env'].val; + return block1([d1]); + } +}" +`; + exports[`hooks mounted callbacks should be called in reverse order from willUnmount callbacks 1`] = ` "function anonymous(bdom, helpers ) { @@ -95,6 +125,35 @@ exports[`hooks mounted callbacks should be called in reverse order from willUnmo }" `; +exports[`hooks parent and child env 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['env'].val; + return block1([d1]); + } +}" +`; + +exports[`hooks parent and child env 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + return function template(ctx, node, key = \\"\\") { + let b2 = text(ctx['env'].val); + let b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx); + return multi([b2, b3]); + } +}" +`; + exports[`hooks two different call to willPatch/patched should work 1`] = ` "function anonymous(bdom, helpers ) { diff --git a/tests/components/concurrency.test.ts b/tests/components/concurrency.test.ts index 914720ba..c9257d0a 100644 --- a/tests/components/concurrency.test.ts +++ b/tests/components/concurrency.test.ts @@ -587,7 +587,7 @@ test("rendering component again in next microtick", async () => { static template = xml`
- +
`; static components = { Child }; @@ -595,14 +595,15 @@ test("rendering component again in next microtick", async () => { useLogLifecycle(steps); } async onClick() { - this.env.flag = true; + this.env.config.flag = true; this.render(); await Promise.resolve(); this.render(); } } - await mount(Parent, fixture); + const env = { config: { flag: false } }; + await new App(Parent).configure({ env }).mount(fixture); expect(fixture.innerHTML).toBe("
"); fixture.querySelector("button")!.click(); await nextTick(); diff --git a/tests/components/env.test.ts b/tests/components/env.test.ts index 9ba9e6f5..a014b29e 100644 --- a/tests/components/env.test.ts +++ b/tests/components/env.test.ts @@ -8,17 +8,6 @@ beforeEach(() => { }); describe("env handling", () => { - test("keeps a reference to env", async () => { - const env = {}; - class Test extends Component { - static template = xml`
`; - } - const app = new App(Test); - app.configure({ env }); - const component = await app.mount(fixture); - expect(component.env).toBe(env); - }); - test("has an env by default", async () => { class Test extends Component { static template = xml`
`; @@ -27,8 +16,23 @@ describe("env handling", () => { expect(component.env).toEqual({}); }); + test("env is shallow frozen", async () => { + const env = { foo: 42, bar: { value: 42 } }; + class Test extends Component { + static template = xml`
`; + } + const component = await new App(Test).configure({ env }).mount(fixture); + expect(Object.isFrozen(component.env)).toBeTruthy(); + expect(component.env).toEqual({ foo: 42, bar: { value: 42 } }); + expect(() => { + component.env.foo = 23; + }).toThrow(/Cannot assign to read only property 'foo' of object/); + component.env.bar.value = 23; + expect(component.env).toEqual({ foo: 42, bar: { value: 23 } }); + }); + test("parent env is propagated to child components", async () => { - const env = {}; + const env = { foo: 42, bar: { value: 42 } }; let child: any = null; class Child extends Component { @@ -43,9 +47,7 @@ describe("env handling", () => { static components = { Child }; } - const app = new App(Test); - app.configure({ env }); - await app.mount(fixture); - expect(child.env).toBe(env); + await new App(Test).configure({ env }).mount(fixture); + expect(child.env).toEqual(env); }); }); diff --git a/tests/components/higher_order_component.test.ts b/tests/components/higher_order_component.test.ts index 45c633f2..ffb63f4c 100644 --- a/tests/components/higher_order_component.test.ts +++ b/tests/components/higher_order_component.test.ts @@ -58,19 +58,17 @@ describe("basics", () => { class Parent extends Component { static template = xml` - - + + `; static components = { Child, OtherChild }; } - const env = { flag: true }; - const app = new App(Parent); - app.configure({ env }); - const parent = await app.mount(fixture); + const env = { options: { flag: true } }; + const parent = await new App(Parent).configure({ env }).mount(fixture); expect(fixture.innerHTML).toBe("CHILD 1"); - env.flag = false; + env.options.flag = false; await parent.render(); expect(fixture.innerHTML).toBe("
CHILD 2
"); }); diff --git a/tests/components/hooks.test.ts b/tests/components/hooks.test.ts index 9a36d527..2088ba13 100644 --- a/tests/components/hooks.test.ts +++ b/tests/components/hooks.test.ts @@ -5,6 +5,8 @@ import { useRef, useState, useComponent, + useEnv, + useSubEnv, onMounted, onPatched, onWillStart, @@ -12,7 +14,7 @@ import { onWillPatch, xml, onWillUnmount, -} from "../../src"; +} from "../../src/index"; import { makeTestFixture, nextTick, snapshotEverything } from "../helpers"; let fixture: HTMLElement; @@ -163,27 +165,30 @@ describe("hooks", () => { }); }); - test.skip("can use useEnv", async () => { - expect.assertions(2); + test("can use useEnv", async () => { + expect.assertions(3); class Test extends Component { static template = xml`
`; setup() { - //expect(useEnv()).toBe(this.env); + expect(useEnv()).toBe(this.env); } } - await mount(Test, fixture); + const env = { val: 1 }; + await new App(Test).configure({ env }).mount(fixture); + expect(fixture.innerHTML).toBe("
1
"); }); - test.skip("can use sub env", async () => { + test("use sub env does not pollute user env", async () => { class Test extends Component { static template = xml`
`; setup() { - //useSubEnv({ val: 3 }); + useSubEnv({ val2: 1 }); } } - const component = await mount(Test, fixture); + const env = { val: 3 }; + const component = await new App(Test).configure({ env }).mount(fixture); expect(fixture.innerHTML).toBe("
3
"); - expect(component.env).not.toHaveProperty("val"); + expect(component.env).not.toHaveProperty("val2"); expect(component.env).toHaveProperty("val"); }); @@ -198,22 +203,20 @@ describe("hooks", () => { await mount(Test, fixture); }); - test.skip("parent and child env", async () => { + test("parent and child env", async () => { class Child extends Component { static template = xml`
`; - super() { - //useSubEnv({ val: 5 }); - } } class Parent extends Component { static template = xml``; static components = { Child }; setup() { - //useSubEnv({ val: 3 }); + useSubEnv({ val: 5 }); } } - mount(Parent, fixture); + const env = { val: 3 }; + await new App(Parent).configure({ env }).mount(fixture); expect(fixture.innerHTML).toBe("3
5
"); });