[IMP] qweb,component: make QWeb an event bus

closes #197
This commit is contained in:
Géry Debongnie
2019-06-22 14:44:09 +02:00
parent 63a8fcd7e2
commit 8c8ffb6a6b
6 changed files with 170 additions and 85 deletions
+56 -27
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>