Compare commits

...

99 Commits

Author SHA1 Message Date
Géry Debongnie 7933328d0a wip 2019-12-02 13:18:50 +01:00
Géry Debongnie 821bd0b4b8 [IMP] tooling: add debug code
closes #521
2019-12-02 09:03:48 +01:00
Géry Debongnie 06a6d890d7 [DOC] move some component doc in sub pages
closes #354
2019-12-02 08:55:12 +01:00
Géry Debongnie c1269288f5 [REL] v1.0.0alpha5 2019-11-29 16:11:34 +01:00
Géry Debongnie 8c17bb0411 [FIX] component: reorganize fiber update algorithm
Closes #473
Closes #484
Closes #489
Closes #492
Closes #512

Co-authored-by: Aaron Bohy <aab@odoo.com>
2019-11-29 15:13:37 +01:00
Lucas Perais (lpe) 7f6782d009 [FIX] qweb: cascading t-call and t-raw="0"
Have a t-call having a t-raw="0"
that call is made to a template of the same form
i.e. itself as a t-call t-raw="0" structure

Before this commit, there was recursion crash
This was because the caller to which a t-raw="0" referred to
was incorrect. The caller here is the node which makes the t-call.
In particular, the caller was always set to last caller of the context

After this commit, there is no crash and this imbrication
of t-call and t-raw="0" is well rendered.
To sum up, we count the recursive calls to t-raw="0" and fetch
the caller in the context accordingly

closes #510
2019-11-29 13:17:40 +01:00
Aaron Bohy 6e5d6aa226 [FIX] slots: use correct scope and vars at update
Part of #473
2019-11-28 14:42:46 +01:00
Aaron Bohy 9400c0adad [FIX] playground: stop using class fields
as Edge doesn't support them.

Closes #509
2019-11-28 10:21:45 +01:00
Aaron Bohy 85d4393242 [FIX] t-slot: correct parented relationship with nested slots
Closes #506
2019-11-28 10:15:57 +01:00
Aaron Bohy 6c8b401092 [FIX] component: slots: multiple slots with components
Closes #508
2019-11-27 15:47:55 +01:00
Aaron Bohy 12be815342 [REF] qweb: use global nextID
and reset it to 1 before each test s.t. snapshots are deterministic.
2019-11-27 15:47:55 +01:00
Sébastien Theys 65344dbf1f [FIX] store: fix useStore update when mixing state and props changes
This commit also fixes an issue where updates would not be triggered when the
new and old revNumber where the same but concerned a different object.
2019-11-27 15:09:47 +01:00
Sébastien Theys a6f9b26057 [FIX] store: ensure useStore proper clean up on destroy 2019-11-27 15:09:47 +01:00
Géry Debongnie 6c753bb49f [FIX] qweb: scope t-key directive to node and subnodes
not to siblings

closes #503
2019-11-27 13:31:40 +01:00
Géry Debongnie 1f079b883e [IMP] store: prevent changes to store state in useStore return value
closes #500
2019-11-27 08:18:44 +01:00
Géry Debongnie 8fb35ed969 [FIX] owl: add prettier to devDependencies
also, run prettier on codebase

closes #498
2019-11-27 08:06:17 +01:00
Géry Debongnie a3f2d07b40 [DOC] add link to useSubEnv in environment doc 2019-11-26 22:32:25 +01:00
Sébastien Theys c993278c80 [IMP] store: allow useStore to return primitive types
Related to #404
2019-11-26 22:17:05 +01:00
Géry Debongnie 3aa586db43 [IMP] component: allow custom validator functions for props
closes #375
2019-11-22 15:43:22 +01:00
Géry Debongnie 14db513f3b [FIX] component: allow fragments in method mount
It is now possible to mount components in fragments

closes #494
2019-11-22 15:00:37 +01:00
Géry Debongnie 7bb04185c7 [FIX] playground: fix wms sample
It was crashing when we close a window, because there are two event
handlers for the click event on Window.  But when we close a window, we
actually do not want to update the z index.
2019-11-22 13:41:44 +01:00
Géry Debongnie 48744cfa87 [DOC] add hooks explanation, and testing/debug page
closes #372
closes #419
2019-11-22 13:41:44 +01:00
Géry Debongnie ff76747a05 [REL] v1.0.0-alpha4 2019-11-22 13:03:02 +01:00
Aaron Bohy 7e6b1a28a0 [IMP] qweb: throw error if t-component not used on a node t
Before this rev, using t-component on a div (for example) node
would silently ignore the div and replace it by the root node of
the component. A way to improve this was to create an extra div
node and to put the component inside it. However, it raised
questions: what do we do with other attributes set on this tag?
Do we apply them on the div, or on the component? In addition,
some of them only make sense on the component (e.g. props), so we
have to detect them. Whatever we would have decided, it wouldn't
have been obvious from the user point of view, so we chose not to
support it, and we thus now raise an error in this case.

Closes #487.
2019-11-21 17:06:35 +01:00
Aaron Bohy 58f6724194 [FIX] component: reset isRendered when reusing fiber
It is important to reset the isRendered flag to false to ensure
that other (subsequent) simultaneous calls to render will be
skipped, as the currentFiber is actually no yet (re-)rendered.

Closes #483
2019-11-21 15:35:30 +01:00
Aaron Bohy 3c0f8ac76f [IMP] component: introduce OwlEvent
Closes #485
2019-11-21 13:12:22 +01:00
Géry Debongnie cb11c0118c [FIX] context tests: make them deterministic
We learn something new every day, and today is no exception. Promise.all
of a pending promise does not actually resolve as fast as possible after
the initial promise is completed, but a small delay after, because it is
put on the macrotask queue or something like that (I assume)

closes #480
2019-11-20 15:44:12 +01:00
Géry Debongnie 27b4eece66 [FIX] component: display correct error in some case
It could happen that a component would crash. But then, the __render
code tried to copy the class properties into the vnode, which does not
exist, creating a new error.

So, the solution is to process the classObj only in the successful part
of the render call, which then will not crash, so the normal error
handling will occur.
2019-11-20 14:35:25 +01:00
Aaron Bohy 2e3e8cd603 [FIX] component: destroy not yet mounted components (2)
Following 2922cee6ea

Previous commit was not correct: used children with a cancelled
fiber (for instance, with a new currentFiber) could be destroyed.
This commit fixes the issue differently, by directly detecting
children that are not used anymore (and not mounted), and destroy
them directly (no need to wait for willStart promise to be resolved
anymore).
2019-11-20 14:31:03 +01:00
Aaron Bohy 28ee790b3e [FIX] context: always remove subscription when destroyed
Before this rev., components destroyed before being mounted didn't
stop listening to the context changes.

Closes #476
2019-11-20 11:43:14 +01:00
Aaron Bohy 2922cee6ea [FIX] component: can destroy not yet mounted components
Sub-issue of #476
2019-11-20 11:43:14 +01:00
Géry Debongnie 7749fd3b96 [FIX] component: remove stale fiber in scheduler
Owl takes care of not rendering a component that will be unmounted
immediately after, and it works.  However, it also should clean
up the newly added fiber.
2019-11-20 11:24:32 +01:00
Géry Debongnie f44b9a38ae [IMP] component: better coordination for errors in rendering
part of #410
2019-11-20 11:24:32 +01:00
Géry Debongnie c7773bfd2a [FIX] qweb/component: fix scoping issue with t-model and t-foreach
Without this fix, the handler set by t-model did not capture properly
the expression that needs to be updated.

closes #474
2019-11-19 13:15:34 +01:00
Géry Debongnie bd39797f17 [FIX] component: propagate errors to caller
Errors from mounted/willPatch/patched should be returned to caller
(either mount or render functions)

part of #410
2019-11-18 14:29:54 +01:00
Géry Debongnie 94a595ef5b [FIX] qweb: add support for typeof expression
closes #469
2019-11-18 07:57:43 +01:00
Lucas Perais (lpe) f83846e054 [FIX] package, vdom: condition always true, TS 3.7.2
Before this commit, a condition deemed always true made
the building crash. The happened with TypeScript == 3.7.2

After this commit, the required typescript version has been
changed, as well as the incriminated true condition
2019-11-15 13:10:02 +01:00
Géry Debongnie a97144366d [FIX] playground: fix downloaded app code
The generated downloaded code from the playground did not work because
it did not load templates into QWeb anymore.

closes #461
2019-11-15 11:19:27 +01:00
Aaron Bohy 79983de4ed [FIX] context: notify changes even if owner not mounted
Closes #444
2019-11-15 07:34:15 +01:00
Géry Debongnie 54e1734f2f [IMP] component: display better error message if render is empty
closes #446
2019-11-14 16:40:07 +01:00
Géry Debongnie 4aea093ed9 [FIX] tools: fix benchmark for v0.24.0
It was not using the correct owl version
2019-11-14 15:51:53 +01:00
Géry Debongnie 56087b95cd [REF] observer: notify subscribers directly
This rev. moves the logic of batching the notifications of changes
occurring in the same microtask tick, from the observer, to the
listeners (the context, and the render function of components).
By doing so in render, we ensure that similar renderings are
batched in a single one, wherever they come from (state change,
store change, direct call...).
2019-11-14 15:12:31 +01:00
Aaron Bohy e5940b4b6b [FIX] component: concurrent calls to mount and render
Closes #450
2019-11-14 15:12:31 +01:00
Géry Debongnie bbdc9d90d7 [FIX] qweb: allow mixing body and expression variables
closes #445
2019-11-14 14:54:27 +01:00
Géry Debongnie f2b3ebd1ec [FIX] component: properly set currentFiber to null in all cases
Whenever a rendering is completed, we need to reset the currentFiber
to null to make sure all subsequent renderings will not be
confused.

However, the way it was done before this commit was wrong in a
specific situation: we resetted the currentFiber to null in the
patch method.  The idea was that this method was called every time
the component completes a rendering. But this is not true: it
can happen that a component is unmounted and remounted without
changes.  Then the component will be patched, but if there is no
change, it will not call recursively the patched methods of its
children.

So, we need to choose a better place to reset it to null.

closes #454
2019-11-14 11:56:13 +01:00
Géry Debongnie 55bb09ba1a [FIX] component: fix invalid prop validation situations
closes #453
2019-11-14 09:27:09 +01:00
Géry Debongnie d249f50d09 [REL] v1.0.0-alpha3 2019-11-13 15:43:47 +01:00
Géry Debongnie 749f0063ea [FIX] component: properly validate optional types in objects
closes #440
2019-11-13 15:36:46 +01:00
Géry Debongnie 263f31fea9 [DOC] miscellaneous improvements
including:

- add a link to the tutorial in the main readme page
- remove learning page on 'env', move content on reference page
- add dynamic sub components section in component page
- add reference page on props
- split qweb page into engine/language pages
- move t-key information into qweb language page
2019-11-13 08:49:25 +01:00
Géry Debongnie b891ae7de4 [REM] tools: remove error handling in playground
There was some code to attempt to display the error in the right
pane, whenever it occured in the app early phase.

However, it made it much harder to properly handle all cases. It could
silently swallow some errors (if more than one was done), And it was
not behaving the same way in Firefox and in Chrome.
2019-11-13 08:49:25 +01:00
Géry Debongnie c5a80497ac [FIX] tooling: properly set playground in dev mode
It was not in dev mode since changes to config system
2019-11-13 08:49:25 +01:00
Aaron Bohy e47f604449 [FIX] component: concurrent rendering issue
Resolved rendering with cancelled fiber for (not yet) destroyed
component -> Cannot read property 'sel' of null

Closes #421
2019-11-12 10:01:12 +01:00
Aaron Bohy 7e721a96b5 [IMP] hooks: useRef on component: set el anyway
Closes #437
2019-11-06 17:06:37 +01:00
Aaron Bohy af4f372506 [REF] component: mount: remove renderBeforeRemount option
Re-render the component all the time before remounting it, which
seems safer anyway.

In the test, we had to add a nextTick to wait for the changes to
be notified (before, we waited thanks to the additional call to
mount). If we don't do the nextTick, mount is called, and render
is called right after (before the promise returned by mount is
resolved). This scenario doesn't work right now (see #441).

Closes #381
2019-11-06 16:26:12 +01:00
Aaron Bohy 5731358607 [IMP] component: expose scheduler instance
Closes #433
2019-11-05 15:11:13 +01:00
Géry Debongnie 1226f015d7 [DOC] add a tutorial (TodoApp) 2019-11-04 16:50:16 +01:00
Géry Debongnie b211e75140 [IMP] store: display error if no store found 2019-11-04 08:41:29 +01:00
Géry Debongnie 8d1dd06340 [IMP] component: error when mounted on invalid target 2019-11-04 08:41:29 +01:00
Géry Debongnie bd3c1265d3 [REL] v1.0.0-alpha2 2019-11-01 09:12:33 +01:00
Géry Debongnie 2413c98f50 [REF] component: small optimization in rare cases
When we have default props, we do not need to allocate a new
props object and copy it. This commit simply modify the props
object in place, which is fine, since the props object is always
a new one any way, generated by the directive.
2019-11-01 09:03:30 +01:00
Géry Debongnie d74b5a03db [IMP] component: add props on root components
This is a partial revert of commit 7d249d6f09.

The reason is that this changes made it much harder to unit test
components.  Before, we could simply instantiate a component
like this:
const my|Comp = new MyComponent(null, props);

and then simply test it.  This commit reestablish that possibility.
2019-11-01 09:03:27 +01:00
Géry Debongnie 83532db48f [DOC] adapt documentation to env changes
part of #430
2019-11-01 09:03:27 +01:00
Géry Debongnie 534152eff7 [REF] component: small improvement
and run prettier
2019-11-01 08:39:35 +01:00
Aaron Bohy 05a678c039 [IMP] component: get env from constructor
closes #430
2019-11-01 08:39:35 +01:00
Géry Debongnie 8fbf2172c5 [DOC] slightly reorganize main documentation page 2019-10-31 12:32:26 +01:00
Géry Debongnie ea1376d0ca [FIX] tools: make benchmarks work on firefox
Firefox does not support the static keyword yet, so the benchmark code should be modified to make it work.
2019-10-31 12:32:26 +01:00
Géry Debongnie ba483b6e2c [FIX] component tests: make them work on windows
For an unknown reason, it looks like the way ticks, micro task ticks or other subtle scheduling issues are different on a windows machine (it may be because of other difference, such as a specific version of nodejs).

Anyway, I could not find the cause of the issue, but
simply waiting an extra microtask tick seems to work.
2019-10-31 12:32:26 +01:00
Géry Debongnie 2fc71cfb62 [REL] v1.0.0-alpha
v1.0alpha

The Alpha release!

Owl is finally getting stable. This relase is all about cleaning Owl
API.  We
are pretty happy with the current state, and hopefully, we won't have to
make
any non trivial change for a while.

QWeb

- add an option to setup a translate function
- implement `t-key` with a directive (it now works on `t` tags)

Component

- properly handle errors in `mount` and `render`
- fix: make sure props are validated in all cases
- fix: make sure default props are applied at the proper time
- fix: better error handling with sub components
- imp: simplify constructor API: does not take an `env`
- imp: do not set `props` in root components
- remove `t-keepalive` directive

Context

- update components concurrently, instead of sequentially

Observer

- remove `revNumber` (and rename `deepRevNumber` into `revNumber`)

Router

- fix: preserve pathname in hash mode

Config

- create new config object, with `mode` and `env` keys

Playground

- log git commit hash in console
2019-10-30 16:35:45 +01:00
Aaron Bohy 08cb83149e [REM] component: remove t-keepalive directive
We don't see any usecase for it, it makes the code more complex,
and there were still potential unresolved concurrency issues with
it.

Part of task #295
2019-10-30 16:35:45 +01:00
Aaron Bohy 7d249d6f09 [IMP] component: do not set props in root components
Also remove props from Fiber, as it was not useful anymore.
2019-10-30 15:09:10 +01:00
Géry Debongnie 0addca63a0 [DOC] update the documentation to new config and env
part of #306
2019-10-30 15:09:10 +01:00
Géry Debongnie 5ba73cc09d [DOC] move quick_start into learning/ sub folder 2019-10-30 15:09:10 +01:00
Aaron Bohy 9106c19066 [IMP] component: simplify constructor API
It now takes two arguments: parent (optional, only for non-root
components) and props (optional). In the case of the root component,
the env is taken from the config (config.defaultEnv). If it doesn't
exists, the default env is created on the fly.

Closes #306
2019-10-30 15:09:10 +01:00
Aaron Bohy 9f93da4765 [REF] config: move mode from owl.__info__ to owl.config
and move it to its own file.
2019-10-30 15:09:10 +01:00
Aaron Bohy 9e37b968e8 [FIX] component: error handling: rendering with sub components
Closes #425
2019-10-30 13:27:48 +01:00
Aaron Bohy fa6801b523 [REF] observer: remove revNumber
Closes #257
2019-10-30 11:13:52 +01:00
Géry Debongnie 9edf29a3a1 [IMP] context: group components by depth
closes #399
2019-10-29 16:02:51 +01:00
Géry Debongnie 6a434310ee [FIX] *: run prettier 2019-10-29 16:02:51 +01:00
Géry Debongnie e7967d0779 [FIX] package.json: improve prettier task
to take into account files at more than one level
2019-10-29 16:02:51 +01:00
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
83 changed files with 11969 additions and 3155 deletions
+15 -35
View File
@@ -1,10 +1,10 @@
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1>
_A no nonsense web framework for structured, dynamic and maintainable applications_
_Class based components with hooks, reactive state and concurrent mode_
## Project Overview
The Odoo Web Library (OWL) is a smallish (~18kb gzipped) UI framework intended to
The Odoo Web Library (OWL) is a smallish (~<20kb gzipped) UI framework intended to
be the basis for the [Odoo](https://www.odoo.com/) Web Client. Owl is a modern
framework, written in Typescript, taking the best ideas from React and Vue in a
simple and consistent way. Owl's main features are:
@@ -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, 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 {
@@ -54,12 +50,12 @@ class App extends Component {
static components = { Counter };
}
const app = new App({ qweb: new QWeb() });
const app = new App();
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
@@ -96,9 +92,10 @@ 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)
- [Tutorial: TodoList application](doc/learning/tutorial_todoapp.md)
- [QWeb templating language](doc/reference/qweb_templating_language.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 +104,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-1.0.0-alpha5.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha5/owl.js)
- [owl-1.0.0-alpha5.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha5/owl.min.js)
Some npm scripts are available:
@@ -131,23 +128,6 @@ Owl components in an application are used to define a (dynamic) tree of componen
C D
```
**Environment:** the root component is special: it is created with an environment,
which should contain a `QWeb` instance. The environment is then automatically
propagated to each sub components (and accessible in the `this.env` property).
```js
const env = { qweb: new QWeb() };
const app = new App(env);
app.mount(document.body);
```
The environment is mostly static. Each application is free to add anything to
the environment, which is very useful, since this can be accessed by each sub
component. Some good use case for that is some configuration keys, session
information or generic services (such as doing rpcs, or accessing local storage).
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
**State:** each component can manage its own local state. It is a simple ES6
class, there are no special rules:
@@ -259,7 +239,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
+32
View File
@@ -0,0 +1,32 @@
# 🦉 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).
+6 -5
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 = {
@@ -274,7 +274,8 @@ class Counter extends Component {
dispatch = useDispatch();
}
const counter = new Counter({ store, qweb });
Counter.env.store = store;
const counter = new Counter();
```
## Hooks
@@ -314,7 +315,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.
@@ -2,8 +2,17 @@
## Static Server
Let us assume that we have a static server running somewhere. We could then
simply add an html page with a few extra files.
Let us assume that we have a static server running somewhere. Let us start by
adding an html page with a few extra files:
```
my-app/
index.html
app.css
app.js
owl-X.Y.Z.js
templates.xml
```
### HTML and CSS
@@ -88,10 +97,8 @@ class ClickCounter extends owl.Component {
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadFile("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const counter = new ClickCounter(env);
ClickCounter.env = { qweb: new owl.QWeb({ templates }) };
const counter = new ClickCounter();
const target = document.getElementById("main");
await counter.mount(target);
}
+132
View File
@@ -0,0 +1,132 @@
# 🦉 Testing Owl components 🦉
## Content
- [Overview](#overview)
- [Unit Tests](#unit-tests)
## Overview
It is a good practice to test applications and components to ensure that they
behave as expected. There are many ways to test a user interface: manual
testing, integration testing, unit testing, ...
In this section, we will discuss how to write unit tests for components, and
how to debug them if necessary.
## Unit Tests
Writing unit tests for Owl components really depends on the testing framework
used in a project. But usually, it involves the following steps:
- create a test file: for example `SomeComponent.test.js`,
- in that file, import the code for `SomeComponent`,
- add a test case:
- create a real DOM element to use as test fixture,
- create a test environment
- create an instance of `SomeComponent`, mount it to the fixture
- interact with the component and assert some properties.
To help with this, it is useful to have a `helper.js` file that contains some
common utility functions:
```js
export function makeTestFixture() {
let fixture = document.createElement("div");
document.body.appendChild(fixture);
return fixture;
}
export function nextTick() {
let requestAnimationFrame = owl.Component.scheduler.requestAnimationFrame;
return new Promise(function(resolve) {
setTimeout(() => requestAnimationFrame(() => resolve()));
});
}
export function makeTestEnv() {
// application specific. It needs a way to load actual templates
const templates = ...;
return {
qweb: new QWeb(templates),
..., // each service can be mocked here
};
}
```
With such a file, a typical test suite for Jest will look like this:
```js
// in SomeComponent.test.js
import { SomeComponent } from "../../src/ui/SomeComponent";
import { nextTick, makeTestFixture, makeTestEnv} from '../helpers';
//------------------------------------------------------------------------------
// Setup
//------------------------------------------------------------------------------
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
// we set here the default environment for each component created in the test
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("SomeComponent", () => {
test("component behaves as expected", async () => {
const props = {...}; // depends on the component
const comp = new SomeComponent(null, props);
await comp.mount(fixture);
// do some assertions
expect(...).toBe(...);
fixture.querySelector('button').click();
await nextTick();
// some other assertions
expect(...).toBe(...);
});
});
```
Note that Owl does wait for the next animation frame to actually update the DOM.
This is why it is necessary to wait with the `nextTick` (or other methods) to
make sure that the DOM is up-to-date.
It is sometimes useful to wait until Owl is completely done updating components
(in particular, if we have a highly concurrent user interface). This next
helper simply polls every 20ms the internal Owl task queue and returns a promise
which resolves when it is empty:
```js
function afterUpdates() {
return new Promise((resolve, reject) => {
let timer = setTimeout(poll, 20);
let counter = 0;
function poll() {
counter++;
if (owl.Component.scheduler.tasks.length) {
if (counter > 10) {
reject(new Error("timeout"));
} else {
timer = setTimeout(poll);
}
} else {
resolve();
}
}
});
}
```
File diff suppressed because it is too large Load Diff
+64 -62
View File
@@ -1,75 +1,77 @@
# 🦉 OWL Documentation 🦉
## Owl Content
Owl is a javascript library that contains some core classes and function to help
build applications. Here is a complete representation of its content:
```
owl
Component
Context
QWeb
Store
useState
core
EventBus
Observer
hooks
onWillStart
onMounted
onWillUpdateProps
onWillPatch
onPatched
onWillUnmount
useContext
useState
useRef
useSubEnv
useStore
useDispatch
useGetters
misc
AsyncRoot
router
Link
RouteComponent
Router
tags
xml
utils
debounce
escape
loadJS
loadFile
shallowEqual
whenReady
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
## 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)
- [Concurrency Model](reference/concurrency_model.md)
- [Configuration](reference/config.md)
- [Context](reference/context.md)
- [Environment](reference/environment.md)
- [Event Bus](reference/event_bus.md)
- [Hooks](reference/hooks.md)
- [Miscellaneous Components](reference/misc.md)
- [Observer](reference/observer.md)
- [Props](reference/props.md)
- [Props Validation](reference/props_validation.md)
- [QWeb Templating Language](reference/qweb_templating_language.md)
- [QWeb Engine](reference/qweb_engine.md)
- [Router](reference/router.md)
- [Store](reference/store.md)
- [Tags](reference/tags.md)
- [Utils](reference/utils.md)
## Learning Resources
- [Quick Start](quick_start.md)
- [Quick Start: create an (almost) empty Owl application](learning/quick_start.md)
- [Tutorial: create a TodoList application](learning/tutorial_todoapp.md)
- [Testing Owl components](learning/testing_components.md)
## Miscellaneous
- [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 targeted
for developers working on Owl itself.
- [Virtual DOM](architecture/vdom.md)
- [Rendering](architecture/rendering.md)
## Owl Content
Here is a complete visual representation of everything exported by the `owl`
global object (so, for example, `Component` is available at `owl.Component`,
and `EventBus` is exported as `owl.core.EventBus`):
```
Component misc
Context AsyncRoot
QWeb router
Store Link
useState RouteComponent
config Router
mode tags
core xml
EventBus utils
Observer debounce
hooks escape
onWillStart loadJS
onMounted loadFile
onWillUpdateProps shallowEqual
onWillPatch whenReady
onPatched
onWillUnmount
useContext
useState
useRef
useSubEnv
useStore
useDispatch
useGetters
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
+95 -379
View File
@@ -11,16 +11,12 @@
- [Methods](#methods)
- [Lifecycle](#lifecycle)
- [Root Component](#root-component)
- [Environment](#environment)
- [Composition](#composition)
- [Event Handling](#event-handling)
- [Form Input Bindings](#form-input-bindings)
- [`t-key` Directive](#t-key-directive)
- [Semantics](#semantics)
- [Props Validation](#props-validation)
- [References](#references)
- [Slots](#slots)
- [Asynchronous Rendering](#asynchronous-rendering)
- [Dynamic sub components](#dynamic-sub-components)
- [Error Handling](#error-handling)
- [Functional Components](#functional-components)
- [SVG components](#svg-components)
@@ -43,7 +39,7 @@ OWL components are the building blocks for user interface. They are designed to
and follow the QWeb specification. This is a requirement for Odoo.
OWL components are defined as a subclass of Component. The rendering is
exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in QWeb).
exclusively done by a [QWeb](qweb_templating_language.md) template (which needs to be preloaded in QWeb).
Rendering a component generates a virtual dom representation
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
@@ -81,8 +77,8 @@ a state object is defined, by using the `useState` hook. It is not mandatory to
## Reference
An Owl component is a small class which represents a component or some UI element.
It exists in the context of an environment (`env`), which is propagated from a
parent to its children. The environment needs to have a QWeb instance, which
It exists in the context of an [environment](environment.md) (`env`), which is propagated from a
parent to its children. The environment needs to have a [QWeb](qweb_templating_language.md) instance, which
will be used to render the component template.
Be aware that the name of the component may be significant: if a component does
@@ -167,7 +163,7 @@ to be called in the constructor.
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
component is not mounted.
- **`env`** (Object): the component environment, which contains a QWeb instance.
- **`env`** (Object): the component [environment](environment.md), which contains a QWeb instance.
- **`props`** (Object): this is an object containing all the properties given by
the parent to a child component. For example, in the following situation,
@@ -205,7 +201,7 @@ to be called in the constructor.
* **`props`** (Object, optional): if given, this is an object that describes the
type and shape of the (actual) props given to the component. If Owl mode is
`dev`, this will be used to validate the props each time the component is
created/updated. See [Props Validation](#props-validation) for more information.
created/updated. See [Props Validation](props_validation.md) for more information.
```js
class Counter extends owl.Component {
@@ -238,16 +234,29 @@ component.
We explain here all the public methods of the `Component` class.
- **`mount(target, renderBeforeRemount=false)`** (async): this is the main way a
- **`mount(target)`** (async): this is the main way a
component is added to the DOM: the root component is mounted to a target
HTMLElement. Obviously, this is asynchronous, since each children need to be
HTMLElement (or document fragment). Obviously, this is asynchronous, since each children need to be
created as well. Most applications will need to call `mount` exactly once, on
the root component.
The `renderBeforeRemount` argument is useful when a component is unmounted and remounted.
In that case, we may want to rerender the component _before_ it is remounted, if
we know that its state (or something in the environment, or ...) has changed.
In that case, it should simply set to `true`.
Note that if a component is mounted, unmounted and remounted, it will be
automatically re-rendered to ensure that changes in its state (or something
in the environment, or in the store, or ...) will be taken into account.
If a component is mounted inside an element or a fragment which is not in the
DOM, then it will be rendered fully, but not active: the `mounted` hooks will
not be called. This is sometimes useful if we want to load an application in
memory. In that case, we need to mount the root component again in an element
which is in the DOM:
```js
const app = new App();
await app.mount(document.createDocumentFragment());
// app is rendered in memory, but not active
await app.mount(document.body);
// app is now visible
```
- **`unmount()`**: in case a component needs to be detached/removed from the DOM, this
method can be used. Most applications should not call `unmount`, this is more
@@ -370,8 +379,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,14 +395,13 @@ 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.
Note that modifying the state is not allowed here. This method is called just
before an actual DOM patch, and is only intended to be used to save some local
DOM state. Also, it will not be called if the component is not in the DOM (this can
happen with components with `t-keepalive`).
DOM state. Also, it will not be called if the component is not in the DOM.
#### `patched(snapshot)`
@@ -403,7 +411,7 @@ likely via a change in its state/props or environment).
This method is not called on the initial render. It is useful to interact
with the DOM (for example, through an external library) whenever the
component was patched. Note that this hook will not be called if the compoent is
not in the DOM (this can happen with components with `t-keepalive`).
not in the DOM.
Updating the component state in this hook is possible, but not encouraged.
One needs to be careful, because updates here will create an additional rendering, which in
@@ -413,7 +421,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,49 +449,17 @@ of an Owl application has to be created manually:
```js
class App extends owl.Component { ... }
const qweb = new owl.QWeb(TEMPLATES);
const env = { qweb: qweb };
const app = new App(env);
const app = new App();
app.mount(document.body);
```
The root component needs an environment.
The root component does not have a parent nor `props` (see note below). It will be setup with an
[environment](environment.md) (either the `env` defined on its class, or a
default empty environment).
### Environment
In Owl, an environment is an object with a `qweb` key, which has to be a
[QWeb](qweb.md) instance. This qweb instance will be used to render everything.
The environment is meant to contain (mostly) static global information and
methods for the whole application. For example, settings keys (`mode` to determine
if we are in desktop or mobile mode, or `theme`: dark or light), `rpc` methods,
session information, ...
The environment will be given to each child, unchanged, in the `env` property.
This can be very useful to share common information/methods. For example, all
rpcs can be made through a `rpc` method in the environment. This makes it very
easy to test a component.
Updating the environment is not as simple as changing a component's state: its
content is not observed, so updates will not be reflected immediately in the
user interface. There is however a mechanism to force root widgets to rerender
themselves whenever the environment is modified: one only needs to call the
`forceUpdate` method on the QWeb instance. For example, a responsive environment
could be done like this:
```js
function setupResponsivePlugin(env) {
const isMobile = () => window.innerWidth <= 768;
env.isMobile = isMobile();
const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile;
env.qweb.forceUpdate();
}
}, 15);
window.addEventListener("resize", updateEnv);
}
```
Note: a root component can however be given a `props` object in its constructor,
like this: `new App(null, {some: 'object'});`. It will not be a true `props`
object, managed by Owl (so, for example, it will never be updated).
### Composition
@@ -537,68 +513,10 @@ the static `components` key, then fallbacks on the global registry.
_Props_: In this example, the child component will receive the object `{count: 4}` in its
constructor. This will be assigned to the `props` variable, which can be accessed
on the component (and also, in the template). Whenever the state is updated, then
the sub component will also be updated automatically.
the sub component will also be updated automatically. See the [props section](props.md)
for more information.
Note that there are some restrictions on 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,
the `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
```xml
<div t-name="ParentComponent">
<t t-component="ChildComponent{{id}}" />
</div>
```
```js
class ParentComponent {
static components = { ChildComponent1, ChildComponent2 };
state = { id: 1 };
}
```
And the `t-props` directive can be used to specify totally dynamic props:
```xml
<div t-name="ParentComponent">
<Child t-props="some.obj"/>
</div>
```
```js
class ParentComponent {
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
```
There is an even more dynamic way to use `t-component`: its value can be an
expression evaluating to an actual component class. In that case, this is the
class that will be used to create the component:
```js
class A extends Component<any, any, any> {
static template = xml`<span>child a</span>`;
}
class B extends Component<any, any, any> {
static template = xml`<span>child b</span>`;
}
class App extends Component<any, any, any> {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = { child: "a" };
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
```
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 +529,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 +539,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.
@@ -660,11 +578,13 @@ A _business_ DOM event is triggered by a call to `trigger` on a component.
}
```
The call to `trigger` generates a [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
of type `menu-loaded` and dispatches it on the component's DOM element
(`this.el`). The event bubbles and is cancelable. The parent component listening
to event `menu-loaded` will receive the payload in its `someMethod` handler
(in the `detail` property of the event), whenever the event is triggered.
The call to `trigger` generates an `OwlEvent`, a subclass of [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
with an additional attribute `originalComponent` (the component that triggered
the event). The generated event is of type `menu-loaded` and dispatches it on
the component's DOM element (`this.el`). The event bubbles and is cancelable.
The parent component listening to event `menu-loaded` will receive the payload
in its `someMethod` handler (in the `detail` property of the event), whenever
the event is triggered.
```js
class ParentComponent {
@@ -677,8 +597,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 +614,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 +723,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 |
| --------- | -------------------------------------------------------------------- |
@@ -821,213 +742,6 @@ update a number whenever the change is done.
Note: the online playground has an example to show how it works.
### `t-key` Directive
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.
There are three main use cases:
- _elements in a list_:
```xml
<span t-foreach="todos" t-as="todo" t-key="todo.id">
<t t-esc="todo.text" />
</span>
```
- _`t-if`/`t-else`_
- _animations_: give a different identity to a component. Ex: thread id with
animations on add/remove message.
### Semantics
We give here an informal description of the way components are created/updated
in an application. Here, ordered lists describe actions that are executed
sequentially, bullet lists describe actions that are executed in parallel.
**Scenario 1: initial rendering** Imagine we want to render the following component tree:
```
A
/ \
B C
/ \
D E
```
Here is what happen whenever we mount the root
component (with some code like `app.mount(document.body)`).
1. `willStart` is called on `A`
2. when it is done, template `A` is rendered.
- component `B` is created
1. `willStart` is called on `B`
2. template `B` is rendered
- component `C` is created
1. `willStart` is called on `C`
2. template `C` is rendered
- component `D` is created
1. `willStart` is called on `D`
2. template `D` is rendered
- component `E` is created
1. `willStart` is called on `E`
2. template `E` is rendered
3. component `A` is patched into a detached DOM element. This will create the actual
component `A` DOM structure. The patching process will cause recursively the
patching of the `B`, `C`, `D` and `E` DOM trees. (so the actual full DOM tree is created
in one pass)
4. the component `A` root element is actually appended to `document.body`
5. The method `mounted` is called recursively on all components in the following
order: `B`, `D`, `E`, `C`, `A`.
**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some
button in `C`, and this results in a state update, which is supposed to:
- update `D`,
- remove `E`,
- add new component `F`.
So, the component tree should look like this:
```
A
/ \
B C
/ \
D F
```
Here is what Owl will do:
1. because of a state change, the method `render` is called on `C`
2. template `C` is rendered again
- component `D` is updated:
1. hook `willUpdateProps` is called on `D` (async)
2. template `D` is rerendered
- component `F` is created:
1. hook `willStart` is called on `E` (async)
2. template `F` is rendered
3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
because it is not mounted yet)
4. component `C` is patched, which will cause recursively:
2. `willUnmount` hook on `E`, then destruction of `E`,
3. (initial) patching of `F`, then hook `mounted` is called on `F`
5. patching of `D`
6. `patched` hooks are called on `D`, `C`
### Props Validation
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
- hard to tell how a component should be used, by looking at its code.
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
A props type system would solve both issues, by describing the types and shapes
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))
- 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.
For example:
```js
class ComponentA extends owl.Component {
static props = ['id', 'url'];
...
}
class ComponentB extends owl.Component {
static props = {
count: {type: Number},
messages: {
type: Array,
element: {type: Object, shape: {id: Boolean, text: 'string' }
},
date: Date,
combinedVal: [Number, Boolean]
};
...
}
```
- it is an object or a list of strings
- a list of strings is a simplified props definition, which only lists the name
of the props. Also, if the name ends with `?`, it is considered optional.
- all props are by default required, unless they are defined with `optional: true`
(in that case, validation is only done if there is a value)
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
constructor functions (so, if you have a `Person` class, it can be used as a type)
- arrays are homogeneous (all elements have the same type/shape)
For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
- a boolean: indicate that the props exists, and is mandatory.
- a constructor: this should describe the type, for example: `id: Number` describe
the props `id` as a number
- a list of constructors. In that case, this means that we allow more than one
type. For example, `id: [Number, String]` means that `id` can be either a string
or a number.
- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed:
- `type`: the main type of the prop being validated
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. It is optional (not set means that we only validate the array, not its elements),
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. It is optional (not set means that we only validate the object, not its elements)
Examples:
```js
// only the existence of those 3 keys is documented
static props = ['message', 'id', 'date'];
```
```js
// size is optional
static props = ['message', 'size?'];
```
```js
static props = {
messageIds: {type: Array, element: Number}, // list of number
otherArr: {type: Array}, // just array. no validation is made on sub elements
otherArr2: Array, // same as otherArr
someObj: {type: Object}, // just an object, no internal validation
someObj2: {
type: Object,
shape: {
id: Number,
name: {type: String, optional: true},
url: String
]}, // object, with keys id (number), name (string, optional) and url (string)
someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop
};
```
### References
The `useRef` hook is useful when we need a way to interact with some inside part
@@ -1140,48 +854,50 @@ be considered the `default` slot. For example:
</div>
```
### Asynchronous Rendering
### Dynamic sub components
Working with asynchronous code always adds a lot of complexity to a system. Whenever
different parts of a system are active at the same time, one needs to think
carefully about all possible interactions. Clearly, this is also true for Owl
components.
It is not common, but sometimes we need a dynamic component name. In this case,
the `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb_templating_language.md#dynamic-attributes) directive):
There are two different common problems with Owl asynchronous rendering model:
```xml
<div t-name="ParentComponent">
<t t-component="ChildComponent{{id}}" />
</div>
```
- any component can delay the rendering (initial and subsequent) of the whole
application
- for a given component, there are two independant situations that will trigger an
asynchronous rerendering: a change in the state, or a change in the props.
These changes may be done at different times, and Owl has no way of knowing
how to reconcile the resulting renderings.
```js
class ParentComponent {
static components = { ChildComponent1, ChildComponent2 };
state = { id: 1 };
}
```
Here are a few tips on how to work with asynchronous components:
There is an even more dynamic way to use `t-component`: its value can be an
expression evaluating to an actual component class. In that case, this is the
class that will be used to create the component:
1. Minimize the use of asynchronous components!
2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
synchronous renderings
3. Lazy loading external libraries is a good use case for async rendering. This
is mostly fine, because we can assume that it will only takes a fraction of a
second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs))
4. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When
this component is met, a new rendering
sub tree is created, such that the rendering of that component (and its
children) is not tied to the rendering of the rest of the interface. It can
be used on an asynchronous component, to prevent it from delaying the
rendering of the whole interface, or on a synchronous one, such that its
rendering isn't delayed by other (asynchronous) components. Note that this
directive has no effect on the first rendering, but only on subsequent ones
(triggered by state or props changes).
```js
class A extends Component<any, any, any> {
static template = xml`<span>child a</span>`;
}
class B extends Component<any, any, any> {
static template = xml`<span>child b</span>`;
}
class App extends Component<any, any, any> {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
```xml
<div t-name="ParentComponent">
<SyncChild />
<AsyncRoot>
<AsyncChild/>
</AsyncRoot>
</div>
```
state = { child: "a" };
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
```
In this example, the component `App` selects dynamically the concrete sub
component class.
Note that the `t-component` directive can only be used on `<t>` nodes.
### Error Handling
+183
View File
@@ -0,0 +1,183 @@
# 🦉 Concurrency Model 🦉
## Content
- [Overview](#overview)
- [Rendering Components](#rendering-components)
- [Semantics](#semantics)
- [Asynchronous Rendering](#asynchronous-rendering)
## Overview
Owl was designed from the very beginning with asynchronous components. This comes
from the `willStart` and the `willUpdateProps` lifecycle hooks. With these
methods, it is possible to build complex highly concurrent applications.
Owl concurrent mode has several benefits: it makes it possible to delay the
rendering until some asynchronous operation is complete, it makes it possible
to lazy load libraries, while keeping the previous screen completely functional.
It is also good for performance reasons: Owl uses it to only apply the result of
many different renderings only once in an animation frame. Owl can cancel
a rendering that is no longer relevant, restart it, reuse it in some cases.
But even though using concurrency is quite simple (and is the default behaviour),
asynchrony is difficult, because it introduces an additional dimension that
vastly increase the complexity of an application. This section will explain
how Owl manages this complexity, how concuurent rendering works in a general way.
## Rendering Components
The word _rendering_ is a little vague, so, let us explain more precisely the
process by which Owl components are displayed on a screen.
When a component is mounted or updated, a new rendering is started. It has
two phases: _virtual rendering_ and _patching_.
### Virtual rendering
This phase represent the process of rendering a template, in memory, which create a virtual representation of the desired component html). The output of this phase is a
virtual DOM.
It is asynchronous: each subcomponents needs to either be created (so, `willStart`
will need to be called), or updated (which is done with the `willUpdateProps`
method). This is completely a recursive process: a component is the root of a
component tree, and each sub component needs to be (virtually) rendered.
### Patching
Once a rendering is complete, it will be applied on the next animation frame.
This is done synchronously: the whole component tree is patched to the real
DOM.
## Semantics
We give here an informal description of the way components are created/updated
in an application. Here, ordered lists describe actions that are executed
sequentially, bullet lists describe actions that are executed in parallel.
**Scenario 1: initial rendering** Imagine we want to render the following component tree:
```
A
/ \
B C
/ \
D E
```
Here is what happen whenever we mount the root
component (with some code like `app.mount(document.body)`).
1. `willStart` is called on `A`
2. when it is done, template `A` is rendered.
- component `B` is created
1. `willStart` is called on `B`
2. template `B` is rendered
- component `C` is created
1. `willStart` is called on `C`
2. template `C` is rendered
- component `D` is created
1. `willStart` is called on `D`
2. template `D` is rendered
- component `E` is created
1. `willStart` is called on `E`
2. template `E` is rendered
3. each components are patched into a detached DOM element, in the following order:
`E`, `D`, `C`, `B`, `A`. (so the actual full DOM tree is created
in one pass)
4. the component `A` root element is actually appended to `document.body`
5. The method `mounted` is called recursively on all components in the following
order: `E`, `D`, `C`, `B`, `A`.
**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some
button in `C`, and this results in a state update, which is supposed to:
- update `D`,
- remove `E`,
- add new component `F`.
So, the component tree should look like this:
```
A
/ \
B C
/ \
D F
```
Here is what Owl will do:
1. because of a state change, the method `render` is called on `C`
2. template `C` is rendered again
- component `D` is updated:
1. hook `willUpdateProps` is called on `D` (async)
2. template `D` is rerendered
- component `F` is created:
1. hook `willStart` is called on `E` (async)
2. template `F` is rendered
3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
because it is not mounted yet)
4. components `F`, `D` are patched in that order
5. component `C` is patched, which will cause recursively:
1. `willUnmount` hook on `E`
2. destruction of `E`,
6. `mounted` hook is called on `F`, `patched` hooks are called on `D`, `C`
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](../tooling.md#single-file-component).
### Asynchronous Rendering
Working with asynchronous code always adds a lot of complexity to a system. Whenever
different parts of a system are active at the same time, one needs to think
carefully about all possible interactions. Clearly, this is also true for Owl
components.
There are two different common problems with Owl asynchronous rendering model:
- any component can delay the rendering (initial and subsequent) of the whole
application
- for a given component, there are two independant situations that will trigger an
asynchronous rerendering: a change in the state, or a change in the props.
These changes may be done at different times, and Owl has no way of knowing
how to reconcile the resulting renderings.
Here are a few tips on how to work with asynchronous components:
1. Minimize the use of asynchronous components!
2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
synchronous renderings
3. Lazy loading external libraries is a good use case for async rendering. This
is mostly fine, because we can assume that it will only takes a fraction of a
second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs))
4. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When
this component is met, a new rendering
sub tree is created, such that the rendering of that component (and its
children) is not tied to the rendering of the rest of the interface. It can
be used on an asynchronous component, to prevent it from delaying the
rendering of the whole interface, or on a synchronous one, such that its
rendering isn't delayed by other (asynchronous) components. Note that this
directive has no effect on the first rendering, but only on subsequent ones
(triggered by state or props changes).
```xml
<div t-name="ParentComponent">
<SyncChild />
<AsyncRoot>
<AsyncChild/>
</AsyncRoot>
</div>
```
+27
View File
@@ -0,0 +1,27 @@
# 🦉 Config 🦉
The Owl framework is designed to work in many situations. However, it is
sometimes necessary to customize some behaviour. This is done by using the
global `config` object. It currently has one key:
- [`mode`](#mode).
## Mode
By default, Owl is in _production_ mode, this means that it will try to do its
job fast, and skip some expensive operations. However, it is sometimes necessary
to have better information on what is going on, this is the purpose
of the `dev` mode.
Owl has a mode flag, in `owl.config.mode`. Its default value is `prod`, but
it can be set to `dev`:
```js
owl.config.mode = "dev";
```
Note that templates compiled with the `prod` settings will not be recompiled.
So, changing this setting is best done at startup.
An important job done by the `dev` mode is to validate props for each component
creation and update. Also, extra props will cause an error.
+9 -12
View File
@@ -28,10 +28,7 @@ context, and add it to the environment:
```js
const deviceContext = new Context({ isMobile: true });
const env = {
qweb: new QWeb(TEMPLATES),
deviceContext
};
App.env.deviceContext = deviceContext;
```
If we want to make it completely responsive, we need to update its value whenever
@@ -56,14 +53,14 @@ fact that we are in a mobile or desktop mode.
```js
class SomeComponent extends Component {
static template = xml`
<div>
<t t-if=device.isMobile>
some simplified user interface
</t>
<t t-else="1">
some more sopthisticated user interface
</t>
`;
<div>
<t t-if=device.isMobile>
some simplified user interface
</t>
<t t-else="1">
a more advanced user interface
</t>
</div>`;
device = useContext(this.env.deviceContext);
}
```
+127
View File
@@ -0,0 +1,127 @@
# 🦉 Environment 🦉
## Content
- [Overview](#overview)
- [Setting an Environment](#setting-an-environment)
- [Using a sub environment](#using-a-sub-environment)
- [Content of an Environment](#content-of-an-environment)
## Overview
An environment is an object which contains a [`QWeb` instance](qweb_engine.md). Whenever
a root component is created, it is assigned an environment (see
[below](#setting-an-environment) for more info on this). This environment is
then automatically given to each sub component (and accessible in the `this.env`
property).
```
Root
/ \
A B
```
This way, all components share the same `QWeb` instance. Owl internally requires
that the environment has a `qweb` key which maps to a
[`QWeb`](qweb_engine.md) instance. This is the QWeb instance that will be used to
render each templates in this specific component tree. Note that if no `QWeb`
instance is provided, Owl will simply generate it on the fly.
The environment is mostly static. Each application is free to add anything to
the environment, which is very useful, since this can be accessed by each sub
component.
## Setting an environment
An Owl application needs an [environment](environment.md) to be executed. The
environment has an important key: the [QWeb](qweb_engine.md) instance, which will render
all templates.
Whenever a root component `App` is mounted, Owl will setup a valid environment by
following the next steps:
- take the `env` object defined on `App.env` (if no `env` was explicitely setup,
this will be return the empty `env` object defined on `Component`)
- if `env.qweb` is not set, then Owl will create a `QWeb` instance.
The correct way to customize an environment is to simply set it up on the root
component class, before the first component is created:
```js
App.env = {
_t: myTranslateFunction,
user: {...},
services: {
...
},
};
const app = new App();
app.mount(document.body);
```
It is also possible to simply share an environment between all root components,
by simply doing this:
```js
Component.env = myEnv; // will be the default env for all components
```
Note that this environment is the global owl environment for an application. The
next section explains how to extend an environment for a specific sub component
and its children.
## Using a sub environment
It is sometimes useful to add one (or more) specific keys to the environment,
from the perspective of a specific component and its children. In that case, the
solution presented above will not work, since it sets the global environment.
There is a hook for this situation: [`useSubEnv`](hooks.md#usesubenv).
```js
class FormComponent extends Component {
constructor(parent, props) {
super(parent, props);
useSubEnv({ myKey: someValue });
}
}
```
## Content of an Environment
Some good use cases for additional keys in the environment are:
- some configuration keys,
- session information,
- generic services (such as doing rpcs, or accessing local storage).
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
For example:
```js
async function myEnv() {
const templates = await loadTemplates();
const qweb = new QWeb({ templates });
const session = getSession();
return {
_t: myTranslateFunction,
session: session,
qweb: qweb,
services: {
localStorage: localStorage,
rpc: rpc
},
debug: false,
inMobileMode: true
};
}
async function start() {
App.env = await myEnv();
const app = new App();
await app.mount(document.body);
}
```
+19 -6
View File
@@ -152,9 +152,15 @@ class SomeComponent extends Component {
}
```
In a hook, the `Component.current` static property is the reference to the
component instance that is currently being created. Hooks need to be called in
the constructor to ensure that this reference is properly set.
As you can see, the `useState` hook does not need to be given a reference to
the component. This is possible because there is a way to get a reference to the
current component: the `Component.current` static property is the reference to the
component instance that is currently being created.
Hooks need to be called in the constructor to ensure that this reference is
properly set. This is also a good thing for performance reasons (Owl can use
this to optimize its implementation), and for a clean architecture (this makes
it easier for developers to understand what is really happening in a component).
### `useState`
@@ -181,6 +187,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
@@ -287,6 +296,7 @@ class Parent extends Component {
// here, if component is mounted, refs are active:
// - this.divRef.el is the div HTMLElement
// - this.subRef.comp is the instance of the sub component
// - this.subRef.el is the root HTML node of the sub component (i.e. this.subRef.comp.el)
}
}
```
@@ -294,11 +304,14 @@ class Parent extends Component {
As shown by the example above, html elements are accessed by using the `el`
key, and components references are accessed with `comp`.
Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`.
Notes:
- if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`,
- on a component, accessing `ref.el` will get the root node of the component.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
(like the [`t-attf-`](qweb_templating_language.md#dynamic-attributes) and
`t-component` directives). For example,
```xml
+97
View File
@@ -0,0 +1,97 @@
# 🦉 Props 🦉
## Content
- [Overview](#overview)
- [Definition](#definition)
- [Good Practices](#good-practices)
- [Dynamic Props](#dynamic-props)
## Overview
In Owl, `props` (short for _properties_) is an object which contains every piece
of data given to a component by its parent.
```js
class Child extends Component {
static template = xml`<div><t t-esc="props.a"/><t t-esc="props.b"/></div>`;
}
class Parent extends Component {
static template = xml`<div><ComponentA a="state.a" b="'string'"/></div>`;
static components = { Child };
state = useState({ a: "fromparent" });
}
```
In this example, the `Child` component receives two props from its parent: `a`
and `b`. They are collected into a `props` object by Owl, with each value being
evaluated in the context of the parent. So, `props.a` is equal to `'fromparent'` and
`props.b` is equal to `'string'`.
Note that `props` is an object that only makes sense from the perspective of the
child component.
## Definition
The `props` object is made of every attributes defined on the template, with the
following exceptions:
- every attribute starting with `t-` are not props (they are QWeb directives),
- `style` and `class` attributes are excluded as well (they are applied by Owl on
the root element of the component).
In the following example:
```xml
<div>
<ComponentA a="state.a" b="'string'"/>
<ComponentB t-if="state.flag" model="model"/>
<ComponentC style="color:red;" class="left-pane" />
</div>
```
the `props` object contains the following keys:
- for `ComponentA`: `a` and `b`,
- for `ComponentB`: `model`,
- for `ComponentC`: empty object
## Good Practices
A `props` object is a collection of values that come from the parent. As such,
they are owned by the parent, and should never be modified by the child:
```js
class MyComponent extends Component {
constructor(parent, props) {
super(parent, props);
props.a.b = 43; // Never do that!!!
}
}
```
Props should be considered readonly, from the perspective of the child component.
If there is a need to modify them, then the request to update them should be
sent to the parent (for example, with an event).
Any value can go in a props. Strings, objects, classes, or even callbacks could
be given to a child component (but then, in the case of callbacks, communicating
with events seems more appropriate).
## Dynamic Props
The `t-props` directive can be used to specify totally dynamic props:
```xml
<div t-name="ParentComponent">
<Child t-props="some.obj"/>
</div>
```
```js
class ParentComponent {
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
```
+103
View File
@@ -0,0 +1,103 @@
# 🦉 Props Validation 🦉
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
- hard to tell how a component should be used, by looking at its code.
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
A props type system solves both issues, by describing the types and shapes
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 [config page](config.md#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.
For example:
```js
class ComponentA extends owl.Component {
static props = ['id', 'url'];
...
}
class ComponentB extends owl.Component {
static props = {
count: {type: Number},
messages: {
type: Array,
element: {type: Object, shape: {id: Boolean, text: 'string' }
},
date: Date,
combinedVal: [Number, Boolean]
};
...
}
```
- it is an object or a list of strings
- a list of strings is a simplified props definition, which only lists the name
of the props. Also, if the name ends with `?`, it is considered optional.
- all props are by default required, unless they are defined with `optional: true`
(in that case, validation is only done if there is a value)
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
constructor functions (so, if you have a `Person` class, it can be used as a type)
- arrays are homogeneous (all elements have the same type/shape)
For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
- a boolean: indicate that the props exists, and is mandatory.
- a constructor: this should describe the type, for example: `id: Number` describe
the props `id` as a number
- a list of constructors. In that case, this means that we allow more than one
type. For example, `id: [Number, String]` means that `id` can be either a string
or a number.
- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory):
- `type`: the main type of the prop being validated
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
- `validate`: this is a function which should return a boolean to determine if
the value is valid or not. Useful for custom validation logic.
Examples:
```js
// only the existence of those 3 keys is documented
static props = ['message', 'id', 'date'];
```
```js
// size is optional
static props = ['message', 'size?'];
```
```js
static props = {
messageIds: {type: Array, element: Number}, // list of number
otherArr: {type: Array}, // just array. no validation is made on sub elements
otherArr2: Array, // same as otherArr
someObj: {type: Object}, // just an object, no internal validation
someObj2: {
type: Object,
shape: {
id: Number,
name: {type: String, optional: true},
url: String
]}, // object, with keys id (number), name (string, optional) and url (string)
someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop
kindofsmallnumber: {
type: Number,
validate: n => (0 <= n && n <= 10)
},
size: {
validate: e => ["small", "medium", "large"].includes(e)
},
};
```
+151
View File
@@ -0,0 +1,151 @@
# 🦉 QWeb Engine 🦉
## Content
- [Overview](#overview)
- [Reference](#reference)
## Overview
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
templating engine used by Odoo. The QWeb class in the OWL project is an
implementation of that specification with a few interesting points:
- it compiles templates into functions that output a virtual DOM instead of a
string. This is necessary for the component system.
- it has a few extra directives: `t-component`, `t-on`, ...
We present in this section the engine, not the templating language.
## Reference
This section is about the javascript code that implements the `QWeb` specification.
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
instantiated:
```js
const qweb = new owl.QWeb();
```
Its API is quite simple:
- **`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: TEMPLATES, translateFn: _t });
```
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
```js
qweb.addTemplate("mytemplate", "<div>hello</div>");
```
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply
ignore templates added for a second time. Otherwise, `QWeb` will crash.
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute).
```js
const TEMPLATES = `
<templates>
<div t-name="App" class="main">main</div>
<div t-name="OtherComponent">other component</div>
</templates>`;
qweb.addTemplates(TEMPLATES);
```
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
which is a virtual representation of the DOM (see [vdom doc](../architecture/vdom.md)).
```js
const vnode = qweb.render("App", component);
```
- **`renderToString(name, context)`**: renders a template, but returns an html
string.
```js
const str = qweb.renderToString("someTemplate", somecontext);
```
- **`registerTemplate(name, template)`**: static function to register a global
QWeb template. This is useful for commonly used components accross the
application, and for making a template available to an application without
having a reference to the actual QWeb instance.
```js
QWeb.registerTemplate("mytemplate", `<div>some template</div>`);
```
- **`registerComponent(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-component` directive). This is useful for commonly used
components accross the application.
```js
class Dialog extends owl.Component { ... }
QWeb.registerComponent("Dialog", Dialog);
...
class ParentComponent extends owl.Component { ... }
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
```
In some way, a `QWeb` instance is the core of an Owl application. It is the only
mandatory element of an [environment](environment.md). As such, it
has an extra responsibility: it can act as an event bus for internal communication
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
### 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.
@@ -1,10 +1,9 @@
# 🦉 QWeb 🦉
# 🦉 QWeb Templating Language🦉
## Content
- [Overview](#overview)
- [Directives](#directives)
- [QWeb Engine](#qweb-engine)
- [Reference](#reference)
- [White Spaces](#white-spaces)
- [Root Nodes](#root-nodes)
@@ -16,18 +15,16 @@
- [Dynamic Attributes](#dynamic-attributes)
- [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates)
- [Translations](#translations)
- [Debugging](#debugging)
## Overview
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
templating engine used by Odoo. It is based on the XML format, and used
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the HTML.
Template directives are specified as XML attributes prefixed with `t-`, for instance `t-if` for conditionals, with elements and other attributes being rendered directly.
To avoid element rendering, a placeholder element `<t>` is also available, which executes its directive but doesn’t generate any output in and of itself.
```xml
<div>
<span t-if="somecondition">Some string</span>
@@ -39,131 +36,48 @@ To avoid element rendering, a placeholder element `<t>` is also available, which
</div>
```
The QWeb class in the OWL project is an implementation of that specification
with a few interesting points:
Template directives are specified as XML attributes prefixed with `t-`, for
instance `t-if` for conditionals, with elements and other attributes being
rendered directly.
- it compiles templates into functions that output a virtual DOM instead of a
string. This is necessary for the component system.
- it has a few extra directives: `t-component`, `t-on`, ...
To avoid element rendering, a placeholder element `<t>` is also available, which
executes its directive but doesn’t generate any output in and of itself.
We present in this section the templating language, including its Owl specific
extensions.
## Directives
We present here a list of all standard QWeb directives:
For reference, here is a list of all standard QWeb directives:
| Name | Description |
| ------------------------------ | ------------------------------------------------------------ |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `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-name` | [Defining a template (not really a directive)](#qweb-engine) |
| Name | Description |
| ------------------------------ | -------------------------------------------------------------- |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `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.md) |
The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
| Name | Description |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| `t-component`, `t-props`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
## QWeb Engine
This section is about the javascript code that implements the `QWeb` specification.
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
instantiated:
```js
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).
```js
const qweb = new owl.QWeb(TEMPLATES);
```
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
```js
qweb.addTemplate("mytemplate", "<div>hello</div>");
```
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply
ignore templates added for a second time. Otherwise, `QWeb` will crash.
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute).
```js
const TEMPLATES = `
<templates>
<div t-name="App" class="main">main</div>
<div t-name="OtherComponent">other component</div>
</templates>`;
qweb.addTemplates(TEMPLATES);
```
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
which is a virtual representation of the DOM (see [vdom doc](vdom.md)).
```js
const vnode = qweb.render("App", component);
```
- **`renderToString(name, context)`**: renders a template, but returns an html
string.
```js
const str = qweb.renderToString("someTemplate", somecontext);
```
- **`registerTemplate(name, template)`**: static function to register a global
QWeb template. This is useful for commonly used components accross the
application, and for making a template available to an application without
having a reference to the actual QWeb instance.
```js
QWeb.registerTemplate("mytemplate", `<div>some template</div>`);
```
- **`registerComponent(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-component` directive). This is useful for commonly used
components accross the application.
```js
class Dialog extends owl.Component { ... }
QWeb.registerComponent("Dialog", Dialog);
...
class ParentComponent extends owl.Component { ... }
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
```
In some way, a `QWeb` instance is the core of an Owl application. It is the only
mandatory element of an [environment](component.md#environment). As such, it
has an extra responsibility: it can act as an event bus for internal communication
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
| Name | Description |
| ------------------------ | ------------------------------------------------------------------------------- |
| `t-component`, `t-props` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
## Reference
We define in this section the specification of how `QWeb` templates should be
rendered. Note that we only document here the standard QWeb specification. Owl
specific extensions are documented in various other parts of the documentation.
### White Spaces
White spaces in a template are handled in a special way:
@@ -201,7 +115,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 +148,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 +357,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 +388,70 @@ into the global context.
<!-- new_variable undefined -->
```
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, 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.
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
and we render them in this template:
```xml
<p t-foreach="items" t-as="item"><t t-esc="item.text"/></p>
```
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:
- 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>
```
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 +500,21 @@ will result in :
</div>
```
### Translations
By default, QWeb specify that templates should be translated. If this behaviour
is not wanted, there is a `t-translation` directive which can turn off
translations (if it is set to the `off` value), with the following rules:
- 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`.
See [here](qweb_engine.md#translations) for more information on how to setup a
translate function in Owl QWeb.
### Debugging
The javascript QWeb implementation provides two useful debugging directives:
@@ -67,6 +67,9 @@ function makeEnvironment() {
await env.router.start();
return env;
}
App.env = makeEnvironment();
// create root component here
```
Notice that the router needs to be started. This is an asynchronous operation
+22 -7
View File
@@ -252,15 +252,30 @@ The `useStore` hook is used to select some part of the store state. It accepts
two arguments:
- a selector function, which takes the store state as first argument (and the
component props as second argument) and returns
an object or an array (which will be then observed),
- optionally, an object with a `store` key (if we want to override the default
store) and an equality function (if we want to specialize the comparison).
component props as second argument) and which must return the part of the
store state that will be made available and observed for changes,
- optionally, an object which can have the following optional keys:
- a `store` key containing a store object if we want to use another store than
the default store,
- an `isEqual` key containing an equality function if we want to specialize
the comparison (the function must accept two arguments: the previous result
and the new result, and must return whether they are equal),
- and an `onUpdate` key containing an update function if we want to execute an
arbitrary code every time the selected state changes (the function will
receive one argument, the new result, and can execute arbitrary code).
If the `useStore` callback selects a sub part of the store state, the component
If the `useStore` selector returns a sub part of the store state, the component
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.
will perform a strict equality check (unless the `isEqual` option is defined,
then it will call it) and will update the component every time this check fails.
Note that if the selector function returns a primitive type, the result of
`useStore` will be immutable and it will not react to changes. In this case, it
is important to define the `onUpdate` option to properly update the value
manually when it changes.
Also, the return value from `useStore` is not supposed to be modified. The store
state should only be updated with actions.
### `useDispatch`
+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 };
}
```
+45 -25
View File
@@ -3,10 +3,10 @@
## Content
- [Overview](#overview)
- [Development Mode](#development-mode)
- [Playground](#playground)
- [Benchmarks](#benchmarks)
- [Single File Component](#single-file-component)
- [Debugging Script](#debugging-script)
## Overview
@@ -21,26 +21,6 @@ by using a static http server. A simple python
server is available in `server.py`. There is also a npm script to start it:
`npm run tools` (and its version with a watcher: `npm run tools:watch`).
## Development Mode
By default, Owl is in _production_ mode, this means that it will try to do its
job fast, and skip some expensive operations. However, in some cases, it is
convenient to have better information on what is going on, this is the purpose
of the dev mode.
Owl has a mode flag, in `owl.__info__.mode`. Its default value is `prod`, but
it can be set to `dev`:
```js
owl.__info__.mode = "dev";
```
Note that templates compiled with the `prod` settings will not be recompiled.
So, changing this setting is best done at startup.
An important job done by the `dev` mode is to validate props for each component
creation and update. Also, extra props will cause an error.
## Playground
The playground is an important application designed to help learning and
@@ -60,12 +40,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_engine.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 +74,34 @@ 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.
## Debugging Script
## Debugging
Non trivial applications become quickly more difficult to understand. It is then
useful to have a solid understanding of what is going on. To help with that,
logging useful information is extremely valuable. There is a [javascript file](../tools/debug.js) which can be evaluated in an application.
Once it is executed, it will log a lot of information on each component main hooks. The following code is a minified version to make it easier to copy/paste:
```
let debugSetup = {
// componentBlackList: /App/, // regexp
// componentWhiteList: /SomeComponent/, // regexp
// methodBlackList: ["mounted"], // list of method names
// methodWhiteList: ["willStart"], // list of method names
logScheduler: false, // display/mute scheduler logs
logStore: true, // display/mute store logs
};
{let o,t="[OWL_DEBUG]";function toStr(o){let t=JSON.stringify(o||{});return t.length>200&&(t=t.slice(0,200)+"..."),t}function debugComponent(o,e,n){let l=`${e}<id=${n}>`,r=o=>(!debugSetup.methodBlackList||!debugSetup.methodBlackList.includes(o))&&!(debugSetup.methodWhiteList&&!debugSetup.methodWhiteList.includes(o));r("constructor")&&console.log(`${t} ${l} constructor, props=${toStr(o.props)}`),r("willStart")&&owl.hooks.onWillStart(()=>{console.log(`${t} ${l} willStart`)}),r("mounted")&&owl.hooks.onMounted(()=>{console.log(`${t} ${l} mounted`)}),r("willUpdateProps")&&owl.hooks.onWillUpdateProps(o=>{console.log(`${t} ${l} willUpdateProps, nextprops=${toStr(o)}`)}),r("willPatch")&&owl.hooks.onWillPatch(()=>{console.log(`${t} ${l} willPatch`)}),r("patched")&&owl.hooks.onPatched(()=>{console.log(`${t} ${l} patched`)}),r("willUnmount")&&owl.hooks.onWillUnmount(()=>{console.log(`${t} ${l} willUnmount`)});const s=o.__render.bind(o);o.__render=function(...o){console.log(`${t} ${l} rendering template`),s(...o)};const u=o.render.bind(o);o.render=function(...o){return console.log(`${t} ${l} render`),u(...o)};const c=o.mount.bind(o);o.mount=function(...o){return console.log(`${t} ${l} mount`),c(...o)}}if(Object.defineProperty(owl.Component,"current",{get:()=>o,set(t){o=t;const e=t.constructor.name;if(debugSetup.componentBlackList&&debugSetup.componentBlackList.test(e))return;if(debugSetup.componentWhiteList&&!debugSetup.componentWhiteList.test(e))return;let n;Object.defineProperty(o,"__owl__",{get:()=>n,set(o){debugComponent(t,e,(n=o).id)}})}}),debugSetup.logScheduler){let o;Object.defineProperty(owl.Component.scheduler,"isRunning",{get:()=>o,set(e){e?console.log(`${t} scheduler: start running tasks queue`):console.log(`${t} scheduler: stop running tasks queue`),o=e}})}if(debugSetup.logStore){let o=owl.Store.prototype.dispatch;owl.Store.prototype.dispatch=function(e,...n){return console.log(`${t} store: action '${e}' dispatched. Payload: '${toStr(n)}'`),o.call(this,e,...n)}}}
```
Note that it is certainly useful to run this code at some point in an application,
just to get a feel of what each user action implies, for the framework.
+7 -3
View File
@@ -1,8 +1,11 @@
{
"name": "owl-framework",
"version": "0.24.0",
"version": "1.0.0-alpha5",
"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",
@@ -15,7 +18,7 @@
"tools": "npm run build && npm run tools:serve",
"pretools:watch": "npm run build",
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"",
"prettier": "prettier {src/**/*.ts,tests/**/*.ts,doc/**/*.md} --write"
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write"
},
"repository": {
"type": "git",
@@ -36,12 +39,13 @@
"jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1",
"npm-run-all": "^4.1.5",
"prettier": "^1.19.1",
"rollup": "^1.6.0",
"rollup-plugin-typescript2": "^0.20.1",
"sass": "^1.16.1",
"source-map-support": "^0.5.10",
"ts-jest": "^23.10.5",
"typescript": "^3.2.2",
"typescript": "^3.7.2",
"uglify-es": "^3.3.9"
},
"jest": {
+12 -10
View File
@@ -1,23 +1,25 @@
# 🦉 OWL Roadmap 🦉
- Current version: 0.24.0
- Current version: 1.0.0-alpha5
- Status: mostly stable
This roadmap is only an attempt at predicting Owl's future. Everything may
change!
### October 2019
We plan to complete the following tasks:
- improve API for root widgets (issue #306),
- replace `t-keepalive`, and maybe `t-transition` by components (issue #295).
### November 2019
Once the previous tasks are done, release version 1.0alpha. This means that the
API should be stable. But it could change a little bit if we need it for our
work on Odoo.
Owl will be used in various Odoo projects. We plan to:
- fix any issues encountered
- maybe cleanup slightly the router API
- improve the documentation
- improve error handling, add more helpful error messages
### December 2019
If all goes well, Owl will be upgraded to beta status. From then, no API change,
even small, is expected.
### End of 2019
+156 -192
View File
@@ -1,10 +1,11 @@
import { Observer } from "../core/observer";
import { OwlEvent } from "../core/owl_event";
import { CompiledTemplate, QWeb } from "../qweb/index";
import { h, patch, VNode } from "../vdom/index";
import { patch, VNode } from "../vdom/index";
import "./directive";
import { Fiber } from "./fiber";
import "./props_validation";
import { Scheduler } from "./scheduler";
import { Scheduler, scheduler } from "./scheduler";
/**
* Owl Component System
@@ -20,8 +21,6 @@ import { Scheduler } from "./scheduler";
//------------------------------------------------------------------------------
// Types/helpers
//------------------------------------------------------------------------------
const raf = window.requestAnimationFrame.bind(window);
export const scheduler = new Scheduler(raf);
/**
* An Env (environment) is an object that will be (mostly) shared between all
@@ -46,6 +45,7 @@ interface Internal<T extends Env, Props> {
// each component has a unique id, useful mostly to handle parent/child
// relationships
readonly id: number;
depth: number;
vnode: VNode | null;
pvnode: VNode | null;
isMounted: boolean;
@@ -61,6 +61,15 @@ interface Internal<T extends Env, Props> {
cmap: { [key: number]: number };
currentFiber: Fiber | null;
// parentLastFiberId is there to help the parent component to detect, among
// its children, those that are not used anymore and thus can be destroyed
parentLastFiberId: number;
// when a rendering is initiated by a parent, it may set variables in 'scope'
// and 'vars' (typically when the component is rendered in a slot). We need to
// store that information in case the component would be re-rendered later on.
scope: any;
vars: any;
boundHandlers: { [key: number]: any };
observer: Observer | null;
@@ -88,6 +97,9 @@ export class Component<T extends Env, Props extends {}> {
static components = {};
static props?: any;
static defaultProps?: any;
static env: any = {};
// expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler;
/**
* The `el` is the root element of the component. Note that it could be null:
@@ -107,45 +119,36 @@ export class Component<T extends Env, Props extends {}> {
/**
* Creates an instance of Component.
*
* The root component of a component tree needs an environment:
*
* ```javascript
* const root = new RootComponent(env, props);
* ```
*
* Every other component simply needs a reference to its parent:
*
* ```javascript
* const child = new SomeComponent(parent, props);
* ```
*
* Note that most of the time, only the root component needs to be created by
* hand. Other components should be created automatically by the framework (with
* the t-component directive in a template)
*/
constructor(parent: Component<T, any> | T, props?: Props) {
const defaultProps = (<any>this.constructor).defaultProps;
constructor(parent?: Component<T, any> | null, props?: Props) {
Component.current = this;
let constr = this.constructor as any;
const defaultProps = constr.defaultProps;
if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps);
props = props || ({} as Props);
this.__applyDefaultProps(props, defaultProps);
}
// is this a good idea?
// Pro: if props is empty, we can create easily a component
// Con: this is not really safe
// Pro: but creating component (by a template) is always unsafe anyway
this.props = <Props>props || <Props>{};
let id: number = nextId++;
let p: Component<T, any> | null = null;
if (parent instanceof Component) {
p = parent;
this.props = <Props>props;
if (QWeb.dev) {
QWeb.utils.validateProps(constr, this.props);
}
const id: number = nextId++;
let depth;
if (parent) {
this.env = parent.env;
parent.__owl__.children[id] = this;
const __powl__ = parent.__owl__;
__powl__.children[id] = this;
depth = __powl__.depth + 1;
} 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);
// we are the root component
this.env = (this.constructor as any).env;
if (!this.env.qweb) {
this.env.qweb = new QWeb();
}
this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) {
@@ -160,19 +163,23 @@ export class Component<T extends Env, Props extends {}> {
this.env.qweb.off("update", this);
}
});
depth = 0;
}
const qweb = this.env.qweb;
const qweb = this.env.qweb;
const template = constr.template || this.__getTemplate(qweb);
this.__owl__ = {
id: id,
depth: depth,
vnode: null,
pvnode: null,
isMounted: false,
isDestroyed: false,
parent: p,
parent: parent || null,
children: {},
cmap: {},
currentFiber: null,
parentLastFiberId: 0,
boundHandlers: {},
mountedCB: null,
willUnmountCB: null,
@@ -181,9 +188,11 @@ export class Component<T extends Env, Props extends {}> {
willStartCB: null,
willUpdatePropsCB: null,
observer: null,
renderFn: qweb.render.bind(qweb, this.__getTemplate(qweb)),
renderFn: qweb.render.bind(qweb, template),
classObj: null,
refs: null
refs: null,
scope: null,
vars: null
};
}
@@ -262,8 +271,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
@@ -277,36 +289,24 @@ export class Component<T extends Env, Props extends {}> {
*
* Note that a component can be mounted an unmounted several times
*/
async mount(target: HTMLElement, renderBeforeRemount: boolean = false): Promise<void> {
async mount(target: HTMLElement | DocumentFragment): Promise<void> {
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return Promise.resolve();
}
if (__owl__.vnode && !renderBeforeRemount) {
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
}
return;
if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) {
let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`;
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message);
}
const fiber = new Fiber(null, this, this.props, undefined, undefined, false);
const fiber = new Fiber(null, this, false, target);
fiber.shouldPatch = false;
if (!__owl__.vnode) {
this.__prepareAndRender(fiber);
} else {
this.__render(fiber);
}
return new Promise(resolve => {
scheduler.addFiber(fiber, () => {
if (!__owl__.isDestroyed) {
this.__patch(fiber.vnode);
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
}
}
resolve();
});
});
return scheduler.addFiber(fiber);
}
/**
@@ -331,22 +331,34 @@ export class Component<T extends Env, Props extends {}> {
*/
async render(force: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
if (
(!__owl__.isMounted && !__owl__.currentFiber) ||
(__owl__.currentFiber && !__owl__.currentFiber.isRendered)
) {
if (!__owl__.isMounted && !__owl__.currentFiber) {
// if we get here, this means that the component was either never mounted,
// or was unmounted and some state change triggered a render. Either way,
// we do not want to actually render anything in this case.
return;
}
const fiber = new Fiber(null, this, this.props, undefined, undefined, force);
this.__render(fiber);
return new Promise(resolve => {
scheduler.addFiber(fiber.root, () => {
if (__owl__.isMounted && fiber === fiber.root) {
fiber.patchComponents();
}
resolve();
});
if (__owl__.currentFiber && !__owl__.currentFiber.isRendered) {
return scheduler.addFiber(__owl__.currentFiber.root);
}
// if we aren't mounted at this point, it implies that there is a
// currentFiber that is already rendered (isRendered is true), so we are
// about to be mounted
const isMounted = __owl__.isMounted;
const fiber = new Fiber(null, this, force, null);
Promise.resolve().then(() => {
if (__owl__.isMounted || !isMounted) {
// we are mounted (__owl__.isMounted), or if we are currently being
// mounted (!isMounted), so we call __render
this.__render(fiber);
} else {
// we were mounted when render was called, but we aren't anymore, so we
// were actually about to be unmounted ; we can thus forget about this
// fiber
fiber.isCompleted = true;
__owl__.currentFiber = null;
}
});
return scheduler.addFiber(fiber);
}
/**
@@ -386,7 +398,7 @@ export class Component<T extends Env, Props extends {}> {
*/
trigger(eventType: string, payload?: any) {
if (this.el) {
const ev = new CustomEvent(eventType, {
const ev = new OwlEvent(this, eventType, {
bubbles: true,
cancelable: true,
detail: payload
@@ -431,25 +443,19 @@ export class Component<T extends Env, Props extends {}> {
}
__owl__.isDestroyed = true;
delete __owl__.vnode;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true;
}
}
__callMounted() {
const __owl__ = this.__owl__;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
comp.__callMounted();
}
}
__owl__.isMounted = true;
try {
this.mounted();
if (__owl__.mountedCB) {
__owl__.mountedCB();
}
} catch (e) {
console.error(e); // TODO : add a test
__owl__.currentFiber = null;
this.mounted();
if (__owl__.mountedCB) {
__owl__.mountedCB();
}
}
@@ -460,6 +466,10 @@ export class Component<T extends Env, Props extends {}> {
}
this.willUnmount();
__owl__.isMounted = false;
if (this.__owl__.currentFiber) {
this.__owl__.currentFiber.isCompleted = true;
this.__owl__.currentFiber.root.counter = 0;
}
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
@@ -473,32 +483,32 @@ export class Component<T extends Env, Props extends {}> {
* The __updateProps method is called by the t-component directive whenever
* it updates a component (so, when the parent template is rerendered).
*/
async __updateProps(
nextProps: Props,
parentFiber: Fiber,
scope: any,
vars: any,
previousSibling?: Fiber | null
): Promise<void> {
async __updateProps(nextProps: Props, parentFiber: Fiber, scope: any, vars: any): Promise<void> {
this.__owl__.scope = scope;
this.__owl__.vars = vars;
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const __owl__ = this.__owl__;
const fiber = new Fiber(parentFiber, this, this.props, scope, vars, parentFiber.force);
const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
if (!parentFiber.child) {
parentFiber.child = fiber;
} else {
previousSibling!.sibling = fiber;
parentFiber.lastChild!.sibling = fiber;
}
parentFiber.lastChild = fiber;
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
this.__applyDefaultProps(nextProps, defaultProps);
}
if (QWeb.dev) {
QWeb.utils.validateProps(this.constructor, nextProps);
}
await Promise.all([
this.willUpdateProps(nextProps),
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps)
]);
if (fiber.isCancelled) {
if (fiber.isCompleted) {
return;
}
this.props = nextProps;
@@ -511,11 +521,10 @@ export class Component<T extends Env, Props extends {}> {
* Main patching method. We call the virtual dom patch method here to convert
* a virtual dom vnode into some actual dom.
*/
__patch(vnode) {
__patch(vnode: VNode) {
const __owl__ = this.__owl__;
const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
__owl__.currentFiber = null;
}
/**
@@ -523,36 +532,35 @@ export class Component<T extends Env, Props extends {}> {
* subcomponent is created. It gets its scope and vars, if any, from the
* parent template.
*/
__prepare(parentFiber: Fiber, scope: any, vars: any, previousSibling?: Fiber | null) {
const fiber = new Fiber(parentFiber, this, this.props, scope, vars, parentFiber.force);
__prepare(parentFiber: Fiber, scope: any, vars: any) {
this.__owl__.scope = scope;
this.__owl__.vars = vars;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
fiber.shouldPatch = false;
if (!parentFiber.child) {
parentFiber.child = fiber;
} else {
previousSibling!.sibling = fiber;
parentFiber.lastChild!.sibling = fiber;
}
parentFiber.lastChild = fiber;
return this.__prepareAndRender(fiber);
}
__getTemplate(qweb: QWeb): string {
let p = (<any>this).constructor;
if (!p.hasOwnProperty("_template")) {
if (p.template) {
p._template = p.template;
} else {
// here, the component and none of its superclasses defines a static `template`
// key. So we fall back on looking for a template matching its name (or
// one of its subclass).
// here, the component and none of its superclasses defines a static `template`
// key. So we fall back on looking for a template matching its name (or
// one of its subclass).
let template: string;
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
p = p.__proto__;
}
if (p === Component) {
throw new Error(`Could not find template for component "${this.constructor.name}"`);
} else {
p._template = template;
}
let template: string;
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
p = p.__proto__;
}
if (p === Component) {
throw new Error(`Could not find template for component "${this.constructor.name}"`);
} else {
p._template = template;
}
}
return p._template;
@@ -561,14 +569,13 @@ 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.vnode = h("div"); // -> we render this div at the end
fiber.handleError(e);
return Promise.resolve();
}
if (this.__owl__.isDestroyed) {
return Promise.resolve();
}
if (!fiber.isCancelled) {
if (!fiber.isCompleted) {
this.__render(fiber);
}
}
@@ -578,49 +585,45 @@ export class Component<T extends Env, Props extends {}> {
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
let vnode;
let error;
try {
vnode = __owl__.renderFn!(this, {
let vnode = __owl__.renderFn!(this, {
handlers: __owl__.boundHandlers,
fiber: fiber
});
// we iterate over the children to detect those that no longer belong to the
// current rendering: those ones, if not mounted yet, can (and have to) be
// destroyed right now, because they are not in the DOM, and thus we won't
// be notified later on (when patching), that they are removed from the DOM
for (let childKey in __owl__.children) {
let child = __owl__.children[childKey];
if (!child.__owl__.isMounted && child.__owl__.parentLastFiberId < fiber.id) {
child.destroy();
}
}
if (!vnode) {
throw new Error(`Rendering '${this.constructor.name}' did not return anything`);
}
fiber.vnode = vnode;
// we apply here the class information described on the component by the
// template (so, something like <MyComponent class="..."/>) to the actual
// root vnode
if (__owl__.classObj) {
const data = vnode.data!;
data.class = Object.assign(data.class || {}, __owl__.classObj);
}
} catch (e) {
vnode = __owl__.vnode || h("div");
errorHandler(e, fiber);
error = e;
}
fiber.vnode = vnode;
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
// we apply here the class information described on the component by the
// template (so, something like <MyComponent class="..."/>) to the actual
// root vnode
if (__owl__.classObj) {
vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj);
}
fiber.root.counter--;
fiber.isRendered = true;
}
/**
* Only called by qweb t-component directive
*/
__mount(fiber: Fiber, elm: HTMLElement): VNode {
if (fiber !== this.__owl__.currentFiber) {
fiber = this.__owl__.currentFiber!; // TODO: check if we can remove fiber arg
if (error) {
fiber.handleError(error);
}
const vnode = fiber.vnode!;
const __owl__ = this.__owl__;
if (__owl__.classObj) {
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, __owl__.classObj);
}
__owl__.vnode = patch(elm, vnode);
__owl__.currentFiber = null;
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
this.__callMounted();
}
return __owl__.vnode;
}
/**
@@ -637,52 +640,13 @@ export class Component<T extends Env, Props extends {}> {
/**
* Apply default props (only top level).
*
* Note that this method does not modify in place the props, it returns a new
* prop object
* Note that this method does modify in place the props
*/
__applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
props = props ? Object.assign({}, props) : {};
__applyDefaultProps(props: Object, defaultProps: Object) {
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
}
}
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();
}
}
+20 -61
View File
@@ -186,7 +186,7 @@ QWeb.utils.defineProxy = function defineProxy(target, source) {
QWeb.addDirective({
name: "component",
extraNames: ["props", "keepalive"],
extraNames: ["props"],
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//COMPONENT");
@@ -194,7 +194,6 @@ QWeb.addDirective({
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
// t-on- events and t-transition
@@ -223,37 +222,14 @@ 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;
ctx.addLine(`let templateId${componentID} = ${locationExpr}\` + ${k};`);
} else {
locationExpr += ctx.loopNumber ? `\${i${ctx.loopNumber}}__\`` : "`";
ctx.addLine(`let templateId${componentID} = ${locationExpr};`);
}
const templateId = `templateId${componentID}`;
const templateKey = ctx.generateTemplateKey();
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey: string = "";
@@ -263,18 +239,15 @@ QWeb.addDirective({
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
}
let transitionsInsertCode = "";
if (transition) {
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
}
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
if (ref && !keepAlive) {
let finalizeComponentCode = `w${componentID}.destroy();`;
if (ref) {
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
}
if (transition) {
finalizeComponentCode = `let finalize = () => {
${finalizeComponentCode}
};
delete w${componentID}.__owl__.transitionInserted;
utils.transitionRemove(vn, '${transition}', finalize);`;
}
@@ -346,14 +319,9 @@ QWeb.addDirective({
}
ctx.addLine(
`let w${componentID} = ${templateId} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateId}]] : false;`
`let w${componentID} = ${templateKey} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateKey}]] : false;`
);
let shouldProxy = !ctx.parentNode;
if (keepAlive) {
ctx.addLine(
`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`
);
}
if (shouldProxy) {
let id = ctx.generateID();
ctx.rootContext.rootNode = id;
@@ -404,31 +372,15 @@ QWeb.addDirective({
ctx.addIf(`w${componentID}`);
// need to update component
let patchQueueCode = keepAlive ? `fiber${componentID}` : "extra.fiber";
if (keepAlive) {
// if we have t-keepalive="1", the component could be unmounted, but then
// we __updateProps is called. This is ok, but we do not want to call
// the willPatch/patched hooks of the component in this case, so we
// 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};});`;
}
ctx.addLine(
`w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars &&
", " + scopeVars}, sibling)${styleCode};`
`w${componentID}.__updateProps(props${componentID}, extra.fiber${scopeVars &&
", " + scopeVars})${styleCode};`
);
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
let keepAliveCode = "";
if (keepAlive) {
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
ctx.addLine(keepAliveCode);
}
if (registerCode) {
ctx.addLine(registerCode);
}
@@ -454,7 +406,13 @@ QWeb.addDirective({
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
);
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
ctx.addLine(`parent.__owl__.cmap[${templateId}] = w${componentID}.__owl__.id;`);
if (transition) {
ctx.addLine(`const __patch${componentID} = w${componentID}.__patch;`);
ctx.addLine(
`w${componentID}.__patch = fiber => {__patch${componentID}.call(w${componentID}, fiber); if(!w${componentID}.__owl__.transitionInserted){w${componentID}.__owl__.transitionInserted = true;utils.transitionInsert(w${componentID}.__owl__.vnode, '${transition}');}};`
);
}
ctx.addLine(`parent.__owl__.cmap[${templateKey}] = w${componentID}.__owl__.id;`);
if (hasSlots) {
const clone = <Element>node.cloneNode(true);
@@ -481,14 +439,15 @@ QWeb.addDirective({
}
}
ctx.addLine(`let def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars}, sibling);`);
ctx.addLine(`let def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
const insertHook = refExpr ? `insert(vn) {${refExpr}},` : "";
ctx.addLine(
`let pvnode = h('dummy', {key: ${templateId}, hook: {insert(vn) { let nvn=w${componentID}.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});`
`let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});`
);
ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`);
ctx.addLine(
`def${defID}.then(function () {if (w${componentID}.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
`def${defID}.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
if (registerCode) {
ctx.addLine(registerCode);
@@ -504,7 +463,7 @@ QWeb.addDirective({
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
}
ctx.addLine(`sibling = w${componentID}.__owl__.currentFiber || sibling;`);
ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`);
return true;
}
+153 -50
View File
@@ -1,5 +1,6 @@
import { VNode } from "../vdom/index";
import { h, VNode } from "../vdom/index";
import { Component } from "./component";
import { scheduler } from "./scheduler";
/**
* Owl Fiber Class
@@ -15,13 +16,17 @@ import { Component } from "./component";
*/
export class Fiber {
static nextId: number = 1;
id: number = Fiber.nextId++;
// The force attribute determines if a rendering should bypass the `shouldUpdate`
// method potentially implemented by a component. It is usually set to false.
force: boolean;
// isCancelled means that the rendering corresponding to this fiber and its
// children is cancelled. No extra work should be done.
isCancelled: boolean = false;
// isCompleted means that the rendering corresponding to this fiber's work is
// done, either because the component has been mounted or patched, or because
// fiber has been cancelled.
isCompleted: boolean = false;
// the fibers corresponding to component updates (updateProps) need to call
// the willPatch and patched hooks from the corresponding component. However,
@@ -41,9 +46,10 @@ export class Fiber {
// scheduler.
counter: number = 0;
target: HTMLElement | null;
scope: any;
vars: any;
props: any;
component: Component<any, any>;
vnode: VNode | null = null;
@@ -51,26 +57,59 @@ export class Fiber {
root: Fiber;
child: Fiber | null = null;
sibling: Fiber | null = null;
lastChild: Fiber | null = null;
parent: Fiber | null = null;
constructor(parent: Fiber | null, component: Component<any, any>, props, scope, vars, force) {
this.force = force;
this.scope = scope;
this.vars = vars;
this.props = props;
error?: Error;
constructor(parent: Fiber | null, component: Component<any, any>, force, target) {
this.component = component;
this.force = force;
this.target = target;
const __owl__ = component.__owl__;
this.scope = __owl__.scope;
this.vars = __owl__.vars;
this.root = parent ? parent.root : this;
this.parent = parent;
let oldFiber = component.__owl__.currentFiber;
if (oldFiber && !oldFiber.isCancelled) {
this._remapFiber(oldFiber);
let oldFiber = __owl__.currentFiber;
if (oldFiber && !oldFiber.isCompleted) {
if (oldFiber.root === oldFiber && !parent) {
// both oldFiber and this fiber are root fibers
this._reuseFiber(oldFiber);
return oldFiber;
} else {
this._remapFiber(oldFiber);
}
}
this.root.counter++;
component.__owl__.currentFiber = this;
__owl__.currentFiber = this;
}
/**
* When the oldFiber is not completed yet, and both oldFiber and this fiber
* are root fibers, we want to reuse the oldFiber instead of creating a new
* one. Doing so will guarantee that the initiator(s) of those renderings will
* be notified (the promise will resolve) when the last rendering will be done.
*
* This function thus assumes that oldFiber is a root fiber.
*/
_reuseFiber(oldFiber: Fiber) {
oldFiber.cancel(); // cancel children fibers
oldFiber.isCompleted = false; // keep the root fiber alive
oldFiber.isRendered = false; // the fiber has to be re-rendered
if (oldFiber.child) {
// remove relation to children
oldFiber.child.parent = null;
oldFiber.child = null;
oldFiber.lastChild = null;
}
oldFiber.counter = 1; // re-initialize counter
oldFiber.id = Fiber.nextId++;
}
/**
@@ -81,14 +120,18 @@ export class Fiber {
*/
_remapFiber(oldFiber: Fiber) {
oldFiber.cancel();
this.shouldPatch = oldFiber.shouldPatch;
if (oldFiber === oldFiber.root) {
oldFiber.root.counter++;
oldFiber.counter++;
}
if (oldFiber.parent && !this.parent) {
// re-map links
this.parent = oldFiber.parent;
this.root = this.parent.root;
this.sibling = oldFiber.sibling;
if (this.parent.lastChild === oldFiber) {
this.parent.lastChild = this;
}
if (this.parent.child === oldFiber) {
this.parent.child = this;
} else {
@@ -131,55 +174,80 @@ export class Fiber {
}
/**
* Apply the given patch queue from a fiber.
* 1) Call 'willPatch' on the component of each patch
* 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in reverse order
* Successfully complete the work of the fiber: call the mount or patch hooks
* and patch the DOM. This function is called once the fiber and its children
* are ready, and the scheduler decides to process it.
*/
patchComponents() {
complete() {
let component = this.component;
let fiber: Fiber = this;
this.isCompleted = true;
if (!this.target && !component.__owl__.isMounted) {
return;
}
// build patchQueue
const patchQueue: Fiber[] = [];
const doWork: (Fiber) => Fiber | null = function(f) {
if (f.shouldPatch) {
patchQueue.push(f);
}
patchQueue.push(f);
return f.child;
};
this._walk(doWork);
let component: Component<any, any> = this.component;
this.shouldPatch = false;
const patchLen = patchQueue.length;
try {
// call willPatch hook on each fiber of patchQueue
for (let i = 0; i < patchLen; i++) {
component = patchQueue[i].component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
fiber = patchQueue[i];
if (fiber.shouldPatch) {
component = fiber.component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
}
component.willPatch();
}
} 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);
}
try {
// call __patch on each fiber of (reversed) patchQueue
for (let i = patchLen - 1; i >= 0; i--) {
component = patchQueue[i].component;
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode!);
if (!fiber.shouldPatch && (!fiber.target || i !== 0)) {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
}
component.__owl__.currentFiber = null;
}
// insert into the DOM (mount case)
let inDOM = false;
if (this.target) {
this.target.appendChild(this.component.el!);
inDOM = document.body.contains(this.target);
}
// call patched/mounted hook on each fiber of (reversed) patchQueue
for (let i = patchLen - 1; i >= 0; i--) {
fiber = patchQueue[i];
component = fiber.component;
if (fiber.shouldPatch && !this.target) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
} else if (this.target ? inDOM : true) {
component.__callMounted();
}
}
} catch (e) {
console.error(e);
// if there is no current fiber on component, we are in the situation where
// components were patched to the DOM, but a mounted/patched hook threw an
// error. In that case, we cannot manage the error at a lower level than
// the root fiber, since some components may not have been properly mounted
// patched yet.
const errorFiber = component.__owl__.currentFiber ? fiber : this;
errorFiber.handleError(e);
}
this.shouldPatch = true;
}
/**
@@ -190,10 +258,45 @@ export class Fiber {
if (!f.isRendered) {
f.root.counter--;
}
f.isCancelled = true;
f.isCompleted = true;
return f.child;
});
}
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 component = this.component;
this.vnode = component.__owl__.vnode || h("div");
const qweb = component.env.qweb;
let root = component;
let canCatch = false;
while (component && !(canCatch = !!component.catchError)) {
root = component;
component = component.__owl__.parent!;
}
qweb.trigger("error", error);
if (canCatch) {
// this.root.isCompleted = false
this.root.isCompleted = false;
// component.__owl__.currentFiber!.root.isCompleted = false;
component.catchError!(error);
} else {
// the 3 next lines aim to mark the root fiber as being in error, and
// to force it to end, without waiting for its children
this.root.counter = 0;
this.root.error = error;
scheduler.flush();
root.destroy();
}
}
}
+25 -6
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}')`);
}
}
@@ -40,9 +40,15 @@ QWeb.utils.validateProps = function(Widget, props: Object) {
break;
}
}
let isValid = isValidProp(props[propName], propsDef[propName]);
let isValid;
try {
isValid = isValidProp(props[propName], propsDef[propName]);
} catch (e) {
e.message = `Invalid prop '${propName}' in component ${Widget.name} (${e.message})`;
throw e;
}
if (!isValid) {
throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`);
throw new Error(`Invalid Prop '${propName}' in component '${Widget.name}'`);
}
}
for (let propName in props) {
@@ -80,17 +86,30 @@ function isValidProp(prop, propDef): boolean {
return result;
}
// propsDef is an object
let result = isValidProp(prop, propDef.type);
if (propDef.type === Array) {
if (propDef.optional && prop === undefined) {
return true;
}
let result = propDef.type ? isValidProp(prop, propDef.type) : true;
if (propDef.validate) {
result = result && propDef.validate(prop);
}
if (propDef.type === Array && propDef.element) {
for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element);
}
}
if (propDef.type === Object) {
if (propDef.type === Object && propDef.shape) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
}
if (result) {
for (let propName in prop) {
if (!(propName in shape)) {
throw new Error(`unknown prop '${propName}'`);
}
}
}
}
return result;
}
+33 -8
View File
@@ -13,7 +13,7 @@ import { Fiber } from "./fiber";
interface Task {
fiber: Fiber;
callback: () => void;
callback: (err?: Error) => void;
}
export class Scheduler {
@@ -25,12 +25,27 @@ export class Scheduler {
this.requestAnimationFrame = requestAnimationFrame;
}
addFiber(fiber, callback) {
this.tasks.push({ fiber, callback });
if (this.isRunning) {
return;
}
this.scheduleTasks();
addFiber(fiber): Promise<void> {
// if the fiber was remapped into a larger rendering fiber, it may not be a
// root fiber. But we only want to register root fibers
fiber = fiber.root;
return new Promise((resolve, reject) => {
if (fiber.error) {
return reject(fiber.error);
}
this.tasks.push({
fiber,
callback: () => {
if (fiber.error) {
return reject(fiber.error);
}
resolve();
}
});
if (!this.isRunning) {
this.scheduleTasks();
}
});
}
/**
@@ -41,10 +56,17 @@ export class Scheduler {
let tasks = this.tasks;
this.tasks = [];
tasks = tasks.filter(task => {
if (task.fiber.isCancelled) {
if (task.fiber.isCompleted) {
task.callback();
return false;
}
if (task.fiber.counter === 0) {
if (!task.fiber.error) {
task.fiber.complete();
if (!task.fiber.isCompleted) {
return true;
}
}
task.callback();
return false;
}
@@ -65,3 +87,6 @@ export class Scheduler {
});
}
}
const raf = window.requestAnimationFrame.bind(window);
export const scheduler = new Scheduler(raf);
+30
View File
@@ -0,0 +1,30 @@
import { QWeb } from "./qweb/index";
/**
* This file creates and exports the OWL 'config' object, with keys:
* - 'mode': 'prod' or 'dev',
* - 'env': the environment to use in root components.
*/
interface Config {
mode: string;
}
export const config = {} as Config;
Object.defineProperty(config, "mode", {
get() {
return QWeb.dev ? "dev" : "prod";
},
set(mode: string) {
QWeb.dev = mode === "dev";
if (QWeb.dev) {
const url = `https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode`;
console.warn(
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
);
} else {
console.log(`Owl is now running in 'prod' mode.`);
}
}
});
+72 -35
View File
@@ -1,7 +1,8 @@
import { Component, scheduler } from "./component/component";
import { Component } from "./component/component";
import { scheduler } from "./component/scheduler";
import { EventBus } from "./core/event_bus";
import { Observer } from "./core/observer";
import { onWillUnmount } from "./hooks";
/**
* The `Context` object provides a way to share data between an arbitrary number
* of component. Usually, data is passed from a parent to its children component,
@@ -12,6 +13,28 @@ import { onWillUnmount } from "./hooks";
* With a `Context` object, each component can subscribe (with the `useContext`
* hook) to its state, and will be updated whenever the context state is updated.
*/
function partitionBy<T>(arr: T[], fn: (t: T) => boolean) {
let lastGroup: T[] | false = false;
let lastValue;
return arr.reduce((acc: T[][], cur) => {
let curVal = fn(cur);
if (lastGroup) {
if (curVal === lastValue) {
lastGroup.push(cur);
} else {
lastGroup = false;
}
}
if (!lastGroup) {
lastGroup = [cur];
acc.push(lastGroup);
}
lastValue = curVal;
return acc;
}, []);
}
export class Context extends EventBus {
state: any;
observer: Observer;
@@ -22,40 +45,52 @@ export class Context extends EventBus {
constructor(state: Object = {}) {
super();
this.observer = new Observer();
this.observer.notifyCB = this.__notifyComponents.bind(this);
this.observer.notifyCB = () => {
// notify components in the next microtask tick to ensure that subscribers
// are notified only once for all changes that occur in the same micro tick
let rev = this.rev;
return Promise.resolve().then(() => {
if (rev === this.rev) {
this.__notifyComponents();
}
});
};
this.state = this.observer.observe(state);
this.subscriptions.update = [];
}
/**
* Instead of using trigger to emit an update event, we actually implement
* our own function to do that. The reason is that we need to be smarter than
* a simple trigger function: we need to wait for parent components to be
* done before doing children components. The reason is that if an update
* as an effect of destroying a children, we do not want to call the
* mapStoreToProps function of the child, nor rendering it.
* done before doing children components. More precisely, if an update
* as an effect of destroying a children, we do not want to call any code
* from the child, and certainly not render it.
*
* This method is not optimal if we have a bunch of asynchronous components:
* we wait sequentially for each component to be completed before updating the
* next. However, the only things that matters is that children are updated
* after their parents. So, this could be optimized by being smarter, and
* updating all widgets concurrently, except for parents/children.
* This method implements a simple grouping algorithm by depth. If we have
* connected components of depths [2, 4,4,4,4, 3,8,8], the Context will notify
* them in the following groups: [2], [4,4,4,4], [3], [8,8]. Each group will
* be updated sequentially, but each components in a given group will be done in
* parallel.
*
* A potential cheap way to improve this situation is to keep track of the
* depth of a component in the component tree. A root component has a depth of
* 1, then its children of 2 and so on... Then, we can update all components
* with the same depth in parallel.
* This is a very simple algorithm, but it avoids checking if a given
* component is a child of another.
*/
async __notifyComponents() {
const rev = ++this.rev;
const subs = this.subscriptions.update || [];
for (let i = 0, iLen = subs.length; i < iLen; i++) {
const sub = subs[i];
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
if (shouldCallback) {
const render = sub.callback.call(sub.owner, rev);
scheduler.flush();
await render;
}
const subscriptions = this.subscriptions.update;
const groups = partitionBy(subscriptions, s => (s.owner ? s.owner.__owl__.depth : -1));
for (let group of groups) {
const proms = group.map(sub => sub.callback.call(sub.owner, rev));
// at this point, each component in the current group has registered a
// top level fiber in the scheduler. It could happen that rendering these
// components is done (if they have no children). This is why we manually
// flush the scheduler. This will force the scheduler to check
// immediately if they are done, which will cause their rendering
// promise to resolve earlier, which means that there is a chance of
// processing the next group in the same frame.
scheduler.flush();
await Promise.all(proms);
}
}
}
@@ -81,15 +116,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;
@@ -103,9 +138,11 @@ export function useContextWithCB(ctx: Context, component: Component<any, any>, m
await method();
}
});
onWillUnmount(() => {
const __destroy = component.__destroy;
component.__destroy = parent => {
ctx.off("update", component);
delete mapping[id];
});
__destroy.call(component, parent);
};
return ctx.state;
}
+3 -17
View File
@@ -24,14 +24,6 @@ export class Observer {
weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {}
async notifyChange() {
this.dirty = true;
await Promise.resolve();
if (this.dirty) {
this.dirty = false;
this.notifyCB();
}
}
observe<T>(value: T, parent?: any): T {
if (value === null || typeof value !== "object" || value instanceof Date) {
@@ -46,10 +38,6 @@ export class Observer {
const metadata = this.weakMap.get(value);
return metadata ? metadata.rev : 0;
}
deepRevNumber(value): number {
const metadata = this.weakMap.get(value);
return metadata ? metadata.deepRev : 0;
}
_observe(value, parent) {
var self = this;
@@ -69,7 +57,7 @@ export class Observer {
}
self._updateRevNumber(target);
target[key] = newVal;
self.notifyChange();
self.notifyCB();
}
return true;
},
@@ -77,7 +65,7 @@ export class Observer {
if (key in target) {
delete target[key];
self._updateRevNumber(target);
self.notifyChange();
self.notifyCB();
}
return true;
}
@@ -87,7 +75,6 @@ export class Observer {
value,
proxy,
rev: this.rev,
deepRev: this.rev,
parent
};
@@ -99,11 +86,10 @@ export class Observer {
_updateRevNumber(target: any) {
this.rev++;
let metadata = this.weakMap.get(target);
metadata.rev!++;
let parent = target;
do {
metadata = this.weakMap.get(parent);
metadata.deepRev++;
metadata.rev++;
} while ((parent = metadata.parent) && parent !== target);
}
}
+15
View File
@@ -0,0 +1,15 @@
import { Component } from "../component/component";
/**
* We define here OwlEvent, a subclass of CustomEvent, with an additional
* attribute:
* - originalComponent: the component that triggered the event
*/
export class OwlEvent<T> extends CustomEvent<T> {
originalComponent: Component<any, any>;
constructor(component, eventType, options) {
super(eventType, options);
this.originalComponent = component;
}
}
+6 -1
View File
@@ -104,7 +104,12 @@ export function useRef(name: string): Ref {
return {
get el(): HTMLElement | null {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof HTMLElement ? val : null;
if (val instanceof HTMLElement) {
return val;
} else if (val instanceof Component) {
return val.el;
}
return null;
},
get comp(): Component<any, any> | null {
const val = __owl__.refs && __owl__.refs[name];
+4 -19
View File
@@ -7,10 +7,11 @@
import { EventBus } from "./core/event_bus";
import { Observer } from "./core/observer";
import { QWeb } from "./qweb/index";
import { config } from "./config";
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";
@@ -19,6 +20,7 @@ import { Router } from "./router/router";
export { Component } from "./component/component";
export { QWeb };
export { config };
export const Context = _context.Context;
export const useState = _hooks.useState;
@@ -27,7 +29,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,
@@ -35,20 +37,3 @@ export const hooks = Object.assign({}, _hooks, {
useStore: _store.useStore
});
export const __info__ = {};
Object.defineProperty(__info__, "mode", {
get() {
return QWeb.dev ? "dev" : "prod";
},
set(mode: string) {
QWeb.dev = mode === "dev";
if (QWeb.dev) {
const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
console.warn(
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
);
} else {
console.log(`Owl is now running in 'prod' mode.`);
}
}
});
+30 -33
View File
@@ -1,5 +1,4 @@
import { CompilationContext } from "./compilation_context";
import { QWebExprVar } from "./expression_parser";
import { QWeb } from "./qweb";
import { htmlToVDOM } from "../vdom/html_to_vdom";
@@ -29,12 +28,15 @@ QWeb.utils.getFragment = function(str: string): DocumentFragment {
QWeb.utils.htmlToVDOM = htmlToVDOM;
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) {
if (value === "0" && ctx.caller) {
qweb._compileNode(ctx.caller, ctx);
return;
if (value === "0") {
const caller = ctx.getCaller();
if (caller) {
qweb._compileNode(caller, ctx.getInliningContext());
return;
}
}
if (value.xml instanceof NodeList) {
if (value.xml instanceof NodeList && !value.id) {
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
}
@@ -71,6 +73,12 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
qweb._compileChildren(node, ctx);
}
if (value.xml instanceof NodeList && value.id) {
ctx.addElse();
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
}
}
ctx.closeIf();
}
@@ -104,22 +112,21 @@ QWeb.addDirective({
atNodeEncounter({ node, ctx }): boolean {
const variable = node.getAttribute("t-set")!;
let value = node.getAttribute("t-value")!;
ctx.variables[variable] = ctx.variables[variable] || {};
let qwebvar = ctx.variables[variable];
if (value) {
const formattedValue = ctx.formatExpression(value);
if (ctx.variables.hasOwnProperty(variable)) {
ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`);
if (ctx.variables.hasOwnProperty(variable) && qwebvar.id) {
ctx.addLine(`${qwebvar.id} = ${formattedValue}`);
} else {
const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`);
ctx.variables[variable] = {
id: varName,
expr: formattedValue
};
qwebvar.id = varName;
qwebvar.expr = formattedValue;
}
} else {
ctx.variables[variable] = {
xml: node.childNodes
};
qwebvar.xml = node.childNodes;
}
return true;
}
@@ -133,7 +140,7 @@ QWeb.addDirective({
priority: 20,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-if")!);
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : cond.id);
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : cond.id!);
return false;
},
finalize({ ctx }) {
@@ -188,11 +195,9 @@ QWeb.addDirective({
// extract variables from nodecopy
const tempCtx = new CompilationContext();
tempCtx.nextID = ctx.rootContext.nextID;
tempCtx.allowMultipleRoots = true;
qweb._compileNode(nodeCopy, tempCtx);
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
ctx.rootContext.nextID = tempCtx.nextID;
const templateMap = Object.create(ctx.templates);
// open new scope, if necessary
@@ -244,8 +249,8 @@ QWeb.addDirective({
// add new variables, if any
for (let key in tempCtx.variables) {
const v = tempCtx.variables[key];
if ((<QWebExprVar>v).expr) {
ctx.addLine(`let ${(<QWebExprVar>v).id} = ${(<QWebExprVar>v).expr};`);
if (v.expr) {
ctx.addLine(`let ${v.id} = ${v.expr};`);
}
// todo: handle XML variables...
}
@@ -294,25 +299,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();
+55 -10
View File
@@ -1,4 +1,4 @@
import { compileExpr, QWebVar, QWebExprVar } from "./expression_parser";
import { compileExpr, QWebVar } from "./expression_parser";
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------
@@ -6,7 +6,7 @@ export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------
export class CompilationContext {
nextID: number = 1;
static nextID: number = 1;
code: string[] = [];
variables: { [key: string]: QWebVar } = {};
escaping: boolean = false;
@@ -22,7 +22,6 @@ export class CompilationContext {
shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false;
shouldDefineResult: boolean = true;
shouldDefineSibling: boolean = true;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
loopNumber: number = 0;
@@ -32,8 +31,9 @@ export class CompilationContext {
hasParentWidget: boolean = false;
scopeVars: any[] = [];
currentKey: string = "";
lastNodeKey: string = ""; // temp variable to communicate to previous caller
templates: { [key: string]: boolean } = {};
callingLevel: number = 0;
inliningLevel: number = 0;
constructor(name?: string) {
this.rootContext = this;
@@ -43,8 +43,31 @@ export class CompilationContext {
}
generateID(): number {
const id = this.rootContext.nextID++;
return id;
return CompilationContext.nextID++;
}
/**
* This method generates a "template key", which is basically a unique key
* which depends on the currently set keys, and on the iteration numbers (if
* we are in a loop).
*
* Such a key is necessary when we need to associate an id to some element
* generated by a template (for example, a component)
*/
generateTemplateKey(): string {
const id = this.generateID();
let locationExpr = `\`__${this.generateID()}__`;
for (let i = 0; i < this.loopNumber - 1; i++) {
locationExpr += `\${i${i + 1}}__`;
}
if (this.currentKey) {
const k = this.currentKey;
this.addLine(`let k${id} = ${locationExpr}\` + ${k};`);
} else {
locationExpr += this.loopNumber ? `\${i${this.loopNumber}}__\`` : "`";
this.addLine(`let k${id} = ${locationExpr};`);
}
return `k${id}`;
}
generateCode(): string[] {
@@ -64,9 +87,6 @@ export class CompilationContext {
if (this.shouldDefineResult) {
this.code.unshift(" let result;");
}
if (this.shouldDefineSibling) {
this.code.unshift(" let sibling = null;");
}
if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
}
@@ -111,6 +131,10 @@ export class CompilationContext {
subContext(key: keyof CompilationContext, value: any): CompilationContext {
const newContext = Object.create(this);
newContext[key] = value;
if (key === "caller") {
newContext.callingLevel++;
newContext.inliningLevel++;
}
return newContext;
}
@@ -148,8 +172,29 @@ export class CompilationContext {
this.dedent();
this.addLine("}");
}
/**
* Recursively (inverse) fetches the `caller` of a context
* Useful to determine to which t-call a t-raw="0" refers
*/
getCaller(targetLevel?: number): Element | null {
if (targetLevel === undefined) {
targetLevel = this.inliningLevel;
}
if (targetLevel === this.callingLevel) {
return this.caller || null;
}
const proto = (this as any).__proto__;
return proto ? proto.getCaller(targetLevel) : null;
}
/**
* Marks the context with the current recursive level
* in which we are for inlining archs (t-raw="0")
*/
getInliningContext(): CompilationContext {
return this.subContext("inliningLevel", this.inliningLevel - 1);
}
getValue(val: any): QWebExprVar | string {
getValue(val: any): QWebVar | string {
return val in this.variables ? this.getValue(this.variables[val]) : val;
}
+11 -14
View File
@@ -25,7 +25,7 @@
// Misc types, constants and helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
@@ -38,17 +38,12 @@ const WORD_REPLACEMENT = {
lte: "<="
};
export interface QWebExprVar {
id: string;
expr: string;
export interface QWebVar {
id?: string;
expr?: string;
xml?: NodeList;
}
export interface QWebXMLVar {
xml: NodeList;
}
export type QWebVar = QWebExprVar | QWebXMLVar;
//------------------------------------------------------------------------------
// Tokenizer
//------------------------------------------------------------------------------
@@ -82,7 +77,9 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
")": "RIGHT_PAREN"
};
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(",");
// note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ".split(",");
type Tokenizer = (expr: string) => Token | false;
@@ -165,9 +162,9 @@ const tokenizeOperator: Tokenizer = function(expr) {
const TOKENIZERS = [
tokenizeString,
tokenizeNumber,
tokenizeOperator,
tokenizeSymbol,
tokenizeStatic,
tokenizeOperator
tokenizeStatic
];
/**
@@ -252,7 +249,7 @@ export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): str
}
if (isVar) {
if (token.value in vars && "id" in vars[token.value]) {
token.value = (<QWebExprVar>vars[token.value]).id;
token.value = vars[token.value].id!;
} else {
token.value = `context['${token.value}']`;
}
+36 -8
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};`);
@@ -103,6 +105,8 @@ QWeb.utils.transitionInsert = function(vn: VNode, name: string) {
elm.classList.add(name + "-enter");
elm.classList.add(name + "-enter-active");
elm.classList.remove(name + "-leave-active");
elm.classList.remove(name + "-leave-to");
const finalize = () => {
elm.classList.remove(name + "-enter-active");
elm.classList.remove(name + "-enter-to");
@@ -121,6 +125,9 @@ QWeb.utils.transitionRemove = function(vn: VNode, name: string, rm: () => void)
elm.classList.add(name + "-leave");
elm.classList.add(name + "-leave-active");
const finalize = () => {
if (!elm.classList.contains(name + "-leave-active")) {
return;
}
elm.classList.remove(name + "-leave-active");
elm.classList.remove(name + "-leave-to");
rm();
@@ -199,12 +206,12 @@ QWeb.addDirective({
if (!ctx.parentNode) {
ctx.rootContext.shouldDefineResult = true;
ctx.rootContext.shouldDefineUtils = true;
parentNode = `children${ctx.nextID++}`;
parentNode = `children${ctx.generateID()}`;
ctx.addLine(`let ${parentNode}= []`);
ctx.addLine(`result = {}`);
}
ctx.addLine(
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: owner}));`
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: extra.parent || owner}));`
);
if (!ctx.parentNode) {
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`);
@@ -229,7 +236,17 @@ QWeb.addDirective({
const type = node.getAttribute("type");
let handler;
let event = fullName.includes(".lazy") ? "change" : "input";
const expr = ctx.formatExpression(value);
// we keep here a reference to the "base expression" (if the expression
// is `t-model="some.expr.value", then the base expression is "some.expr").
// This is necessary so we can capture it in the handler closure.
let expr = ctx.formatExpression(value);
const index = expr.lastIndexOf(".");
const baseExpr = expr.slice(0, index);
ctx.addLine(`let expr${nodeID} = ${baseExpr};`);
expr = `expr${nodeID}.${expr.slice(index + 1)}`;
const key = ctx.generateTemplateKey();
if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
addNodeHook("create", `n.elm.value=${expr};`);
@@ -253,9 +270,20 @@ QWeb.addDirective({
}
handler = `(ev) => {${expr} = ${valueCode}}`;
}
ctx.addLine(
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`);
}
});
//------------------------------------------------------------------------------
// t-key
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "key",
priority: 45,
atNodeEncounter({ ctx, value }) {
let id = ctx.generateID();
ctx.addLine(`const nodeKey${id} = ${ctx.formatExpression(value)};`);
ctx.currentKey = `nodeKey${id}`;
}
});
+36 -16
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;
}
}
@@ -350,8 +362,7 @@ export class QWeb extends EventBus {
if (parentContext) {
ctx.templates = Object.create(parentContext.templates);
ctx.variables = Object.create(parentContext.variables);
ctx.nextID = parentContext.nextID + 1;
ctx.parentNode = parentContext.parentNode || ctx.nextID++;
ctx.parentNode = parentContext.parentNode || ctx.generateID();
ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true;
ctx.shouldDefineResult = false;
@@ -418,6 +429,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}\`});`);
@@ -438,11 +454,19 @@ export class QWeb extends EventBus {
return;
}
if (ctx !== ctx.rootContext) {
ctx = ctx.subContext("currentKey", ctx.currentKey);
}
const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
node.setAttribute("t-component", node.tagName);
} else if (node.tagName !== "t" && node.hasAttribute("t-component")) {
throw new Error(
`Directive 't-component' can only be used on <t> nodes (used on a <${node.tagName}>)`
);
}
const attributes = (<Element>node).attributes;
@@ -523,7 +547,6 @@ export class QWeb extends EventBus {
if (node.nodeName !== "t") {
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
let nodeHooks = {};
let addNodeHook = function(hook, handler) {
nodeHooks[hook] = nodeHooks[hook] || [];
@@ -617,7 +640,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 +730,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.currentKey || 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);
+35 -21
View File
@@ -77,56 +77,70 @@ export class Store extends Context {
interface SelectorOptions {
store?: Store;
isEqual?: (a: any, b: any) => boolean;
onUpdate?: (result: any) => any;
}
const isStrictEqual = (a, b) => a === b;
export function useStore(selector, options: SelectorOptions = {}): any {
const component: Component<any, any> = Component.current!;
const componentId = component.__owl__.id;
const store = options.store || (component.env.store as Store);
let result = selector(store.state, component.props);
const hashFn = store.observer.deepRevNumber.bind(store.observer);
let revNumber = hashFn(result) || result;
const isEqual = options.isEqual || isStrictEqual;
if (!store.updateFunctions[component.__owl__.id]) {
store.updateFunctions[component.__owl__.id] = [];
if (!(store instanceof Store)) {
throw new Error(`No store found when connecting '${component.constructor.name}'`);
}
const updateFunctions = store.updateFunctions[component.__owl__.id];
updateFunctions.push(function(): boolean {
let result = selector(store.state, component.props);
const hashFn = store.observer.revNumber.bind(store.observer);
let revNumber = hashFn(result);
const isEqual = options.isEqual || isStrictEqual;
if (!store.updateFunctions[componentId]) {
store.updateFunctions[componentId] = [];
}
function selectCompareUpdate(state, props): boolean {
const oldResult = result;
result = selector(store!.state, component.props);
result = selector(state, props);
const newRevNumber = hashFn(result);
if (
(newRevNumber > 0 && revNumber !== newRevNumber) ||
(newRevNumber === 0 && !isEqual(oldResult, result))
) {
if ((newRevNumber > 0 && revNumber !== newRevNumber) || !isEqual(oldResult, result)) {
revNumber = newRevNumber;
if (options.onUpdate) {
options.onUpdate(result);
}
return true;
}
return false;
}
store.updateFunctions[componentId].push(function(): boolean {
return selectCompareUpdate(store!.state, component.props);
});
useContextWithCB(store, component, function(): Promise<void> | void {
let shouldRender = false;
updateFunctions.forEach(function(updateFn) {
shouldRender = updateFn() || shouldRender;
});
for (let fn of store.updateFunctions[componentId]) {
shouldRender = fn() || shouldRender;
}
if (shouldRender) {
return component.render();
}
});
onWillUpdateProps(props => {
// FIXME: only do that if not keepalive + do it in destroy in that case
delete store.updateFunctions[component.__owl__.id];
result = selector(store.state, props);
selectCompareUpdate(store.state, props);
});
const __destroy = component.__destroy;
component.__destroy = parent => {
delete store.updateFunctions[componentId];
__destroy.call(component, parent);
};
if (typeof result !== "object") {
return result;
}
return new Proxy(result, {
get(target, k) {
return result[k];
},
set(target, k, v) {
result[k] = v;
return true;
throw new Error("Store state should only be modified through actions");
}
});
}
+2 -4
View File
@@ -20,10 +20,8 @@ function htmlToVNode(node: ChildNode): VNode {
attrs[attr.name] = attr.textContent;
}
const children: VNode[] = [];
if (node.hasChildNodes) {
for (let c of node.childNodes) {
children.push(htmlToVNode(c));
}
for (let c of node.childNodes) {
children.push(htmlToVNode(c));
}
return h((node as Element).tagName, { attrs }, children);
}
+7 -5
View File
@@ -101,7 +101,7 @@ function isVnode(vnode: any): vnode is VNode {
type KeyToIndexMap = { [key: string]: number };
type ArraysOf<T> = { [K in keyof T]: (T[K])[] };
type ArraysOf<T> = { [K in keyof T]: T[K][] };
type ModuleHooks = ArraysOf<Module>;
@@ -176,10 +176,12 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
}
vnode.elm = api.createComment(vnode.text as string);
} else if (sel !== undefined) {
const elm = (vnode.elm =
isDef(data) && isDef((i = (data as VNodeData).ns))
? api.createElementNS(i, sel)
: api.createElement(sel));
const elm =
vnode.elm ||
(vnode.elm =
isDef(data) && isDef((i = (data as VNodeData).ns))
? api.createElementNS(i, sel)
: api.createElement(sel));
for (i = 0, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode);
if (array(children)) {
for (i = 0, iLen = children.length; i < iLen; ++i) {
+70 -20
View File
@@ -7,20 +7,19 @@ exports[`animations t-transition combined with component 1`] = `
let QWeb = this.constructor;
let parent = context;
let owner = context;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let templateId3 = \`__4__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let k4 = \`__5__\`;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
w3.__updateProps(props3, extra.fiber, undefined, undefined);
let pvnode = w3.__owl__.pvnode;
c1.push(pvnode);
} else {
@@ -28,18 +27,21 @@ exports[`animations t-transition combined with component 1`] = `
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;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
const __patch3 = w3.__patch;
w3.__patch = fiber => {__patch3.call(w3, fiber); if(!w3.__owl__.transitionInserted){w3.__owl__.transitionInserted = true;utils.transitionInsert(w3.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {let finalize = () => {
w3.destroy();
};
delete w3.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
sibling = w3.__owl__.currentFiber || sibling;
w3.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
@@ -51,21 +53,20 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
let QWeb = this.constructor;
let parent = context;
let owner = context;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].display) {
//COMPONENT
let templateId3 = \`__4__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let k4 = \`__5__\`;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
w3.__updateProps(props3, extra.fiber, undefined, undefined);
let pvnode = w3.__owl__.pvnode;
c1.push(pvnode);
} else {
@@ -73,18 +74,69 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
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;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
const __patch3 = w3.__patch;
w3.__patch = fiber => {__patch3.call(w3, fiber); if(!w3.__owl__.transitionInserted){w3.__owl__.transitionInserted = true;utils.transitionInsert(w3.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {let finalize = () => {
w3.destroy();
};
delete w3.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
sibling = w3.__owl__.currentFiber || sibling;
w3.__owl__.parentLastFiberId = extra.fiber.id;
}
return vn1;
}"
`;
exports[`animations t-transition combined with t-component, remove and re-add before transitionend 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].flag) {
//COMPONENT
let k4 = \`__5__\`;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined);
let pvnode = w3.__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);
const __patch3 = w3.__patch;
w3.__patch = fiber => {__patch3.call(w3, fiber); if(!w3.__owl__.transitionInserted){w3.__owl__.transitionInserted = true;utils.transitionInsert(w3.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {let finalize = () => {
w3.destroy();
};
delete w3.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
w3.__owl__.parentLastFiberId = extra.fiber.id;
}
return vn1;
}"
@@ -94,7 +146,6 @@ exports[`animations t-transition with no delay/duration 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
@@ -115,7 +166,6 @@ exports[`animations t-transition, on a simple node (insert) 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
+81 -31
View File
@@ -1,13 +1,15 @@
import { Component, Env } from "../src/component/component";
import { QWeb } from "../src/qweb/index";
import { useState, useRef } from "../src/hooks";
import { xml } from "../src/tags";
import {
makeDeferred,
makeTestFixture,
makeTestEnv,
patchNextFrame,
renderToDOM,
unpatchNextFrame
unpatchNextFrame,
nextTick
} from "./helpers";
//------------------------------------------------------------------------------
@@ -29,6 +31,7 @@ let cssEl: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
qweb = new QWeb();
});
@@ -108,7 +111,7 @@ describe("animations", () => {
class TestWidget extends Widget {
state = useState({ hide: false });
}
const widget = new TestWidget(env);
const widget = new TestWidget();
// insert widget into the DOM
let def = makeDeferred();
@@ -151,7 +154,7 @@ describe("animations", () => {
state = useState({ hide: false });
span = useRef("span");
}
const widget = new TestWidget(env);
const widget = new TestWidget();
// insert widget into the DOM
let def = makeDeferred();
@@ -180,7 +183,7 @@ describe("animations", () => {
class Parent extends Widget {
static components = { Child: Child };
}
const widget = new Parent(env);
const widget = new Parent();
let def = makeDeferred();
var spanNode;
@@ -220,7 +223,7 @@ describe("animations", () => {
static components = { Child: Child };
state = useState({ display: true });
}
const widget = new Parent(env);
const widget = new Parent();
let def = makeDeferred();
var spanNode;
@@ -251,11 +254,11 @@ describe("animations", () => {
widget.state.display = false;
patchNextFrame(cb => {
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="__4__">blue</span></div>'
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="__5__">blue</span></div>'
);
cb();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__4__">blue</span></div>'
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__5__">blue</span></div>'
);
def.resolve();
});
@@ -283,7 +286,7 @@ describe("animations", () => {
}
}
const widget = new Parent(env);
const widget = new Parent();
await widget.mount(fixture);
let button = widget.el!.querySelector("button");
@@ -320,30 +323,23 @@ describe("animations", () => {
});
test("t-transition combined with t-component, remove and re-add before transitionend", async () => {
expect.assertions(11);
expect.assertions(12);
env.qweb.addTemplates(
`<templates>
<div t-name="Parent">
<button t-on-click="toggle">Toggle</button>
<t t-if="state.flag" t-component="Child" t-transition="chimay"/>
</div>
<span t-name="Child">blue</span>
</templates>`
);
class Child extends Widget {}
class Child extends Widget {
static template = xml`<span>blue</span>`;
}
class Parent extends Widget {
static template = xml`
<div t-name="Parent">
<t t-if="state.flag" t-component="Child" t-transition="chimay"/>
</div>`;
static components = { Child };
state = useState({ flag: false });
toggle() {
this.state.flag = !this.state.flag;
}
}
const widget = new Parent(env);
const widget = new Parent();
await widget.mount(fixture);
let button = widget.el!.querySelector("button");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
let def = makeDeferred();
let phase = "enter";
@@ -356,24 +352,78 @@ describe("animations", () => {
def.resolve();
});
// click display the span
button!.click();
// display the span
widget.state.flag = true;
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
// click to remove the span, and click again to re-add it before transitionend
def = makeDeferred();
phase = "leave";
button!.click();
widget.state.flag = false;
await def; // wait for the mocked repaint to be done
def = makeDeferred();
phase = "enter";
button!.click();
widget.state.flag = true;
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__5__">blue</span></div>');
});
test("transitionInsert is called the correct amount of times", async () => {
const oldTransitionInsert = QWeb.utils.transitionInsert;
QWeb.utils.transitionInsert = jest.fn(oldTransitionInsert);
class Child extends Widget {
static template = xml`<span>blue</span>`;
}
class Parent extends Widget {
static template = xml`
<div t-name="Parent">
<Child t-if="state.flag" t-transition="chimay"/>
</div>`;
static components = { Child };
state = useState({ flag: false });
}
patchNextFrame(cb => cb());
const widget = new Parent();
await widget.mount(fixture);
widget.state.flag = true;
await nextTick();
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__5__">blue</span></div>'
);
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__5__">blue</span></div>'
);
expect(QWeb.utils.transitionInsert).toBeCalledTimes(2);
widget.state.flag = false;
await nextTick();
widget.state.flag = true;
await nextTick();
expect(QWeb.utils.transitionInsert).toBeCalledTimes(3);
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__5__">blue</span></div>');
QWeb.utils.transitionInsert = oldTransitionInsert;
});
});
File diff suppressed because it is too large Load Diff
@@ -7,21 +7,19 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
let QWeb = this.constructor;
let parent = context;
let owner = context;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let templateId3 = \`__4__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let k4 = \`__5__\`;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props3 = {message:1};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
}
if (w3) {
utils.validateProps(w3.constructor, props3)
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
w3.__updateProps(props3, extra.fiber, undefined, undefined);
let pvnode = w3.__owl__.pvnode;
c1.push(pvnode);
} else {
@@ -29,15 +27,15 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
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();}}});
parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: k4, hook: {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; });
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
sibling = w3.__owl__.currentFiber || sibling;
w3.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
File diff suppressed because it is too large Load Diff
+567 -98
View File
@@ -15,6 +15,7 @@ let dev: boolean = false;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
dev = QWeb.dev;
QWeb.dev = true;
});
@@ -35,17 +36,32 @@ describe("props validation", () => {
static props = ["message"];
static template = xml`<div>hey</div>`;
}
class Parent extends Widget {
static components = { TestWidget };
static template = xml`<div><TestWidget /></div>`;
}
let error;
QWeb.dev = true;
expect(() => {
new TestWidget(env);
}).toThrow();
try {
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'message' (component 'TestWidget')`);
error = undefined;
QWeb.dev = false;
expect(() => {
new TestWidget(env);
}).not.toThrow();
try {
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
});
test("props: list of strings", async () => {
@@ -53,10 +69,20 @@ describe("props validation", () => {
static props = ["message"];
static template = xml`<div>hey</div>`;
}
class Parent extends Widget {
static components = { TestWidget };
static template = xml`<div><TestWidget /></div>`;
}
expect(() => {
new TestWidget(env);
}).toThrow("Missing props 'message' (component 'TestWidget')");
let error;
try {
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'message' (component 'TestWidget')`);
});
test("validate simple types", async () => {
@@ -69,23 +95,50 @@ describe("props validation", () => {
{ type: Function, ok: () => {}, ko: "1" }
];
let props;
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
get p() {
return props.p;
}
}
for (let test of Tests) {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
static props = { p: test.type };
};
Parent.components = { TestWidget };
expect(() => {
new TestWidget(env);
}).toThrow("Missing props 'p'");
let error;
props = {};
try {
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'p' (component '_a')`);
expect(() => {
new TestWidget(env, { p: test.ok });
}).not.toThrow();
error = undefined;
props = { p: test.ok };
try {
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => {
new TestWidget(env, { p: test.ko });
}).toThrow("Props 'p' of invalid type in component");
props = { p: test.ko };
try {
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component '_a'");
}
});
@@ -99,119 +152,306 @@ describe("props validation", () => {
{ type: Function, ok: () => {}, ko: "1" }
];
let props;
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
get p() {
return props.p;
}
}
for (let test of Tests) {
let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`;
let TestWidget = class extends Component<any, any> {
static props = { p: { type: test.type } };
static template = xml`<div>hey</div>`;
};
Parent.components = { TestWidget };
expect(() => {
new TestWidget(env);
}).toThrow("Missing props 'p'");
let error;
props = {};
try {
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'p' (component '_a')`);
expect(() => {
new TestWidget(env, { p: test.ok });
}).not.toThrow();
error = undefined;
props = { p: test.ok };
try {
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => {
new TestWidget(env, { p: test.ko });
}).toThrow("Props 'p' of invalid type in component");
props = { p: test.ko };
try {
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component '_a'");
}
});
test("can validate a prop with multiple types", async () => {
let TestWidget = class extends Widget {
class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`;
static props = { p: [String, Boolean] };
};
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
}
expect(() => {
new TestWidget(env, { p: "string" });
new TestWidget(env, { p: true });
}).not.toThrow();
let error;
let props;
try {
props = { p: "string" };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => {
new TestWidget(env, { p: 1 });
}).toThrow("Props 'p' of invalid type in component");
try {
props = { p: true };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
try {
props = { p: 1 };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
});
test("can validate an optional props", async () => {
let TestWidget = class extends Widget {
class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`;
static props = { p: { type: String, optional: true } };
};
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
}
expect(() => {
new TestWidget(env, { p: "hey" });
new TestWidget(env, {});
}).not.toThrow();
let error;
let props;
try {
props = { p: "key" };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => {
new TestWidget(env, { p: 1 });
}).toThrow();
try {
props = {};
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
try {
props = { p: 1 };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
});
test("can validate an array with given primitive type", async () => {
let TestWidget = class extends Widget {
class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`;
static props = { p: { type: Array, element: String } };
};
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
}
expect(() => {
new TestWidget(env, { p: [] });
new TestWidget(env, { p: ["string"] });
}).not.toThrow();
let error;
let props;
try {
props = { p: [] };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => {
new TestWidget(env, { p: [1] });
}).toThrow();
try {
props = { p: ["string"] };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => {
new TestWidget(env, { p: ["string", 1] });
}).toThrow();
try {
props = { p: [1] };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
error = undefined;
try {
props = { p: ["string", 1] };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
});
test("can validate an array with multiple sub element types", async () => {
let TestWidget = class extends Widget {
class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`;
static props = { p: { type: Array, element: [String, Boolean] } };
};
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
}
expect(() => {
new TestWidget(env, { p: [] });
new TestWidget(env, { p: ["string"] });
new TestWidget(env, { p: [false, true, "string"] });
}).not.toThrow();
let error;
let props;
try {
props = { p: [] };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => {
new TestWidget(env, { p: [true, 1] });
}).toThrow();
try {
props = { p: ["string"] };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
try {
props = { p: [false, true, "string"] };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
try {
props = { p: [true, 1] };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
});
test("can validate an object with simple shape", async () => {
let TestWidget = class extends Widget {
class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`;
static props = {
p: { type: Object, shape: { id: Number, url: String } }
};
};
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
}
expect(() => {
new TestWidget(env, { p: { id: 1, url: "url" } });
new TestWidget(env, { p: { id: 1, url: "url", extra: true } });
}).not.toThrow();
let error;
let props;
try {
props = { p: { id: 1, url: "url" } };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => {
new TestWidget(env, { p: { id: "1", url: "url" } });
}).toThrow();
try {
props = { p: { id: 1, url: "url", extra: true } };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid prop 'p' in component TestWidget (unknown prop 'extra')");
expect(() => {
new TestWidget(env, { p: { id: 1 } });
}).toThrow();
try {
props = { p: { id: "1", url: "url" } };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
error = undefined;
try {
props = { p: { id: 1 } };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
});
test("can validate recursively complicated prop def", async () => {
let TestWidget = class extends Widget {
class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`;
static props = {
p: {
@@ -222,16 +462,141 @@ describe("props validation", () => {
}
}
};
};
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
}
expect(() => {
new TestWidget(env, { p: { id: 1, url: true } });
new TestWidget(env, { p: { id: 1, url: [12] } });
}).not.toThrow();
let error;
let props;
try {
props = { p: { id: 1, url: true } };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => {
new TestWidget(env, { p: { id: 1, url: [12, true] } });
}).toThrow();
try {
props = { p: { id: 1, url: [12] } };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
try {
props = { p: { id: 1, url: [12, true] } };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
});
test("can validate optional attributes in nested sub props", () => {
class TestComponent extends Component<any, any> {
static props = {
myprop: {
type: Array,
element: {
type: Object,
shape: {
num: { type: Number, optional: true }
}
}
}
};
}
let error;
try {
QWeb.utils.validateProps(TestComponent, { myprop: [{}] });
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
try {
QWeb.utils.validateProps(TestComponent, { myprop: [{ a: 1 }] });
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(
"Invalid prop 'myprop' in component TestComponent (unknown prop 'a')"
);
});
test("can validate with a custom validator", () => {
class TestComponent extends Component<any, any> {
static props = {
size: {
validate: e => ["small", "medium", "large"].includes(e)
}
};
}
let error;
try {
QWeb.utils.validateProps(TestComponent, { size: "small" });
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
try {
QWeb.utils.validateProps(TestComponent, { size: "abcdef" });
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'size' in component 'TestComponent'");
});
test("can validate with a custom validator, and a type", () => {
const validator = jest.fn(n => 0 <= n && n <= 10);
class TestComponent extends Component<any, any> {
static props = {
n: {
type: Number,
validate: validator
}
};
}
let error;
try {
QWeb.utils.validateProps(TestComponent, { n: 3 });
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(validator).toBeCalledTimes(1);
try {
QWeb.utils.validateProps(TestComponent, { n: "str" });
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
expect(validator).toBeCalledTimes(1);
error = null;
try {
QWeb.utils.validateProps(TestComponent, { n: 100 });
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
expect(validator).toBeCalledTimes(2);
});
test("props are validated in dev mode (code snapshot)", async () => {
@@ -249,7 +614,7 @@ describe("props validation", () => {
class App extends Widget {
static components = { Child };
}
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
// need to make sure there are 2 call to update props. one at component
@@ -280,6 +645,32 @@ describe("props validation", () => {
}).toThrow();
});
test("props with type array, and no element", async () => {
class TestWidget extends Widget {
static props = { myprop: { type: Array } };
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { myprop: [1] });
}).not.toThrow();
expect(() => {
QWeb.utils.validateProps(TestWidget, { myprop: 1 });
}).toThrow(`Invalid Prop 'myprop' in component 'TestWidget'`);
});
test("props with type object, and no shape", async () => {
class TestWidget extends Widget {
static props = { myprop: { type: Object } };
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { myprop: { a: 3 } });
}).not.toThrow();
expect(() => {
QWeb.utils.validateProps(TestWidget, { myprop: false });
}).toThrow(`Invalid Prop 'myprop' in component 'TestWidget'`);
});
test("props: extra props cause an error", async () => {
class TestWidget extends Widget {
static props = ["message"];
@@ -322,17 +713,95 @@ 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(undefined, {});
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();
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();
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", () => {
test("can set default values", async () => {
class TestWidget extends Widget {
class TestWidget extends Component<any, any> {
static defaultProps = { p: 4 };
static template = xml`<div>hey</div>`;
static template = xml`<div><t t-esc="props.p"/></div>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget /></div>`;
static components = { TestWidget };
}
const w = new TestWidget(env, {});
expect(w.props.p).toBe(4);
const w = new Parent();
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
});
test("default values are also set whenever component is updated", async () => {
@@ -346,7 +815,7 @@ describe("default props", () => {
state: any = useState({ p: 1 });
}
const w = new Parent(env);
const w = new Parent();
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
@@ -367,7 +836,7 @@ describe("default props", () => {
static components = { TestWidget };
}
const w = new App(env, {});
const w = new App(undefined, {});
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>heyhey</span></div>");
});
+127 -12
View File
@@ -1,5 +1,5 @@
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick } from "./helpers";
import { Component, Env } from "../src/component/component";
import { Component } from "../src/component/component";
import { Context, useContext } from "../src/context";
import { xml } from "../src/tags";
import { useState } from "../src/hooks";
@@ -11,14 +11,13 @@ import { useState } from "../src/hooks";
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
// - a test env, necessary to create components, that is set as env
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = makeTestEnv();
});
afterEach(() => {
@@ -37,7 +36,7 @@ describe("Context", () => {
static template = xml`<div><t t-esc="contextObj.value"/></div>`;
contextObj = useContext(testContext);
}
const test = new Test(env);
const test = new Test();
await test.mount(fixture);
expect(fixture.innerHTML).toBe("<div>123</div>");
});
@@ -49,7 +48,7 @@ describe("Context", () => {
static template = xml`<div><t t-esc="contextObj.value"/></div>`;
contextObj = useContext(testContext);
}
const test = new Test(env);
const test = new Test();
await test.mount(fixture);
expect(fixture.innerHTML).toBe("<div>123</div>");
test.contextObj.value = 321;
@@ -68,7 +67,7 @@ describe("Context", () => {
static template = xml`<div><Child /><Child /></div>`;
static components = { Child };
}
const parent = new Parent(env);
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
testContext.state.value = 321;
@@ -76,6 +75,93 @@ describe("Context", () => {
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
});
test("two async components are updated in parallel", async () => {
const testContext = new Context({ value: 123 });
const def = makeDeferred();
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="contextObj.value"/></span>`;
contextObj = useContext(testContext);
async render() {
steps.push("render");
await def;
return super.render();
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child /><Child /></div>`;
static components = { Child };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
testContext.state.value = 321;
await nextTick();
expect(steps).toEqual(["render", "render"]);
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
});
test("two async components on two levels are updated (mostly) in parallel", async () => {
const testContext = new Context({ value: 123 });
const def = makeDeferred();
const steps: string[] = [];
class SlowComp extends Component<any, any> {
static template = xml`<p><t t-esc="props.value"/></p>`;
willUpdateProps() {
return def;
}
}
class Child extends Component<any, any> {
static template = xml`<span><SlowComp value="contextObj.value"/></span>`;
static components = { SlowComp };
contextObj = useContext(testContext);
render() {
steps.push("render");
return super.render();
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child /><Child /></div>`;
static components = { Child };
}
class App extends Component<any, any> {
static template = xml`<div><Child /><Parent /></div>`;
static components = { Child, Parent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><span><p>123</p></span><div><span><p>123</p></span><span><p>123</p></span></div></div>"
);
testContext.state.value = 321;
await nextTick();
expect(steps).toEqual(["render"]);
expect(fixture.innerHTML).toBe(
"<div><span><p>123</p></span><div><span><p>123</p></span><span><p>123</p></span></div></div>"
);
def.resolve();
await nextTick();
// we need to wait for an extra tick because it could happen (even though it
// is rare) that the second batch of renderings is not done yet, because
// the initial promise has been given to the macrotask queue, so a small
// delay happens.
await nextTick();
expect(steps).toEqual(["render", "render", "render"]);
expect(fixture.innerHTML).toBe(
"<div><span><p>321</p></span><div><span><p>321</p></span><span><p>321</p></span></div></div>"
);
});
test("one components can subscribe twice to same context", async () => {
const testContext = new Context({ a: 1, b: 2 });
const steps: string[] = [];
@@ -93,7 +179,7 @@ describe("Context", () => {
static template = xml`<div><Child /></div>`;
static components = { Child };
}
const parent = new Parent(env);
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
expect(steps).toEqual(["child"]);
@@ -124,7 +210,7 @@ describe("Context", () => {
return super.__render(fiber);
}
}
const parent = new Parent(env);
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span>321</div>");
expect(steps).toEqual(["parent", "child"]);
@@ -158,7 +244,7 @@ describe("Context", () => {
return super.__render(fiber);
}
}
const parent = new Parent(env);
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
expect(steps).toEqual(["parent", "child"]);
@@ -174,6 +260,35 @@ describe("Context", () => {
expect(testContext.subscriptions.update.length).toBe(0);
});
test("destroyed component before being mounted is inactive", async () => {
const testContext = new Context({ a: 123 });
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
contextObj = useContext(testContext);
willStart() {
return makeDeferred();
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: true });
}
const parent = new Parent();
const prom = parent.mount(fixture);
await nextTick(); // wait for Child to be instantiated
expect(testContext.subscriptions.update.length).toBe(1);
parent.state.flag = false;
await prom;
expect(fixture.innerHTML).toBe("<div></div>");
// kind of whitebox...
// we make sure we do not have any pending subscriptions to the 'update'
// event
expect(testContext.subscriptions.update.length).toBe(0);
});
test("concurrent renderings", async () => {
const testContext = new Context({ x: { n: 1 }, key: "x" });
const def = makeDeferred();
@@ -202,12 +317,12 @@ describe("Context", () => {
context = useContext(testContext);
}
const component = new ComponentA(env);
const component = new ComponentA();
await component.mount(fixture);
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();
+10 -44
View File
@@ -8,13 +8,11 @@ describe("observer", () => {
expect(typeof obj).toBe("object");
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
const obj2: any = observer.observe({ a: 1 });
expect(observer.revNumber(obj2)).toBe(1);
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
obj2.a = 2;
@@ -33,23 +31,19 @@ describe("observer", () => {
const obj: any = observer.observe({ a: null, b: undefined });
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
obj.a = 3;
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
obj.b = 5;
expect(observer.revNumber(obj)).toBe(3);
expect(observer.deepRevNumber(obj)).toBe(3);
expect(observer.rev).toBe(3);
obj.a = null;
obj.b = undefined;
expect(observer.revNumber(obj)).toBe(5);
expect(observer.deepRevNumber(obj)).toBe(5);
expect(observer.rev).toBe(5);
expect(obj).toEqual({
a: null,
@@ -63,7 +57,6 @@ describe("observer", () => {
const obj: any = observer.observe({ date });
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
expect(typeof obj.date.getFullYear()).toBe("number");
expect(obj.date).toBe(date);
@@ -71,7 +64,6 @@ describe("observer", () => {
obj.date = new Date();
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
expect(obj.date).not.toBe(date);
});
@@ -82,14 +74,11 @@ describe("observer", () => {
expect(Array.isArray(obj.arr)).toBe(true);
expect(observer.revNumber(obj.arr)).toBe(1);
expect(observer.deepRevNumber(obj.arr)).toBe(1);
expect(observer.rev).toBe(1);
obj.arr[0] = "nope";
expect(observer.revNumber(obj.arr)).toBe(2);
expect(observer.deepRevNumber(obj.arr)).toBe(2);
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.revNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
});
@@ -98,24 +87,20 @@ describe("observer", () => {
const obj: any = observer.observe({ a: 1 });
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
obj.a = 2;
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
// same value again
obj.a = 2;
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
obj.a = 3;
expect(observer.revNumber(obj)).toBe(3);
expect(observer.deepRevNumber(obj)).toBe(3);
expect(observer.rev).toBe(3);
});
@@ -126,19 +111,16 @@ describe("observer", () => {
expect(Array.isArray(arr)).toBe(true);
expect(arr.length).toBe(0);
expect(observer.revNumber(arr)).toBe(1);
expect(observer.deepRevNumber(arr)).toBe(1);
expect(observer.rev).toBe(1);
arr.push(1);
expect(observer.revNumber(arr)).toBe(2);
expect(observer.deepRevNumber(arr)).toBe(2);
expect(observer.rev).toBe(2);
expect(arr.length).toBe(1);
expect(arr).toEqual([1]);
arr.splice(1, 0, "hey");
expect(observer.revNumber(arr)).toBe(3);
expect(observer.deepRevNumber(arr)).toBe(3);
expect(observer.rev).toBe(3);
expect(arr).toEqual([1, "hey"]);
expect(arr.length).toBe(2);
@@ -146,7 +128,6 @@ describe("observer", () => {
arr.unshift("lindemans");
//it generates 3 primitive operations
expect(observer.revNumber(arr)).toBe(6);
expect(observer.deepRevNumber(arr)).toBe(6);
expect(observer.rev).toBe(6);
expect(arr).toEqual(["lindemans", 1, "hey"]);
expect(arr.length).toBe(3);
@@ -154,21 +135,18 @@ describe("observer", () => {
arr.reverse();
//it generates 2 primitive operations
expect(observer.revNumber(arr)).toBe(8);
expect(observer.deepRevNumber(arr)).toBe(8);
expect(observer.rev).toBe(8);
expect(arr).toEqual(["hey", 1, "lindemans"]);
expect(arr.length).toBe(3);
arr.pop(); // one set, one delete
expect(observer.revNumber(arr)).toBe(10);
expect(observer.deepRevNumber(arr)).toBe(10);
expect(observer.rev).toBe(10);
expect(arr).toEqual(["hey", 1]);
expect(arr.length).toBe(2);
arr.shift(); // 2 sets, 1 delete
expect(observer.revNumber(arr)).toBe(13);
expect(observer.deepRevNumber(arr)).toBe(13);
expect(observer.rev).toBe(13);
expect(arr).toEqual([1]);
expect(arr.length).toBe(1);
@@ -187,8 +165,7 @@ describe("observer", () => {
arr[0].kriek = 6;
expect(observer.rev).toBe(3);
expect(observer.revNumber(arr)).toBe(2);
expect(observer.deepRevNumber(arr)).toBe(3);
expect(observer.revNumber(arr)).toBe(3);
expect(observer.revNumber(arr[0])).toBe(3);
});
@@ -238,7 +215,6 @@ describe("observer", () => {
expect(observer.rev).toBe(1);
expect(observer.revNumber(state)).toBe(1);
expect(observer.deepRevNumber(state)).toBe(1);
expect(observer.notifyCB).toBeCalledTimes(0);
state[1] = "b";
@@ -247,7 +223,6 @@ describe("observer", () => {
expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(2);
expect(observer.deepRevNumber(state)).toBe(2);
expect(observer.notifyCB).toBeCalledTimes(1);
expect(state).toEqual(["a", "b"]);
@@ -259,13 +234,11 @@ describe("observer", () => {
expect(observer.rev).toBe(1);
expect(observer.revNumber(state.arr)).toBe(1);
expect(observer.deepRevNumber(state.arr)).toBe(1);
expect(state.arr.length).toBe(0);
state.arr.push(1);
expect(observer.rev).toBe(2);
expect(observer.revNumber(state.arr)).toBe(2);
expect(observer.deepRevNumber(state.arr)).toBe(2);
expect(state.arr.length).toBe(1);
});
@@ -280,7 +253,7 @@ describe("observer", () => {
state.arr[0].something = 2;
expect(observer.rev).toBe(2);
expect(observer.revNumber(state.arr)).toBe(1);
expect(observer.revNumber(state.arr)).toBe(2);
expect(observer.revNumber(state.arr[0])).toBe(2);
});
@@ -294,7 +267,7 @@ describe("observer", () => {
state.a.b = 2;
expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(1);
expect(observer.revNumber(state)).toBe(2);
expect(observer.revNumber(state.a)).toBe(2);
});
@@ -312,7 +285,7 @@ describe("observer", () => {
expect(observer.revNumber(obj.a)).toBe(2);
obj.a.b = 3;
expect(observer.rev).toBe(3);
expect(observer.revNumber(obj)).toBe(2);
expect(observer.revNumber(obj)).toBe(3);
expect(observer.revNumber(obj.a)).toBe(3);
});
@@ -320,22 +293,18 @@ describe("observer", () => {
const observer = new Observer();
const state: any = observer.observe({ o: { a: 1 }, arr: [1], n: 13 });
expect(observer.revNumber(state)).toBe(1);
expect(observer.deepRevNumber(state)).toBe(1);
state.o.a = 2;
expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(1);
expect(observer.deepRevNumber(state)).toBe(2);
expect(observer.revNumber(state)).toBe(2);
state.arr.push(2);
expect(observer.rev).toBe(3);
expect(observer.revNumber(state)).toBe(1);
expect(observer.deepRevNumber(state)).toBe(3);
expect(observer.revNumber(state)).toBe(3);
state.n = 155;
expect(observer.rev).toBe(4);
expect(observer.revNumber(state)).toBe(2);
expect(observer.deepRevNumber(state)).toBe(4);
expect(observer.revNumber(state)).toBe(4);
});
test("properly handle already observed state", () => {
@@ -361,18 +330,15 @@ describe("observer", () => {
const obj: any = observer.observe({});
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
obj.aku = "always finds annoying problems";
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
obj.aku = "always finds good problems";
expect(observer.revNumber(obj)).toBe(3);
expect(observer.deepRevNumber(obj)).toBe(3);
expect(observer.rev).toBe(3);
});
@@ -416,7 +382,7 @@ describe("observer", () => {
expect(observer.revNumber(obj2)).toBe(1);
obj2.key = 3;
expect(observer.revNumber(obj1)).toBe(1);
expect(observer.revNumber(obj1)).toBe(2);
expect(observer.revNumber(obj2)).toBe(2);
});
@@ -442,7 +408,7 @@ describe("observer", () => {
obj.a = 111;
obj.f = 222;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(4);
expect(observer.notifyCB).toBeCalledTimes(5);
});
test("throw error when state is mutated in object if allowMutation=false", async () => {
+124 -72
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,127 @@ 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", "tools/debug.js"];
export function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean {
if (link.link.startsWith("http")) {
// no check on external links
return true;
}
// Step 1: extract path, name, hash
// path = ['doc', 'architecture]
// name = 'rendering.md'
// hash = 'blabla' (or '' if no hash)
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)) {
return false;
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 {
const file = parts[0] === "" ? current : files.find(f => f.name === fullName);
if (!file) {
return false;
}
if (!file.sections.find(s => s.slug === parts[1])) {
// 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;
}
// 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;
}
}
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 +161,23 @@ function slugify(str) {
.replace(/-+$/, ""); // Trim - from end of text
}
function readDocData(files: string[]): FileData[] {
const result: FileData[] = [];
//--------------------------------------------------------------------------
// Test
//--------------------------------------------------------------------------
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] });
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++;
}
} 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;
}
expect(invalidLinkNumber).toBe(0);
expect(linkNumber).toBeGreaterThan(10);
});
+4 -1
View File
@@ -1,5 +1,7 @@
import { Env, scheduler } from "../src/component/component";
import { Env } from "../src/component/component";
import { scheduler } from "../src/component/scheduler";
import { EvalContext, QWeb } from "../src/qweb/qweb";
import { CompilationContext } from "../src/qweb/compilation_context";
import { patch } from "../src/vdom";
import "../src/qweb/base_directives";
import "../src/qweb/extensions";
@@ -19,6 +21,7 @@ let TEMPLATES;
beforeEach(() => {
nextSlotId = QWeb.nextSlotId;
CompilationContext.nextID = 1;
slots = Object.assign({}, QWeb.slots);
nextId = QWeb.nextId;
TEMPLATES = Object.assign({}, QWeb.TEMPLATES);
+144 -42
View File
@@ -28,6 +28,7 @@ let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
});
afterEach(() => {
@@ -44,7 +45,7 @@ describe("hooks", () => {
static template = xml`<div><t t-esc="counter.value"/></div>`;
counter = useState({ value: 42 });
}
const counter = new Counter(env);
const counter = new Counter();
await counter.mount(fixture);
expect(fixture.innerHTML).toBe("<div>42</div>");
counter.counter.value = 3;
@@ -64,12 +65,12 @@ describe("hooks", () => {
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
constructor() {
super();
useMyHook();
}
}
const component = new MyComponent(env);
const component = new MyComponent();
await component.mount(fixture);
expect(component).not.toHaveProperty("mounted");
expect(component).not.toHaveProperty("willUnmount");
@@ -92,8 +93,8 @@ describe("hooks", () => {
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
constructor(parent, props) {
super(parent, props);
useMyHook();
}
}
@@ -103,7 +104,7 @@ describe("hooks", () => {
static components = { MyComponent };
state = useState({ flag: true });
}
const parent = new Parent(env);
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
expect(steps).toEqual(["mounted"]);
@@ -126,8 +127,8 @@ describe("hooks", () => {
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
constructor() {
super();
useMyHook();
}
mounted() {
@@ -137,7 +138,7 @@ describe("hooks", () => {
steps.push("comp:willunmount");
}
}
const component = new MyComponent(env);
const component = new MyComponent();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.unmount();
@@ -157,8 +158,8 @@ describe("hooks", () => {
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
constructor(parent, props) {
super(parent, props);
useMyHook();
}
mounted() {
@@ -175,7 +176,7 @@ describe("hooks", () => {
state = useState({ flag: true });
}
const parent = new Parent(env);
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
parent.state.flag = false;
@@ -197,13 +198,13 @@ describe("hooks", () => {
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
constructor() {
super();
useMyHook(1);
useMyHook(2);
}
}
const component = new MyComponent(env);
const component = new MyComponent();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.unmount();
@@ -226,9 +227,12 @@ describe("hooks", () => {
(this.button.el as HTMLButtonElement).innerHTML = String(this.value);
}
}
const counter = new Counter(env);
const counter = new Counter();
expect(counter.button.el).toBe(null);
await counter.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>0</button></div>");
expect(counter.button.el).not.toBe(null);
expect(counter.button.el).toBe(fixture.querySelector("button"));
counter.increment();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
@@ -247,7 +251,7 @@ describe("hooks", () => {
expect(this.spanRef.el).toBeNull();
}
}
const component = new TestRef(env);
const component = new TestRef();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>owl</span></div>");
component.state.flag = false;
@@ -255,6 +259,104 @@ describe("hooks", () => {
expect(fixture.innerHTML).toBe("<div></div>");
});
test("t-refs on widget are components", async () => {
class WidgetB extends Component<any, any> {
static template = xml`<div>b</div>`;
}
class WidgetC extends Component<any, any> {
static template = xml`<div class="outer-div">Hello<WidgetB t-ref="mywidgetb" /></div>`;
static components = { WidgetB };
ref = useRef("mywidgetb");
}
const widget = new WidgetC();
expect(widget.ref.comp).toBe(null);
expect(widget.ref.el).toBe(null);
await widget.mount(fixture);
expect(widget.ref.comp).toBeInstanceOf(WidgetB);
expect(widget.ref.el).toEqual(fixture.querySelector(".outer-div > div"));
});
test("t-refs are bound at proper timing", async () => {
expect.assertions(2);
class Widget extends Component<any, any> {
static template = xml`<div>widget</div>`;
}
class ParentWidget extends Component<any, any> {
static template = xml`
<div>
<t t-foreach="state.list" t-as="elem" t-ref="child" t-key="elem" t-component="Widget"/>
</div>
`;
static components = { Widget };
state = useState({ list: <any>[] });
child = useRef("child");
willPatch() {
expect(this.child.comp).toBeNull();
}
patched() {
expect(this.child.comp).not.toBeNull();
}
}
const parent = new ParentWidget();
await parent.mount(fixture);
parent.state.list.push(1);
await nextTick();
});
test("t-refs are bound at proper timing (2)", async () => {
expect.assertions(10);
class Widget extends Component<any, any> {
static template = xml`<div>widget</div>`;
}
class ParentWidget extends Component<any, any> {
static template = xml`
<div>
<t t-if="state.child1" t-ref="child1" t-component="Widget"/>
<t t-if="state.child2" t-ref="child2" t-component="Widget"/>
</div>`;
static components = { Widget };
state = useState({ child1: true, child2: false });
child1 = useRef("child1");
child2 = useRef("child2");
count = 0;
mounted() {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeNull();
}
willPatch() {
if (this.count === 0) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeNull();
}
if (this.count === 1) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeDefined();
}
}
patched() {
if (this.count === 0) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeDefined();
}
if (this.count === 1) {
expect(this.child1.comp).toBeNull();
expect(this.child2.comp).toBeDefined();
}
this.count++;
}
}
const parent = new ParentWidget();
await parent.mount(fixture);
parent.state.child2 = true;
await nextTick();
parent.state.child1 = false;
await nextTick();
});
test("can use onPatched, onWillPatch", async () => {
const steps: string[] = [];
function useMyHook() {
@@ -270,13 +372,13 @@ describe("hooks", () => {
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
state = useState({ flag: true });
constructor(env) {
super(env);
constructor() {
super();
useMyHook();
}
}
const component = new MyComponent(env);
const component = new MyComponent();
await component.mount(fixture);
expect(component).not.toHaveProperty("patched");
expect(component).not.toHaveProperty("willPatch");
@@ -304,8 +406,8 @@ describe("hooks", () => {
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
state = useState({ flag: true });
constructor(env) {
super(env);
constructor() {
super();
useMyHook();
}
willPatch() {
@@ -315,7 +417,7 @@ describe("hooks", () => {
steps.push("comp:patched");
}
}
const component = new MyComponent(env);
const component = new MyComponent();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.state.flag = false;
@@ -337,13 +439,13 @@ describe("hooks", () => {
class MyComponent extends Component<any, any> {
static template = xml`<div>hey<t t-esc="state.value"/></div>`;
state = useState({ value: 1 });
constructor(env) {
super(env);
constructor() {
super();
useMyHook(1);
useMyHook(2);
}
}
const component = new MyComponent(env);
const component = new MyComponent();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey1</div>");
component.state.value++;
@@ -377,13 +479,13 @@ describe("hooks", () => {
<input t-ref="input2"/>
</div>`;
constructor(env) {
super(env);
constructor() {
super();
useAutofocus("input2");
}
}
const component = new SomeComponent(env);
const component = new SomeComponent();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><input></div>");
const input2 = fixture.querySelectorAll("input")[1];
@@ -399,13 +501,13 @@ describe("hooks", () => {
</div>`;
state = useState({ flag: false });
constructor(env) {
super(env);
constructor() {
super();
useAutofocus("input2");
}
}
const component = new SomeComponent(env);
const component = new SomeComponent();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input></div>");
expect(document.activeElement).toBe(document.body);
@@ -420,12 +522,12 @@ describe("hooks", () => {
test("can use sub env", async () => {
class TestComponent extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor(env) {
super(env);
constructor() {
super();
useSubEnv({ val: 3 });
}
}
const component = new TestComponent(env);
const component = new TestComponent();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(env).not.toHaveProperty("val");
@@ -435,8 +537,8 @@ describe("hooks", () => {
test("parent and child env", async () => {
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor(env) {
super(env);
constructor(parent, props) {
super(parent, props);
useSubEnv({ val: 5 });
}
}
@@ -444,12 +546,12 @@ describe("hooks", () => {
class Parent extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/><Child/></div>`;
static components = { Child };
constructor(env) {
super(env);
constructor() {
super();
useSubEnv({ val: 3 });
}
}
const component = new Parent(env);
const component = new Parent();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3<div>5</div></div>");
});
@@ -478,7 +580,7 @@ describe("hooks", () => {
state = useState({ value: 1 });
}
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(app).not.toHaveProperty("willStart");
expect(app).not.toHaveProperty("willUpdateProps");
+6 -7
View File
@@ -2,7 +2,7 @@ import { AsyncRoot } from "../../src/misc/async_root";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeDeferred, makeTestFixture, makeTestEnv, nextTick } from "../helpers";
import { Env, Component } from "../../src/component/component";
import { Component } from "../../src/component/component";
//------------------------------------------------------------------------------
// Setup and helpers
@@ -11,14 +11,13 @@ import { Env, Component } from "../../src/component/component";
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
// - a test env, necessary to create components, that is set as env
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = makeTestEnv();
});
afterEach(() => {
@@ -55,7 +54,7 @@ describe("Asyncroot", () => {
}
}
const parent = new Parent(env);
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0</span><span>0</span>");
@@ -103,7 +102,7 @@ describe("Asyncroot", () => {
}
}
const parent = new Parent(env);
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0</span><span>0</span>");
@@ -158,7 +157,7 @@ describe("Asyncroot", () => {
}
}
const parent = new Parent(env);
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0/0</span><span>0/0</span>");
File diff suppressed because it is too large Load Diff
+162 -12
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
@@ -241,6 +242,32 @@ describe("t-set", () => {
);
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe("<div>45</div>");
});
test("t-set, t-if, and mix of expression/body lookup, 1", () => {
qweb.addTemplate(
"test",
`<div>
<t t-if="flag" t-set="ourvar">1</t>
<t t-else="" t-set="ourvar" t-value="0"></t>
<t t-esc="ourvar"/>
</div>`
);
expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>");
expect(renderToString(qweb, "test", { flag: false })).toBe("<div>0</div>");
});
test("t-set, t-if, and mix of expression/body lookup, 2", () => {
qweb.addTemplate(
"test",
`<div>
<t t-if="flag" t-set="ourvar" t-value="1"></t>
<t t-else="" t-set="ourvar">0</t>
<t t-esc="ourvar"/>
</div>`
);
expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>");
expect(renderToString(qweb, "test", { flag: false })).toBe("<div>0</div>");
});
});
describe("t-if", () => {
@@ -646,6 +673,40 @@ describe("t-call (template calling", () => {
expect(renderToString(qweb, "main")).toBe(expected);
});
test("cascading t-call t-raw='0'", () => {
qweb.addTemplates(`
<templates>
<div t-name="finalTemplate">
<span>cascade 2</span>
<t t-raw="0"/>
</div>
<div t-name="subSubTemplate">
<t t-call="finalTemplate">
<span>cascade 1</span>
<t t-raw="0"/>
</t>
</div>
<div t-name="SubTemplate">
<t t-call="subSubTemplate">
<span>cascade 0</span>
<t t-raw="0"/>
</t>
</div>
<div t-name="main">
<t t-call="SubTemplate">
<span>hey</span> <span>yay</span>
</t>
</div>
</templates>
`);
const expected =
"<div><div><div><div><span>cascade 2</span><span>cascade 1</span><span>cascade 0</span><span>hey</span> <span>yay</span></div></div></div></div>";
expect(renderToString(qweb, "main")).toBe(expected);
});
test("recursive template, part 1", () => {
qweb.addTemplates(`
<templates>
@@ -659,7 +720,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 +748,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 +775,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 +898,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 +1108,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 +1122,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);
@@ -1193,7 +1259,10 @@ describe("t-on", () => {
);
const steps: string[] = [];
const owner = {
projects: [{ id: 1, name: "Project 1" }, { id: 2, name: "Project 2" }],
projects: [
{ id: 1, name: "Project 1" },
{ id: 2, name: "Project 2" }
],
onEdit(projectId, ev) {
expect(ev.defaultPrevented).toBe(true);
@@ -1223,7 +1292,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 +1372,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 +1586,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]);
});
});
+9 -1
View File
@@ -27,7 +27,10 @@ describe("tokenizer", () => {
{ type: "VALUE", value: "2" },
{ type: "RIGHT_BRACE", value: "}" }
]);
expect(tokenize("a,")).toEqual([{ type: "SYMBOL", value: "a" }, { type: "COMMA", value: "," }]);
expect(tokenize("a,")).toEqual([
{ type: "SYMBOL", value: "a" },
{ type: "COMMA", value: "," }
]);
expect(tokenize("][")).toEqual([
{ type: "RIGHT_BRACKET", value: "]" },
{ type: "LEFT_BRACKET", value: "[" }
@@ -43,6 +46,10 @@ describe("tokenizer", () => {
{ type: "OPERATOR", value: "!==" },
{ type: "OPERATOR", value: "!=" }
]);
expect(tokenize("typeof a")).toEqual([
{ type: "OPERATOR", value: "typeof " },
{ type: "SYMBOL", value: "a" }
]);
});
test("strings", () => {
@@ -112,6 +119,7 @@ describe("expression evaluation", () => {
expect(compileExpr("!flag", {})).toBe("!context['flag']");
expect(compileExpr("-3", {})).toBe("-3");
expect(compileExpr("-a", {})).toBe("-context['a']");
expect(compileExpr("typeof a", {})).toBe("typeof context['a']");
});
test("various binary operators", () => {
+10 -11
View File
@@ -5,18 +5,17 @@ exports[`Link component can render simple cases 1`] = `
) {
let utils = this.constructor.utils;
let owner = context;
let sibling = null;
var h = this.h;
let _1 = utils.toObj({'router-link-active':context['isActive']});
var _2 = context['href'];
let c3 = [], p3 = {key:3,attrs:{href: _2},class:_1,on:{}};
var vn3 = h('a', p3, c3);
extra.handlers['click' + 3] = extra.handlers['click' + 3] || function (e) {const fn = context['navigate'];if (fn) { fn.call(owner, e); } else { context.navigate; }};
p3.on['click'] = extra.handlers['click' + 3];
const slot4 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot4) {
slot4.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c3, vars: extra.vars, parent: owner}));
let _6 = utils.toObj({'router-link-active':context['isActive']});
var _7 = context['href'];
let c8 = [], p8 = {key:8,attrs:{href: _7},class:_6,on:{}};
var vn8 = h('a', p8, c8);
extra.handlers['click' + 8] = extra.handlers['click' + 8] || function (e) {const fn = context['navigate'];if (fn) { fn.call(owner, e); } else { context.navigate; }};
p8.on['click'] = extra.handlers['click' + 8];
const slot9 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot9) {
slot9.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c8, vars: extra.vars, parent: extra.parent || owner}));
}
return vn3;
return vn8;
}"
`;
@@ -7,39 +7,38 @@ exports[`RouteComponent can render simple cases 1`] = `
let QWeb = this.constructor;
let parent = context;
let owner = context;
let sibling = null;
let result;
var h = this.h;
if (context['routeComponent']) {
const nodeKey6 = 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 vn5 = {};
result = vn5;
let props2 = Object.assign({}, context['env'].router.currentParams);
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
let k9 = \`__10__\` + nodeKey6;
let w8 = k9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k9]] : false;
let vn11 = {};
result = vn11;
let props8 = Object.assign({}, context['env'].router.currentParams);
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
w8.destroy();
w8 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined, undefined, sibling);
let pvnode = w2.__owl__.pvnode;
utils.defineProxy(vn5, pvnode);
if (w8) {
w8.__updateProps(props8, extra.fiber, undefined, undefined);
let pvnode = w8.__owl__.pvnode;
utils.defineProxy(vn11, 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; });
utils.defineProxy(vn5, pvnode);
w2.__owl__.pvnode = pvnode;
let componentKey8 = \`routeComponent\`;
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| context['routeComponent'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(parent, props8);
parent.__owl__.cmap[k9] = w8.__owl__.id;
let def7 = w8.__prepare(extra.fiber, undefined, undefined);
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
const fiber = w8.__owl__.currentFiber;
def7.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
utils.defineProxy(vn11, pvnode);
w8.__owl__.pvnode = pvnode;
}
sibling = w2.__owl__.currentFiber || sibling;
w8.__owl__.parentLastFiberId = extra.fiber.id;
}
return result;
}"
+11 -4
View File
@@ -12,6 +12,7 @@ describe("Link component", () => {
beforeEach(() => {
fixture = makeTestFixture();
env = <RouterEnv>makeTestEnv();
Component.env = env;
});
afterEach(() => {
@@ -34,11 +35,14 @@ describe("Link component", () => {
static components = { Link: Link };
}
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
const routes = [
{ name: "about", path: "/about" },
{ name: "users", path: "/users" }
];
router = new TestRouter(env, routes, { mode: "history" });
router.navigate({ to: "users" });
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe('<div><a href="/about">About</a></div>');
@@ -65,11 +69,14 @@ describe("Link component", () => {
static components = { Link: Link };
}
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
const routes = [
{ name: "about", path: "/about" },
{ name: "users", path: "/users" }
];
router = new TestRouter(env, routes, { mode: "history" });
router.navigate({ to: "users" });
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(window.location.pathname).toBe("/users");
+4 -3
View File
@@ -12,6 +12,7 @@ describe("RouteComponent", () => {
beforeEach(() => {
fixture = makeTestFixture();
env = <RouterEnv>makeTestEnv();
Component.env = env;
});
afterEach(() => {
@@ -45,7 +46,7 @@ describe("RouteComponent", () => {
router = new TestRouter(env, routes, { mode: "history" });
await router.navigate({ to: "about" });
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>About</span></div>");
@@ -72,7 +73,7 @@ describe("RouteComponent", () => {
const routes = [{ name: "book", path: "/book/{{title}}", component: Book }];
router = new TestRouter(env, routes, { mode: "history" });
await router.navigate({ to: "book", params: { title: "1984" } });
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984</span></div>");
});
@@ -98,7 +99,7 @@ describe("RouteComponent", () => {
const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }];
router = new TestRouter(env, routes, { mode: "history" });
await router.navigate({ to: "book", params: { title: "1984", val: "123" } });
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
});
+8
View File
@@ -52,6 +52,14 @@ 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", () => {
+232 -24
View File
@@ -12,6 +12,7 @@ describe("connecting a component to store", () => {
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
});
afterEach(() => {
@@ -37,7 +38,7 @@ describe("connecting a component to store", () => {
}
(<any>env).store = store;
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>");
@@ -47,6 +48,76 @@ describe("connecting a component to store", () => {
expect(fixture.innerHTML).toBe("<div><span>hello</span></div>");
});
test("useStore can observe primitive types and call onUpdate", async () => {
const state = { isBoolean: false };
const actions = {
setTrue({ state }) {
state.isBoolean = true;
}
};
const store = new Store({ state, actions });
class App extends Component<any, any> {
static template = xml`
<div>
<span t-if="isBoolean">ok</span>
</div>`;
isBoolean: boolean;
constructor() {
super();
this.isBoolean = useStore(state => state.isBoolean, {
onUpdate: isBoolean => {
this.isBoolean = isBoolean;
}
});
}
}
(<any>env).store = store;
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>");
store.dispatch("setTrue");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>ok</span></div>");
});
test("throw error if no store is found", async () => {
class App extends Component<any, any> {
static template = xml`<div></div>`;
todos = useStore(state => state.todos);
}
let error;
try {
new App();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("No store found when connecting 'App'");
});
test("cannot modify state returned by usestore", async () => {
const state = { a: { b: 1 } };
const actions = {};
const store = new Store({ state, actions });
class App extends Component<any, any> {
static template = xml`<div/>`;
storeState = useStore(state => state.a);
}
(<any>env).store = store;
const app = new App();
expect(app.storeState.b).toBe(1);
expect(() => (app.storeState.b = 2)).toThrow(
"Store state should only be modified through actions"
);
});
test("can use useStore twice in a component", async () => {
const state = { a: 1, b: 2 };
const actions = {
@@ -69,7 +140,7 @@ describe("connecting a component to store", () => {
App.prototype.__render = jest.fn(App.prototype.__render);
(<any>env).store = store;
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>1</span><span>2</span></div>");
@@ -101,7 +172,7 @@ describe("connecting a component to store", () => {
App.prototype.__render = jest.fn(App.prototype.__render);
(<any>env).store = store;
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>");
@@ -134,7 +205,7 @@ describe("connecting a component to store", () => {
}
(<any>env).store = store;
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>0</div>");
@@ -163,7 +234,7 @@ describe("connecting a component to store", () => {
App.prototype.__render = jest.fn(App.prototype.__render);
(<any>env).store = store;
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>0</div>");
@@ -198,7 +269,7 @@ describe("connecting a component to store", () => {
todos = useStore(state => state.todos, { store });
}
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>");
@@ -228,7 +299,7 @@ describe("connecting a component to store", () => {
storeState = useStore(state => state);
dispatch = useDispatch();
}
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>Inc</button><span>1</span></div>");
@@ -240,7 +311,12 @@ describe("connecting a component to store", () => {
});
test("useStore can use props", async () => {
const state = { todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "chimay" }] };
const state = {
todos: [
{ id: 1, text: "jupiler" },
{ id: 2, text: "chimay" }
]
};
const store = new Store({ state, actions: {} });
class TodoItem extends Component<any, any> {
@@ -257,7 +333,7 @@ describe("connecting a component to store", () => {
}
(<any>env).store = store;
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
@@ -294,7 +370,7 @@ describe("connecting a component to store", () => {
}
(<any>env).store = store;
const app = new TodoList(env);
const app = new TodoList();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
@@ -307,7 +383,10 @@ describe("connecting a component to store", () => {
test("can call useGetters to receive store getters", async () => {
const state = {
importantID: 1,
todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }]
todos: [
{ id: 1, text: "jupiler" },
{ id: 2, text: "bertinchamps" }
]
};
const getters = {
importantTodoText({ state }) {
@@ -347,7 +426,7 @@ describe("connecting a component to store", () => {
}
(<any>env).store = store;
const app = new TodoList(env);
const app = new TodoList();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
@@ -370,7 +449,7 @@ describe("connecting a component to store", () => {
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
const store = new Store({ state });
(<any>env).store = store;
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
@@ -401,7 +480,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
@@ -411,6 +490,68 @@ describe("connecting a component to store", () => {
expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>kwak</span></div>");
});
test("connected component is updated when mixing store and props changes", async () => {
let counter = 0;
class Beer extends Component<any, any> {
static template = xml`<span><t t-esc="beer.name"/></span>`;
beer = useStore((state, props) => state.beers[props.id], {
onUpdate: result => {
++counter;
}
});
}
class App extends Component<any, any> {
static template = xml`<div><Beer id="state.beerId"/></div>`;
static components = { Beer };
state = useState({ beerId: 1 });
}
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
const actions = {
renameBeer({ state }, { id, name }) {
state.beers[id].name = name;
}
};
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
expect(counter).toBe(0);
app.state.beerId = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>kwak</span></div>");
expect(counter).toBe(1);
store.dispatch("renameBeer", { id: 2, name: "orval" });
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>orval</span></div>");
expect(counter).toBe(2);
});
test("connected component is properly cleaned up on destroy", async () => {
class App extends Component<any, any> {
static template = xml`<div></div>`;
state = useStore((state, props) => state);
}
const store = new Store({ state: {} });
(<any>env).store = store;
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>");
expect(store.updateFunctions[app.__owl__.id].length).toBe(1);
app.destroy();
expect(store.updateFunctions[app.__owl__.id]).toBe(undefined);
});
test("connected component with undefined, null and string props", async () => {
class Beer extends Component<any, any> {
static template = xml`
@@ -446,7 +587,7 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>");
@@ -513,7 +654,7 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>");
@@ -555,6 +696,31 @@ describe("connecting a component to store", () => {
);
});
test("store changes occuring when mounting a component are notified", async () => {
const initialState = { x: { val: 1 } };
const actions = {
setValue({ state }, val) {
state.x.val = val;
}
};
class Parent extends Component<any, any> {
static template = xml`<div><t t-esc="x.val"/></div>`;
x = useStore(state => {
return Object.assign({}, state.x);
});
dispatch = useDispatch();
}
Parent.prototype.__render = jest.fn(Parent.prototype.__render);
Parent.env.store = new Store({ state: initialState, actions });
const parent = new Parent();
const prom = parent.mount(fixture);
parent.dispatch("setValue", 2);
await prom;
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(Parent.prototype.__render).toHaveBeenCalledTimes(1);
});
test("correct update order when parent/children are connected", async () => {
const steps: string[] = [];
@@ -588,7 +754,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new Parent(env);
const app = new Parent();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>a</span></div>");
@@ -639,7 +805,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new Parent(env);
const app = new Parent();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>abc</span></div>");
@@ -713,7 +879,7 @@ describe("connecting a component to store", () => {
}
(<any>env).store = store;
const app = new TodoApp(env);
const app = new TodoApp();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
@@ -787,7 +953,7 @@ describe("connecting a component to store", () => {
}
(<any>env).store = store;
const app = new TodoApp(env);
const app = new TodoApp();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
@@ -833,7 +999,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>a</div>");
@@ -870,7 +1036,7 @@ describe("connecting a component to store", () => {
}
const store = new TestStore({ state: { val: 1 } });
(<any>env).store = store;
const parent = new Parent(env);
const parent = new Parent();
await parent.mount(fixture);
expect(steps).toEqual(["on:update"]);
@@ -882,6 +1048,46 @@ describe("connecting a component to store", () => {
expect(steps).toEqual(["on:update", "off:update"]);
});
test("connected child component destroyed by dispatched action", async () => {
let steps: any = [];
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="store.val"/></div>`;
store = useStore(s => {
steps.push("child selector");
return s;
});
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="store.child" /></div>`;
static components = { Child };
store = useStore(s => {
steps.push("parent selector");
return s;
});
dispatch = useDispatch();
}
const state = { child: true, val: 1 };
const actions = {
toggleChild({ state }) {
state.child = !state.child;
}
};
const store = new Store({ state, actions });
(<any>env).store = store;
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
expect(steps).toEqual(["parent selector", "child selector"]);
parent.dispatch("toggleChild");
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["parent selector", "child selector", "parent selector"]);
});
test("dispatch an action", async () => {
class App extends Component<any, any> {
static template = xml`<div><t t-esc="store.counter"/></div>`;
@@ -901,7 +1107,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, actions });
(<any>env).store = store;
const app = new App(env);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>0</div>");
@@ -920,6 +1126,7 @@ describe("various scenarios", () => {
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
});
afterEach(() => {
@@ -978,7 +1185,8 @@ describe("various scenarios", () => {
}
(<any>env).store = store;
const message = new Message(env);
const message = new Message();
await message.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
+2 -4
View File
@@ -15,8 +15,6 @@ class Counter extends owl.Component {
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
static components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
@@ -26,12 +24,11 @@ class Message extends owl.Component {
});
}
}
Message.components = { Counter };
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
static components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
@@ -137,6 +134,7 @@ class App extends owl.Component {
this.refs.log.innerHTML = "";
}
}
App.components = { Message };
//------------------------------------------------------------------------------
// Application initialization
+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>
File diff suppressed because it is too large Load Diff
+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>
+5 -6
View File
@@ -16,8 +16,6 @@ class Counter extends owl.Component {
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
static components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
@@ -27,12 +25,12 @@ class Message extends owl.Component {
});
}
}
Message.components = { Counter };
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
static components = { Message };
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
@@ -139,16 +137,17 @@ class App extends owl.Component {
this.logRef.el.innerHTML = "";
}
}
App.components = { Message };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadFile("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
App.env = {
qweb: new owl.QWeb({ templates })
};
const app = new App(env);
const app = new App();
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);
});
}
}
+137
View File
@@ -0,0 +1,137 @@
/**
* Debug Script
*
* This code is intended to be evaluated in an environment where owl is available,
* to log lot of helpful information on how Owl components behave.
*/
let debugSetup = {
// componentBlackList: /App/, // regexp
// componentWhiteList: /SomeComponent/, // regexp
// methodBlackList: ["mounted"], // list of method names
// methodWhiteList: ["willStart"], // list of method names
logScheduler: true, // display/mute scheduler logs
logStore: true // display/mute store logs
};
{
let prefix = "[OWL_DEBUG]";
let current;
Object.defineProperty(owl.Component, "current", {
get() {
return current;
},
set(comp) {
current = comp;
const name = comp.constructor.name;
if (debugSetup.componentBlackList && debugSetup.componentBlackList.test(name)) {
return;
}
if (debugSetup.componentWhiteList && !debugSetup.componentWhiteList.test(name)) {
return;
}
let __owl__;
Object.defineProperty(current, "__owl__", {
get() {
return __owl__;
},
set(val) {
__owl__ = val;
debugComponent(comp, name, __owl__.id);
}
});
}
});
function toStr(obj) {
let str = JSON.stringify(obj || {});
if (str.length > 200) {
str = str.slice(0, 200) + "...";
}
return str;
}
function debugComponent(component, name, id) {
let fullName = `${name}<id=${id}>`;
let shouldDebug = method => {
if (debugSetup.methodBlackList && debugSetup.methodBlackList.includes(method)) {
return false;
}
if (debugSetup.methodWhiteList && !debugSetup.methodWhiteList.includes(method)) {
return false;
}
return true;
};
if (shouldDebug("constructor")) {
console.log(`${prefix} ${fullName} constructor, props=${toStr(component.props)}`);
}
if (shouldDebug("willStart")) {
owl.hooks.onWillStart(() => {
console.log(`${prefix} ${fullName} willStart`);
});
}
if (shouldDebug("mounted")) {
owl.hooks.onMounted(() => {
console.log(`${prefix} ${fullName} mounted`);
});
}
if (shouldDebug("willUpdateProps")) {
owl.hooks.onWillUpdateProps(nextProps => {
console.log(`${prefix} ${fullName} willUpdateProps, nextprops=${toStr(nextProps)}`);
});
}
if (shouldDebug("willPatch")) {
owl.hooks.onWillPatch(() => {
console.log(`${prefix} ${fullName} willPatch`);
});
}
if (shouldDebug("patched")) {
owl.hooks.onPatched(() => {
console.log(`${prefix} ${fullName} patched`);
});
}
if (shouldDebug("willUnmount")) {
owl.hooks.onWillUnmount(() => {
console.log(`${prefix} ${fullName} willUnmount`);
});
}
const __render = component.__render.bind(component);
component.__render = function(...args) {
console.log(`${prefix} ${fullName} rendering template`);
__render(...args);
};
const render = component.render.bind(component);
component.render = function(...args) {
console.log(`${prefix} ${fullName} render`);
return render(...args);
};
const mount = component.mount.bind(component);
component.mount = function(...args) {
console.log(`${prefix} ${fullName} mount`);
return mount(...args);
};
}
if (debugSetup.logScheduler) {
let isRunning;
Object.defineProperty(owl.Component.scheduler, "isRunning", {
get() {
return isRunning;
},
set(val) {
if (val) {
console.log(`${prefix} scheduler: start running tasks queue`);
} else {
console.log(`${prefix} scheduler: stop running tasks queue`);
}
isRunning = val;
}
});
}
if (debugSetup.logStore) {
let dispatch = owl.Store.prototype.dispatch;
owl.Store.prototype.dispatch = function(action, ...payload) {
console.log(`${prefix} store: action '${action}' dispatched. Payload: '${toStr(payload)}'`);
return dispatch.call(this, action, ...payload);
};
}
}
+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>
+43 -57
View File
@@ -70,13 +70,9 @@ if __name__ == "__main__":
/**
* Make an iframe, with all the js, css and xml properly injected.
*/
function makeCodeIframe(js, css, xml, errorHandler) {
// check templates
var qweb = new owl.QWeb();
function makeCodeIframe(js, css, xml) {
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "");
// will throw error if there is something wrong with xml
qweb.addTemplates(sanitizedXML);
// create iframe
const iframe = document.createElement("iframe");
@@ -90,16 +86,15 @@ 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}`;
script.innerHTML = content;
iframe.contentWindow.addEventListener("error", errorHandler);
iframe.contentWindow.addEventListener("unhandledrejection", errorHandler);
setTimeout(function() {
if (iframe.contentWindow) {
iframe.contentWindow.removeEventListener("error", errorHandler);
iframe.contentWindow.removeEventListener("unhandledrejection", errorHandler);
const content = `
{
owl.config.mode = 'dev';
let templates = \`${sanitizedXML}\`;
const qweb = new owl.QWeb({ templates });
owl.Component.env = { qweb };
}
}, 200);
${js}`;
script.innerHTML = content;
doc.body.appendChild(script);
});
doc.head.appendChild(owlScript);
@@ -125,20 +120,34 @@ async function makeApp(js, css, xml) {
.join("\n");
const JS = `
async function loadTemplates() {
try {
return owl.utils.loadFile('app.xml');
} catch(e) {
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
}
}
function start([TEMPLATES]) {
// Application code
/**
* This is the javascript code defined in the playground.
* In a larger application, this code should probably be moved in different
* sub files.
*/
function app() {
${processedJS}
}
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
/**
* Initialization code
* This code load templates, and make sure everything is properly connected.
*/
async function start() {
let templates;
try {
templates = await owl.utils.loadFile('app.xml');
} catch(e) {
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
return;
}
const env = { qweb: new owl.QWeb({templates})};
owl.Component.env = env;
await owl.utils.whenReady();
app();
}
start();
`;
zip.file("app.js", JS);
@@ -310,7 +319,6 @@ class App extends owl.Component {
js: this.SAMPLES[0].code,
css: this.SAMPLES[0].css || "",
xml: this.SAMPLES[0].xml || DEFAULT_XML,
error: false,
displayWelcome: true,
splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2),
@@ -323,37 +331,12 @@ class App extends owl.Component {
this.content = useRef("content");
}
displayError(error) {
this.state.error = error;
if (error) {
setTimeout(() => {
this.content.el.innerHTML = "";
});
return;
}
}
runCode() {
this.state.displayWelcome = false;
let subiframe;
let error = false;
const errorHandler = e => this.displayError(e.message || e.reason.message);
try {
const { js, css, xml } = this.state;
subiframe = makeCodeIframe(js, css, xml, errorHandler);
} catch (e) {
//probably problem with the templates
error = e;
// we still log the error, always useful to have it available
console.error(e);
}
if (error) {
this.displayError(error.message);
return;
} else {
this.state.error = false;
}
this.content.el.innerHTML = "";
this.state.displayWelcome = false;
const { js, css, xml } = this.state;
const subiframe = makeCodeIframe(js, css, xml);
this.content.el.appendChild(subiframe);
}
@@ -429,12 +412,15 @@ 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 app = new App({ qweb });
const qweb = new owl.QWeb({ templates });
owl.Component.env = { qweb };
const app = new App();
app.mount(document.body);
}
-13
View File
@@ -179,16 +179,3 @@ body {
padding: 5%;
}
.right-pane .error {
height: 100%;
width: 90%;
padding-top: 30%;
font-size: 18px;
color: darkred;
margin-left: 5%;
}
.right-pane .error pre {
overflow: auto;
width: 100%;
}
+115 -80
View File
@@ -2,7 +2,10 @@ const COMPONENTS = `// In this example, we show how components can be defined an
const { Component, useState } = owl;
class Greeter extends Component {
state = useState({ word: 'Hello' });
constructor() {
super(...arguments);
this.state = useState({ word: 'Hello' });
}
toggle() {
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
@@ -11,14 +14,15 @@ class Greeter extends Component {
// Main root component
class App extends Component {
state = useState({ name: 'World'});
constructor() {
super(...arguments);
this.state = useState({ name: 'World'});
}
}
App.components = { Greeter };
// Application setup
// Note that the xml templates are injected into the global TEMPLATES variable.
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
const app = new App();
app.mount(document.body);
`;
@@ -49,7 +53,10 @@ const ANIMATION = `// The goal of this component is to see how the t-transition
const { Component, useState } = owl;
class Counter extends Component {
state = useState({ value: 0 });
constructor() {
super(...arguments);
this.state = useState({ value: 0 });
}
increment() {
this.state.value++;
@@ -57,7 +64,10 @@ class Counter extends Component {
}
class App extends Component {
state = useState({ flag: false, componentFlag: false, numbers: [] });
constructor() {
super(...arguments);
this.state = useState({ flag: false, componentFlag: false, numbers: [] });
}
toggle(key) {
this.state[key] = !this.state[key];
@@ -70,8 +80,7 @@ class App extends Component {
}
App.components = { Counter };
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({qweb});
const app = new App();
app.mount(document.body);
`;
@@ -216,7 +225,10 @@ class DemoComponent extends Component {
}
class App extends Component {
state = useState({ n: 0, flag: true });
constructor() {
super(...arguments);
this.state = useState({ n: 0, flag: true });
}
increment() {
this.state.n++;
@@ -228,8 +240,7 @@ class App extends Component {
}
App.components = { DemoComponent };
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
const app = new App();
app.mount(document.body);
`;
@@ -286,11 +297,14 @@ function useMouse() {
// Main root component
class App extends owl.Component {
// simple state hook (reactive object)
counter = useState({ value: 0 });
constructor() {
super(...arguments);
// simple state hook (reactive object)
this.counter = useState({ value: 0 });
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
// this hooks is bound to the 'mouse' property.
this.mouse = useMouse();
}
increment() {
this.counter.value++;
@@ -298,8 +312,7 @@ class App extends owl.Component {
}
// Application setup
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
const app = new App();
app.mount(document.body);
`;
@@ -323,7 +336,10 @@ const { Component, Context } = owl;
const { useContext } = owl.hooks;
class ToolbarButton extends Component {
theme = useContext(this.env.themeContext);
constructor() {
super(...arguments);
this.theme = useContext(this.env.themeContext);
}
get style () {
const theme = this.theme;
@@ -349,11 +365,9 @@ const themeContext = new Context({
background: '#000',
foreground: '#fff',
});
const env = {
qweb: new owl.QWeb(TEMPLATES),
themeContext: themeContext,
};
const app = new App(env);
// Add the themeContext the environment to make it available to all components
App.env.themeContext = themeContext;
const app = new App();
app.mount(document.body);
`;
@@ -458,12 +472,11 @@ const actions = {
// TodoItem
//------------------------------------------------------------------------------
class TodoItem extends Component {
state = useState({ isEditing: false });
dispatch = useDispatch();
constructor(...args) {
super(...args);
constructor() {
super(...arguments);
useAutofocus("input");
this.state = useState({ isEditing: false });
this.dispatch = useDispatch();
}
handleKeyup(ev) {
@@ -490,9 +503,12 @@ class TodoItem extends Component {
// TodoApp
//------------------------------------------------------------------------------
class TodoApp extends Component {
state = useState({ filter: "all" });
todos = useStore(state => state.todos);
dispatch = useDispatch();
constructor() {
super(...arguments);
this.state = useState({ filter: "all" });
this.todos = useStore(state => state.todos);
this.dispatch = useDispatch();
}
get visibleTodos() {
switch (this.state.filter) {
@@ -534,26 +550,25 @@ TodoApp.components = { TodoItem };
//------------------------------------------------------------------------------
// App Initialization
//------------------------------------------------------------------------------
function saveState(state) {
const str = JSON.stringify(state);
window.localStorage.setItem(LOCALSTORAGE_KEY, str);
}
function loadState() {
const localState = window.localStorage.getItem(LOCALSTORAGE_KEY);
return localState ? JSON.parse(localState) : initialState;
}
function makeStore() {
function saveState(state) {
const str = JSON.stringify(state);
window.localStorage.setItem(LOCALSTORAGE_KEY, str);
}
function loadState() {
const localState = window.localStorage.getItem(LOCALSTORAGE_KEY);
return localState ? JSON.parse(localState) : initialState;
}
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);
return { qweb, store };
return store;
}
const env = makeEnv();
const app = new TodoApp(env);
TodoApp.env.store = makeStore();
const app = new TodoApp();
app.mount(document.body);
`;
@@ -1016,7 +1031,10 @@ class FormView extends owl.Component {}
FormView.components = { AdvancedComponent };
class Chatter extends owl.Component {
messages = Array.from(Array(100).keys());
constructor() {
super(...arguments);
this.messages = Array.from(Array(100).keys());
}
}
class App extends owl.Component {}
@@ -1040,12 +1058,9 @@ function setupResponsivePlugin(env) {
//------------------------------------------------------------------------------
// Application Startup
//------------------------------------------------------------------------------
const env = {
qweb: new owl.QWeb(TEMPLATES),
};
setupResponsivePlugin(env);
setupResponsivePlugin(App.env);
const app = new App(env);
const app = new App();
app.mount(document.body);
`;
@@ -1161,7 +1176,10 @@ const SLOTS = `// We show here how slots can be used to create generic component
const { Component, useState } = owl;
class Card extends Component {
state = useState({ showContent: true });
constructor() {
super(...arguments);
this.state = useState({ showContent: true });
}
toggleDisplay() {
this.state.showContent = !this.state.showContent;
@@ -1169,7 +1187,10 @@ class Card extends Component {
}
class Counter extends Component {
state = useState({val: 1});
constructor() {
super(...arguments);
this.state = useState({val: 1});
}
inc() {
this.state.val++;
@@ -1178,7 +1199,10 @@ class Counter extends Component {
// Main root component
class App extends Component {
state = useState({a: 1, b: 3});
constructor() {
super(...arguments);
this.state = useState({a: 1, b: 3});
}
inc(key, delta) {
this.state[key] += delta;
@@ -1187,8 +1211,7 @@ class App extends Component {
App.components = {Card, Counter};
// Application setup
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
const app = new App();
app.mount(document.body);`;
const SLOTS_XML = `<templates>
@@ -1289,7 +1312,10 @@ class SlowComponent extends Component {
class NotificationList extends Component {}
class App extends Component {
state = useState({ value: 0, notifs: [] });
constructor() {
super(...arguments);
this.state = useState({ value: 0, notifs: [] });
}
increment() {
this.state.value++;
@@ -1301,10 +1327,9 @@ class App extends Component {
}, 3000);
}
}
App.components = {SlowComponent, NotificationList};
App.components = {SlowComponent, NotificationList, AsyncRoot};
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
const app = new App();
app.mount(document.body);
`;
@@ -1363,18 +1388,20 @@ const FORM = `// This example illustrate how the t-model directive can be used t
const { Component, useState } = owl;
class Form extends Component {
state = useState({
text: "",
othertext: "",
number: 11,
color: "",
bool: false
});
constructor() {
super(...arguments);
this.state = useState({
text: "",
othertext: "",
number: 11,
color: "",
bool: false
});
}
}
// Application setup
const qweb = new owl.QWeb(TEMPLATES);
const form = new Form({ qweb });
const form = new Form();
form.mount(document.body);
`;
@@ -1433,7 +1460,10 @@ const { useRef } = owl.hooks;
class HelloWorld extends Component {}
class Counter extends Component {
state = useState({ value: 0 });
constructor() {
super(...arguments);
this.state = useState({ value: 0 });
}
inc() {
this.state.value++;
@@ -1484,11 +1514,14 @@ class Window extends Component {
}
class WindowManager extends Component {
windows = [];
nextId = 1;
currentZindex = 1;
nextLeft = 0;
nextTop = 0;
constructor() {
super(...arguments);
this.windows = [];
this.nextId = 1;
this.currentZindex = 1;
this.nextLeft = 0;
this.nextTop = 0;
}
addWindow(name) {
const info = this.env.windows.find(w => w.name === name);
@@ -1532,7 +1565,10 @@ class WindowManager extends Component {
WindowManager.components = { Window };
class App extends Component {
wmRef = useRef("wm");
constructor() {
super(...arguments);
this.wmRef = useRef("wm");
}
addWindow(name) {
this.wmRef.comp.addWindow(name);
@@ -1540,7 +1576,6 @@ class App extends Component {
}
App.components = { WindowManager };
const qweb = new owl.QWeb(TEMPLATES);
const windows = [
{
name: "Hello",
@@ -1558,8 +1593,8 @@ const windows = [
}
];
const env = { qweb, windows };
const app = new App(env);
App.env.windows = windows;
const app = new App();
app.mount(document.body);
`;
@@ -1567,7 +1602,7 @@ const WMS_XML = `<templates>
<div t-name="Window" class="window" t-att-style="style" t-on-click="updateZIndex">
<div class="header">
<span t-on-mousedown="startDragAndDrop"><t t-esc="props.info.title"/></span>
<span class="close" t-on-click="close">×</span>
<span class="close" t-on-click.stop="close">×</span>
</div>
<t t-slot="default"/>
</div>
+1 -5
View File
@@ -31,7 +31,7 @@
t-att-style="topEditorStyle"/>
<t t-if="state.splitLayout">
<div class="separator horizontal"/>
<TabbedEditor t-keepalive="1"
<TabbedEditor
js="false"
css="state.css"
xml="state.xml"
@@ -51,10 +51,6 @@
</p>
</div>
</div>
<div t-if="state.error" class="error">
<h3>Error</h3>
<pre t-esc="state.error"/>
</div>
<div class="content" t-ref="content"/>
</div>
</div>