Compare commits

..

20 Commits

Author SHA1 Message Date
Géry Debongnie b0e2ef82f7 [REL] bump to v0.24.1 2019-10-28 14:58:52 +01:00
Aaron Bohy c9c2b3fa6d [IMP] tests: add concurrency test 2019-10-28 14:46:21 +01:00
Géry Debongnie 62608cbb0d [DOC] add architecture notes on rendering pipeline 2019-10-28 14:23:02 +01:00
Géry Debongnie 3035a9f009 [DOC] move reference doc in subfolder 2019-10-28 14:23:02 +01:00
Géry Debongnie c2adb429bd [FIX] package.json: update node and typescript version
closes #414
2019-10-28 13:58:44 +01:00
Géry Debongnie da9dda5eca [DOC] component: add more information on t-key
closes #353
2019-10-28 11:45:51 +01:00
Géry Debongnie 5614c85b04 [IMP] qweb: implement t-key with a directive
closes #331
2019-10-28 11:45:51 +01:00
Aaron Bohy 5d57a7bf13 [FIX] component: updateProps: validation and default values
Before this rev., default values weren't taken into account when
validating props whenever a component was updated. Moreover, there
was no test attesting that props were validated at update.
2019-10-28 11:32:43 +01:00
Géry Debongnie 4ebe419c56 [FIX] component: make sure props are validated in all cases
closes #379
2019-10-28 11:32:43 +01:00
Géry Debongnie 9779cd196c [REF] fiber: small cleanups 2019-10-28 11:32:43 +01:00
Géry Debongnie 9cee12d7b4 [IMP] component: propagate error to mount and render 2019-10-28 11:32:43 +01:00
Géry Debongnie 2c563ee380 [REF] component: move error handling into the fiber 2019-10-28 11:32:43 +01:00
Géry Debongnie 2aa705f5c8 [FIX] router: preserve pathname in hash mode
closes #397
2019-10-28 11:32:43 +01:00
Géry Debongnie 5911c8e3f6 [DOC] store,hooks: useState/useStore should return object
or array

closes #404
2019-10-28 11:32:43 +01:00
Géry Debongnie 2279dafbec [IMP] qweb: add an option to setup a translate function
closes #393
2019-10-28 11:32:41 +01:00
Géry Debongnie f07ec21a07 [FIX] playground: add missing ;
Because of that missing ";", the playground application tried to inject
this:

window.TEMPLATES = `...`

instead of this:

window.TEMPLATES = `...`;

So, when the following line started with a (, then the javascript code
was interpreted as a function call. And it could happen, because some
people like to use the IIFE syntax to have some contained code.
2019-10-26 09:34:51 +02:00
Géry Debongnie b5b6b7342e [DOC] improve main readme.md 2019-10-26 09:27:35 +02:00
Géry Debongnie 97b914b9cd [IMP] playground: log url for current owl commit
closes #405
2019-10-26 09:21:04 +02:00
Géry Debongnie 9dfcfda365 [DOC] add more info on xml tag and single file component
closes #401
2019-10-26 09:10:50 +02:00
Géry Debongnie e026f537ae [ADD] tools: add benchmarks for v0.24.0 2019-10-25 17:17:20 +02:00
50 changed files with 1411 additions and 563 deletions
+11 -15
View File
@@ -28,20 +28,16 @@ Owl is currently mostly stable. Possible future changes are explained in the
Here is a short example to illustrate interactive components:
```javascript
import { Component, QWeb, useState } from "owl";
import { xml } from "owl/tags";
const { Component, QWeb, useState } = owl;
const { xml } = owl.tags;
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
<button t-on-click="state.value++">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
class App extends Component {
@@ -58,8 +54,8 @@ const app = new App({ qweb: new QWeb() });
app.mount(document.body);
```
Note that the counter component is made reactive with the [`useState`](doc/hooks.md#usestate)
hook. Also, all examples here uses the `xml` helper to define inline templates.
Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
Also, all examples here uses the [`xml` helper](doc/reference/tags.md#xml-tag) to define inline templates.
But this is not mandatory, many applications will load templates separately.
More interesting examples can be found on the
@@ -97,8 +93,8 @@ A complete documentation for Owl can be found here:
The most important sections are:
- [Quick Start](doc/quick_start.md)
- [Component](doc/component.md)
- [Hooks](doc/hooks.md)
- [Component](doc/reference/component.md)
- [Hooks](doc/reference/hooks.md)
Found an issue in the documentation? A broken link? Some outdated information?
Submit a PR!
@@ -107,8 +103,8 @@ Submit a PR!
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-0.24.0.js](https://github.com/odoo/owl/releases/download/v0.24.0/owl.js)
- [owl-0.24.0.min.js](https://github.com/odoo/owl/releases/download/v0.24.0/owl.min.js)
- [owl-0.24.1.js](https://github.com/odoo/owl/releases/download/v0.24.1/owl.js)
- [owl-0.24.1.min.js](https://github.com/odoo/owl/releases/download/v0.24.1/owl.min.js)
Some npm scripts are available:
@@ -132,7 +128,7 @@ Owl components in an application are used to define a (dynamic) tree of componen
```
**Environment:** the root component is special: it is created with an environment,
which should contain a `QWeb` instance. The environment is then automatically
which should contain a [`QWeb` instance](doc/reference/qweb.md). The environment is then automatically
propagated to each sub components (and accessible in the `this.env` property).
```js
@@ -259,7 +255,7 @@ class Parent extends Component {
In this example, the `OrderLine` component trigger a `add-to-order` event. This
will generate a DOM event which will bubble along the DOM tree. It will then be
intercepted by the parent component, which will then get the line (from the
`detail` key) and then increment its quantity. See the section on [event handling](doc/component.md#event-handling)
`detail` key) and then increment its quantity. See the section on [event handling](doc/reference/component.md#event-handling)
for more details on how events work.
Note that this example would have also worked if the `OrderLine` component
+34
View File
@@ -0,0 +1,34 @@
# 🦉 Rendering Pipeline 🦉
We explain here how Owl is designed, from the perspective of its rendering
pipeline.
Warning: these notes are technical by nature, and intended for people working
on Owl (or interested in understanding its design).
## Overview
A rendering occurs in two phases:
- virtual rendering: this generates the virtual dom in memory, asynchronously
- patch: applies a virtual tree to the screen (synchronously)
There are several classes involved in a rendering:
- components
- a scheduler
- fibers: small objects containing some metadata, associated with a rendering of
a specific component
Components are organized in a dynamic component tree, visible in the user
interface. Whenever a rendering is initiated in a component `C`:
- a fiber is created on `C` with the rendering props information
- the virtual rendering phase starts on C (will asynchronously render all the
child components)
- the fiber is added to the scheduler, which will poll continuously, every
animation frame, if the fiber is done
- once it is done, the scheduler will call the task callback, which will apply
the patch (if it was not cancelled in the meantime).
+4 -4
View File
@@ -79,7 +79,7 @@ additional tools, we made a lot of effort to make the most of the web platform.
For example, Owl uses the standard `xml` parser that comes with every browser.
Because of that, Owl did not have to write its own template parser. Another
example is the [`xml`](tags.md#xml-tag) tag helper function, which makes use of
example is the [`xml`](reference/tags.md#xml-tag) tag helper function, which makes use of
native template literals to allow in a natural way to write `xml` templates
directly in the javascript code. This can be easily integrated with editor
plugins to have autocompletion inside the template.
@@ -127,7 +127,7 @@ structured than a template language. Note that the tooling is quite impressive:
there is a syntax highlighter for jsx here on github!
By comparison, here is the equivalent Owl component, written with the
[`xml`](tags.md#xml-tag) tag helper:
[`xml`](reference/tags.md#xml-tag) tag helper:
```js
class Clock extends Component {
@@ -251,7 +251,7 @@ keeps track of who get data, and retrigger a render when it was changed.
Owl store is a little bit like a mix of redux and vuex: it has actions (but not
mutations), and like VueX, it keeps track of the state changes. However, it does
not notify a component when the state changes. Instead, components need to connect
to the store like in redux, with the `useStore` hook (see the [store documentation](store.md#connecting-a-component)).
to the store like in redux, with the `useStore` hook (see the [store documentation](reference/store.md#connecting-a-component)).
```javascript
const actions = {
@@ -314,7 +314,7 @@ This work is based on the new ideas introduced by React hooks.
From the way React and Vue introduce their hooks, it may look like hooks are not
compatible with class components. However, this is not the case, as shown by
Owl [hooks](hooks.md). They are inspired by both React and Vue. For example,
Owl [hooks](reference/hooks.md). They are inspired by both React and Vue. For example,
the `useState` hook is named after React, but its API is closer to the `reactive`
Vue hook.
+1 -1
View File
@@ -89,7 +89,7 @@ class ClickCounter extends owl.Component {
async function start() {
const templates = await owl.utils.loadFile("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
qweb: new owl.QWeb({ templates })
};
const counter = new ClickCounter(env);
const target = document.getElementById("main");
+20 -13
View File
@@ -50,19 +50,18 @@ Note that for convenience, the `useState` hook is also exported at the root of t
## Reference
- [Animations](animations.md)
- [Component](component.md)
- [Context](context.md)
- [Event Bus](event_bus.md)
- [Hooks](hooks.md)
- [Misc](misc.md)
- [Observer](observer.md)
- [QWeb](qweb.md)
- [Router](router.md)
- [Store](store.md)
- [Tags](tags.md)
- [Utils](utils.md)
- [Virtual DOM](vdom.md)
- [Animations](reference/animations.md)
- [Component](reference/component.md)
- [Context](reference/context.md)
- [Event Bus](reference/event_bus.md)
- [Hooks](reference/hooks.md)
- [Misc](reference/misc.md)
- [Observer](reference/observer.md)
- [QWeb](reference/qweb.md)
- [Router](reference/router.md)
- [Store](reference/store.md)
- [Tags](reference/tags.md)
- [Utils](reference/utils.md)
## Learning Resources
@@ -73,3 +72,11 @@ Note that for convenience, the `useState` hook is also exported at the root of t
- [Comparison with React/Vue](comparison.md)
- [Tooling](tooling.md)
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
## Architecture
This section explains in more detail the inner workings of Owl. It is more
useful for people working on Owl code.
- [Virtual DOM](architecture/vdom.md)
- [Rendering](architecture/rendering.md)
+69 -29
View File
@@ -370,8 +370,8 @@ will slightly slow down the component.
#### `willUpdateProps(nextProps)`
The willUpdateProps is an asynchronous hook, called just before new props
are set. This is useful if the component needs some asynchronous task
performed, depending on the props (for example, assuming that the props are
are set. This is useful if the component needs to perform an asynchronous task,
depending on the props (for example, assuming that the props are
some record Id, fetching the record data).
```javascript
@@ -386,7 +386,7 @@ and performs a similar job).
#### `willPatch()`
The willPatch hook is called just before the DOM patching process starts.
It is not called on the initial render. This is useful to read some
It is not called on the initial render. This is useful to read
information from the DOM. For example, the current position of the
scrollbar.
@@ -413,7 +413,7 @@ careful at avoiding endless cycles.
#### `willUnmount()`
willUnmount is a hook that is called each time just before a component is unmounted from
the DOM. This is a good place to remove some listeners, for example.
the DOM. This is a good place to remove listeners, for example.
```javascript
mounted() {
@@ -441,7 +441,7 @@ of an Owl application has to be created manually:
```js
class App extends owl.Component { ... }
const qweb = new owl.QWeb(TEMPLATES);
const qweb = new owl.QWeb({ templates: TEMPLATES });
const env = { qweb: qweb };
const app = new App(env);
app.mount(document.body);
@@ -539,7 +539,7 @@ constructor. This will be assigned to the `props` variable, which can be accesse
on the component (and also, in the template). Whenever the state is updated, then
the sub component will also be updated automatically.
Note that there are some restrictions on prop names: `class`, `style` and any
Note that there are restrictions on valid prop names: `class`, `style` and any
string which starts with `t-` are not allowed.
It is not common, but sometimes we need a dynamic component name and/or dynamic props. In this case,
@@ -598,7 +598,7 @@ class App extends Component<any, any, any> {
In this example, the component `App` selects dynamically the concrete sub
component class.
**CSS and style:** there is some specific support to allow the parent to declare
**CSS and style:** Owl allows the parent to declare
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
root component element.
@@ -611,7 +611,7 @@ root component element.
Warning: there is a small caveat with dynamic class attributes: since Owl needs
to be able to add/remove proper classes whenever necessary, it needs to be aware
of the possible classes. Otherwise, it will not be able to make the difference
between a valid css class added by the component, or some custom code, and a
between a valid css class added by the component, or other custom code, and a
class that need to be removed. This is why we only support the explicit syntax
with a class object:
@@ -621,7 +621,7 @@ with a class object:
### Event Handling
In a component's template, it is useful to be able to register handlers on some
In a component's template, it is useful to be able to register handlers on DOM
elements to some specific events. This is what makes a template _alive_. There
are four different use cases.
@@ -677,8 +677,7 @@ to event `menu-loaded` will receive the payload in its `someMethod` handler
By convention, we use KebabCase for the name of _business_ events.
The `t-on` directive allows to prebind some arguments. For example,
The `t-on` directive allows to prebind its arguments. For example,
```xml
<button t-on-click="someMethod(expr)">Do something</button>
@@ -695,10 +694,12 @@ One can also directly specify inline statements. For example,
Here, `state` must be defined in the rendering context (typically the component)
as it will be translated to:
```js
button.addEventListener("click", () => { component.state.counter++; });
```
```js
button.addEventListener("click", () => {
component.state.counter++;
});
```
In order to remove the DOM event details from the event handlers (like calls to
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
@@ -802,7 +803,7 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
</div>
```
Like event handling, the `t-model` directive accepts some modifiers:
Like event handling, the `t-model` directive accepts the following modifiers:
| Modifier | Description |
| --------- | -------------------------------------------------------------------- |
@@ -827,24 +828,63 @@ Even though Owl tries to be as declarative as possible, the DOM does not fully
expose its state declaratively in the DOM tree. For example, the scrolling state,
the current user selection, the focused element or the state of an input are not
set as attribute in the DOM tree. This is why we use a virtual dom
algorithm to keep the actual DOM node as much as possible. However, this is
sometimes not enough, and we need to help Owl decide if an element is actually
the same, or is different. The `t-key` directive is used to give an identity to an element.
algorithm to keep the actual DOM node as much as possible.
There are three main use cases:
However, in some situations, this is not enough, and we need to help Owl decide
if an element is actually the same, or is a different element with the same
properties.
- _elements in a list_:
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
and we render them in this template:
```xml
<span t-foreach="todos" t-as="todo" t-key="todo.id">
<t t-esc="todo.text" />
</span>
```
```xml
<p t-foreach="items" t-as="item"><t t-esc="item.text"/></p>
```
- _`t-if`/`t-else`_
The result will be two `<p>` tags with text `a` and `b`. Now, if we swap them,
and rerender the template, Owl needs to know what the intent is:
- _animations_: give a different identity to a component. Ex: thread id with
animations on add/remove message.
- should Owl actually swap the DOM nodes,
- or should it keep the DOM nodes, but with an updated text content?
This might look trivial, but it actually matters. These two possibilities lead
to different results in some cases. For example, if the user selected the text
of the first `p`, swapping them will keep the selection while updating the
text content will not.
There are many other cases where this is important: `input` tags with their
value, css classes and animations, scroll position...
So, the `t-key` directive is used to give an identity to an element. It allows
Owl to understand if different elements of a list are actually different or not.
The above example could be modified by adding an ID: `[{id: 1, text: "a"}, {id: 2, text: "b"}]`.
Then, the template could look like this:
```xml
<p t-foreach="items" t-as="item" t-key="item.id"><t t-esc="item.text"/></p>
```
The `t-key` directive is useful for lists (`t-foreach`). A key should be
a unique number or string (objects will not work: they will be cast to the
`"[object Object]"` string, which is obviously not unique).
Also, the key can be set on a `t` tag or on its children. The following variations
are all equivalent:
```xml
<p t-foreach="items" t-as="item" t-key="item.id">
<t t-esc="item.text"/>
</p>
<t t-foreach="items" t-as="item" t-key="item.id">
<p t-esc="item.text"/>
</t>
<t t-foreach="items" t-as="item">
<p t-key="item.id" t-esc="item.text"/>
</t>
```
### Semantics
@@ -946,7 +986,7 @@ of the props. Here is how it works in Owl:
- `props` key is a static key (so, different from `this.props` in a component instance)
- it is optional: it is ok for a component to not define a `props` key.
- props are validated whenever a component is created/updated
- props are only validated in `dev` mode (see [tooling page](tooling.md#development-mode))
- props are only validated in `dev` mode (see [tooling page](../tooling.md#development-mode))
- if a key does not match the description, an error is thrown
- it validates keys defined in (static) `props`. Additional keys given by the
parent will cause an error.
+1 -1
View File
@@ -29,7 +29,7 @@ context, and add it to the environment:
```js
const deviceContext = new Context({ isMobile: true });
const env = {
qweb: new QWeb(TEMPLATES),
qweb: new QWeb({ templates: TEMPLATES }),
deviceContext
};
```
+3
View File
@@ -181,6 +181,9 @@ class Counter extends owl.Component {
}
```
It is important to remember that `useState` only works with objects or arrays. It
is necessary, since Owl needs to react to a change in state.
### `onMounted`
`onMounted` is not a user hook, but is a building block designed to help make useful
+80 -14
View File
@@ -16,6 +16,7 @@
- [Dynamic Attributes](#dynamic-attributes)
- [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates)
- [Translations](#translations)
- [Debugging](#debugging)
## Overview
@@ -60,6 +61,7 @@ We present here a list of all standard QWeb directives:
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-translation` | [Disabling the translation of a node](#translations) |
| `t-name` | [Defining a template (not really a directive)](#qweb-engine) |
The component system in Owl requires additional directives, to express various
@@ -87,11 +89,14 @@ const qweb = new owl.QWeb();
Its API is quite simple:
- **`constructor(data)`**: constructor. Takes an optional string to add initial
templates (see `addTemplates` for more information on format of the string).
- **`constructor(config)`**: constructor. Takes an optional configuration object
with an optional `templates` string to add initial
templates (see `addTemplates` for more information on format of the string)
and an optional `translateFn` translate function (see the section on
[translations](#translations)).
```js
const qweb = new owl.QWeb(TEMPLATES);
const qweb = new owl.QWeb({ templates: TEMPLATES, translateFn: _t });
```
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
@@ -116,7 +121,7 @@ Its API is quite simple:
```
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
which is a virtual representation of the DOM (see [vdom doc](vdom.md)).
which is a virtual representation of the DOM (see [vdom doc](../architecture/vdom.md)).
```js
const vnode = qweb.render("App", component);
@@ -201,7 +206,7 @@ root nodes.
### Expression Evaluation
QWeb expressions are strings that will be processed at compile time. Each variable in
the javascript expression will be replaced by a lookup in the context (so, the
the javascript expression will be replaced with a lookup in the context (so, the
component). For example, `a + b.c(d)` will be converted into:
```js
@@ -234,14 +239,14 @@ It is useful to explain the various rules that apply on these expressions:
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
`&` or `|`. This is useful to make sure that we still write valid XML.
| Word | will be replaced by |
| ----- | ------------------- |
| `and` | `&&` |
| `or` | `\|\|` |
| `gt` | `>` |
| `gte` | `>=` |
| `lt` | `<` |
| `lte` | `<=` |
| Word | replaced with |
| ----- | ------------- |
| `and` | `&&` |
| `or` | `\|\|` |
| `gt` | `>` |
| `gte` | `>=` |
| `lt` | `<` |
| `lte` | `<=` |
So, one can write this:
@@ -443,7 +448,7 @@ is equivalent to the previous example.
or an object (the current item will be the current key).
In addition to the name passed via t-as, `t-foreach` provides a few other
variables for various data points (note: `$as` will be replaced by the name
variables for various data points (note: `$as` will be replaced with the name
passed to `t-as`):
- `$as_value`: the current iteration value, identical to `$as` for lists and
@@ -474,6 +479,20 @@ into the global context.
<!-- new_variable undefined -->
```
Owl QWeb is used as the template engine for components. Components are frequently
updated, and reuse as much of the previous DOM as possible. Loops offer a specific
problem for this usecase: how does the template engine know if two rows have
been swapped, or if the content of these rows was changed? To help Owl with that,
there is an additional directive: [`t-key`](component.md#t-key-directive).
```xml
<p t-foreach="state.things" t-as="thing" t-key="thing.id">
<t t-esc="thing.content"/>
</p>
```
If there is no `t-key` directive, Owl will use the index as a default key.
### Rendering Sub Templates
QWeb templates can be used for top level rendering, but they can also be used
@@ -522,6 +541,53 @@ will result in :
</div>
```
### Translations
If properly setup, Owl QWeb engine can translate all rendered templates. To do
so, it needs a translate function, which takes a string and returns a string.
For example:
```js
const translations = {
hello: "bonjour",
yes: "oui",
no: "non"
};
const translateFn = str => translations[str] || str;
const qweb = new QWeb({ translateFn });
```
Once setup, all rendered templates will be translated using `translateFn`:
- each text node will be replaced with its translation,
- each of the following attribute values will be translated as well: `title`,
`placeholder`, `label` and `alt`,
- translating text nodes can be disabled with the special attribute `t-translation`,
if its value is `off`.
So, with the above `translateFn`, the following templates:
```xml
<div>hello</div>
<div t-translation="off">hello</div>
<div>Are you sure?</div>
<input placeholder="hello" other="yes"/>
```
will be rendered as:
```xml
<div>bonjour</div>
<div>hello</div>
<div>Are you sure?</div>
<input placeholder="bonjour" other="yes"/>
```
Note that the translation is done during the compilation of the template, not
when it is rendered.
### Debugging
The javascript QWeb implementation provides two useful debugging directives:
+4
View File
@@ -262,6 +262,10 @@ will only be rerendered whenever this part of the state changes. Otherwise, it
will perform a strict equality check and will update the component every time this
check fails.
Also, it may not be obvious, but it is crucial to remember that the selector
function should return an object or an array. The reason is that it needs to be
observed, otherwise the component would not be able to react to changes.
### `useDispatch`
The `useDispatch` hook is useful when a component needs to be able to dispatch
+10 -3
View File
@@ -1,8 +1,15 @@
# 🦉 Tags 🦉
## Content
- [Overview](#overview)
- [`xml` tag](#xml-tag)
## Overview
Tags are very small helpers to make it easy to write inline templates. There is
only one currently available tag: `xml`, but we plan to add other tags later,
such as a `css` tag, which will be used to write single file components.
such as a `css` tag, which will be used to write [single file components](../tooling.md#single-file-component).
## XML tag
@@ -31,8 +38,8 @@ With tags, this process is slightly simplified. The name is uniquely generated,
and the template is automatically registered:
```js
import { Component } from 'owl'
import { xml } from 'owl/tags'
const { Component } = owl;
const { xml } = owl.tags;
class MyComponent extends Component {
static template = xml`
+2 -2
View File
@@ -20,7 +20,7 @@ argument, it executes it as soon as the DOM ready (or directly).
```js
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 });
app.mount(document.body);
});
@@ -62,7 +62,7 @@ initial usecase for this function is to load a template file. For example:
```js
async function makeEnv() {
const templates = await owl.utils.loadFile("templates.xml");
const qweb = new owl.QWeb(templates);
const qweb = new owl.QWeb({ templates });
return { qweb };
}
```
+18 -4
View File
@@ -60,12 +60,21 @@ useful to compare various performance metrics on some tasks.
## Single File Component
If you want to have `xml` syntax highlighting while using the `xml` helper which
helps you define inline templates, there is a VS Code addon `Comment tagged template`
which, if installed, does exactly that. To enable it, you need to add a comment,
like this:
It is very useful to group code by feature instead of by type of file. It makes
it easier to scale application to larger size.
To do so, Owl currently has a small helper that makes it easy to define a
template inside a javascript (or typescript) file: the [`xml`](reference/tags.md#xml-tag)
helper. With this, a template is automatically registered to [QWeb](reference/qweb.md).
This means that the template and the javascript code can be defined in the same
file. It is not currently possible to add css to the same file, but Owl may
get a `css` tag helper later.
```js
const { Component } = owl;
const { xml } = owl.tags;
// -----------------------------------------------------------------------------
// TEMPLATE
// -----------------------------------------------------------------------------
@@ -85,3 +94,8 @@ class MyComponent extends Component {
// rest of component...
}
```
Note that the above example has an inline xml comment, just after the `xml` call.
This is useful for some editor plugins, such as the VS Code addon
`Comment tagged template`, which, if installed, add syntax highlighting to the
content of the template string.
+5 -2
View File
@@ -1,8 +1,11 @@
{
"name": "owl-framework",
"version": "0.24.0",
"version": "0.24.1",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"engines": {
"node": ">=10.15.3"
},
"scripts": {
"build:js": "tsc --target esnext --module es6 --outDir dist/owl",
"build:bundle": "rollup -c",
@@ -41,7 +44,7 @@
"sass": "^1.16.1",
"source-map-support": "^0.5.10",
"ts-jest": "^23.10.5",
"typescript": "^3.2.2",
"typescript": "^3.6.4",
"uglify-es": "^3.3.9"
},
"jest": {
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉
- Current version: 0.24.0
- Current version: 0.24.1
- Status: mostly stable
This roadmap is only an attempt at predicting Owl's future. Everything may
+24 -48
View File
@@ -134,6 +134,9 @@ export class Component<T extends Env, Props extends {}> {
// Con: this is not really safe
// Pro: but creating component (by a template) is always unsafe anyway
this.props = <Props>props || <Props>{};
if (QWeb.dev) {
QWeb.utils.validateProps(this.constructor, this.props);
}
let id: number = nextId++;
let p: Component<T, any> | null = null;
if (parent instanceof Component) {
@@ -142,11 +145,6 @@ export class Component<T extends Env, Props extends {}> {
parent.__owl__.children[id] = this;
} else {
this.env = parent;
if (QWeb.dev) {
// we only validate props for root widgets here. "Regular" widget
// props are validated by the t-component directive
QWeb.utils.validateProps(this.constructor, this.props);
}
this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) {
this.render(true);
@@ -262,8 +260,11 @@ export class Component<T extends Env, Props extends {}> {
/**
* catchError is a method called whenever some error happens in the rendering or
* lifecycle hooks of a child.
*
* It needs to be implemented by a component that is designed to handle the
* error properly.
*/
catchError(error: Error): void {}
catchError?(error?: Error): void;
//--------------------------------------------------------------------------
// Public
@@ -295,8 +296,12 @@ export class Component<T extends Env, Props extends {}> {
} else {
this.__render(fiber);
}
return new Promise(resolve => {
scheduler.addFiber(fiber, () => {
return new Promise((resolve, reject) => {
scheduler.addFiber(fiber, err => {
if (err) {
reject(err);
return;
}
if (!__owl__.isDestroyed) {
this.__patch(fiber.vnode);
target.appendChild(this.el!);
@@ -339,8 +344,12 @@ export class Component<T extends Env, Props extends {}> {
}
const fiber = new Fiber(null, this, this.props, undefined, undefined, force);
this.__render(fiber);
return new Promise(resolve => {
scheduler.addFiber(fiber.root, () => {
return new Promise((resolve, reject) => {
scheduler.addFiber(fiber.root, err => {
if (err) {
reject(err);
return;
}
if (__owl__.isMounted && fiber === fiber.root) {
fiber.patchComponents();
}
@@ -494,6 +503,9 @@ export class Component<T extends Env, Props extends {}> {
if (defaultProps) {
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
}
if (QWeb.dev) {
QWeb.utils.validateProps(this.constructor, nextProps);
}
await Promise.all([
this.willUpdateProps(nextProps),
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps)
@@ -561,7 +573,7 @@ export class Component<T extends Env, Props extends {}> {
try {
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
} catch (e) {
errorHandler(e, fiber);
fiber.handleError(e);
fiber.vnode = h("div"); // -> we render this div at the end
return Promise.resolve();
}
@@ -586,7 +598,7 @@ export class Component<T extends Env, Props extends {}> {
});
} catch (e) {
vnode = __owl__.vnode || h("div");
errorHandler(e, fiber);
fiber.handleError(e);
}
fiber.vnode = vnode;
if (__owl__.observer) {
@@ -650,39 +662,3 @@ export class Component<T extends Env, Props extends {}> {
return <Props>props;
}
}
//------------------------------------------------------------------------------
// Error handling
//------------------------------------------------------------------------------
Fiber.prototype.handleError = function(error) {
errorHandler(error, this);
};
/**
* This is the global error handler for errors occurring in Owl main lifecycle
* methods. Caught errors are triggered on the QWeb instance, and are
* potentially given to some parent component which implements `catchError`.
*
* If there are no such component, we destroy everything. This is better than
* being in a corrupted state.
*/
function errorHandler(error: Error, fiber: Fiber) {
let canCatch = false;
let component = fiber.component;
let qweb = component.env.qweb;
let root = component;
while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
root = component;
component = component.__owl__.parent!;
}
console.error(error);
qweb.trigger("error", error);
if (canCatch) {
setTimeout(() => {
component.catchError(error);
});
} else {
root.destroy();
}
}
+2 -16
View File
@@ -223,30 +223,19 @@ QWeb.addDirective({
}
}
let key = node.getAttribute("t-key");
if (key) {
key = ctx.formatExpression(key);
}
// computing the props string representing the props object
let propStr = Object.keys(props)
.map(k => k + ":" + props[k])
.join(",");
let defID = ctx.generateID();
let componentID = ctx.generateID();
let keyID = key && ctx.generateID();
if (key) {
// we bind a variable to the key (could be a complex expression, so we
// want to evaluate it only once)
ctx.addLine(`let key${keyID} = 'key' + ${key};`);
}
let locationExpr = `\`__${ctx.generateID()}__`;
for (let i = 0; i < ctx.loopNumber - 1; i++) {
locationExpr += `\${i${i + 1}}__`;
}
if (key || ctx.currentKey) {
const k = key ? `key${keyID}` : ctx.currentKey;
if (ctx.lastNodeKey || ctx.currentKey) {
const k = ctx.lastNodeKey || ctx.currentKey;
ctx.addLine(`let templateId${componentID} = ${locationExpr}\` + ${k};`);
} else {
locationExpr += ctx.loopNumber ? `\${i${ctx.loopNumber}}__\`` : "`";
@@ -412,9 +401,6 @@ QWeb.addDirective({
// disable the patch queue
patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`;
}
if (QWeb.dev) {
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
}
let styleCode = "";
if (tattStyle) {
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
+35 -11
View File
@@ -53,6 +53,8 @@ export class Fiber {
sibling: Fiber | null = null;
parent: Fiber | null = null;
error?: Error;
constructor(parent: Fiber | null, component: Component<any, any>, props, scope, vars, force) {
this.force = force;
this.scope = scope;
@@ -146,7 +148,6 @@ export class Fiber {
};
this._walk(doWork);
let component: Component<any, any> = this.component;
this.shouldPatch = false;
const patchLen = patchQueue.length;
try {
for (let i = 0; i < patchLen; i++) {
@@ -159,14 +160,10 @@ export class Fiber {
} catch (e) {
console.error(e);
}
try {
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode);
}
} catch (e) {
this.handleError(e);
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode);
}
try {
for (let i = patchLen - 1; i >= 0; i--) {
@@ -179,7 +176,6 @@ export class Fiber {
} catch (e) {
console.error(e);
}
this.shouldPatch = true;
}
/**
@@ -195,5 +191,33 @@ export class Fiber {
});
}
handleError(e: Error) {}
/**
* This is the global error handler for errors occurring in Owl main lifecycle
* methods. Caught errors are triggered on the QWeb instance, and are
* potentially given to some parent component which implements `catchError`.
*
* If there are no such component, we destroy everything. This is better than
* being in a corrupted state.
*/
handleError(error: Error) {
let canCatch = false;
let component = this.component;
let qweb = component.env.qweb;
let root = component;
while (component && !(canCatch = !!component.catchError)) {
root = component;
component = component.__owl__.parent!;
}
qweb.trigger("error", error);
if (canCatch) {
setTimeout(() => {
console.error(error);
component.catchError!(error);
});
} else {
this.root.error = error;
root.destroy();
}
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ QWeb.utils.validateProps = function(Widget, props: Object) {
// optional prop
break;
}
if (!props[propName]) {
if (!(propName in props)) {
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
}
}
+2 -2
View File
@@ -13,7 +13,7 @@ import { Fiber } from "./fiber";
interface Task {
fiber: Fiber;
callback: () => void;
callback: (err?: Error) => void;
}
export class Scheduler {
@@ -45,7 +45,7 @@ export class Scheduler {
return false;
}
if (task.fiber.counter === 0) {
task.callback();
task.callback(task.fiber.error);
return false;
}
return true;
+9 -9
View File
@@ -81,15 +81,15 @@ export function useContextWithCB(ctx: Context, component: Component<any, any>, m
__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();
}
__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;
const renderFn = __owl__.renderFn;
+2 -2
View File
@@ -10,7 +10,7 @@ import { QWeb } from "./qweb/index";
import * as _store from "./store";
import * as _utils from "./utils";
import * as _tags from "./tags";
import {AsyncRoot} from "./misc/async_root";
import { AsyncRoot } from "./misc/async_root";
import * as _hooks from "./hooks";
import * as _context from "./context";
import { Link } from "./router/link";
@@ -27,7 +27,7 @@ export const router = { Router, RouteComponent, Link };
export const Store = _store.Store;
export const utils = _utils;
export const tags = _tags;
export const misc = { AsyncRoot};
export const misc = { AsyncRoot };
export const hooks = Object.assign({}, _hooks, {
useContext: _context.useContext,
useDispatch: _store.useDispatch,
+6 -14
View File
@@ -294,25 +294,17 @@ QWeb.addDirective({
ctx.addToScope(name, `_${keysID}[${loopVar}]`);
ctx.addToScope(name + "_value", `_${valuesID}[${loopVar}]`);
const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
if (!shouldWarn && node.tagName === "t") {
if (node.hasAttribute("t-component") && !node.hasAttribute("t-key")) {
shouldWarn = true;
}
if (
!shouldWarn &&
node.children.length === 1 &&
node.children[0].tagName !== "t" &&
!node.children[0].hasAttribute("t-key")
) {
shouldWarn = true;
}
}
let shouldWarn =
!nodeCopy.hasAttribute("t-key") &&
node.children.length === 1 &&
node.children[0].tagName !== "t" &&
!node.children[0].hasAttribute("t-key");
if (shouldWarn) {
console.warn(
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
);
}
nodeCopy.removeAttribute("t-foreach");
qweb._compileNode(nodeCopy, ctx);
ctx.dedent();
+16 -1
View File
@@ -58,7 +58,9 @@ QWeb.addDirective({
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
} else {
const handlerKey = `handler${ctx.generateID()}`;
ctx.addLine(`const ${handlerKey} = context['${handlerName}'] && context['${handlerName}'].bind(${params});`);
ctx.addLine(
`const ${handlerKey} = context['${handlerName}'] && context['${handlerName}'].bind(${params});`
);
handler += `if (${handlerKey}) { ${handlerKey}(e); } else { context.${value}; }`;
handler += `}`;
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
@@ -259,3 +261,16 @@ QWeb.addDirective({
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
}
});
//------------------------------------------------------------------------------
// t-key
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "key",
priority: 45,
atNodeEncounter({ ctx, value }) {
let id = ctx.generateID();
ctx.addLine(`const nodeKey${id} = ${ctx.formatExpression(value)};`);
ctx.lastNodeKey = `nodeKey${id}`;
}
});
+27 -13
View File
@@ -57,12 +57,19 @@ export interface Directive {
finalize?(info: CompilationInfo): void;
}
interface QWebConfig {
templates?: string;
translateFn?(text: string): string;
}
//------------------------------------------------------------------------------
// Const/global stuff/helpers
//------------------------------------------------------------------------------
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
@@ -134,6 +141,7 @@ function parseXML(xml: string): Document {
//------------------------------------------------------------------------------
// QWeb rendering engine
//------------------------------------------------------------------------------
export class QWeb extends EventBus {
templates: { [name: string]: Template };
static utils = UTILS;
@@ -143,7 +151,7 @@ export class QWeb extends EventBus {
name: 1,
att: 1,
attf: 1,
key: 1
translation: 1
};
static DIRECTIVES: Directive[] = [];
@@ -166,12 +174,16 @@ export class QWeb extends EventBus {
recursiveFns = {};
isUpdating: boolean = false;
translateFn?: QWebConfig["translateFn"];
constructor(data?: string) {
constructor(config: QWebConfig = {}) {
super();
this.templates = Object.create(QWeb.TEMPLATES);
if (data) {
this.addTemplates(data);
if (config.templates) {
this.addTemplates(config.templates);
}
if (config.translateFn) {
this.translateFn = config.translateFn;
}
}
@@ -418,6 +430,11 @@ export class QWeb extends EventBus {
}
text = text.replace(whitespaceRE, " ");
}
if (this.translateFn) {
if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
text = this.translateFn(text);
}
}
if (ctx.parentNode) {
if (node.nodeType === 3) {
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
@@ -617,7 +634,11 @@ export class QWeb extends EventBus {
for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name;
const value = attributes[i].textContent!;
let value = attributes[i].textContent!;
if (this.translateFn && TRANSLATABLE_ATTRS.includes(name)) {
value = this.translateFn(value);
}
// regular attributes
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
@@ -703,14 +724,7 @@ export class QWeb extends EventBus {
}
}
let nodeID = ctx.generateID();
let nodeKey: any = (<Element>node).getAttribute("t-key");
if (nodeKey) {
ctx.addLine(`const nodeKey${nodeID} = ${ctx.formatExpression(nodeKey)}`);
nodeKey = `nodeKey${nodeID}`;
ctx.lastNodeKey = nodeKey;
} else {
nodeKey = nodeID;
}
let nodeKey = ctx.lastNodeKey || nodeID;
const parts = [`key:${nodeKey}`];
if (attrs.length + tattrs.length > 0) {
parts.push(`attrs:{${attrs.join(",")}}`);
+1 -1
View File
@@ -148,7 +148,7 @@ export class Router {
//--------------------------------------------------------------------------
private setUrlFromPath(path: string) {
const separator = this.mode === "hash" ? "/" : "";
const separator = this.mode === "hash" ? location.pathname : "";
const url = location.origin + separator + path;
if (url !== window.location.href) {
window.history.pushState({}, path, url);
@@ -305,33 +305,33 @@ exports[`composition sub components with some state rendered in a loop 1`] = `
context.number_index = i1;
context.number = _3[i1];
context.number_value = _4[i1];
const nodeKey5 = context['number'];
//COMPONENT
let key7 = 'key' + context['number'];
let templateId6 = \`__8__\` + key7;
let w6 = templateId6 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId6]] : false;
let props6 = {};
if (w6 && w6.__owl__.currentFiber && !w6.__owl__.vnode) {
w6.destroy();
w6 = false;
let templateId7 = \`__8__\` + nodeKey5;
let w7 = templateId7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId7]] : false;
let props7 = {};
if (w7 && w7.__owl__.currentFiber && !w7.__owl__.vnode) {
w7.destroy();
w7 = false;
}
if (w6) {
w6.__updateProps(props6, extra.fiber, undefined, undefined, sibling);
let pvnode = w6.__owl__.pvnode;
if (w7) {
w7.__updateProps(props7, extra.fiber, undefined, undefined, sibling);
let pvnode = w7.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey6 = \`ChildWidget\`;
let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6]|| context['ChildWidget'];
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
w6 = new W6(parent, props6);
parent.__owl__.cmap[templateId6] = w6.__owl__.id;
let def5 = w6.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId6, hook: {insert(vn) { let nvn=w6.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w6.destroy();}}});
const fiber = w6.__owl__.currentFiber;
def5.then(function () {if (w6.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let componentKey7 = \`ChildWidget\`;
let W7 = context.constructor.components[componentKey7] || QWeb.components[componentKey7]|| context['ChildWidget'];
if (!W7) {throw new Error('Cannot find the definition of component \\"' + componentKey7 + '\\"')}
w7 = new W7(parent, props7);
parent.__owl__.cmap[templateId7] = w7.__owl__.id;
let def6 = w7.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId7, hook: {insert(vn) { let nvn=w7.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});
const fiber = w7.__owl__.currentFiber;
def6.then(function () {if (w7.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w6.__owl__.pvnode = pvnode;
w7.__owl__.pvnode = pvnode;
}
sibling = w6.__owl__.currentFiber || sibling;
sibling = w7.__owl__.currentFiber || sibling;
}
return vn1;
}"
@@ -993,33 +993,33 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
const nodeKey2 = 'somestring';
//COMPONENT
let key4 = 'key' + 'somestring';
let templateId3 = \`__5__\` + key4;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let props3 = {flag:context['state'].flag};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
let templateId4 = \`__5__\` + nodeKey2;
let w4 = templateId4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId4]] : false;
let props4 = {flag:context['state'].flag};
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
w4.destroy();
w4 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode;
if (w4) {
w4.__updateProps(props4, extra.fiber, undefined, undefined, sibling);
let pvnode = w4.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey3 = \`child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let componentKey4 = \`child\`;
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['child'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[templateId4] = w4.__owl__.id;
let def3 = w4.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId4, hook: {insert(vn) { let nvn=w4.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});
const fiber = w4.__owl__.currentFiber;
def3.then(function () {if (w4.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
w4.__owl__.pvnode = pvnode;
}
sibling = w3.__owl__.currentFiber || sibling;
sibling = w4.__owl__.currentFiber || sibling;
return vn1;
}"
`;
@@ -1050,34 +1050,34 @@ exports[`random stuff/miscellaneous t-on with handler bound to dynamic argument
context.item_index = i1;
context.item = _3[i1];
context.item_value = _4[i1];
const nodeKey5 = context['item'];
//COMPONENT
let key7 = 'key' + context['item'];
let templateId6 = \`__8__\` + key7;
let templateId7 = \`__8__\` + nodeKey5;
let arg9 = context['item'];
let w6 = templateId6 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId6]] : false;
let props6 = {};
if (w6 && w6.__owl__.currentFiber && !w6.__owl__.vnode) {
w6.destroy();
w6 = false;
let w7 = templateId7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId7]] : false;
let props7 = {};
if (w7 && w7.__owl__.currentFiber && !w7.__owl__.vnode) {
w7.destroy();
w7 = false;
}
if (w6) {
w6.__updateProps(props6, extra.fiber, undefined, undefined, sibling);
let pvnode = w6.__owl__.pvnode;
if (w7) {
w7.__updateProps(props7, extra.fiber, undefined, undefined, sibling);
let pvnode = w7.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey6 = \`Child\`;
let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6]|| context['Child'];
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
w6 = new W6(parent, props6);
parent.__owl__.cmap[templateId6] = w6.__owl__.id;
let def5 = w6.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId6, hook: {insert(vn) { let nvn=w6.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w6.destroy();}}});
const fiber = w6.__owl__.currentFiber;
def5.then(function () {if (w6.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['onEv'];if (fn) { fn.call(owner, arg9, e); } else { owner.onEv; }});}};});
let componentKey7 = \`Child\`;
let W7 = context.constructor.components[componentKey7] || QWeb.components[componentKey7]|| context['Child'];
if (!W7) {throw new Error('Cannot find the definition of component \\"' + componentKey7 + '\\"')}
w7 = new W7(parent, props7);
parent.__owl__.cmap[templateId7] = w7.__owl__.id;
let def6 = w7.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId7, hook: {insert(vn) { let nvn=w7.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});
const fiber = w7.__owl__.currentFiber;
def6.then(function () {if (w7.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['onEv'];if (fn) { fn.call(owner, arg9, e); } else { owner.onEv; }});}};});
c1.push(pvnode);
w6.__owl__.pvnode = pvnode;
w7.__owl__.pvnode = pvnode;
}
sibling = w6.__owl__.currentFiber || sibling;
sibling = w7.__owl__.currentFiber || sibling;
}
return vn1;
}"
@@ -1541,37 +1541,37 @@ exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
scope.user = context.user;
context.user_value = _5[i1];
scope.user_value = context.user_value;
const nodeKey6 = context['user'].id
let c6 = [], p6 = {key:nodeKey6};
var vn6 = h('li', p6, c6);
c2.push(vn6);
const nodeKey6 = context['user'].id;
let c7 = [], p7 = {key:nodeKey6};
var vn7 = h('li', p7, c7);
c2.push(vn7);
//COMPONENT
let templateId8 = \`__9__\` + nodeKey6;
let w8 = templateId8 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId8]] : false;
let props8 = {to:'/user/'+context['user'].id};
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
w8.destroy();
w8 = false;
let templateId9 = \`__10__\` + nodeKey6;
let w9 = templateId9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId9]] : false;
let props9 = {to:'/user/'+context['user'].id};
if (w9 && w9.__owl__.currentFiber && !w9.__owl__.vnode) {
w9.destroy();
w9 = false;
}
if (w8) {
w8.__updateProps(props8, extra.fiber, Object.assign({}, scope), undefined, sibling);
let pvnode = w8.__owl__.pvnode;
c6.push(pvnode);
if (w9) {
w9.__updateProps(props9, extra.fiber, Object.assign({}, scope), undefined, sibling);
let pvnode = w9.__owl__.pvnode;
c7.push(pvnode);
} else {
let componentKey8 = \`Link\`;
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| context['Link'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(parent, props8);
parent.__owl__.cmap[templateId8] = w8.__owl__.id;
w8.__owl__.slotId = 1;
let def7 = w8.__prepare(extra.fiber, Object.assign({}, scope), undefined, sibling);
let pvnode = h('dummy', {key: templateId8, hook: {insert(vn) { let nvn=w8.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w8.destroy();}}});
const fiber = w8.__owl__.currentFiber;
def7.then(function () {if (w8.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c6.push(pvnode);
w8.__owl__.pvnode = pvnode;
let componentKey9 = \`Link\`;
let W9 = context.constructor.components[componentKey9] || QWeb.components[componentKey9]|| context['Link'];
if (!W9) {throw new Error('Cannot find the definition of component \\"' + componentKey9 + '\\"')}
w9 = new W9(parent, props9);
parent.__owl__.cmap[templateId9] = w9.__owl__.id;
w9.__owl__.slotId = 1;
let def8 = w9.__prepare(extra.fiber, Object.assign({}, scope), undefined, sibling);
let pvnode = h('dummy', {key: templateId9, hook: {insert(vn) { let nvn=w9.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w9.destroy();}}});
const fiber = w9.__owl__.currentFiber;
def8.then(function () {if (w9.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c7.push(pvnode);
w9.__owl__.pvnode = pvnode;
}
sibling = w8.__owl__.currentFiber || sibling;
sibling = w9.__owl__.currentFiber || sibling;
}
return vn1;
}"
@@ -1582,12 +1582,12 @@ exports[`t-slot directive slots are rendered with proper context, part 2 3`] = `
) {
let sibling = null;
var h = this.h;
let c6 = extra.parentNode;
let c7 = extra.parentNode;
Object.assign(context, extra.fiber.scope);
c6.push({text: \`User \`});
var _11 = context['user'].name;
if (_11 || _11 === 0) {
c6.push({text: _11});
c7.push({text: \`User \`});
var _12 = context['user'].name;
if (_12 || _12 === 0) {
c7.push({text: _12});
}
}"
`;
@@ -1644,38 +1644,38 @@ exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
scope.user = context.user;
context.user_value = _5[i1];
scope.user_value = context.user_value;
const nodeKey6 = context['user'].id
let c6 = [], p6 = {key:nodeKey6};
var vn6 = h('li', p6, c6);
c2.push(vn6);
var _7 = 'User '+context['user'].name;
const nodeKey6 = context['user'].id;
let c7 = [], p7 = {key:nodeKey6};
var vn7 = h('li', p7, c7);
c2.push(vn7);
var _8 = 'User '+context['user'].name;
//COMPONENT
let templateId9 = \`__10__\` + nodeKey6;
let w9 = templateId9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId9]] : false;
let props9 = {to:'/user/'+context['user'].id};
if (w9 && w9.__owl__.currentFiber && !w9.__owl__.vnode) {
w9.destroy();
w9 = false;
let templateId10 = \`__11__\` + nodeKey6;
let w10 = templateId10 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId10]] : false;
let props10 = {to:'/user/'+context['user'].id};
if (w10 && w10.__owl__.currentFiber && !w10.__owl__.vnode) {
w10.destroy();
w10 = false;
}
if (w9) {
w9.__updateProps(props9, extra.fiber, Object.assign({}, scope), {_7}, sibling);
let pvnode = w9.__owl__.pvnode;
c6.push(pvnode);
if (w10) {
w10.__updateProps(props10, extra.fiber, Object.assign({}, scope), {_8}, sibling);
let pvnode = w10.__owl__.pvnode;
c7.push(pvnode);
} else {
let componentKey9 = \`Link\`;
let W9 = context.constructor.components[componentKey9] || QWeb.components[componentKey9]|| context['Link'];
if (!W9) {throw new Error('Cannot find the definition of component \\"' + componentKey9 + '\\"')}
w9 = new W9(parent, props9);
parent.__owl__.cmap[templateId9] = w9.__owl__.id;
w9.__owl__.slotId = 1;
let def8 = w9.__prepare(extra.fiber, Object.assign({}, scope), {_7}, sibling);
let pvnode = h('dummy', {key: templateId9, hook: {insert(vn) { let nvn=w9.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w9.destroy();}}});
const fiber = w9.__owl__.currentFiber;
def8.then(function () {if (w9.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c6.push(pvnode);
w9.__owl__.pvnode = pvnode;
let componentKey10 = \`Link\`;
let W10 = context.constructor.components[componentKey10] || QWeb.components[componentKey10]|| context['Link'];
if (!W10) {throw new Error('Cannot find the definition of component \\"' + componentKey10 + '\\"')}
w10 = new W10(parent, props10);
parent.__owl__.cmap[templateId10] = w10.__owl__.id;
w10.__owl__.slotId = 1;
let def9 = w10.__prepare(extra.fiber, Object.assign({}, scope), {_8}, sibling);
let pvnode = h('dummy', {key: templateId10, hook: {insert(vn) { let nvn=w10.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w10.destroy();}}});
const fiber = w10.__owl__.currentFiber;
def9.then(function () {if (w10.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c7.push(pvnode);
w10.__owl__.pvnode = pvnode;
}
sibling = w9.__owl__.currentFiber || sibling;
sibling = w10.__owl__.currentFiber || sibling;
}
return vn1;
}"
@@ -1686,11 +1686,11 @@ exports[`t-slot directive slots are rendered with proper context, part 3 3`] = `
) {
let sibling = null;
var h = this.h;
let c6 = extra.parentNode;
let _7 = extra.fiber.vars._7
let c7 = extra.parentNode;
let _8 = extra.fiber.vars._8
Object.assign(context, extra.fiber.scope);
if (_7 || _7 === 0) {
c6.push({text: _7});
if (_8 || _8 === 0) {
c7.push({text: _8});
}
}"
`;
@@ -20,7 +20,6 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
w3 = false;
}
if (w3) {
utils.validateProps(w3.constructor, props3)
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode;
c1.push(pvnode);
+184 -31
View File
@@ -1090,12 +1090,15 @@ describe("composition", () => {
static components = { SomeWidget: Widget };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(console.error).toBeCalledTimes(1);
expect((<any>console.error).mock.calls[0][0].message).toMatch(
'Cannot find the definition of component "SomeMispelledWidget"'
);
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe('Cannot find the definition of component "SomeMispelledWidget"');
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
@@ -2142,7 +2145,7 @@ describe("other directives with t-component", () => {
<Child t-on-ev="handler"/>
</div>`;
static components = { Child };
state = useState({counter: 0});
state = useState({ counter: 0 });
get handler() {
this.state.counter++;
return () => {};
@@ -2152,12 +2155,12 @@ describe("other directives with t-component", () => {
await parent.mount(fixture);
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe('<div>0<span></span></div>');
expect(fixture.innerHTML).toBe("<div>0<span></span></div>");
let child = children(parent)[0];
child.trigger("ev");
await nextTick();
expect(fixture.innerHTML).toBe('<div>1<span></span></div>');
expect(fixture.innerHTML).toBe("<div>1<span></span></div>");
});
test("t-on with inline statement", async () => {
@@ -2171,18 +2174,18 @@ describe("other directives with t-component", () => {
<Child t-on-ev="state.counter++"/>
</div>`;
static components = { Child };
state = useState({counter: 0});
state = useState({ counter: 0 });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe('<div>0<span></span></div>');
expect(fixture.innerHTML).toBe("<div>0<span></span></div>");
let child = children(parent)[0];
child.trigger("ev");
await nextTick();
expect(fixture.innerHTML).toBe('<div>1<span></span></div>');
expect(fixture.innerHTML).toBe("<div>1<span></span></div>");
});
test("t-on with no handler (only modifiers)", async () => {
@@ -3433,6 +3436,69 @@ describe("async rendering", () => {
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>2c</p></div>");
});
test("concurrent renderings scenario 9", async () => {
// Here is the global idea of this scenario:
// A
// / \
// B C
// |
// D
// A state is updated, triggering a whole re-rendering
// B is async, and blocked
// C (and D) are rendered
// C state is updated, producing a re-rendering of C and D
// this last re-rendering of C should be correctly re-mapped to the whole
// re-rendering
const def = makeDeferred();
let stateC;
class ComponentD extends Component<any, any> {
static template = xml`<span><t t-esc="props.fromA"/><t t-esc="props.fromC"/></span>`;
}
class ComponentC extends Component<any, any> {
static template = xml`<p><ComponentD fromA="props.fromA" fromC="state.fromC" /></p>`;
static components = { ComponentD };
state = useState({ fromC: "b1" });
constructor(parent, props) {
super(parent, props);
stateC = this.state;
}
}
class ComponentB extends Component<any, any> {
static template = xml`<b><t t-esc="props.fromA"/></b>`;
willUpdateProps() {
return def;
}
}
class ComponentA extends Component<any, any> {
static template = xml`
<div>
<t t-esc="state.fromA"/>
<ComponentB fromA="state.fromA"/>
<ComponentC fromA="state.fromA"/>
</div>`;
static components = { ComponentB, ComponentC };
state = useState({ fromA: "a1" });
}
const component = new ComponentA(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>a1<b>a1</b><p><span>a1b1</span></p></div>");
component.state.fromA = "a2";
await nextTick();
expect(fixture.innerHTML).toBe("<div>a1<b>a1</b><p><span>a1b1</span></p></div>");
stateC.fromC = "b2";
await nextTick();
expect(fixture.innerHTML).toBe("<div>a1<b>a1</b><p><span>a1b1</span></p></div>");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div>a2<b>a2</b><p><span>a2b2</span></p></div>");
});
});
describe("widget and observable state", () => {
@@ -3466,12 +3532,15 @@ describe("widget and observable state", () => {
static components = { Child };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(console.error).toBeCalledTimes(1);
expect((<any>console.error).mock.calls[0][0].message).toMatch(
'Observed state cannot be changed here! (key: "coffee", val: "2")'
);
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe('Observed state cannot be changed here! (key: "coffee", val: "2")');
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
});
@@ -4359,22 +4428,27 @@ describe("component error handling (catchError)", () => {
});
test("no component catching error lead to full app destruction", async () => {
expect.assertions(6);
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorComponent">hey<t t-esc="props.flag and state.this.will.crash"/>
</div>
<div t-name="App">
<ErrorComponent flag="state.flag"/>
</div>
</templates>`);
class ErrorComponent extends Widget {}
class ErrorComponent extends Widget {
static template = xml`<div>hey<t t-esc="props.flag and state.this.will.crash"/></div>`;
}
class App extends Widget {
state = useState({ flag: false });
static template = xml`<div><ErrorComponent flag="state.flag"/></div>`;
static components = { ErrorComponent };
state = useState({ flag: false });
async render() {
try {
await super.render();
} catch (e) {
expect(e.message).toBe("Cannot read property 'this' of undefined");
}
}
}
const app = new App(env);
await app.mount(fixture);
@@ -4385,7 +4459,7 @@ describe("component error handling (catchError)", () => {
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(1);
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(app.__owl__.isDestroyed).toBe(true);
expect(handler).toBeCalledTimes(1);
@@ -4551,6 +4625,8 @@ describe("component error handling (catchError)", () => {
test.skip("can catch an error in the willPatch call", async () => {
// we do not catch error in willPatch anymore
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Widget {
static template = xml`<div><t t-esc="props.message"/></div>`;
willPatch() {
@@ -4572,6 +4648,7 @@ describe("component error handling (catchError)", () => {
class App extends Widget {
static template = xml`
<div>
<span><t t-esc="state.message"/></span>
<ErrorBoundary><ErrorComponent message="state.message" /></ErrorBoundary>
</div>`;
state = useState({ message: "abc" });
@@ -4579,12 +4656,88 @@ describe("component error handling (catchError)", () => {
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div>abc</div></div></div>");
expect(fixture.innerHTML).toBe("<div><span>abc</span><div><div>abc</div></div></div>");
app.state.message = "def";
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(fixture.innerHTML).toBe("<div><span>def</span><div>Error handled</div></div>");
expect(console.error).toHaveBeenCalledTimes(1);
console.error = consoleError;
});
test("a rendering error will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class App extends Component<any, any> {
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
}
const app = new App(env);
let error;
try {
await app.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("a rendering error in a sub component will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
}
class App extends Component<any, any> {
static template = xml`<div><Child/></div>`;
static components = { Child };
}
const app = new App(env);
let error;
try {
await app.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("a rendering error will reject the render promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class App extends Component<any, any> {
static template = xml`<div><t t-if="flag" t-esc="this.will.crash"/></div>`;
flag = false;
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>");
app.flag = true;
let error;
try {
await app.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
});
+73
View File
@@ -322,6 +322,79 @@ describe("props validation", () => {
QWeb.utils.validateProps(TestWidget, { message: null });
}).toThrow();
});
test("missing required boolean prop causes an error", async () => {
class TestWidget extends Widget {
static props = ["p"];
static template = xml`<span><t t-if="props.p">hey</t></span>`;
}
class App extends Widget {
static template = xml`<div><TestWidget/></div>`;
static components = { TestWidget };
}
const w = new App(env, {});
let error;
try {
await w.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Missing props 'p' (component 'TestWidget')");
});
test("props are validated whenever component is updated", async () => {
let error;
class TestWidget extends Component<any, any> {
static props = { p: { type: Number } };
static template = xml`<div><t t-esc="props.p"/></div>`;
async __updateProps() {
try {
await Component.prototype.__updateProps.apply(this, arguments);
} catch(e) {
error = e;
}
}
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="state.p"/></div>`;
static components = { TestWidget };
state: any = useState({ p: 1 });
}
const w = new Parent(env);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
w.state.p = undefined;
await nextTick();
expect(error).toBeDefined();
expect(error.message).toBe("Missing props 'p' (component 'TestWidget')");
});
test("default values are applied before validating props at update", async () => {
class TestWidget extends Component<any, any> {
static props = { p: { type: Number } };
static template = xml`<div><t t-esc="props.p"/></div>`;
static defaultProps = { p: 4 };
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="state.p"/></div>`;
static components = { TestWidget };
state: any = useState({ p: 1 });
}
const w = new Parent(env);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
w.state.p = undefined;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
});
});
describe("default props", () => {
+1 -1
View File
@@ -207,7 +207,7 @@ describe("Context", () => {
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
testContext.state.key = "y";
testContext.state.y = {n: 2};
testContext.state.y = { n: 2 };
delete testContext.state.x;
await nextTick();
+135 -78
View File
@@ -6,36 +6,9 @@
*/
import * as fs from "fs";
const LINK_REGEXP = /\[([^\[]+)\]\(([^\)]+)\)/g;
const HEADING_REGEXP = /\n(#+\s*)(.*)/g;
// files to be checked
function getFiles(): string[] {
const DOCFILES = fs.readdirSync("doc").map(f => `doc/${f}`);
const MAINREADME = "README.md";
DOCFILES.push("roadmap.md", MAINREADME);
return DOCFILES;
}
test("All markdown links work", () => {
let linkNumber = 0;
let invalidLinkNumber = 0;
const files = getFiles();
const data = readDocData(files);
for (let file of data) {
for (let link of file.links) {
// DEBUG: uncomment next line
// console.warn(`Checking "${link.name}" in "${file.name}"`);
linkNumber++;
if (!isLinkValid(link, file, data)) {
console.warn(`Invalid Link: "${link.name}" in "${file.name}"`);
invalidLinkNumber++;
}
}
}
expect(invalidLinkNumber).toBe(0);
expect(linkNumber).toBeGreaterThan(10);
});
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
interface MarkDownLink {
name: string;
@@ -49,38 +22,130 @@ interface MarkDownSection {
interface FileData {
name: string;
path: string[];
fullName: string
links: MarkDownLink[];
sections: MarkDownSection[];
}
function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean {
const LINK_REGEXP = /\[([^\[]+)\]\(([^\)]+)\)/g;
const HEADING_REGEXP = /\n(#+\s*)(.*)/g;
export function addMardownData(fileData): void {
const sep = fileData.path.length > 0 ? '/' : '';
const fullName = fileData.path.join('/') + sep + fileData.name;
const content = fs.readFileSync(fullName, { encoding: "utf8" });
let m;
// get links info
do {
m = LINK_REGEXP.exec(content);
if (m) {
fileData.links.push({ name: m[0], link: m[2] });
}
} while (m);
// get sections info
do {
m = HEADING_REGEXP.exec(content);
if (m) {
fileData.sections.push({ name: m[0], slug: slugify(m[2]) });
}
} while (m);
}
/**
* Returns a list of FileData corresponding to all files that need to be
* validated.
*/
function getFiles(path: string[] = []): FileData[] {
if (path.length === 0) {
const baseFiles: FileData[] = [
{ name: "README.md", path: [], links: [], sections: [], fullName: "README.md" },
{ name: "roadmap.md", path: [], links: [], sections: [], fullName: "roadmap.md" }
];
const rest = getFiles(["doc"]);
const result = baseFiles.concat(rest);
result.forEach(addMardownData);
return result;
}
const files = fs.readdirSync(path.join("/"), { withFileTypes: true }).map(f => {
if (f.isDirectory()) {
return getFiles(path.concat(f.name));
}
const fullName = path.join('/') + (path.length > 0 ? '/' : '') + f.name;
return [
{
name: f.name,
path,
links: [],
sections: [],
fullName
}
];
});
return Array.prototype.concat(...files);
}
const LOCAL_FILES = ['LICENSE'];
export function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean {
if (link.link.startsWith("http")) {
// no check on external links
return true;
}
const parts = link.link.split("#");
const currentParts = current.name.split("/");
const path = currentParts.length > 1 ? currentParts[0] + "/" : "";
const fullName = path + parts[0];
if (parts.length === 1) {
// no # in url
if (parts[0].endsWith(".md")) {
// it is a local md file
if (!files.find(f => f.name === fullName)) {
// Step 1: extract path, name, hash
// path = ['doc', 'architecture]
// name = 'rendering.md'
// hash = 'blabla' (or '' if no hash)
const parts = link.link.split('#');
const hash = parts[1] || '';
let name;
let path;
if (parts[0]) {
let temp = parts[0].split('/');
name = temp[temp.length - 1];
temp.splice(-1);
path = current.path.slice();
for (let elem of temp) {
if (elem === '..') {
path.splice(-1);
} else if (elem !== '.') {
path.push(elem);
}
}
} else {
// there are no file name, so this is a relative link to the current file
name = current.name;
path = current.path;
}
// Step 2: build normalized link file name
const linkFullName = path.join('/') + (path.length > 0 ? '/' : '') + name;
// Step 3: check link name against white list of local files
if (LOCAL_FILES.includes(linkFullName)) {
return true;
}
// Step 4: check if there is a matching file
let target: FileData | undefined = files.find(f => f.fullName === linkFullName);
if (!target) {
return false;
}
}
} else {
const file = parts[0] === "" ? current : files.find(f => f.name === fullName);
if (!file) {
return false;
// Step 5: if necessary, check if there is a corresponding link inside the target
// link name
if (hash) {
if (!target.sections.find(s => s.slug === hash)) {
return false;
}
}
if (!file.sections.find(s => s.slug === parts[1])) {
return false;
}
}
return true;
return true;
}
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
function slugify(str) {
const a = "àáäâãåăæçèéëêǵḧìíïîḿńǹñòóöôœøṕŕßśșțùúüûǘẃẍÿź·_,:;";
@@ -99,33 +164,25 @@ function slugify(str) {
.replace(/-+$/, ""); // Trim - from end of text
}
function readDocData(files: string[]): FileData[] {
const result: FileData[] = [];
for (let file of files) {
const fileData: FileData = {
name: file,
links: [],
sections: []
};
const content = fs.readFileSync(file, { encoding: "utf8" });
let m;
// get links info
do {
m = LINK_REGEXP.exec(content);
if (m) {
fileData.links.push({ name: m[0], link: m[2] });
}
} while (m);
// get sections info
do {
m = HEADING_REGEXP.exec(content);
if (m) {
fileData.sections.push({ name: m[0], slug: slugify(m[2]) });
}
} while (m);
result.push(fileData);
}
return result;
}
//--------------------------------------------------------------------------
// Test
//--------------------------------------------------------------------------
test("All markdown links work", () => {
let linkNumber = 0;
let invalidLinkNumber = 0;
const data = getFiles();
for (let file of data) {
for (let link of file.links) {
linkNumber++;
if (!isLinkValid(link, file, data)) {
console.warn(`Invalid Link: "${link.name}" in "${file.name}"`);
invalidLinkNumber++;
}
}
}
expect(invalidLinkNumber).toBe(0);
expect(linkNumber).toBeGreaterThan(10);
});
+129 -62
View File
@@ -387,13 +387,13 @@ exports[`foreach iterate on items (on a element node) 1`] = `
context.item_index = i1;
context.item = _3[i1];
context.item_value = _4[i1];
const nodeKey5 = context['item']
let c5 = [], p5 = {key:nodeKey5};
var vn5 = h('span', p5, c5);
c1.push(vn5);
var _6 = context['item'];
if (_6 || _6 === 0) {
c5.push({text: _6});
const nodeKey5 = context['item'];
let c6 = [], p6 = {key:nodeKey5};
var vn6 = h('span', p6, c6);
c1.push(vn6);
var _7 = context['item'];
if (_7 || _7 === 0) {
c6.push({text: _7});
}
}
return vn1;
@@ -1643,14 +1643,14 @@ exports[`t-key can use t-key directive on a node 1`] = `
) {
let sibling = null;
var h = this.h;
const nodeKey1 = context['beer'].id
let c1 = [], p1 = {key:nodeKey1};
var vn1 = h('div', p1, c1);
var _2 = context['beer'].name;
if (_2 || _2 === 0) {
c1.push({text: _2});
const nodeKey1 = context['beer'].id;
let c2 = [], p2 = {key:nodeKey1};
var vn2 = h('div', p2, c2);
var _3 = context['beer'].name;
if (_3 || _3 === 0) {
c2.push({text: _3});
}
return vn1;
return vn2;
}"
`;
@@ -1676,13 +1676,13 @@ exports[`t-key t-key directive in a list 1`] = `
context.beer_index = i1;
context.beer = _3[i1];
context.beer_value = _4[i1];
const nodeKey5 = context['beer'].id
let c5 = [], p5 = {key:nodeKey5};
var vn5 = h('li', p5, c5);
c1.push(vn5);
var _6 = context['beer'].name;
if (_6 || _6 === 0) {
c5.push({text: _6});
const nodeKey5 = context['beer'].id;
let c6 = [], p6 = {key:nodeKey5};
var vn6 = h('li', p6, c6);
c1.push(vn6);
var _7 = context['beer'].name;
if (_7 || _7 === 0) {
c6.push({text: _7});
}
}
return vn1;
@@ -1774,16 +1774,16 @@ exports[`t-on can bind handlers with loop variable as argument 1`] = `
context.action_index = i1;
context.action = _3[i1];
context.action_value = _4[i1];
const nodeKey5 = context['action_index']
let c5 = [], p5 = {key:nodeKey5};
var vn5 = h('li', p5, c5);
c1.push(vn5);
let c6 = [], p6 = {key:6,on:{}};
var vn6 = h('a', p6, c6);
c5.push(vn6);
const handler7 = context['activate'] && context['activate'].bind(owner, context['action']);
p6.on['click'] = function (e) {if (handler7) { handler7(e); } else { context.activate(action); }};
c6.push({text: \`link\`});
const nodeKey5 = context['action_index'];
let c6 = [], p6 = {key:nodeKey5};
var vn6 = h('li', p6, c6);
c1.push(vn6);
let c7 = [], p7 = {key:nodeKey5,on:{}};
var vn7 = h('a', p7, c7);
c6.push(vn7);
const handler8 = context['activate'] && context['activate'].bind(owner, context['action']);
p7.on['click'] = function (e) {if (handler8) { handler8(e); } else { context.activate(action); }};
c7.push({text: \`link\`});
}
return vn1;
}"
@@ -2000,17 +2000,17 @@ exports[`t-on t-on with prevent modifier in t-foreach 1`] = `
context.project_index = i1;
context.project = _3[i1];
context.project_value = _4[i1];
var _5 = '#';
const nodeKey6 = context['project']
let c6 = [], p6 = {key:nodeKey6,attrs:{href: _5},on:{}};
var vn6 = h('a', p6, c6);
c1.push(vn6);
const handler7 = context['onEdit'] && context['onEdit'].bind(owner, context['project'].id);
p6.on['click'] = function (e) {e.preventDefault();if (handler7) { handler7(e); } else { context.onEdit(project.id); }};
c6.push({text: \` Edit \`});
var _8 = context['project'].name;
if (_8 || _8 === 0) {
c6.push({text: _8});
const nodeKey5 = context['project'];
var _6 = '#';
let c7 = [], p7 = {key:nodeKey5,attrs:{href: _6},on:{}};
var vn7 = h('a', p7, c7);
c1.push(vn7);
const handler8 = context['onEdit'] && context['onEdit'].bind(owner, context['project'].id);
p7.on['click'] = function (e) {e.preventDefault();if (handler8) { handler8(e); } else { context.onEdit(project.id); }};
c7.push({text: \` Edit \`});
var _9 = context['project'].name;
if (_9 || _9 === 0) {
c7.push({text: _9});
}
}
return vn1;
@@ -2207,22 +2207,22 @@ exports[`t-ref refs in a loop 1`] = `
context.item_index = i1;
context.item = _3[i1];
context.item_value = _4[i1];
const nodeKey5 = context['item']
let c5 = [], p5 = {key:nodeKey5};
var vn5 = h('div', p5, c5);
c1.push(vn5);
const ref6 = (context['item']);
p5.hook = {
const nodeKey5 = context['item'];
let c6 = [], p6 = {key:nodeKey5};
var vn6 = h('div', p6, c6);
c1.push(vn6);
const ref7 = (context['item']);
p6.hook = {
create: (_, n) => {
context.__owl__.refs[ref6] = n.elm;
context.__owl__.refs[ref7] = n.elm;
},
destroy: () => {
delete context.__owl__.refs[ref6];
delete context.__owl__.refs[ref7];
},
};
var _7 = context['item'];
if (_7 || _7 === 0) {
c5.push({text: _7});
var _8 = context['item'];
if (_8 || _8 === 0) {
c6.push({text: _8});
}
}
return vn1;
@@ -2383,16 +2383,16 @@ exports[`t-set t-set should reuse variable if possible 1`] = `
context.elem_index = i1;
context.elem = _4[i1];
context.elem_value = _5[i1];
const nodeKey6 = context['elem_index']
let c6 = [], p6 = {key:nodeKey6};
var vn6 = h('div', p6, c6);
c1.push(vn6);
let c7 = [], p7 = {key:7};
var vn7 = h('span', p7, c7);
c6.push(vn7);
c7.push({text: \`v\`});
const nodeKey6 = context['elem_index'];
let c7 = [], p7 = {key:nodeKey6};
var vn7 = h('div', p7, c7);
c1.push(vn7);
let c8 = [], p8 = {key:nodeKey6};
var vn8 = h('span', p8, c8);
c7.push(vn8);
c8.push({text: \`v\`});
if (_2 || _2 === 0) {
c7.push({text: _2});
c8.push({text: _2});
}
_2 = context['elem']
}
@@ -2415,6 +2415,73 @@ exports[`t-set value priority 1`] = `
}"
`;
exports[`translation support can translate node content 1`] = `
"function anonymous(context,extra
) {
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
c1.push({text: \`mot\`});
return vn1;
}"
`;
exports[`translation support does not translate node content if disabled 1`] = `
"function anonymous(context,extra
) {
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
c2.push({text: \`mot\`});
let c3 = [], p3 = {key:3};
var vn3 = h('span', p3, c3);
c1.push(vn3);
c3.push({text: \`word\`});
return vn1;
}"
`;
exports[`translation support some attributes are translated 1`] = `
"function anonymous(context,extra
) {
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 'mot';
let c3 = [], p3 = {key:3,attrs:{label: _2}};
var vn3 = h('p', p3, c3);
c1.push(vn3);
c3.push({text: \`mot\`});
var _4 = 'mot';
let c5 = [], p5 = {key:5,attrs:{title: _4}};
var vn5 = h('p', p5, c5);
c1.push(vn5);
c5.push({text: \`mot\`});
var _6 = 'mot';
let c7 = [], p7 = {key:7,attrs:{placeholder: _6}};
var vn7 = h('p', p7, c7);
c1.push(vn7);
c7.push({text: \`mot\`});
var _8 = 'mot';
let c9 = [], p9 = {key:9,attrs:{alt: _8}};
var vn9 = h('p', p9, c9);
c1.push(vn9);
c9.push({text: \`mot\`});
var _10 = 'word';
let c11 = [], p11 = {key:11,attrs:{something: _10}};
var vn11 = h('p', p11, c11);
c1.push(vn11);
c11.push({text: \`mot\`});
return vn1;
}"
`;
exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = `
"function anonymous(context,extra
) {
+98 -11
View File
@@ -1,5 +1,6 @@
import { QWeb } from "../../src/qweb/index";
import { normalize, renderToDOM, renderToString, trim, nextTick } from "../helpers";
import { nextTick, normalize, renderToDOM, renderToString, trim } from "../helpers";
import { patch } from "../../src/vdom";
//------------------------------------------------------------------------------
// Setup and helpers
@@ -659,7 +660,7 @@ describe("t-call (template calling", () => {
`);
const expected = "<div><span>hey</span></div>";
expect(renderToString(qweb, "recursive")).toBe(expected);
const recursiveFn = Object.values(qweb.recursiveFns)[0];
const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot();
});
@@ -687,7 +688,7 @@ describe("t-call (template calling", () => {
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
expected
);
const recursiveFn = Object.values(qweb.recursiveFns)[0];
const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot();
});
@@ -714,7 +715,7 @@ describe("t-call (template calling", () => {
const expected =
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
expect(renderToString(qweb, "Parent", { root }, { fiber: {} })).toBe(expected);
const recursiveFn = Object.values(qweb.recursiveFns)[0];
const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot();
});
@@ -837,6 +838,9 @@ describe("foreach", () => {
);
renderToString(qweb, "test");
expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenCalledWith(
"Directive t-foreach should always be used with a t-key! (in template: 'test')"
);
console.warn = consoleWarn;
});
});
@@ -1044,8 +1048,8 @@ describe("t-on", () => {
qweb.addTemplate("test", `<button t-on-click="state.counter++">Click</button>`);
let owner = {
state: {
counter: 0,
},
counter: 0
}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.counter).toBe(0);
@@ -1058,8 +1062,10 @@ describe("t-on", () => {
let owner = {
state: {
counter: 0,
incrementCounter: (inc) => { owner.state.counter += inc; },
},
incrementCounter: inc => {
owner.state.counter += inc;
}
}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.counter).toBe(0);
@@ -1223,7 +1229,7 @@ describe("t-on", () => {
);
const node = renderToDOM(qweb, "test", {}, { handlers: [] });
node.addEventListener('click', (e) => {
node.addEventListener("click", e => {
expect(e.defaultPrevented).toBe(true);
});
@@ -1303,12 +1309,12 @@ describe("t-ref", () => {
describe("loading templates", () => {
test("can initialize qweb with a string", () => {
const data = `
const templates = `
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="hey">jupiler</div>
</templates>`;
const qweb = new QWeb(data);
const qweb = new QWeb({ templates });
expect(renderToString(qweb, "hey")).toBe("<div>jupiler</div>");
});
@@ -1517,3 +1523,84 @@ describe("properly support svg", () => {
);
});
});
describe("translation support", () => {
test("can translate node content", () => {
const translations = {
word: "mot"
};
const translateFn = expr => translations[expr] || expr;
const qweb = new QWeb({ translateFn });
qweb.addTemplate("test", "<div>word</div>");
expect(renderToString(qweb, "test")).toBe("<div>mot</div>");
});
test("does not translate node content if disabled", () => {
const translations = {
word: "mot"
};
const translateFn = expr => translations[expr] || expr;
const qweb = new QWeb({ translateFn });
qweb.addTemplate(
"test",
`
<div>
<span>word</span>
<span t-translation="off">word</span>
</div>`
);
expect(renderToString(qweb, "test")).toBe("<div><span>mot</span><span>word</span></div>");
});
test("some attributes are translated", () => {
const translations = {
word: "mot"
};
const translateFn = expr => translations[expr] || expr;
const qweb = new QWeb({ translateFn });
qweb.addTemplate(
"test",
`
<div>
<p label="word">word</p>
<p title="word">word</p>
<p placeholder="word">word</p>
<p alt="word">word</p>
<p something="word">word</p>
</div>`
);
expect(renderToString(qweb, "test")).toBe(
'<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>'
);
});
});
describe("t-key tests", () => {
test("t-key on t-foreach", async () => {
qweb.addTemplate(
"test",
`
<div>
<t t-foreach="things" t-as="thing" t-key="thing">
<span/>
</t>
</div>`
);
let vnode = qweb.render("test", { things: [1, 2] });
vnode = patch(document.createElement("div"), vnode);
let elm = vnode.elm as HTMLElement;
expect(elm.outerHTML).toBe("<div><span></span><span></span></div>");
const first = elm.querySelectorAll("span")[0];
const second = elm.querySelectorAll("span")[1];
patch(vnode, qweb.render("test", { things: [2, 1] }));
expect(elm.outerHTML).toBe("<div><span></span><span></span></div>");
expect(first).toBe(elm.querySelectorAll("span")[1]);
expect(second).toBe(elm.querySelectorAll("span")[0]);
});
});
@@ -11,35 +11,35 @@ exports[`RouteComponent can render simple cases 1`] = `
let result;
var h = this.h;
if (context['routeComponent']) {
const nodeKey1 = context['env'].router.currentRouteName;
//COMPONENT
let key3 = 'key' + context['env'].router.currentRouteName;
let templateId2 = \`__4__\` + key3;
let w2 = templateId2 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId2]] : false;
let templateId3 = \`__4__\` + nodeKey1;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let vn5 = {};
result = vn5;
let props2 = Object.assign({}, context['env'].router.currentParams);
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
let props3 = Object.assign({}, context['env'].router.currentParams);
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined, undefined, sibling);
let pvnode = w2.__owl__.pvnode;
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode;
utils.defineProxy(vn5, pvnode);
} else {
let componentKey2 = \`routeComponent\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| context['routeComponent'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap[templateId2] = w2.__owl__.id;
let def1 = w2.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId2, hook: {insert(vn) { let nvn=w2.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w2.destroy();}}});
const fiber = w2.__owl__.currentFiber;
def1.then(function () {if (w2.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let componentKey3 = \`routeComponent\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['routeComponent'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
utils.defineProxy(vn5, pvnode);
w2.__owl__.pvnode = pvnode;
w3.__owl__.pvnode = pvnode;
}
sibling = w2.__owl__.currentFiber || sibling;
sibling = w3.__owl__.currentFiber || sibling;
}
return result;
}"
+9
View File
@@ -52,6 +52,15 @@ describe("router miscellaneous", () => {
expect(window.location.hash).toBe("#/users/5");
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(2);
});
test("navigate in hash mode preserve location", async () => {
router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }], {mode: "hash"});
window.history.pushState({}, "title", window.location.origin + '/test.html');
expect(window.location.href).toBe("http://localhost/test.html");
await router.navigate({ to: "users", params: { id: 3 } });
expect(window.location.href).toBe("http://localhost/test.html#/users/3");
});
});
describe("routeToPath", () => {
+155
View File
@@ -0,0 +1,155 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = useState({ counter: 0 });
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
static components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
static components = { Message };
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
messages[i] = msg;
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.logRef.el.appendChild(div);
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
}
clearLog() {
this.logRef.el.innerHTML = "";
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadFile("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL v0.24.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='../../owl.js'></script>
</head>
<body>
<script src='app.js' type="module"></script>
</body>
</html>
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-model="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-model="state.clearAfterFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+1 -1
View File
@@ -146,7 +146,7 @@ class App extends owl.Component {
async function start() {
const templates = await owl.utils.loadFile("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
qweb: new owl.QWeb({ templates })
};
const app = new App(env);
app.mount(document.body);
+12 -10
View File
@@ -44,15 +44,17 @@ export function startMeasure(descr) {
export function stopMeasure(cb) {
let last = lastMeasure;
if (lastMeasure) {
window.setTimeout(function() {
lastMeasure = null;
const stop = performance.now();
const delta = stop - startTime;
const msg = `[${last}] took ${formatNumber(delta)}ms`;
console.log(msg);
if (cb) {
cb({ msg, delta });
}
}, 0);
window.requestAnimationFrame(() => {
window.setTimeout(function() {
lastMeasure = null;
const stop = performance.now();
const delta = stop - startTime;
const msg = `[${last}] took ${formatNumber(delta)}ms`;
console.log(msg);
if (cb) {
cb({ msg, delta });
}
}, 0);
});
}
}
+1
View File
@@ -40,6 +40,7 @@
<li><a href="benchmarks/owl-0.17.0">OWL 0.17.0</a></li>
<li><a href="benchmarks/owl-0.18.0">OWL 0.18.0</a></li>
<li><a href="benchmarks/owl-0.21.0">OWL 0.21.0</a></li>
<li><a href="benchmarks/owl-0.24.0">OWL 0.24.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul>
<ul>
+4 -2
View File
@@ -90,7 +90,7 @@ function makeCodeIframe(js, css, xml, errorHandler) {
owlScript.addEventListener("load", () => {
const script = doc.createElement("script");
script.type = "text/javascript";
const content = `owl.__info__.mode = 'dev';\nwindow.TEMPLATES = \`${sanitizedXML}\`\n${js}`;
const content = `owl.__info__.mode = 'dev';\nwindow.TEMPLATES = \`${sanitizedXML}\`;\n${js}`;
script.innerHTML = content;
iframe.contentWindow.addEventListener("error", errorHandler);
iframe.contentWindow.addEventListener("unhandledrejection", errorHandler);
@@ -429,11 +429,13 @@ App.components = { TabbedEditor };
//------------------------------------------------------------------------------
async function start() {
document.title = `${document.title} (v${owl.__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([
owl.utils.loadFile("templates.xml"),
owl.utils.whenReady()
]);
const qweb = new owl.QWeb(templates);
const qweb = new owl.QWeb({ templates });
const app = new App({ qweb });
app.mount(document.body);
}
+11 -11
View File
@@ -17,7 +17,7 @@ App.components = { Greeter };
// Application setup
// Note that the xml templates are injected into the global TEMPLATES variable.
const qweb = new owl.QWeb(TEMPLATES);
const qweb = new owl.QWeb({ templates: TEMPLATES});
const app = new App({ qweb });
app.mount(document.body);
`;
@@ -70,7 +70,7 @@ class App extends Component {
}
App.components = { Counter };
const qweb = new owl.QWeb(TEMPLATES);
const qweb = new owl.QWeb({ templates: TEMPLATES});
const app = new App({qweb});
app.mount(document.body);
`;
@@ -228,7 +228,7 @@ class App extends Component {
}
App.components = { DemoComponent };
const qweb = new owl.QWeb(TEMPLATES);
const qweb = new owl.QWeb({ templates: TEMPLATES});
const app = new App({ qweb });
app.mount(document.body);
`;
@@ -298,7 +298,7 @@ class App extends owl.Component {
}
// Application setup
const qweb = new owl.QWeb(TEMPLATES);
const qweb = new owl.QWeb({ templates: TEMPLATES});
const app = new App({ qweb });
app.mount(document.body);
`;
@@ -350,7 +350,7 @@ const themeContext = new Context({
foreground: '#fff',
});
const env = {
qweb: new owl.QWeb(TEMPLATES),
qweb: new owl.QWeb({ templates: TEMPLATES}),
themeContext: themeContext,
};
const app = new App(env);
@@ -548,7 +548,7 @@ function makeEnv() {
const state = loadState();
const store = new owl.Store({ state, actions });
store.on("update", null, () => saveState(store.state));
const qweb = new owl.QWeb(TEMPLATES);
const qweb = new owl.QWeb({ templates: TEMPLATES});
return { qweb, store };
}
@@ -1041,7 +1041,7 @@ function setupResponsivePlugin(env) {
// Application Startup
//------------------------------------------------------------------------------
const env = {
qweb: new owl.QWeb(TEMPLATES),
qweb: new owl.QWeb({ templates: TEMPLATES}),
};
setupResponsivePlugin(env);
@@ -1187,7 +1187,7 @@ class App extends Component {
App.components = {Card, Counter};
// Application setup
const qweb = new owl.QWeb(TEMPLATES);
const qweb = new owl.QWeb({ templates: TEMPLATES});
const app = new App({ qweb });
app.mount(document.body);`;
@@ -1303,7 +1303,7 @@ class App extends Component {
}
App.components = {SlowComponent, NotificationList};
const qweb = new owl.QWeb(TEMPLATES);
const qweb = new owl.QWeb({ templates: TEMPLATES});
const app = new App({ qweb });
app.mount(document.body);
`;
@@ -1373,7 +1373,7 @@ class Form extends Component {
}
// Application setup
const qweb = new owl.QWeb(TEMPLATES);
const qweb = new owl.QWeb({ templates: TEMPLATES});
const form = new Form({ qweb });
form.mount(document.body);
`;
@@ -1540,7 +1540,7 @@ class App extends Component {
}
App.components = { WindowManager };
const qweb = new owl.QWeb(TEMPLATES);
const qweb = new owl.QWeb({ templates: TEMPLATES});
const windows = [
{
name: "Hello",