Compare commits

...

13 Commits

Author SHA1 Message Date
Géry Debongnie 144b323d2b [REL] v1.2.0
# Owl v1.2.0

## Changes

- translation fix for terms surrounded by spaces
- fix: properly remount components with shouldUpdate=false
- add: add two new generic hooks:  and
- fix: do not skip rendering in components using store and local state (in some cases)

Note that the last change is a pretty significant change: component connected to
a store should be very careful if the data that they represent is deleted, because
they will always be rendered with the current state of the store.
2020-12-14 13:23:46 +01:00
Géry Debongnie bb64e87634 [FIX] store: properly render, even if shouldupdate is implemented
Before this commit, an unwanted behaviour happened when using components
with shouldUpdate implemented, and store/state.

The actual problem is the following: the sub component is using a store,
and shouldupdate.  Whenever the component is rendered, it checks if there is an incoming
rendering from the context.  If that is the case, it skips the
rendering, because we actually only want to be rendered by the store
rendering (otherwise, we may run into issue with inconsistent data (more
recent data from the context, older data in the component).

However, if shouldUpdate is implemented, then the rendering coming from
the context simply does not arrive.

To fix this, we tried to just force these renderings to go through all children,
regardless of their shouldUpdate status. This actually works, but then
shouldUpdate is ignored, which is an issue in Odoo discuss.

Then, after discussing this situation, we noticed that the context/store
system is actually unsafe: the protection given by the check mentioned
above is in fact fundamentally insufficient: there are other perfectly
valid situations where a rendering can be triggered on the component,
which will bypass the check (for example, an explicit call to
this.render() or a rendering initiated by some parent component) and
cause a crash if the component is not properly defensively written.

Therefore, it is currently mandatory for all components using
context/store to be aware of that, and to protect themselves against
such situations. So, in that regard, the check is not really a
protection, it just helps hiding an unsafe situation anyway and we
decided to remove it.

Note that this is a potentially breaking change: components using a
store and some local state will now be rendered twice in some cases...

closes #799
2020-12-14 13:19:36 +01:00
Géry Debongnie 9a87b9a4a0 [IMP] hooks: add some building blocks for hooks
This commit introduces two new hooks: useComponent, and
useEnv.
2020-12-14 11:53:01 +01:00
Géry Debongnie 2a53a9592e [FIX] component: force mounting subcomponents with shouldUpdate
Components can implement shouldUpdate to return false.  In that case,
renderings coming from above should be ignored.

However, if the component was unmounted and is remounted, we actually
need to force a rerendering in that case, so it is mounted, otherwise
the subcomponent is left in unmounted state, which means that rendering
are ignored.

closes #800
2020-12-11 15:19:37 +01:00
Ivan Yelizariev f54b9a4a0c [FIX] properly translate terms surrounded by spaces
This reimplements logic from Odoo v13 [1], i.e. if we have a text that follows
by space e.g. "This database will expire in " [2], then we want get translation
for the term without that space and add the space manually. This way we can
export trimmed terms for translations and don't miss the space, when atranslator
forget to add it at the end of translation

[1] https://github.com/odoo/odoo/blob/26927417e2957acba6fb79446c2520107daa7eea/addons/web/static/src/js/core/qweb.js#L46
[2] https://github.com/odoo/enterprise/blob/ab0e893493f909ec3033e8a1cf6c141ea9375587/web_enterprise/static/src/xml/base.xml#L21

---

opw-2410708
2020-12-03 13:36:17 +01:00
Géry Debongnie abb9d0b364 [REL] v1.1.1
# Owl v1.1.1

## Changes

- export  object to make utility functions testable
- minor doc improvements and corrections
- fix: make sure owl does not crash in iframe in private mode
2020-11-20 11:02:19 +01:00
Géry Debongnie 8483cc805e [FIX/IMP] export browser object, prevent crash in private mode
This commit performs two tasks:

1. it exports the internal browser object (in owl.browser), for ease of
testing
2. it makes sure that Owl does not crash inside iframes in incognito
mode, because window.localStorage raises an exception in that case

closes #791
2020-11-20 10:55:08 +01:00
Simon Genin 142f47ac82 [DOC] Fix animation documentation (#783)
The documentation was a bit missleading around which and when css
classes were added to the DOM.
Also added a scss mixin usage exemple.

Co-authored-by: Simon Genin (ges) <ges@odoo.com>
2020-11-18 09:41:50 +01:00
Stephen Mugisha c8a27aa1a3 Fix minor grammatical errors
Correct minor grammatical mistakes in the `why_owl.md` file
2020-11-02 16:36:16 +01:00
Simon Genin 323cb61d2c [DOC] minor fix - missing this.
Proposed by @obayit with https://github.com/odoo/owl/pull/689
2020-11-02 16:33:02 +01:00
glovebx ac9cc91701 typo correct 2020-11-02 16:03:10 +01:00
Géry Debongnie d83cfc12ee [IMP] qweb: the t-slot directive is now dynamic
It is useful for some kind of components to be able to use a completely
dynamic slot expression.
2020-10-30 15:31:03 +01:00
Géry Debongnie cb07c99d40 [IMP] owl: add a new mount method 2020-10-30 15:31:03 +01:00
39 changed files with 633 additions and 192 deletions
+3 -4
View File
@@ -41,7 +41,7 @@ find some more additional information [here](doc/miscellaneous/comparison.md).
Here is a short example to illustrate interactive components: Here is a short example to illustrate interactive components:
```javascript ```javascript
const { Component, useState } = owl; const { Component, useState, mount } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
class Counter extends Component { class Counter extends Component {
@@ -63,8 +63,7 @@ class App extends Component {
static components = { Counter }; static components = { Counter };
} }
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
``` ```
Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate). Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
@@ -121,7 +120,7 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here: If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.0.13](https://github.com/odoo/owl/releases/tag/v1.0.13) - [owl-1.2.0](https://github.com/odoo/owl/releases/tag/v1.2.0)
## License ## License
+1 -2
View File
@@ -85,8 +85,7 @@ afterEach(() => {
describe("SomeComponent", () => { describe("SomeComponent", () => {
test("component behaves as expected", async () => { test("component behaves as expected", async () => {
const props = {...}; // depends on the component const props = {...}; // depends on the component
const comp = new SomeComponent(null, props); const comp = await mount(SomeComponent, { target: fixture, props });
await comp.mount(fixture);
// do some assertions // do some assertions
expect(...).toBe(...); expect(...).toBe(...);
+1 -1
View File
@@ -96,7 +96,7 @@ class OrderLine extends Component {
</div>`; </div>`;
add() { add() {
this.trigger("add-to-order", { line: props.line }); this.trigger("add-to-order", { line: this.props.line });
} }
} }
+8 -11
View File
@@ -54,7 +54,7 @@ Now, `index.html` should contain the following:
And `app.js` should look like this: And `app.js` should look like this:
```js ```js
const { Component } = owl; const { Component, mount } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
const { whenReady } = owl.utils; const { whenReady } = owl.utils;
@@ -65,8 +65,7 @@ class App extends Component {
// Setup code // Setup code
function setup() { function setup() {
const app = new App(); mount(App, target: { document.body })
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
@@ -124,7 +123,7 @@ Here is the content of `app.js` and `main.js`:
```js ```js
// app.js ---------------------------------------------------------------------- // app.js ----------------------------------------------------------------------
const { Component } = owl; const { Component, mount } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
export class App extends Component { export class App extends Component {
@@ -135,8 +134,7 @@ export class App extends Component {
import { App } from "./app.js"; import { App } from "./app.js";
function setup() { function setup() {
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
} }
owl.utils.whenReady(setup); owl.utils.whenReady(setup);
@@ -240,12 +238,11 @@ export class App extends Component {
} }
// src/main.js ----------------------------------------------------------------- // src/main.js -----------------------------------------------------------------
import { utils } from "@odoo/owl"; import { utils, mount } from "@odoo/owl";
import { App } from "./components/App"; import { App } from "./components/App";
function setup() { function setup() {
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
} }
utils.whenReady(setup); utils.whenReady(setup);
@@ -253,6 +250,7 @@ utils.whenReady(setup);
// tests/components/App.test.js ------------------------------------------------ // tests/components/App.test.js ------------------------------------------------
import { App } from "../../src/components/App"; import { App } from "../../src/components/App";
import { makeTestFixture, nextTick, click } from "../helpers"; import { makeTestFixture, nextTick, click } from "../helpers";
import { mount } from "@odoo/owl";
let fixture; let fixture;
@@ -266,8 +264,7 @@ afterEach(() => {
describe("App", () => { describe("App", () => {
test("Works as expected...", async () => { test("Works as expected...", async () => {
const app = new App(); await mount(App, { target: fixture });
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>"); expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
click(fixture, "div"); click(fixture, "div");
+8 -13
View File
@@ -83,7 +83,7 @@ a single root component. Let us start by defining an `App` component. Replace th
content of the function in `app.js` by the following code: content of the function in `app.js` by the following code:
```js ```js
const { Component } = owl; const { Component, mount } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
const { whenReady } = owl.utils; const { whenReady } = owl.utils;
@@ -94,8 +94,7 @@ class App extends Component {
// Setup code // Setup code
function setup() { function setup() {
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
@@ -279,8 +278,7 @@ class App extends Component {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
function setup() { function setup() {
owl.config.mode = "dev"; owl.config.mode = "dev";
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
@@ -639,8 +637,7 @@ function setup() {
owl.config.mode = "dev"; owl.config.mode = "dev";
const store = new Store({ actions, state: initialState }); const store = new Store({ actions, state: initialState });
App.env.store = store; App.env.store = store;
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
@@ -666,9 +663,8 @@ function makeStore() {
function setup() { function setup() {
owl.config.mode = "dev"; owl.config.mode = "dev";
App.env.store = makeStore(); const env = {store = makeStore()};
const app = new App(); mount(App, { target: document.body, env });
app.mount(document.body);
} }
``` ```
@@ -943,9 +939,8 @@ For reference, here is the final code:
function setup() { function setup() {
owl.config.mode = "dev"; owl.config.mode = "dev";
App.env.store = makeStore(); const env = {store = makeStore()};
const app = new App(); mount(App, { target: document.body, env });
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
+12 -12
View File
@@ -61,14 +61,14 @@ because a lot of the state is hidden in their internals.
React or Vue have a huge community, and a lot of effort have been made into their React or Vue have a huge community, and a lot of effort have been made into their
tooling. This is wonderful, but at the same time, a pretty big issue for Odoo: tooling. This is wonderful, but at the same time, a pretty big issue for Odoo:
since the assets are totally dynamic (and could change whenever the user install since the assets are totally dynamic (and could change whenever the user installs
or remove an addon), we need to have all that kind of tooling on the production or removes an addon), we need to have all that kind of tooling on the production
servers. This is certainly not ideal. servers. This is certainly not ideal.
Also, this makes it very complicated to setup Vue or React tools: Odoo code is Also, this makes it very complicated to setup Vue or React tools: Odoo code is
not a simple file that import other files. It changes all the time, assets not a simple file that import other files. It changes all the time, assets
are bundled differently in different contexts. This is the reason why Odoo has are bundled differently in different contexts. This is the reason why Odoo has
its own module system, which are resolve at runtime, by the browser. The its own module system, which are resolved at runtime, by the browser. The
dynamic nature of Odoo means that we often need to delay work as late as possible dynamic nature of Odoo means that we often need to delay work as late as possible
(in other word, we want a JIT user interface!) (in other word, we want a JIT user interface!)
@@ -78,12 +78,12 @@ deploy. Using React without JSX, or Vue without vue file is not very appealing.
At the same time, Owl is designed to solve this issue: it compiles templates At the same time, Owl is designed to solve this issue: it compiles templates
by the browser, it doesn't need much code for that, since we use the XML parser by the browser, it doesn't need much code for that, since we use the XML parser
built into each browser. Owl works with or without any additional tooling. It built into each browser. Owl works with or without any additional tooling. It
can use template strings to write single file component, and is easy to integrate can use template strings to write single file components, and is easy to integrate
in any html page, with a simple `<script>` tag. in any html page, with a simple `<script>` tag.
## Template based ## Template based
Odoo stores template as XML document in a database. This is very powerful, since Odoo stores templates as XML documents in a database. This is very powerful, since
this allow the use of xpaths to customize other templates. This is a very this allow the use of xpaths to customize other templates. This is a very
important feature of odoo, and one of the key to Odoo modularity. important feature of odoo, and one of the key to Odoo modularity.
@@ -104,12 +104,12 @@ awkward, and very confusing.
## Developer Experience ## Developer Experience
This brings us to the following point: developer experience. We see this choice This brings us to the following point: developer experience. We see this choice
as an investment for the future, and we want to make onboarding developer as as an investment for the future, and we want to make onboarding developers as
easy as possible. easy as possible.
While many javascript professionals clearly think that react/vue is not difficult While many javascript professionals clearly think that react/vue is not difficult
(which is true to some extent), it is alsy true that many non js specialists are (which is true to some extent), it is alsy true that many non js specialists are
overwhelmed with the frontend world: functional component, hooks, and many other overwhelmed with the frontend world: functional components, hooks, and many other
fancy words. Also, what is available in the compilation context may be difficult, fancy words. Also, what is available in the compilation context may be difficult,
there is a lot of black magic going on in pretty much every framework. Vue there is a lot of black magic going on in pretty much every framework. Vue
somehow join various namespaces into one, under the hood, and add various internal somehow join various namespaces into one, under the hood, and add various internal
@@ -135,7 +135,7 @@ needs: Odoo will fetch templates from the database and need to compile them only
at the last possible moment, so we can apply all necessary xpaths. at the last possible moment, so we can apply all necessary xpaths.
Even more: Odoo needs to be able to generate (and compile) templates at runtime. Even more: Odoo needs to be able to generate (and compile) templates at runtime.
Currently, Odoo form views interpret a xml description. But the form view code Currently, Odoo form views interpret an xml description. But the form view code
then needs to do a lot of complicated operations. With Owl, we will be able to then needs to do a lot of complicated operations. With Owl, we will be able to
transform a view description into a QWeb template, then compile that and use it transform a view description into a QWeb template, then compile that and use it
immediately. immediately.
@@ -147,16 +147,16 @@ For example, the reactivity system. We like the way Vue did it, but it has a
flaw: it is not really optional. There is actually a way to opt out of the reactivity flaw: it is not really optional. There is actually a way to opt out of the reactivity
system by freezing the state, but then, it is freezed. system by freezing the state, but then, it is freezed.
And there certainly are situations where we need a state, which is not readonly, And there certainly are situations where we need a state, which is not read-only,
and not observed. For example, imagine a spreadsheet component. It may have a and not observed. For example, imagine a spreadsheet component. It may have a
very large internal state, and it knows exactly when it needs to be rendered very large internal state, and it knows exactly when it needs to be rendered
(basically, whenever the user perform some action). Then, observing its state (basically, whenever the user performs some action). Then, observing its state
is a net performance loss, both for the CPU and the memory. is a net performance loss, both for the CPU and the memory.
## Concurrency ## Concurrency
Many applications are happy to simply display a spinner whenever a new asynchronous Many applications are happy to simply display a spinner whenever a new asynchronous
action is performed, but Odoo want a different user experience: most asynchronous action is performed, but Odoo wants a different user experience: most asynchronous
state changes are not displayed until ready. This is sometimes called a concurrent state changes are not displayed until ready. This is sometimes called a concurrent
mode: the UI is rendered in memory, and displayed only when it is ready (and mode: the UI is rendered in memory, and displayed only when it is ready (and
only if it has not been cancelled by subsequent user actions). only if it has not been cancelled by subsequent user actions).
@@ -175,6 +175,6 @@ that current standard frameworks are not tailored to our needs. It is perfectly
fine, because they each chose a different set of tradeoffs. fine, because they each chose a different set of tradeoffs.
However, we feel that there is still room in the framework world for something However, we feel that there is still room in the framework world for something
that is different. For a framework that make choices compatible with Odoo. that is different. For a framework that makes choices compatible with Odoo.
And that is why we built Owl 🦉. And that is why we built Owl 🦉.
+2
View File
@@ -17,6 +17,7 @@ You will find here a complete reference of every feature, class or object
provided by Owl. provided by Owl.
- [Animations](reference/animations.md) - [Animations](reference/animations.md)
- [Browser](reference/browser.md)
- [Component](reference/component.md) - [Component](reference/component.md)
- [Content](reference/content.md) - [Content](reference/content.md)
- [Concurrency Model](reference/concurrency_model.md) - [Concurrency Model](reference/concurrency_model.md)
@@ -27,6 +28,7 @@ provided by Owl.
- [Event Handling](reference/event_handling.md) - [Event Handling](reference/event_handling.md)
- [Error Handling](reference/error_handling.md) - [Error Handling](reference/error_handling.md)
- [Hooks](reference/hooks.md) - [Hooks](reference/hooks.md)
- [Mounting a component](reference/mounting.md)
- [Miscellaneous Components](reference/misc.md) - [Miscellaneous Components](reference/misc.md)
- [Observer](reference/observer.md) - [Observer](reference/observer.md)
- [Props](reference/props.md) - [Props](reference/props.md)
+41 -9
View File
@@ -52,21 +52,20 @@ sequence of events will happen:
At node insertion: At node insertion:
- the css classes `name-enter` and `name-enter-active` will be added directly - the css classes `name-enter` and `name-enter-active` will be added directly
when the node is inserted into the DOM, when the node is inserted into the DOM.
- on the next animation frame: the css class `name-enter` will be removed and the - on the next animation frame: the css class `name-enter` will be removed and the
class `name-enter-to` will be added (so they can be used to trigger css class `name-enter-to` will be added (so they can be used to trigger css
transition effects), transition effects).
- the css class `name-enter-active` will be removed whenever a css transition - at the end of the transition, `name-enter-to` and `name-enter-active` will be removed.
ends.
At node destruction: At node destruction:
- the css classes `name-leave` and `name-leave-active` will be added before the - the css classes `name-leave` and `name-leave-active` will be added before the
node is removed to the DOM, node is removed to the DOM.
- the css class `name-leave` will be removed on the next animation frame (so it - on the next animation frame: the css class `name-leave` will be removed and the
can be used to trigger css transition effects), class `name-leave-to` will be added (so they can be used to trigger css
- the css class `name-leave-active` will be removed whenever a css transition transition effects).
ends. Only then will the element be removed from the DOM. - at the end of the transition, `name-leave-to` and `name-leave-active` will be removed.
For example, a simple fade in/out effect can be done with this: For example, a simple fade in/out effect can be done with this:
@@ -93,3 +92,36 @@ Notes:
Owl does not support more than one transition on a single node, so the Owl does not support more than one transition on a single node, so the
`t-transition` expression must be a single value (i.e. no space allowed). `t-transition` expression must be a single value (i.e. no space allowed).
## SCSS Mixins
If you use SCSS, you can use mixins to make generic animations. Here is an exemple with a fade in / fade out animation:
```scss
@mixin animation-fade($time, $name) {
.#{$name}_fade-enter-active,
.#{$name}_fade-active {
transition: all $time;
}
.#{$name}_fade-enter {
opacity: 0;
}
.#{$name}_fade-leave-to {
opacity: 0;
}
}
```
Usage:
```scss
@include animation-fade(0.5s, "o_notification");
```
You can now have in your template:
```xml
<SomeTag t-transition="o_notification_fade"/>
```
+33
View File
@@ -0,0 +1,33 @@
# 🦉 Browser 🦉
## Content
- [Overview](#overview)
- [Browser Content](#browser-content)
## Overview
The browser object contains some browser native APIs, such as `setTimeout`, that
are used by Owl and its utility functions. They are exposed with the intent of
making them mockable if necessary.
```js
owl.browser.setTimeout === window.setTimeout; // return true
```
For now, this object contains some functions that are not used by Owl. They
will eventually be removed in Owl 2.0.
## Browser Content
More specifically, the `browser` object contains the following methods and objects:
- `setTimeout`
- `clearTimeout`
- `setInterval`
- `clearInterval`
- `requestAnimationFrame`
- `random`
- `Date`
- `fetch`
- `localStorage`
+4
View File
@@ -270,6 +270,10 @@ We explain here all the public methods of the `Component` class.
// app is now visible // app is now visible
``` ```
Note that the normal way of mounting an application is by using the `mount`
method on a component class, not by creating the instance by hand. See the
documentation on [mounting applications](mounting.md).
* **`unmount()`**: in case a component needs to be detached/removed from the DOM, this * **`unmount()`**: in case a component needs to be detached/removed from the DOM, this
method can be used. Most applications should not call `unmount`, this is more method can be used. Most applications should not call `unmount`, this is more
useful to the underlying component system. useful to the underlying component system.
+8 -4
View File
@@ -7,13 +7,15 @@ For example, `Component` is available at `owl.Component` and `EventBus` is
exported as `owl.core.EventBus`. exported as `owl.core.EventBus`.
``` ```
browser
Component misc Component misc
Context AsyncRoot Context AsyncRoot
QWeb Portal QWeb Portal
Store router mount router
useState Link Store Link
config RouteComponent useState RouteComponent
mode Router config Router
mode
core tags core tags
EventBus css EventBus css
Observer xml Observer xml
@@ -27,6 +29,8 @@ hooks utils
useContext useContext
useState useState
useRef useRef
useComponent
useEnv
useSubEnv useSubEnv
useStore useStore
useDispatch useDispatch
+5 -20
View File
@@ -49,15 +49,14 @@ The correct way to customize an environment is to simply set it up on the root
component class, before the first component is created: component class, before the first component is created:
```js ```js
App.env = { const env = {
_t: myTranslateFunction, _t: myTranslateFunction,
user: {...}, user: {...},
services: { services: {
... ...
}, },
}; };
const app = new App(); mount(App, { target: document.body, env });
app.mount(document.body);
``` ```
It is also possible to simply share an environment between all root components, It is also possible to simply share an environment between all root components,
@@ -121,9 +120,8 @@ async function myEnv() {
} }
async function start() { async function start() {
App.env = await myEnv(); const env = await myEnv();
const app = new App(); mount(App, { target: document.body, env });
await app.mount(document.body);
} }
``` ```
@@ -135,17 +133,4 @@ the `QWeb` instance and a `browser` object:
- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary - `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
for Owl to be able to render anything for Owl to be able to render anything
- `browser`: this is an object that contains some common access points to the - `browser`: this is an object that contains some common access points to the
browser methods with a side effect. This is particularly useful when one want browser methods with a side effect. See [browser](browser.md) for more information. Note that the browser object will be removed from the environment in Owl 2.0.
to test more advanced components, and be able to mock those methods.
More specifically, the `browser` object contains the following methods and objects:
- `setTimeout`
- `clearTimeout`
- `setInterval`
- `clearInterval`
- `requestAnimationFrame`
- `random`
- `Date`
- `fetch`
- `localStorage`
+14 -4
View File
@@ -21,6 +21,8 @@
- [`useStore`](#usestore) - [`useStore`](#usestore)
- [`useDispatch`](#usedispatch) - [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters) - [`useGetters`](#usegetters)
- [`useComponent`](#usecomponent)
- [`useEnv`](#useenv)
- [Making customized hooks](#making-customized-hooks) - [Making customized hooks](#making-customized-hooks)
## Overview ## Overview
@@ -381,6 +383,16 @@ The `useDispatch` hook is the way for components to get a reference to the store
The `useGetters` hook is the way for components to get a reference to the store The `useGetters` hook is the way for components to get a reference to the store
getters. See the [store documentation](store.md) for more information. getters. See the [store documentation](store.md) for more information.
### `useComponent`
The `useComponent` hook is useful as a building block for some customized hooks,
that may need a reference to the component calling them.
### `useEnv`
The `useEnv` hook is useful as a building block for some customized hooks,
that may need a reference to the env of the component calling them.
### Making customized hooks ### Making customized hooks
Hooks are a wonderful way to organize the code of a complex component by feature Hooks are a wonderful way to organize the code of a complex component by feature
@@ -435,13 +447,11 @@ not the solution to every problem.
```js ```js
function useRouter() { function useRouter() {
return Component.current.env.router; const env = useEnv();
return env.router;
} }
``` ```
This means that we give control to the application developer to create the This means that we give control to the application developer to create the
router, which is good, so they can set it up, subclass it, ... And then, to router, which is good, so they can set it up, subclass it, ... And then, to
test our components, we can just add a mock router in the environment. test our components, we can just add a mock router in the environment.
Note: the code above makes use of the `Component.current` property. This is the
way hooks are able to get a reference to the component currently being created.
+2 -3
View File
@@ -43,7 +43,7 @@ workflow to help the user put in some data, which it could use later on.
JavaScript: JavaScript:
```js ```js
const { Component } = owl; const { Component, mount } = owl;
const { Portal } = owl.misc; const { Portal } = owl.misc;
class TeleportedComponent extends Component {} class TeleportedComponent extends Component {}
@@ -51,8 +51,7 @@ class App extends Component {
static components = { Portal, TeleportedComponent }; static components = { Portal, TeleportedComponent };
} }
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
``` ```
XML: XML:
+60
View File
@@ -0,0 +1,60 @@
# 🦉 Mounting an application 🦉
## Content
- [Overview](#overview)
- [API](#api)
## Overview
Mounting an Owl application is done by using the `mount` method (available in
`owl.mount` if you are using the iife build, or it can be directly imported
from `owl` if you are using a module system):
```js
const mount = { owl }; // if owl is available as an object
const env = { ... };
const app = await mount(MyComponent, { target: document.body, env });
```
Another example:
```js
const config = {
env: ...,
props: ...,
target: document.body,
position: "self",
};
const app = await mount(App, config);
```
A common way to initialize an application is to first setup an environment,
then to call the `mount` method.
## API
Mount takes two parameters:
- `C`, which should be a component class (NOT instance),
- `params`, which is an object with the following keys:
- `target (HTMLElement | DocumentFragment)`: the target of the mount operation
- `env (optional, Env)` an environment
- `position (optional, "first-child" | "last-child" | "self")` the position
where it should be mounted (see below for more informations)
- `props (optional, any)`: some initial values that are given as props. Useful
when the root component is configurable, or when testing sub components
Here are the various positions supported by Owl:
- `first-child`: with this option, the component will be prepended inside the target,
- `last-child` (default value): with this option, the component will be
appended in the target element,
- `self`: the target will be used as the root element for the component. This
means that the target has to be an HTMLElement (and not a document fragment).
In this situation, it is possible that the component cannot be unmounted. For
example, if its target is `document.body`.
The `mount` method returns a promise that resolves to the instance of the created
component.
+1 -1
View File
@@ -18,7 +18,7 @@ class Child extends Component {
} }
class Parent extends Component { class Parent extends Component {
static template = xml`<div><ComponentA a="state.a" b="'string'"/></div>`; static template = xml`<div><Child a="state.a" b="'string'"/></div>`;
static components = { Child }; static components = { Child };
state = useState({ a: "fromparent" }); state = useState({ a: "fromparent" });
} }
+17 -2
View File
@@ -25,6 +25,8 @@ some sub template, but still be the owner. For example, a generic dialog compone
will need to render some content, some footer, but with the parent as the will need to render some content, some footer, but with the parent as the
rendering context. rendering context.
Slots are inserted with the `t-slot` directive:
```xml ```xml
<div t-name="Dialog" class="modal"> <div t-name="Dialog" class="modal">
<div class="modal-title"><t t-esc="props.title"/></div> <div class="modal-title"><t t-esc="props.title"/></div>
@@ -62,7 +64,9 @@ This is deprecated and should no longer be used in new code.
## Reference ## Reference
Default slot: the first element inside the component which is not a named slot will ### Default Slot
The first element inside the component which is not a named slot will
be considered the `default` slot. For example: be considered the `default` slot. For example:
```xml ```xml
@@ -77,7 +81,9 @@ be considered the `default` slot. For example:
</div> </div>
``` ```
Default content: slots can define a default content, in case the parent did not define them: ### Default content
Slots can define a default content, in case the parent did not define them:
```xml ```xml
<div t-name="Parent"> <div t-name="Parent">
@@ -94,3 +100,12 @@ Rendering context: the content of the slots is actually rendered with the
rendering context corresponding to where it was defined, not where it is rendering context corresponding to where it was defined, not where it is
positioned. This allows the user to define event handlers that will be bound positioned. This allows the user to define event handlers that will be bound
to the correct component (usually, the grandparent of the slot content). to the correct component (usually, the grandparent of the slot content).
### Dynamic Slots
The `t-slot` directive is actually able to use any expressions, using string
interplolation:
```xml
<t t-slot="{{current}}" />
```
+4 -4
View File
@@ -21,8 +21,8 @@ argument, it executes it as soon as the DOM ready (or directly).
```js ```js
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function ([templates]) { Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function ([templates]) {
const qweb = new owl.QWeb({ templates }); const qweb = new owl.QWeb({ templates });
const app = new App({ qweb }); const env = { qweb };
app.mount(document.body); await mount(App, { env, target: document.body });
}); });
``` ```
@@ -31,8 +31,8 @@ or alternatively:
```js ```js
owl.utils.whenReady(function () { owl.utils.whenReady(function () {
const qweb = new owl.QWeb(); const qweb = new owl.QWeb();
const app = new App({ qweb }); const env = { qweb };
app.mount(document.body); await mount(App, { env, target: document.body });
}); });
``` ```
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "1.0.13", "version": "1.2.0",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js", "main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js", "browser": "dist/owl.iife.js",
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉 # 🦉 OWL Roadmap 🦉
- Current version: 1.0.13 - Current version: 1.2.0
- Status: stable - Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may This roadmap is only an attempt at predicting Owl's future. Everything may
+8 -1
View File
@@ -10,6 +10,8 @@ export interface Browser {
localStorage: Window["localStorage"]; localStorage: Window["localStorage"];
} }
let localStorage: Window["localStorage"] | null = null;
export const browser: Browser = { export const browser: Browser = {
setTimeout: window.setTimeout.bind(window), setTimeout: window.setTimeout.bind(window),
clearTimeout: window.clearTimeout.bind(window), clearTimeout: window.clearTimeout.bind(window),
@@ -19,5 +21,10 @@ export const browser: Browser = {
random: Math.random, random: Math.random,
Date: window.Date, Date: window.Date,
fetch: (window.fetch || (() => {})).bind(window), fetch: (window.fetch || (() => {})).bind(window),
localStorage: window.localStorage, get localStorage() {
return localStorage || window.localStorage;
},
set localStorage(newLocalStorage: Window["localStorage"]) {
localStorage = newLocalStorage;
},
}; };
+33 -1
View File
@@ -159,6 +159,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
if (!this.env.qweb) { if (!this.env.qweb) {
this.env.qweb = new QWeb(); this.env.qweb = new QWeb();
} }
// TODO: remove this in owl 2.0
if (!this.env.browser) { if (!this.env.browser) {
this.env.browser = browser; this.env.browser = browser;
} }
@@ -332,7 +333,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`; message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message); throw new Error(message);
} }
const fiber = new Fiber(null, this, false, target, position); const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false; fiber.shouldPatch = false;
if (!__owl__.vnode) { if (!__owl__.vnode) {
this.__prepareAndRender(fiber, () => {}); this.__prepareAndRender(fiber, () => {});
@@ -729,3 +730,34 @@ export class Component<Props extends {} = any, T extends Env = Env> {
} }
} }
} }
interface MountParameters {
env?: Env;
target: HTMLElement | DocumentFragment;
props?: any;
position?: MountOptions["position"];
}
interface Type<T> extends Function {
new (...args: any[]): T;
}
export async function mount<T extends Type<Component>>(
C: T,
params: MountParameters
): Promise<InstanceType<T>> {
const { env, props, target } = params;
let origEnv = C.hasOwnProperty("env") ? (C as any).env : null;
if (env) {
((C as any) as typeof Component).env = env;
}
const component: Component = new C(null, props);
if (origEnv) {
(C as any).env = origEnv;
} else {
delete (C as any).env;
}
const position = params.position || "last-child";
await component.mount(target, { position });
return component as any;
}
-10
View File
@@ -115,16 +115,6 @@ export function useContextWithCB(ctx: Context, component: Component, method): an
__owl__.observer = new Observer(); __owl__.observer = new Observer();
__owl__.observer.notifyCB = component.render.bind(component); __owl__.observer.notifyCB = component.render.bind(component);
} }
const currentCB = __owl__.observer.notifyCB;
__owl__.observer.notifyCB = function () {
if (ctx.rev > mapping[id]) {
// in this case, the context has been updated since we were rendering
// last, and we do not need to render here with the observer. A
// rendering is coming anyway, with the correct props.
return;
}
currentCB();
};
mapping[id] = 0; mapping[id] = 0;
const renderFn = __owl__.renderFn; const renderFn = __owl__.renderFn;
+21 -1
View File
@@ -1,4 +1,4 @@
import { Component } from "./component/component"; import { Component, Env } from "./component/component";
import { Observer } from "./core/observer"; import { Observer } from "./core/observer";
/** /**
@@ -118,6 +118,26 @@ export function useRef<C extends Component = Component>(name: string): Ref<C> {
}; };
} }
// -----------------------------------------------------------------------------
// "Builder" hooks
// -----------------------------------------------------------------------------
/**
* This hook is useful as a building block for some customized hooks, that may
* need a reference to the component calling them.
*/
export function useComponent<P, E extends Env>(): Component<P, E> {
return Component.current as any;
}
/**
* This hook is useful as a building block for some customized hooks, that may
* need a reference to the env of the component calling them.
*/
export function useEnv<E extends Env>(): E {
return Component.current.env as any;
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// useSubEnv // useSubEnv
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
+3 -1
View File
@@ -19,9 +19,10 @@ import { Link } from "./router/link";
import { RouteComponent } from "./router/route_component"; import { RouteComponent } from "./router/route_component";
import { Router } from "./router/router"; import { Router } from "./router/router";
export { Component } from "./component/component"; export { Component, mount } from "./component/component";
export { QWeb }; export { QWeb };
export { config }; export { config };
export { browser } from "./browser";
export const Context = _context.Context; export const Context = _context.Context;
export const useState = _hooks.useState; export const useState = _hooks.useState;
@@ -37,4 +38,5 @@ export const hooks = Object.assign({}, _hooks, {
useGetters: _store.useGetters, useGetters: _store.useGetters,
useStore: _store.useStore, useStore: _store.useStore,
}); });
export const __info__ = {}; export const __info__ = {};
+3 -1
View File
@@ -1,4 +1,5 @@
import { VNode } from "../vdom/index"; import { VNode } from "../vdom/index";
import { INTERP_REGEXP } from "./compilation_context";
import { QWeb } from "./qweb"; import { QWeb } from "./qweb";
/** /**
@@ -228,8 +229,9 @@ QWeb.addDirective({
priority: 80, priority: 80,
atNodeEncounter({ ctx, value, node, qweb }): boolean { atNodeEncounter({ ctx, value, node, qweb }): boolean {
const slotKey = ctx.generateID(); const slotKey = ctx.generateID();
const valueExpr = value.match(INTERP_REGEXP) ? ctx.interpolate(value) : `'${value}'`;
ctx.addLine( ctx.addLine(
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];` `const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + ${valueExpr}];`
); );
ctx.addIf(`slot${slotKey}`); ctx.addIf(`slot${slotKey}`);
let parentNode = `c${ctx.parentNode}`; let parentNode = `c${ctx.parentNode}`;
+3 -1
View File
@@ -70,6 +70,7 @@ const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/; const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g; const whitespaceRE = /\s+/g;
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
const NODE_HOOKS_PARAMS = { const NODE_HOOKS_PARAMS = {
create: "(_, n)", create: "(_, n)",
@@ -496,7 +497,8 @@ export class QWeb extends EventBus {
} }
if (this.translateFn) { if (this.translateFn) {
if ((node.parentNode as any).getAttribute("t-translation") !== "off") { if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
text = this.translateFn(text); const match = translationRE.exec(text);
text = match[1] + this.translateFn(match[2]) + match[3];
} }
} }
if (ctx.parentNode) { if (ctx.parentNode) {
+1 -2
View File
@@ -1,5 +1,4 @@
import { Component } from "./component/component"; import { Component, Env } from "./component/component";
import { Env } from "./component/component";
import { Context, useContextWithCB } from "./context"; import { Context, useContextWithCB } from "./context";
import { onWillUpdateProps } from "./hooks"; import { onWillUpdateProps } from "./hooks";
@@ -220,6 +220,25 @@ exports[`t-slot directive default slot work with text nodes 1`] = `
}" }"
`; `;
exports[`t-slot directive dynamic t-slot call 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c10 = [], p10 = {key:10,on:{}};
let vn10 = h('button', p10, c10);
extra.handlers['click__11__'] = extra.handlers['click__11__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['toggle'](e);};
p10.on['click'] = extra.handlers['click__11__'];
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + (scope['current'].slot)];
if (slot12) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c10, parent: extra.parent || context}));
}
return vn10;
}"
`;
exports[`t-slot directive multiple roots are allowed in a default slot 1`] = ` exports[`t-slot directive multiple roots are allowed in a default slot 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
+19 -20
View File
@@ -1,4 +1,4 @@
import { Component, Env } from "../../src/component/component"; import { Component, Env, mount } from "../../src/component/component";
import { EventBus } from "../../src/core/event_bus"; import { EventBus } from "../../src/core/event_bus";
import { useRef, useState } from "../../src/hooks"; import { useRef, useState } from "../../src/hooks";
import { QWeb } from "../../src/qweb/qweb"; import { QWeb } from "../../src/qweb/qweb";
@@ -69,18 +69,23 @@ describe("basic widget properties", () => {
class SomeWidget extends Component { class SomeWidget extends Component {
static template = xml`<div>content</div>`; static template = xml`<div>content</div>`;
} }
const widget = new SomeWidget(); await mount(SomeWidget, { target: fixture });
widget.mount(fixture);
await nextTick();
expect(fixture.innerHTML).toBe("<div>content</div>"); expect(fixture.innerHTML).toBe("<div>content</div>");
}); });
test("can be mounted with props", async () => {
class SomeWidget extends Component {
static template = xml`<div><t t-esc="props.content"/></div>`;
}
await mount(SomeWidget, { target: fixture, props: { content: "foo" } });
expect(fixture.innerHTML).toBe("<div>foo</div>");
});
test("can be mounted on a documentFragment", async () => { test("can be mounted on a documentFragment", async () => {
class SomeWidget extends Component { class SomeWidget extends Component {
static template = xml`<div>content</div>`; static template = xml`<div>content</div>`;
} }
const widget = new SomeWidget(); const widget = await mount(SomeWidget, { target: document.createDocumentFragment() });
await widget.mount(document.createDocumentFragment());
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
await widget.mount(fixture); await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>content</div>"); expect(fixture.innerHTML).toBe("<div>content</div>");
@@ -90,10 +95,9 @@ describe("basic widget properties", () => {
class SomeWidget extends Component { class SomeWidget extends Component {
static template = xml`<div>content</div>`; static template = xml`<div>content</div>`;
} }
const widget = new SomeWidget();
let error; let error;
try { try {
await widget.mount(null as any); await mount(SomeWidget, { target: null as any });
} catch (e) { } catch (e) {
error = e; error = e;
} }
@@ -107,10 +111,9 @@ describe("basic widget properties", () => {
class SomeWidget extends Component { class SomeWidget extends Component {
static template = xml`<t/>`; static template = xml`<t/>`;
} }
const widget = new SomeWidget();
let error; let error;
try { try {
await widget.mount(fixture); await mount(SomeWidget, { target: fixture });
} catch (e) { } catch (e) {
error = e; error = e;
} }
@@ -137,8 +140,7 @@ describe("basic widget properties", () => {
}); });
} }
const counter = new Counter(); const counter = await mount(Counter, { target: fixture });
counter.mount(fixture);
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div>0<button>Inc</button></div>"); expect(fixture.innerHTML).toBe("<div>0<button>Inc</button></div>");
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0]; const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
@@ -156,8 +158,7 @@ describe("basic widget properties", () => {
static components = { Child }; static components = { Child };
} }
const parent = new Parent(); await mount(Parent, { target: fixture });
await parent.mount(fixture);
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe("<div><span></span></div>"); expect(fixture.innerHTML).toBe("<div><span></span></div>");
}); });
@@ -171,9 +172,8 @@ describe("basic widget properties", () => {
}); });
} }
const counter = new Counter();
const target = document.createElement("div"); const target = document.createElement("div");
await counter.mount(target); const counter = await mount(Counter, { target: target });
expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>"); expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>");
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0]; const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
button.click(); button.click();
@@ -188,8 +188,7 @@ describe("basic widget properties", () => {
<div style="font-weight:bold;" class="some-class">world</div> <div style="font-weight:bold;" class="some-class">world</div>
`; `;
} }
const widget = new StyledWidget(); await mount(StyledWidget, { target: fixture });
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div style="font-weight:bold;" class="some-class">world</div>`); expect(fixture.innerHTML).toBe(`<div style="font-weight:bold;" class="some-class">world</div>`);
}); });
@@ -212,8 +211,8 @@ describe("basic widget properties", () => {
steps.push("patched"); steps.push("patched");
} }
} }
const widget = new TestW(); await mount(TestW, { target: fixture });
await widget.mount(fixture);
expect(steps).toEqual(["__render", "mounted"]); expect(steps).toEqual(["__render", "mounted"]);
}); });
+34 -1
View File
@@ -1,4 +1,4 @@
import { Component, Env } from "../../src/component/component"; import { Component, Env, mount } from "../../src/component/component";
import { QWeb } from "../../src/qweb/qweb"; import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks"; import { useState, useRef } from "../../src/hooks";
@@ -1085,4 +1085,37 @@ describe("t-slot directive", () => {
expect(fixture.innerHTML).toBe("<div><div><p>Ablip</p><div><p>Bblip</p></div></div></div>"); expect(fixture.innerHTML).toBe("<div><div><p>Ablip</p><div><p>Bblip</p></div></div></div>");
}); });
test("dynamic t-slot call", async () => {
class Toggler extends Component {
static template = xml`<button t-on-click="toggle"><t t-slot="{{current.slot}}"/></button>`;
current = useState({ slot: "slot1" });
toggle() {
this.current.slot = this.current.slot === "slot1" ? "slot2" : "slot1";
}
}
class Parent extends Component {
static template = xml`
<div>
<Toggler>
<t t-set-slot="slot1"><p>slot1</p><span>content</span></t>
<t t-set-slot="slot2"><h1>slot2</h1></t>
</Toggler>
</div>`;
static components = { Toggler };
}
await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div><button><p>slot1</p><span>content</span></button></div>");
fixture.querySelector<HTMLElement>("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button><h1>slot2</h1></button></div>");
fixture.querySelector<HTMLElement>("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button><p>slot1</p><span>content</span></button></div>");
expect(env.qweb.templates[Toggler.template].fn.toString()).toMatchSnapshot();
});
}); });
+56 -17
View File
@@ -1,4 +1,4 @@
import { Component, Env } from "../../src/component/component"; import { Component, Env, mount } from "../../src/component/component";
import { useState } from "../../src/hooks"; import { useState } from "../../src/hooks";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick, nextMicroTick } from "../helpers"; import { makeDeferred, makeTestEnv, makeTestFixture, nextTick, nextMicroTick } from "../helpers";
@@ -37,8 +37,7 @@ describe("mount targets", () => {
div.innerHTML = `<p>pre-existing</p>`; div.innerHTML = `<p>pre-existing</p>`;
fixture.appendChild(div); fixture.appendChild(div);
const app = new App(); const app = await mount(App, { target: div, position: "self" });
await app.mount(div, { position: "self" });
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
`<div class="arbitrary custom"><p>pre-existing</p>app<p>another tag</p></div>` `<div class="arbitrary custom"><p>pre-existing</p>app<p>another tag</p></div>`
@@ -66,10 +65,9 @@ describe("mount targets", () => {
const div = document.createElement("div"); const div = document.createElement("div");
fixture.appendChild(div); fixture.appendChild(div);
const app = new App();
let error; let error;
try { try {
await app.mount(div, { position: "self" }); await mount(App, { target: div, position: "self" });
} catch (e) { } catch (e) {
error = e; error = e;
} }
@@ -84,8 +82,7 @@ describe("mount targets", () => {
const span = document.createElement("span"); const span = document.createElement("span");
fixture.appendChild(span); fixture.appendChild(span);
const app = new App(); await mount(App, { target: fixture, position: "first-child" });
await app.mount(fixture, { position: "first-child" });
expect(fixture.innerHTML).toBe("<div>app</div><span></span>"); expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
}); });
@@ -96,8 +93,7 @@ describe("mount targets", () => {
const span = document.createElement("span"); const span = document.createElement("span");
fixture.appendChild(span); fixture.appendChild(span);
const app = new App(); await mount(App, { target: fixture, position: "last-child" });
await app.mount(fixture, { position: "last-child" });
expect(fixture.innerHTML).toBe("<span></span><div>app</div>"); expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
}); });
@@ -108,8 +104,7 @@ describe("mount targets", () => {
const span = document.createElement("span"); const span = document.createElement("span");
fixture.appendChild(span); fixture.appendChild(span);
const app = new App(); await mount(App, { target: fixture });
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<span></span><div>app</div>"); expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
}); });
}); });
@@ -133,8 +128,7 @@ describe("unmounting and remounting", () => {
} }
} }
const w = new MyWidget(); const w = await mount(MyWidget, { target: fixture });
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>"); expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]); expect(steps).toEqual(["willstart", "mounted"]);
@@ -162,8 +156,7 @@ describe("unmounting and remounting", () => {
} }
} }
const w = new MyWidget(); const w = await mount(MyWidget, { target: fixture });
await w.mount(fixture);
await w.mount(fixture); await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>"); expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]); expect(steps).toEqual(["willstart", "mounted"]);
@@ -203,8 +196,7 @@ describe("unmounting and remounting", () => {
state = useState({ val: 1, flag: true }); state = useState({ val: 1, flag: true });
} }
const widget = new Parent(); const widget = await mount(Parent, { target: fixture });
await widget.mount(fixture);
expect(steps).toEqual(["render"]); expect(steps).toEqual(["render"]);
expect(fixture.innerHTML).toBe("<div><span>12</span></div>"); expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
widget.state.flag = false; widget.state.flag = false;
@@ -608,4 +600,51 @@ describe("unmounting and remounting", () => {
await parent.render(); await parent.render();
expect(fixture.textContent).toBe("fixedsome text"); expect(fixture.textContent).toBe("fixedsome text");
}); });
test("remounting component tree where a component implement shouldupdate", async () => {
let state: any;
const steps = [];
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/></div>`;
state = useState({ word: "hello" });
constructor(parent, props) {
super(parent, props);
state = this.state;
}
patched() {
steps.push("patched");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willUnmount");
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
}
const parent = await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
parent.unmount();
expect(fixture.innerHTML).toBe("");
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
state.word = "test";
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld</div></div>");
expect(steps).toEqual(["mounted", "willUnmount", "mounted", "patched"]);
});
}); });
+20 -1
View File
@@ -289,7 +289,26 @@ describe("Context", () => {
expect(testContext.subscriptions.update.length).toBe(0); expect(testContext.subscriptions.update.length).toBe(0);
}); });
test("concurrent renderings", async () => { test.skip("concurrent renderings", async () => {
/**
* Note: this test is interesting, but sadly just an incomplete attempt at
* protecting users against themselves. With the context API, it is not
* possible for the framework to protect completely against crashes. Maybe
* like in this case, when a component is in a simple hierarchy where all
* renderings come from the context changes, but in a real case, where some
* code can trigger a rendering independently, it is insufficient.
*
* The main problem is that the sub component depends on some external state,
* which may be modified, and then incompatible with the component actual
* state (for example, if the sub component has an id key related to some
* object that has been removed from the context).
*
* For now, sadly, the only solution is that components that depends on external
* state should guarantee their own integrity themselves. Then maybe this
* could be solved at the level of a state management solution that has a
* more advanced API, to let components determine if they should be updated
* or not (so, something slightly more advanced that the useStore hook).
*/
const testContext = new Context({ x: { n: 1 }, key: "x" }); const testContext = new Context({ x: { n: 1 }, key: "x" });
const def = makeDeferred(); const def = makeDeferred();
let stateC; let stateC;
+28
View File
@@ -9,8 +9,10 @@ import {
onWillPatch, onWillPatch,
onWillStart, onWillStart,
onWillUpdateProps, onWillUpdateProps,
useEnv,
useSubEnv, useSubEnv,
useExternalListener, useExternalListener,
useComponent,
} from "../src/hooks"; } from "../src/hooks";
import { xml } from "../src/tags"; import { xml } from "../src/tags";
@@ -520,6 +522,19 @@ describe("hooks", () => {
}); });
}); });
test("can use useEnv", async () => {
expect.assertions(1);
class TestComponent extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor() {
super();
expect(useEnv()).toBe(env);
}
}
const component = new TestComponent();
await component.mount(fixture);
});
test("can use sub env", async () => { test("can use sub env", async () => {
class TestComponent extends Component { class TestComponent extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`; static template = xml`<div><t t-esc="env.val"/></div>`;
@@ -535,6 +550,19 @@ describe("hooks", () => {
expect(component.env).toHaveProperty("val"); expect(component.env).toHaveProperty("val");
}); });
test("can use useComponent", async () => {
expect.assertions(1);
class TestComponent extends Component {
static template = xml`<div></div>`;
constructor() {
super();
expect(useComponent()).toBe(this);
}
}
const component = new TestComponent();
await component.mount(fixture);
});
test("parent and child env", async () => { test("parent and child env", async () => {
class Child extends Component { class Child extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`; static template = xml`<div><t t-esc="env.val"/></div>`;
@@ -3924,6 +3924,18 @@ exports[`translation support some attributes are translated 1`] = `
}" }"
`; `;
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
c1.push({text: \` mot \`});
return vn1;
}"
`;
exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = ` exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
+11
View File
@@ -2154,6 +2154,17 @@ describe("translation support", () => {
'<div><p label="mot">mot</p><p title="mot">mot</p><p placeholder="mot">mot</p><p alt="mot">mot</p><p something="word">mot</p></div>' '<div><p label="mot">mot</p><p title="mot">mot</p><p placeholder="mot">mot</p><p alt="mot">mot</p><p something="word">mot</p></div>'
); );
}); });
test("translation is done on the trimmed text, with extra spaces readded after", () => {
const translations = {
word: "mot",
};
const translateFn = jest.fn((expr) => translations[expr] || expr);
const qweb = new QWeb({ translateFn });
qweb.addTemplate("test", "<div> word </div>");
expect(renderToString(qweb, "test")).toBe("<div> mot </div>");
expect(translateFn).toHaveBeenCalledWith("word");
});
}); });
describe("t-key tests", () => { describe("t-key tests", () => {
+105 -1
View File
@@ -1,4 +1,4 @@
import { Component, Env } from "../src/component/component"; import { Component, Env, mount } from "../src/component/component";
import { Store, useStore, useDispatch, useGetters, EnvWithStore } from "../src/store"; import { Store, useStore, useDispatch, useGetters, EnvWithStore } from "../src/store";
import { useState } from "../src/hooks"; import { useState } from "../src/hooks";
import { xml } from "../src/tags"; import { xml } from "../src/tags";
@@ -1241,4 +1241,108 @@ describe("various scenarios", () => {
await nextTick(); await nextTick();
expect(fixture.innerHTML).toMatchSnapshot(); expect(fixture.innerHTML).toMatchSnapshot();
}); });
test("component with store, useState and shouldUpdate=false", async () => {
let state: any;
const store = new Store({ state: { rev: 0 } });
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/></div>`;
state = useState({ word: "hello" });
constructor(parent, props) {
super(parent, props);
state = this.state;
useStore((props) => {
return 1;
});
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
constructor(parent, props) {
super(parent, props);
useStore((props) => store.state.rev);
}
}
(env as any).store = store;
await mount(Parent, { target: fixture, env });
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
store.state.rev++;
// this is the key to the bug, it makes Parent be in "render" state but not
// yet rendered while the change of state happens
await Promise.resolve();
state.word = "test";
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld</div></div>");
});
test("component with store, useState, shouldUpdate=false and child with shouldupdate false", async () => {
let state: any;
const store = new Store({ state: { rev: 0 } });
class ChildChild extends Component {
static template = xml`<div><t t-esc="props.value"/></div>`;
shouldUpdate() {
return false;
}
}
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/><ChildChild value="state.value"/></div>`;
static components = { ChildChild };
state = useState({ word: "hello", value: 3 });
constructor(parent, props) {
super(parent, props);
state = this.state;
useStore((props) => {
return 1;
});
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
constructor(parent, props) {
super(parent, props);
useStore((props) => store.state.rev);
}
}
(env as any).store = store;
await mount(Parent, { target: fixture, env });
expect(fixture.innerHTML).toBe("<div><div>helloWorld<div>3</div></div></div>");
store.state.rev++;
// this is the key to the bug, it makes Parent be in "render" state but not
// yet rendered while the change of state happens
await Promise.resolve();
state.word = "test";
state.value = 44;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld<div>3</div></div></div>");
});
}); });
+4 -4
View File
@@ -1,5 +1,6 @@
import { SAMPLES } from "./samples.js"; import { SAMPLES } from "./samples.js";
const { useState, useRef, onMounted, onWillUnmount } = owl.hooks; const { mount, hooks } = owl;
const { useState, useRef, onMounted, onWillUnmount } = hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Constants, helpers, utils // Constants, helpers, utils
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -419,9 +420,8 @@ async function start() {
owl.utils.whenReady() owl.utils.whenReady()
]); ]);
const qweb = new owl.QWeb({ templates }); const qweb = new owl.QWeb({ templates });
owl.Component.env = { qweb }; const env = { qweb };
const app = new App(); await mount(App, {target: document.body, env});
app.mount(document.body);
} }
start(); start();
+27 -38
View File
@@ -1,5 +1,5 @@
const COMPONENTS = `// In this example, we show how components can be defined and created. const COMPONENTS = `// In this example, we show how components can be defined and created.
const { Component, useState } = owl; const { Component, useState, mount } = owl;
class Greeter extends Component { class Greeter extends Component {
constructor() { constructor() {
@@ -22,8 +22,7 @@ class App extends Component {
App.components = { Greeter }; App.components = { Greeter };
// Application setup // Application setup
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const COMPONENTS_XML = `<templates> const COMPONENTS_XML = `<templates>
@@ -50,7 +49,7 @@ const COMPONENTS_CSS = `.greeter {
const ANIMATION = `// The goal of this component is to see how the t-transition directive can be const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
// used to generate simple transition effects. // used to generate simple transition effects.
const { Component, useState } = owl; const { Component, useState, mount } = owl;
class Counter extends Component { class Counter extends Component {
constructor() { constructor() {
@@ -80,8 +79,7 @@ class App extends Component {
} }
App.components = { Counter }; App.components = { Counter };
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const ANIMATION_XML = `<templates> const ANIMATION_XML = `<templates>
@@ -193,7 +191,7 @@ const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
// methods in the console. Try modifying its state by clicking on it, or by // methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what // clicking on the two main buttons, and look into the console to see what
// happens. // happens.
const { Component, useState } = owl; const { Component, useState, mount } = owl;
class DemoComponent extends Component { class DemoComponent extends Component {
constructor() { constructor() {
@@ -240,8 +238,7 @@ class App extends Component {
} }
App.components = { DemoComponent }; App.components = { DemoComponent };
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const LIFECYCLE_DEMO_XML = `<templates> const LIFECYCLE_DEMO_XML = `<templates>
@@ -273,7 +270,8 @@ const LIFECYCLE_CSS = `button {
}`; }`;
const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined. const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined.
const {useState, onMounted, onWillUnmount} = owl.hooks; const { hooks, mount } = owl;
const {useState, onMounted, onWillUnmount} = hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse // We define here a custom behaviour: this hook tracks the state of the mouse
// position // position
@@ -312,8 +310,7 @@ class App extends owl.Component {
} }
// Application setup // Application setup
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const HOOKS_DEMO_XML = `<templates> const HOOKS_DEMO_XML = `<templates>
@@ -332,7 +329,7 @@ const HOOKS_CSS = `button {
const CONTEXT_JS = `// In this example, we show how components can use the Context and 'useContext' const CONTEXT_JS = `// In this example, we show how components can use the Context and 'useContext'
// hook to share information between them. // hook to share information between them.
const { Component, Context } = owl; const { Component, Context, mount } = owl;
const { useContext } = owl.hooks; const { useContext } = owl.hooks;
class ToolbarButton extends Component { class ToolbarButton extends Component {
@@ -367,8 +364,7 @@ const themeContext = new Context({
}); });
// Add the themeContext the environment to make it available to all components // Add the themeContext the environment to make it available to all components
App.env.themeContext = themeContext; App.env.themeContext = themeContext;
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const CONTEXT_XML = `<templates> const CONTEXT_XML = `<templates>
@@ -395,7 +391,7 @@ const TODO_APP_STORE = `// This example is an implementation of the TodoList app
// //
// In this implementation, we use the owl Store class to manage the state. It // In this implementation, we use the owl Store class to manage the state. It
// is very similar to the VueX store. // is very similar to the VueX store.
const { Component, useState } = owl; const { Component, useState, mount } = owl;
const { useRef, useStore, useDispatch, onPatched, onMounted } = owl.hooks; const { useRef, useStore, useDispatch, onPatched, onMounted } = owl.hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -568,8 +564,7 @@ function makeStore() {
} }
TodoApp.env.store = makeStore(); TodoApp.env.store = makeStore();
const app = new TodoApp(); mount(TodoApp, { target: document.body });
app.mount(document.body);
`; `;
const TODO_APP_STORE_XML = `<templates> const TODO_APP_STORE_XML = `<templates>
@@ -1060,8 +1055,7 @@ function setupResponsivePlugin(env) {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
setupResponsivePlugin(App.env); setupResponsivePlugin(App.env);
const app = new App(); owl.mount(App, { target: document.body });
app.mount(document.body);
`; `;
const RESPONSIVE_XML = `<templates> const RESPONSIVE_XML = `<templates>
@@ -1173,7 +1167,7 @@ const SLOTS = `// We show here how slots can be used to create generic component
// //
// Note that the t-on-click event, defined in the App template, is executed in // Note that the t-on-click event, defined in the App template, is executed in
// the context of the App component, even though it is inside the Card component // the context of the App component, even though it is inside the Card component
const { Component, useState } = owl; const { Component, useState, mount } = owl;
class Card extends Component { class Card extends Component {
constructor() { constructor() {
@@ -1211,8 +1205,8 @@ class App extends Component {
App.components = {Card, Counter}; App.components = {Card, Counter};
// Application setup // Application setup
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);`; `;
const SLOTS_XML = `<templates> const SLOTS_XML = `<templates>
<div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'"> <div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'">
@@ -1298,7 +1292,7 @@ const ASYNC_COMPONENTS = `// This example will not work if your browser does not
// However, we don't want renderings of the other sub component to be delayed // However, we don't want renderings of the other sub component to be delayed
// because of the slow component. We use the AsyncRoot component for this // because of the slow component. We use the AsyncRoot component for this
// purpose. Try removing it to see the difference. // purpose. Try removing it to see the difference.
const { Component, useState } = owl; const { Component, useState, mount } = owl;
const { AsyncRoot } = owl.misc; const { AsyncRoot } = owl.misc;
class SlowComponent extends Component { class SlowComponent extends Component {
@@ -1329,8 +1323,7 @@ class App extends Component {
} }
App.components = {SlowComponent, NotificationList, AsyncRoot}; App.components = {SlowComponent, NotificationList, AsyncRoot};
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const ASYNC_COMPONENTS_XML = `<templates> const ASYNC_COMPONENTS_XML = `<templates>
@@ -1385,7 +1378,7 @@ const FORM = `// This example illustrate how the t-model directive can be used t
// data between html inputs (and select/textareas) and the state of a component. // data between html inputs (and select/textareas) and the state of a component.
// Note that there are two controls with t-model="color": they are totally // Note that there are two controls with t-model="color": they are totally
// synchronized. // synchronized.
const { Component, useState } = owl; const { Component, useState, mount } = owl;
class Form extends Component { class Form extends Component {
constructor() { constructor() {
@@ -1401,8 +1394,7 @@ class Form extends Component {
} }
// Application setup // Application setup
const form = new Form(); mount(Form, { target: document.body });
form.mount(document.body);
`; `;
const FORM_XML = `<templates> const FORM_XML = `<templates>
@@ -1448,7 +1440,7 @@ const PORTAL_COMPONENTS = `
// This shows the expected use case of Portal // This shows the expected use case of Portal
// which is to implement something similar // which is to implement something similar
// to bootstrap modal // to bootstrap modal
const { Component, useState } = owl; const { Component, useState, mount } = owl;
const { Portal } = owl.misc; const { Portal } = owl.misc;
class Modal extends Component {} class Modal extends Component {}
@@ -1470,8 +1462,7 @@ class App extends Component {
App.components = { Dialog , Interstellar }; App.components = { Dialog , Interstellar };
// Application setup // Application setup
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const PORTAL_XML = ` const PORTAL_XML = `
@@ -1559,7 +1550,7 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate
// - minimal width/height // - minimal width/height
// - better heuristic for initial window position // - better heuristic for initial window position
// - ... // - ...
const { Component, useState } = owl; const { Component, useState, mount } = owl;
const { useRef } = owl.hooks; const { useRef } = owl.hooks;
class HelloWorld extends Component {} class HelloWorld extends Component {}
@@ -1699,8 +1690,7 @@ const windows = [
]; ];
App.env.windows = windows; App.env.windows = windows;
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
const WMS_XML = `<templates> const WMS_XML = `<templates>
@@ -1818,7 +1808,7 @@ const SFC = `// This example illustrates how Owl enables single file components,
// Note that this example has no external xml or css file, everything is // Note that this example has no external xml or css file, everything is
// contained in a single js file. // contained in a single js file.
const { Component, useState, tags } = owl; const { Component, useState, tags, mount } = owl;
const { xml, css } = tags; const { xml, css } = tags;
// Counter component // Counter component
@@ -1850,8 +1840,7 @@ App.template = APP_TEMPLATE;
App.components = { Counter }; App.components = { Counter };
// Application setup // Application setup
const app = new App(); mount(App, { target: document.body });
app.mount(document.body);
`; `;
export const SAMPLES = [ export const SAMPLES = [