Compare commits

..

67 Commits

Author SHA1 Message Date
Géry Debongnie 0762eeb010 [REL] v1.0.0-beta2 2019-12-11 10:19:04 +01:00
Géry Debongnie bc455be720 [IMP] components: slots can now define default content
closes #554
2019-12-11 10:11:47 +01:00
Lucas Perais (lpe) 9145799ae9 [FIX] qweb: evaluate body of t-set immediately 2019-12-11 10:07:41 +01:00
Géry Debongnie ce9c2f8613 [FIX] qweb/component: refactoring of scoping/variables/slots
This commit is a significant refactoring of the internal of QWeb. It
simplifies the way variables/scoping and slots interact together.  The
main idea is that we use a simple scope object instead of a
context/var/scope object.

This commit also implement the actual correct QWeb semantic for the
t-call directive with a sub body.  Before, we simply extracted the
variables from the body and injected them at the top of the sub
template.  We now simply compile the body before the sub template.

This is a joint work with Lucas (lpe).

closes #541
closes #544
closes #545

closes #557
closes #556
2019-12-11 10:07:41 +01:00
Géry Debongnie b890e7ceae [IMP] qweb: add template name in generated compiled code 2019-12-10 16:14:09 +01:00
Géry Debongnie 124211c18b [DOC] clarify store documentation on connected components
closes #538
2019-12-06 12:25:55 +01:00
Géry Debongnie be8c3bbf6f [IMP] tooling: add warning if component not mounted, but rendered
closes #551
2019-12-06 12:07:54 +01:00
Géry Debongnie e162a6fb6c [IMP] qweb: add support for arrow functions in expression parser
closes #542
2019-12-06 11:49:55 +01:00
Géry Debongnie 7ec05ff8cf [IMP] component: add position option to mount method
A component can now be mounted in different positions: either 'before' (at
the start), 'after' (at the end), or 'attach' (take possession of a target
element)

closes #539
2019-12-06 10:53:01 +01:00
Géry Debongnie a0bfbf6dd0 [FIX] qweb/component: do not call handlers for unmounted components
Unmounted components should be considered inactive, at least from the
perspective of Owl itself.

closes #543
2019-12-06 09:58:12 +01:00
Géry Debongnie b67cb71048 [FIX] slots: propagate vars to correct function
closes #529
2019-12-06 09:53:48 +01:00
Géry Debongnie f5b3ef5cf3 [DOC] add some information on variables and t-call
closes #540
2019-12-05 21:49:51 +01:00
Lucas Perais (lpe) ecfccf2448 [FIX] portal: manually unmounting works
Before this commit, when manually unmounting the portal
or one of its parents, the teleported elements were not cleaned
and stayed teleported forever

After this commit, the teleported content is brought back and
unmounted much like any component

closes #531
2019-12-04 09:48:08 +01:00
Géry Debongnie f12d3373c9 [FIX] qweb: properly handle empty class attribute
The classList.add method actually crashes when given an empty string (or
a string with just white spaces).

closes #530
2019-12-03 16:19:09 +01:00
Géry Debongnie 286090efca [FIX] tooling: debug tool logged too many scheduler stops
This was caused by additional calls to flush (because of connected
components)

closes #533
2019-12-03 15:16:29 +01:00
Géry Debongnie 2b4ff7b2fd [FIX] doc: the debugOwl helper was missing a parameter
closes #532
2019-12-03 14:13:42 +01:00
Géry Debongnie c43178a45b [CLEANUP] observer: remove useless variable 2019-12-03 11:17:37 +01:00
Géry Debongnie 033a431e18 [ADD] tools: add benchmark for 1.0.0-beta1, remove old ones 2019-12-03 09:12:52 +01:00
Géry Debongnie 42aa9d3ae1 [REL] v1.0.0-beta1 2019-12-03 08:52:04 +01:00
Géry Debongnie 85f26a0286 [IMP] tools: update debug script, add tests
closes #525
closes #526
2019-12-03 08:28:46 +01:00
Géry Debongnie ed4ab51c17 [FIX] component: tricky concurrency issue
This is a really interesting problem: there was a situation where a
component would not have an event handler properly bound. The reason was
that we were in a situation where the scheduler task queue was flushed
at an unfortunate timing:

- parent component template is rendered
- subcomponent is prepared:
   - sub component is willStarted
   - it is rendered (so, fiber counter is set to 0)
- scheduler task queue is flushed => we patch the DOM
- the code registered in the __prepare.then(handler) is executed, which
  add the createHook to the vnode (but after the dom is patched)

Usually, the last two steps happen in a different order, but in an
environment with connected components, we sometimes flush the task queue
at various moments, so some code could be executed between the end of
__prepare and the registered handler.

The fix is interesting: we give a callback to the __prepare function
instead of registering a .then handler to the deferred.  This callback
is then guaranteed to be called at exactly the proper timing.

Thanks seb for the hard work of finding the root cause of this issue

closes #520
2019-12-03 08:23:10 +01:00
Lucas Perais (lpe) 556a4644f0 [ADD] misc: component Portal
The component Portal is used to teleport the content
in its slot as a child of an element
somewhere else in the DOM, usually 'body'

It is designed to be transparent and is just here to do the teleportation task

OwlEvents (with method `Component.trigger`) are redirected from
any Component instanciated within the Portal (and therefore teleported
somewhere else) to the place it would have been without teleportation
i.e. they are re-triggered onto the Portal root node

Co-authored-by: Aaron Bohy <aab@odoo.com>

closes #184
closes #466
2019-12-02 21:04:07 +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
81 changed files with 15241 additions and 17287 deletions
+4 -4
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:
@@ -104,8 +104,8 @@ Submit a PR!
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.0.0-alpha3.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha3/owl.js)
- [owl-1.0.0-alpha3.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha3/owl.min.js)
- [owl-1.0.0-beta2.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta2/owl.js)
- [owl-1.0.0-beta2.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta2/owl.min.js)
Some npm scripts are available:
+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();
}
}
});
}
```
+1 -1
View File
@@ -297,7 +297,7 @@ A lot of stuff happened here:
- the `Task` component has a `props` key: this is only useful for validation
purpose. It says that each `Task` should be given exactly one prop, named
`task`. If this is not the case, Owl will throw an
[error](../reference/component.md#props-validation). This is extremely
[error](../reference/props_validation.md). This is extremely
useful when refactoring components
- finally, to activate the props validation, we need to set Owl's
[mode](../reference/config.md#mode) to `dev`. This is done in the `setup`
+17 -14
View File
@@ -4,6 +4,7 @@
- [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)
@@ -12,6 +13,7 @@
- [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)
@@ -23,6 +25,7 @@
- [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
@@ -47,20 +50,20 @@ 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
QWeb Portal
Store router
useState Link
config RouteComponent
mode Router
core tags
EventBus xml
Observer utils
hooks debounce
onWillStart escape
onMounted loadJS
onWillUpdateProps loadFile
onWillPatch shallowEqual
onPatched whenReady
onWillUnmount
useContext
useState
+53 -239
View File
@@ -14,12 +14,9 @@
- [Composition](#composition)
- [Event Handling](#event-handling)
- [Form Input Bindings](#form-input-bindings)
- [Semantics](#semantics)
- [Props Validation](#props-validation)
- [References](#references)
- [Slots](#slots)
- [Dynamic sub components](#dynamic-sub-components)
- [Asynchronous Rendering](#asynchronous-rendering)
- [Error Handling](#error-handling)
- [Functional Components](#functional-components)
- [SVG components](#svg-components)
@@ -204,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 {
@@ -237,35 +234,60 @@ component.
We explain here all the public methods of the `Component` class.
- **`mount(target)`** (async): this is the main way a
- **`mount(target, options)`** (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 `options` argument is an optional object with a `position` key. The
`position` key can have three possible values: `first-child`, `last-child`, `self`.
- `first-child`: with this option, the component will be prepended inside the target,
- `last-child` (default value): with this option, the component will be
appended in the target element,
- `self`: the target will be used as the root element for the component. This
means that the target has to be an HTMLElement (and not a document fragment).
In this situation, it is possible that the component cannot be unmounted. For
example, if its target is `document.body`.
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.
- **`unmount()`**: in case a component needs to be detached/removed from the DOM, this
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
useful to the underlying component system.
- **`render()`** (async): calling this method directly will cause a rerender. Note
* **`render()`** (async): calling this method directly will cause a rerender. Note
that this should be very rare to have to do it manually, the Owl framework is
most of the time responsible for doing that at an appropriate moment.
Note that the render method is asynchronous, so one cannot observe the updated
DOM in the same stack frame.
- **`shouldUpdate(nextProps)`**: this method is called each time a component's props
* **`shouldUpdate(nextProps)`**: this method is called each time a component's props
are updated. It returns a boolean, which indicates if the component should
ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
can be useful if we are handling large number of components.
- **`destroy()`**. As its name suggests, this method will remove the component,
* **`destroy()`**. As its name suggests, this method will remove the component,
and perform all necessary cleanup, such as unmounting the component, its children,
removing the parent/children relationship. This method should almost never be
called directly (except maybe on the root component), but should be done by the
@@ -567,11 +589,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 {
@@ -729,188 +753,6 @@ update a number whenever the change is done.
Note: the online playground has an example to show how it works.
### 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 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:
- `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
@@ -1023,6 +865,19 @@ be considered the `default` slot. For example:
</div>
```
Slots can define a default content, in case the parent did not define them:
```xml
<div t-name="Parent">
<Child/>
</div>
<span t-name="Child">
<t t-slot="default">default content</t>
</span>
<!-- will be rendered as: <div><span>default content</span></div> -->
```
### Dynamic sub components
It is not common, but sometimes we need a dynamic component name. In this case,
@@ -1066,48 +921,7 @@ class App extends Component<any, any, any> {
In this example, the component `App` selects dynamically the concrete sub
component class.
### 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>
```
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>
```
+22
View File
@@ -4,6 +4,7 @@
- [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
@@ -65,6 +66,27 @@ by simply doing this:
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:
+9 -3
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`
+111
View File
@@ -1,5 +1,116 @@
# 🦉 Miscellaneous 🦉
## Content
- [Portal](#portal)
- [AsyncRoot](#asyncroot)
## `Portal`
### Overview
The component `Portal` is meant to be used as a transparent way to 'teleport' a piece
of DOM to the node represented by its sole `target` props.
This component aims at helping the implementation of the needed infrastructure
for modals (as in `bootstrap-modal`).
### Usage
The content it will teleport is defined within the `<Portal>` node and
internally uses the `default` [Slot](component.md#slots).
This slot must contain only **one** node, which in turn can have as many children as necessary.
The element under which the content will be teleported is represented as a selector
by the `target` props which only accepts a string as value.
The `target` props only supports static selector, and is not meant to be passed to `Portal`
as a variable. Namely, `<Portal target="'body'" />` is the intended use.
By contrast, `<Portal target="state.target" />` is not supported.
The component `Portal` has no particular state, rather it is meant to be a slave to its parent,
and ultimately just a way for the parent to teleport a piece of its own DOM elsewhere.
The `Portal`'s root node is always `<portal/>` and is placed where the teleported content
_would have_ been. It is this element that the [teleported events](#expected-behaviors) are re-directed on.
### Example
The canonic use-case is to implement a Dialog, where a Component may choose to break the natural
workflow to help the user put in some data, which it could use later on.
JavaScript:
```js
const { Component } = owl;
const { Portal } = owl.misc;
class TeleportedComponent extends Component {}
class App extends Component {
static components = { Portal, TeleportedComponent };
}
const app = new App();
app.mount(document.body);
```
XML:
```xml
<templates>
<div t-name="TeleportedComponent">
<span>I will move soon enough</span>
</div>
<div t-name="App">
<span>I am like the rest of us</span>
<Portal target="'body'">
<TeleportedComponent />
</Portal>
</div>
</templates>
```
In this example, the `Portal` component will teleport the `TeleportedComponent`'s `div` as a child of the `body`.
`TeleportedComponent` is acting as a Dialog here.
The resulting DOM will look like:
```xml
<body>
<div>
<span>I am like the rest of us</span>
<portal></portal>
</div>
<div>
<span>I will move soon enough</span>
</div>
</body>
```
### Expected Behaviors
The teleported piece is updated as any other `Component`'s DOM and in the same sequence.
Namely the teleported piece will be updated in function of its parents components, and patched as
a normal child.
The [_business_ events](component.md#event-handling) triggered by a child component will be stopped
to not bubble outside of the `target`. They will, on the other hand, be re-directed onto the
`Portal`'s root node and bubble up the DOM as if it were triggered by a regular child component.
Beware that those re-directed events are copies of the original event.
They have:
- The same payload.
- The same `originalComponent` than their original counterpart,
that is the actual Component that triggered it.
- A **different** `target` property than their original counterpart.
The `target` of a re-directed event is necessarily the `Portal`'s root node.
Pure DOM events do not follow this pattern and are free to bubble their natural, unaltered way
up to the `body`.
## `AsyncRoot`
When this component is used, a new rendering sub tree is created, such that the
+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)
},
};
```
@@ -500,6 +500,15 @@ will result in :
</div>
```
This can be used to define variables scoped to a sub template:
```xml
<t t-call="other-template">
<t t-set="var" t-value="1"/>
</t>
<!-- "var" does not exist here -->
```
### Translations
By default, QWeb specify that templates should be translated. If this behaviour
+23 -12
View File
@@ -25,8 +25,8 @@ component should own which part of the state.
Owl's solution to this issue is a centralized store. It is a class that owns
some (or all) state, and lets the developer update it in a structured way, with
`actions`. Owl components can then connect to the store, and will be updated if
necessary.
`actions`. Owl components can then connect to the store to read their relevant
state, and they will be rerendered if the state is updated.
Note: Owl store is inspired by React Redux and VueX.
@@ -252,19 +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.
Also, it may not be obvious, but it is crucial to remember that the selector
function should return an object or an array. The reason is that it needs to be
observed, otherwise the component would not be able to react to changes.
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`
+26
View File
@@ -6,6 +6,7 @@
- [Playground](#playground)
- [Benchmarks](#benchmarks)
- [Single File Component](#single-file-component)
- [Debugging Script](#debugging-script)
## Overview
@@ -78,3 +79,28 @@ 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:
```
function debugOwl(t,n){let e,o="[OWL_DEBUG]";function s(t){let n=JSON.stringify(t||{});return n.length>200&&(n=n.slice(0,200)+"..."),n}if(Object.defineProperty(t.Component,"current",{get:()=>e,set(i){e=i;const r=i.constructor.name;if(n.componentBlackList&&n.componentBlackList.test(r))return;if(n.componentWhiteList&&!n.componentWhiteList.test(r))return;let l;Object.defineProperty(e,"__owl__",{get:()=>l,set(e){!function(e,i,r){let l=`${i}<id=${r}>`,c=t=>console.log(`${o} ${l} ${t}`),u=t=>(!n.methodBlackList||!n.methodBlackList.includes(t))&&!(n.methodWhiteList&&!n.methodWhiteList.includes(t));u("constructor")&&c(`constructor, props=${s(e.props)}`);u("willStart")&&t.hooks.onWillStart(()=>{c("willStart")});u("mounted")&&t.hooks.onMounted(()=>{c("mounted")});u("willUpdateProps")&&t.hooks.onWillUpdateProps(t=>{c(`willUpdateProps, nextprops=${s(t)}`)});u("willPatch")&&t.hooks.onWillPatch(()=>{c("willPatch")});u("patched")&&t.hooks.onPatched(()=>{c("patched")});u("willUnmount")&&t.hooks.onWillUnmount(()=>{c("willUnmount")});const d=e.__render.bind(e);e.__render=function(...t){c("rendering template"),d(...t)};const h=e.render.bind(e);e.render=function(...t){const n=e.__owl__;let o="render";return n.isMounted||n.currentFiber||(o+=" (warning: component is not mounted, this render has no effect)"),c(o),h(...t)};const p=e.mount.bind(e);e.mount=function(...t){return c("mount"),p(...t)}}(i,r,(l=e).id)}})}}),n.logScheduler){let n=t.Component.scheduler.start,e=t.Component.scheduler.stop;t.Component.scheduler.start=function(){this.isRunning||console.log(`${o} scheduler: start running tasks queue`),n.call(this)},t.Component.scheduler.stop=function(){this.isRunning&&console.log(`${o} scheduler: stop running tasks queue`),e.call(this)}}if(n.logStore){let n=t.Store.prototype.dispatch;t.Store.prototype.dispatch=function(t,...e){return console.log(`${o} store: action '${t}' dispatched. Payload: '${s(e)}'`),n.call(this,t,...e)}}}
debugOwl(owl, {
// 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
});
```
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.
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "owl-framework",
"version": "1.0.0-alpha3",
"version": "1.0.0-beta2",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"engines": {
@@ -39,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.6.4",
"typescript": "^3.7.2",
"uglify-es": "^3.3.9"
},
"jest": {
+5 -12
View File
@@ -1,25 +1,17 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.0.0-alpha3
- Current version: 1.0.0-beta2
- Status: mostly stable
This roadmap is only an attempt at predicting Owl's future. Everything may
change!
### November 2019
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.
If all goes well, Owl is upgraded to beta status. From now on, no API change,
even small, is expected (but it still could happen).
### End of 2019
@@ -41,7 +33,8 @@ Release v1.0
Maybe:
- reimplement vdom to use *block* system, like Vue 3,
- reimplement vdom to use *block* system, like Vue 3, which should make Owl
much faster
- refactor `QWeb` to use an intermediate representation (some kind of AST) to
allow additional optimisations.
+150 -119
View File
@@ -1,6 +1,7 @@
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";
@@ -35,6 +36,10 @@ export interface Env {
[key: string]: any;
}
interface MountOptions {
position?: "first-child" | "last-child" | "self";
}
/**
* This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the
@@ -60,6 +65,14 @@ 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'
// (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;
boundHandlers: { [key: number]: any };
observer: Observer | null;
@@ -74,6 +87,8 @@ interface Internal<T extends Env, Props> {
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
}
export const portalSymbol = Symbol("portal"); // FIXME
//------------------------------------------------------------------------------
// Component
//------------------------------------------------------------------------------
@@ -90,6 +105,7 @@ export class Component<T extends Env, Props extends {}> {
static env: any = {};
// expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler;
__target: HTMLElement | undefined;
/**
* The `el` is the root element of the component. Note that it could be null:
@@ -169,6 +185,7 @@ export class Component<T extends Env, Props extends {}> {
children: {},
cmap: {},
currentFiber: null,
parentLastFiberId: 0,
boundHandlers: {},
mountedCB: null,
willUnmountCB: null,
@@ -179,7 +196,8 @@ export class Component<T extends Env, Props extends {}> {
observer: null,
renderFn: qweb.render.bind(qweb, template),
classObj: null,
refs: null
refs: null,
scope: null
};
}
@@ -276,38 +294,34 @@ export class Component<T extends Env, Props extends {}> {
*
* Note that a component can be mounted an unmounted several times
*/
async mount(target: HTMLElement): Promise<void> {
async mount(target: HTMLElement | DocumentFragment, options: MountOptions = {}): Promise<void> {
const position = options.position || "last-child";
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return Promise.resolve();
}
if (!(target instanceof HTMLElement)) {
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);
}
return new Promise((resolve, reject) => {
const fiber = new Fiber(null, this, undefined, undefined, false);
scheduler.addFiber(fiber, err => {
if (err) {
reject(err);
return;
}
if (!__owl__.isDestroyed) {
this.__patch(fiber.vnode);
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
}
}
resolve();
});
if (!__owl__.vnode) {
this.__prepareAndRender(fiber);
} else {
this.__render(fiber);
}
});
let inserter =
position === "last-child"
? el => target.appendChild(el)
: position === "first-child"
? el => target.prepend(el)
: el => {};
if (position === "self") {
this.__target = target as HTMLElement;
}
const fiber = new Fiber(null, this, false, inserter);
fiber.shouldPatch = false;
if (!__owl__.vnode) {
this.__prepareAndRender(fiber, () => {});
} else {
this.__render(fiber);
}
return scheduler.addFiber(fiber);
}
/**
@@ -332,26 +346,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;
}
return new Promise((resolve, reject) => {
const fiber = new Fiber(null, this, undefined, undefined, force);
scheduler.addFiber(fiber.root, err => {
if (err) {
reject(err);
return;
}
if (__owl__.isMounted && fiber === fiber.root) {
fiber.patchComponents();
}
resolve();
});
this.__render(fiber);
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);
}
/**
@@ -390,14 +412,7 @@ export class Component<T extends Env, Props extends {}> {
* willUnmount().
*/
trigger(eventType: string, payload?: any) {
if (this.el) {
const ev = new CustomEvent(eventType, {
bubbles: true,
cancelable: true,
detail: payload
});
this.el.dispatchEvent(ev);
}
this.__trigger(this, eventType, payload);
}
//--------------------------------------------------------------------------
@@ -437,27 +452,18 @@ export class Component<T extends Env, Props extends {}> {
__owl__.isDestroyed = true;
delete __owl__.vnode;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCancelled = true;
__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();
}
}
@@ -468,6 +474,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];
@@ -476,27 +486,40 @@ export class Component<T extends Env, Props extends {}> {
}
}
}
/**
* Private trigger method, allows to choose the component which triggered
* the event in the first place
*/
__trigger(component: Component<any, any>, eventType: string, payload?: any) {
if (this.el) {
const ev = new OwlEvent(component, eventType, {
bubbles: true,
cancelable: true,
detail: payload
});
const triggerHook = this.env[portalSymbol as any];
if (triggerHook) {
triggerHook(ev);
}
this.el.dispatchEvent(ev);
}
}
/**
* 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): Promise<void> {
this.__owl__.scope = scope;
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const __owl__ = this.__owl__;
const fiber = new Fiber(parentFiber, this, 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) {
@@ -509,7 +532,7 @@ export class Component<T extends Env, Props extends {}> {
this.willUpdateProps(nextProps),
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps)
]);
if (fiber.isCancelled) {
if (fiber.isCompleted) {
return;
}
this.props = nextProps;
@@ -522,27 +545,39 @@ 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;
if (this.__target) {
if (this.__target.tagName.toLowerCase() !== vnode.sel) {
throw new Error(
`Cannot attach '${this.constructor.name}' to target node (not same tag name)`
);
}
__owl__.vnode = patch(this.__target, vnode);
delete this.__target;
} else {
const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
}
}
/**
* The __prepare method is only called by the t-component directive, when a
* subcomponent is created. It gets its scope and vars, if any, from the
* subcomponent is created. It gets its scope, if any, from the
* parent template.
*/
__prepare(parentFiber: Fiber, scope: any, vars: any, previousSibling?: Fiber | null) {
const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force);
__prepare(parentFiber: Fiber, scope: any, cb: CallableFunction): Fiber {
this.__owl__.scope = scope;
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;
}
return this.__prepareAndRender(fiber);
parentFiber.lastChild = fiber;
this.__prepareAndRender(fiber, cb);
return fiber;
}
__getTemplate(qweb: QWeb): string {
@@ -564,19 +599,19 @@ export class Component<T extends Env, Props extends {}> {
}
return p._template;
}
async __prepareAndRender(fiber: Fiber) {
async __prepareAndRender(fiber: Fiber, cb: CallableFunction) {
try {
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
} catch (e) {
fiber.handleError(e);
fiber.vnode = h("div"); // -> we render this div at the end
return Promise.resolve();
}
if (this.__owl__.isDestroyed) {
return Promise.resolve();
}
if (!fiber.isCancelled) {
if (!fiber.isCompleted) {
this.__render(fiber);
cb();
}
}
@@ -585,49 +620,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");
fiber.handleError(e);
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;
}
/**
+24 -52
View File
@@ -190,10 +190,10 @@ QWeb.addDirective({
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//COMPONENT");
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
ctx.rootContext.shouldDefineScope = true;
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
// t-on- events and t-transition
@@ -226,22 +226,9 @@ QWeb.addDirective({
let propStr = Object.keys(props)
.map(k => k + ":" + props[k])
.join(",");
let defID = ctx.generateID();
let componentID = ctx.generateID();
let locationExpr = `\`__${ctx.generateID()}__`;
for (let i = 0; i < ctx.loopNumber - 1; i++) {
locationExpr += `\${i${i + 1}}__`;
}
if (ctx.lastNodeKey || ctx.currentKey) {
const k = ctx.lastNodeKey || ctx.currentKey;
ctx.addLine(`let templateId${componentID} = ${locationExpr}\` + ${k};`);
} else {
locationExpr += ctx.loopNumber ? `\${i${ctx.loopNumber}}__\`` : "`";
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 = "";
@@ -251,10 +238,6 @@ 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}.destroy();`;
if (ref) {
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
@@ -263,6 +246,7 @@ QWeb.addDirective({
finalizeComponentCode = `let finalize = () => {
${finalizeComponentCode}
};
delete w${componentID}.__owl__.transitionInserted;
utils.transitionRemove(vn, '${transition}', finalize);`;
}
@@ -301,7 +285,7 @@ QWeb.addDirective({
}
let eventsCode = events
.map(function([eventName, mods, handlerValue, extraArgs]) {
let params = "owner";
let params = "context";
if (extraArgs) {
if (ctx.loopNumber) {
let argId = ctx.generateID();
@@ -309,20 +293,20 @@ QWeb.addDirective({
// be set asynchronously later when the widget is ready, and the
// context might be different.
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
params = `owner, arg${argId}`;
params = `context, arg${argId}`;
} else {
params = `owner, ${ctx.formatExpression(extraArgs)}`;
params = `context, ${ctx.formatExpression(extraArgs)}`;
}
}
let handler = `function (e) {`;
let handler = `function (e) {if(!context.__owl__.isMounted){return}`;
handler += mods
.map(function(mod) {
return T_COMPONENT_MODS_CODE[mod];
})
.join("");
if (handlerValue) {
handler += `const fn = owner['${handlerValue}'];`;
handler += `if (fn) { fn.call(${params}, e); } else { owner.${handlerValue}; }`;
handler += `const fn = context['${handlerValue}'];`;
handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerValue}; }`;
}
handler += `}`;
return `vn.elm.addEventListener('${eventName}', ${handler});`;
@@ -334,7 +318,7 @@ 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 (shouldProxy) {
@@ -364,25 +348,9 @@ QWeb.addDirective({
}
// SLOTS
const varDefs: string[] = [];
const hasSlots = node.childNodes.length;
if (hasSlots) {
ctx.rootContext.shouldTrackScope = true;
for (let v of Object.values(ctx.variables)) {
if (v["id"]) {
varDefs.push(v["id"]);
}
}
}
let scopeVars;
if (hasSlots) {
let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
scopeVars = `${scope}, ${vars}`;
} else {
scopeVars = "undefined, undefined";
}
let scope = hasSlots ? `Object.assign(Object.create(context), scope)` : "undefined";
ctx.addIf(`w${componentID}`);
@@ -392,8 +360,7 @@ QWeb.addDirective({
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
}
ctx.addLine(
`w${componentID}.__updateProps(props${componentID}, extra.fiber${scopeVars &&
", " + scopeVars}, sibling)${styleCode};`
`w${componentID}.__updateProps(props${componentID}, extra.fiber, ${scope})${styleCode};`
);
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
if (registerCode) {
@@ -421,7 +388,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);
@@ -448,14 +421,13 @@ QWeb.addDirective({
}
}
ctx.addLine(`let def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars}, sibling);`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
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 fiber = w${componentID}.__prepare(extra.fiber, ${scope}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
const insertHook = refExpr ? `insert(vn) {${refExpr}},` : "";
ctx.addLine(
`def${defID}.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
`let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});`
);
if (registerCode) {
ctx.addLine(registerCode);
@@ -471,7 +443,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;
}
+102 -42
View File
@@ -1,4 +1,4 @@
import { VNode } from "../vdom/index";
import { h, VNode } from "../vdom/index";
import { Component } from "./component";
import { scheduler } from "./scheduler";
@@ -16,13 +16,17 @@ import { scheduler } from "./scheduler";
*/
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,
@@ -42,8 +46,9 @@ export class Fiber {
// scheduler.
counter: number = 0;
inserter: (el: HTMLElement) => void | null;
scope: any;
vars: any;
component: Component<any, any>;
vnode: VNode | null = null;
@@ -51,27 +56,58 @@ export class Fiber {
root: Fiber;
child: Fiber | null = null;
sibling: Fiber | null = null;
lastChild: Fiber | null = null;
parent: Fiber | null = null;
error?: Error;
constructor(parent: Fiber | null, component: Component<any, any>, scope, vars, force) {
this.force = force;
this.scope = scope;
this.vars = vars;
constructor(parent: Fiber | null, component: Component<any, any>, force, inserter) {
this.component = component;
this.force = force;
this.inserter = inserter;
const __owl__ = component.__owl__;
this.scope = __owl__.scope;
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++;
}
/**
@@ -82,14 +118,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 {
@@ -132,48 +172,69 @@ 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;
this.isCompleted = true;
if (!this.inserter && !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;
const patchLen = patchQueue.length;
try {
for (let i = 0; i < patchLen; i++) {
component = patchQueue[i].component;
// call willPatch hook on each fiber of patchQueue
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
if (fiber.shouldPatch) {
component = fiber.component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
}
} catch (e) {
console.error(e);
}
for (let i = 0; i < patchLen; i++) {
// call __patch on each fiber of (reversed) patchQueue
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode);
component.__patch(fiber.vnode!);
if (!fiber.shouldPatch && (!fiber.inserter || i !== 0)) {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
}
component.__owl__.currentFiber = null;
}
try {
for (let i = patchLen - 1; i >= 0; i--) {
component = patchQueue[i].component;
// insert into the DOM (mount case)
let inDOM = false;
if (this.inserter) {
this.inserter(this.component.el!);
inDOM = document.body.contains(this.component.el);
this.component.env.qweb.trigger("dom-appended");
}
// call patched/mounted hook on each fiber of (reversed) patchQueue
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
if (fiber.shouldPatch && !this.inserter) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
} else if (this.inserter ? inDOM : true) {
component.__callMounted();
}
} catch (e) {
console.error(e);
}
}
@@ -185,7 +246,7 @@ export class Fiber {
if (!f.isRendered) {
f.root.counter--;
}
f.isCancelled = true;
f.isCompleted = true;
return f.child;
});
}
@@ -199,10 +260,12 @@ export class Fiber {
* being in a corrupted state.
*/
handleError(error: Error) {
let canCatch = false;
let component = this.component;
let qweb = component.env.qweb;
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!;
@@ -210,10 +273,7 @@ export class Fiber {
qweb.trigger("error", error);
if (canCatch) {
setTimeout(() => {
console.error(error);
component.catchError!(error);
});
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
+7 -4
View File
@@ -48,7 +48,7 @@ QWeb.utils.validateProps = function(Widget, props: Object) {
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) {
@@ -89,13 +89,16 @@ function isValidProp(prop, propDef): boolean {
if (propDef.optional && prop === undefined) {
return true;
}
let result = isValidProp(prop, propDef.type);
if (propDef.type === Array) {
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]);
+43 -11
View File
@@ -25,14 +25,38 @@ export class Scheduler {
this.requestAnimationFrame = requestAnimationFrame;
}
addFiber(fiber, callback) {
this.tasks.push({ fiber, callback });
if (this.isRunning) {
return;
}
start() {
this.isRunning = true;
this.scheduleTasks();
}
stop() {
this.isRunning = false;
}
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.start();
}
});
}
/**
* Process all current tasks. This only applies to the fibers that are ready.
* Other tasks are left unchanged.
@@ -41,26 +65,34 @@ 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) {
task.callback(task.fiber.error);
if (!task.fiber.error) {
try {
task.fiber.complete();
} catch (e) {
task.fiber.handleError(e);
}
}
task.callback();
return false;
}
return true;
});
this.tasks = tasks.concat(this.tasks);
if (this.tasks.length === 0) {
this.stop();
}
}
scheduleTasks() {
this.isRunning = true;
this.requestAnimationFrame(() => {
this.flush();
if (this.tasks.length > 0) {
if (this.isRunning) {
this.scheduleTasks();
} else {
this.isRunning = false;
}
});
}
+17 -12
View File
@@ -2,7 +2,7 @@ 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,
@@ -45,7 +45,16 @@ 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 = [];
}
@@ -72,13 +81,7 @@ export class Context extends EventBus {
const subscriptions = this.subscriptions.update;
const groups = partitionBy(subscriptions, s => (s.owner ? s.owner.__owl__.depth : -1));
for (let group of groups) {
const proms = Promise.all(
group.map(sub => {
if (sub.owner ? sub.owner.__owl__.isMounted : true) {
return sub.callback.call(sub.owner, rev);
}
})
);
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
@@ -87,7 +90,7 @@ export class Context extends EventBus {
// promise to resolve earlier, which means that there is a chance of
// processing the next group in the same frame.
scheduler.flush();
await proms;
await Promise.all(proms);
}
}
}
@@ -135,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;
}
+2 -11
View File
@@ -20,18 +20,9 @@
export class Observer {
rev: number = 1;
allowMutations: boolean = true;
dirty: boolean = false;
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) {
@@ -65,7 +56,7 @@ export class Observer {
}
self._updateRevNumber(target);
target[key] = newVal;
self.notifyChange();
self.notifyCB();
}
return true;
},
@@ -73,7 +64,7 @@ export class Observer {
if (key in target) {
delete target[key];
self._updateRevNumber(target);
self.notifyChange();
self.notifyCB();
}
return true;
}
+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;
}
}
+2 -1
View File
@@ -12,6 +12,7 @@ import * as _store from "./store";
import * as _utils from "./utils";
import * as _tags from "./tags";
import { AsyncRoot } from "./misc/async_root";
import { Portal } from "./misc/portal";
import * as _hooks from "./hooks";
import * as _context from "./context";
import { Link } from "./router/link";
@@ -29,7 +30,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, Portal };
export const hooks = Object.assign({}, _hooks, {
useContext: _context.useContext,
useDispatch: _store.useDispatch,
+167
View File
@@ -0,0 +1,167 @@
import { Component, portalSymbol } from "../component/component";
import { VNode, patch } from "../vdom/index";
import { xml } from "../tags";
import { OwlEvent } from "../core/owl_event";
import { useSubEnv } from "../hooks";
/**
* Portal
*
* The Portal component allows to render a part of a component outside it's DOM.
* It is for example useful for dialogs: for css reasons, dialogs are in general
* placed in a specific spot of the DOM (e.g. directly in the body). With the
* Portal, a component can conditionally specify in its tempate that it contains
* a dialog, and where this dialog should be inserted in the DOM.
*
* The Portal component ensures that the communication between the content of
* the Portal and its parent properly works: business events reaching the Portal
* are re-triggered on an empty <portal> node located in the parent's DOM.
*/
export class Portal extends Component<any, any> {
static template = xml`<portal><t t-slot="default"/></portal>`;
static props = {
target: {
type: String
}
};
// boolean to indicate whether or not we must listen to 'dom-appended' event
// to hook on the moment when the target is inserted into the DOM (because it
// is not when the portal is rendered)
doTargetLookUp: boolean = true;
// set of encountered events that need to be redirected
_handledEvents: Set<string> = new Set();
// function that will be the event's tunnel (needs to be an arrow function to
// avoid having to rebind `this`)
_handlerTunnel: (f: OwlEvent<any>) => void = (ev: OwlEvent<any>) => {
ev.stopPropagation();
this.__trigger(ev.originalComponent, ev.type, ev.detail);
};
// Storing the parent's env
parentEnv: any = null;
// represents the element that is moved somewhere else
portal: VNode | null = null;
// the target where we will move `portal`
target: HTMLElement | null = null;
constructor(parent, props) {
super(parent, props);
this.parentEnv = parent ? parent.env : {};
// put a callback in the env that is propagated to children s.t. portal can
// register an handler to those events just before children will trigger them
useSubEnv({
[portalSymbol]: ev => {
if (!this._handledEvents.has(ev.type)) {
this.portal!.elm!.addEventListener(ev.type, this._handlerTunnel);
this._handledEvents.add(ev.type);
}
}
});
}
/**
* Override to revert back to a classic Component's structure
*
* @override
*/
__callWillUnmount() {
super.__callWillUnmount();
this.el!.appendChild(this.portal!.elm!);
this.doTargetLookUp = true;
}
/**
* At each DOM change, we must ensure that the portal contains exactly one
* child
*/
__checkVNodeStructure(vnode: VNode) {
const children = vnode.children!;
let countRealNodes = 0;
for (let child of children) {
if ((child as VNode).sel) {
countRealNodes++;
}
}
if (countRealNodes !== 1) {
throw new Error(`Portal must have exactly one non-text child (has ${countRealNodes})`);
}
}
/**
* Ensure the target is still there at whichever time we render
*/
__checkTargetPresence() {
if (!this.target || !document.contains(this.target)) {
throw new Error(`Could not find any match for "${this.props.target}"`);
}
}
/**
* Move the portal's element to the target
*/
__deployPortal() {
this.__checkTargetPresence();
this.target!.appendChild(this.portal!.elm!);
}
/**
* Override to remove from the DOM the element we have teleported
*
* @override
*/
__destroy(parent) {
if (this.portal && this.portal.elm) {
const displacedElm = this.portal.elm!;
const parent = displacedElm.parentNode;
if (parent) {
parent.removeChild(displacedElm);
}
}
super.__destroy(parent);
}
/**
* Override to patch the element that has been teleported
*
* @override
*/
__patch(vnode) {
if (this.doTargetLookUp) {
const target = document.querySelector(this.props.target);
if (!target) {
this.env.qweb.on("dom-appended", this, () => {
this.doTargetLookUp = false;
this.env.qweb.off("dom-appended", this);
this.target = document.querySelector(this.props.target);
this.__deployPortal();
});
} else {
this.doTargetLookUp = false;
this.target = target;
}
}
this.__checkVNodeStructure(vnode);
const shouldDeploy =
(!this.portal || this.el!.contains(this.portal.elm!)) && !this.doTargetLookUp;
if (!this.doTargetLookUp && !shouldDeploy) {
// Only on pure patching, provided the
// this.target's parent has not been unmounted
this.__checkTargetPresence();
}
const portalPatch = this.portal ? this.portal : document.createElement(vnode.children[0].sel);
this.portal = patch(portalPatch, vnode.children![0] as VNode);
vnode.children = [];
super.__patch(vnode);
if (shouldDeploy) {
this.__deployPortal();
}
}
/**
* Override to set the env
*/
__trigger(component: Component<any, any>, eventType: string, payload?: any) {
const env = this.env;
this.env = this.parentEnv;
super.__trigger(component, eventType, payload);
this.env = env;
}
}
+134 -107
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";
@@ -20,35 +19,40 @@ import { htmlToVDOM } from "../vdom/html_to_vdom";
//------------------------------------------------------------------------------
// t-esc and t-raw
//------------------------------------------------------------------------------
QWeb.utils.getFragment = function(str: string): DocumentFragment {
const temp = document.createElement("template");
temp.innerHTML = str;
return temp.content;
};
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.xml instanceof NodeList) {
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
ctx.rootContext.shouldDefineScope = true;
if (value === "0") {
if (ctx.parentNode) {
// the 'zero' magical symbol is where we can find the result of the rendering
// of the body of the t-call.
ctx.rootContext.shouldDefineUtils = true;
const zeroArgs = ctx.escaping
? `{text: utils.vDomToString(scope[utils.zero])}`
: `...scope[utils.zero]`;
ctx.addLine(`c${ctx.parentNode}.push(${zeroArgs});`);
}
return;
}
let exprID: string;
if (typeof value === "string") {
exprID = `_${ctx.generateID()}`;
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
} else {
exprID = value.id;
exprID = `scope.${value.id}`;
}
ctx.addIf(`${exprID} || ${exprID} === 0`);
if (ctx.escaping) {
let protectID;
if (value.hasBody) {
protectID = ctx.startProtectScope();
ctx.addLine(
`${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};`
);
}
if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
} else if (ctx.parentNode) {
@@ -62,9 +66,19 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
ctx.addLine(`result = vn${nodeID}`);
}
}
if (value.hasBody) {
ctx.stopProtectScope(protectID);
}
} else {
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
if (value.hasBody) {
ctx.addLine(
`const vnodeArray = ${exprID} instanceof utils.VDomArray ? ${exprID} : utils.htmlToVDOM(${exprID});`
);
ctx.addLine(`c${ctx.parentNode}.push(...vnodeArray);`);
} else {
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
}
}
if (node.childNodes.length) {
ctx.addElse();
@@ -101,25 +115,46 @@ QWeb.addDirective({
name: "set",
extraNames: ["value"],
priority: 60,
atNodeEncounter({ node, ctx }): boolean {
atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldDefineScope = true;
const variable = node.getAttribute("t-set")!;
let value = node.getAttribute("t-value")!;
ctx.variables[variable] = ctx.variables[variable] || {};
let qwebvar = ctx.variables[variable];
const hasBody = node.hasChildNodes();
qwebvar.id = variable;
qwebvar.expr = `scope.${variable}`;
if (value) {
const formattedValue = ctx.formatExpression(value);
if (ctx.variables.hasOwnProperty(variable)) {
ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`);
} else {
const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`);
ctx.variables[variable] = {
id: varName,
expr: formattedValue
};
ctx.addLine(`${qwebvar.expr} = ${formattedValue};`);
qwebvar.value = formattedValue;
}
if (hasBody) {
ctx.rootContext.shouldDefineUtils = true;
if (value) {
ctx.addIf(`!(${qwebvar.expr})`);
}
const tempParentNodeID = ctx.generateID();
const _parentNode = ctx.parentNode;
ctx.parentNode = tempParentNodeID;
ctx.addLine(`const c${tempParentNodeID} = new utils.VDomArray();`);
const nodeCopy = node.cloneNode(true) as Element;
for (let attr of ["t-set", "t-value", "t-if", "t-else", "t-elif"]) {
nodeCopy.removeAttribute(attr);
}
qweb._compileNode(nodeCopy, ctx);
ctx.addLine(`${qwebvar.expr} = c${tempParentNodeID}`);
qwebvar.value = `c${tempParentNodeID}`;
qwebvar.hasBody = true;
ctx.parentNode = _parentNode;
if (value) {
ctx.closeIf();
}
} else {
ctx.variables[variable] = {
xml: node.childNodes
};
}
return true;
}
@@ -133,7 +168,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) : `scope.${cond.id!}`);
return false;
},
finalize({ ctx }) {
@@ -146,7 +181,9 @@ QWeb.addDirective({
priority: 30,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-elif")!);
ctx.addLine(`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : cond.id}) {`);
ctx.addLine(
`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id}`}) {`
);
ctx.indent();
return false;
},
@@ -175,6 +212,9 @@ QWeb.addDirective({
name: "call",
priority: 50,
atNodeEncounter({ node, qweb, ctx }): boolean {
// Step 1: sanity checks
// ------------------------------------------------
ctx.rootContext.shouldDefineScope = true;
if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')");
}
@@ -183,79 +223,63 @@ QWeb.addDirective({
if (!nodeTemplate) {
throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
}
const nodeCopy = node.cloneNode(true) as Element;
nodeCopy.removeAttribute("t-call");
// 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
const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
// compile sub template
let subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
subCtx = subCtx.subContext("templates", templateMap);
if (templateMap[subTemplate]) {
// OUCH, IT IS A RECURSIVE TEMPLATE SITUATION...
// This is a tricky situation... We obviously cannot inline the compiled
// template. So, what we need to do is to compile it, and make sure we
// properly transfer everything from the current scope to the sub template.
ctx.rootContext.shouldTrackScope = true;
ctx.rootContext.shouldDefineOwner = true;
let subTemplateName;
if (ctx.hasParentWidget) {
subTemplateName = ctx.templateName;
} else {
subTemplateName = `__${ctx.generateID()}`;
subCtx.variables = {};
let id = 0;
for (let v in vars) {
subCtx.variables[v] = vars[v];
(vars[v] as any).id = `_v${id++}`;
}
const subTemplateFn = qweb._compile(subTemplateName, nodeTemplate.elem, subCtx);
qweb.recursiveFns[subTemplateName] = subTemplateFn;
}
let varCode = `{}`;
if (Object.keys(vars).length) {
let id = 0;
const content = Object.values(vars)
.map((v: any) => `_v${id++}: ${v.expr}`)
.join(",");
varCode = `{${content}}`;
}
ctx.addLine(
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`
);
return true;
// Step 2: compile target template in sub templates
// ------------------------------------------------
if (!qweb.subTemplates[subTemplate]) {
qweb.subTemplates[subTemplate] = true;
const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx);
qweb.subTemplates[subTemplate] = subTemplateFn;
}
templateMap[subTemplate] = true;
if (hasNewVariables) {
ctx.addLine("{");
// Step 3: compile t-call body if necessary
// ------------------------------------------------
let hasBody = node.hasChildNodes();
let protectID;
if (hasBody) {
// we add a sub scope to protect the ambient scope
ctx.addLine(`{`);
ctx.indent();
// 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};`);
}
// todo: handle XML variables...
}
}
qweb._compileNode(nodeTemplate.elem, subCtx);
// close new scope
if (hasNewVariables) {
protectID = ctx.startProtectScope();
const nodeCopy = node.cloneNode(true) as Element;
nodeCopy.removeAttribute("t-call");
const parentNode = ctx.parentNode;
ctx.parentNode = "__0";
// this local scope is intended to trap c__0
ctx.addLine(`{`);
ctx.indent();
ctx.addLine("let c__0 = [];");
qweb._compileNode(nodeCopy, ctx);
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine("scope[utils.zero] = c__0;");
ctx.parentNode = parentNode;
ctx.dedent();
ctx.addLine("}");
ctx.addLine(`}`);
}
// Step 4: add the appropriate function call to current component
// ------------------------------------------------
const callingScope = hasBody ? "scope" : "Object.assign(Object.create(context), scope)";
if (ctx.parentNode) {
ctx.addLine(
`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, Object.assign({}, extra, {parentNode: c${ctx.parentNode}}));`
);
} else {
// this is a t-call with no parentnode, we need to extract the result
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = []`);
ctx.addLine(
`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, Object.assign({}, extra, {parentNode: result}));`
);
ctx.addLine(`result = result[0]`);
}
// Step 5: restore previous scope
// ------------------------------------------------
if (hasBody) {
ctx.stopProtectScope(protectID);
ctx.dedent();
ctx.addLine(`}`);
}
return true;
@@ -270,7 +294,7 @@ QWeb.addDirective({
extraNames: ["as"],
priority: 10,
atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldProtectContext = true;
ctx.rootContext.shouldDefineScope = true;
ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1);
const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!;
@@ -285,14 +309,16 @@ QWeb.addDirective({
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf();
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
let varsID = ctx.startProtectScope();
const loopVar = `i${ctx.loopNumber}`;
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
ctx.indent();
ctx.addToScope(name + "_first", `${loopVar} === 0`);
ctx.addToScope(name + "_last", `${loopVar} === _length${keysID} - 1`);
ctx.addToScope(name + "_index", loopVar);
ctx.addToScope(name, `_${keysID}[${loopVar}]`);
ctx.addToScope(name + "_value", `_${valuesID}[${loopVar}]`);
ctx.addLine(`scope.${name}_first = ${loopVar} === 0`);
ctx.addLine(`scope.${name}_last = ${loopVar} === _length${keysID} - 1`);
ctx.addLine(`scope.${name}_index = ${loopVar}`);
ctx.addLine(`scope.${name} = _${keysID}[${loopVar}]`);
ctx.addLine(`scope.${name}_value = _${valuesID}[${loopVar}]`);
const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn =
!nodeCopy.hasAttribute("t-key") &&
@@ -309,6 +335,7 @@ QWeb.addDirective({
qweb._compileNode(nodeCopy, ctx);
ctx.dedent();
ctx.addLine("}");
ctx.stopProtectScope(varsID);
return true;
}
});
+44 -40
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,75 +6,73 @@ export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------
export class CompilationContext {
nextID: number = 1;
static nextID: number = 1;
code: string[] = [];
variables: { [key: string]: QWebVar } = {};
escaping: boolean = false;
parentNode: number | null = null;
parentNode: number | null | string = null;
parentTextNode: number | null = null;
rootNode: number | null = null;
indentLevel: number = 0;
rootContext: CompilationContext;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldDefineParent: boolean = false;
shouldDefineScope: boolean = false;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false;
shouldDefineResult: boolean = true;
shouldDefineSibling: boolean = true;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
loopNumber: number = 0;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false;
scopeVars: any[] = [];
currentKey: string = "";
lastNodeKey: string = ""; // temp variable to communicate to previous caller
templates: { [key: string]: boolean } = {};
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
this.templates[this.templateName] = true;
this.addLine("var h = this.h;");
}
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[] {
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
if (shouldTrackScope) {
// add some vars to scope if needed
for (let scopeVar of this.scopeVars.reverse()) {
let { index, key, indent } = scopeVar;
const prefix = new Array(indent + 2).join(" ");
this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
}
this.code.unshift(" const scope = Object.create(null);");
}
if (this.shouldProtectContext) {
this.code.unshift(" context = Object.create(context);");
}
if (this.shouldDefineResult) {
this.code.unshift(" let result;");
}
if (this.shouldDefineSibling) {
this.code.unshift(" let sibling = null;");
if (this.shouldDefineScope) {
this.code.unshift(" let scope = Object.create(context);");
}
if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
}
if (this.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
this.code.unshift(" let owner = context;");
}
if (this.shouldDefineParent) {
if (this.hasParentWidget) {
this.code.unshift(" let parent = extra.parent;");
@@ -128,11 +126,6 @@ export class CompilationContext {
return this.code.length - 1;
}
addToScope(key: string, expr: string) {
const index = this.addLine(`context.${key} = ${expr};`);
this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
}
addIf(condition: string) {
this.addLine(`if (${condition}) {`);
this.indent();
@@ -149,7 +142,7 @@ export class CompilationContext {
this.addLine("}");
}
getValue(val: any): QWebExprVar | string {
getValue(val: any): QWebVar | string {
return val in this.variables ? this.getValue(this.variables[val]) : val;
}
@@ -160,6 +153,7 @@ export class CompilationContext {
* - replace already defined variables by their internal name
*/
formatExpression(expr: string): string {
this.rootContext.shouldDefineScope = true;
return compileExpr(expr, this.variables);
}
@@ -180,4 +174,14 @@ export class CompilationContext {
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`";
}
startProtectScope(): number {
const protectID = this.generateID();
this.rootContext.shouldDefineScope = true;
this.addLine(`const _origScope${protectID} = scope;`);
this.addLine(`scope = Object.assign(Object.create(context), scope);`);
return protectID;
}
stopProtectScope(protectID: number) {
this.addLine(`scope = _origScope${protectID};`);
}
}
+43 -26
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,13 @@ const WORD_REPLACEMENT = {
lte: "<="
};
export interface QWebExprVar {
id: string;
expr: string;
export interface QWebVar {
id: string; // foo
expr: string; // scope.foo (local variables => only foo)
value?: string; // 1 + 3
hasBody?: boolean;
}
export interface QWebXMLVar {
xml: NodeList;
}
export type QWebVar = QWebExprVar | QWebXMLVar;
//------------------------------------------------------------------------------
// Tokenizer
//------------------------------------------------------------------------------
@@ -68,6 +64,7 @@ type TKind =
interface Token {
type: TKind;
value: string;
originalValue?: string;
size?: number;
}
@@ -82,7 +79,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 +164,9 @@ const tokenizeOperator: Tokenizer = function(expr) {
const TOKENIZERS = [
tokenizeString,
tokenizeNumber,
tokenizeOperator,
tokenizeSymbol,
tokenizeStatic,
tokenizeOperator
tokenizeStatic
];
/**
@@ -230,35 +229,53 @@ export function tokenize(expr: string): Token[] {
* - unless the previous token is a dot (in that case, this is a property: `a.b`)
* - or if the previous token is a left brace or a comma, and the next token is
* a colon (in that case, this is an object key: `{a: b}`)
*
* Some specific code is also required to support arrow functions. If we detect
* the arrow operator, then we add the current (or some previous tokens) token to
* the list of variables so it does not get replaced by a lookup in the context
*/
export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): string {
export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): string {
scope = Object.create(scope);
const tokens = tokenize(expr);
let result = "";
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
let prevToken = tokens[i - 1];
let nextToken = tokens[i + 1];
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
// we need to find if it is a variable
let isVar = true;
let prevToken = tokens[i - 1];
if (prevToken) {
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false;
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
let nextToken = tokens[i + 1];
if (nextToken && nextToken.type === "COLON") {
isVar = false;
}
}
}
if (isVar) {
if (token.value in vars && "id" in vars[token.value]) {
token.value = (<QWebExprVar>vars[token.value]).id;
} else {
token.value = `context['${token.value}']`;
}
if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
if (token.type === "RIGHT_PAREN") {
let j = i - 1;
while (j > 0 && tokens[j].type !== "LEFT_PAREN") {
if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
tokens[j].value = tokens[j].originalValue!;
scope[tokens[j].value] = { id: tokens[j].value, expr: tokens[j].value };
}
j--;
}
} else {
scope[token.value] = { id: token.value, expr: token.value };
}
}
if (isVar) {
if (token.value in scope && "id" in scope[token.value]) {
token.value = scope[token.value].expr!;
} else {
token.originalValue = token.value;
token.value = `scope['${token.value}']`;
}
}
result += token.value;
}
return result;
return tokens.map(t => t.value).join("");
}
+30 -13
View File
@@ -30,7 +30,6 @@ QWeb.addDirective({
name: "on",
priority: 90,
atNodeCreation({ ctx, fullName, value, nodeID }) {
ctx.rootContext.shouldDefineOwner = true;
const [eventName, ...mods] = fullName.slice(5).split(".");
if (!eventName) {
throw new Error("Missing event name with t-on directive");
@@ -40,8 +39,8 @@ QWeb.addDirective({
extraArgs = args.slice(1, -1);
return "";
});
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
let handler = `function (e) {`;
let params = extraArgs ? `context, ${ctx.formatExpression(extraArgs)}` : "context";
let handler = `function (e) {if (!context.__owl__.isMounted){return}`;
handler += mods
.map(function(mod) {
return MODS_CODE[mod];
@@ -105,6 +104,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");
@@ -123,6 +124,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();
@@ -190,9 +194,8 @@ QWeb.addDirective({
QWeb.addDirective({
name: "slot",
priority: 80,
atNodeEncounter({ ctx, value }): boolean {
atNodeEncounter({ ctx, value, node, qweb }): boolean {
const slotKey = ctx.generateID();
ctx.rootContext.shouldDefineOwner = true;
ctx.addLine(
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
);
@@ -201,16 +204,22 @@ 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__.scope, Object.assign({}, extra, {parentNode: ${parentNode}, parent: extra.parent || context}));`
);
if (!ctx.parentNode) {
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`);
}
if (node.hasChildNodes()) {
ctx.addElse();
const nodeCopy = <Element>node.cloneNode(true);
nodeCopy.removeAttribute("t-slot");
qweb._compileNode(nodeCopy, ctx);
}
ctx.closeIf();
return true;
}
@@ -231,7 +240,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};`);
@@ -255,10 +274,8 @@ 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}];`);
}
});
@@ -271,6 +288,6 @@ QWeb.addDirective({
atNodeEncounter({ ctx, value }) {
let id = ctx.generateID();
ctx.addLine(`const nodeKey${id} = ${ctx.formatExpression(value)};`);
ctx.lastNodeKey = `nodeKey${id}`;
ctx.currentKey = `nodeKey${id}`;
}
});
+37 -26
View File
@@ -87,6 +87,7 @@ interface Utils {
}
const UTILS: Utils = {
zero: Symbol("zero"),
toObj(expr) {
if (typeof expr === "string") {
expr = expr.trim();
@@ -105,6 +106,20 @@ const UTILS: Utils = {
shallowEqual,
addNameSpace(vnode) {
addNS(vnode.data, vnode.children, vnode.sel);
},
VDomArray: class VDomArray extends Array {},
vDomToString: function(vdom: VNode[]): string {
return vdom
.map(vnode => {
if (vnode.sel) {
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
} else {
return vnode.text;
}
})
.join();
}
};
@@ -171,7 +186,7 @@ export class QWeb extends EventBus {
// recursiveTemplates contains sub templates called with t-call, but which
// ends up in recursive situations. This is very similar to the slot situation,
// as in we need to propagate the scope.
recursiveFns = {};
subTemplates = {};
isUpdating: boolean = false;
translateFn?: QWebConfig["translateFn"];
@@ -360,24 +375,12 @@ export class QWeb extends EventBus {
ctx.shouldDefineResult = false;
}
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;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
for (let v in parentContext.variables) {
let variable = <any>parentContext.variables[v];
if (variable.id) {
ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`);
}
}
}
if (parentContext) {
ctx.addLine(" Object.assign(context, extra.fiber.scope);");
}
this._compileNode(elem, ctx);
@@ -393,12 +396,13 @@ export class QWeb extends EventBus {
}
let code = ctx.generateCode();
const templateName = ctx.templateName.replace(/`/g, "'").slice(0, 200);
code.unshift(` // Template name: "${templateName}"`);
let template;
try {
template = new Function("context", "extra", code.join("\n")) as CompiledTemplate;
template = new Function("context, extra", code.join("\n")) as CompiledTemplate;
} catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(code.join("\n"));
console.groupEnd();
@@ -454,12 +458,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;
@@ -540,7 +551,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] || [];
@@ -644,13 +654,14 @@ export class QWeb extends EventBus {
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
const attID = ctx.generateID();
if (name === "class") {
let classDef = value
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
if ((value = value.trim())) {
let classDef = value
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
} else {
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
@@ -666,7 +677,7 @@ export class QWeb extends EventBus {
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
const v = ctx.getValue(value);
let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : v.id;
let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : `scope.${v.id}`;
if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true;
@@ -724,7 +735,7 @@ export class QWeb extends EventBus {
}
}
let nodeID = ctx.generateID();
let nodeKey = ctx.lastNodeKey || nodeID;
let nodeKey = ctx.currentKey || nodeID;
const parts = [`key:${nodeKey}`];
if (attrs.length + tattrs.length > 0) {
parts.push(`attrs:{${attrs.join(",")}}`);
+29 -17
View File
@@ -77,58 +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);
if (!(store instanceof Store)) {
throw new Error(`No store found when connecting '${component.constructor.name}'`);
}
let result = selector(store.state, component.props);
const hashFn = store.observer.revNumber.bind(store.observer);
let revNumber = hashFn(result) || result;
let revNumber = hashFn(result);
const isEqual = options.isEqual || isStrictEqual;
if (!store.updateFunctions[component.__owl__.id]) {
store.updateFunctions[component.__owl__.id] = [];
if (!store.updateFunctions[componentId]) {
store.updateFunctions[componentId] = [];
}
const updateFunctions = store.updateFunctions[component.__owl__.id];
updateFunctions.push(function(): boolean {
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 => {
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);
}
+1 -1
View File
@@ -219,7 +219,7 @@ function updateClass(oldVnode: VNode, vnode: VNode): void {
elm = vnode.elm as Element;
for (name in oldClass) {
if (!klass[name]) {
if (name && !klass[name]) {
elm.classList.remove(name);
}
}
+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) {
+102 -53
View File
@@ -1,100 +1,149 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`animations t-transition combined with component 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"Parent\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let sibling = null;
let scope = Object.create(context);
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 props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
let k3 = \`__4__\`;
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode;
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w3.destroy();
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k3] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
delete w2.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
w2.__owl__.pvnode = pvnode;
}
sibling = w3.__owl__.currentFiber || sibling;
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`animations t-transition combined with t-component and t-if 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"Parent\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let sibling = null;
let scope = Object.create(context);
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].display) {
if (scope['state'].display) {
//COMPONENT
let templateId3 = \`__4__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
let k3 = \`__4__\`;
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode;
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w3.destroy();
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k3] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
delete w2.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
w2.__owl__.pvnode = pvnode;
}
sibling = w3.__owl__.currentFiber || sibling;
w2.__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
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (scope['state'].flag) {
//COMPONENT
let k3 = \`__4__\`;
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k3] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {let finalize = () => {
w2.destroy();
};
delete w2.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
}
return vn1;
}"
`;
exports[`animations t-transition with no delay/duration 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
@@ -112,10 +161,10 @@ exports[`animations t-transition with no delay/duration 1`] = `
`;
exports[`animations t-transition, on a simple node (insert) 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
+72 -23
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";
//------------------------------------------------------------------------------
@@ -321,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();
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";
@@ -357,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="__4__">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="__4__">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="__4__">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="__4__">blue</span></div>');
QWeb.utils.transitionInsert = oldTransitionInsert;
});
});
File diff suppressed because it is too large Load Diff
@@ -1,42 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`props validation props are validated in dev mode (code snapshot) 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"App\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let sibling = null;
let scope = Object.create(context);
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 props3 = {message:1};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
let k3 = \`__4__\`;
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
let props2 = {message:1};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode;
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap[k3] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
w2.__owl__.pvnode = pvnode;
}
sibling = w3.__owl__.currentFiber || sibling;
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
File diff suppressed because it is too large Load Diff
+98 -8
View File
@@ -138,7 +138,7 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component '_a'`);
expect(error.message).toBe("Invalid Prop 'p' in component '_a'");
}
});
@@ -195,7 +195,7 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component '_a'`);
expect(error.message).toBe("Invalid Prop 'p' in component '_a'");
}
});
@@ -240,7 +240,7 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Props 'p' of invalid type in component 'TestWidget'");
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
});
test("can validate an optional props", async () => {
@@ -284,7 +284,7 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`);
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
});
test("can validate an array with given primitive type", async () => {
@@ -389,7 +389,7 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`);
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
});
test("can validate an object with simple shape", async () => {
@@ -436,7 +436,7 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`);
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
error = undefined;
try {
@@ -447,7 +447,7 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`);
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
});
test("can validate recursively complicated prop def", async () => {
@@ -499,7 +499,7 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`);
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
});
test("can validate optional attributes in nested sub props", () => {
@@ -535,6 +535,70 @@ describe("props validation", () => {
);
});
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 () => {
env.qweb.addTemplates(`
<templates>
@@ -581,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"];
+35 -1
View File
@@ -106,7 +106,7 @@ describe("Context", () => {
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
});
test("two async components on two levels are updated in parallel", async () => {
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[] = [];
@@ -151,6 +151,11 @@ describe("Context", () => {
);
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>"
@@ -255,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();
+1 -1
View File
@@ -408,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 () => {
+7
View File
@@ -1,6 +1,7 @@
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";
@@ -20,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);
@@ -85,6 +87,11 @@ export function renderToDOM(
context: EvalContext = {},
extra?: any
): HTMLElement | Text {
if (!context.__owl__) {
// we add `__owl__` to better simulate a component as context. This is
// particularly important for event handlers added with the `t-on` directive.
context.__owl__ = { isMounted: true };
}
const vnode = qweb.render(template, context, extra);
// we snapshot here the compiled code. This is useful to prevent unwanted code
+890
View File
@@ -0,0 +1,890 @@
import { Portal } from "../../src/misc/portal";
import { xml } from "../../src/tags";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
import { Component } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { QWeb } from "../../src/qweb";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - outside: a div with id #outside appended into fixture, meant to be used as
// target by Portal component
// - a test env, necessary to create components, that is set on Component
let fixture: HTMLElement;
let outside: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
outside = document.createElement("div");
outside.setAttribute("id", "outside");
fixture.appendChild(outside);
Component.env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
describe("Portal: Props validation", () => {
test("target is mandatory", async () => {
const dev = QWeb.dev;
QWeb.dev = true;
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal>
<div>2</div>
</Portal>
</div>`;
}
let error;
try {
const parent = new Parent();
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'target' (component 'Portal')`);
QWeb.dev = dev;
});
test("target is not list", async () => {
const dev = QWeb.dev;
QWeb.dev = true;
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="['body']">
<div>2</div>
</Portal>
</div>`;
}
let error;
try {
const parent = new Parent();
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Invalid Prop 'target' in component 'Portal'`);
QWeb.dev = dev;
});
});
describe("Portal: Basic use and DOM placement", () => {
test("basic use of portal", async () => {
const dev = QWeb.dev;
QWeb.dev = true;
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<span>1</span>
<Portal target="'#outside'">
<div>2</div>
</Portal>
</div>`;
}
let error;
let parent;
try {
parent = new Parent();
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<div>2</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
QWeb.dev = dev;
});
test("conditional use of Portal", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<span>1</span>
<Portal target="'#outside'" t-if="state.hasPortal">
<div>2</div>
</Portal>
</div>`;
state = useState({ hasPortal: false });
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.hasPortal = true;
await nextTick();
expect(outside.innerHTML).toBe("<div>2</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
parent.state.hasPortal = false;
await nextTick();
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.hasPortal = true;
await nextTick();
expect(outside.innerHTML).toBe("<div>2</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
});
test("conditional use of Portal (with sub Component)", async () => {
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="props.val"/></div>`;
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<span>1</span>
<Portal t-if="state.hasPortal" target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ hasPortal: false, val: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.hasPortal = true;
await nextTick();
expect(outside.innerHTML).toBe("<div>1</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
parent.state.hasPortal = false;
await nextTick();
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.val = 2;
await nextTick();
expect(outside.innerHTML).toBe("");
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
parent.state.hasPortal = true;
await nextTick();
expect(outside.innerHTML).toBe("<div>2</div>");
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
});
test("with target in template (before portal)", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<div id="local-target"></div>
<span>1</span>
<Portal target="'#local-target'">
<p>2</p>
</Portal>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(parent.el!.innerHTML).toBe(
'<div id="local-target"><p>2</p></div><span>1</span><portal></portal>'
);
});
test("with target in template (after portal)", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<span>1</span>
<Portal target="'#local-target'">
<p>2</p>
</Portal>
<div id="local-target"></div>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(parent.el!.innerHTML).toBe(
'<span>1</span><portal></portal><div id="local-target"><p>2</p></div>'
);
});
test("portal with target not in dom", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#does-not-exist'">
<div>2</div>
</Portal>
</div>`;
}
const parent = new Parent();
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe('Could not find any match for "#does-not-exist"');
expect(console.error).toBeCalledTimes(0);
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
console.error = consoleError;
});
test("portal with child and props", async () => {
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
mounted() {
steps.push("mounted");
expect(outside.innerHTML).toBe("<span>1</span>");
}
patched() {
steps.push("patched");
expect(outside.innerHTML).toBe("<span>2</span>");
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ val: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>1</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
parent.state.val = 2;
await nextTick();
expect(outside.innerHTML).toBe("<span>2</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
expect(steps).toEqual(["mounted", "patched"]);
});
test("portal with only text as content", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<t t-esc="'only text'"/>
</Portal>
</div>`;
}
const parent = new Parent();
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Portal must have exactly one non-text child (has 0)");
expect(console.error).toBeCalledTimes(0);
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
console.error = consoleError;
});
test("portal with no content", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<t t-if="false" t-esc="'ABC'"/>
</Portal>
</div>`;
}
const parent = new Parent();
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Portal must have exactly one non-text child (has 0)");
expect(console.error).toBeCalledTimes(0);
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
console.error = consoleError;
});
test("portal with many children", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<div>1</div>
<p>2</p>
</Portal>
</div>`;
}
const parent = new Parent();
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Portal must have exactly one non-text child (has 2)");
expect(console.error).toBeCalledTimes(0);
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
console.error = consoleError;
});
test("portal with dynamic body", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<span t-if="state.val" t-esc="state.val"/>
<div t-else=""/>
</Portal>
</div>`;
state = useState({ val: "ab" });
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe(`<span>ab</span>`);
parent.state.val = "";
await nextTick();
expect(outside.innerHTML).toBe(`<div></div>`);
});
test("portal could have dynamically no content", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<span t-if="state.val" t-esc="state.val"/>
</Portal>
</div>`;
state = { val: "ab" };
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe(`<span>ab</span>`);
let error;
try {
parent.state.val = "";
await parent.render();
} catch (e) {
error = e;
}
expect(outside.innerHTML).toBe(``);
expect(error).toBeDefined();
expect(error.message).toBe("Portal must have exactly one non-text child (has 0)");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("lifecycle hooks of portal sub component are properly called", async () => {
const steps: any[] = [];
class Child extends Component<any, any> {
static template = xml`<span t-esc="props.val"/>`;
mounted() {
steps.push("child:mounted");
}
willPatch() {
steps.push("child:willPatch");
}
patched() {
steps.push("child:patched");
}
willUnmount() {
steps.push("child:willUnmount");
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal t-if="state.hasChild" target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ hasChild: false, val: 1 });
mounted() {
steps.push("parent:mounted");
}
willPatch() {
steps.push("parent:willPatch");
}
patched() {
steps.push("parent:patched");
}
willUnmount() {
steps.push("parent:willUnmount");
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(steps).toEqual(["parent:mounted"]);
parent.state.hasChild = true;
await nextTick();
expect(steps).toEqual([
"parent:mounted",
"parent:willPatch",
"child:mounted",
"parent:patched"
]);
parent.state.val = 2;
await nextTick();
expect(steps).toEqual([
"parent:mounted",
"parent:willPatch",
"child:mounted",
"parent:patched",
"parent:willPatch",
"child:willPatch",
"child:patched",
"parent:patched"
]);
parent.state.hasChild = false;
await nextTick();
expect(steps).toEqual([
"parent:mounted",
"parent:willPatch",
"child:mounted",
"parent:patched",
"parent:willPatch",
"child:willPatch",
"child:patched",
"parent:patched",
"parent:willPatch",
"child:willUnmount",
"parent:patched"
]);
});
test("portal destroys on crash", async () => {
class Child extends Component<any, any> {
static template = xml`<span t-esc="props.error and this.will.crash" />`;
state = {};
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'" >
<Child error="state.error"/>
</Portal>
</div>`;
state = { error: false };
}
const parent = new Parent();
await parent.mount(fixture);
parent.state.error = true;
let error;
try {
await parent.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
});
test("portal manual unmount", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<span>gloria</span>
</Portal>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>gloria</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
parent.unmount();
expect(outside.innerHTML).toBe("");
expect(parent.el!.innerHTML).toBe("<portal><span>gloria</span></portal>");
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>gloria</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
});
test("portal manual unmount with subcomponent", async () => {
expect.assertions(9);
class Child extends Component<any, any> {
static template = xml`<span>gloria</span>`;
mounted() {
expect(outside.contains(this.el)).toBeTruthy();
}
willUnmount() {
expect(outside.contains(this.el)).toBeTruthy();
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'">
<Child />
</Portal>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>gloria</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
parent.unmount();
expect(outside.innerHTML).toBe("");
expect(parent.el!.innerHTML).toBe("<portal><span>gloria</span></portal>");
await parent.mount(fixture);
expect(outside.innerHTML).toBe("<span>gloria</span>");
expect(parent.el!.innerHTML).toBe("<portal></portal>");
});
});
describe("Portal: Events handling", () => {
test("events triggered on movable pure node are handled", async () => {
class Parent extends Component<any, any> {
static components = { Portal };
static template = xml`
<div>
<Portal target="'#outside'">
<span id="trigger-me" t-on-custom="_onCustom" t-esc="state.val"/>
</Portal>
</div>`;
state = useState({ val: "ab" });
_onCustom() {
this.state.val = "triggered";
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe(`<span id="trigger-me">ab</span>`);
outside.querySelector("#trigger-me")!.dispatchEvent(new Event("custom"));
await nextTick();
expect(outside.innerHTML).toBe(`<span id="trigger-me">triggered</span>`);
});
test("events triggered on movable owl components are redirected", async () => {
let childInst: Component<any, any> | null = null;
class Child extends Component<any, any> {
static template = xml`
<span t-on-custom="_onCustom" t-esc="props.val"/>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
_onCustom() {
this.trigger("custom-portal");
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div t-on-custom-portal="_onCustomPortal">
<Portal target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ val: "ab" });
_onCustomPortal() {
this.state.val = "triggered";
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(outside.innerHTML).toBe(`<span>ab</span>`);
childInst!.trigger("custom");
await nextTick();
expect(outside.innerHTML).toBe(`<span>triggered</span>`);
});
test("events triggered on contained movable owl components are redirected", async () => {
const steps: string[] = [];
let childInst: Component<any, any> | null = null;
class Child extends Component<any, any> {
static template = xml`
<span t-on-custom="_onCustom"/>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
_onCustom() {
this.trigger("custom-portal");
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div t-on-custom="_handled" t-on-custom-portal="_handled">
<Portal target="'#outside'">
<div>
<Child/>
</div>
</Portal>
</div>`;
_handled(ev) {
steps.push(ev.type);
}
}
const parent = new Parent();
await parent.mount(fixture);
childInst!.trigger("custom");
await nextTick();
// This is expected because trigger is synchronous
expect(steps).toMatchObject(["custom-portal", "custom"]);
});
test("Dom events are not mapped", async () => {
let childInst: Component<any, any> | null = null;
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`
<button>child</button>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div t-on-click="_handled">
<Portal target="'#outside'">
<Child />
</Portal>
</div>`;
_handled(ev) {
steps.push(ev.type as string);
}
}
const bodyListener = ev => {
steps.push(`body: ${ev.type}`);
};
document.body.addEventListener("click", bodyListener);
const parent = new Parent();
await parent.mount(fixture);
childInst!.el!.click();
expect(steps).toEqual(["body: click"]);
document.body.removeEventListener("click", bodyListener);
});
test("Nested portals event propagation", async () => {
const outside2 = document.createElement("div");
outside2.setAttribute("id", "outside2");
fixture.appendChild(outside2);
const steps: Array<string> = [];
let childInst: Component<any, any> | null = null;
class Child2 extends Component<any, any> {
static template = xml`<div>child2</div>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
}
class Child extends Component<any, any> {
static components = { Portal, Child2 };
static template = xml`
<Portal target="'#outside2'">
<Child2 />
</Portal>`;
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div t-on-custom='_handled'>
<Portal target="'#outside'">
<Child/>
</Portal>
</div>`;
_handled(ev) {
steps.push(`${ev.type} from ${ev.originalComponent.constructor.name}`);
}
}
const parent = new Parent();
await parent.mount(fixture);
childInst!.trigger("custom");
expect(steps).toEqual(["custom from Child2"]);
});
test("portal's parent's env is not polluted", async () => {
class Child extends Component<any, any> {
static template = xml`
<button>child</button>`;
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'">
<Child />
</Portal>
</div>`;
}
const parent = new Parent();
const parentEnv = Object.assign({}, parent.env);
await parent.mount(fixture);
expect(parentEnv).toStrictEqual(parent.env);
});
test("Portal composed with t-slot", async () => {
const steps: Array<string> = [];
let childInst: Component<any, any> | null = null;
class Child2 extends Component<any, any> {
static template = xml`<div>child2</div>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
}
class Child extends Component<any, any> {
static components = { Portal, Child2 };
static template = xml`
<Portal target="'#outside'">
<t t-slot="default"/>
</Portal>`;
}
class Parent extends Component<any, any> {
static components = { Child, Child2 };
static template = xml`
<div t-on-custom='_handled'>
<Child>
<Child2/>
</Child>
</div>`;
_handled(ev) {
steps.push(ev.type as string);
}
}
const parent = new Parent();
await parent.mount(fixture);
childInst!.trigger("custom");
expect(steps).toEqual(["custom"]);
});
});
describe("Portal: UI/UX", () => {
test("focus is kept across re-renders", async () => {
class Child extends Component<any, any> {
static template = xml`
<input id="target-me" t-att-placeholder="props.val"/>`;
}
class Parent extends Component<any, any> {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'">
<Child val="state.val"/>
</Portal>
</div>`;
state = useState({ val: "ab" });
}
const parent = new Parent();
await parent.mount(fixture);
const input = document.querySelector("#target-me");
expect(input!.nodeName).toBe("INPUT");
expect((input as HTMLInputElement).placeholder).toBe("ab");
(input as HTMLInputElement).focus();
expect(document.activeElement === input).toBeTruthy();
parent.state.val = "bc";
await nextTick();
const inputReRendered = document.querySelector("#target-me");
expect(inputReRendered!.nodeName).toBe("INPUT");
expect((inputReRendered as HTMLInputElement).placeholder).toBe("bc");
expect(document.activeElement === inputReRendered).toBeTruthy();
});
});
File diff suppressed because it is too large Load Diff
+224 -6
View File
@@ -46,6 +46,16 @@ describe("static templates", () => {
expect(renderToString(qweb, "test")).toBe("<div>word</div>");
});
test("div with a class attribute", () => {
qweb.addTemplate("test", `<div class="abc">word</div>`);
expect(renderToString(qweb, "test")).toBe(`<div class="abc">word</div>`);
});
test("div with a empty class attribute", () => {
qweb.addTemplate("test", `<div class="">word</div>`);
expect(renderToString(qweb, "test")).toBe(`<div>word</div>`);
});
test("div with a span child node", () => {
qweb.addTemplate("test", "<div><span>word</span></div>");
expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>");
@@ -129,6 +139,17 @@ describe("t-esc", () => {
qweb.addTemplate("test", `<span t-esc="var">nope</span>`);
expect(renderToString(qweb, "test")).toBe("<span>nope</span>");
});
test("t-esc is escaped", () => {
qweb.addTemplate("test", `<div><t t-set="var"><p>escaped</p></t><t t-esc="var"/></div>`);
const domRendered = renderToDOM(qweb, "test");
expect(domRendered.textContent).toBe("<p>escaped</p>");
});
test("t-esc=0 is escaped", () => {
qweb.addTemplate("test", `<span><t t-esc="0"/></span>`);
qweb.addTemplate("testCaller", `<div><t t-call="test"><p>escaped</p></t></div>`);
const domRendered = renderToDOM(qweb, "testCaller") as HTMLElement;
expect(domRendered.querySelector("span")!.textContent).toBe("<p>escaped</p>");
});
});
describe("t-raw", () => {
@@ -242,6 +263,82 @@ 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>");
});
test("t-set body is evaluated immediately", () => {
qweb.addTemplate(
"test",
`<div>
<t t-set="v1" t-value="'before'"/>
<t t-set="v2">
<span><t t-esc="v1"/></span>
</t>
<t t-set="v1" t-value="'after'"/>
<t t-raw="v2"/>
</div>`
);
expect(renderToString(qweb, "test")).toBe("<div><span>before</span></div>");
});
test("t-set with t-value (falsy) and body", () => {
qweb.addTemplate(
"test",
`<div>
<t t-set="v3" t-value="false"/>
<t t-set="v1" t-value="'before'"/>
<t t-set="v2" t-value="v3">
<span><t t-esc="v1"/></span>
</t>
<t t-set="v1" t-value="'after'"/>
<t t-set="v3" t-value="true"/>
<t t-raw="v2"/>
</div>`);
expect(renderToString(qweb, "test")).toBe("<div><span>before</span></div>");
});
test("t-set with t-value (truthy) and body", () => {
qweb.addTemplate(
"test",
`<div>
<t t-set="v3" t-value="'Truthy'"/>
<t t-set="v1" t-value="'before'"/>
<t t-set="v2" t-value="v3">
<span><t t-esc="v1"/></span>
</t>
<t t-set="v1" t-value="'after'"/>
<t t-set="v3" t-value="false"/>
<t t-raw="v2"/>
</div>`);
expect(renderToString(qweb, "test")).toBe("<div>Truthy</div>");
});
});
describe("t-if", () => {
@@ -406,6 +503,18 @@ describe("attributes", () => {
expect(result).toBe(`<div foo="bar"></div>`);
});
test("dynamic class attribute", () => {
qweb.addTemplate("test", `<div t-att-class="c"/>`);
const result = renderToString(qweb, "test", { c: "abc" });
expect(result).toBe(`<div class="abc"></div>`);
});
test("dynamic empty class attribute", () => {
qweb.addTemplate("test", `<div t-att-class="c"/>`);
const result = renderToString(qweb, "test", { c: "" });
expect(result).toBe(`<div></div>`);
});
test("dynamic attribute with a dash", () => {
qweb.addTemplate("test", `<div t-att-data-action-id="id"/>`);
const result = renderToString(qweb, "test", { id: 32 });
@@ -550,10 +659,19 @@ describe("attributes", () => {
describe("t-call (template calling", () => {
test("basic caller", () => {
qweb.addTemplate("_basic-callee", "<span>ok</span>");
qweb.addTemplate("caller", '<div><t t-call="_basic-callee"/></div>');
const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller")).toBe(expected);
expect(qweb.subTemplates["_basic-callee"].toString()).toMatchSnapshot();
});
test("basic caller, no parent node", () => {
qweb.addTemplate("_basic-callee", "<div>ok</div>");
qweb.addTemplate("caller", '<t t-call="_basic-callee"/>');
const expected = "<div>ok</div>";
expect(renderToString(qweb, "caller")).toBe(expected);
expect(qweb.subTemplates["_basic-callee"].toString()).toMatchSnapshot();
});
test("t-call with t-if", () => {
@@ -561,6 +679,7 @@ describe("t-call (template calling", () => {
qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>');
const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller", { flag: true })).toBe(expected);
expect(qweb.subTemplates["sub"].toString()).toMatchSnapshot();
});
test("t-call not allowed on a non t node", () => {
@@ -645,6 +764,41 @@ describe("t-call (template calling", () => {
`);
const expected = "<div><div><span>hey</span> <span>yay</span></div></div>";
expect(renderToString(qweb, "main")).toBe(expected);
expect(qweb.subTemplates["SubTemplate"].toString()).toMatchSnapshot();
});
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", () => {
@@ -660,7 +814,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] as any;
const recursiveFn = Object.values(qweb.subTemplates)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot();
});
@@ -688,7 +842,7 @@ describe("t-call (template calling", () => {
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
expected
);
const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
const recursiveFn = Object.values(qweb.subTemplates)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot();
});
@@ -715,7 +869,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] as any;
const recursiveFn = Object.values(qweb.subTemplates)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot();
});
@@ -725,6 +879,51 @@ describe("t-call (template calling", () => {
const expected = "<div><span>desk</span></div>";
expect(trim(renderToString(qweb, "abcd"))).toBe(expected);
});
test("t-call with t-set inside and outside", () => {
qweb.addTemplates(`
<templates>
<div t-name="main">
<t t-foreach="list" t-as="v">
<t t-set="val" t-value="v.val"/>
<t t-call="sub">
<t t-set="val3" t-value="val*3"/>
</t>
</t>
</div>
<t t-name="sub">
<span t-esc="val3"/>
</t>
</templates>
`);
const expected = "<div><span>3</span><span>6</span><span>9</span></div>";
const context = { list: [{ val: 1 }, { val: 2 }, { val: 3 }] };
expect(trim(renderToString(qweb, "main", context))).toBe(expected);
});
test("t-call with t-set inside and outside. 2", () => {
qweb.addTemplates(`
<templates>
<div t-name="main">
<t t-foreach="list" t-as="v">
<t t-set="val" t-value="v.val"/>
<t t-call="sub">
<t t-set="val3" t-value="val*3"/>
</t>
</t>
</div>
<t t-name="sub">
<span t-esc="val3"/>
<t t-esc="w"/>
</t>
<p t-name="wrapper"><t t-set="w" t-value="'fromwrapper'"/><t t-call="main"/></p>
</templates>
`);
const expected =
"<p><div><span>3</span>fromwrapper<span>6</span>fromwrapper<span>9</span>fromwrapper</div></p>";
const context = { list: [{ val: 1 }, { val: 2 }, { val: 3 }] };
expect(trim(renderToString(qweb, "wrapper", context))).toBe(expected);
});
});
describe("foreach", () => {
@@ -793,9 +992,9 @@ describe("foreach", () => {
<t t-foreach="[1]" t-as="item"><t t-esc="item"/></t>
</div>`
);
const context = {};
const context = { __owl__: {} };
renderToString(qweb, "test", context);
expect(Object.keys(context).length).toBe(0);
expect(Object.keys(context)).toEqual(["__owl__"]);
});
test("t-foreach in t-forach", () => {
@@ -1199,7 +1398,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);
@@ -1451,6 +1653,22 @@ describe("debugging", () => {
console.log = consoleLog;
});
test("t-debug on sub template", () => {
const consoleLog = console.log;
console.log = jest.fn();
qweb.addTemplates(`
<templates>
<p t-name="sub" t-debug="1">coucou</p>
<div t-name="test">
<t t-call="sub"/>
</div>
</templates>`);
qweb.render("test");
expect(console.log).toHaveBeenCalledTimes(1);
console.log = consoleLog;
});
test("t-log", () => {
const consoleLog = console.log;
console.log = jest.fn();
+44 -29
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", () => {
@@ -85,7 +92,7 @@ describe("expression evaluation", () => {
test("parenthesis", () => {
expect(compileExpr("(1)", {})).toBe("(1)");
expect(compileExpr("a*(1 +3)", {})).toBe("context['a']*(1+3)");
expect(compileExpr("a*(1 +3)", {})).toBe("scope['a']*(1+3)");
});
test("objects and sub objects", () => {
@@ -93,8 +100,8 @@ describe("expression evaluation", () => {
});
test("replacing variables", () => {
expect(compileExpr("a", {})).toBe("context['a']");
expect(compileExpr("a", { a: { id: "_3", expr: "" } })).toBe("_3");
expect(compileExpr("a", {})).toBe("scope['a']");
expect(compileExpr("a", { a: { id: "_3", expr: "scope._3" } })).toBe("scope._3");
});
test("arrays and objects", () => {
@@ -104,55 +111,63 @@ describe("expression evaluation", () => {
});
test("dot operator", () => {
expect(compileExpr("a.b", {})).toBe("context['a'].b");
expect(compileExpr("a.b.c", {})).toBe("context['a'].b.c");
expect(compileExpr("a.b", {})).toBe("scope['a'].b");
expect(compileExpr("a.b.c", {})).toBe("scope['a'].b.c");
});
test("various unary operators", () => {
expect(compileExpr("!flag", {})).toBe("!context['flag']");
expect(compileExpr("!flag", {})).toBe("!scope['flag']");
expect(compileExpr("-3", {})).toBe("-3");
expect(compileExpr("-a", {})).toBe("-context['a']");
expect(compileExpr("-a", {})).toBe("-scope['a']");
expect(compileExpr("typeof a", {})).toBe("typeof scope['a']");
});
test("various binary operators", () => {
expect(compileExpr("color == 'black'", {})).toBe("context['color']=='black'");
expect(compileExpr("a || b", {})).toBe("context['a']||context['b']");
expect(compileExpr("color === 'black'", {})).toBe("context['color']==='black'");
expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']");
expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1");
expect(compileExpr("color == 'black'", {})).toBe("scope['color']=='black'");
expect(compileExpr("a || b", {})).toBe("scope['a']||scope['b']");
expect(compileExpr("color === 'black'", {})).toBe("scope['color']==='black'");
expect(compileExpr("'li_'+item", {})).toBe("'li_'+scope['item']");
expect(compileExpr("state.val > 1", {})).toBe("scope['state'].val>1");
});
test("boolean operations", () => {
expect(compileExpr("a && b", {})).toBe("context['a']&&context['b']");
expect(compileExpr("a && b", {})).toBe("scope['a']&&scope['b']");
});
test("ternary operators", () => {
expect(compileExpr("a ? b: '2'", {})).toBe("context['a']?context['b']:'2'");
expect(compileExpr("a ? b: (c or '2') ", {})).toBe(
"context['a']?context['b']:(context['c']||'2')"
);
expect(compileExpr("a ? b: '2'", {})).toBe("scope['a']?scope['b']:'2'");
expect(compileExpr("a ? b: (c or '2') ", {})).toBe("scope['a']?scope['b']:(scope['c']||'2')");
expect(compileExpr("a ? {test:c}: [1,u]", {})).toBe(
"context['a']?{test:context['c']}:[1,context['u']]"
"scope['a']?{test:scope['c']}:[1,scope['u']]"
);
});
test("word replacement", () => {
expect(compileExpr("a or b", {})).toBe("context['a']||context['b']");
expect(compileExpr("a and b", {})).toBe("context['a']&&context['b']");
expect(compileExpr("a or b", {})).toBe("scope['a']||scope['b']");
expect(compileExpr("a and b", {})).toBe("scope['a']&&scope['b']");
});
test("function calls", () => {
expect(compileExpr("a()", {})).toBe("context['a']()");
expect(compileExpr("a(1)", {})).toBe("context['a'](1)");
expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)");
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("context['a'](1,2,{a:[context['a']]})");
expect(compileExpr("a()", {})).toBe("scope['a']()");
expect(compileExpr("a(1)", {})).toBe("scope['a'](1)");
expect(compileExpr("a(1,2)", {})).toBe("scope['a'](1,2)");
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("scope['a'](1,2,{a:[scope['a']]})");
expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()");
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe("'x'.toUpperCase({a:3})");
expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })).toBe(
"'x'.toUpperCase(_v5)"
expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "scope._v5" } })).toBe(
"'x'.toUpperCase(scope._v5)"
);
expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })).toBe(
"'x'.toUpperCase({b:_v5})"
expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "scope._v5" } })).toBe(
"'x'.toUpperCase({b:scope._v5})"
);
});
test("arrow functions", () => {
expect(compileExpr("list.map(e => e.val)", {})).toBe("scope['list'].map(e=>e.val)");
expect(compileExpr("list.map(e => a + e)", {})).toBe("scope['list'].map(e=>scope['a']+e)");
expect(compileExpr("list.map((e) => e)", {})).toBe("scope['list'].map((e)=>e)");
expect(compileExpr("list.map((elem, index) => elem + index)", {})).toBe(
"scope['list'].map((elem,index)=>elem+index)"
);
});
});
+13 -13
View File
@@ -1,22 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Link component can render simple cases 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let utils = this.constructor.utils;
let owner = context;
let sibling = null;
let scope = Object.create(context);
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 _5 = utils.toObj({'router-link-active':scope['isActive']});
var _6 = scope['href'];
let c7 = [], p7 = {key:7,attrs:{href: _6},class:_5,on:{}};
var vn7 = h('a', p7, c7);
extra.handlers['click' + 7] = extra.handlers['click' + 7] || function (e) {if (!context.__owl__.isMounted){return}const fn = context['navigate'];if (fn) { fn.call(context, e); } else { context.navigate; }};
p7.on['click'] = extra.handlers['click' + 7];
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot8) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context}));
}
return vn3;
return vn7;
}"
`;
@@ -1,45 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RouteComponent can render simple cases 1`] = `
"function anonymous(context,extra
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let sibling = null;
let scope = Object.create(context);
let result;
var h = this.h;
if (context['routeComponent']) {
const nodeKey1 = context['env'].router.currentRouteName;
if (scope['routeComponent']) {
const nodeKey5 = scope['env'].router.currentRouteName;
//COMPONENT
let templateId3 = \`__4__\` + nodeKey1;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let vn5 = {};
result = vn5;
let props3 = Object.assign({}, context['env'].router.currentParams);
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
let k7 = \`__8__\` + nodeKey5;
let w6 = k7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k7]] : false;
let vn9 = {};
result = vn9;
let props6 = Object.assign({}, scope['env'].router.currentParams);
if (w6 && w6.__owl__.currentFiber && !w6.__owl__.vnode) {
w6.destroy();
w6 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode;
utils.defineProxy(vn5, pvnode);
if (w6) {
w6.__updateProps(props6, extra.fiber, undefined);
let pvnode = w6.__owl__.pvnode;
utils.defineProxy(vn9, pvnode);
} else {
let componentKey3 = \`routeComponent\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['routeComponent'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
utils.defineProxy(vn5, pvnode);
w3.__owl__.pvnode = pvnode;
let componentKey6 = \`routeComponent\`;
let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6]|| scope['routeComponent'];
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
w6 = new W6(parent, props6);
parent.__owl__.cmap[k7] = w6.__owl__.id;
let fiber = w6.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k7, hook: {remove() {},destroy(vn) {w6.destroy();}}});
utils.defineProxy(vn9, pvnode);
w6.__owl__.pvnode = pvnode;
}
sibling = w3.__owl__.currentFiber || sibling;
w6.__owl__.parentLastFiberId = extra.fiber.id;
}
return result;
}"
+8 -2
View File
@@ -35,7 +35,10 @@ 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" });
@@ -66,7 +69,10 @@ 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" });
+191 -2
View File
@@ -48,6 +48,42 @@ 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>`;
@@ -64,6 +100,24 @@ describe("connecting a component to store", () => {
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 = {
@@ -257,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> {
@@ -324,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 }) {
@@ -428,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`
@@ -572,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[] = [];
@@ -899,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>`;
+57
View File
@@ -0,0 +1,57 @@
/**
* We can only make one test per file, since the debug tool modify in place
* the owl object in a way that is difficult to undo.
*/
import { debugOwl } from "../../tools/debug";
import * as owl from "../../src/index";
import { Component, Env } from "../../src/component/component";
import { xml } from "../../src/tags";
import { useState } from "../../src/hooks";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
let fixture: HTMLElement = makeTestFixture();
let env: Env = makeTestEnv();
Component.env = env;
debugOwl(owl, {});
test("can log full lifecycle", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
class Child extends Component<any, any> {
static template = xml`<div>child</div>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: false });
}
const parent = new Parent(null, {});
await parent.mount(fixture);
parent.state.flag = true;
await nextTick();
expect(steps).toEqual([
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
"[OWL_DEBUG] Parent<id=1> mount",
"[OWL_DEBUG] Parent<id=1> willStart",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] Parent<id=1> render",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] Child<id=2> constructor, props={}",
"[OWL_DEBUG] Child<id=2> willStart",
"[OWL_DEBUG] Child<id=2> rendering template",
"[OWL_DEBUG] Parent<id=1> willPatch",
"[OWL_DEBUG] Child<id=2> mounted",
"[OWL_DEBUG] Parent<id=1> patched"
]);
console.log = log;
});
+50
View File
@@ -0,0 +1,50 @@
/**
* We can only make one test per file, since the debug tool modify in place
* the owl object in a way that is difficult to undo.
*/
import { debugOwl } from "../../tools/debug";
import * as owl from "../../src/index";
import { Component, Env } from "../../src/component/component";
import { xml } from "../../src/tags";
import { makeTestFixture, makeTestEnv } from "../helpers";
let fixture: HTMLElement = makeTestFixture();
let env: Env = makeTestEnv();
Component.env = env;
debugOwl(owl, { logScheduler: true });
test("can log scheduler start and stop", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
class Child extends Component<any, any> {
static template = xml`<div>child</div>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Child /></div>`;
static components = { Child };
}
const parent = new Parent(null, {});
await parent.mount(fixture);
expect(steps).toEqual([
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
"[OWL_DEBUG] Parent<id=1> mount",
"[OWL_DEBUG] Parent<id=1> willStart",
"[OWL_DEBUG] scheduler: start running tasks queue",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] Child<id=2> constructor, props={}",
"[OWL_DEBUG] Child<id=2> willStart",
"[OWL_DEBUG] Child<id=2> rendering template",
"[OWL_DEBUG] Child<id=2> mounted",
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue"
]);
console.log = log;
});
+46
View File
@@ -0,0 +1,46 @@
/**
* We can only make one test per file, since the debug tool modify in place
* the owl object in a way that is difficult to undo.
*/
import { debugOwl } from "../../tools/debug";
import * as owl from "../../src/index";
import { Component, Env } from "../../src/component/component";
import { xml } from "../../src/tags";
import { makeTestFixture, makeTestEnv } from "../helpers";
let fixture: HTMLElement = makeTestFixture();
let env: Env = makeTestEnv();
Component.env = env;
debugOwl(owl, { logScheduler: true });
test("log a specific message for render method calls if component is not mounted", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
class Parent extends Component<any, any> {
static template = xml`<div><t t-esc="state.value"/></div>`;
state = owl.hooks.useState({ value: 1 });
}
const parent = new Parent(null, {});
await parent.mount(fixture);
parent.unmount();
parent.state.value = 2;
expect(steps).toEqual([
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
"[OWL_DEBUG] Parent<id=1> mount",
"[OWL_DEBUG] Parent<id=1> willStart",
"[OWL_DEBUG] scheduler: start running tasks queue",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Parent<id=1> render (warning: component is not mounted, this render has no effect)"
]);
console.log = log;
});
@@ -85,7 +85,7 @@ function getFiles(path: string[] = []): FileData[] {
return Array.prototype.concat(...files);
}
const LOCAL_FILES = ["LICENSE"];
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
-173
View File
@@ -1,173 +0,0 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
template = "Counter";
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
template = "Message";
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
template = "App";
mounted() {
this.log(
`Benchmarking Owl v${owl._version} (build date: ${
owl._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 < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(data) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === data.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.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.10.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<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-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="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">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-170
View File
@@ -1,170 +0,0 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
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 < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(data) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === data.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.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<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-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="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">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.14.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<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-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="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">
<t t-widget="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>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-161
View File
@@ -1,161 +0,0 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
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 {
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
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 < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(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.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.16.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>OWL v0.24.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='../../owl.js'></script>
<script src='owl.js'></script>
</head>
<body>
<script src='app.js' type="module"></script>
File diff suppressed because it is too large Load Diff
-173
View File
@@ -1,173 +0,0 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
template = "Counter";
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
template = "Message";
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
template = "App";
mounted() {
this.log(
`Benchmarking Owl v${owl._version} (build date: ${
owl._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 < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(data) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === data.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.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.8.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl-0.8.0.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<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-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="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">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
@@ -1,10 +1,11 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
state = useState({ counter: 0 });
increment() {
this.state.counter++;
@@ -15,8 +16,6 @@ class Counter extends owl.Component {
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
@@ -26,13 +25,14 @@ class Message extends owl.Component {
});
}
}
Message.components = { Counter };
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
@@ -108,10 +108,10 @@ class App extends owl.Component {
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
for (let i = 0; i < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
messages[i] = msg;
}
});
}
@@ -129,32 +129,25 @@ class App extends owl.Component {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
this.logRef.el.appendChild(div);
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
this.logRef.el.innerHTML = "";
}
}
App.components = { Message };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
const templates = await owl.utils.loadFile("templates.xml");
App.env = {
qweb: new owl.QWeb({ templates })
};
const app = new App(env);
const app = new App();
app.mount(document.body);
}
@@ -2,12 +2,11 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.12.0 Benchmark</title>
<title>OWL v1.0.0-beta1 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -12,11 +12,11 @@
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<input type="checkbox" id="multipleflag" t-model="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<input type="checkbox" id="clearFlag" t-model="state.clearAfterFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
+149
View File
@@ -0,0 +1,149 @@
/**
* 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.
*/
function debugOwl(owl, options) {
let prefix = "[OWL_DEBUG]";
let current;
Object.defineProperty(owl.Component, "current", {
get() {
return current;
},
set(comp) {
current = comp;
const name = comp.constructor.name;
if (options.componentBlackList && options.componentBlackList.test(name)) {
return;
}
if (options.componentWhiteList && !options.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 log = str => console.log(`${prefix} ${fullName} ${str}`);
let shouldDebug = method => {
if (options.methodBlackList && options.methodBlackList.includes(method)) {
return false;
}
if (options.methodWhiteList && !options.methodWhiteList.includes(method)) {
return false;
}
return true;
};
if (shouldDebug("constructor")) {
log(`constructor, props=${toStr(component.props)}`);
}
if (shouldDebug("willStart")) {
owl.hooks.onWillStart(() => {
log(`willStart`);
});
}
if (shouldDebug("mounted")) {
owl.hooks.onMounted(() => {
log(`mounted`);
});
}
if (shouldDebug("willUpdateProps")) {
owl.hooks.onWillUpdateProps(nextProps => {
log(`willUpdateProps, nextprops=${toStr(nextProps)}`);
});
}
if (shouldDebug("willPatch")) {
owl.hooks.onWillPatch(() => {
log(`willPatch`);
});
}
if (shouldDebug("patched")) {
owl.hooks.onPatched(() => {
log(`patched`);
});
}
if (shouldDebug("willUnmount")) {
owl.hooks.onWillUnmount(() => {
log(`willUnmount`);
});
}
const __render = component.__render.bind(component);
component.__render = function(...args) {
log(`rendering template`);
__render(...args);
};
const render = component.render.bind(component);
component.render = function(...args) {
const __owl__ = component.__owl__;
let msg = `render`;
if (!__owl__.isMounted && !__owl__.currentFiber) {
msg += ` (warning: component is not mounted, this render has no effect)`;
}
log(msg);
return render(...args);
};
const mount = component.mount.bind(component);
component.mount = function(...args) {
log(`mount`);
return mount(...args);
};
}
if (options.logScheduler) {
let start = owl.Component.scheduler.start;
let stop = owl.Component.scheduler.stop;
owl.Component.scheduler.start = function () {
if (!this.isRunning) {
console.log(`${prefix} scheduler: start running tasks queue`);
}
start.call(this);
};
owl.Component.scheduler.stop = function () {
if (this.isRunning) {
console.log(`${prefix} scheduler: stop running tasks queue`);
}
stop.call(this);
};
}
if (options.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);
};
}
}
// This debug function can then be used like this:
//
// debugOwl(owl, {
// 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
// });
module.exports.debugOwl = debugOwl;
+1 -5
View File
@@ -28,19 +28,15 @@
<div class="benchmarks">
<ul>
<li><a href="benchmarks/owl-0.7.0">OWL 0.7.0</a></li>
<li><a href="benchmarks/owl-0.8.0">OWL 0.8.0</a></li>
<li><a href="benchmarks/owl-0.9.0">OWL 0.9.0</a></li>
<li><a href="benchmarks/owl-0.10.0">OWL 0.10.0</a></li>
<li><a href="benchmarks/owl-0.11.0">OWL 0.11.0</a></li>
<li><a href="benchmarks/owl-0.12.0">OWL 0.12.0</a></li>
<li><a href="benchmarks/owl-0.13.0">OWL 0.13.0</a></li>
<li><a href="benchmarks/owl-0.14.0">OWL 0.14.0</a></li>
<li><a href="benchmarks/owl-0.15.0">OWL 0.15.0</a></li>
<li><a href="benchmarks/owl-0.16.0">OWL 0.16.0</a></li>
<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-1.0.0-beta1">OWL 1.0.0-beta1</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul>
<ul>
+25 -11
View File
@@ -120,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);
+200 -39
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,7 +14,10 @@ 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 };
@@ -47,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++;
@@ -55,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];
@@ -213,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++;
@@ -282,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++;
@@ -318,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;
@@ -451,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) {
@@ -483,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) {
@@ -1008,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 {}
@@ -1150,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;
@@ -1158,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++;
@@ -1167,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;
@@ -1277,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++;
@@ -1350,13 +1388,16 @@ 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
@@ -1403,6 +1444,111 @@ const FORM_XML = `<templates>
</templates>
`;
const PORTAL_COMPONENTS = `
// This shows the expected use case of Portal
// which is to implement something similar
// to bootstrap modal
const { Component, useState } = owl;
const { Portal } = owl.misc;
class Modal extends Component {}
Modal.components = { Portal };
class Dialog extends Component {}
Dialog.components = { Modal };
class Interstellar extends Component {}
// Main root component
class App extends Component {
state = useState({
name: 'Portal used for Dialog (Modal)',
dialog: false,
text: 'Hello !',
});
}
App.components = { Dialog , Interstellar };
// Application setup
const app = new App();
app.mount(document.body);
`;
const PORTAL_XML = `
<templates>
<t t-name="Modal">
<Portal target="'body'">
<div class="owl-modal-supercontainer">
<div class="owl-modal-backdrop"></div>
<div class="owl-modal-container">
<t t-slot="default" />
</div>
</div>
</Portal>
</t>
<t t-name="Dialog">
<Modal>
<div class="owl-dialog-body">
<t t-slot="default" />
</div>
</Modal>
</t>
<div t-name="Interstellar" class="owl-interstellar">
<h4>This is a subComponent</h4>
<p>The events it triggers will go through the Portal and be teleported
on the other side of the wormhole it has created</p>
<button t-on-click="trigger('collapse-all')">Close the wormhole</button>
</div>
<div t-name="App" t-on-collapse-all="state.dialog=false">
<div t-esc="state.name"/>
<button t-on-click="state.dialog = true">Open Dialog</button>
<Dialog t-if="state.dialog">
<div t-esc="state.text"/>
<Interstellar />
</Dialog>
</div>
</templates>
`;
const PORTAL_CSS = `
.owl-modal-supercontainer {
position: static;
}
.owl-modal-backdrop {
position: fixed;
top: 0;
left:0;
background-color: #000000;
opacity: 0.5;
width: 100vw;
height: 100vh;
z-index: 1000;
}
.owl-modal-container {
opacity:1;
z-index: 1050;
position: fixed;
top: 0;
left:0;
width: 100%;
height: 100%;
}
.owl-dialog-body {
max-width: 500px;
margin: 0 auto;
position: relative;
text-align: center;
padding: 2rem;
background-color: #FFFFFF;
max-height: 100%;
}
.owl-interstellar {
border: groove;
}`
const WMS = `// This example is slightly more complex than usual. We demonstrate
// here a way to manage sub windows in Owl, declaratively. This is still just a
// demonstration. Managing windows can be as complex as we want. For example,
@@ -1419,7 +1565,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++;
@@ -1470,11 +1619,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);
@@ -1518,7 +1670,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);
@@ -1552,7 +1707,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>
@@ -1720,5 +1875,11 @@ export const SAMPLES = [
code: ASYNC_COMPONENTS,
xml: ASYNC_COMPONENTS_XML,
css: ASYNC_COMPONENTS_CSS
}
},
{
description: "Portal (Dialog)",
code: PORTAL_COMPONENTS,
xml: PORTAL_XML,
css: PORTAL_CSS,
},
];