mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
+56
-27
@@ -4,12 +4,13 @@
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Root Component And Environment](#root-component-and-environment)
|
||||
- [Reference](#reference)
|
||||
- [Properties](#properties)
|
||||
- [Static Properties](#static-properties)
|
||||
- [Methods](#methods)
|
||||
- [Lifecycle](#lifecycle)
|
||||
- [Root Component](#root-component)
|
||||
- [Environment](#environment)
|
||||
- [Composition](#composition)
|
||||
- [Event Handling](#event-handling)
|
||||
- [Form Input Bindings](#form-input-bindings)
|
||||
@@ -76,30 +77,6 @@ a state object is defined. It is not mandatory to use the state object, but it
|
||||
is certainly encouraged. The state object is [observed](observer.md), and any
|
||||
change to it will cause a rerendering.
|
||||
|
||||
## Root Component And Environment
|
||||
|
||||
Most of the time, an Owl component will be created automatically by a tag (or the `t-component`
|
||||
directive) in a template. There is however an obvious exception: the root component
|
||||
of an Owl application has to be created manually:
|
||||
|
||||
```js
|
||||
class App extends owl.Component { ... }
|
||||
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const env = { qweb: qweb };
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
```
|
||||
|
||||
The root component needs an environment. In Owl, an environment is an object with
|
||||
a `qweb` key, which has to be a [QWeb](qweb.md) instance. This qweb instance will
|
||||
be used to render everything.
|
||||
|
||||
The environment will be given to each child, unchanged, in the `env` property.
|
||||
This can be very useful to share common information/methods. For example, all
|
||||
rpcs can be made through a `rpc` method in the environment. This makes it very
|
||||
easy to test a component.
|
||||
|
||||
## Reference
|
||||
|
||||
An Owl component is a small class which represent a component or some UI element.
|
||||
@@ -335,6 +312,59 @@ the DOM. This is a good place to remove some listeners, for example.
|
||||
|
||||
This is the opposite method of `mounted`.
|
||||
|
||||
### Root Component
|
||||
|
||||
Most of the time, an Owl component will be created automatically by a tag (or the `t-component`
|
||||
directive) in a template. There is however an obvious exception: the root component
|
||||
of an Owl application has to be created manually:
|
||||
|
||||
```js
|
||||
class App extends owl.Component { ... }
|
||||
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const env = { qweb: qweb };
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
```
|
||||
|
||||
The root component needs an environment.
|
||||
|
||||
### Environment
|
||||
|
||||
In Owl, an environment is an object with a `qweb` key, which has to be a
|
||||
[QWeb](qweb.md) instance. This qweb instance will be used to render everything.
|
||||
|
||||
The environment is meant to contain (mostly) static global information and
|
||||
methods for the whole application. For example, settings keys (`mode` to determine
|
||||
if we are in desktop or mobile mode, or `theme`: dark or light), `rpc` methods,
|
||||
session information, ...
|
||||
|
||||
The environment will be given to each child, unchanged, in the `env` property.
|
||||
This can be very useful to share common information/methods. For example, all
|
||||
rpcs can be made through a `rpc` method in the environment. This makes it very
|
||||
easy to test a component.
|
||||
|
||||
Updating the environment is not as simple as changing a component's state: its
|
||||
content is not observed, so updates will not be reflected immediately in the
|
||||
user interface. There is however a mechanism to force root widgets to rerender
|
||||
themselves whenever the environment is modified: one only needs to trigger the
|
||||
`update` event on the QWeb instance. For example, a responsive environment
|
||||
could be programmed like this:
|
||||
|
||||
```js
|
||||
function setupResponsivePlugin(env) {
|
||||
const isMobile = () => window.innerWidth <= 768;
|
||||
env.isMobile = isMobile();
|
||||
const updateEnv = owl.utils.debounce(() => {
|
||||
if (env.isMobile !== isMobile()) {
|
||||
env.isMobile = !env.isMobile;
|
||||
env.qweb.trigger('update');
|
||||
}
|
||||
}, 15);
|
||||
window.addEventListener("resize", updateEnv);
|
||||
}
|
||||
```
|
||||
|
||||
### Composition
|
||||
|
||||
The example above shows a QWeb template with a sub component. In a template,
|
||||
@@ -359,12 +389,11 @@ In this example, the `ParentComponent`'s template creates a component `MyCompone
|
||||
after the span. The `info` key will be added to the subcomponent's `props`. Each
|
||||
`props` is a string which represents a javascript (QWeb) expression, so it is
|
||||
dynamic. If it is necessary to give a string, this can be done by quoting it:
|
||||
`someString="'somevalue'"`.
|
||||
`someString="'somevalue'"`.
|
||||
|
||||
Note that the rendering context for the template is the component itself. This means
|
||||
that the template can access `state`, `props`, `env`, or any methods defined in the component.
|
||||
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
<ChildComponent count="state.val" />
|
||||
|
||||
+14
-9
@@ -65,16 +65,16 @@ We present here a list of all standard QWeb directives:
|
||||
The component system in Owl requires additional directives, to express various
|
||||
needs. Here is a list of all Owl specific directives:
|
||||
|
||||
| Name | Description |
|
||||
| -------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| Name | Description |
|
||||
| ------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `t-component`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
|
||||
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
||||
| `t-on-*` | [Event handling](component.md#event-handling) |
|
||||
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
||||
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
|
||||
| `t-slot` | [Rendering a slot](component.md#slots) |
|
||||
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
||||
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
||||
| `t-on-*` | [Event handling](component.md#event-handling) |
|
||||
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
||||
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
|
||||
| `t-slot` | [Rendering a slot](component.md#slots) |
|
||||
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
||||
|
||||
## QWeb Engine
|
||||
|
||||
@@ -135,6 +135,11 @@ It's API is quite simple:
|
||||
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
|
||||
```
|
||||
|
||||
In some way, a `QWeb` instance is the core of an Owl application. It is the only
|
||||
mandatory element of an [environment](component.md#environment). As such, it
|
||||
has an extra responsability: it can act as an event bus for internal communication
|
||||
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
|
||||
|
||||
## Reference
|
||||
|
||||
We define in this section the specification of how `QWeb` templates should be
|
||||
|
||||
+19
-2
@@ -134,6 +134,19 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
parent.__owl__.children[id] = this;
|
||||
} else {
|
||||
this.env = parent;
|
||||
this.env.qweb.on("update", this, () => {
|
||||
if (this.__owl__.isMounted) {
|
||||
this.render(true);
|
||||
}
|
||||
if (this.__owl__.isDestroyed) {
|
||||
// this is unlikely to happen, but if a root widget is destroyed,
|
||||
// we want to remove our subscription. The usual way to do that
|
||||
// would be to perform some check in the destroy method, but since
|
||||
// it is very performance sensitive, and since this is a rare event,
|
||||
// we simply do it lazily
|
||||
this.env.qweb.off("update", this);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.__owl__ = {
|
||||
id: id,
|
||||
@@ -593,7 +606,9 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
for (let i = 0, l = propsDef.length; i < l; i++) {
|
||||
if (!(propsDef[i] in props)) {
|
||||
throw new Error(
|
||||
`Missing props '${propsDef[i]}' (component '${this.constructor.name}')`
|
||||
`Missing props '${propsDef[i]}' (component '${
|
||||
this.constructor.name
|
||||
}')`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -603,7 +618,9 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
if (!(propName in props)) {
|
||||
if (propsDef[propName] && !propsDef[propName].optional) {
|
||||
throw new Error(
|
||||
`Missing props '${propName}' (component '${this.constructor.name}')`
|
||||
`Missing props '${propName}' (component '${
|
||||
this.constructor.name
|
||||
}')`
|
||||
);
|
||||
} else {
|
||||
break;
|
||||
|
||||
+8
-6
@@ -1,5 +1,6 @@
|
||||
import { VNode, h } from "./vdom";
|
||||
import { QWebVar, compileExpr } from "./qweb_expressions";
|
||||
import { EventBus } from "./event_bus";
|
||||
|
||||
/**
|
||||
* Owl QWeb Engine
|
||||
@@ -127,7 +128,7 @@ let nextID = 1;
|
||||
//------------------------------------------------------------------------------
|
||||
// QWeb rendering engine
|
||||
//------------------------------------------------------------------------------
|
||||
export class QWeb {
|
||||
export class QWeb extends EventBus {
|
||||
templates: { [name: string]: Template } = {};
|
||||
utils = UTILS;
|
||||
static components = Object.create(null);
|
||||
@@ -146,6 +147,7 @@ export class QWeb {
|
||||
nextSlotId = 1;
|
||||
|
||||
constructor(data?: string) {
|
||||
super();
|
||||
if (data) {
|
||||
this.addTemplates(data);
|
||||
}
|
||||
@@ -348,10 +350,10 @@ export class QWeb {
|
||||
|
||||
const firstLetter = node.tagName[0];
|
||||
if (firstLetter === firstLetter.toUpperCase()) {
|
||||
// this is a component, we modify in place the xml document to change
|
||||
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
|
||||
node.setAttribute('t-component', node.tagName);
|
||||
node.nodeValue = 't';
|
||||
// this is a component, we modify in place the xml document to change
|
||||
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
|
||||
node.setAttribute("t-component", node.tagName);
|
||||
node.nodeValue = "t";
|
||||
}
|
||||
const attributes = (<Element>node).attributes;
|
||||
|
||||
@@ -391,7 +393,7 @@ export class QWeb {
|
||||
fullName = name;
|
||||
value = attributes[j].textContent;
|
||||
validDirectives.push({ directive, value, fullName });
|
||||
if (directive.name === "on" || directive.name === 'model') {
|
||||
if (directive.name === "on" || directive.name === "model") {
|
||||
withHandlers = true;
|
||||
}
|
||||
}
|
||||
|
||||
+58
-25
@@ -1,5 +1,6 @@
|
||||
import { Component, Env } from "../src/component";
|
||||
import { QWeb } from "../src/qweb_core";
|
||||
import { EventBus } from "../src/event_bus";
|
||||
import {
|
||||
makeDeferred,
|
||||
makeTestFixture,
|
||||
@@ -547,10 +548,7 @@ describe("lifecycle hooks", () => {
|
||||
|
||||
test("willUpdateProps hook is called", async () => {
|
||||
let def = makeDeferred();
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
'<span><Child n="state.n"/></span>'
|
||||
);
|
||||
env.qweb.addTemplate("Parent", '<span><Child n="state.n"/></span>');
|
||||
class Parent extends Widget {
|
||||
state = { n: 1 };
|
||||
components = { Child: HookWidget };
|
||||
@@ -599,10 +597,7 @@ describe("lifecycle hooks", () => {
|
||||
test("patched hook is called after updateProps", async () => {
|
||||
let n = 0;
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
'<div><Child a="state.a"/></div>'
|
||||
);
|
||||
env.qweb.addTemplate("Parent", '<div><Child a="state.a"/></div>');
|
||||
class Parent extends Widget {
|
||||
state = { a: 1 };
|
||||
components = { Child: TestWidget };
|
||||
@@ -642,10 +637,7 @@ describe("lifecycle hooks", () => {
|
||||
|
||||
test("shouldUpdate hook prevent rerendering", async () => {
|
||||
let shouldUpdate = false;
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><Child val="state.val"/></div>`
|
||||
);
|
||||
env.qweb.addTemplate("Parent", `<div><Child val="state.val"/></div>`);
|
||||
class Parent extends Widget {
|
||||
state = { val: 42 };
|
||||
components = { Child: TestWidget };
|
||||
@@ -867,7 +859,10 @@ describe("composition", () => {
|
||||
|
||||
test("can use components from the global registry", async () => {
|
||||
QWeb.register("WidgetB", WidgetB);
|
||||
env.qweb.addTemplate("ParentWidget", `<div><t t-component="WidgetB"/></div>`);
|
||||
env.qweb.addTemplate(
|
||||
"ParentWidget",
|
||||
`<div><t t-component="WidgetB"/></div>`
|
||||
);
|
||||
class ParentWidget extends Widget {}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
@@ -877,7 +872,10 @@ describe("composition", () => {
|
||||
|
||||
test("don't fallback to global registry if widget defined locally", async () => {
|
||||
QWeb.register("WidgetB", WidgetB); // should not use this widget
|
||||
env.qweb.addTemplate("ParentWidget", `<div><t t-component="WidgetB"/></div>`);
|
||||
env.qweb.addTemplate(
|
||||
"ParentWidget",
|
||||
`<div><t t-component="WidgetB"/></div>`
|
||||
);
|
||||
env.qweb.addTemplate("AnotherWidgetB", `<span>Belgium</span>`);
|
||||
class AnotherWidgetB extends Widget {}
|
||||
class ParentWidget extends Widget {
|
||||
@@ -897,7 +895,7 @@ describe("composition", () => {
|
||||
</templates>`);
|
||||
class C extends Widget {}
|
||||
class P extends Widget {
|
||||
components = {C};
|
||||
components = { C };
|
||||
}
|
||||
const parent = new P(env);
|
||||
await parent.mount(fixture);
|
||||
@@ -906,10 +904,7 @@ describe("composition", () => {
|
||||
|
||||
test("throw a nice error if it cannot find component", async () => {
|
||||
expect.assertions(1);
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><SomeMispelledWidget /></div>`
|
||||
);
|
||||
env.qweb.addTemplate("Parent", `<div><SomeMispelledWidget /></div>`);
|
||||
class Parent extends Widget {
|
||||
components = { SomeWidget: Widget };
|
||||
}
|
||||
@@ -1012,7 +1007,10 @@ describe("composition", () => {
|
||||
});
|
||||
|
||||
test("modifying a sub widget", async () => {
|
||||
env.qweb.addTemplate("ParentWidget", `<div><t t-component="Counter"/></div>`);
|
||||
env.qweb.addTemplate(
|
||||
"ParentWidget",
|
||||
`<div><t t-component="Counter"/></div>`
|
||||
);
|
||||
class ParentWidget extends Widget {
|
||||
components = { Counter };
|
||||
}
|
||||
@@ -1068,7 +1066,10 @@ describe("composition", () => {
|
||||
});
|
||||
|
||||
test("rerendering a widget with a sub widget", async () => {
|
||||
env.qweb.addTemplate("ParentWidget", `<div><t t-component="Counter"/></div>`);
|
||||
env.qweb.addTemplate(
|
||||
"ParentWidget",
|
||||
`<div><t t-component="Counter"/></div>`
|
||||
);
|
||||
class ParentWidget extends Widget {
|
||||
components = { Counter };
|
||||
}
|
||||
@@ -2721,10 +2722,7 @@ describe("widget and observable state", () => {
|
||||
|
||||
test("subcomponents cannot change observable state received from parent", async () => {
|
||||
expect.assertions(1);
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><Child obj="state.obj"/></div>`
|
||||
);
|
||||
env.qweb.addTemplate("Parent", `<div><Child obj="state.obj"/></div>`);
|
||||
class Parent extends Widget {
|
||||
state = { obj: { coffee: 1 } };
|
||||
components = { Child };
|
||||
@@ -3215,3 +3213,38 @@ describe("t-model directive", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("environment and plugins", () => {
|
||||
// some source of external events
|
||||
let bus = new EventBus();
|
||||
|
||||
// definition of a plugin
|
||||
const somePlugin = env => {
|
||||
env.someFlag = true;
|
||||
bus.on("some-event", null, () => {
|
||||
env.someFlag = !env.someFlag;
|
||||
env.qweb.trigger("update");
|
||||
});
|
||||
};
|
||||
|
||||
test("plugin works as expected", async () => {
|
||||
somePlugin(env);
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="App">
|
||||
<t t-if="env.someFlag">Red</t>
|
||||
<t t-else="1">Blue</t>
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
class App extends Widget {}
|
||||
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div>Red</div>");
|
||||
bus.trigger("some-event");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>Blue</div>");
|
||||
});
|
||||
});
|
||||
|
||||
+15
-16
@@ -942,31 +942,30 @@ class App extends owl.Component {
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application Startup
|
||||
// Responsive plugin
|
||||
//------------------------------------------------------------------------------
|
||||
function isMobile() {
|
||||
return window.innerWidth <= 768;
|
||||
function setupResponsivePlugin(env) {
|
||||
const isMobile = () => window.innerWidth <= 768;
|
||||
env.isMobile = isMobile();
|
||||
const updateEnv = owl.utils.debounce(() => {
|
||||
if (env.isMobile !== isMobile()) {
|
||||
env.isMobile = !env.isMobile;
|
||||
env.qweb.trigger('update');
|
||||
}
|
||||
}, 15);
|
||||
window.addEventListener("resize", updateEnv);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application Startup
|
||||
//------------------------------------------------------------------------------
|
||||
const env = {
|
||||
qweb: new owl.QWeb(TEMPLATES),
|
||||
isMobile: isMobile()
|
||||
};
|
||||
|
||||
setupResponsivePlugin(env);
|
||||
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
|
||||
function updateEnv() {
|
||||
const _isMobile = isMobile();
|
||||
if (_isMobile !== env.isMobile) {
|
||||
app.updateEnv({
|
||||
isMobile: _isMobile
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", owl.utils.debounce(updateEnv, 20));
|
||||
`;
|
||||
|
||||
const RESPONSIVE_XML = `<templates>
|
||||
|
||||
Reference in New Issue
Block a user