mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
+108
@@ -0,0 +1,108 @@
|
|||||||
|
# 🦉 Context 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Example](#example)
|
||||||
|
- [Reference](#reference)
|
||||||
|
- [`Context`](#context)
|
||||||
|
- [`useContext`](#usecontext)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `Context` object provides a way to share data between an arbitrary number
|
||||||
|
of component. Usually, data is passed from a parent to its children component,
|
||||||
|
but when we have to deal with some mostly global information, this can be
|
||||||
|
annoying, since each component will need to pass the information to each children,
|
||||||
|
even though some or most of them will not use the information.
|
||||||
|
|
||||||
|
With a `Context` object, each component can subscribe (with the `useContext` hook)
|
||||||
|
to its state, and will be updated whenever the context state is updated.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Assume that we have an application with various components which needs to render
|
||||||
|
differently depending on the size of the device. Here is how we could proceed
|
||||||
|
to make sure that the information is properly shared. First, let us create a
|
||||||
|
context, and add it to the environment:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const deviceContext = new Context({ isMobile: true });
|
||||||
|
const env = {
|
||||||
|
qweb: new QWeb(TEMPLATES),
|
||||||
|
deviceContext
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
If we want to make it completely responsive, we need to update its value whenever
|
||||||
|
the size of the screen is updated:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const isMobile = () => window.innerWidth <= 768;
|
||||||
|
window.addEventListener(
|
||||||
|
"resize",
|
||||||
|
owl.utils.debounce(() => {
|
||||||
|
const state = deviceContext.state;
|
||||||
|
if (state.isMobile !== isMobile()) {
|
||||||
|
state.isMobile = !state.isMobile;
|
||||||
|
}
|
||||||
|
}, 15)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, each component that want can subscribe and render differently depending on the
|
||||||
|
fact that we are in a mobile or desktop mode.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<t t-if=device.isMobile>
|
||||||
|
some simplified user interface
|
||||||
|
</t>
|
||||||
|
<t t-else="1">
|
||||||
|
some more sopthisticated user interface
|
||||||
|
</t>
|
||||||
|
`;
|
||||||
|
device = useContext(this.env.deviceContext);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
### `Context`
|
||||||
|
|
||||||
|
A `Context` object should be created with a state object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const someContext = new Context({ some: "key" });
|
||||||
|
```
|
||||||
|
|
||||||
|
Its state is now available in the `state` key:
|
||||||
|
|
||||||
|
```js
|
||||||
|
someContext.state.some = "other key";
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the way some global code (such as the responsive code above) should
|
||||||
|
read and update the context state. However, components should not ever read the
|
||||||
|
context state directly from the context, they should instead use the `useContext`
|
||||||
|
hook to properly register themselves to state changes.
|
||||||
|
|
||||||
|
Note that the `Context` hook is different from the React version. For example,
|
||||||
|
there is no concept of provider/consumer. So, the `Context` feature does not
|
||||||
|
by itself allow the use of a different context state depending on the component
|
||||||
|
place in the component tree. However, this functionality can be obtained, if
|
||||||
|
necessary, with the use of sub environment.
|
||||||
|
|
||||||
|
### `useContext`
|
||||||
|
|
||||||
|
The `useContext` hook is the normal way for a component to register themselve
|
||||||
|
to context state changes. The `useContext` method returns the context state:
|
||||||
|
|
||||||
|
```js
|
||||||
|
device = useContext(this.env.deviceContext);
|
||||||
|
```
|
||||||
|
|
||||||
|
It is a simple observed state (with an owl `Observer`), which contains the shared
|
||||||
|
information.
|
||||||
+8
-4
@@ -12,6 +12,7 @@
|
|||||||
- [`onWillUnmount`](#onwillunmount)
|
- [`onWillUnmount`](#onwillunmount)
|
||||||
- [`onWillPatch`](#onwillpatch)
|
- [`onWillPatch`](#onwillpatch)
|
||||||
- [`onPatched`](#onpatched)
|
- [`onPatched`](#onpatched)
|
||||||
|
- [`useContext`](#usecontext)
|
||||||
- [`useRef`](#useref)
|
- [`useRef`](#useref)
|
||||||
- [`useSubEnv`](#usesubenv)
|
- [`useSubEnv`](#usesubenv)
|
||||||
|
|
||||||
@@ -182,7 +183,6 @@ is mounted (see example on top of this page).
|
|||||||
abstractions. `onWillUnmount` registers a callback, which will be called when the component
|
abstractions. `onWillUnmount` registers a callback, which will be called when the component
|
||||||
is unmounted (see example on top of this page).
|
is unmounted (see example on top of this page).
|
||||||
|
|
||||||
|
|
||||||
### `onWillPatch`
|
### `onWillPatch`
|
||||||
|
|
||||||
`onWillPatch` is not an user hook, but is a building block designed to help make useful
|
`onWillPatch` is not an user hook, but is a building block designed to help make useful
|
||||||
@@ -195,6 +195,10 @@ before the component patched.
|
|||||||
abstractions. `onPatched` registers a callback, which will be called just
|
abstractions. `onPatched` registers a callback, which will be called just
|
||||||
after the component patched.
|
after the component patched.
|
||||||
|
|
||||||
|
### `useContext`
|
||||||
|
|
||||||
|
See [`useContext`](context.md#usecontext) for reference documentation.
|
||||||
|
|
||||||
### `useRef`
|
### `useRef`
|
||||||
|
|
||||||
The `useRef` hook is useful when we need a way to interact with some inside part
|
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||||
@@ -251,7 +255,7 @@ If this is not the case, accessing `el` or `comp` on it will return `null`.
|
|||||||
### `useSubEnv`
|
### `useSubEnv`
|
||||||
|
|
||||||
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 component, but not to the whole application.
|
`model` object available to all sub component, but not to the whole application.
|
||||||
@@ -271,5 +275,5 @@ class FormComponent extends Component {
|
|||||||
|
|
||||||
The `useSubEnv` takes one argument: an object which contains some key/value that
|
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
|
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
|
the parent environment. And of course, the parent environment will not be
|
||||||
affected.
|
affected.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ build applications. Here is a complete representation of its content:
|
|||||||
```
|
```
|
||||||
owl
|
owl
|
||||||
Component
|
Component
|
||||||
|
Context
|
||||||
QWeb
|
QWeb
|
||||||
useState
|
useState
|
||||||
core
|
core
|
||||||
@@ -18,6 +19,7 @@ owl
|
|||||||
onWillUnmount
|
onWillUnmount
|
||||||
onWillPatch
|
onWillPatch
|
||||||
onPatched
|
onPatched
|
||||||
|
useContext
|
||||||
useState
|
useState
|
||||||
useRef
|
useRef
|
||||||
useSubEnv
|
useSubEnv
|
||||||
@@ -44,6 +46,7 @@ Note that for convenience, the `useState` hook is also exported at the root of t
|
|||||||
|
|
||||||
- [Animations](animations.md)
|
- [Animations](animations.md)
|
||||||
- [Component](component.md)
|
- [Component](component.md)
|
||||||
|
- [Context](context.md)
|
||||||
- [Event Bus](event_bus.md)
|
- [Event Bus](event_bus.md)
|
||||||
- [Hooks](hooks.md)
|
- [Hooks](hooks.md)
|
||||||
- [Observer](observer.md)
|
- [Observer](observer.md)
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Component } from "./component/component";
|
||||||
|
import { Observer } from "./core/observer";
|
||||||
|
import { EventBus } from "./core/event_bus";
|
||||||
|
import { onWillUnmount } from "./hooks";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `Context` object provides a way to share data between an arbitrary number
|
||||||
|
* of component. Usually, data is passed from a parent to its children component,
|
||||||
|
* but when we have to deal with some mostly global information, this can be
|
||||||
|
* annoying, since each component will need to pass the information to each
|
||||||
|
* children, even though some or most of them will not use the information.
|
||||||
|
*
|
||||||
|
* With a `Context` object, each component can subscribe (with the `useContext`
|
||||||
|
* hook) to its state, and will be updated whenever the context state is updated.
|
||||||
|
*/
|
||||||
|
export class Context extends EventBus {
|
||||||
|
state: any;
|
||||||
|
observer: Observer;
|
||||||
|
id: number = 1;
|
||||||
|
// mapping from component id to last observed context id
|
||||||
|
mapping: { [componentId: number]: number } = {};
|
||||||
|
|
||||||
|
constructor(state: Object = {}) {
|
||||||
|
super();
|
||||||
|
this.observer = new Observer();
|
||||||
|
this.observer.notifyCB = this.__notifyComponents.bind(this);
|
||||||
|
this.state = this.observer.observe(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instead of using trigger to emit an update event, we actually implement
|
||||||
|
* our own function to do that. The reason is that we need to be smarter than
|
||||||
|
* a simple trigger function: we need to wait for parent components to be
|
||||||
|
* done before doing children components. The reason is that if an update
|
||||||
|
* as an effect of destroying a children, we do not want to call the
|
||||||
|
* mapStoreToProps function of the child, nor rendering it.
|
||||||
|
*
|
||||||
|
* This method is not optimal if we have a bunch of asynchronous components:
|
||||||
|
* we wait sequentially for each component to be completed before updating the
|
||||||
|
* next. However, the only things that matters is that children are updated
|
||||||
|
* after their parents. So, this could be optimized by being smarter, and
|
||||||
|
* updating all widgets concurrently, except for parents/children.
|
||||||
|
*/
|
||||||
|
async __notifyComponents() {
|
||||||
|
const id = ++this.id;
|
||||||
|
const subs = this.subscriptions.update || [];
|
||||||
|
for (let i = 0, iLen = subs.length; i < iLen; i++) {
|
||||||
|
const sub = subs[i];
|
||||||
|
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
|
||||||
|
if (shouldCallback) {
|
||||||
|
await sub.callback.call(sub.owner, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The`useContext` hook is the normal way for a component to register themselve
|
||||||
|
* to context state changes. The `useContext` method returns the context state
|
||||||
|
*/
|
||||||
|
export function useContext(ctx: Context): any {
|
||||||
|
const component: Component<any, any> = Component._current;
|
||||||
|
const __owl__ = component.__owl__;
|
||||||
|
const id = __owl__.id;
|
||||||
|
const mapping = ctx.mapping;
|
||||||
|
if (id in mapping) {
|
||||||
|
return ctx.state;
|
||||||
|
}
|
||||||
|
mapping[id] = 0;
|
||||||
|
const renderFn = __owl__.render;
|
||||||
|
__owl__.render = function(comp, params) {
|
||||||
|
mapping[id] = ctx.id;
|
||||||
|
return renderFn(comp, params);
|
||||||
|
};
|
||||||
|
ctx.on("update", component, async contextId => {
|
||||||
|
if (mapping[id] < contextId) {
|
||||||
|
mapping[id] = contextId;
|
||||||
|
await component.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onWillUnmount(() => {
|
||||||
|
ctx.off("update", component);
|
||||||
|
delete mapping[id];
|
||||||
|
});
|
||||||
|
return ctx.state;
|
||||||
|
}
|
||||||
+3
-1
@@ -12,6 +12,7 @@ import { Store } from "./store/store";
|
|||||||
import * as _utils from "./utils";
|
import * as _utils from "./utils";
|
||||||
import * as _tags from "./tags";
|
import * as _tags from "./tags";
|
||||||
import * as _hooks from "./hooks";
|
import * as _hooks from "./hooks";
|
||||||
|
import * as _context from "./Context";
|
||||||
import { Link } from "./router/Link";
|
import { Link } from "./router/Link";
|
||||||
import { RouteComponent } from "./router/RouteComponent";
|
import { RouteComponent } from "./router/RouteComponent";
|
||||||
import { Router } from "./router/Router";
|
import { Router } from "./router/Router";
|
||||||
@@ -19,13 +20,14 @@ import { Router } from "./router/Router";
|
|||||||
export { Component } from "./component/component";
|
export { Component } from "./component/component";
|
||||||
export { QWeb };
|
export { QWeb };
|
||||||
|
|
||||||
|
export const Context = _context.Context;
|
||||||
export const useState = _hooks.useState;
|
export const useState = _hooks.useState;
|
||||||
export const core = { EventBus, Observer };
|
export const core = { EventBus, Observer };
|
||||||
export const router = { Router, RouteComponent, Link };
|
export const router = { Router, RouteComponent, Link };
|
||||||
export const store = { Store, ConnectedComponent };
|
export const store = { Store, ConnectedComponent };
|
||||||
export const utils = _utils;
|
export const utils = _utils;
|
||||||
export const tags = _tags;
|
export const tags = _tags;
|
||||||
export const hooks = _hooks;
|
export const hooks = Object.assign({}, _hooks, { useContext: _context.useContext });
|
||||||
export const __info__ = {};
|
export const __info__ = {};
|
||||||
|
|
||||||
Object.defineProperty(__info__, "mode", {
|
Object.defineProperty(__info__, "mode", {
|
||||||
|
|||||||
+4
-42
@@ -1,6 +1,5 @@
|
|||||||
import { Env } from "../component/component";
|
import { Env } from "../component/component";
|
||||||
import { EventBus } from "../core/event_bus";
|
import { Context } from "../Context";
|
||||||
import { Observer } from "../core/observer";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl Store
|
* Owl Store
|
||||||
@@ -31,27 +30,15 @@ interface StoreConfig {
|
|||||||
getters?: { [name: string]: Getter };
|
getters?: { [name: string]: Getter };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StoreOption {
|
export class Store extends Context {
|
||||||
debug?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Store extends EventBus {
|
|
||||||
state: any;
|
|
||||||
actions: any;
|
actions: any;
|
||||||
mutations: any;
|
|
||||||
debug: boolean;
|
|
||||||
env: any;
|
env: any;
|
||||||
observer: Observer;
|
|
||||||
getters: { [name: string]: (payload?) => any };
|
getters: { [name: string]: (payload?) => any };
|
||||||
|
|
||||||
constructor(config: StoreConfig, options: StoreOption = {}) {
|
constructor(config: StoreConfig) {
|
||||||
super();
|
super(config.state);
|
||||||
this.debug = options.debug || false;
|
|
||||||
this.actions = config.actions;
|
this.actions = config.actions;
|
||||||
this.env = config.env;
|
this.env = config.env;
|
||||||
this.observer = new Observer();
|
|
||||||
this.observer.notifyCB = this.__notifyComponents.bind(this);
|
|
||||||
this.state = this.observer.observe(config.state || {});
|
|
||||||
this.getters = {};
|
this.getters = {};
|
||||||
if (config.getters) {
|
if (config.getters) {
|
||||||
const firstArg = {
|
const firstArg = {
|
||||||
@@ -79,29 +66,4 @@ export class Store extends EventBus {
|
|||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Instead of using trigger to emit an update event, we actually implement
|
|
||||||
* our own function to do that. The reason is that we need to be smarter than
|
|
||||||
* a simple trigger function: we need to wait for parent components to be
|
|
||||||
* done before doing children components. The reason is that if an update
|
|
||||||
* as an effect of destroying a children, we do not want to call the
|
|
||||||
* mapStoreToProps function of the child, nor rendering it.
|
|
||||||
*
|
|
||||||
* This method is not optimal if we have a bunch of asynchronous components:
|
|
||||||
* we wait sequentially for each component to be completed before updating the
|
|
||||||
* next. However, the only things that matters is that children are updated
|
|
||||||
* after their parents. So, this could be optimized by being smarter, and
|
|
||||||
* updating all widgets concurrently, except for parents/children.
|
|
||||||
*/
|
|
||||||
async __notifyComponents() {
|
|
||||||
const subs = this.subscriptions.update || [];
|
|
||||||
for (let i = 0, iLen = subs.length; i < iLen; i++) {
|
|
||||||
const sub = subs[i];
|
|
||||||
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
|
|
||||||
if (shouldCallback) {
|
|
||||||
await sub.callback.call(sub.owner);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
|
||||||
|
import { Component, Env } from "../src/component/component";
|
||||||
|
import { Context, useContext } from "../src/context";
|
||||||
|
import { xml } from "../src/tags";
|
||||||
|
import { useState } from "../src/hooks";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Setup and helpers
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// We create before each test:
|
||||||
|
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||||
|
// manipulations. Note that it is removed after each test.
|
||||||
|
// - env: a WEnv, necessary to create new components
|
||||||
|
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
let env: Env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
env = makeTestEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("Context", () => {
|
||||||
|
test("very simple use, with initial value", async () => {
|
||||||
|
const testContext = new Context({ value: 123 });
|
||||||
|
|
||||||
|
class Test extends Component<any, any> {
|
||||||
|
static template = xml`<div><t t-esc="contextObj.value"/></div>`;
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
}
|
||||||
|
const test = new Test(env);
|
||||||
|
await test.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>123</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("useContext hook is reactive, for one component", async () => {
|
||||||
|
const testContext = new Context({ value: 123 });
|
||||||
|
|
||||||
|
class Test extends Component<any, any> {
|
||||||
|
static template = xml`<div><t t-esc="contextObj.value"/></div>`;
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
}
|
||||||
|
const test = new Test(env);
|
||||||
|
await test.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>123</div>");
|
||||||
|
test.contextObj.value = 321;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div>321</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("two components can subscribe to same context", async () => {
|
||||||
|
const testContext = new Context({ value: 123 });
|
||||||
|
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="contextObj.value"/></span>`;
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child /><Child /></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
const parent = new Parent(env);
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
|
||||||
|
testContext.state.value = 321;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("one components can subscribe twice to same context", async () => {
|
||||||
|
const testContext = new Context({ a: 1, b: 2 });
|
||||||
|
const steps: string[] = [];
|
||||||
|
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="contextObj1.a"/><t t-esc="contextObj2.b"/></span>`;
|
||||||
|
contextObj1 = useContext(testContext);
|
||||||
|
contextObj2 = useContext(testContext);
|
||||||
|
__render(fiber) {
|
||||||
|
steps.push("child");
|
||||||
|
return super.__render(fiber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child /></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
const parent = new Parent(env);
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
|
||||||
|
expect(steps).toEqual(['child']);
|
||||||
|
testContext.state.a = 3;
|
||||||
|
await nextTick();
|
||||||
|
expect(steps).toEqual(['child', 'child']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parent and children subscribed to same context", async () => {
|
||||||
|
const testContext = new Context({ a: 123, b: 321 });
|
||||||
|
const steps: string[] = [];
|
||||||
|
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
__render(fiber) {
|
||||||
|
steps.push("child");
|
||||||
|
return super.__render(fiber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child /><t t-esc="contextObj.b"/></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
__render(fiber) {
|
||||||
|
steps.push("parent");
|
||||||
|
return super.__render(fiber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parent = new Parent(env);
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>123</span>321</div>");
|
||||||
|
expect(steps).toEqual(["parent", "child"]);
|
||||||
|
|
||||||
|
parent.contextObj.a = 124;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>124</span>321</div>");
|
||||||
|
|
||||||
|
// we only want one render from the child here, not two
|
||||||
|
expect(steps).toEqual(["parent", "child", "parent", "child"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("destroyed component is inactive", async () => {
|
||||||
|
const testContext = new Context({ a: 123 });
|
||||||
|
const steps: string[] = [];
|
||||||
|
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
__render(fiber) {
|
||||||
|
steps.push("child");
|
||||||
|
return super.__render(fiber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child t-if="state.flag"/></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
state = useState({ flag: true });
|
||||||
|
__render(fiber) {
|
||||||
|
steps.push("parent");
|
||||||
|
return super.__render(fiber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parent = new Parent(env);
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
|
||||||
|
expect(steps).toEqual(["parent", "child"]);
|
||||||
|
expect(testContext.subscriptions.update.length).toBe(1);
|
||||||
|
|
||||||
|
parent.state.flag = false;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div></div>");
|
||||||
|
expect(steps).toEqual(["parent", "child", "parent"]);
|
||||||
|
// kind of whitebox...
|
||||||
|
// we make sure we do not have any pending subscriptions to the 'update'
|
||||||
|
// event
|
||||||
|
expect(testContext.subscriptions.update.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -318,6 +318,66 @@ const HOOKS_CSS = `button {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
const CONTEXT_JS = `// In this example, we show how components can use the Context and 'useContext'
|
||||||
|
// hook to share information between them.
|
||||||
|
const { Component, Context } = owl;
|
||||||
|
const { useContext } = owl.hooks;
|
||||||
|
|
||||||
|
class ToolbarButton extends Component {
|
||||||
|
theme = useContext(this.env.themeContext);
|
||||||
|
|
||||||
|
get style () {
|
||||||
|
const theme = this.theme;
|
||||||
|
return \`background-color: \${theme.background}; color: \${theme.foreground}\`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Toolbar extends Component {
|
||||||
|
static components = { ToolbarButton };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main root component
|
||||||
|
class App extends Component {
|
||||||
|
static components = { Toolbar };
|
||||||
|
|
||||||
|
toggleTheme() {
|
||||||
|
const { background, foreground } = this.env.themeContext.state;
|
||||||
|
this.env.themeContext.state.background = foreground;
|
||||||
|
this.env.themeContext.state.foreground = background;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application setup
|
||||||
|
const themeContext = new Context({
|
||||||
|
background: '#000',
|
||||||
|
foreground: '#fff',
|
||||||
|
});
|
||||||
|
const env = {
|
||||||
|
qweb: new owl.QWeb(TEMPLATES),
|
||||||
|
themeContext: themeContext,
|
||||||
|
};
|
||||||
|
const app = new App(env);
|
||||||
|
app.mount(document.body);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CONTEXT_XML = `<templates>
|
||||||
|
<button t-name="ToolbarButton" t-att-style="style">
|
||||||
|
<t t-esc="props.name"/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div t-name="Toolbar">
|
||||||
|
<ToolbarButton name="'A'"/>
|
||||||
|
<ToolbarButton name="'B'"/>
|
||||||
|
<ToolbarButton name="'C'"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="App">
|
||||||
|
<button t-on-click="toggleTheme">Toggle Mode</button>
|
||||||
|
<Toolbar/>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`;
|
||||||
|
|
||||||
const TODO_APP_STORE = `// This example is an implementation of the TodoList application, from the
|
const TODO_APP_STORE = `// This example is an implementation of the TodoList application, from the
|
||||||
// www.todomvc.com project. This is a non trivial application with some
|
// www.todomvc.com project. This is a non trivial application with some
|
||||||
// interesting user interactions. It uses the local storage for persistence.
|
// interesting user interactions. It uses the local storage for persistence.
|
||||||
@@ -1655,6 +1715,11 @@ export const SAMPLES = [
|
|||||||
xml: HOOKS_DEMO_XML,
|
xml: HOOKS_DEMO_XML,
|
||||||
css: HOOKS_CSS
|
css: HOOKS_CSS
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Context",
|
||||||
|
code: CONTEXT_JS,
|
||||||
|
xml: CONTEXT_XML,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "Todo List App (with store)",
|
description: "Todo List App (with store)",
|
||||||
code: TODO_APP_STORE,
|
code: TODO_APP_STORE,
|
||||||
|
|||||||
Reference in New Issue
Block a user