[IMP] hooks: introduce useChildSubEnv and change useSubEnv

This commit is contained in:
Géry Debongnie
2022-01-24 14:39:12 +01:00
committed by Samuel Degueldre
parent d7850aaf7a
commit 4a971f2963
8 changed files with 183 additions and 31 deletions
+7 -10
View File
@@ -73,6 +73,8 @@ All changes are documented here in no particular order.
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect)) - new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
- breaking: `Context` is removed ([details](#15-context-is-removed)) - breaking: `Context` is removed ([details](#15-context-is-removed))
- breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty)) - breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty))
- breaking: `env` is now frozen ([details](#28-env-is-now-frozen))
- new hook: `useChildSubEnv` (only applies to child components) ([details](#27-usechildsubenv-only-applies-to-child-components))
- breaking: most exports are exported at top level ([details](#18-most-exports-are-exported-at-top-level)) - breaking: most exports are exported at top level ([details](#18-most-exports-are-exported-at-top-level))
- breaking: properties are no longer set as attributes ([details](#19-properties-are-no-longer-set-as-attributes)) - breaking: properties are no longer set as attributes ([details](#19-properties-are-no-longer-set-as-attributes))
- breaking: `EventBus` api changed: it is now an `EventTarget` ([details](#21-eventbus-api-changed-it-is-now-an-eventtarget)) - breaking: `EventBus` api changed: it is now an `EventTarget` ([details](#21-eventbus-api-changed-it-is-now-an-eventtarget))
@@ -81,8 +83,6 @@ All changes are documented here in no particular order.
- breaking: transition system is removed ([details](#24-transition-system-is-removed)) - breaking: transition system is removed ([details](#24-transition-system-is-removed))
- breaking: no more global components or templates ([details](#25-no-more-global-components-or-templates)) - breaking: no more global components or templates ([details](#25-no-more-global-components-or-templates))
- breaking: `AsyncRoot` utility component is removed ([details](#26-asyncroot-utility-component-is-removed)) - breaking: `AsyncRoot` utility component is removed ([details](#26-asyncroot-utility-component-is-removed))
- breaking: `useSubEnv` only applies to child components ([details](#27-usesubenv-only-applies-to-child-components))
- breaking: `env` is now frozen ([details](#28-env-is-now-frozen))
- breaking: `renderToString` function on qweb has been removed ([details](#32-rendertostring-on-qweb-has-been-removed)) - breaking: `renderToString` function on qweb has been removed ([details](#32-rendertostring-on-qweb-has-been-removed))
- breaking: `debounce` utility function has been removed ([details](#34-debounce-utility-function-has-been-removed)) - breaking: `debounce` utility function has been removed ([details](#34-debounce-utility-function-has-been-removed))
- breaking: `browser` object has been removed ([details](#39-browser-object-has-been-removed)) - breaking: `browser` object has been removed ([details](#39-browser-object-has-been-removed))
@@ -565,15 +565,12 @@ either a fallback when the data is not ready, or the actual component with data
as props. If there is no escape, and `AsyncRoot` is needed, please reach out to as props. If there is no escape, and `AsyncRoot` is needed, please reach out to
us so we can study this usecase. us so we can study this usecase.
### 27. `useSubEnv` only applies to child components ### 27. `useChildSubEnv` (only applies to child components)
In Owl 1, a call to `useSubEnv` would define a new environment for the children In Owl, a call to `useSubEnv` would define a new environment for the children
AND the component. It now only defines an environment for the children. AND the component. It is very useful, but in some cases, one only need to update
the children component environment. This can now be done with a new hook:
Rationale: This was a subtle cause for bugs: some code had to be rrun [`useChildSubEnv`](doc/reference/hooks.md#usesubenv-and-usechildsubenv)
before the call to `useSubEnv`, otherwise it could interfere with the sub environment.
Documentation: [Hooks](doc/reference/hooks.md#usesubenv)
### 28. `env` is now frozen ### 28. `env` is now frozen
+2 -1
View File
@@ -36,7 +36,8 @@ Other hooks:
- [`useEnv`](reference/hooks.md#useenv): return a reference to the current env - [`useEnv`](reference/hooks.md#useenv): return a reference to the current env
- [`useExternalListener`](reference/hooks.md#useexternallistener): add a listener outside of a component DOM - [`useExternalListener`](reference/hooks.md#useexternallistener): add a listener outside of a component DOM
- [`useRef`](reference/hooks.md#useref): get an object representing a reference (`t-ref`) - [`useRef`](reference/hooks.md#useref): get an object representing a reference (`t-ref`)
- [`useSubEnv`](reference/hooks.md#usesubenv): extend the current env with additional information for child components - [`useChildSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for child components)
- [`useSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for current component and child components)
Utility/helpers: Utility/helpers:
+1 -1
View File
@@ -52,7 +52,7 @@ It is sometimes useful to add one (or more) specific keys to the environment,
from the perspective of a specific component and its children. In that case, the from the perspective of a specific component and its children. In that case, the
solution presented above will not work, since it sets the global environment. solution presented above will not work, since it sets the global environment.
There is a hook for this situation: [`useSubEnv`](hooks.md#usesubenv). There are two hooks for this situation: [`useSubEnv` and `useChildSubEnv`](hooks.md#usesubenv-and-usechildsubenv).
```js ```js
class SomeComponent extends Component { class SomeComponent extends Component {
+18 -7
View File
@@ -8,7 +8,7 @@
- [Other hooks](#other-hooks) - [Other hooks](#other-hooks)
- [`useState`](#usestate) - [`useState`](#usestate)
- [`useRef`](#useref) - [`useRef`](#useref)
- [`useSubEnv`](#usesubenv) - [`useSubEnv` and `useChildSubEnv`](#usesubenv-and-usechildsubenv)
- [`useExternalListener`](#useexternallistener) - [`useExternalListener`](#useexternallistener)
- [`useComponent`](#usecomponent) - [`useComponent`](#usecomponent)
- [`useEnv`](#useenv) - [`useEnv`](#useenv)
@@ -152,14 +152,14 @@ this.ref2 = useRef("component_2");
References are only guaranteed to be active while the parent component is mounted. 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`. If this is not the case, accessing `el` or `comp` on it will return `null`.
### `useSubEnv` ### `useSubEnv` and `useChildSubEnv`
The environment is sometimes useful to share some common information between The environment is sometimes useful to share some common information between
all components. But sometimes, we want to _scope_ that knowledge to a subtree. 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 For example, if we have a form view component, maybe we would like to make some
`model` object available to all sub components, but not to the whole application. `model` object available to all sub components, but not to the whole application.
This is where the `useSubEnv` hook may be useful: it lets a component add some This is where the `useChildSubEnv` hook may be useful: it lets a component add some
information to the environment in a way that only its children information to the environment in a way that only its children
can access it: can access it:
@@ -167,15 +167,26 @@ can access it:
class FormComponent extends Component { class FormComponent extends Component {
setup() { setup() {
const model = makeModel(); const model = makeModel();
// model will be available on this.env for this component and all children
useSubEnv({ model }); useSubEnv({ model });
// someKey will be available on this.env for all children
useChildSubEnv({ someKey: "value" });
} }
} }
``` ```
The `useSubEnv` takes one argument: an object which contains some key/value that The `useSubEnv` and `useChildSubEnv` hooks take one argument: an object which
will be added to the parent environment. Note that it will extend, not replace contains some key/value that will be added to the current environment. These hooks
the parent environment. And of course, the parent environment will not be will create a new env object with the new information:
affected.
- `useSubEnv` will assign this new `env` to itself and to all children components
- `useChildSubEnv` will only assign this new `env` to all children components.
As usual in Owl, [environments](environment.md) created with these two hooks are
frozen, to prevent unwanted modifications.
Note that both these hooks can be called an arbitrary number of times. The `env`
will then be updated accordingly.
### `useExternalListener` ### `useExternalListener`
+13 -3
View File
@@ -32,6 +32,12 @@ export function useEnv<E extends Env>(): E {
return getCurrent()!.component.env as any; return getCurrent()!.component.env as any;
} }
function extendEnv(currentEnv: Object, extension: Object): Object {
const env = Object.create(currentEnv);
const descrs = Object.getOwnPropertyDescriptors(extension);
return Object.freeze(Object.defineProperties(env, descrs));
}
/** /**
* This hook is a simple way to let components use a sub environment. Note that * 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 * like for all hooks, it is important that this is only called in the
@@ -39,11 +45,15 @@ export function useEnv<E extends Env>(): E {
*/ */
export function useSubEnv(envExtension: Env) { export function useSubEnv(envExtension: Env) {
const node = getCurrent()!; const node = getCurrent()!;
const env = Object.create(node.childEnv); const newEnv = extendEnv(node.component.env as any, envExtension);
const descrs = Object.getOwnPropertyDescriptors(envExtension); node.component.env = extendEnv(node.component.env as any, envExtension);
node.childEnv = Object.freeze(Object.defineProperties(env, descrs)); node.childEnv = newEnv;
} }
export function useChildSubEnv(envExtension: Env) {
const node = getCurrent()!;
node.childEnv = extendEnv(node.childEnv, envExtension);
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// useEffect // useEffect
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
+1 -1
View File
@@ -43,7 +43,7 @@ export { status } from "./component/status";
export { Memo } from "./memo"; export { Memo } from "./memo";
export { xml } from "./app/template_set"; export { xml } from "./app/template_set";
export { useState, reactive, markRaw, toRaw } from "./reactivity"; export { useState, reactive, markRaw, toRaw } from "./reactivity";
export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks"; export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
export { EventBus, whenReady, loadFile, markup } from "./utils"; export { EventBus, whenReady, loadFile, markup } from "./utils";
export { export {
onWillStart, onWillStart,
@@ -103,7 +103,7 @@ exports[`hooks mounted callbacks should be called in reverse order from willUnmo
}" }"
`; `;
exports[`hooks parent and child env 1`] = ` exports[`hooks parent and child env (with useChildSubEnv) 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -116,7 +116,34 @@ exports[`hooks parent and child env 1`] = `
}" }"
`; `;
exports[`hooks parent and child env 2`] = ` exports[`hooks parent and child env (with useChildSubEnv) 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['env'].val;
return block1([txt1]);
}
}"
`;
exports[`hooks parent and child env (with useSubEnv) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
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 parent and child env (with useSubEnv) 2`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -144,7 +171,7 @@ exports[`hooks two different call to willPatch/patched should work 1`] = `
}" }"
`; `;
exports[`hooks use sub env does not pollute user env 1`] = ` exports[`hooks useChildSubEnv does not pollute user env 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -158,7 +185,7 @@ exports[`hooks use sub env does not pollute user env 1`] = `
}" }"
`; `;
exports[`hooks use sub env supports arbitrary descriptor 1`] = ` exports[`hooks useChildSubEnv supports arbitrary descriptor 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -169,7 +196,7 @@ exports[`hooks use sub env supports arbitrary descriptor 1`] = `
}" }"
`; `;
exports[`hooks use sub env supports arbitrary descriptor 2`] = ` exports[`hooks useChildSubEnv supports arbitrary descriptor 2`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -287,3 +314,43 @@ exports[`hooks useRef hook: basic use 1`] = `
} }
}" }"
`; `;
exports[`hooks useSubEnv modifies user env 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['env'].val;
return block1([txt1]);
}
}"
`;
exports[`hooks useSubEnv supports arbitrary descriptor 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`hooks useSubEnv supports arbitrary descriptor 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/> <block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['env'].someVal;
let txt2 = ctx['env'].someVal2;
return block1([txt1, txt2]);
}
}"
`;
+69 -3
View File
@@ -14,6 +14,7 @@ import {
useExternalListener, useExternalListener,
useRef, useRef,
useState, useState,
useChildSubEnv,
useSubEnv, useSubEnv,
xml, xml,
} from "../../src/index"; } from "../../src/index";
@@ -180,7 +181,7 @@ describe("hooks", () => {
expect(fixture.innerHTML).toBe("<div>1</div>"); expect(fixture.innerHTML).toBe("<div>1</div>");
}); });
test("use sub env does not pollute user env", async () => { test("useSubEnv modifies user env", async () => {
class Test extends Component { class Test extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`; static template = xml`<div><t t-esc="env.val"/></div>`;
setup() { setup() {
@@ -190,11 +191,25 @@ describe("hooks", () => {
const env = { val: 3 }; const env = { val: 3 };
const component = await mount(Test, fixture, { env }); const component = await mount(Test, fixture, { env });
expect(fixture.innerHTML).toBe("<div>3</div>"); expect(fixture.innerHTML).toBe("<div>3</div>");
expect(component.env).toHaveProperty("val2");
expect(component.env).toHaveProperty("val");
});
test("useChildSubEnv does not pollute user env", async () => {
class Test extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
setup() {
useChildSubEnv({ val2: 1 });
}
}
const env = { val: 3 };
const component = await mount(Test, fixture, { env });
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(component.env).not.toHaveProperty("val2"); expect(component.env).not.toHaveProperty("val2");
expect(component.env).toHaveProperty("val"); expect(component.env).toHaveProperty("val");
}); });
test("use sub env supports arbitrary descriptor", async () => { test("useSubEnv supports arbitrary descriptor", async () => {
let someVal = "maggot"; let someVal = "maggot";
let someVal2 = "brain"; let someVal2 = "brain";
@@ -213,6 +228,40 @@ describe("hooks", () => {
}); });
} }
} }
const env = {
get someVal() {
return someVal;
},
};
const component = await mount(Test, fixture, { env });
expect(fixture.innerHTML).toBe("<div>maggot brain</div>");
someVal = "brain";
someVal2 = "maggot";
component.render();
await nextTick();
expect(fixture.innerHTML).toBe("<div>brain maggot</div>");
});
test("useChildSubEnv supports arbitrary descriptor", async () => {
let someVal = "maggot";
let someVal2 = "brain";
class Child extends Component {
static template = xml`<div><t t-esc="env.someVal" /> <t t-esc="env.someVal2" /></div>`;
}
class Test extends Component {
static template = xml`<Child />`;
static components = { Child };
setup() {
useChildSubEnv({
get someVal2() {
return someVal2;
},
});
}
}
someVal = "maggot"; someVal = "maggot";
const env = { const env = {
get someVal() { get someVal() {
@@ -239,7 +288,7 @@ describe("hooks", () => {
await mount(Test, fixture); await mount(Test, fixture);
}); });
test("parent and child env", async () => { test("parent and child env (with useSubEnv)", async () => {
class Child extends Component { class Child extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`; static template = xml`<div><t t-esc="env.val"/></div>`;
} }
@@ -253,6 +302,23 @@ describe("hooks", () => {
} }
const env = { val: 3 }; const env = { val: 3 };
await mount(Parent, fixture, { env }); await mount(Parent, fixture, { env });
expect(fixture.innerHTML).toBe("5<div>5</div>");
});
test("parent and child env (with useChildSubEnv)", async () => {
class Child extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
}
class Parent extends Component {
static template = xml`<t t-esc="env.val"/><Child/>`;
static components = { Child };
setup() {
useChildSubEnv({ val: 5 });
}
}
const env = { val: 3 };
await mount(Parent, fixture, { env });
expect(fixture.innerHTML).toBe("3<div>5</div>"); expect(fixture.innerHTML).toBe("3<div>5</div>");
}); });