diff --git a/doc/component.md b/doc/component.md
index 995198fb..192763c4 100644
--- a/doc/component.md
+++ b/doc/component.md
@@ -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
diff --git a/doc/qweb.md b/doc/qweb.md
index d1a08bae..f046cfdf 100644
--- a/doc/qweb.md
+++ b/doc/qweb.md
@@ -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", "
");
```
+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
diff --git a/src/component.ts b/src/component.ts
index fbf219e8..a525558e 100644
--- a/src/component.ts
+++ b/src/component.ts
@@ -134,6 +134,19 @@ export class Component
{
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 {
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 {
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;
diff --git a/src/qweb_core.ts b/src/qweb_core.ts
index 55ba92bd..5d0b536b 100644
--- a/src/qweb_core.ts
+++ b/src/qweb_core.ts
@@ -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
- // to
- node.setAttribute('t-component', node.tagName);
- node.nodeValue = 't';
+ // this is a component, we modify in place the xml document to change
+ // to
+ node.setAttribute("t-component", node.tagName);
+ node.nodeValue = "t";
}
const attributes = (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;
}
}
diff --git a/tests/component.test.ts b/tests/component.test.ts
index 3cf895eb..590b113d 100644
--- a/tests/component.test.ts
+++ b/tests/component.test.ts
@@ -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",
- ''
- );
+ env.qweb.addTemplate("Parent", '');
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",
- '
'
- );
+ env.qweb.addTemplate("Parent", '
');
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",
- `
`
- );
+ env.qweb.addTemplate("Parent", `
`);
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", `
`);
+ env.qweb.addTemplate(
+ "ParentWidget",
+ `
`
+ );
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", `
`);
+ env.qweb.addTemplate(
+ "ParentWidget",
+ `
`
+ );
env.qweb.addTemplate("AnotherWidgetB", `Belgium`);
class AnotherWidgetB extends Widget {}
class ParentWidget extends Widget {
@@ -897,7 +895,7 @@ describe("composition", () => {
`);
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",
- `
`
- );
+ env.qweb.addTemplate("Parent", `
`);
class Parent extends Widget {
components = { SomeWidget: Widget };
}
@@ -1012,7 +1007,10 @@ describe("composition", () => {
});
test("modifying a sub widget", async () => {
- env.qweb.addTemplate("ParentWidget", `
`);
+ env.qweb.addTemplate(
+ "ParentWidget",
+ `
`
+ );
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", `
`);
+ env.qweb.addTemplate(
+ "ParentWidget",
+ `
`
+ );
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",
- `
`
- );
+ env.qweb.addTemplate("Parent", `
`);
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(`
+
+
+ Red
+ Blue
+
+
+ `);
+ class App extends Widget {}
+
+ const app = new App(env);
+ await app.mount(fixture);
+
+ expect(fixture.innerHTML).toBe("Red
");
+ bus.trigger("some-event");
+ await nextTick();
+ expect(fixture.innerHTML).toBe("Blue
");
+ });
+});
diff --git a/tools/playground/samples.js b/tools/playground/samples.js
index e037e42e..2ff0a373 100644
--- a/tools/playground/samples.js
+++ b/tools/playground/samples.js
@@ -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 = `