Compare commits

..

38 Commits

Author SHA1 Message Date
Géry Debongnie ef6fd457f8 [FIX] qweb: add support for spread operator
closes #575
2019-12-13 22:35:26 +01:00
Sébastien Theys 455e45c148 [FIX] store: make Array.map work correctly on the result of selector
closes #577
2019-12-13 20:32:02 +01:00
Lucas Perais (lpe) 5e7f1d5e6d [FIX] qweb: t-call must pass extra parent
Before this commit, when template was t-call'ed and defined
t-component directive within it, there was a crash because extra.parent was never defined

After this commit, this use case works
2019-12-13 20:28:49 +01:00
Géry Debongnie 0b05ad619f [FIX] qweb: t-set with a body should not be joined with comma
closes #574
2019-12-13 20:15:37 +01:00
Géry Debongnie 29a80d0b28 [FIX] qweb: properly display falsy values
While it is not tested, nor documented, the reference QWeb
implementation display the `false` value as "false". So, we have to
adapt the Owl implementation to match that behaviour.

At the same time, this commit uses 'let' instead of 'var' in various places,
to make the compiled code more consistant.
2019-12-13 14:48:19 +01:00
Géry Debongnie 0931a4dc5b [FIX] qweb: bind handlers to component
Before this commit, the handlers were bound to the current context,
which is often the component but not necessarily.  In some cases, a sub
scope can be created (with t-foreach, or slots, or t-call), and the
context is actually an object with the actual component instance it its
prototype chain, but not the component.
2019-12-12 14:32:43 +01:00
Géry Debongnie 4e22dbcad6 [FIX] qweb: accept assignations in qweb expressions
closes #560
closes #569
2019-12-12 14:32:43 +01:00
Géry Debongnie b283e65ad4 [REF] qweb: improve t-on generated code
and deduplicate logic between directive t-on applied on a node and on a
component
2019-12-12 14:32:43 +01:00
Géry Debongnie f517d44e32 [IMP] qweb: slightly simplify generated templates
- In some cases, we can simply inline a template key instead of assigning
  it to a variable and then using that variable
- Add component name in generated code for components
2019-12-12 09:39:26 +01:00
Lucas Perais (lpe) 08022b49df [FIX] qweb: conditional t-call with body
When a t-call has a condition and a body
the t-if's and its flavors should be ignored when compiling the body of a t-call
2019-12-11 14:09:45 +01:00
Géry Debongnie b96ea79f2b [FIX] qweb: renderToString now properly escape
closes #565
2019-12-11 12:25:05 +01:00
Géry Debongnie 97d8b3ed8c [REF] tests: remove Widget common class
Bad name, and bad practice to use it. It makes tests less standalone.
2019-12-11 12:25:05 +01:00
Géry Debongnie 9921ae07b1 [REF] tests: move slots tests out of main component file 2019-12-11 12:25:05 +01:00
Géry Debongnie 41cb5f1abd [FIX] *: remove useless values to t-else and t-debug
The actual value does not matter, only the fact that it is present.

closes #564
2019-12-11 12:25:05 +01:00
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
71 changed files with 11210 additions and 18876 deletions
+2 -2
View File
@@ -104,8 +104,8 @@ Submit a PR!
If you want to use a simple `<script>` tag, the last release can be downloaded here: If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.0.0-alpha5.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha5/owl.js) - [owl-1.0.0-beta2.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta2/owl.js)
- [owl-1.0.0-alpha5.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha5/owl.min.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: Some npm scripts are available:
+1 -80
View File
@@ -1,10 +1,9 @@
# 🦉 Testing and Debugging Owl components 🦉 # 🦉 Testing Owl components 🦉
## Content ## Content
- [Overview](#overview) - [Overview](#overview)
- [Unit Tests](#unit-tests) - [Unit Tests](#unit-tests)
- [Debugging](#debugging)
## Overview ## Overview
@@ -131,81 +130,3 @@ function afterUpdates() {
}); });
} }
``` ```
## 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,
the following code can simply be copy/pasted in an application. Once it is
executed, it will log a lot of information on each component main hooks.
```js
let current;
Object.defineProperty(owl.Component, "current", {
get() {
return current;
},
set(comp) {
current = comp;
const name = comp.constructor.name;
let __owl__;
Object.defineProperty(current, "__owl__", {
get() {
return __owl__;
},
set(val) {
__owl__ = val;
debugComponent(comp, name, __owl__.id);
}
});
}
});
function toStr(props) {
let str = JSON.stringify(props || {});
if (str.length > 200) {
str = str.slice(0, 200) + "...";
}
return str;
}
function debugComponent(component, name, id) {
console.log(`[DEBUG] constructor ${name}<id=${id}>, props=${toStr(component.props)}`);
owl.hooks.onWillStart(() => {
console.log(`[DEBUG] willStart: '${name}<id=${id}>'`);
});
owl.hooks.onMounted(() => {
console.log(`[DEBUG] mounted: '${name}<id=${id}>'`);
});
owl.hooks.onWillUpdateProps(nextProps => {
console.log(`[DEBUG] willUpdateProps: '${name}<id=${id}> nextprops=${toStr(nextProps)}`);
});
owl.hooks.onWillPatch(() => {
console.log(`[DEBUG] willPatch: '${name}<id=${id}>'`);
});
owl.hooks.onPatched(() => {
console.log(`[DEBUG] patched: '${name}<id=${id}>'`);
});
owl.hooks.onWillUnmount(() => {
console.log(`[DEBUG] willUnmount: '${name}<id=${id}>'`);
});
const __render = component.__render.bind(component);
component.__render = function(...args) {
console.log(`[DEBUG] rendering template: '${name}<id=${id}>'`);
__render(...args);
};
const render = component.render.bind(component);
component.render = function(...args) {
console.log(`[DEBUG] render: '${name}<id=${id}>'`);
return render(...args);
};
const mount = component.mount.bind(component);
component.mount = function(...args) {
console.log(`[DEBUG] mount: '${name}<id=${id}>'`);
return mount(...args);
};
}
```
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.
+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 - 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 purpose. It says that each `Task` should be given exactly one prop, named
`task`. If this is not the case, Owl will throw an `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 useful when refactoring components
- finally, to activate the props validation, we need to set Owl's - 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` [mode](../reference/config.md#mode) to `dev`. This is done in the `setup`
+17 -15
View File
@@ -4,6 +4,7 @@
- [Animations](reference/animations.md) - [Animations](reference/animations.md)
- [Component](reference/component.md) - [Component](reference/component.md)
- [Concurrency Model](reference/concurrency_model.md)
- [Configuration](reference/config.md) - [Configuration](reference/config.md)
- [Context](reference/context.md) - [Context](reference/context.md)
- [Environment](reference/environment.md) - [Environment](reference/environment.md)
@@ -12,6 +13,7 @@
- [Miscellaneous Components](reference/misc.md) - [Miscellaneous Components](reference/misc.md)
- [Observer](reference/observer.md) - [Observer](reference/observer.md)
- [Props](reference/props.md) - [Props](reference/props.md)
- [Props Validation](reference/props_validation.md)
- [QWeb Templating Language](reference/qweb_templating_language.md) - [QWeb Templating Language](reference/qweb_templating_language.md)
- [QWeb Engine](reference/qweb_engine.md) - [QWeb Engine](reference/qweb_engine.md)
- [Router](reference/router.md) - [Router](reference/router.md)
@@ -23,7 +25,7 @@
- [Quick Start: create an (almost) empty Owl application](learning/quick_start.md) - [Quick Start: create an (almost) empty Owl application](learning/quick_start.md)
- [Tutorial: create a TodoList application](learning/tutorial_todoapp.md) - [Tutorial: create a TodoList application](learning/tutorial_todoapp.md)
- [Testing and Debugging Owl components](learning/testing_components.md) - [Testing Owl components](learning/testing_components.md)
## Miscellaneous ## Miscellaneous
@@ -48,20 +50,20 @@ and `EventBus` is exported as `owl.core.EventBus`):
``` ```
Component misc Component misc
Context AsyncRoot Context AsyncRoot
QWeb router QWeb Portal
Store Link Store router
useState RouteComponent useState Link
config Router config RouteComponent
mode tags mode Router
core xml core tags
EventBus utils EventBus xml
Observer debounce Observer utils
hooks escape hooks debounce
onWillStart loadJS onWillStart escape
onMounted loadFile onMounted loadJS
onWillUpdateProps shallowEqual onWillUpdateProps loadFile
onWillPatch whenReady onWillPatch shallowEqual
onPatched onPatched whenReady
onWillUnmount onWillUnmount
useContext useContext
useState useState
+42 -244
View File
@@ -14,12 +14,9 @@
- [Composition](#composition) - [Composition](#composition)
- [Event Handling](#event-handling) - [Event Handling](#event-handling)
- [Form Input Bindings](#form-input-bindings) - [Form Input Bindings](#form-input-bindings)
- [Semantics](#semantics)
- [Props Validation](#props-validation)
- [References](#references) - [References](#references)
- [Slots](#slots) - [Slots](#slots)
- [Dynamic sub components](#dynamic-sub-components) - [Dynamic sub components](#dynamic-sub-components)
- [Asynchronous Rendering](#asynchronous-rendering)
- [Error Handling](#error-handling) - [Error Handling](#error-handling)
- [Functional Components](#functional-components) - [Functional Components](#functional-components)
- [SVG components](#svg-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 * **`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 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 `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 ```js
class Counter extends owl.Component { class Counter extends owl.Component {
@@ -237,12 +234,23 @@ component.
We explain here all the public methods of the `Component` class. 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 component is added to the DOM: the root component is mounted to a target
HTMLElement (or document fragment). 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 created as well. Most applications will need to call `mount` exactly once, on
the root component. 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 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 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. in the environment, or in the store, or ...) will be taken into account.
@@ -261,25 +269,25 @@ We explain here all the public methods of the `Component` class.
// app is now visible // app is now visible
``` ```
- **`unmount()`**: in case a component needs to be detached/removed from the DOM, this * **`unmount()`**: in case a component needs to be detached/removed from the DOM, this
method can be used. Most applications should not call `unmount`, this is more method can be used. Most applications should not call `unmount`, this is more
useful to the underlying component system. useful to the underlying component system.
- **`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 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. 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 Note that the render method is asynchronous, so one cannot observe the updated
DOM in the same stack frame. 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 are updated. It returns a boolean, which indicates if the component should
ignore a props update. If it returns false, then `willUpdateProps` will not ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to 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 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. 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, and perform all necessary cleanup, such as unmounting the component, its children,
removing the parent/children relationship. This method should almost never be 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 called directly (except maybe on the root component), but should be done by the
@@ -620,10 +628,20 @@ as it will be translated to:
```js ```js
button.addEventListener("click", () => { button.addEventListener("click", () => {
component.state.counter++; context.state.counter++;
}); });
``` ```
Warning: inline expressions are evaluated in the context of the template. This
means that they can access the component methods and properties. But if they set
a key, the inline statement will actually not modify the component, but a key in
a sub scope.
```xml
<button t-on-click="value = 1">Set value to 1 (does not work!!!)</button>
<button t-on-click="state.value = 1">Set state.value to 1 (work as expected)</button>
```
In order to remove the DOM event details from the event handlers (like calls to In order to remove the DOM event details from the event handlers (like calls to
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be `event.preventDefault`) and let them focus on data logic, _modifiers_ can be
specified as additional suffixes of the `t-on` directive. specified as additional suffixes of the `t-on` directive.
@@ -745,196 +763,6 @@ update a number whenever the change is done.
Note: the online playground has an example to show how it works. 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. 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`
### 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)
},
};
```
### References ### References
The `useRef` hook is useful when we need a way to interact with some inside part The `useRef` hook is useful when we need a way to interact with some inside part
@@ -1047,6 +875,19 @@ be considered the `default` slot. For example:
</div> </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 ### Dynamic sub components
It is not common, but sometimes we need a dynamic component name. In this case, It is not common, but sometimes we need a dynamic component name. In this case,
@@ -1092,49 +933,6 @@ component class.
Note that the `t-component` directive can only be used on `<t>` nodes. Note that the `t-component` directive can only be used on `<t>` nodes.
### 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>
```
### Error Handling ### Error Handling
By default, whenever an error occurs in the rendering of an Owl application, we By default, whenever an error occurs in the rendering of an Owl application, we
@@ -1158,7 +956,7 @@ For example, here is how we could implement an `ErrorBoundary` component:
<t t-if="state.error"> <t t-if="state.error">
Error handled Error handled
</t> </t>
<t t-else="1"> <t t-else="">
<t t-slot="default" /> <t t-slot="default" />
</t> </t>
</div> </div>
+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>
```
+1 -1
View File
@@ -57,7 +57,7 @@ class SomeComponent extends Component {
<t t-if=device.isMobile> <t t-if=device.isMobile>
some simplified user interface some simplified user interface
</t> </t>
<t t-else="1"> <t t-else="">
a more advanced user interface a more advanced user interface
</t> </t>
</div>`; </div>`;
+111
View File
@@ -1,5 +1,116 @@
# 🦉 Miscellaneous 🦉 # 🦉 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` ## `AsyncRoot`
When this component is used, a new rendering sub tree is created, such that the 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)
},
};
```
+11 -2
View File
@@ -28,7 +28,7 @@ generate a virtual dom representation of the HTML.
```xml ```xml
<div> <div>
<span t-if="somecondition">Some string</span> <span t-if="somecondition">Some string</span>
<ul t-else="1"> <ul t-else="">
<li t-foreach="messages" t-as="message"> <li t-foreach="messages" t-as="message">
<t t-esc="message"/> <t t-esc="message"/>
</li> </li>
@@ -101,7 +101,7 @@ precisely, the result of a template rendering should have a single root node:
<!–– ok: result has one single root node ––> <!–– ok: result has one single root node ––>
<t> <t>
<div t-if="someCondition">foo</div> <div t-if="someCondition">foo</div>
<span t-else="1">bar</span> <span t-else="">bar</span>
</t> </t>
``` ```
@@ -500,6 +500,15 @@ will result in :
</div> </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 ### Translations
By default, QWeb specify that templates should be translated. If this behaviour By default, QWeb specify that templates should be translated. If this behaviour
+2 -2
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 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 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 `actions`. Owl components can then connect to the store to read their relevant
necessary. state, and they will be rerendered if the state is updated.
Note: Owl store is inspired by React Redux and VueX. Note: Owl store is inspired by React Redux and VueX.
+26
View File
@@ -6,6 +6,7 @@
- [Playground](#playground) - [Playground](#playground)
- [Benchmarks](#benchmarks) - [Benchmarks](#benchmarks)
- [Single File Component](#single-file-component) - [Single File Component](#single-file-component)
- [Debugging Script](#debugging-script)
## Overview ## 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 This is useful for some editor plugins, such as the VS Code addon
`Comment tagged template`, which, if installed, add syntax highlighting to the `Comment tagged template`, which, if installed, add syntax highlighting to the
content of the template string. 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.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "owl-framework", "name": "owl-framework",
"version": "1.0.0-alpha5", "version": "1.0.0-beta2",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "src/index.ts", "main": "src/index.ts",
"engines": { "engines": {
+5 -12
View File
@@ -1,25 +1,17 @@
# 🦉 OWL Roadmap 🦉 # 🦉 OWL Roadmap 🦉
- Current version: 1.0.0-alpha5 - Current version: 1.0.0-beta2
- Status: mostly stable - Status: mostly stable
This roadmap is only an attempt at predicting Owl's future. Everything may This roadmap is only an attempt at predicting Owl's future. Everything may
change! 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 ### December 2019
If all goes well, Owl will be upgraded to beta status. From then, no API change, If all goes well, Owl is upgraded to beta status. From now on, no API change,
even small, is expected. even small, is expected (but it still could happen).
### End of 2019 ### End of 2019
@@ -41,7 +33,8 @@ Release v1.0
Maybe: 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 - refactor `QWeb` to use an intermediate representation (some kind of AST) to
allow additional optimisations. allow additional optimisations.
+60 -25
View File
@@ -36,6 +36,10 @@ export interface Env {
[key: string]: any; [key: string]: any;
} }
interface MountOptions {
position?: "first-child" | "last-child" | "self";
}
/** /**
* This is mostly an internal detail of implementation. The Meta interface is * 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 * useful to typecheck and describe the internal keys used by Owl to manage the
@@ -66,10 +70,9 @@ interface Internal<T extends Env, Props> {
parentLastFiberId: number; parentLastFiberId: number;
// when a rendering is initiated by a parent, it may set variables in 'scope' // when a rendering is initiated by a parent, it may set variables in 'scope'
// and 'vars' (typically when the component is rendered in a slot). We need to // (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. // store that information in case the component would be re-rendered later on.
scope: any; scope: any;
vars: any;
boundHandlers: { [key: number]: any }; boundHandlers: { [key: number]: any };
observer: Observer | null; observer: Observer | null;
@@ -84,6 +87,8 @@ interface Internal<T extends Env, Props> {
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null; refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
} }
export const portalSymbol = Symbol("portal"); // FIXME
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Component // Component
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -100,6 +105,7 @@ export class Component<T extends Env, Props extends {}> {
static env: any = {}; static env: any = {};
// expose scheduler s.t. it can be mocked for testing purposes // expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler; static scheduler: Scheduler = scheduler;
__target: HTMLElement | undefined;
/** /**
* The `el` is the root element of the component. Note that it could be null: * The `el` is the root element of the component. Note that it could be null:
@@ -191,8 +197,7 @@ export class Component<T extends Env, Props extends {}> {
renderFn: qweb.render.bind(qweb, template), renderFn: qweb.render.bind(qweb, template),
classObj: null, classObj: null,
refs: null, refs: null,
scope: null, scope: null
vars: null
}; };
} }
@@ -289,7 +294,8 @@ export class Component<T extends Env, Props extends {}> {
* *
* Note that a component can be mounted an unmounted several times * Note that a component can be mounted an unmounted several times
*/ */
async mount(target: HTMLElement | DocumentFragment): Promise<void> { async mount(target: HTMLElement | DocumentFragment, options: MountOptions = {}): Promise<void> {
const position = options.position || "last-child";
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (__owl__.isMounted) { if (__owl__.isMounted) {
return Promise.resolve(); return Promise.resolve();
@@ -299,10 +305,19 @@ export class Component<T extends Env, Props extends {}> {
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`; message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message); throw new Error(message);
} }
const fiber = new Fiber(null, this, false, target); 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; fiber.shouldPatch = false;
if (!__owl__.vnode) { if (!__owl__.vnode) {
this.__prepareAndRender(fiber); this.__prepareAndRender(fiber, () => {});
} else { } else {
this.__render(fiber); this.__render(fiber);
} }
@@ -397,14 +412,7 @@ export class Component<T extends Env, Props extends {}> {
* willUnmount(). * willUnmount().
*/ */
trigger(eventType: string, payload?: any) { trigger(eventType: string, payload?: any) {
if (this.el) { this.__trigger(this, eventType, payload);
const ev = new OwlEvent(this, eventType, {
bubbles: true,
cancelable: true,
detail: payload
});
this.el.dispatchEvent(ev);
}
} }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -478,14 +486,30 @@ 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 * The __updateProps method is called by the t-component directive whenever
* it updates a component (so, when the parent template is rerendered). * it updates a component (so, when the parent template is rerendered).
*/ */
async __updateProps(nextProps: Props, parentFiber: Fiber, scope: any, vars: any): Promise<void> { async __updateProps(nextProps: Props, parentFiber: Fiber, scope: any): Promise<void> {
this.__owl__.scope = scope; this.__owl__.scope = scope;
this.__owl__.vars = vars;
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps); const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) { if (shouldUpdate) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
@@ -523,18 +547,27 @@ export class Component<T extends Env, Props extends {}> {
*/ */
__patch(vnode: VNode) { __patch(vnode: VNode) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
const target = __owl__.vnode || document.createElement(vnode.sel!); if (this.__target) {
__owl__.vnode = patch(target, vnode); 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 * 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. * parent template.
*/ */
__prepare(parentFiber: Fiber, scope: any, vars: any) { __prepare(parentFiber: Fiber, scope: any, cb: CallableFunction): Fiber {
this.__owl__.scope = scope; this.__owl__.scope = scope;
this.__owl__.vars = vars;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null); const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
fiber.shouldPatch = false; fiber.shouldPatch = false;
if (!parentFiber.child) { if (!parentFiber.child) {
@@ -543,7 +576,8 @@ export class Component<T extends Env, Props extends {}> {
parentFiber.lastChild!.sibling = fiber; parentFiber.lastChild!.sibling = fiber;
} }
parentFiber.lastChild = fiber; parentFiber.lastChild = fiber;
return this.__prepareAndRender(fiber); this.__prepareAndRender(fiber, cb);
return fiber;
} }
__getTemplate(qweb: QWeb): string { __getTemplate(qweb: QWeb): string {
@@ -565,7 +599,7 @@ export class Component<T extends Env, Props extends {}> {
} }
return p._template; return p._template;
} }
async __prepareAndRender(fiber: Fiber) { async __prepareAndRender(fiber: Fiber, cb: CallableFunction) {
try { try {
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]); await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
} catch (e) { } catch (e) {
@@ -577,6 +611,7 @@ export class Component<T extends Env, Props extends {}> {
} }
if (!fiber.isCompleted) { if (!fiber.isCompleted) {
this.__render(fiber); this.__render(fiber);
cb();
} }
} }
+19 -62
View File
@@ -1,6 +1,6 @@
import { QWeb } from "../qweb/index"; import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/compilation_context"; import { INTERP_REGEXP } from "../qweb/compilation_context";
import { MODS_CODE } from "../qweb/extensions"; import { makeHandlerCode, MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-component // t-component
@@ -189,15 +189,15 @@ QWeb.addDirective({
extraNames: ["props"], extraNames: ["props"],
priority: 100, priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean { atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//COMPONENT"); ctx.addLine(`// Component '${value}'`);
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true; ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true; ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
ctx.rootContext.shouldDefineScope = true;
let hasDynamicProps = node.getAttribute("t-props") ? true : false; let hasDynamicProps = node.getAttribute("t-props") ? true : false;
// t-on- events and t-transition // t-on- events and t-transition
const events: [string, string[], string, string][] = []; const events: [string, string][] = [];
let transition: string = ""; let transition: string = "";
const attributes = (<Element>node).attributes; const attributes = (<Element>node).attributes;
const props: { [key: string]: string } = {}; const props: { [key: string]: string } = {};
@@ -205,13 +205,7 @@ QWeb.addDirective({
const name = attributes[i].name; const name = attributes[i].name;
const value = attributes[i].textContent!; const value = attributes[i].textContent!;
if (name.startsWith("t-on-")) { if (name.startsWith("t-on-")) {
const [eventName, ...mods] = name.slice(5).split("."); events.push([name, value]);
let extraArgs;
let handlerValue = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
events.push([eventName, mods, handlerValue, extraArgs]);
} else if (name === "t-transition") { } else if (name === "t-transition") {
transition = value; transition = value;
} else if (!name.startsWith("t-")) { } else if (!name.startsWith("t-")) {
@@ -226,7 +220,6 @@ QWeb.addDirective({
let propStr = Object.keys(props) let propStr = Object.keys(props)
.map(k => k + ":" + props[k]) .map(k => k + ":" + props[k])
.join(","); .join(",");
let defID = ctx.generateID();
let componentID = ctx.generateID(); let componentID = ctx.generateID();
const templateKey = ctx.generateTemplateKey(); const templateKey = ctx.generateTemplateKey();
@@ -285,32 +278,15 @@ QWeb.addDirective({
} }
} }
let eventsCode = events let eventsCode = events
.map(function([eventName, mods, handlerValue, extraArgs]) { .map(function([name, value]) {
let params = "owner"; const { event, handler } = makeHandlerCode(
if (extraArgs) { ctx,
if (ctx.loopNumber) { name,
let argId = ctx.generateID(); value,
// we need to evaluate the arguments now, because the handler will false,
// be set asynchronously later when the widget is ready, and the T_COMPONENT_MODS_CODE
// context might be different. );
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`); return `vn.elm.addEventListener('${event}', ${handler});`;
params = `owner, arg${argId}`;
} else {
params = `owner, ${ctx.formatExpression(extraArgs)}`;
}
}
let handler = `function (e) {`;
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 += `}`;
return `vn.elm.addEventListener('${eventName}', ${handler});`;
}) })
.join(""); .join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false); const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
@@ -349,25 +325,9 @@ QWeb.addDirective({
} }
// SLOTS // SLOTS
const varDefs: string[] = [];
const hasSlots = node.childNodes.length; 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; let scope = hasSlots ? `Object.assign(Object.create(context), scope)` : "undefined";
if (hasSlots) {
let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
scopeVars = `${scope}, ${vars}`;
} else {
scopeVars = "undefined, undefined";
}
ctx.addIf(`w${componentID}`); ctx.addIf(`w${componentID}`);
@@ -377,8 +337,7 @@ QWeb.addDirective({
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`; styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
} }
ctx.addLine( ctx.addLine(
`w${componentID}.__updateProps(props${componentID}, extra.fiber${scopeVars && `w${componentID}.__updateProps(props${componentID}, extra.fiber, ${scope})${styleCode};`
", " + scopeVars})${styleCode};`
); );
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`); ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
if (registerCode) { if (registerCode) {
@@ -439,16 +398,14 @@ QWeb.addDirective({
} }
} }
ctx.addLine(`let def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`); ctx.addLine(
`let fiber = w${componentID}.__prepare(extra.fiber, ${scope}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
// hack: specify empty remove hook to prevent the node from being removed from the DOM // hack: specify empty remove hook to prevent the node from being removed from the DOM
const insertHook = refExpr ? `insert(vn) {${refExpr}},` : ""; const insertHook = refExpr ? `insert(vn) {${refExpr}},` : "";
ctx.addLine( ctx.addLine(
`let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});` `let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});`
); );
ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`);
ctx.addLine(
`def${defID}.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
if (registerCode) { if (registerCode) {
ctx.addLine(registerCode); ctx.addLine(registerCode);
} }
+11 -12
View File
@@ -46,10 +46,9 @@ export class Fiber {
// scheduler. // scheduler.
counter: number = 0; counter: number = 0;
target: HTMLElement | null; inserter: (el: HTMLElement) => void | null;
scope: any; scope: any;
vars: any;
component: Component<any, any>; component: Component<any, any>;
vnode: VNode | null = null; vnode: VNode | null = null;
@@ -62,14 +61,13 @@ export class Fiber {
error?: Error; error?: Error;
constructor(parent: Fiber | null, component: Component<any, any>, force, target) { constructor(parent: Fiber | null, component: Component<any, any>, force, inserter) {
this.component = component; this.component = component;
this.force = force; this.force = force;
this.target = target; this.inserter = inserter;
const __owl__ = component.__owl__; const __owl__ = component.__owl__;
this.scope = __owl__.scope; this.scope = __owl__.scope;
this.vars = __owl__.vars;
this.root = parent ? parent.root : this; this.root = parent ? parent.root : this;
this.parent = parent; this.parent = parent;
@@ -181,7 +179,7 @@ export class Fiber {
complete() { complete() {
let component = this.component; let component = this.component;
this.isCompleted = true; this.isCompleted = true;
if (!this.target && !component.__owl__.isMounted) { if (!this.inserter && !component.__owl__.isMounted) {
return; return;
} }
@@ -211,7 +209,7 @@ export class Fiber {
const fiber = patchQueue[i]; const fiber = patchQueue[i];
component = fiber.component; component = fiber.component;
component.__patch(fiber.vnode!); component.__patch(fiber.vnode!);
if (!fiber.shouldPatch && (!fiber.target || i !== 0)) { if (!fiber.shouldPatch && (!fiber.inserter || i !== 0)) {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm; component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
} }
component.__owl__.currentFiber = null; component.__owl__.currentFiber = null;
@@ -219,21 +217,22 @@ export class Fiber {
// insert into the DOM (mount case) // insert into the DOM (mount case)
let inDOM = false; let inDOM = false;
if (this.target) { if (this.inserter) {
this.target.appendChild(this.component.el!); this.inserter(this.component.el!);
inDOM = document.body.contains(this.target); inDOM = document.body.contains(this.component.el);
this.component.env.qweb.trigger("dom-appended");
} }
// call patched/mounted hook on each fiber of (reversed) patchQueue // call patched/mounted hook on each fiber of (reversed) patchQueue
for (let i = patchLen - 1; i >= 0; i--) { for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i]; const fiber = patchQueue[i];
component = fiber.component; component = fiber.component;
if (fiber.shouldPatch && !this.target) { if (fiber.shouldPatch && !this.inserter) {
component.patched(); component.patched();
if (component.__owl__.patchedCB) { if (component.__owl__.patchedCB) {
component.__owl__.patchedCB(); component.__owl__.patchedCB();
} }
} else if (this.target ? inDOM : true) { } else if (this.inserter ? inDOM : true) {
component.__callMounted(); component.__callMounted();
} }
} }
+14 -5
View File
@@ -25,6 +25,15 @@ export class Scheduler {
this.requestAnimationFrame = requestAnimationFrame; this.requestAnimationFrame = requestAnimationFrame;
} }
start() {
this.isRunning = true;
this.scheduleTasks();
}
stop() {
this.isRunning = false;
}
addFiber(fiber): Promise<void> { addFiber(fiber): Promise<void> {
// if the fiber was remapped into a larger rendering fiber, it may not be a // 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 // root fiber. But we only want to register root fibers
@@ -43,7 +52,7 @@ export class Scheduler {
} }
}); });
if (!this.isRunning) { if (!this.isRunning) {
this.scheduleTasks(); this.start();
} }
}); });
} }
@@ -74,16 +83,16 @@ export class Scheduler {
return true; return true;
}); });
this.tasks = tasks.concat(this.tasks); this.tasks = tasks.concat(this.tasks);
if (this.tasks.length === 0) {
this.stop();
}
} }
scheduleTasks() { scheduleTasks() {
this.isRunning = true;
this.requestAnimationFrame(() => { this.requestAnimationFrame(() => {
this.flush(); this.flush();
if (this.tasks.length > 0) { if (this.isRunning) {
this.scheduleTasks(); this.scheduleTasks();
} else {
this.isRunning = false;
} }
}); });
} }
-1
View File
@@ -20,7 +20,6 @@
export class Observer { export class Observer {
rev: number = 1; rev: number = 1;
allowMutations: boolean = true; allowMutations: boolean = true;
dirty: boolean = false;
weakMap: WeakMap<any, any> = new WeakMap(); weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {} notifyCB() {}
+2 -1
View File
@@ -12,6 +12,7 @@ import * as _store from "./store";
import * as _utils from "./utils"; import * as _utils from "./utils";
import * as _tags from "./tags"; import * as _tags from "./tags";
import { AsyncRoot } from "./misc/async_root"; import { AsyncRoot } from "./misc/async_root";
import { Portal } from "./misc/portal";
import * as _hooks from "./hooks"; import * as _hooks from "./hooks";
import * as _context from "./context"; import * as _context from "./context";
import { Link } from "./router/link"; import { Link } from "./router/link";
@@ -29,7 +30,7 @@ export const router = { Router, RouteComponent, Link };
export const Store = _store.Store; export const Store = _store.Store;
export const utils = _utils; export const utils = _utils;
export const tags = _tags; export const tags = _tags;
export const misc = { AsyncRoot }; export const misc = { AsyncRoot, Portal };
export const hooks = Object.assign({}, _hooks, { export const hooks = Object.assign({}, _hooks, {
useContext: _context.useContext, useContext: _context.useContext,
useDispatch: _store.useDispatch, 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;
}
}
+139 -113
View File
@@ -19,38 +19,40 @@ import { htmlToVDOM } from "../vdom/html_to_vdom";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-esc and t-raw // 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; QWeb.utils.htmlToVDOM = htmlToVDOM;
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) { function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) {
ctx.rootContext.shouldDefineScope = true;
if (value === "0") { if (value === "0") {
const caller = ctx.getCaller(); if (ctx.parentNode) {
if (caller) { // the 'zero' magical symbol is where we can find the result of the rendering
qweb._compileNode(caller, ctx.getInliningContext()); // of the body of the t-call.
return; ctx.rootContext.shouldDefineUtils = true;
} const zeroArgs = ctx.escaping
} ? `{text: utils.vDomToString(scope[utils.zero])}`
: `...scope[utils.zero]`;
if (value.xml instanceof NodeList && !value.id) { ctx.addLine(`c${ctx.parentNode}.push(${zeroArgs});`);
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
} }
return; return;
} }
let exprID: string; let exprID: string;
if (typeof value === "string") { if (typeof value === "string") {
exprID = `_${ctx.generateID()}`; exprID = `_${ctx.generateID()}`;
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`); ctx.addLine(`let ${exprID} = ${ctx.formatExpression(value)};`);
} else { } else {
exprID = value.id; exprID = `scope.${value.id}`;
} }
ctx.addIf(`${exprID} || ${exprID} === 0`); ctx.addIf(`${exprID} != null`);
if (ctx.escaping) { 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) { if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`); ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
} else if (ctx.parentNode) { } else if (ctx.parentNode) {
@@ -59,26 +61,30 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
let nodeID = ctx.generateID(); let nodeID = ctx.generateID();
ctx.rootContext.rootNode = nodeID; ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID; ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`); ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`);
if (ctx.rootContext.shouldDefineResult) { if (ctx.rootContext.shouldDefineResult) {
ctx.addLine(`result = vn${nodeID}`); ctx.addLine(`result = vn${nodeID}`);
} }
} }
if (value.hasBody) {
ctx.stopProtectScope(protectID);
}
} else { } else {
ctx.rootContext.shouldDefineUtils = true; 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) { if (node.childNodes.length) {
ctx.addElse(); ctx.addElse();
qweb._compileChildren(node, ctx); qweb._compileChildren(node, ctx);
} }
if (value.xml instanceof NodeList && value.id) {
ctx.addElse();
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
}
}
ctx.closeIf(); ctx.closeIf();
} }
@@ -109,24 +115,46 @@ QWeb.addDirective({
name: "set", name: "set",
extraNames: ["value"], extraNames: ["value"],
priority: 60, priority: 60,
atNodeEncounter({ node, ctx }): boolean { atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldDefineScope = true;
const variable = node.getAttribute("t-set")!; const variable = node.getAttribute("t-set")!;
let value = node.getAttribute("t-value")!; let value = node.getAttribute("t-value")!;
ctx.variables[variable] = ctx.variables[variable] || {}; ctx.variables[variable] = ctx.variables[variable] || {};
let qwebvar = ctx.variables[variable]; let qwebvar = ctx.variables[variable];
const hasBody = node.hasChildNodes();
qwebvar.id = variable;
qwebvar.expr = `scope.${variable}`;
if (value) { if (value) {
const formattedValue = ctx.formatExpression(value); const formattedValue = ctx.formatExpression(value);
if (ctx.variables.hasOwnProperty(variable) && qwebvar.id) { ctx.addLine(`${qwebvar.expr} = ${formattedValue};`);
ctx.addLine(`${qwebvar.id} = ${formattedValue}`); qwebvar.value = formattedValue;
} else { }
const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`); if (hasBody) {
qwebvar.id = varName; ctx.rootContext.shouldDefineUtils = true;
qwebvar.expr = formattedValue; if (value) {
ctx.addIf(`!(${qwebvar.expr})`);
}
const tempParentNodeID = ctx.generateID();
const _parentNode = ctx.parentNode;
ctx.parentNode = tempParentNodeID;
ctx.addLine(`let 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 {
qwebvar.xml = node.childNodes;
} }
return true; return true;
} }
@@ -140,7 +168,7 @@ QWeb.addDirective({
priority: 20, priority: 20,
atNodeEncounter({ node, ctx }): boolean { atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-if")!); 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; return false;
}, },
finalize({ ctx }) { finalize({ ctx }) {
@@ -153,7 +181,9 @@ QWeb.addDirective({
priority: 30, priority: 30,
atNodeEncounter({ node, ctx }): boolean { atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-elif")!); 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(); ctx.indent();
return false; return false;
}, },
@@ -182,6 +212,10 @@ QWeb.addDirective({
name: "call", name: "call",
priority: 50, priority: 50,
atNodeEncounter({ node, qweb, ctx }): boolean { atNodeEncounter({ node, qweb, ctx }): boolean {
// Step 1: sanity checks
// ------------------------------------------------
ctx.rootContext.shouldDefineScope = true;
ctx.rootContext.shouldDefineUtils = true;
if (node.nodeName !== "t") { if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')"); throw new Error("Invalid tag for t-call directive (should be 't')");
} }
@@ -190,77 +224,66 @@ QWeb.addDirective({
if (!nodeTemplate) { if (!nodeTemplate) {
throw new Error(`Cannot find template "${subTemplate}" (t-call)`); throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
} }
const nodeCopy = node.cloneNode(true) as Element;
nodeCopy.removeAttribute("t-call");
// extract variables from nodecopy // Step 2: compile target template in sub templates
const tempCtx = new CompilationContext(); // ------------------------------------------------
tempCtx.allowMultipleRoots = true; if (!qweb.subTemplates[subTemplate]) {
qweb._compileNode(nodeCopy, tempCtx); qweb.subTemplates[subTemplate] = true;
const vars = Object.assign({}, ctx.variables, tempCtx.variables); const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx);
qweb.subTemplates[subTemplate] = subTemplateFn;
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;
} }
templateMap[subTemplate] = true;
if (hasNewVariables) { // Step 3: compile t-call body if necessary
ctx.addLine("{"); // ------------------------------------------------
let hasBody = node.hasChildNodes();
let protectID;
if (hasBody) {
// we add a sub scope to protect the ambient scope
ctx.addLine(`{`);
ctx.indent(); ctx.indent();
// add new variables, if any protectID = ctx.startProtectScope();
for (let key in tempCtx.variables) { const nodeCopy = node.cloneNode(true) as Element;
const v = tempCtx.variables[key]; for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) {
if (v.expr) { nodeCopy.removeAttribute(attr);
ctx.addLine(`let ${v.id} = ${v.expr};`);
}
// todo: handle XML variables...
} }
} const parentNode = ctx.parentNode;
qweb._compileNode(nodeTemplate.elem, subCtx); ctx.parentNode = "__0";
// this local scope is intended to trap c__0
// close new scope ctx.addLine(`{`);
if (hasNewVariables) { 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.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)";
const parentComponent = `utils.getComponent(context)`;
if (ctx.parentNode) {
ctx.addLine(
`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, parent: ${parentComponent}}));`
);
} 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, parent: ${parentComponent}}));`
);
ctx.addLine(`result = result[0]`);
}
// Step 5: restore previous scope
// ------------------------------------------------
if (hasBody) {
ctx.stopProtectScope(protectID);
ctx.dedent();
ctx.addLine(`}`);
} }
return true; return true;
@@ -275,29 +298,31 @@ QWeb.addDirective({
extraNames: ["as"], extraNames: ["as"],
priority: 10, priority: 10,
atNodeEncounter({ node, qweb, ctx }): boolean { atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldProtectContext = true; ctx.rootContext.shouldDefineScope = true;
ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1); ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1);
const elems = node.getAttribute("t-foreach")!; const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!; const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID(); let arrayID = ctx.generateID();
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`); ctx.addLine(`let _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`); ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
let keysID = ctx.generateID(); let keysID = ctx.generateID();
let valuesID = ctx.generateID(); let valuesID = ctx.generateID();
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`); ctx.addLine(`let _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addIf(`!(_${arrayID} instanceof Array)`); ctx.addIf(`!(_${arrayID} instanceof Array)`);
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`); ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`); ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf(); ctx.closeIf();
ctx.addLine(`var _length${keysID} = _${keysID}.length;`); ctx.addLine(`let _length${keysID} = _${keysID}.length;`);
let varsID = ctx.startProtectScope();
const loopVar = `i${ctx.loopNumber}`; const loopVar = `i${ctx.loopNumber}`;
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`); ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
ctx.indent(); ctx.indent();
ctx.addToScope(name + "_first", `${loopVar} === 0`);
ctx.addToScope(name + "_last", `${loopVar} === _length${keysID} - 1`); ctx.addLine(`scope.${name}_first = ${loopVar} === 0`);
ctx.addToScope(name + "_index", loopVar); ctx.addLine(`scope.${name}_last = ${loopVar} === _length${keysID} - 1`);
ctx.addToScope(name, `_${keysID}[${loopVar}]`); ctx.addLine(`scope.${name}_index = ${loopVar}`);
ctx.addToScope(name + "_value", `_${valuesID}[${loopVar}]`); ctx.addLine(`scope.${name} = _${keysID}[${loopVar}]`);
ctx.addLine(`scope.${name}_value = _${valuesID}[${loopVar}]`);
const nodeCopy = <Element>node.cloneNode(true); const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn = let shouldWarn =
!nodeCopy.hasAttribute("t-key") && !nodeCopy.hasAttribute("t-key") &&
@@ -314,6 +339,7 @@ QWeb.addDirective({
qweb._compileNode(nodeCopy, ctx); qweb._compileNode(nodeCopy, ctx);
ctx.dedent(); ctx.dedent();
ctx.addLine("}"); ctx.addLine("}");
ctx.stopProtectScope(varsID);
return true; return true;
} }
}); });
+23 -61
View File
@@ -10,36 +10,28 @@ export class CompilationContext {
code: string[] = []; code: string[] = [];
variables: { [key: string]: QWebVar } = {}; variables: { [key: string]: QWebVar } = {};
escaping: boolean = false; escaping: boolean = false;
parentNode: number | null = null; parentNode: number | null | string = null;
parentTextNode: number | null = null; parentTextNode: number | null = null;
rootNode: number | null = null; rootNode: number | null = null;
indentLevel: number = 0; indentLevel: number = 0;
rootContext: CompilationContext; rootContext: CompilationContext;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldDefineParent: boolean = false; shouldDefineParent: boolean = false;
shouldDefineScope: boolean = false;
shouldDefineQWeb: boolean = false; shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false; shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false; shouldDefineRefs: boolean = false;
shouldDefineResult: boolean = true; shouldDefineResult: boolean = true;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
loopNumber: number = 0; loopNumber: number = 0;
inPreTag: boolean = false; inPreTag: boolean = false;
templateName: string; templateName: string;
allowMultipleRoots: boolean = false; allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false; hasParentWidget: boolean = false;
scopeVars: any[] = [];
currentKey: string = ""; currentKey: string = "";
templates: { [key: string]: boolean } = {};
callingLevel: number = 0;
inliningLevel: number = 0;
constructor(name?: string) { constructor(name?: string) {
this.rootContext = this; this.rootContext = this;
this.templateName = name || "noname"; this.templateName = name || "noname";
this.templates[this.templateName] = true; this.addLine("let h = this.h;");
this.addLine("var h = this.h;");
} }
generateID(): number { generateID(): number {
@@ -54,9 +46,12 @@ export class CompilationContext {
* Such a key is necessary when we need to associate an id to some element * Such a key is necessary when we need to associate an id to some element
* generated by a template (for example, a component) * generated by a template (for example, a component)
*/ */
generateTemplateKey(): string { generateTemplateKey(prefix: string = ""): string {
const id = this.generateID(); const id = this.generateID();
let locationExpr = `\`__${this.generateID()}__`; if (this.loopNumber === 0 && !this.currentKey) {
return `'${prefix}__${id}__'`;
}
let locationExpr = `\`${prefix}__${id}__`;
for (let i = 0; i < this.loopNumber - 1; i++) { for (let i = 0; i < this.loopNumber - 1; i++) {
locationExpr += `\${i${i + 1}}__`; locationExpr += `\${i${i + 1}}__`;
} }
@@ -71,30 +66,16 @@ export class CompilationContext {
} }
generateCode(): string[] { 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) { if (this.shouldDefineResult) {
this.code.unshift(" let result;"); this.code.unshift(" let result;");
} }
if (this.shouldDefineScope) {
this.code.unshift(" let scope = Object.create(context);");
}
if (this.shouldDefineRefs) { if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};"); 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.shouldDefineParent) {
if (this.hasParentWidget) { if (this.hasParentWidget) {
this.code.unshift(" let parent = extra.parent;"); this.code.unshift(" let parent = extra.parent;");
@@ -131,10 +112,6 @@ export class CompilationContext {
subContext(key: keyof CompilationContext, value: any): CompilationContext { subContext(key: keyof CompilationContext, value: any): CompilationContext {
const newContext = Object.create(this); const newContext = Object.create(this);
newContext[key] = value; newContext[key] = value;
if (key === "caller") {
newContext.callingLevel++;
newContext.inliningLevel++;
}
return newContext; return newContext;
} }
@@ -152,11 +129,6 @@ export class CompilationContext {
return this.code.length - 1; 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) { addIf(condition: string) {
this.addLine(`if (${condition}) {`); this.addLine(`if (${condition}) {`);
this.indent(); this.indent();
@@ -172,27 +144,6 @@ export class CompilationContext {
this.dedent(); this.dedent();
this.addLine("}"); this.addLine("}");
} }
/**
* Recursively (inverse) fetches the `caller` of a context
* Useful to determine to which t-call a t-raw="0" refers
*/
getCaller(targetLevel?: number): Element | null {
if (targetLevel === undefined) {
targetLevel = this.inliningLevel;
}
if (targetLevel === this.callingLevel) {
return this.caller || null;
}
const proto = (this as any).__proto__;
return proto ? proto.getCaller(targetLevel) : null;
}
/**
* Marks the context with the current recursive level
* in which we are for inlining archs (t-raw="0")
*/
getInliningContext(): CompilationContext {
return this.subContext("inliningLevel", this.inliningLevel - 1);
}
getValue(val: any): QWebVar | string { getValue(val: any): QWebVar | string {
return val in this.variables ? this.getValue(this.variables[val]) : val; return val in this.variables ? this.getValue(this.variables[val]) : val;
@@ -205,6 +156,7 @@ export class CompilationContext {
* - replace already defined variables by their internal name * - replace already defined variables by their internal name
*/ */
formatExpression(expr: string): string { formatExpression(expr: string): string {
this.rootContext.shouldDefineScope = true;
return compileExpr(expr, this.variables); return compileExpr(expr, this.variables);
} }
@@ -225,4 +177,14 @@ export class CompilationContext {
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}"); let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`"; return "`" + r + "`";
} }
startProtectScope(): number {
const protectID = this.generateID();
this.rootContext.shouldDefineScope = true;
this.addLine(`let _origScope${protectID} = scope;`);
this.addLine(`scope = Object.assign(Object.create(context), scope);`);
return protectID;
}
stopProtectScope(protectID: number) {
this.addLine(`scope = _origScope${protectID};`);
}
} }
+37 -17
View File
@@ -39,9 +39,10 @@ const WORD_REPLACEMENT = {
}; };
export interface QWebVar { export interface QWebVar {
id?: string; id: string; // foo
expr?: string; expr: string; // scope.foo (local variables => only foo)
xml?: NodeList; value?: string; // 1 + 3
hasBody?: boolean;
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -63,6 +64,7 @@ type TKind =
interface Token { interface Token {
type: TKind; type: TKind;
value: string; value: string;
originalValue?: string;
size?: number; size?: number;
} }
@@ -79,7 +81,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
// note that the space after typeof is relevant. It makes sure that the formatted // note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof // expression has a space after typeof
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ".split(","); const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;".split(",");
type Tokenizer = (expr: string) => Token | false; type Tokenizer = (expr: string) => Token | false;
@@ -227,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`) * - 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 * - 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}`) * 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); const tokens = tokenize(expr);
let result = "";
for (let i = 0; i < tokens.length; i++) { for (let i = 0; i < tokens.length; i++) {
let token = tokens[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)) { 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) {
if (prevToken.type === "OPERATOR" && prevToken.value === ".") { if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false; isVar = false;
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") { } else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
let nextToken = tokens[i + 1];
if (nextToken && nextToken.type === "COLON") { if (nextToken && nextToken.type === "COLON") {
isVar = false; isVar = false;
} }
} }
} }
if (isVar) { }
if (token.value in vars && "id" in vars[token.value]) { if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
token.value = vars[token.value].id!; if (token.type === "RIGHT_PAREN") {
} else { let j = i - 1;
token.value = `context['${token.value}']`; 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("");
} }
+64 -42
View File
@@ -26,49 +26,66 @@ export const MODS_CODE = {
stop: "e.stopPropagation();" stop: "e.stopPropagation();"
}; };
interface HandlerInfo {
event: string;
handler: string;
}
const FNAMEREGEXP = /^[$A-Z_][0-9A-Z_$]*$/i;
export function makeHandlerCode(
ctx,
fullName,
value,
putInCache: boolean,
modcodes = MODS_CODE
): HandlerInfo {
const [event, ...mods] = fullName.slice(5).split(".");
if (!event) {
throw new Error("Missing event name with t-on directive");
}
let code: string;
// check if it is a method with no args, a method with args or an expression
let args: string = "";
const name: string = value.replace(/\(.*\)/, function(_args) {
args = _args.slice(1, -1);
return "";
});
const isMethodCall = name.match(FNAMEREGEXP);
// then generate code
if (isMethodCall) {
ctx.rootContext.shouldDefineUtils = true;
const comp = `utils.getComponent(context)`;
if (args) {
let argId = ctx.generateID();
ctx.addLine(`let args${argId} = [${ctx.formatExpression(args)}];`);
code = `${comp}['${name}'](...args${argId}, e);`;
putInCache = false;
} else {
code = `${comp}['${name}'](e);`;
}
} else {
// if we get here, then it is an expression
putInCache = false;
code = ctx.formatExpression(value);
}
const modCode = mods.map(mod => modcodes[mod]).join("");
let handler = `function (e) {if (!context.__owl__.isMounted){return}${modCode}${code}}`;
if (putInCache) {
const key = ctx.generateTemplateKey(event);
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || ${handler};`);
handler = `extra.handlers[${key}]`;
}
return { event, handler };
}
QWeb.addDirective({ QWeb.addDirective({
name: "on", name: "on",
priority: 90, priority: 90,
atNodeCreation({ ctx, fullName, value, nodeID }) { atNodeCreation({ ctx, fullName, value, nodeID }) {
ctx.rootContext.shouldDefineOwner = true; const { event, handler } = makeHandlerCode(ctx, fullName, value, true);
const [eventName, ...mods] = fullName.slice(5).split("."); ctx.addLine(`p${nodeID}.on['${event}'] = ${handler};`);
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
let handlerName = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
let handler = `function (e) {`;
handler += mods
.map(function(mod) {
return MODS_CODE[mod];
})
.join("");
if (handlerName) {
if (!extraArgs) {
handler += `const fn = context['${handlerName}'];`;
handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerName}; }`;
handler += `}`;
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
} else {
const handlerKey = `handler${ctx.generateID()}`;
ctx.addLine(
`const ${handlerKey} = context['${handlerName}'] && context['${handlerName}'].bind(${params});`
);
handler += `if (${handlerKey}) { ${handlerKey}(e); } else { context.${value}; }`;
handler += `}`;
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
}
} else {
handler += "}";
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
}
} }
}); });
@@ -195,9 +212,8 @@ QWeb.addDirective({
QWeb.addDirective({ QWeb.addDirective({
name: "slot", name: "slot",
priority: 80, priority: 80,
atNodeEncounter({ ctx, value }): boolean { atNodeEncounter({ ctx, value, node, qweb }): boolean {
const slotKey = ctx.generateID(); const slotKey = ctx.generateID();
ctx.rootContext.shouldDefineOwner = true;
ctx.addLine( ctx.addLine(
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];` `const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
); );
@@ -211,11 +227,17 @@ QWeb.addDirective({
ctx.addLine(`result = {}`); ctx.addLine(`result = {}`);
} }
ctx.addLine( ctx.addLine(
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: extra.parent || owner}));` `slot${slotKey}.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: ${parentNode}, parent: extra.parent || context}));`
); );
if (!ctx.parentNode) { if (!ctx.parentNode) {
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`); 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(); ctx.closeIf();
return true; return true;
} }
+54 -34
View File
@@ -1,7 +1,7 @@
import { EventBus } from "../core/event_bus"; import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index"; import { h, patch, VNode } from "../vdom/index";
import { CompilationContext } from "./compilation_context"; import { CompilationContext } from "./compilation_context";
import { shallowEqual } from "../utils"; import { shallowEqual, escape } from "../utils";
import { addNS } from "../vdom/vdom"; import { addNS } from "../vdom/vdom";
/** /**
@@ -87,6 +87,7 @@ interface Utils {
} }
const UTILS: Utils = { const UTILS: Utils = {
zero: Symbol("zero"),
toObj(expr) { toObj(expr) {
if (typeof expr === "string") { if (typeof expr === "string") {
expr = expr.trim(); expr = expr.trim();
@@ -105,6 +106,26 @@ const UTILS: Utils = {
shallowEqual, shallowEqual,
addNameSpace(vnode) { addNameSpace(vnode) {
addNS(vnode.data, vnode.children, vnode.sel); 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("");
},
getComponent(obj) {
while (obj && !obj.hasOwnProperty("__owl__")) {
obj = obj.__proto__;
}
return obj;
} }
}; };
@@ -171,7 +192,7 @@ export class QWeb extends EventBus {
// recursiveTemplates contains sub templates called with t-call, but which // recursiveTemplates contains sub templates called with t-call, but which
// ends up in recursive situations. This is very similar to the slot situation, // ends up in recursive situations. This is very similar to the slot situation,
// as in we need to propagate the scope. // as in we need to propagate the scope.
recursiveFns = {}; subTemplates = {};
isUpdating: boolean = false; isUpdating: boolean = false;
translateFn?: QWebConfig["translateFn"]; translateFn?: QWebConfig["translateFn"];
@@ -333,8 +354,17 @@ export class QWeb extends EventBus {
return vnode.text!; return vnode.text!;
} }
const node = document.createElement(vnode.sel); const node = document.createElement(vnode.sel);
const result = patch(node, vnode); const elem = patch(node, vnode).elm as HTMLElement;
return (<HTMLElement>result.elm).outerHTML; function escapeTextNodes(node) {
if (node.nodeType === 3) {
node.textContent = escape(node.textContent);
}
for (let n of node.childNodes) {
escapeTextNodes(n);
}
}
escapeTextNodes(elem);
return elem.outerHTML;
} }
/** /**
@@ -360,23 +390,12 @@ export class QWeb extends EventBus {
ctx.shouldDefineResult = false; ctx.shouldDefineResult = false;
} }
if (parentContext) { if (parentContext) {
ctx.templates = Object.create(parentContext.templates);
ctx.variables = Object.create(parentContext.variables); ctx.variables = Object.create(parentContext.variables);
ctx.parentNode = parentContext.parentNode || ctx.generateID(); ctx.parentNode = parentContext.parentNode || ctx.generateID();
ctx.allowMultipleRoots = true; ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true; ctx.hasParentWidget = true;
ctx.shouldDefineResult = false; ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`); 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); this._compileNode(elem, ctx);
@@ -392,12 +411,13 @@ export class QWeb extends EventBus {
} }
let code = ctx.generateCode(); let code = ctx.generateCode();
const templateName = ctx.templateName.replace(/`/g, "'").slice(0, 200);
code.unshift(` // Template name: "${templateName}"`);
let template; let template;
try { try {
template = new Function("context", "extra", code.join("\n")) as CompiledTemplate; template = new Function("context, extra", code.join("\n")) as CompiledTemplate;
} catch (e) { } catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`); console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(code.join("\n")); console.warn(code.join("\n"));
console.groupEnd(); console.groupEnd();
@@ -446,14 +466,13 @@ export class QWeb extends EventBus {
// this is an unusual situation: this text node is the result of the // this is an unusual situation: this text node is the result of the
// template rendering. // template rendering.
let nodeID = ctx.generateID(); let nodeID = ctx.generateID();
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`); ctx.addLine(`let vn${nodeID} = {text: \`${text}\`};`);
ctx.addLine(`result = vn${nodeID};`); ctx.addLine(`result = vn${nodeID};`);
ctx.rootContext.rootNode = nodeID; ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID; ctx.rootContext.parentTextNode = nodeID;
} }
return; return;
} }
if (ctx !== ctx.rootContext) { if (ctx !== ctx.rootContext) {
ctx = ctx.subContext("currentKey", ctx.currentKey); ctx = ctx.subContext("currentKey", ctx.currentKey);
} }
@@ -650,15 +669,16 @@ export class QWeb extends EventBus {
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) { if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
const attID = ctx.generateID(); const attID = ctx.generateID();
if (name === "class") { if (name === "class") {
let classDef = value if ((value = value.trim())) {
.trim() let classDef = value
.split(/\s+/) .split(/\s+/)
.map(a => `'${a}':true`) .map(a => `'${a}':true`)
.join(","); .join(",");
classObj = `_${ctx.generateID()}`; classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`); ctx.addLine(`let ${classObj} = {${classDef}};`);
}
} else { } else {
ctx.addLine(`var _${attID} = '${value}';`); ctx.addLine(`let _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) { if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it // attribute contains 'non letters' => we want to quote it
name = '"' + name + '"'; name = '"' + name + '"';
@@ -672,7 +692,7 @@ export class QWeb extends EventBus {
if (name.startsWith("t-att-")) { if (name.startsWith("t-att-")) {
let attName = name.slice(6); let attName = name.slice(6);
const v = ctx.getValue(value); 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") { if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
@@ -694,12 +714,12 @@ export class QWeb extends EventBus {
const attValue = (<Element>node).getAttribute(attName); const attValue = (<Element>node).getAttribute(attName);
if (attValue) { if (attValue) {
const attValueID = ctx.generateID(); const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`); ctx.addLine(`let _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`; formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":")); const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1); attrs.splice(attrIndex, 1);
} }
ctx.addLine(`var _${attID} = ${formattedValue};`); ctx.addLine(`let _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`); attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID); handleBooleanProps(attName, attID);
} }
@@ -715,9 +735,9 @@ export class QWeb extends EventBus {
const attID = ctx.generateID(); const attID = ctx.generateID();
let staticVal = (<Element>node).getAttribute(attName); let staticVal = (<Element>node).getAttribute(attName);
if (staticVal) { if (staticVal) {
ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`); ctx.addLine(`let _${attID} = '${staticVal} ' + ${formattedExpr};`);
} else { } else {
ctx.addLine(`var _${attID} = ${formattedExpr};`); ctx.addLine(`let _${attID} = ${formattedExpr};`);
} }
attrs.push(`${attName}: _${attID}`); attrs.push(`${attName}: _${attID}`);
} }
@@ -725,7 +745,7 @@ export class QWeb extends EventBus {
// t-att= attributes // t-att= attributes
if (name === "t-att") { if (name === "t-att") {
let id = ctx.generateID(); let id = ctx.generateID();
ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`); ctx.addLine(`let _${id} = ${ctx.formatExpression(value!)};`);
tattrs.push(id); tattrs.push(id);
} }
} }
@@ -757,7 +777,7 @@ export class QWeb extends EventBus {
ctx.addLine(`}`); ctx.addLine(`}`);
ctx.closeIf(); ctx.closeIf();
} }
ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`); ctx.addLine(`let vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
if (ctx.parentNode) { if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`); ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
} }
+3
View File
@@ -141,6 +141,9 @@ export function useStore(selector, options: SelectorOptions = {}): any {
}, },
set(target, k, v) { set(target, k, v) {
throw new Error("Store state should only be modified through actions"); throw new Error("Store state should only be modified through actions");
},
has(target, k) {
return k in result;
} }
}); });
} }
+3 -6
View File
@@ -58,12 +58,9 @@ export function escape(str: string | number | undefined): string {
if (typeof str === "number") { if (typeof str === "number") {
return String(str); return String(str);
} }
return str const p = document.createElement("p");
.replace(/&/g, "&amp;") p.textContent = str;
.replace(/</g, "&lt;") return p.innerHTML;
.replace(/>/g, "&gt;")
.replace(/"/g, "&#x27;")
.replace(/`/g, "&#x60;");
} }
/** /**
+1 -1
View File
@@ -219,7 +219,7 @@ function updateClass(oldVnode: VNode, vnode: VNode): void {
elm = vnode.elm as Element; elm = vnode.elm as Element;
for (name in oldClass) { for (name in oldClass) {
if (!klass[name]) { if (name && !klass[name]) {
elm.classList.remove(name); elm.classList.remove(name);
} }
} }
+91 -95
View File
@@ -1,154 +1,149 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`animations t-transition combined with component 1`] = ` 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 utils = this.constructor.utils;
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let owner = context; let scope = Object.create(context);
var h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
//COMPONENT // Component 'Child'
let k4 = \`__5__\`; let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false; let props2 = {};
let props3 = {}; if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) { w2.destroy();
w3.destroy(); w2 = false;
w3 = false;
} }
if (w3) { if (w2) {
w3.__updateProps(props3, extra.fiber, undefined, undefined); w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w3.__owl__.pvnode; let pvnode = w2.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey3 = \`Child\`; let componentKey2 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child']; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w3 = new W3(parent, props3); w2 = new W2(parent, props2);
const __patch3 = w3.__patch; const __patch2 = w2.__patch;
w3.__patch = fiber => {__patch3.call(w3, fiber); if(!w3.__owl__.transitionInserted){w3.__owl__.transitionInserted = true;utils.transitionInsert(w3.__owl__.vnode, 'chimay');}}; w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k4] = w3.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {let finalize = () => { let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
w3.destroy(); w2.destroy();
}; };
delete w3.__owl__.transitionInserted; delete w2.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}}); utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode); c1.push(pvnode);
w3.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
} }
w3.__owl__.parentLastFiberId = extra.fiber.id; w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1; return vn1;
}" }"
`; `;
exports[`animations t-transition combined with t-component and t-if 1`] = ` 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 utils = this.constructor.utils;
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let owner = context; let scope = Object.create(context);
var h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
if (context['state'].display) { if (scope['state'].display) {
//COMPONENT // Component 'Child'
let k4 = \`__5__\`; let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false; let props2 = {};
let props3 = {}; if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) { w2.destroy();
w3.destroy(); w2 = false;
w3 = false;
} }
if (w3) { if (w2) {
w3.__updateProps(props3, extra.fiber, undefined, undefined); w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w3.__owl__.pvnode; let pvnode = w2.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey3 = \`Child\`; let componentKey2 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child']; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w3 = new W3(parent, props3); w2 = new W2(parent, props2);
const __patch3 = w3.__patch; const __patch2 = w2.__patch;
w3.__patch = fiber => {__patch3.call(w3, fiber); if(!w3.__owl__.transitionInserted){w3.__owl__.transitionInserted = true;utils.transitionInsert(w3.__owl__.vnode, 'chimay');}}; w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k4] = w3.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {let finalize = () => { let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
w3.destroy(); w2.destroy();
}; };
delete w3.__owl__.transitionInserted; delete w2.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}}); utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode); c1.push(pvnode);
w3.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
} }
w3.__owl__.parentLastFiberId = extra.fiber.id; w2.__owl__.parentLastFiberId = extra.fiber.id;
} }
return vn1; return vn1;
}" }"
`; `;
exports[`animations t-transition combined with t-component, remove and re-add before transitionend 1`] = ` exports[`animations t-transition combined with t-component, remove and re-add before transitionend 1`] = `
"function anonymous(context,extra "function anonymous(context, extra
) { ) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let owner = context; let scope = Object.create(context);
var h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
if (context['state'].flag) { if (scope['state'].flag) {
//COMPONENT // Component 'Child'
let k4 = \`__5__\`; let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false; let props2 = {};
let props3 = {}; if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) { w2.destroy();
w3.destroy(); w2 = false;
w3 = false;
} }
if (w3) { if (w2) {
w3.__updateProps(props3, extra.fiber, undefined, undefined); w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w3.__owl__.pvnode; let pvnode = w2.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey3 = \`Child\`; let componentKey2 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child']; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w3 = new W3(parent, props3); w2 = new W2(parent, props2);
const __patch3 = w3.__patch; const __patch2 = w2.__patch;
w3.__patch = fiber => {__patch3.call(w3, fiber); if(!w3.__owl__.transitionInserted){w3.__owl__.transitionInserted = true;utils.transitionInsert(w3.__owl__.vnode, 'chimay');}}; w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap[k4] = w3.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {let finalize = () => { let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
w3.destroy(); w2.destroy();
}; };
delete w3.__owl__.transitionInserted; delete w2.__owl__.transitionInserted;
utils.transitionRemove(vn, 'chimay', finalize);}}}); utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode); c1.push(pvnode);
w3.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
} }
w3.__owl__.parentLastFiberId = extra.fiber.id; w2.__owl__.parentLastFiberId = extra.fiber.id;
} }
return vn1; return vn1;
}" }"
`; `;
exports[`animations t-transition with no delay/duration 1`] = ` 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 utils = this.constructor.utils;
var h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1); let vn1 = h('span', p1, c1);
p1.hook = { p1.hook = {
insert: vn => { insert: vn => {
utils.transitionInsert(vn, 'jupiler'); utils.transitionInsert(vn, 'jupiler');
@@ -163,12 +158,13 @@ exports[`animations t-transition with no delay/duration 1`] = `
`; `;
exports[`animations t-transition, on a simple node (insert) 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 utils = this.constructor.utils;
var h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1); let vn1 = h('span', p1, c1);
p1.hook = { p1.hook = {
insert: vn => { insert: vn => {
utils.transitionInsert(vn, 'chimay'); utils.transitionInsert(vn, 'chimay');
+6 -6
View File
@@ -254,11 +254,11 @@ describe("animations", () => {
widget.state.display = false; widget.state.display = false;
patchNextFrame(cb => { patchNextFrame(cb => {
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="__5__">blue</span></div>' '<div><span class="chimay-leave chimay-leave-active" data-owl-key="__3__">blue</span></div>'
); );
cb(); cb();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__5__">blue</span></div>' '<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>'
); );
def.resolve(); def.resolve();
}); });
@@ -371,7 +371,7 @@ describe("animations", () => {
await def; // wait for the mocked repaint to be done await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__5__">blue</span></div>'); expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__3__">blue</span></div>');
}); });
test("transitionInsert is called the correct amount of times", async () => { test("transitionInsert is called the correct amount of times", async () => {
@@ -405,14 +405,14 @@ describe("animations", () => {
widget.state.flag = false; widget.state.flag = false;
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__5__">blue</span></div>' '<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>'
); );
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1); expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = true; widget.state.flag = true;
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__5__">blue</span></div>' '<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__3__">blue</span></div>'
); );
expect(QWeb.utils.transitionInsert).toBeCalledTimes(2); expect(QWeb.utils.transitionInsert).toBeCalledTimes(2);
@@ -423,7 +423,7 @@ describe("animations", () => {
expect(QWeb.utils.transitionInsert).toBeCalledTimes(3); expect(QWeb.utils.transitionInsert).toBeCalledTimes(3);
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__5__">blue</span></div>'); expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__3__">blue</span></div>');
QWeb.utils.transitionInsert = oldTransitionInsert; QWeb.utils.transitionInsert = oldTransitionInsert;
}); });
}); });
File diff suppressed because it is too large Load Diff
@@ -1,41 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`props validation props are validated in dev mode (code snapshot) 1`] = ` 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 utils = this.constructor.utils;
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let owner = context; let scope = Object.create(context);
var h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
//COMPONENT // Component 'Child'
let k4 = \`__5__\`; let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false; let props2 = {message:1};
let props3 = {message:1}; if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) { w2.destroy();
w3.destroy(); w2 = false;
w3 = false;
} }
if (w3) { if (w2) {
w3.__updateProps(props3, extra.fiber, undefined, undefined); w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w3.__owl__.pvnode; let pvnode = w2.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey3 = \`Child\`; let componentKey2 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child']; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w3 = new W3(parent, props3); w2 = new W2(parent, props2);
parent.__owl__.cmap[k4] = w3.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {w3.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode); c1.push(pvnode);
w3.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
} }
w3.__owl__.parentLastFiberId = extra.fiber.id; w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1; return vn1;
}" }"
`; `;
@@ -0,0 +1,488 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`t-slot directive can define and call slots 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"Parent\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Dialog'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Dialog\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Dialog'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`t-slot directive can define and call slots 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"Dialog\\"
let h = this.h;
let c6 = [], p6 = {key:6};
let vn6 = h('div', p6, c6);
let c7 = [], p7 = {key:7};
let vn7 = h('div', p7, c7);
c6.push(vn7);
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot8) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context}));
}
let c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9);
c6.push(vn9);
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot10) {
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
}
return vn6;
}"
`;
exports[`t-slot directive can define and call slots 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_header_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`header\`});
}"
`;
exports[`t-slot directive can define and call slots 4`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`footer\`});
}"
`;
exports[`t-slot directive content is the default slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts rocks\`});
}"
`;
exports[`t-slot directive dafault slots can define a default content 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let h = this.h;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot5) {
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context}));
} else {
c4.push({text: \`default content\`});
}
return vn4;
}"
`;
exports[`t-slot directive default slot work with text nodes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let h = this.h;
let c1 = extra.parentNode;
c1.push({text: \`sts rocks\`});
}"
`;
exports[`t-slot directive multiple roots are allowed in a default slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts\`});
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`rocks\`});
}"
`;
exports[`t-slot directive multiple roots are allowed in a named slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_content_template\\"
let h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts\`});
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`rocks\`});
}"
`;
exports[`t-slot directive named slots can define a default content 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let h = this.h;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot5) {
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context}));
} else {
c4.push({text: \`default content\`});
}
return vn4;
}"
`;
exports[`t-slot directive refs are properly bound in slots 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils;
context.__owl__.refs = context.__owl__.refs || {};
let h = this.h;
let c1 = extra.parentNode;
let c8 = [], p8 = {key:8,on:{}};
let vn8 = h('button', p8, c8);
c1.push(vn8);
extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p8.on['click'] = extra.handlers['click__9__'];
const ref10 = \`myButton\`;
p8.hook = {
create: (_, n) => {
context.__owl__.refs[ref10] = n.elm;
},
destroy: () => {
delete context.__owl__.refs[ref10];
},
};
c8.push({text: \`do something\`});
}"
`;
exports[`t-slot directive slots are rendered with proper context 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils;
let h = this.h;
let c1 = extra.parentNode;
let c8 = [], p8 = {key:8,on:{}};
let vn8 = h('button', p8, c8);
c1.push(vn8);
extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p8.on['click'] = extra.handlers['click__9__'];
c8.push({text: \`do something\`});
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 2 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"Link\\"
let scope = Object.create(context);
let h = this.h;
let _12 = scope['props'].to;
let c13 = [], p13 = {key:13,attrs:{href: _12}};
let vn13 = h('a', p13, c13);
const slot14 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot14) {
slot14.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c13, parent: extra.parent || context}));
}
return vn13;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"App\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2};
let vn2 = h('u', p2, c2);
c1.push(vn2);
let _3 = scope['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
let _4 = _5 = _3;
if (!(_3 instanceof Array)) {
_4 = Object.keys(_3);
_5 = Object.values(_3);
}
let _length4 = _4.length;
let _origScope6 = scope;
scope = Object.assign(Object.create(context), scope);
for (let i1 = 0; i1 < _length4; i1++) {
scope.user_first = i1 === 0
scope.user_last = i1 === _length4 - 1
scope.user_index = i1
scope.user = _4[i1]
scope.user_value = _5[i1]
const nodeKey7 = scope['user'].id;
let c8 = [], p8 = {key:nodeKey7};
let vn8 = h('li', p8, c8);
c2.push(vn8);
// Component 'Link'
let k10 = \`__10__\` + nodeKey7;
let w9 = k10 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k10]] : false;
let props9 = {to:'/user/'+scope['user'].id};
if (w9 && w9.__owl__.currentFiber && !w9.__owl__.vnode) {
w9.destroy();
w9 = false;
}
if (w9) {
w9.__updateProps(props9, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w9.__owl__.pvnode;
c8.push(pvnode);
} else {
let componentKey9 = \`Link\`;
let W9 = context.constructor.components[componentKey9] || QWeb.components[componentKey9]|| scope['Link'];
if (!W9) {throw new Error('Cannot find the definition of component \\"' + componentKey9 + '\\"')}
w9 = new W9(parent, props9);
parent.__owl__.cmap[k10] = w9.__owl__.id;
w9.__owl__.slotId = 1;
let fiber = w9.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k10, hook: {remove() {},destroy(vn) {w9.destroy();}}});
c8.push(pvnode);
w9.__owl__.pvnode = pvnode;
}
w9.__owl__.parentLastFiberId = extra.fiber.id;
}
scope = _origScope6;
return vn1;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 2 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let scope = Object.create(context);
let h = this.h;
let c8 = extra.parentNode;
c8.push({text: \`User \`});
let _11 = scope['user'].name;
if (_11 != null) {
c8.push({text: _11});
}
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 3 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"Link\\"
let scope = Object.create(context);
let h = this.h;
let _11 = scope['props'].to;
let c12 = [], p12 = {key:12,attrs:{href: _11}};
let vn12 = h('a', p12, c12);
const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot13) {
slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
}
return vn12;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"App\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2};
let vn2 = h('u', p2, c2);
c1.push(vn2);
let _3 = scope['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
let _4 = _5 = _3;
if (!(_3 instanceof Array)) {
_4 = Object.keys(_3);
_5 = Object.values(_3);
}
let _length4 = _4.length;
let _origScope6 = scope;
scope = Object.assign(Object.create(context), scope);
for (let i1 = 0; i1 < _length4; i1++) {
scope.user_first = i1 === 0
scope.user_last = i1 === _length4 - 1
scope.user_index = i1
scope.user = _4[i1]
scope.user_value = _5[i1]
const nodeKey7 = scope['user'].id;
let c8 = [], p8 = {key:nodeKey7};
let vn8 = h('li', p8, c8);
c2.push(vn8);
scope.userdescr = 'User '+scope['user'].name;
// Component 'Link'
let k10 = \`__10__\` + nodeKey7;
let w9 = k10 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k10]] : false;
let props9 = {to:'/user/'+scope['user'].id};
if (w9 && w9.__owl__.currentFiber && !w9.__owl__.vnode) {
w9.destroy();
w9 = false;
}
if (w9) {
w9.__updateProps(props9, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w9.__owl__.pvnode;
c8.push(pvnode);
} else {
let componentKey9 = \`Link\`;
let W9 = context.constructor.components[componentKey9] || QWeb.components[componentKey9]|| scope['Link'];
if (!W9) {throw new Error('Cannot find the definition of component \\"' + componentKey9 + '\\"')}
w9 = new W9(parent, props9);
parent.__owl__.cmap[k10] = w9.__owl__.id;
w9.__owl__.slotId = 1;
let fiber = w9.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k10, hook: {remove() {},destroy(vn) {w9.destroy();}}});
c8.push(pvnode);
w9.__owl__.pvnode = pvnode;
}
w9.__owl__.parentLastFiberId = extra.fiber.id;
}
scope = _origScope6;
return vn1;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 3 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let scope = Object.create(context);
let h = this.h;
let c8 = extra.parentNode;
if (scope.userdescr != null) {
c8.push({text: scope.userdescr});
}
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 4 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"App\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
scope.userdescr = 'User '+scope['state'].user.name;
// Component 'Link'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {to:'/user/'+scope['state'].user.id};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Link\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Link'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let scope = Object.create(context);
let h = this.h;
let c1 = extra.parentNode;
if (scope.userdescr != null) {
c1.push({text: scope.userdescr});
}
}"
`;
exports[`t-slot directive template can just return a slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let result;
let h = this.h;
const slot6 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot6) {
let children7= []
result = {}
slot6.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: children7, parent: extra.parent || context}));
utils.defineProxy(result, children7[0]);
}
return result;
}"
`;
File diff suppressed because it is too large Load Diff
+883
View File
@@ -0,0 +1,883 @@
import { Component, Env } from "../../src/component/component";
import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
env.qweb.addTemplate("Component<any,any>", "<div></div>");
env.qweb.addTemplate(
"Counter",
`<div><t t-esc="state.counter"/><button t-on-click="inc">Inc</button></div>`
);
env.qweb.addTemplate("WidgetA", `<div>Hello<t t-component="b"/></div>`);
env.qweb.addTemplate("WidgetB", `<div>world</div>`);
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
function children(w: Component<any, any>): Component<any, any>[] {
const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]);
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("t-slot directive", () => {
test("can define and call slots", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>
<t t-set="header"><span>header</span></t>
<t t-set="footer"><span>footer</span></t>
</Dialog>
</div>
<div t-name="Dialog">
<div><t t-slot="header"/></div>
<div><t t-slot="footer"/></div>
</div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div><div><span>header</span></div><div><span>footer</span></div></div></div>"
);
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Dialog.fn.toString()).toMatchSnapshot();
expect(QWeb.slots["1_header"].toString()).toMatchSnapshot();
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("named slots can define a default content", async () => {
class Dialog extends Component<any, any> {
static template = xml`
<span>
<t t-slot="header">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Dialog/></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>default content</span></div>");
expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot();
});
test("dafault slots can define a default content", async () => {
class Dialog extends Component<any, any> {
static template = xml`
<span>
<t t-slot="default">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Dialog/></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>default content</span></div>");
expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot();
});
test("default content is not rendered if slot is provided", async () => {
class Dialog extends Component<any, any> {
static template = xml`
<span>
<t t-slot="default">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Dialog>hey</Dialog></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
});
test("default content is not rendered if named slot is provided", async () => {
class Dialog extends Component<any, any> {
static template = xml`
<span>
<t t-slot="header">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Dialog><t t-set="header">hey</t></Dialog></div>`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
});
test("slots are rendered with proper context", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<span class="counter"><t t-esc="state.val"/></span>
<Dialog>
<t t-set="footer"><button t-on-click="doSomething">do something</button></t>
</Dialog>
</div>
<span t-name="Dialog"><t t-slot="footer"/></span>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
state = useState({ val: 0 });
doSomething() {
this.state.val++;
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><span class="counter">0</span><span><button>do something</button></span></div>'
);
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><span class="counter">1</span><span><button>do something</button></span></div>'
);
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("slots are rendered with proper context, part 2", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<u><li t-foreach="state.users" t-as="user" t-key="user.id">
<Link to="'/user/' + user.id">User <t t-esc="user.name"/></Link>
</li></u>
</div>
</templates>
`);
class Link extends Component<any, any> {}
class App extends Component<any, any> {
state = useState({
users: [
{ id: 1, name: "Aaron" },
{ id: 2, name: "David" }
]
});
static components = { Link };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User David</a></li></u></div>'
);
expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.users[1].name = "Mathieu";
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User Mathieu</a></li></u></div>'
);
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("slots are rendered with proper context, part 3", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<u><li t-foreach="state.users" t-as="user" t-key="user.id" >
<t t-set="userdescr" t-value="'User ' + user.name"/>
<Link to="'/user/' + user.id"><t t-esc="userdescr"/></Link>
</li></u>
</div>
</templates>
`);
class Link extends Component<any, any> {}
class App extends Component<any, any> {
state = useState({
users: [
{ id: 1, name: "Aaron" },
{ id: 2, name: "David" }
]
});
static components = { Link };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User David</a></li></u></div>'
);
expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.users[1].name = "Mathieu";
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User Mathieu</a></li></u></div>'
);
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("slots are rendered with proper context, part 4", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<t t-set="userdescr" t-value="'User ' + state.user.name"/>
<Link to="'/user/' + state.user.id"><t t-esc="userdescr"/></Link>
</div>
</templates>
`);
class Link extends Component<any, any> {}
class App extends Component<any, any> {
state = useState({ user: { id: 1, name: "Aaron" } });
static components = { Link };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe('<div><a href="/user/1">User Aaron</a></div>');
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.user.name = "David";
await nextTick();
expect(fixture.innerHTML).toBe('<div><a href="/user/1">User David</a></div>');
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("refs are properly bound in slots", async () => {
class Dialog extends Component<any, any> {
static template = xml`<span><t t-slot="footer"/></span>`;
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<span class="counter"><t t-esc="state.val"/></span>
<Dialog>
<t t-set="footer"><button t-ref="myButton" t-on-click="doSomething">do something</button></t>
</Dialog>
</div>
`;
static components = { Dialog };
state = useState({ val: 0 });
button = useRef("myButton");
doSomething() {
this.state.val++;
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><span class="counter">0</span><span><button>do something</button></span></div>'
);
parent.button.el!.click();
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><span class="counter">1</span><span><button>do something</button></span></div>'
);
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("content is the default slot", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>
<span>sts rocks</span>
</Dialog>
</div>
<div t-name="Dialog"><t t-slot="default"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>sts rocks</span></div></div>");
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("default slot work with text nodes", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>sts rocks</Dialog>
</div>
<div t-name="Dialog"><t t-slot="default"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>sts rocks</div></div>");
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("multiple roots are allowed in a named slot", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>
<t t-set="content">
<span>sts</span>
<span>rocks</span>
</t>
</Dialog>
</div>
<div t-name="Dialog"><t t-slot="content"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>sts</span><span>rocks</span></div></div>");
expect(QWeb.slots["1_content"].toString()).toMatchSnapshot();
});
test("multiple roots are allowed in a default slot", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog>
<span>sts</span>
<span>rocks</span>
</Dialog>
</div>
<div t-name="Dialog"><t t-slot="default"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>sts</span><span>rocks</span></div></div>");
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("missing slots are ignored", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog/>
</div>
<span t-name="Dialog">
<t t-slot="default"/>
<span>some content</span>
<t t-slot="footer"/>
</span>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span><span>some content</span></span></div>");
});
test("t-debug on a t-set (defining a slot)", async () => {
const consoleLog = console.log;
console.log = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Dialog><t t-set="content" t-debug="">abc</t></Dialog>
</div>
<span t-name="Dialog">
<t t-slot="content"/>
</span>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(console.log).toHaveBeenCalledTimes(0);
console.log = consoleLog;
});
test("slot preserves properly parented relationship", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Child>
<GrandChild/>
</Child>
</div>
<div t-name="Child"><t t-slot="default"/></div>
<div t-name="GrandChild">Grand Child</div>
</templates>
`);
class Child extends Component<any, any> {}
class GrandChild extends Component<any, any> {}
class Parent extends Component<any, any> {
static components = { Child, GrandChild };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div>Grand Child</div></div></div>");
const parentChildren = children(parent);
expect(parentChildren.length).toBe(1);
expect(parentChildren[0]).toBeInstanceOf(Child);
const childrenChildren = children(parentChildren[0]);
expect(childrenChildren.length).toBe(1);
expect(childrenChildren[0]).toBeInstanceOf(GrandChild);
});
test("nested slots: evaluation context and parented relationship", async () => {
let slot;
class Slot extends Component<any, any> {
static template = xml`<span t-esc="props.val"/>`;
constructor(parent, props) {
super(parent, props);
slot = this;
}
}
class GrandChild extends Component<any, any> {
static template = xml`<div><t t-slot="default"/></div>`;
}
class Child extends Component<any, any> {
static components = { GrandChild };
static template = xml`
<GrandChild>
<t t-slot="default"/>
</GrandChild>`;
}
class Parent extends Component<any, any> {
static components = { Child, Slot };
static template = xml`<Child><Slot val="state.val"/></Child>`;
state = useState({ val: 3 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
expect(slot.__owl__.parent).toBeInstanceOf(GrandChild);
});
test("slot are properly rendered if inner props are changed", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
SC:<t t-esc="props.val"/>
</div>
<div t-name="GenericComponent">
<t t-slot="default" />
</div>
<div t-name="App">
<button t-on-click="inc">Inc[<t t-esc="state.val"/>]</button>
<GenericComponent>
<SomeComponent val="state.val"/>
</GenericComponent>
</div>
</templates>
`);
class SomeComponent extends Component<any, any> {}
class GenericComponent extends Component<any, any> {}
class App extends Component<any, any> {
static components = { GenericComponent, SomeComponent };
state = useState({ val: 4 });
inc() {
this.state.val++;
}
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>Inc[4]</button><div><div> SC:4</div></div></div>");
(<any>fixture.querySelector("button")).click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>Inc[5]</button><div><div> SC:5</div></div></div>");
});
test("slots and wrapper components", async () => {
class Link extends Component<any, any> {
static template = xml`
<a href="abc">
<t t-slot="default"/>
</a>`;
}
class A extends Component<any, any> {
static template = xml`<Link>hey</Link>`;
static components = { Link: Link };
}
const a = new A();
await a.mount(fixture);
expect(fixture.innerHTML).toBe(`<a href="abc">hey</a>`);
});
test("template can just return a slot", async () => {
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.value"/></span>`;
}
class SlotComponent extends Component<any, any> {
static template = xml`<t t-slot="default"/>`;
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<SlotComponent><Child value="state.value"/></SlotComponent>
</div>`;
static components = { SlotComponent, Child };
state = useState({ value: 3 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
expect(QWeb.TEMPLATES[SlotComponent.template].fn.toString()).toMatchSnapshot();
parent.state.value = 5;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>5</span></div>");
});
test("multiple slots containing components", async () => {
class C extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class B extends Component<any, any> {
static template = xml`<div><t t-slot="s1"/><t t-slot="s2"/></div>`;
}
class A extends Component<any, any> {
static template = xml`
<B>
<t t-set="s1"><C val="1"/></t>
<t t-set="s2"><C val="2"/></t>
</B>`;
static components = { B, C };
}
const a = new A();
await a.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><span>1</span><span>2</span></div>`);
});
test("slots in t-foreach and re-rendering", async () => {
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="state.val"/><t t-slot="default"/></span>`;
state = useState({ val: "A" });
mounted() {
this.state.val = "B";
}
}
class Parent extends Component<any, any> {
static components = { Child };
static template = xml`
<div>
<t t-foreach="Array(2)" t-as="n" t-key="n_index">
<Child><t t-esc="n_index"/></Child>
</t>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>A0</span><span>A1</span></div>");
await nextTick(); // wait for the changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("<div><span>B0</span><span>B1</span></div>");
});
test("slots in t-foreach with t-set and re-rendering", async () => {
class Child extends Component<any, any> {
static template = xml`
<span>
<t t-esc="state.val"/>
<t t-slot="default"/>
</span>`;
state = useState({ val: "A" });
mounted() {
this.state.val = "B";
}
}
class ParentWidget extends Component<any, any> {
static components = { Child };
static template = xml`
<div>
<t t-foreach="Array(2)" t-as="n" t-key="n_index">
<t t-set="dummy" t-value="n_index"/>
<Child><t t-esc="dummy"/></Child>
</t>
</div>`;
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>A0</span><span>A1</span></div>");
await nextTick(); // wait for changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("<div><span>B0</span><span>B1</span></div>");
});
test("nested slots in same template", async () => {
let child, child2, child3;
class Child extends Component<any, any> {
static template = xml`
<span id="c1">
<div>
<t t-slot="default"/>
</div>
</span>`;
constructor(parent, props) {
super(parent, props);
child = this;
}
}
class Child2 extends Component<any, any> {
static template = xml`
<span id="c2">
<t t-slot="default"/>
</span>`;
constructor(parent, props) {
super(parent, props);
child2 = this;
}
}
class Child3 extends Component<any, any> {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
super(parent, props);
child3 = this;
}
}
class Parent extends Component<any, any> {
static components = { Child, Child2, Child3 };
static template = xml`
<span id="parent">
<Child>
<Child2>
<Child3/>
</Child2>
</Child>
</span>`;
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
'<span id="parent"><span id="c1"><div><span id="c2"><span>Child 3</span></span></div></span></span>'
);
expect(child3.__owl__.parent).toStrictEqual(child2);
expect(child2.__owl__.parent).toStrictEqual(child);
expect(child.__owl__.parent).toStrictEqual(widget);
});
test("t-slot nested within another slot", async () => {
let portal, modal, child3;
class Child3 extends Component<any, any> {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
super(parent, props);
child3 = this;
}
}
class Modal extends Component<any, any> {
static template = xml`
<span id="modal">
<t t-slot="default"/>
</span>`;
constructor(parent, props) {
super(parent, props);
modal = this;
}
}
class Portal extends Component<any, any> {
static template = xml`
<span id="portal">
<t t-slot="default"/>
</span>`;
constructor(parent, props) {
super(parent, props);
portal = this;
}
}
class Dialog extends Component<any, any> {
static components = { Modal, Portal };
static template = xml`
<span id="c2">
<Modal>
<Portal>
<t t-slot="default"/>
</Portal>
</Modal>
</span>`;
}
class Parent extends Component<any, any> {
static components = { Child3, Dialog };
static template = xml`
<span id="c1">
<Dialog>
<Child3/>
</Dialog>
</span>`;
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
'<span id="c1"><span id="c2"><span id="modal"><span id="portal"><span>Child 3</span></span></span></span></span>'
);
expect(child3.__owl__.parent).toStrictEqual(portal);
expect(portal.__owl__.parent).toStrictEqual(modal);
});
test("t-slot supports many instances", async () => {
let child3;
class Child3 extends Component<any, any> {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
super(parent, props);
child3 = this;
}
}
class Dialog extends Component<any, any> {
static template = xml`
<span id="c2">
<t t-slot="default"/>
</span>`;
}
class Parent extends Component<any, any> {
static components = { Child3, Dialog };
static template = xml`
<span id="c1">
<Dialog>
<Child3 val="state.lol"/>
</Dialog>
</span>`;
state = { lol: "k" };
}
const widget = new Parent();
await widget.mount(fixture);
expect(child3.props.val).toBe("k");
const widget_1 = new Parent();
widget_1.state.lol = "m";
await widget_1.mount(fixture);
expect(child3.props.val).toBe("m");
});
test("slots in slots, with vars", async () => {
class B extends Component<any, any> {
static template = xml`<span><t t-slot="default"/></span>`;
}
class A extends Component<any, any> {
static template = xml`
<div>
<B>
<t t-slot="default"/>
</B>
</div>`;
static components = { B };
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<t t-set="test" t-value="state.name"/>
<A>
<p>hey<t t-esc="test"/></p>
</A>
</div>`;
static components = { A };
state = { name: "aaron" };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span><p>heyaaron</p></span></div></div>");
});
});
+10 -6
View File
@@ -1,4 +1,4 @@
import { Env } from "../src/component/component"; import { Env, Component } from "../src/component/component";
import { scheduler } from "../src/component/scheduler"; import { scheduler } from "../src/component/scheduler";
import { EvalContext, QWeb } from "../src/qweb/qweb"; import { EvalContext, QWeb } from "../src/qweb/qweb";
import { CompilationContext } from "../src/qweb/compilation_context"; import { CompilationContext } from "../src/qweb/compilation_context";
@@ -25,6 +25,7 @@ beforeEach(() => {
slots = Object.assign({}, QWeb.slots); slots = Object.assign({}, QWeb.slots);
nextId = QWeb.nextId; nextId = QWeb.nextId;
TEMPLATES = Object.assign({}, QWeb.TEMPLATES); TEMPLATES = Object.assign({}, QWeb.TEMPLATES);
Component.scheduler.tasks = [];
}); });
afterEach(() => { afterEach(() => {
@@ -32,6 +33,7 @@ afterEach(() => {
QWeb.slots = slots; QWeb.slots = slots;
QWeb.nextId = nextId; QWeb.nextId = nextId;
QWeb.TEMPLATES = TEMPLATES; QWeb.TEMPLATES = TEMPLATES;
Component.scheduler.tasks = [];
}); });
// helpers // helpers
@@ -87,6 +89,11 @@ export function renderToDOM(
context: EvalContext = {}, context: EvalContext = {},
extra?: any extra?: any
): HTMLElement | Text { ): 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); const vnode = qweb.render(template, context, extra);
// we snapshot here the compiled code. This is useful to prevent unwanted code // we snapshot here the compiled code. This is useful to prevent unwanted code
@@ -116,11 +123,8 @@ export function renderToString(
context: EvalContext = {}, context: EvalContext = {},
extra?: any extra?: any
): string { ): string {
const node = renderToDOM(qweb, t, context, extra); const result = qweb.renderToString(t, context, extra);
const result = node instanceof Text ? node.textContent! : node.outerHTML; expect(qweb.templates[t].fn.toString()).toMatchSnapshot();
if (result !== qweb.renderToString(t, context, extra)) {
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
}
return result; return result;
} }
+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
+294 -11
View File
@@ -46,6 +46,16 @@ describe("static templates", () => {
expect(renderToString(qweb, "test")).toBe("<div>word</div>"); 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", () => { test("div with a span child node", () => {
qweb.addTemplate("test", "<div><span>word</span></div>"); qweb.addTemplate("test", "<div><span>word</span></div>");
expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>"); expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>");
@@ -110,9 +120,11 @@ describe("t-esc", () => {
expect(renderToString(qweb, "test", { var: "ok" })).toBe("<span>ok</span>"); expect(renderToString(qweb, "test", { var: "ok" })).toBe("<span>ok</span>");
}); });
test.skip("escaping", () => { test("escaping", () => {
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`); qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<ok>" })).toBe("<span>&lt;ok&gt;</span>"); expect(renderToString(qweb, "test", { var: "<ok>abc</ok>" })).toBe(
"<span>&amp;lt;ok&amp;gt;abc&amp;lt;/ok&amp;gt;</span>"
);
}); });
test("escaping on a node", () => { test("escaping on a node", () => {
@@ -129,6 +141,49 @@ describe("t-esc", () => {
qweb.addTemplate("test", `<span t-esc="var">nope</span>`); qweb.addTemplate("test", `<span t-esc="var">nope</span>`);
expect(renderToString(qweb, "test")).toBe("<span>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>");
});
test("div with falsy values", () => {
qweb.addTemplate(
"test",
`
<div>
<p t-esc="v1"/>
<p t-esc="v2"/>
<p t-esc="v3"/>
<p t-esc="v4"/>
<p t-esc="v5"/>
</div>`
);
const vals = {
v1: false,
v2: undefined,
v3: null,
v4: 0,
v5: ""
};
expect(renderToString(qweb, "test", vals)).toBe(
"<div><p>false</p><p></p><p></p><p>0</p><p></p></div>"
);
});
test("t-esc work with spread operator", () => {
qweb.addTemplate("test", `<span><t t-esc="[...state.list]"/></span>`);
const result = renderToString(qweb, "test", { state: { list: [1, 2] } });
expect(result).toBe("<span>1,2</span>");
});
}); });
describe("t-raw", () => { describe("t-raw", () => {
@@ -235,6 +290,18 @@ describe("t-set", () => {
); );
}); });
test("t-set with content and sub t-esc", () => {
qweb.addTemplate(
"test",
`<div>
<t t-set="setvar"><t t-esc="beep"/> boop</t>
<t t-esc="setvar"/>
</div>`
);
expect(renderToString(qweb, "test", { beep: "beep" })).toBe("<div>beep boop</div>");
});
test("evaluate value expression, part 2", () => { test("evaluate value expression, part 2", () => {
qweb.addTemplate( qweb.addTemplate(
"test", "test",
@@ -268,6 +335,58 @@ describe("t-set", () => {
expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>"); expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>");
expect(renderToString(qweb, "test", { flag: false })).toBe("<div>0</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", () => { describe("t-if", () => {
@@ -355,7 +474,7 @@ describe("t-if", () => {
}); });
test("t-esc with t-elif", () => { test("t-esc with t-elif", () => {
qweb.addTemplate("test", `<div><t t-if="false">abc</t><t t-else="1" t-esc="'x'"/></div>`); qweb.addTemplate("test", `<div><t t-if="false">abc</t><t t-else="" t-esc="'x'"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>x</div>"); expect(renderToString(qweb, "test")).toBe("<div>x</div>");
}); });
@@ -432,6 +551,18 @@ describe("attributes", () => {
expect(result).toBe(`<div foo="bar"></div>`); 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", () => { test("dynamic attribute with a dash", () => {
qweb.addTemplate("test", `<div t-att-data-action-id="id"/>`); qweb.addTemplate("test", `<div t-att-data-action-id="id"/>`);
const result = renderToString(qweb, "test", { id: 32 }); const result = renderToString(qweb, "test", { id: 32 });
@@ -535,7 +666,7 @@ describe("attributes", () => {
expect(result).toBe(`<div foo="a 0 is 1 of 2 ]"></div>`); expect(result).toBe(`<div foo="a 0 is 1 of 2 ]"></div>`);
}); });
test.skip("various escapes", () => { test("various escapes", () => {
// not needed?? // not needed??
qweb.addTemplate( qweb.addTemplate(
"test", "test",
@@ -551,7 +682,7 @@ describe("attributes", () => {
baz: 1, baz: 1,
qux: { qux: "<>" } qux: { qux: "<>" }
}); });
const expected = `<div foo="&lt;foo" bar="&lt;bar&gt;" baz="&lt;&quot;&lt;baz&gt;&quot;&gt;" qux="&lt;&gt;"></div>`; const expected = '<div foo="<foo" bar="0" baz="<1>" qux="<>"></div>';
expect(result).toBe(expected); expect(result).toBe(expected);
}); });
@@ -576,10 +707,19 @@ describe("attributes", () => {
describe("t-call (template calling", () => { describe("t-call (template calling", () => {
test("basic caller", () => { 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("_basic-callee", "<div>ok</div>");
qweb.addTemplate("caller", '<t t-call="_basic-callee"/>'); qweb.addTemplate("caller", '<t t-call="_basic-callee"/>');
const expected = "<div>ok</div>"; const expected = "<div>ok</div>";
expect(renderToString(qweb, "caller")).toBe(expected); expect(renderToString(qweb, "caller")).toBe(expected);
expect(qweb.subTemplates["_basic-callee"].toString()).toMatchSnapshot();
}); });
test("t-call with t-if", () => { test("t-call with t-if", () => {
@@ -587,6 +727,7 @@ describe("t-call (template calling", () => {
qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>'); qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>');
const expected = "<div><span>ok</span></div>"; const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller", { flag: true })).toBe(expected); expect(renderToString(qweb, "caller", { flag: true })).toBe(expected);
expect(qweb.subTemplates["sub"].toString()).toMatchSnapshot();
}); });
test("t-call not allowed on a non t node", () => { test("t-call not allowed on a non t node", () => {
@@ -671,6 +812,7 @@ describe("t-call (template calling", () => {
`); `);
const expected = "<div><div><span>hey</span> <span>yay</span></div></div>"; const expected = "<div><div><span>hey</span> <span>yay</span></div></div>";
expect(renderToString(qweb, "main")).toBe(expected); expect(renderToString(qweb, "main")).toBe(expected);
expect(qweb.subTemplates["SubTemplate"].toString()).toMatchSnapshot();
}); });
test("cascading t-call t-raw='0'", () => { test("cascading t-call t-raw='0'", () => {
@@ -720,7 +862,7 @@ describe("t-call (template calling", () => {
`); `);
const expected = "<div><span>hey</span></div>"; const expected = "<div><span>hey</span></div>";
expect(renderToString(qweb, "recursive")).toBe(expected); 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(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -748,7 +890,7 @@ describe("t-call (template calling", () => {
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe( expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
expected expected
); );
const recursiveFn = Object.values(qweb.recursiveFns)[0] as any; const recursiveFn = Object.values(qweb.subTemplates)[0] as any;
expect(recursiveFn.toString()).toMatchSnapshot(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -775,7 +917,7 @@ describe("t-call (template calling", () => {
const expected = const expected =
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>"; "<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); 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(); expect(recursiveFn.toString()).toMatchSnapshot();
}); });
@@ -785,6 +927,68 @@ describe("t-call (template calling", () => {
const expected = "<div><span>desk</span></div>"; const expected = "<div><span>desk</span></div>";
expect(trim(renderToString(qweb, "abcd"))).toBe(expected); expect(trim(renderToString(qweb, "abcd"))).toBe(expected);
}); });
test("t-call, conditional and t-set in t-call body", () => {
QWeb.registerTemplate("callee1", "<div>callee1</div>");
QWeb.registerTemplate("callee2", '<div>callee2 <t t-esc="v"/></div>');
QWeb.registerTemplate(
"caller",
`<div>
<t t-set="v1" t-value="'elif'"/>
<t t-if="v1 === 'if'" t-call="callee1" />
<t t-elif="v1 === 'elif'" t-call="callee2" >
<t t-set="v" t-value="'success'" />
</t>
</div>`
);
const rendered = renderToString(qweb, "caller");
expect(rendered).toBe(`<div><div>callee2 success</div></div>`);
});
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", () => { describe("foreach", () => {
@@ -853,9 +1057,9 @@ describe("foreach", () => {
<t t-foreach="[1]" t-as="item"><t t-esc="item"/></t> <t t-foreach="[1]" t-as="item"><t t-esc="item"/></t>
</div>` </div>`
); );
const context = {}; const context = { __owl__: {} };
renderToString(qweb, "test", context); renderToString(qweb, "test", context);
expect(Object.keys(context).length).toBe(0); expect(Object.keys(context)).toEqual(["__owl__"]);
}); });
test("t-foreach in t-forach", () => { test("t-foreach in t-forach", () => {
@@ -1133,6 +1337,69 @@ describe("t-on", () => {
expect(owner.state.counter).toBe(2); expect(owner.state.counter).toBe(2);
}); });
test("t-on with inline statement, part 2", () => {
qweb.addTemplate("test", `<button t-on-click="state.flag = !state.flag">Toggle</button>`);
let owner = {
state: {
flag: true
}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.flag).toBe(true);
(<HTMLElement>node).click();
expect(owner.state.flag).toBe(false);
(<HTMLElement>node).click();
expect(owner.state.flag).toBe(true);
});
test("t-on with inline statement, part 3", () => {
qweb.addTemplate("test", `<button t-on-click="state.n = someFunction(3)">Toggle</button>`);
let owner = {
someFunction(n) {
return n + 1;
},
state: {
n: 11
}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.n).toBe(11);
(<HTMLElement>node).click();
expect(owner.state.n).toBe(4);
});
test("t-on with t-call", async () => {
expect.assertions(2);
qweb.addTemplate("sub", `<p t-on-click="update">lucas</p>`);
qweb.addTemplate("main", `<div><t t-call="sub"/></div>`);
let owner = {
update() {
expect(this).toBe(owner);
}
};
const node = renderToDOM(qweb, "main", owner, { handlers: [] });
(<HTMLElement>node).querySelector("p")!.click();
});
test("t-on, with arguments and t-call", async () => {
expect.assertions(3);
qweb.addTemplate("sub", `<p t-on-click="update(value)">lucas</p>`);
qweb.addTemplate("main", `<div><t t-call="sub"/></div>`);
let owner = {
update(val) {
expect(this).toBe(owner);
expect(val).toBe(444);
},
value: 444
};
const node = renderToDOM(qweb, "main", owner, { handlers: [] });
(<HTMLElement>node).querySelector("p")!.click();
});
test("t-on with prevent and/or stop modifiers", async () => { test("t-on with prevent and/or stop modifiers", async () => {
expect.assertions(7); expect.assertions(7);
qweb.addTemplate( qweb.addTemplate(
@@ -1505,7 +1772,7 @@ describe("debugging", () => {
console.log = jest.fn(); console.log = jest.fn();
qweb.addTemplate( qweb.addTemplate(
"test", "test",
`<div t-debug="1"><t t-if="true"><span t-debug="1">hey</span></t></div>` `<div t-debug=""><t t-if="true"><span t-debug="">hey</span></t></div>`
); );
qweb.render("test"); qweb.render("test");
expect(qweb.templates.test.fn.toString()).toMatchSnapshot(); expect(qweb.templates.test.fn.toString()).toMatchSnapshot();
@@ -1514,6 +1781,22 @@ describe("debugging", () => {
console.log = consoleLog; 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="">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", () => { test("t-log", () => {
const consoleLog = console.log; const consoleLog = console.log;
console.log = jest.fn(); console.log = jest.fn();
+55 -29
View File
@@ -50,6 +50,12 @@ describe("tokenizer", () => {
{ type: "OPERATOR", value: "typeof " }, { type: "OPERATOR", value: "typeof " },
{ type: "SYMBOL", value: "a" } { type: "SYMBOL", value: "a" }
]); ]);
expect(tokenize("a...1")).toEqual([
{ type: "SYMBOL", value: "a" },
{ type: "OPERATOR", value: "..." },
{ type: "VALUE", value: "1" }
]);
}); });
test("strings", () => { test("strings", () => {
@@ -92,7 +98,7 @@ describe("expression evaluation", () => {
test("parenthesis", () => { test("parenthesis", () => {
expect(compileExpr("(1)", {})).toBe("(1)"); 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", () => { test("objects and sub objects", () => {
@@ -100,8 +106,8 @@ describe("expression evaluation", () => {
}); });
test("replacing variables", () => { test("replacing variables", () => {
expect(compileExpr("a", {})).toBe("context['a']"); expect(compileExpr("a", {})).toBe("scope['a']");
expect(compileExpr("a", { a: { id: "_3", expr: "" } })).toBe("_3"); expect(compileExpr("a", { a: { id: "_3", expr: "scope._3" } })).toBe("scope._3");
}); });
test("arrays and objects", () => { test("arrays and objects", () => {
@@ -111,56 +117,76 @@ describe("expression evaluation", () => {
}); });
test("dot operator", () => { test("dot operator", () => {
expect(compileExpr("a.b", {})).toBe("context['a'].b"); expect(compileExpr("a.b", {})).toBe("scope['a'].b");
expect(compileExpr("a.b.c", {})).toBe("context['a'].b.c"); expect(compileExpr("a.b.c", {})).toBe("scope['a'].b.c");
}); });
test("various unary operators", () => { test("various unary operators", () => {
expect(compileExpr("!flag", {})).toBe("!context['flag']"); expect(compileExpr("!flag", {})).toBe("!scope['flag']");
expect(compileExpr("-3", {})).toBe("-3"); expect(compileExpr("-3", {})).toBe("-3");
expect(compileExpr("-a", {})).toBe("-context['a']"); expect(compileExpr("-a", {})).toBe("-scope['a']");
expect(compileExpr("typeof a", {})).toBe("typeof context['a']"); expect(compileExpr("typeof a", {})).toBe("typeof scope['a']");
}); });
test("various binary operators", () => { test("various binary operators", () => {
expect(compileExpr("color == 'black'", {})).toBe("context['color']=='black'"); expect(compileExpr("color == 'black'", {})).toBe("scope['color']=='black'");
expect(compileExpr("a || b", {})).toBe("context['a']||context['b']"); expect(compileExpr("a || b", {})).toBe("scope['a']||scope['b']");
expect(compileExpr("color === 'black'", {})).toBe("context['color']==='black'"); expect(compileExpr("color === 'black'", {})).toBe("scope['color']==='black'");
expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']"); expect(compileExpr("'li_'+item", {})).toBe("'li_'+scope['item']");
expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1"); expect(compileExpr("state.val > 1", {})).toBe("scope['state'].val>1");
}); });
test("boolean operations", () => { test("boolean operations", () => {
expect(compileExpr("a && b", {})).toBe("context['a']&&context['b']"); expect(compileExpr("a && b", {})).toBe("scope['a']&&scope['b']");
}); });
test("ternary operators", () => { test("ternary operators", () => {
expect(compileExpr("a ? b: '2'", {})).toBe("context['a']?context['b']:'2'"); expect(compileExpr("a ? b: '2'", {})).toBe("scope['a']?scope['b']:'2'");
expect(compileExpr("a ? b: (c or '2') ", {})).toBe( expect(compileExpr("a ? b: (c or '2') ", {})).toBe("scope['a']?scope['b']:(scope['c']||'2')");
"context['a']?context['b']:(context['c']||'2')"
);
expect(compileExpr("a ? {test:c}: [1,u]", {})).toBe( 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", () => { test("word replacement", () => {
expect(compileExpr("a or b", {})).toBe("context['a']||context['b']"); expect(compileExpr("a or b", {})).toBe("scope['a']||scope['b']");
expect(compileExpr("a and b", {})).toBe("context['a']&&context['b']"); expect(compileExpr("a and b", {})).toBe("scope['a']&&scope['b']");
}); });
test("function calls", () => { test("function calls", () => {
expect(compileExpr("a()", {})).toBe("context['a']()"); expect(compileExpr("a()", {})).toBe("scope['a']()");
expect(compileExpr("a(1)", {})).toBe("context['a'](1)"); expect(compileExpr("a(1)", {})).toBe("scope['a'](1)");
expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)"); expect(compileExpr("a(1,2)", {})).toBe("scope['a'](1,2)");
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("context['a'](1,2,{a:[context['a']]})"); 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()", {})).toBe("'x'.toUpperCase()");
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe("'x'.toUpperCase({a:3})"); expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe("'x'.toUpperCase({a:3})");
expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })).toBe( expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "scope._v5" } })).toBe(
"'x'.toUpperCase(_v5)" "'x'.toUpperCase(scope._v5)"
); );
expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })).toBe( expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "scope._v5" } })).toBe(
"'x'.toUpperCase({b:_v5})" "'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)"
);
});
test("assignation", () => {
expect(compileExpr("a = b", {})).toBe("scope['a']=scope['b']");
expect(compileExpr("a += b", {})).toBe("scope['a']+=scope['b']");
expect(compileExpr("a -= b", {})).toBe("scope['a']-=scope['b']");
expect(compileExpr("a.b = !a.b", {})).toBe("scope['a'].b=!scope['a'].b");
});
test("spread operator", () => {
expect(compileExpr("[...state.list]", {})).toBe("[...scope['state'].list]");
expect(compileExpr("f(...state.list)", {})).toBe("scope['f'](...scope['state'].list)");
expect(compileExpr("f([...list])", {})).toBe("scope['f']([...scope['list']])");
});
}); });
+14 -13
View File
@@ -1,21 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Link component can render simple cases 1`] = ` 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 utils = this.constructor.utils;
let owner = context; let scope = Object.create(context);
var h = this.h; let h = this.h;
let _6 = utils.toObj({'router-link-active':context['isActive']}); let _4 = utils.toObj({'router-link-active':scope['isActive']});
var _7 = context['href']; let _5 = scope['href'];
let c8 = [], p8 = {key:8,attrs:{href: _7},class:_6,on:{}}; let c6 = [], p6 = {key:6,attrs:{href: _5},class:_4,on:{}};
var vn8 = h('a', p8, c8); let vn6 = h('a', p6, c6);
extra.handlers['click' + 8] = extra.handlers['click' + 8] || function (e) {const fn = context['navigate'];if (fn) { fn.call(owner, e); } else { context.navigate; }}; extra.handlers['click__7__'] = extra.handlers['click__7__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['navigate'](e);};
p8.on['click'] = extra.handlers['click' + 8]; p6.on['click'] = extra.handlers['click__7__'];
const slot9 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot9) { if (slot8) {
slot9.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c8, vars: extra.vars, parent: extra.parent || owner})); slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c6, parent: extra.parent || context}));
} }
return vn8; return vn6;
}" }"
`; `;
@@ -1,44 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RouteComponent can render simple cases 1`] = ` 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 utils = this.constructor.utils;
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let owner = context; let scope = Object.create(context);
let result; let result;
var h = this.h; let h = this.h;
if (context['routeComponent']) { if (scope['routeComponent']) {
const nodeKey6 = context['env'].router.currentRouteName; const nodeKey4 = scope['env'].router.currentRouteName;
//COMPONENT // Component 'routeComponent'
let k9 = \`__10__\` + nodeKey6; let k6 = \`__6__\` + nodeKey4;
let w8 = k9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k9]] : false; let w5 = k6 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k6]] : false;
let vn11 = {}; let vn7 = {};
result = vn11; result = vn7;
let props8 = Object.assign({}, context['env'].router.currentParams); let props5 = Object.assign({}, scope['env'].router.currentParams);
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) { if (w5 && w5.__owl__.currentFiber && !w5.__owl__.vnode) {
w8.destroy(); w5.destroy();
w8 = false; w5 = false;
} }
if (w8) { if (w5) {
w8.__updateProps(props8, extra.fiber, undefined, undefined); w5.__updateProps(props5, extra.fiber, undefined);
let pvnode = w8.__owl__.pvnode; let pvnode = w5.__owl__.pvnode;
utils.defineProxy(vn11, pvnode); utils.defineProxy(vn7, pvnode);
} else { } else {
let componentKey8 = \`routeComponent\`; let componentKey5 = \`routeComponent\`;
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| context['routeComponent']; let W5 = context.constructor.components[componentKey5] || QWeb.components[componentKey5]|| scope['routeComponent'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')} if (!W5) {throw new Error('Cannot find the definition of component \\"' + componentKey5 + '\\"')}
w8 = new W8(parent, props8); w5 = new W5(parent, props5);
parent.__owl__.cmap[k9] = w8.__owl__.id; parent.__owl__.cmap[k6] = w5.__owl__.id;
let def7 = w8.__prepare(extra.fiber, undefined, undefined); let fiber = w5.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}}); let pvnode = h('dummy', {key: k6, hook: {remove() {},destroy(vn) {w5.destroy();}}});
const fiber = w8.__owl__.currentFiber; utils.defineProxy(vn7, pvnode);
def7.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); w5.__owl__.pvnode = pvnode;
utils.defineProxy(vn11, pvnode);
w8.__owl__.pvnode = pvnode;
} }
w8.__owl__.parentLastFiberId = extra.fiber.id; w5.__owl__.parentLastFiberId = extra.fiber.id;
} }
return result; return result;
}" }"
+32
View File
@@ -84,6 +84,38 @@ describe("connecting a component to store", () => {
expect(fixture.innerHTML).toBe("<div><span>ok</span></div>"); expect(fixture.innerHTML).toBe("<div><span>ok</span></div>");
}); });
test("map works on the result of useStore when the resulting array changes for a bigger one", async () => {
const state = { smallerArray: [1], biggerArray: [2, 3], useSmallArray: true };
const store = new Store({ state });
class App extends Component<any, any> {
static template = xml`<div t-esc="mapAdd"/>`;
storeProps = {
array: useStore(state => {
if (state.useSmallArray) {
return state.smallerArray;
}
return state.biggerArray;
})
};
get mapAdd() {
return this.storeProps.array.map(a => {
return a + 1;
});
}
}
(<any>env).store = store;
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>2</div>");
store.state.useSmallArray = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div>3,4</div>");
});
test("throw error if no store is found", async () => { test("throw error if no store is found", async () => {
class App extends Component<any, any> { class App extends Component<any, any> {
static template = xml`<div></div>`; static template = xml`<div></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); 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 { export function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean {
if (link.link.startsWith("http")) { if (link.link.startsWith("http")) {
// no check on external links // 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>
-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"; import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Likes Counter Widget // Likes Counter Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class Counter extends owl.Component { class Counter extends owl.Component {
state = { counter: 0 }; state = useState({ counter: 0 });
increment() { increment() {
this.state.counter++; this.state.counter++;
@@ -15,8 +16,6 @@ class Counter extends owl.Component {
// Message Widget // Message Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class Message extends owl.Component { class Message extends owl.Component {
widgets = { Counter };
shouldUpdate(nextProps) { shouldUpdate(nextProps) {
return nextProps.message !== this.props.message; return nextProps.message !== this.props.message;
} }
@@ -26,13 +25,14 @@ class Message extends owl.Component {
}); });
} }
} }
Message.components = { Counter };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Root Widget // Root Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class App extends owl.Component { class App extends owl.Component {
widgets = { Message }; state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
state = { messages: [], multipleFlag: false, clearAfterFlag: false }; logRef = useRef("log");
mounted() { mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`); this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
@@ -108,10 +108,10 @@ class App extends owl.Component {
updateSomeMessages() { updateSomeMessages() {
this.benchmark("update every 10th", () => { this.benchmark("update every 10th", () => {
const messages = this.state.messages; 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]); const msg = Object.assign({}, messages[i]);
msg.author += "!!!"; msg.author += "!!!";
this.set(messages, i, msg); messages[i] = msg;
} }
}); });
} }
@@ -129,32 +129,25 @@ class App extends owl.Component {
div.classList.add("bold"); div.classList.add("bold");
} }
div.textContent = `> ${str}`; div.textContent = `> ${str}`;
this.refs.log.appendChild(div); this.logRef.el.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight; this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
} }
clearLog() { clearLog() {
this.refs.log.innerHTML = ""; this.logRef.el.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
} }
} }
App.components = { Message };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Application initialization // Application initialization
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
async function start() { async function start() {
const templates = await owl.utils.loadTemplates("templates.xml"); const templates = await owl.utils.loadFile("templates.xml");
const env = { App.env = {
qweb: new owl.QWeb(templates) qweb: new owl.QWeb({ templates })
}; };
const app = new App(env); const app = new App();
app.mount(document.body); app.mount(document.body);
} }
@@ -2,12 +2,11 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <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"/> <link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script> <script src='owl.js'></script>
</head> </head>
<body> <body>
<div id='main'></div>
<script src='app.js' type="module"></script> <script src='app.js' type="module"></script>
</body> </body>
</html> </html>
File diff suppressed because it is too large Load Diff
@@ -12,11 +12,11 @@
</div> </div>
<div class="flags"> <div class="flags">
<div> <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> <label for="multipleflag">Do it 20x</label>
</div> </div>
<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> <label for="clearFlag">Clear after</label>
</div> </div>
</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"> <div class="benchmarks">
<ul> <ul>
<li><a href="benchmarks/owl-0.7.0">OWL 0.7.0</a></li> <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.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.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.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.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.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.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.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-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> <li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul> </ul>
<ul> <ul>
+114 -3
View File
@@ -1101,7 +1101,7 @@ const RESPONSIVE_XML = `<templates>
<t t-raw="maincontent"/> <t t-raw="maincontent"/>
</div> </div>
</div> </div>
<t t-else="1"> <t t-else="">
<t t-raw="maincontent"/> <t t-raw="maincontent"/>
</t> </t>
</div> </div>
@@ -1438,12 +1438,117 @@ const FORM_XML = `<templates>
<div>Text: <t t-esc="state.text"/></div> <div>Text: <t t-esc="state.text"/></div>
<div>Other Text: <t t-esc="state.othertext"/></div> <div>Other Text: <t t-esc="state.othertext"/></div>
<div>Number: <t t-esc="state.number"/></div> <div>Number: <t t-esc="state.number"/></div>
<div>Boolean: <t t-if="state.bool">True</t><t t-else="1">False</t></div> <div>Boolean: <t t-if="state.bool">True</t><t t-else="">False</t></div>
<div>Color: <t t-esc="state.color"/></div> <div>Color: <t t-esc="state.color"/></div>
</div> </div>
</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 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 // 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, // demonstration. Managing windows can be as complex as we want. For example,
@@ -1770,5 +1875,11 @@ export const SAMPLES = [
code: ASYNC_COMPONENTS, code: ASYNC_COMPONENTS,
xml: ASYNC_COMPONENTS_XML, xml: ASYNC_COMPONENTS_XML,
css: ASYNC_COMPONENTS_CSS css: ASYNC_COMPONENTS_CSS
} },
{
description: "Portal (Dialog)",
code: PORTAL_COMPONENTS,
xml: PORTAL_XML,
css: PORTAL_CSS,
},
]; ];