diff --git a/doc/context.md b/doc/context.md
new file mode 100644
index 00000000..4e5e70fc
--- /dev/null
+++ b/doc/context.md
@@ -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`
+
+
+ some simplified user interface
+
+
+ some more sopthisticated user interface
+
+ `;
+ 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.
diff --git a/doc/hooks.md b/doc/hooks.md
index f9e6c164..ff31b0c7 100644
--- a/doc/hooks.md
+++ b/doc/hooks.md
@@ -12,6 +12,7 @@
- [`onWillUnmount`](#onwillunmount)
- [`onWillPatch`](#onwillpatch)
- [`onPatched`](#onpatched)
+ - [`useContext`](#usecontext)
- [`useRef`](#useref)
- [`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
is unmounted (see example on top of this page).
-
### `onWillPatch`
`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
after the component patched.
+### `useContext`
+
+See [`useContext`](context.md#usecontext) for reference documentation.
+
### `useRef`
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`
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
`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
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
-affected.
\ No newline at end of file
+the parent environment. And of course, the parent environment will not be
+affected.
diff --git a/doc/readme.md b/doc/readme.md
index 1434a24c..3533e9a9 100644
--- a/doc/readme.md
+++ b/doc/readme.md
@@ -8,6 +8,7 @@ build applications. Here is a complete representation of its content:
```
owl
Component
+ Context
QWeb
useState
core
@@ -18,6 +19,7 @@ owl
onWillUnmount
onWillPatch
onPatched
+ useContext
useState
useRef
useSubEnv
@@ -44,6 +46,7 @@ Note that for convenience, the `useState` hook is also exported at the root of t
- [Animations](animations.md)
- [Component](component.md)
+- [Context](context.md)
- [Event Bus](event_bus.md)
- [Hooks](hooks.md)
- [Observer](observer.md)
diff --git a/src/Context.ts b/src/Context.ts
new file mode 100644
index 00000000..7bfe373d
--- /dev/null
+++ b/src/Context.ts
@@ -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 = 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;
+}
diff --git a/src/index.ts b/src/index.ts
index cc593ce9..90cdfb49 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,6 +12,7 @@ import { Store } from "./store/store";
import * as _utils from "./utils";
import * as _tags from "./tags";
import * as _hooks from "./hooks";
+import * as _context from "./Context";
import { Link } from "./router/Link";
import { RouteComponent } from "./router/RouteComponent";
import { Router } from "./router/Router";
@@ -19,13 +20,14 @@ import { Router } from "./router/Router";
export { Component } from "./component/component";
export { QWeb };
+export const Context = _context.Context;
export const useState = _hooks.useState;
export const core = { EventBus, Observer };
export const router = { Router, RouteComponent, Link };
export const store = { Store, ConnectedComponent };
export const utils = _utils;
export const tags = _tags;
-export const hooks = _hooks;
+export const hooks = Object.assign({}, _hooks, { useContext: _context.useContext });
export const __info__ = {};
Object.defineProperty(__info__, "mode", {
diff --git a/src/store/store.ts b/src/store/store.ts
index aee91784..1ac3c06e 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -1,6 +1,5 @@
import { Env } from "../component/component";
-import { EventBus } from "../core/event_bus";
-import { Observer } from "../core/observer";
+import { Context } from "../Context";
/**
* Owl Store
@@ -31,27 +30,15 @@ interface StoreConfig {
getters?: { [name: string]: Getter };
}
-interface StoreOption {
- debug?: boolean;
-}
-
-export class Store extends EventBus {
- state: any;
+export class Store extends Context {
actions: any;
- mutations: any;
- debug: boolean;
env: any;
- observer: Observer;
getters: { [name: string]: (payload?) => any };
- constructor(config: StoreConfig, options: StoreOption = {}) {
- super();
- this.debug = options.debug || false;
+ constructor(config: StoreConfig) {
+ super(config.state);
this.actions = config.actions;
this.env = config.env;
- this.observer = new Observer();
- this.observer.notifyCB = this.__notifyComponents.bind(this);
- this.state = this.observer.observe(config.state || {});
this.getters = {};
if (config.getters) {
const firstArg = {
@@ -79,29 +66,4 @@ export class Store extends EventBus {
);
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);
- }
- }
- }
}
diff --git a/tests/Context.test.ts b/tests/Context.test.ts
new file mode 100644
index 00000000..b3536da7
--- /dev/null
+++ b/tests/Context.test.ts
@@ -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 {
+ static template = xml`
`;
+ contextObj = useContext(testContext);
+ }
+ const test = new Test(env);
+ await test.mount(fixture);
+ expect(fixture.innerHTML).toBe("
123
");
+ });
+
+ test("useContext hook is reactive, for one component", async () => {
+ const testContext = new Context({ value: 123 });
+
+ class Test extends Component {
+ static template = xml`
`;
+ contextObj = useContext(testContext);
+ }
+ const test = new Test(env);
+ await test.mount(fixture);
+ expect(fixture.innerHTML).toBe("
");
+ expect(steps).toEqual(["parent", "child"]);
+ expect(testContext.subscriptions.update.length).toBe(1);
+
+ parent.state.flag = false;
+ await nextTick();
+ expect(fixture.innerHTML).toBe("");
+ 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);
+ });
+});
diff --git a/tools/playground/samples.js b/tools/playground/samples.js
index cb041cdb..d777ecb8 100644
--- a/tools/playground/samples.js
+++ b/tools/playground/samples.js
@@ -318,6 +318,66 @@ const HOOKS_CSS = `button {
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 = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
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
// interesting user interactions. It uses the local storage for persistence.
@@ -1655,6 +1715,11 @@ export const SAMPLES = [
xml: HOOKS_DEMO_XML,
css: HOOKS_CSS
},
+ {
+ description: "Context",
+ code: CONTEXT_JS,
+ xml: CONTEXT_XML,
+ },
{
description: "Todo List App (with store)",
code: TODO_APP_STORE,