Compare commits

..

57 Commits

Author SHA1 Message Date
Géry Debongnie ff76747a05 [REL] v1.0.0-alpha4 2019-11-22 13:03:02 +01:00
Aaron Bohy 7e6b1a28a0 [IMP] qweb: throw error if t-component not used on a node t
Before this rev, using t-component on a div (for example) node
would silently ignore the div and replace it by the root node of
the component. A way to improve this was to create an extra div
node and to put the component inside it. However, it raised
questions: what do we do with other attributes set on this tag?
Do we apply them on the div, or on the component? In addition,
some of them only make sense on the component (e.g. props), so we
have to detect them. Whatever we would have decided, it wouldn't
have been obvious from the user point of view, so we chose not to
support it, and we thus now raise an error in this case.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

The Alpha release!

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

QWeb

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

Component

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

Context

- update components concurrently, instead of sequentially

Observer

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

Router

- fix: preserve pathname in hash mode

Config

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

Playground

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

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

Closes #306
2019-10-30 15:09:10 +01:00
Aaron Bohy 9f93da4765 [REF] config: move mode from owl.__info__ to owl.config
and move it to its own file.
2019-10-30 15:09:10 +01:00
Aaron Bohy 9e37b968e8 [FIX] component: error handling: rendering with sub components
Closes #425
2019-10-30 13:27:48 +01:00
Aaron Bohy fa6801b523 [REF] observer: remove revNumber
Closes #257
2019-10-30 11:13:52 +01:00
Géry Debongnie 9edf29a3a1 [IMP] context: group components by depth
closes #399
2019-10-29 16:02:51 +01:00
Géry Debongnie 6a434310ee [FIX] *: run prettier 2019-10-29 16:02:51 +01:00
Géry Debongnie e7967d0779 [FIX] package.json: improve prettier task
to take into account files at more than one level
2019-10-29 16:02:51 +01:00
65 changed files with 8948 additions and 2120 deletions
+6 -22
View File
@@ -28,7 +28,7 @@ Owl is currently mostly stable. Possible future changes are explained in the
Here is a short example to illustrate interactive components: Here is a short example to illustrate interactive components:
```javascript ```javascript
const { Component, QWeb, useState } = owl; const { Component, useState } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
class Counter extends Component { class Counter extends Component {
@@ -50,7 +50,7 @@ class App extends Component {
static components = { Counter }; static components = { Counter };
} }
const app = new App({ qweb: new QWeb() }); const app = new App();
app.mount(document.body); app.mount(document.body);
``` ```
@@ -92,7 +92,8 @@ A complete documentation for Owl can be found here:
The most important sections are: The most important sections are:
- [Quick Start](doc/quick_start.md) - [Tutorial: TodoList application](doc/learning/tutorial_todoapp.md)
- [QWeb templating language](doc/reference/qweb_templating_language.md)
- [Component](doc/reference/component.md) - [Component](doc/reference/component.md)
- [Hooks](doc/reference/hooks.md) - [Hooks](doc/reference/hooks.md)
@@ -103,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-0.24.1.js](https://github.com/odoo/owl/releases/download/v0.24.1/owl.js) - [owl-1.0.0-alpha4.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha3/owl.js)
- [owl-0.24.1.min.js](https://github.com/odoo/owl/releases/download/v0.24.1/owl.min.js) - [owl-1.0.0-alpha4.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha3/owl.min.js)
Some npm scripts are available: Some npm scripts are available:
@@ -127,23 +128,6 @@ Owl components in an application are used to define a (dynamic) tree of componen
C D C D
``` ```
**Environment:** the root component is special: it is created with an environment,
which should contain a [`QWeb` instance](doc/reference/qweb.md). The environment is then automatically
propagated to each sub components (and accessible in the `this.env` property).
```js
const env = { qweb: new QWeb() };
const app = new App(env);
app.mount(document.body);
```
The environment is mostly static. Each application is free to add anything to
the environment, which is very useful, since this can be accessed by each sub
component. Some good use case for that is some configuration keys, session
information or generic services (such as doing rpcs, or accessing local storage).
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
**State:** each component can manage its own local state. It is a simple ES6 **State:** each component can manage its own local state. It is a simple ES6
class, there are no special rules: class, there are no special rules:
+1 -3
View File
@@ -13,7 +13,6 @@ A rendering occurs in two phases:
- virtual rendering: this generates the virtual dom in memory, asynchronously - virtual rendering: this generates the virtual dom in memory, asynchronously
- patch: applies a virtual tree to the screen (synchronously) - patch: applies a virtual tree to the screen (synchronously)
There are several classes involved in a rendering: There are several classes involved in a rendering:
- components - components
@@ -21,9 +20,9 @@ There are several classes involved in a rendering:
- fibers: small objects containing some metadata, associated with a rendering of - fibers: small objects containing some metadata, associated with a rendering of
a specific component a specific component
Components are organized in a dynamic component tree, visible in the user Components are organized in a dynamic component tree, visible in the user
interface. Whenever a rendering is initiated in a component `C`: interface. Whenever a rendering is initiated in a component `C`:
- a fiber is created on `C` with the rendering props information - a fiber is created on `C` with the rendering props information
- the virtual rendering phase starts on C (will asynchronously render all the - the virtual rendering phase starts on C (will asynchronously render all the
child components) child components)
@@ -31,4 +30,3 @@ interface. Whenever a rendering is initiated in a component `C`:
animation frame, if the fiber is done animation frame, if the fiber is done
- once it is done, the scheduler will call the task callback, which will apply - once it is done, the scheduler will call the task callback, which will apply
the patch (if it was not cancelled in the meantime). the patch (if it was not cancelled in the meantime).
+2 -1
View File
@@ -274,7 +274,8 @@ class Counter extends Component {
dispatch = useDispatch(); dispatch = useDispatch();
} }
const counter = new Counter({ store, qweb }); Counter.env.store = store;
const counter = new Counter();
``` ```
## Hooks ## Hooks
@@ -2,8 +2,17 @@
## Static Server ## Static Server
Let us assume that we have a static server running somewhere. We could then Let us assume that we have a static server running somewhere. Let us start by
simply add an html page with a few extra files. adding an html page with a few extra files:
```
my-app/
index.html
app.css
app.js
owl-X.Y.Z.js
templates.xml
```
### HTML and CSS ### HTML and CSS
@@ -88,10 +97,8 @@ class ClickCounter extends owl.Component {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
async function start() { async function start() {
const templates = await owl.utils.loadFile("templates.xml"); const templates = await owl.utils.loadFile("templates.xml");
const env = { ClickCounter.env = { qweb: new owl.QWeb({ templates }) };
qweb: new owl.QWeb({ templates }) const counter = new ClickCounter();
};
const counter = new ClickCounter(env);
const target = document.getElementById("main"); const target = document.getElementById("main");
await counter.mount(target); await counter.mount(target);
} }
File diff suppressed because it is too large Load Diff
+45 -53
View File
@@ -1,63 +1,19 @@
# 🦉 OWL Documentation 🦉 # 🦉 OWL Documentation 🦉
## Owl Content
Owl is a javascript library that contains some core classes and function to help
build applications. Here is a complete representation of its content:
```
owl
Component
Context
QWeb
Store
useState
core
EventBus
Observer
hooks
onWillStart
onMounted
onWillUpdateProps
onWillPatch
onPatched
onWillUnmount
useContext
useState
useRef
useSubEnv
useStore
useDispatch
useGetters
misc
AsyncRoot
router
Link
RouteComponent
Router
tags
xml
utils
debounce
escape
loadJS
loadFile
shallowEqual
whenReady
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
## Reference ## Reference
- [Animations](reference/animations.md) - [Animations](reference/animations.md)
- [Component](reference/component.md) - [Component](reference/component.md)
- [Configuration](reference/config.md)
- [Context](reference/context.md) - [Context](reference/context.md)
- [Environment](reference/environment.md)
- [Event Bus](reference/event_bus.md) - [Event Bus](reference/event_bus.md)
- [Hooks](reference/hooks.md) - [Hooks](reference/hooks.md)
- [Misc](reference/misc.md) - [Miscellaneous Components](reference/misc.md)
- [Observer](reference/observer.md) - [Observer](reference/observer.md)
- [QWeb](reference/qweb.md) - [Props](reference/props.md)
- [QWeb Templating Language](reference/qweb_templating_language.md)
- [QWeb Engine](reference/qweb_engine.md)
- [Router](reference/router.md) - [Router](reference/router.md)
- [Store](reference/store.md) - [Store](reference/store.md)
- [Tags](reference/tags.md) - [Tags](reference/tags.md)
@@ -65,7 +21,8 @@ Note that for convenience, the `useState` hook is also exported at the root of t
## Learning Resources ## Learning Resources
- [Quick Start](quick_start.md) - [Quick Start: create an (almost) empty Owl application](learning/quick_start.md)
- [Tutorial: create a TodoList application](learning/tutorial_todoapp.md)
## Miscellaneous ## Miscellaneous
@@ -75,8 +32,43 @@ Note that for convenience, the `useState` hook is also exported at the root of t
## Architecture ## Architecture
This section explains in more detail the inner workings of Owl. It is more This section explains in more detail the inner workings of Owl. It is targeted
useful for people working on Owl code. for developers working on Owl itself.
- [Virtual DOM](architecture/vdom.md) - [Virtual DOM](architecture/vdom.md)
- [Rendering](architecture/rendering.md) - [Rendering](architecture/rendering.md)
## Owl Content
Here is a complete visual representation of everything exported by the `owl`
global object (so, for example, `Component` is available at `owl.Component`,
and `EventBus` is exported as `owl.core.EventBus`):
```
Component misc
Context AsyncRoot
QWeb router
Store Link
useState RouteComponent
config Router
mode tags
core xml
EventBus utils
Observer debounce
hooks escape
onWillStart loadJS
onMounted loadFile
onWillUpdateProps shallowEqual
onWillPatch whenReady
onPatched
onWillUnmount
useContext
useState
useRef
useSubEnv
useStore
useDispatch
useGetters
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
+74 -184
View File
@@ -11,15 +11,14 @@
- [Methods](#methods) - [Methods](#methods)
- [Lifecycle](#lifecycle) - [Lifecycle](#lifecycle)
- [Root Component](#root-component) - [Root Component](#root-component)
- [Environment](#environment)
- [Composition](#composition) - [Composition](#composition)
- [Event Handling](#event-handling) - [Event Handling](#event-handling)
- [Form Input Bindings](#form-input-bindings) - [Form Input Bindings](#form-input-bindings)
- [`t-key` Directive](#t-key-directive)
- [Semantics](#semantics) - [Semantics](#semantics)
- [Props Validation](#props-validation) - [Props Validation](#props-validation)
- [References](#references) - [References](#references)
- [Slots](#slots) - [Slots](#slots)
- [Dynamic sub components](#dynamic-sub-components)
- [Asynchronous Rendering](#asynchronous-rendering) - [Asynchronous Rendering](#asynchronous-rendering)
- [Error Handling](#error-handling) - [Error Handling](#error-handling)
- [Functional Components](#functional-components) - [Functional Components](#functional-components)
@@ -43,7 +42,7 @@ OWL components are the building blocks for user interface. They are designed to
and follow the QWeb specification. This is a requirement for Odoo. and follow the QWeb specification. This is a requirement for Odoo.
OWL components are defined as a subclass of Component. The rendering is OWL components are defined as a subclass of Component. The rendering is
exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in QWeb). exclusively done by a [QWeb](qweb_templating_language.md) template (which needs to be preloaded in QWeb).
Rendering a component generates a virtual dom representation Rendering a component generates a virtual dom representation
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way. of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
@@ -81,8 +80,8 @@ a state object is defined, by using the `useState` hook. It is not mandatory to
## Reference ## Reference
An Owl component is a small class which represents a component or some UI element. An Owl component is a small class which represents a component or some UI element.
It exists in the context of an environment (`env`), which is propagated from a It exists in the context of an [environment](environment.md) (`env`), which is propagated from a
parent to its children. The environment needs to have a QWeb instance, which parent to its children. The environment needs to have a [QWeb](qweb_templating_language.md) instance, which
will be used to render the component template. will be used to render the component template.
Be aware that the name of the component may be significant: if a component does Be aware that the name of the component may be significant: if a component does
@@ -167,7 +166,7 @@ to be called in the constructor.
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the - **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
component is not mounted. component is not mounted.
- **`env`** (Object): the component environment, which contains a QWeb instance. - **`env`** (Object): the component [environment](environment.md), which contains a QWeb instance.
- **`props`** (Object): this is an object containing all the properties given by - **`props`** (Object): this is an object containing all the properties given by
the parent to a child component. For example, in the following situation, the parent to a child component. For example, in the following situation,
@@ -238,16 +237,15 @@ 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, renderBeforeRemount=false)`** (async): this is the main way a - **`mount(target)`** (async): this is the main way a
component is added to the DOM: the root component is mounted to a target component is added to the DOM: the root component is mounted to a target
HTMLElement. Obviously, this is asynchronous, since each children need to be HTMLElement. 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 `renderBeforeRemount` argument is useful when a component is unmounted and remounted. Note that if a component is mounted, unmounted and remounted, it will be
In that case, we may want to rerender the component _before_ it is remounted, if automatically re-rendered to ensure that changes in its state (or something
we know that its state (or something in the environment, or ...) has changed. in the environment, or in the store, or ...) will be taken into account.
In that case, it should simply set to `true`.
- **`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
@@ -392,8 +390,7 @@ scrollbar.
Note that modifying the state is not allowed here. This method is called just Note that modifying the state is not allowed here. This method is called just
before an actual DOM patch, and is only intended to be used to save some local before an actual DOM patch, and is only intended to be used to save some local
DOM state. Also, it will not be called if the component is not in the DOM (this can DOM state. Also, it will not be called if the component is not in the DOM.
happen with components with `t-keepalive`).
#### `patched(snapshot)` #### `patched(snapshot)`
@@ -403,7 +400,7 @@ likely via a change in its state/props or environment).
This method is not called on the initial render. It is useful to interact This method is not called on the initial render. It is useful to interact
with the DOM (for example, through an external library) whenever the with the DOM (for example, through an external library) whenever the
component was patched. Note that this hook will not be called if the compoent is component was patched. Note that this hook will not be called if the compoent is
not in the DOM (this can happen with components with `t-keepalive`). not in the DOM.
Updating the component state in this hook is possible, but not encouraged. Updating the component state in this hook is possible, but not encouraged.
One needs to be careful, because updates here will create an additional rendering, which in One needs to be careful, because updates here will create an additional rendering, which in
@@ -441,49 +438,17 @@ of an Owl application has to be created manually:
```js ```js
class App extends owl.Component { ... } class App extends owl.Component { ... }
const qweb = new owl.QWeb({ templates: TEMPLATES }); const app = new App();
const env = { qweb: qweb };
const app = new App(env);
app.mount(document.body); app.mount(document.body);
``` ```
The root component needs an environment. The root component does not have a parent nor `props` (see note below). It will be setup with an
[environment](environment.md) (either the `env` defined on its class, or a
default empty environment).
### Environment Note: a root component can however be given a `props` object in its constructor,
like this: `new App(null, {some: 'object'});`. It will not be a true `props`
In Owl, an environment is an object with a `qweb` key, which has to be a object, managed by Owl (so, for example, it will never be updated).
[QWeb](qweb.md) instance. This qweb instance will be used to render everything.
The environment is meant to contain (mostly) static global information and
methods for the whole application. For example, settings keys (`mode` to determine
if we are in desktop or mobile mode, or `theme`: dark or light), `rpc` methods,
session information, ...
The environment will be given to each child, unchanged, in the `env` property.
This can be very useful to share common information/methods. For example, all
rpcs can be made through a `rpc` method in the environment. This makes it very
easy to test a component.
Updating the environment is not as simple as changing a component's state: its
content is not observed, so updates will not be reflected immediately in the
user interface. There is however a mechanism to force root widgets to rerender
themselves whenever the environment is modified: one only needs to call the
`forceUpdate` method on the QWeb instance. For example, a responsive environment
could be done like this:
```js
function setupResponsivePlugin(env) {
const isMobile = () => window.innerWidth <= 768;
env.isMobile = isMobile();
const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile;
env.qweb.forceUpdate();
}
}, 15);
window.addEventListener("resize", updateEnv);
}
```
### Composition ### Composition
@@ -537,66 +502,8 @@ the static `components` key, then fallbacks on the global registry.
_Props_: In this example, the child component will receive the object `{count: 4}` in its _Props_: In this example, the child component will receive the object `{count: 4}` in its
constructor. This will be assigned to the `props` variable, which can be accessed constructor. This will be assigned to the `props` variable, which can be accessed
on the component (and also, in the template). Whenever the state is updated, then on the component (and also, in the template). Whenever the state is updated, then
the sub component will also be updated automatically. the sub component will also be updated automatically. See the [props section](props.md)
for more information.
Note that there are restrictions on valid prop names: `class`, `style` and any
string which starts with `t-` are not allowed.
It is not common, but sometimes we need a dynamic component name and/or dynamic props. In this case,
the `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
```xml
<div t-name="ParentComponent">
<t t-component="ChildComponent{{id}}" />
</div>
```
```js
class ParentComponent {
static components = { ChildComponent1, ChildComponent2 };
state = { id: 1 };
}
```
And the `t-props` directive can be used to specify totally dynamic props:
```xml
<div t-name="ParentComponent">
<Child t-props="some.obj"/>
</div>
```
```js
class ParentComponent {
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
```
There is an even more dynamic way to use `t-component`: its value can be an
expression evaluating to an actual component class. In that case, this is the
class that will be used to create the component:
```js
class A extends Component<any, any, any> {
static template = xml`<span>child a</span>`;
}
class B extends Component<any, any, any> {
static template = xml`<span>child b</span>`;
}
class App extends Component<any, any, any> {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = { child: "a" };
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
```
In this example, the component `App` selects dynamically the concrete sub
component class.
**CSS and style:** Owl allows the parent to declare **CSS and style:** Owl allows the parent to declare
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
@@ -660,11 +567,13 @@ A _business_ DOM event is triggered by a call to `trigger` on a component.
} }
``` ```
The call to `trigger` generates a [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events) The call to `trigger` generates an `OwlEvent`, a subclass of [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
of type `menu-loaded` and dispatches it on the component's DOM element with an additional attribute `originalComponent` (the component that triggered
(`this.el`). The event bubbles and is cancelable. The parent component listening the event). The generated event is of type `menu-loaded` and dispatches it on
to event `menu-loaded` will receive the payload in its `someMethod` handler the component's DOM element (`this.el`). The event bubbles and is cancelable.
(in the `detail` property of the event), whenever the event is triggered. The parent component listening to event `menu-loaded` will receive the payload
in its `someMethod` handler (in the `detail` property of the event), whenever
the event is triggered.
```js ```js
class ParentComponent { class ParentComponent {
@@ -822,70 +731,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.
### `t-key` Directive
Even though Owl tries to be as declarative as possible, the DOM does not fully
expose its state declaratively in the DOM tree. For example, the scrolling state,
the current user selection, the focused element or the state of an input are not
set as attribute in the DOM tree. This is why we use a virtual dom
algorithm to keep the actual DOM node as much as possible.
However, in some situations, this is not enough, and we need to help Owl decide
if an element is actually the same, or is a different element with the same
properties.
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
and we render them in this template:
```xml
<p t-foreach="items" t-as="item"><t t-esc="item.text"/></p>
```
The result will be two `<p>` tags with text `a` and `b`. Now, if we swap them,
and rerender the template, Owl needs to know what the intent is:
- should Owl actually swap the DOM nodes,
- or should it keep the DOM nodes, but with an updated text content?
This might look trivial, but it actually matters. These two possibilities lead
to different results in some cases. For example, if the user selected the text
of the first `p`, swapping them will keep the selection while updating the
text content will not.
There are many other cases where this is important: `input` tags with their
value, css classes and animations, scroll position...
So, the `t-key` directive is used to give an identity to an element. It allows
Owl to understand if different elements of a list are actually different or not.
The above example could be modified by adding an ID: `[{id: 1, text: "a"}, {id: 2, text: "b"}]`.
Then, the template could look like this:
```xml
<p t-foreach="items" t-as="item" t-key="item.id"><t t-esc="item.text"/></p>
```
The `t-key` directive is useful for lists (`t-foreach`). A key should be
a unique number or string (objects will not work: they will be cast to the
`"[object Object]"` string, which is obviously not unique).
Also, the key can be set on a `t` tag or on its children. The following variations
are all equivalent:
```xml
<p t-foreach="items" t-as="item" t-key="item.id">
<t t-esc="item.text"/>
</p>
<t t-foreach="items" t-as="item" t-key="item.id">
<p t-esc="item.text"/>
</t>
<t t-foreach="items" t-as="item">
<p t-key="item.id" t-esc="item.text"/>
</t>
```
### Semantics ### Semantics
We give here an informal description of the way components are created/updated We give here an informal description of the way components are created/updated
@@ -980,13 +825,13 @@ As an application becomes complex, it may be quite unsafe to define props in an
- hard to tell how a component should be used, by looking at its code. - 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. - unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
A props type system would solve both issues, by describing the types and shapes A props type system solves both issues, by describing the types and shapes
of the props. Here is how it works in Owl: 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) - `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. - 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 validated whenever a component is created/updated
- props are only validated in `dev` mode (see [tooling page](../tooling.md#development-mode)) - props are only validated in `dev` mode (see [config page](config.md#mode))
- if a key does not match the description, an error is thrown - if a key does not match the description, an error is thrown
- it validates keys defined in (static) `props`. Additional keys given by the - it validates keys defined in (static) `props`. Additional keys given by the
parent will cause an error. parent will cause an error.
@@ -1180,6 +1025,51 @@ be considered the `default` slot. For example:
</div> </div>
``` ```
### Dynamic sub components
It is not common, but sometimes we need a dynamic component name. In this case,
the `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb_templating_language.md#dynamic-attributes) directive):
```xml
<div t-name="ParentComponent">
<t t-component="ChildComponent{{id}}" />
</div>
```
```js
class ParentComponent {
static components = { ChildComponent1, ChildComponent2 };
state = { id: 1 };
}
```
There is an even more dynamic way to use `t-component`: its value can be an
expression evaluating to an actual component class. In that case, this is the
class that will be used to create the component:
```js
class A extends Component<any, any, any> {
static template = xml`<span>child a</span>`;
}
class B extends Component<any, any, any> {
static template = xml`<span>child b</span>`;
}
class App extends Component<any, any, any> {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = { child: "a" };
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
```
In this example, the component `App` selects dynamically the concrete sub
component class.
Note that the `t-component` directive can only be used on `<t>` nodes.
### Asynchronous Rendering ### Asynchronous Rendering
Working with asynchronous code always adds a lot of complexity to a system. Whenever Working with asynchronous code always adds a lot of complexity to a system. Whenever
+27
View File
@@ -0,0 +1,27 @@
# 🦉 Config 🦉
The Owl framework is designed to work in many situations. However, it is
sometimes necessary to customize some behaviour. This is done by using the
global `config` object. It currently has one key:
- [`mode`](#mode).
## Mode
By default, Owl is in _production_ mode, this means that it will try to do its
job fast, and skip some expensive operations. However, it is sometimes necessary
to have better information on what is going on, this is the purpose
of the `dev` mode.
Owl has a mode flag, in `owl.config.mode`. Its default value is `prod`, but
it can be set to `dev`:
```js
owl.config.mode = "dev";
```
Note that templates compiled with the `prod` settings will not be recompiled.
So, changing this setting is best done at startup.
An important job done by the `dev` mode is to validate props for each component
creation and update. Also, extra props will cause an error.
+3 -6
View File
@@ -28,10 +28,7 @@ context, and add it to the environment:
```js ```js
const deviceContext = new Context({ isMobile: true }); const deviceContext = new Context({ isMobile: true });
const env = { App.env.deviceContext = deviceContext;
qweb: new QWeb({ templates: TEMPLATES }),
deviceContext
};
``` ```
If we want to make it completely responsive, we need to update its value whenever If we want to make it completely responsive, we need to update its value whenever
@@ -61,9 +58,9 @@ class SomeComponent extends Component {
some simplified user interface some simplified user interface
</t> </t>
<t t-else="1"> <t t-else="1">
some more sopthisticated user interface a more advanced user interface
</t> </t>
`; </div>`;
device = useContext(this.env.deviceContext); device = useContext(this.env.deviceContext);
} }
``` ```
+105
View File
@@ -0,0 +1,105 @@
# 🦉 Environment 🦉
## Content
- [Overview](#overview)
- [Setting an Environment](#setting-an-environment)
- [Content of an Environment](#content-of-an-environment)
## Overview
An environment is an object which contains a [`QWeb` instance](qweb_engine.md). Whenever
a root component is created, it is assigned an environment (see
[below](#setting-an-environment) for more info on this). This environment is
then automatically given to each sub component (and accessible in the `this.env`
property).
```
Root
/ \
A B
```
This way, all components share the same `QWeb` instance. Owl internally requires
that the environment has a `qweb` key which maps to a
[`QWeb`](qweb_engine.md) instance. This is the QWeb instance that will be used to
render each templates in this specific component tree. Note that if no `QWeb`
instance is provided, Owl will simply generate it on the fly.
The environment is mostly static. Each application is free to add anything to
the environment, which is very useful, since this can be accessed by each sub
component.
## Setting an environment
An Owl application needs an [environment](environment.md) to be executed. The
environment has an important key: the [QWeb](qweb_engine.md) instance, which will render
all templates.
Whenever a root component `App` is mounted, Owl will setup a valid environment by
following the next steps:
- take the `env` object defined on `App.env` (if no `env` was explicitely setup,
this will be return the empty `env` object defined on `Component`)
- if `env.qweb` is not set, then Owl will create a `QWeb` instance.
The correct way to customize an environment is to simply set it up on the root
component class, before the first component is created:
```js
App.env = {
_t: myTranslateFunction,
user: {...},
services: {
...
},
};
const app = new App();
app.mount(document.body);
```
It is also possible to simply share an environment between all root components,
by simply doing this:
```js
Component.env = myEnv; // will be the default env for all components
```
## Content of an Environment
Some good use cases for additional keys in the environment are:
- some configuration keys,
- session information,
- generic services (such as doing rpcs, or accessing local storage).
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
For example:
```js
async function myEnv() {
const templates = await loadTemplates();
const qweb = new QWeb({ templates });
const session = getSession();
return {
_t: myTranslateFunction,
session: session,
qweb: qweb,
services: {
localStorage: localStorage,
rpc: rpc
},
debug: false,
inMobileMode: true
};
}
async function start() {
App.env = await myEnv();
const app = new App();
await app.mount(document.body);
}
```
+7 -3
View File
@@ -290,6 +290,7 @@ class Parent extends Component {
// here, if component is mounted, refs are active: // here, if component is mounted, refs are active:
// - this.divRef.el is the div HTMLElement // - this.divRef.el is the div HTMLElement
// - this.subRef.comp is the instance of the sub component // - this.subRef.comp is the instance of the sub component
// - this.subRef.el is the root HTML node of the sub component (i.e. this.subRef.comp.el)
} }
} }
``` ```
@@ -297,11 +298,14 @@ class Parent extends Component {
As shown by the example above, html elements are accessed by using the `el` As shown by the example above, html elements are accessed by using the `el`
key, and components references are accessed with `comp`. key, and components references are accessed with `comp`.
Note: if used on a component, the reference will be set in the `refs` Notes:
variable between `willPatch` and `patched`.
- if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`,
- on a component, accessing `ref.el` will get the root node of the component.
The `t-ref` directive also accepts dynamic values with string interpolation The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb.md#dynamic-attributes) and (like the [`t-attf-`](qweb_templating_language.md#dynamic-attributes) and
`t-component` directives). For example, `t-component` directives). For example,
```xml ```xml
+97
View File
@@ -0,0 +1,97 @@
# 🦉 Props 🦉
## Content
- [Overview](#overview)
- [Definition](#definition)
- [Good Practices](#good-practices)
- [Dynamic Props](#dynamic-props)
## Overview
In Owl, `props` (short for _properties_) is an object which contains every piece
of data given to a component by its parent.
```js
class Child extends Component {
static template = xml`<div><t t-esc="props.a"/><t t-esc="props.b"/></div>`;
}
class Parent extends Component {
static template = xml`<div><ComponentA a="state.a" b="'string'"/></div>`;
static components = { Child };
state = useState({ a: "fromparent" });
}
```
In this example, the `Child` component receives two props from its parent: `a`
and `b`. They are collected into a `props` object by Owl, with each value being
evaluated in the context of the parent. So, `props.a` is equal to `'fromparent'` and
`props.b` is equal to `'string'`.
Note that `props` is an object that only makes sense from the perspective of the
child component.
## Definition
The `props` object is made of every attributes defined on the template, with the
following exceptions:
- every attribute starting with `t-` are not props (they are QWeb directives),
- `style` and `class` attributes are excluded as well (they are applied by Owl on
the root element of the component).
In the following example:
```xml
<div>
<ComponentA a="state.a" b="'string'"/>
<ComponentB t-if="state.flag" model="model"/>
<ComponentC style="color:red;" class="left-pane" />
</div>
```
the `props` object contains the following keys:
- for `ComponentA`: `a` and `b`,
- for `ComponentB`: `model`,
- for `ComponentC`: empty object
## Good Practices
A `props` object is a collection of values that come from the parent. As such,
they are owned by the parent, and should never be modified by the child:
```js
class MyComponent extends Component {
constructor(parent, props) {
super(parent, props);
props.a.b = 43; // Never do that!!!
}
}
```
Props should be considered readonly, from the perspective of the child component.
If there is a need to modify them, then the request to update them should be
sent to the parent (for example, with an event).
Any value can go in a props. Strings, objects, classes, or even callbacks could
be given to a child component (but then, in the case of callbacks, communicating
with events seems more appropriate).
## Dynamic Props
The `t-props` directive can be used to specify totally dynamic props:
```xml
<div t-name="ParentComponent">
<Child t-props="some.obj"/>
</div>
```
```js
class ParentComponent {
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
```
+151
View File
@@ -0,0 +1,151 @@
# 🦉 QWeb Engine 🦉
## Content
- [Overview](#overview)
- [Reference](#reference)
## Overview
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
templating engine used by Odoo. The QWeb class in the OWL project is an
implementation of that specification with a few interesting points:
- it compiles templates into functions that output a virtual DOM instead of a
string. This is necessary for the component system.
- it has a few extra directives: `t-component`, `t-on`, ...
We present in this section the engine, not the templating language.
## Reference
This section is about the javascript code that implements the `QWeb` specification.
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
instantiated:
```js
const qweb = new owl.QWeb();
```
Its API is quite simple:
- **`constructor(config)`**: constructor. Takes an optional configuration object
with an optional `templates` string to add initial
templates (see `addTemplates` for more information on format of the string)
and an optional `translateFn` translate function (see the section on
[translations](#translations)).
```js
const qweb = new owl.QWeb({ templates: TEMPLATES, translateFn: _t });
```
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
```js
qweb.addTemplate("mytemplate", "<div>hello</div>");
```
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply
ignore templates added for a second time. Otherwise, `QWeb` will crash.
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute).
```js
const TEMPLATES = `
<templates>
<div t-name="App" class="main">main</div>
<div t-name="OtherComponent">other component</div>
</templates>`;
qweb.addTemplates(TEMPLATES);
```
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
which is a virtual representation of the DOM (see [vdom doc](../architecture/vdom.md)).
```js
const vnode = qweb.render("App", component);
```
- **`renderToString(name, context)`**: renders a template, but returns an html
string.
```js
const str = qweb.renderToString("someTemplate", somecontext);
```
- **`registerTemplate(name, template)`**: static function to register a global
QWeb template. This is useful for commonly used components accross the
application, and for making a template available to an application without
having a reference to the actual QWeb instance.
```js
QWeb.registerTemplate("mytemplate", `<div>some template</div>`);
```
- **`registerComponent(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-component` directive). This is useful for commonly used
components accross the application.
```js
class Dialog extends owl.Component { ... }
QWeb.registerComponent("Dialog", Dialog);
...
class ParentComponent extends owl.Component { ... }
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
```
In some way, a `QWeb` instance is the core of an Owl application. It is the only
mandatory element of an [environment](environment.md). As such, it
has an extra responsibility: it can act as an event bus for internal communication
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
### Translations
If properly setup, Owl QWeb engine can translate all rendered templates. To do
so, it needs a translate function, which takes a string and returns a string.
For example:
```js
const translations = {
hello: "bonjour",
yes: "oui",
no: "non"
};
const translateFn = str => translations[str] || str;
const qweb = new QWeb({ translateFn });
```
Once setup, all rendered templates will be translated using `translateFn`:
- each text node will be replaced with its translation,
- each of the following attribute values will be translated as well: `title`,
`placeholder`, `label` and `alt`,
- translating text nodes can be disabled with the special attribute `t-translation`,
if its value is `off`.
So, with the above `translateFn`, the following templates:
```xml
<div>hello</div>
<div t-translation="off">hello</div>
<div>Are you sure?</div>
<input placeholder="hello" other="yes"/>
```
will be rendered as:
```xml
<div>bonjour</div>
<div>hello</div>
<div>Are you sure?</div>
<input placeholder="bonjour" other="yes"/>
```
Note that the translation is done during the compilation of the template, not
when it is rendered.
@@ -1,10 +1,9 @@
# 🦉 QWeb 🦉 # 🦉 QWeb Templating Language🦉
## Content ## Content
- [Overview](#overview) - [Overview](#overview)
- [Directives](#directives) - [Directives](#directives)
- [QWeb Engine](#qweb-engine)
- [Reference](#reference) - [Reference](#reference)
- [White Spaces](#white-spaces) - [White Spaces](#white-spaces)
- [Root Nodes](#root-nodes) - [Root Nodes](#root-nodes)
@@ -21,14 +20,11 @@
## Overview ## Overview
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used [QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
templating engine used by Odoo. It is based on the XML format, and used
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the HTML. generate a virtual dom representation of the HTML.
Template directives are specified as XML attributes prefixed with `t-`, for instance `t-if` for conditionals, with elements and other attributes being rendered directly.
To avoid element rendering, a placeholder element `<t>` is also available, which executes its directive but doesnt generate any output in and of itself.
```xml ```xml
<div> <div>
<span t-if="somecondition">Some string</span> <span t-if="somecondition">Some string</span>
@@ -40,19 +36,22 @@ To avoid element rendering, a placeholder element `<t>` is also available, which
</div> </div>
``` ```
The QWeb class in the OWL project is an implementation of that specification Template directives are specified as XML attributes prefixed with `t-`, for
with a few interesting points: instance `t-if` for conditionals, with elements and other attributes being
rendered directly.
- it compiles templates into functions that output a virtual DOM instead of a To avoid element rendering, a placeholder element `<t>` is also available, which
string. This is necessary for the component system. executes its directive but doesnt generate any output in and of itself.
- it has a few extra directives: `t-component`, `t-on`, ...
We present in this section the templating language, including its Owl specific
extensions.
## Directives ## Directives
We present here a list of all standard QWeb directives: For reference, here is a list of all standard QWeb directives:
| Name | Description | | Name | Description |
| ------------------------------ | ------------------------------------------------------------ | | ------------------------------ | -------------------------------------------------------------- |
| `t-esc` | [Outputting safely a value](#outputting-data) | | `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-raw` | [Outputting value, without escaping](#outputting-data) | | `t-raw` | [Outputting value, without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) | | `t-set`, `t-value` | [Setting variables](#setting-variables) |
@@ -62,113 +61,23 @@ We present here a list of all standard QWeb directives:
| `t-call` | [Rendering sub templates](#rendering-sub-templates) | | `t-call` | [Rendering sub templates](#rendering-sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) | | `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-translation` | [Disabling the translation of a node](#translations) | | `t-translation` | [Disabling the translation of a node](#translations) |
| `t-name` | [Defining a template (not really a directive)](#qweb-engine) | | `t-name` | [Defining a template (not really a directive)](qweb_engine.md) |
The component system in Owl requires additional directives, to express various The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives: needs. Here is a list of all Owl specific directives:
| Name | Description | | Name | Description |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- | | ------------------------ | ------------------------------------------------------------------------------- |
| `t-component`, `t-props`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) | | `t-component`, `t-props` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) | | `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) | | `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
| `t-on-*` | [Event handling](component.md#event-handling) | | `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) | | `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-slot` | [Rendering a slot](component.md#slots) | | `t-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) | | `t-model` | [Form input bindings](component.md#form-input-bindings) |
## QWeb Engine
This section is about the javascript code that implements the `QWeb` specification.
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
instantiated:
```js
const qweb = new owl.QWeb();
```
Its API is quite simple:
- **`constructor(config)`**: constructor. Takes an optional configuration object
with an optional `templates` string to add initial
templates (see `addTemplates` for more information on format of the string)
and an optional `translateFn` translate function (see the section on
[translations](#translations)).
```js
const qweb = new owl.QWeb({ templates: TEMPLATES, translateFn: _t });
```
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
```js
qweb.addTemplate("mytemplate", "<div>hello</div>");
```
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply
ignore templates added for a second time. Otherwise, `QWeb` will crash.
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute).
```js
const TEMPLATES = `
<templates>
<div t-name="App" class="main">main</div>
<div t-name="OtherComponent">other component</div>
</templates>`;
qweb.addTemplates(TEMPLATES);
```
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
which is a virtual representation of the DOM (see [vdom doc](../architecture/vdom.md)).
```js
const vnode = qweb.render("App", component);
```
- **`renderToString(name, context)`**: renders a template, but returns an html
string.
```js
const str = qweb.renderToString("someTemplate", somecontext);
```
- **`registerTemplate(name, template)`**: static function to register a global
QWeb template. This is useful for commonly used components accross the
application, and for making a template available to an application without
having a reference to the actual QWeb instance.
```js
QWeb.registerTemplate("mytemplate", `<div>some template</div>`);
```
- **`registerComponent(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-component` directive). This is useful for commonly used
components accross the application.
```js
class Dialog extends owl.Component { ... }
QWeb.registerComponent("Dialog", Dialog);
...
class ParentComponent extends owl.Component { ... }
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
```
In some way, a `QWeb` instance is the core of an Owl application. It is the only
mandatory element of an [environment](component.md#environment). As such, it
has an extra responsibility: it can act as an event bus for internal communication
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
## Reference ## Reference
We define in this section the specification of how `QWeb` templates should be
rendered. Note that we only document here the standard QWeb specification. Owl
specific extensions are documented in various other parts of the documentation.
### White Spaces ### White Spaces
White spaces in a template are handled in a special way: White spaces in a template are handled in a special way:
@@ -479,16 +388,66 @@ into the global context.
<!-- new_variable undefined --> <!-- new_variable undefined -->
``` ```
Owl QWeb is used as the template engine for components. Components are frequently Even though Owl tries to be as declarative as possible, the DOM does not fully
updated, and reuse as much of the previous DOM as possible. Loops offer a specific expose its state declaratively in the DOM tree. For example, the scrolling state,
problem for this usecase: how does the template engine know if two rows have the current user selection, the focused element or the state of an input are not
been swapped, or if the content of these rows was changed? To help Owl with that, set as attribute in the DOM tree. This is why we use a virtual dom
there is an additional directive: [`t-key`](component.md#t-key-directive). algorithm to keep the actual DOM node as much as possible.
However, in some situations, this is not enough, and we need to help Owl decide
if an element is actually the same, or is a different element with the same
properties.
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
and we render them in this template:
```xml ```xml
<p t-foreach="state.things" t-as="thing" t-key="thing.id"> <p t-foreach="items" t-as="item"><t t-esc="item.text"/></p>
<t t-esc="thing.content"/> ```
The result will be two `<p>` tags with text `a` and `b`. Now, if we swap them,
and rerender the template, Owl needs to know what the intent is:
- should Owl actually swap the DOM nodes,
- or should it keep the DOM nodes, but with an updated text content?
This might look trivial, but it actually matters. These two possibilities lead
to different results in some cases. For example, if the user selected the text
of the first `p`, swapping them will keep the selection while updating the
text content will not.
There are many other cases where this is important: `input` tags with their
value, css classes and animations, scroll position...
So, the `t-key` directive is used to give an identity to an element. It allows
Owl to understand if different elements of a list are actually different or not.
The above example could be modified by adding an ID: `[{id: 1, text: "a"}, {id: 2, text: "b"}]`.
Then, the template could look like this:
```xml
<p t-foreach="items" t-as="item" t-key="item.id"><t t-esc="item.text"/></p>
```
The `t-key` directive is useful for lists (`t-foreach`). A key should be
a unique number or string (objects will not work: they will be cast to the
`"[object Object]"` string, which is obviously not unique).
Also, the key can be set on a `t` tag or on its children. The following variations
are all equivalent:
```xml
<p t-foreach="items" t-as="item" t-key="item.id">
<t t-esc="item.text"/>
</p> </p>
<t t-foreach="items" t-as="item" t-key="item.id">
<p t-esc="item.text"/>
</t>
<t t-foreach="items" t-as="item">
<p t-key="item.id" t-esc="item.text"/>
</t>
``` ```
If there is no `t-key` directive, Owl will use the index as a default key. If there is no `t-key` directive, Owl will use the index as a default key.
@@ -543,23 +502,9 @@ will result in :
### Translations ### Translations
If properly setup, Owl QWeb engine can translate all rendered templates. To do By default, QWeb specify that templates should be translated. If this behaviour
so, it needs a translate function, which takes a string and returns a string. is not wanted, there is a `t-translation` directive which can turn off
translations (if it is set to the `off` value), with the following rules:
For example:
```js
const translations = {
hello: "bonjour",
yes: "oui",
no: "non"
};
const translateFn = str => translations[str] || str;
const qweb = new QWeb({ translateFn });
```
Once setup, all rendered templates will be translated using `translateFn`:
- each text node will be replaced with its translation, - each text node will be replaced with its translation,
- each of the following attribute values will be translated as well: `title`, - each of the following attribute values will be translated as well: `title`,
@@ -567,26 +512,8 @@ Once setup, all rendered templates will be translated using `translateFn`:
- translating text nodes can be disabled with the special attribute `t-translation`, - translating text nodes can be disabled with the special attribute `t-translation`,
if its value is `off`. if its value is `off`.
So, with the above `translateFn`, the following templates: See [here](qweb_engine.md#translations) for more information on how to setup a
translate function in Owl QWeb.
```xml
<div>hello</div>
<div t-translation="off">hello</div>
<div>Are you sure?</div>
<input placeholder="hello" other="yes"/>
```
will be rendered as:
```xml
<div>bonjour</div>
<div>hello</div>
<div>Are you sure?</div>
<input placeholder="bonjour" other="yes"/>
```
Note that the translation is done during the compilation of the template, not
when it is rendered.
### Debugging ### Debugging
+3
View File
@@ -67,6 +67,9 @@ function makeEnvironment() {
await env.router.start(); await env.router.start();
return env; return env;
} }
App.env = makeEnvironment();
// create root component here
``` ```
Notice that the router needs to be started. This is an asynchronous operation Notice that the router needs to be started. This is an asynchronous operation
+1 -22
View File
@@ -3,7 +3,6 @@
## Content ## Content
- [Overview](#overview) - [Overview](#overview)
- [Development Mode](#development-mode)
- [Playground](#playground) - [Playground](#playground)
- [Benchmarks](#benchmarks) - [Benchmarks](#benchmarks)
- [Single File Component](#single-file-component) - [Single File Component](#single-file-component)
@@ -21,26 +20,6 @@ by using a static http server. A simple python
server is available in `server.py`. There is also a npm script to start it: server is available in `server.py`. There is also a npm script to start it:
`npm run tools` (and its version with a watcher: `npm run tools:watch`). `npm run tools` (and its version with a watcher: `npm run tools:watch`).
## Development Mode
By default, Owl is in _production_ mode, this means that it will try to do its
job fast, and skip some expensive operations. However, in some cases, it is
convenient to have better information on what is going on, this is the purpose
of the dev mode.
Owl has a mode flag, in `owl.__info__.mode`. Its default value is `prod`, but
it can be set to `dev`:
```js
owl.__info__.mode = "dev";
```
Note that templates compiled with the `prod` settings will not be recompiled.
So, changing this setting is best done at startup.
An important job done by the `dev` mode is to validate props for each component
creation and update. Also, extra props will cause an error.
## Playground ## Playground
The playground is an important application designed to help learning and The playground is an important application designed to help learning and
@@ -65,7 +44,7 @@ it easier to scale application to larger size.
To do so, Owl currently has a small helper that makes it easy to define a To do so, Owl currently has a small helper that makes it easy to define a
template inside a javascript (or typescript) file: the [`xml`](reference/tags.md#xml-tag) template inside a javascript (or typescript) file: the [`xml`](reference/tags.md#xml-tag)
helper. With this, a template is automatically registered to [QWeb](reference/qweb.md). helper. With this, a template is automatically registered to [QWeb](reference/qweb_engine.md).
This means that the template and the javascript code can be defined in the same This means that the template and the javascript code can be defined in the same
file. It is not currently possible to add css to the same file, but Owl may file. It is not currently possible to add css to the same file, but Owl may
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "owl-framework", "name": "owl-framework",
"version": "0.24.1", "version": "1.0.0-alpha4",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "src/index.ts", "main": "src/index.ts",
"engines": { "engines": {
@@ -18,7 +18,7 @@
"tools": "npm run build && npm run tools:serve", "tools": "npm run build && npm run tools:serve",
"pretools:watch": "npm run build", "pretools:watch": "npm run build",
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"", "tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"",
"prettier": "prettier {src/**/*.ts,tests/**/*.ts,doc/**/*.md} --write" "prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -44,7 +44,7 @@
"sass": "^1.16.1", "sass": "^1.16.1",
"source-map-support": "^0.5.10", "source-map-support": "^0.5.10",
"ts-jest": "^23.10.5", "ts-jest": "^23.10.5",
"typescript": "^3.6.4", "typescript": "^3.7.2",
"uglify-es": "^3.3.9" "uglify-es": "^3.3.9"
}, },
"jest": { "jest": {
+12 -10
View File
@@ -1,23 +1,25 @@
# 🦉 OWL Roadmap 🦉 # 🦉 OWL Roadmap 🦉
- Current version: 0.24.1 - Current version: 1.0.0-alpha4
- 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!
### October 2019
We plan to complete the following tasks:
- improve API for root widgets (issue #306),
- replace `t-keepalive`, and maybe `t-transition` by components (issue #295).
### November 2019 ### November 2019
Once the previous tasks are done, release version 1.0alpha. This means that the Owl will be used in various Odoo projects. We plan to:
API should be stable. But it could change a little bit if we need it for our
work on Odoo. - fix any issues encountered
- maybe cleanup slightly the router API
- improve the documentation
- improve error handling, add more helpful error messages
### December 2019
If all goes well, Owl will be upgraded to beta status. From then, no API change,
even small, is expected.
### End of 2019 ### End of 2019
+105 -104
View File
@@ -1,10 +1,11 @@
import { Observer } from "../core/observer"; import { Observer } from "../core/observer";
import { OwlEvent } from "../core/owl_event";
import { CompiledTemplate, QWeb } from "../qweb/index"; import { CompiledTemplate, QWeb } from "../qweb/index";
import { h, patch, VNode } from "../vdom/index"; import { patch, VNode } from "../vdom/index";
import "./directive"; import "./directive";
import { Fiber } from "./fiber"; import { Fiber } from "./fiber";
import "./props_validation"; import "./props_validation";
import { Scheduler } from "./scheduler"; import { Scheduler, scheduler } from "./scheduler";
/** /**
* Owl Component System * Owl Component System
@@ -20,8 +21,6 @@ import { Scheduler } from "./scheduler";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Types/helpers // Types/helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const raf = window.requestAnimationFrame.bind(window);
export const scheduler = new Scheduler(raf);
/** /**
* An Env (environment) is an object that will be (mostly) shared between all * An Env (environment) is an object that will be (mostly) shared between all
@@ -46,6 +45,7 @@ interface Internal<T extends Env, Props> {
// each component has a unique id, useful mostly to handle parent/child // each component has a unique id, useful mostly to handle parent/child
// relationships // relationships
readonly id: number; readonly id: number;
depth: number;
vnode: VNode | null; vnode: VNode | null;
pvnode: VNode | null; pvnode: VNode | null;
isMounted: boolean; isMounted: boolean;
@@ -61,6 +61,9 @@ interface Internal<T extends Env, Props> {
cmap: { [key: number]: number }; cmap: { [key: number]: number };
currentFiber: Fiber | null; currentFiber: Fiber | null;
// parentLastFiberId is there to help the parent component to detect, among
// its children, those that are not used anymore and thus can be destroyed
parentLastFiberId: number;
boundHandlers: { [key: number]: any }; boundHandlers: { [key: number]: any };
observer: Observer | null; observer: Observer | null;
@@ -88,6 +91,9 @@ export class Component<T extends Env, Props extends {}> {
static components = {}; static components = {};
static props?: any; static props?: any;
static defaultProps?: any; static defaultProps?: any;
static env: any = {};
// expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler;
/** /**
* The `el` is the root element of the component. Note that it could be null: * The `el` is the root element of the component. Note that it could be null:
@@ -107,44 +113,37 @@ export class Component<T extends Env, Props extends {}> {
/** /**
* Creates an instance of Component. * Creates an instance of Component.
* *
* The root component of a component tree needs an environment:
*
* ```javascript
* const root = new RootComponent(env, props);
* ```
*
* Every other component simply needs a reference to its parent:
*
* ```javascript
* const child = new SomeComponent(parent, props);
* ```
*
* Note that most of the time, only the root component needs to be created by * Note that most of the time, only the root component needs to be created by
* hand. Other components should be created automatically by the framework (with * hand. Other components should be created automatically by the framework (with
* the t-component directive in a template) * the t-component directive in a template)
*/ */
constructor(parent: Component<T, any> | T, props?: Props) { constructor(parent?: Component<T, any> | null, props?: Props) {
const defaultProps = (<any>this.constructor).defaultProps;
Component.current = this; Component.current = this;
let constr = this.constructor as any;
const defaultProps = constr.defaultProps;
if (defaultProps) { if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps); props = props || ({} as Props);
this.__applyDefaultProps(props, defaultProps);
} }
// is this a good idea? this.props = <Props>props;
// Pro: if props is empty, we can create easily a component
// Con: this is not really safe
// Pro: but creating component (by a template) is always unsafe anyway
this.props = <Props>props || <Props>{};
if (QWeb.dev) { if (QWeb.dev) {
QWeb.utils.validateProps(this.constructor, this.props); QWeb.utils.validateProps(constr, this.props);
} }
let id: number = nextId++;
let p: Component<T, any> | null = null; const id: number = nextId++;
if (parent instanceof Component) { let depth;
p = parent; if (parent) {
this.env = parent.env; this.env = parent.env;
parent.__owl__.children[id] = this; const __powl__ = parent.__owl__;
__powl__.children[id] = this;
depth = __powl__.depth + 1;
} else { } else {
this.env = parent; // we are the root component
this.env = (this.constructor as any).env;
if (!this.env.qweb) {
this.env.qweb = new QWeb();
}
this.env.qweb.on("update", this, () => { this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) { if (this.__owl__.isMounted) {
this.render(true); this.render(true);
@@ -158,19 +157,23 @@ export class Component<T extends Env, Props extends {}> {
this.env.qweb.off("update", this); this.env.qweb.off("update", this);
} }
}); });
depth = 0;
} }
const qweb = this.env.qweb;
const qweb = this.env.qweb;
const template = constr.template || this.__getTemplate(qweb);
this.__owl__ = { this.__owl__ = {
id: id, id: id,
depth: depth,
vnode: null, vnode: null,
pvnode: null, pvnode: null,
isMounted: false, isMounted: false,
isDestroyed: false, isDestroyed: false,
parent: p, parent: parent || null,
children: {}, children: {},
cmap: {}, cmap: {},
currentFiber: null, currentFiber: null,
parentLastFiberId: 0,
boundHandlers: {}, boundHandlers: {},
mountedCB: null, mountedCB: null,
willUnmountCB: null, willUnmountCB: null,
@@ -179,7 +182,7 @@ export class Component<T extends Env, Props extends {}> {
willStartCB: null, willStartCB: null,
willUpdatePropsCB: null, willUpdatePropsCB: null,
observer: null, observer: null,
renderFn: qweb.render.bind(qweb, this.__getTemplate(qweb)), renderFn: qweb.render.bind(qweb, template),
classObj: null, classObj: null,
refs: null refs: null
}; };
@@ -278,40 +281,23 @@ 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, renderBeforeRemount: boolean = false): Promise<void> { async mount(target: HTMLElement): Promise<void> {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (__owl__.isMounted) { if (__owl__.isMounted) {
return Promise.resolve(); return Promise.resolve();
} }
if (__owl__.vnode && !renderBeforeRemount) { if (!(target instanceof HTMLElement)) {
target.appendChild(this.el!); let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`;
if (document.body.contains(target)) { message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
this.__callMounted(); throw new Error(message);
} }
return; const fiber = new Fiber(null, this, undefined, undefined, false, target);
}
const fiber = new Fiber(null, this, this.props, undefined, undefined, false);
if (!__owl__.vnode) { if (!__owl__.vnode) {
this.__prepareAndRender(fiber); this.__prepareAndRender(fiber);
} else { } else {
this.__render(fiber); this.__render(fiber);
} }
return new Promise((resolve, reject) => { return scheduler.addFiber(fiber);
scheduler.addFiber(fiber, err => {
if (err) {
reject(err);
return;
}
if (!__owl__.isDestroyed) {
this.__patch(fiber.vnode);
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
}
}
resolve();
});
});
} }
/** /**
@@ -336,26 +322,34 @@ export class Component<T extends Env, Props extends {}> {
*/ */
async render(force: boolean = false): Promise<void> { async render(force: boolean = false): Promise<void> {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if ( if (!__owl__.isMounted && !__owl__.currentFiber) {
(!__owl__.isMounted && !__owl__.currentFiber) || // if we get here, this means that the component was either never mounted,
(__owl__.currentFiber && !__owl__.currentFiber.isRendered) // or was unmounted and some state change triggered a render. Either way,
) { // we do not want to actually render anything in this case.
return; return;
} }
const fiber = new Fiber(null, this, this.props, undefined, undefined, force); if (__owl__.currentFiber && !__owl__.currentFiber.isRendered) {
return scheduler.addFiber(__owl__.currentFiber.root);
}
// if we aren't mounted at this point, it implies that there is a
// currentFiber that is already rendered (isRendered is true), so we are
// about to be mounted
const isMounted = __owl__.isMounted;
const fiber = new Fiber(null, this, undefined, undefined, force, null);
Promise.resolve().then(() => {
if (__owl__.isMounted || !isMounted) {
// we are mounted (__owl__.isMounted), or if we are currently being
// mounted (!isMounted), so we call __render
this.__render(fiber); this.__render(fiber);
return new Promise((resolve, reject) => { } else {
scheduler.addFiber(fiber.root, err => { // we were mounted when render was called, but we aren't anymore, so we
if (err) { // were actually about to be unmounted ; we can thus forget about this
reject(err); // fiber
return; fiber.isCompleted = true;
__owl__.currentFiber = null;
} }
if (__owl__.isMounted && fiber === fiber.root) {
fiber.patchComponents();
}
resolve();
});
}); });
return scheduler.addFiber(fiber);
} }
/** /**
@@ -395,7 +389,7 @@ export class Component<T extends Env, Props extends {}> {
*/ */
trigger(eventType: string, payload?: any) { trigger(eventType: string, payload?: any) {
if (this.el) { if (this.el) {
const ev = new CustomEvent(eventType, { const ev = new OwlEvent(this, eventType, {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
detail: payload detail: payload
@@ -440,6 +434,9 @@ export class Component<T extends Env, Props extends {}> {
} }
__owl__.isDestroyed = true; __owl__.isDestroyed = true;
delete __owl__.vnode; delete __owl__.vnode;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true;
}
} }
__callMounted() { __callMounted() {
@@ -452,14 +449,11 @@ export class Component<T extends Env, Props extends {}> {
} }
} }
__owl__.isMounted = true; __owl__.isMounted = true;
try { __owl__.currentFiber = null;
this.mounted(); this.mounted();
if (__owl__.mountedCB) { if (__owl__.mountedCB) {
__owl__.mountedCB(); __owl__.mountedCB();
} }
} catch (e) {
console.error(e); // TODO : add a test
}
} }
__callWillUnmount() { __callWillUnmount() {
@@ -492,7 +486,7 @@ export class Component<T extends Env, Props extends {}> {
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__;
const fiber = new Fiber(parentFiber, this, this.props, scope, vars, parentFiber.force); const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force, null);
if (!parentFiber.child) { if (!parentFiber.child) {
parentFiber.child = fiber; parentFiber.child = fiber;
} else { } else {
@@ -501,7 +495,7 @@ export class Component<T extends Env, Props extends {}> {
const defaultProps = (<any>this.constructor).defaultProps; const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) { if (defaultProps) {
nextProps = this.__applyDefaultProps(nextProps, defaultProps); this.__applyDefaultProps(nextProps, defaultProps);
} }
if (QWeb.dev) { if (QWeb.dev) {
QWeb.utils.validateProps(this.constructor, nextProps); QWeb.utils.validateProps(this.constructor, nextProps);
@@ -510,7 +504,7 @@ export class Component<T extends Env, Props extends {}> {
this.willUpdateProps(nextProps), this.willUpdateProps(nextProps),
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps) __owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps)
]); ]);
if (fiber.isCancelled) { if (fiber.isCompleted) {
return; return;
} }
this.props = nextProps; this.props = nextProps;
@@ -527,7 +521,6 @@ export class Component<T extends Env, Props extends {}> {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
const target = __owl__.vnode || document.createElement(vnode.sel!); const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode); __owl__.vnode = patch(target, vnode);
__owl__.currentFiber = null;
} }
/** /**
@@ -536,7 +529,7 @@ export class Component<T extends Env, Props extends {}> {
* parent template. * parent template.
*/ */
__prepare(parentFiber: Fiber, scope: any, vars: any, previousSibling?: Fiber | null) { __prepare(parentFiber: Fiber, scope: any, vars: any, previousSibling?: Fiber | null) {
const fiber = new Fiber(parentFiber, this, this.props, scope, vars, parentFiber.force); const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force, null);
fiber.shouldPatch = false; fiber.shouldPatch = false;
if (!parentFiber.child) { if (!parentFiber.child) {
parentFiber.child = fiber; parentFiber.child = fiber;
@@ -549,9 +542,6 @@ export class Component<T extends Env, Props extends {}> {
__getTemplate(qweb: QWeb): string { __getTemplate(qweb: QWeb): string {
let p = (<any>this).constructor; let p = (<any>this).constructor;
if (!p.hasOwnProperty("_template")) { if (!p.hasOwnProperty("_template")) {
if (p.template) {
p._template = p.template;
} else {
// here, the component and none of its superclasses defines a static `template` // here, the component and none of its superclasses defines a static `template`
// key. So we fall back on looking for a template matching its name (or // key. So we fall back on looking for a template matching its name (or
// one of its subclass). // one of its subclass).
@@ -566,7 +556,6 @@ export class Component<T extends Env, Props extends {}> {
p._template = template; p._template = template;
} }
} }
}
return p._template; return p._template;
} }
async __prepareAndRender(fiber: Fiber) { async __prepareAndRender(fiber: Fiber) {
@@ -574,13 +563,12 @@ export class Component<T extends Env, Props extends {}> {
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) {
fiber.handleError(e); fiber.handleError(e);
fiber.vnode = h("div"); // -> we render this div at the end
return Promise.resolve(); return Promise.resolve();
} }
if (this.__owl__.isDestroyed) { if (this.__owl__.isDestroyed) {
return Promise.resolve(); return Promise.resolve();
} }
if (!fiber.isCancelled) { if (!fiber.isCompleted) {
this.__render(fiber); this.__render(fiber);
} }
} }
@@ -590,29 +578,45 @@ export class Component<T extends Env, Props extends {}> {
if (__owl__.observer) { if (__owl__.observer) {
__owl__.observer.allowMutations = false; __owl__.observer.allowMutations = false;
} }
let vnode; let error;
try { try {
vnode = __owl__.renderFn!(this, { let vnode = __owl__.renderFn!(this, {
handlers: __owl__.boundHandlers, handlers: __owl__.boundHandlers,
fiber: fiber fiber: fiber
}); });
} catch (e) { // we iterate over the children to detect those that no longer belong to the
vnode = __owl__.vnode || h("div"); // current rendering: those ones, if not mounted yet, can (and have to) be
fiber.handleError(e); // destroyed right now, because they are not in the DOM, and thus we won't
// be notified later on (when patching), that they are removed from the DOM
for (let childKey in __owl__.children) {
let child = __owl__.children[childKey];
if (!child.__owl__.isMounted && child.__owl__.parentLastFiberId < fiber.id) {
child.destroy();
}
}
if (!vnode) {
throw new Error(`Rendering '${this.constructor.name}' did not return anything`);
} }
fiber.vnode = vnode; fiber.vnode = vnode;
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
// we apply here the class information described on the component by the // we apply here the class information described on the component by the
// template (so, something like <MyComponent class="..."/>) to the actual // template (so, something like <MyComponent class="..."/>) to the actual
// root vnode // root vnode
if (__owl__.classObj) { if (__owl__.classObj) {
vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj); const data = vnode.data!;
data.class = Object.assign(data.class || {}, __owl__.classObj);
} }
} catch (e) {
error = e;
}
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
fiber.root.counter--; fiber.root.counter--;
fiber.isRendered = true; fiber.isRendered = true;
if (error) {
fiber.handleError(error);
}
} }
/** /**
@@ -649,16 +653,13 @@ export class Component<T extends Env, Props extends {}> {
/** /**
* Apply default props (only top level). * Apply default props (only top level).
* *
* Note that this method does not modify in place the props, it returns a new * Note that this method does modify in place the props
* prop object
*/ */
__applyDefaultProps(props: Object | undefined, defaultProps: Object): Props { __applyDefaultProps(props: Object, defaultProps: Object) {
props = props ? Object.assign({}, props) : {};
for (let propName in defaultProps) { for (let propName in defaultProps) {
if (props![propName] === undefined) { if (props![propName] === undefined) {
props![propName] = defaultProps[propName]; props![propName] = defaultProps[propName];
} }
} }
return <Props>props;
} }
} }
+10 -40
View File
@@ -186,7 +186,7 @@ QWeb.utils.defineProxy = function defineProxy(target, source) {
QWeb.addDirective({ QWeb.addDirective({
name: "component", name: "component",
extraNames: ["props", "keepalive"], extraNames: ["props"],
priority: 100, priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean { atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//COMPONENT"); ctx.addLine("//COMPONENT");
@@ -194,7 +194,6 @@ QWeb.addDirective({
ctx.rootContext.shouldDefineQWeb = true; ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true; ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
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
@@ -230,19 +229,7 @@ QWeb.addDirective({
let defID = ctx.generateID(); let defID = ctx.generateID();
let componentID = ctx.generateID(); let componentID = ctx.generateID();
let locationExpr = `\`__${ctx.generateID()}__`; const templateKey = ctx.generateTemplateKey();
for (let i = 0; i < ctx.loopNumber - 1; i++) {
locationExpr += `\${i${i + 1}}__`;
}
if (ctx.lastNodeKey || ctx.currentKey) {
const k = ctx.lastNodeKey || ctx.currentKey;
ctx.addLine(`let templateId${componentID} = ${locationExpr}\` + ${k};`);
} else {
locationExpr += ctx.loopNumber ? `\${i${ctx.loopNumber}}__\`` : "`";
ctx.addLine(`let templateId${componentID} = ${locationExpr};`);
}
const templateId = `templateId${componentID}`;
let ref = node.getAttribute("t-ref"); let ref = node.getAttribute("t-ref");
let refExpr = ""; let refExpr = "";
let refKey: string = ""; let refKey: string = "";
@@ -256,8 +243,8 @@ QWeb.addDirective({
if (transition) { if (transition) {
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`; transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
} }
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`; let finalizeComponentCode = `w${componentID}.destroy();`;
if (ref && !keepAlive) { if (ref) {
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`; finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
} }
if (transition) { if (transition) {
@@ -335,14 +322,9 @@ QWeb.addDirective({
} }
ctx.addLine( ctx.addLine(
`let w${componentID} = ${templateId} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateId}]] : false;` `let w${componentID} = ${templateKey} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateKey}]] : false;`
); );
let shouldProxy = !ctx.parentNode; let shouldProxy = !ctx.parentNode;
if (keepAlive) {
ctx.addLine(
`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`
);
}
if (shouldProxy) { if (shouldProxy) {
let id = ctx.generateID(); let id = ctx.generateID();
ctx.rootContext.rootNode = id; ctx.rootContext.rootNode = id;
@@ -393,28 +375,15 @@ QWeb.addDirective({
ctx.addIf(`w${componentID}`); ctx.addIf(`w${componentID}`);
// need to update component // need to update component
let patchQueueCode = keepAlive ? `fiber${componentID}` : "extra.fiber";
if (keepAlive) {
// if we have t-keepalive="1", the component could be unmounted, but then
// we __updateProps is called. This is ok, but we do not want to call
// the willPatch/patched hooks of the component in this case, so we
// disable the patch queue
patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`;
}
let styleCode = ""; let styleCode = "";
if (tattStyle) { if (tattStyle) {
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}, ${patchQueueCode}${scopeVars && `w${componentID}.__updateProps(props${componentID}, extra.fiber${scopeVars &&
", " + scopeVars}, sibling)${styleCode};` ", " + scopeVars}, sibling)${styleCode};`
); );
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`); ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
let keepAliveCode = "";
if (keepAlive) {
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
ctx.addLine(keepAliveCode);
}
if (registerCode) { if (registerCode) {
ctx.addLine(registerCode); ctx.addLine(registerCode);
} }
@@ -440,7 +409,7 @@ QWeb.addDirective({
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}` `if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
); );
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`); ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
ctx.addLine(`parent.__owl__.cmap[${templateId}] = w${componentID}.__owl__.id;`); ctx.addLine(`parent.__owl__.cmap[${templateKey}] = w${componentID}.__owl__.id;`);
if (hasSlots) { if (hasSlots) {
const clone = <Element>node.cloneNode(true); const clone = <Element>node.cloneNode(true);
@@ -470,11 +439,11 @@ QWeb.addDirective({
ctx.addLine(`let def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars}, sibling);`); ctx.addLine(`let def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars}, sibling);`);
// 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
ctx.addLine( ctx.addLine(
`let pvnode = h('dummy', {key: ${templateId}, hook: {insert(vn) { let nvn=w${componentID}.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});` `let pvnode = h('dummy', {key: ${templateKey}, hook: {insert(vn) { let nvn=w${componentID}.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});`
); );
ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`); ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`);
ctx.addLine( ctx.addLine(
`def${defID}.then(function () {if (w${componentID}.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});` `def${defID}.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
); );
if (registerCode) { if (registerCode) {
ctx.addLine(registerCode); ctx.addLine(registerCode);
@@ -490,6 +459,7 @@ QWeb.addDirective({
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`); ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
} }
ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`);
ctx.addLine(`sibling = w${componentID}.__owl__.currentFiber || sibling;`); ctx.addLine(`sibling = w${componentID}.__owl__.currentFiber || sibling;`);
return true; return true;
+73 -25
View File
@@ -1,5 +1,6 @@
import { VNode } from "../vdom/index"; import { h, VNode } from "../vdom/index";
import { Component } from "./component"; import { Component } from "./component";
import { scheduler } from "./scheduler";
/** /**
* Owl Fiber Class * Owl Fiber Class
@@ -15,13 +16,17 @@ import { Component } from "./component";
*/ */
export class Fiber { export class Fiber {
static nextId: number = 1;
id: number = Fiber.nextId++;
// The force attribute determines if a rendering should bypass the `shouldUpdate` // The force attribute determines if a rendering should bypass the `shouldUpdate`
// method potentially implemented by a component. It is usually set to false. // method potentially implemented by a component. It is usually set to false.
force: boolean; force: boolean;
// isCancelled means that the rendering corresponding to this fiber and its // isCompleted means that the rendering corresponding to this fiber's work is
// children is cancelled. No extra work should be done. // done, either because the component has been mounted or patched, or because
isCancelled: boolean = false; // fiber has been cancelled.
isCompleted: boolean = false;
// the fibers corresponding to component updates (updateProps) need to call // the fibers corresponding to component updates (updateProps) need to call
// the willPatch and patched hooks from the corresponding component. However, // the willPatch and patched hooks from the corresponding component. However,
@@ -41,9 +46,10 @@ export class Fiber {
// scheduler. // scheduler.
counter: number = 0; counter: number = 0;
target: HTMLElement | null;
scope: any; scope: any;
vars: any; vars: any;
props: any;
component: Component<any, any>; component: Component<any, any>;
vnode: VNode | null = null; vnode: VNode | null = null;
@@ -55,26 +61,53 @@ export class Fiber {
error?: Error; error?: Error;
constructor(parent: Fiber | null, component: Component<any, any>, props, scope, vars, force) { constructor(parent: Fiber | null, component: Component<any, any>, scope, vars, force, target) {
this.force = force; this.force = force;
this.scope = scope; this.scope = scope;
this.vars = vars; this.vars = vars;
this.props = props;
this.component = component; this.component = component;
this.target = target;
this.root = parent ? parent.root : this; this.root = parent ? parent.root : this;
this.parent = parent; this.parent = parent;
let oldFiber = component.__owl__.currentFiber; let oldFiber = component.__owl__.currentFiber;
if (oldFiber && !oldFiber.isCancelled) { if (oldFiber && !oldFiber.isCompleted) {
if (oldFiber.root === oldFiber && !parent) {
// both oldFiber and this fiber are root fibers
this._reuseFiber(oldFiber);
return oldFiber;
} else {
this._remapFiber(oldFiber); this._remapFiber(oldFiber);
} }
}
this.root.counter++; this.root.counter++;
component.__owl__.currentFiber = this; component.__owl__.currentFiber = this;
} }
/**
* When the oldFiber is not completed yet, and both oldFiber and this fiber
* are root fibers, we want to reuse the oldFiber instead of creating a new
* one. Doing so will guarantee that the initiator(s) of those renderings will
* be notified (the promise will resolve) when the last rendering will be done.
*
* This function thus assumes that oldFiber is a root fiber.
*/
_reuseFiber(oldFiber: Fiber) {
oldFiber.cancel(); // cancel children fibers
oldFiber.isCompleted = false; // keep the root fiber alive
oldFiber.isRendered = false; // the fiber has to be re-rendered
if (oldFiber.child) {
// remove relation to children
oldFiber.child.parent = null;
oldFiber.child = null;
}
oldFiber.counter = 1; // re-initialize counter
oldFiber.id = Fiber.nextId++;
}
/** /**
* In some cases, a rendering initiated at some component can detect that it * In some cases, a rendering initiated at some component can detect that it
* should be part of a larger rendering initiated somewhere up the component * should be part of a larger rendering initiated somewhere up the component
@@ -84,7 +117,7 @@ export class Fiber {
_remapFiber(oldFiber: Fiber) { _remapFiber(oldFiber: Fiber) {
oldFiber.cancel(); oldFiber.cancel();
if (oldFiber === oldFiber.root) { if (oldFiber === oldFiber.root) {
oldFiber.root.counter++; oldFiber.counter++;
} }
if (oldFiber.parent && !this.parent) { if (oldFiber.parent && !this.parent) {
// re-map links // re-map links
@@ -133,7 +166,26 @@ export class Fiber {
} }
/** /**
* Apply the given patch queue from a fiber. * Successfully complete the work of the fiber: call the mount or patch hooks
* and patch the DOM. This function is called once the fiber and its children
* are ready, and the scheduler decides to process it.
*/
complete() {
const component = this.component;
if (this.target) {
component.__patch(this.vnode);
this.target.appendChild(component.el!);
if (document.body.contains(this.target)) {
component.__callMounted();
}
} else if (component.__owl__.isMounted && this === this.root) {
this.patchComponents();
}
this.isCompleted = true;
}
/**
* Compute and apply the patch queue of the fiber.
* 1) Call 'willPatch' on the component of each patch * 1) Call 'willPatch' on the component of each patch
* 2) Call '__patch' on the component of each patch * 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in reverse order * 3) Call 'patched' on the component of each patch, in reverse order
@@ -143,13 +195,12 @@ export class Fiber {
const doWork: (Fiber) => Fiber | null = function(f) { const doWork: (Fiber) => Fiber | null = function(f) {
if (f.shouldPatch) { if (f.shouldPatch) {
patchQueue.push(f); patchQueue.push(f);
}
return f.child; return f.child;
}
}; };
this._walk(doWork); this._walk(doWork);
let component: Component<any, any> = this.component; let component: Component<any, any> = this.component;
const patchLen = patchQueue.length; const patchLen = patchQueue.length;
try {
for (let i = 0; i < patchLen; i++) { for (let i = 0; i < patchLen; i++) {
component = patchQueue[i].component; component = patchQueue[i].component;
if (component.__owl__.willPatchCB) { if (component.__owl__.willPatchCB) {
@@ -157,15 +208,12 @@ export class Fiber {
} }
component.willPatch(); component.willPatch();
} }
} catch (e) {
console.error(e);
}
for (let i = 0; i < patchLen; i++) { for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i]; const fiber = patchQueue[i];
component = fiber.component; component = fiber.component;
component.__patch(fiber.vnode); component.__patch(fiber.vnode);
component.__owl__.currentFiber = null;
} }
try {
for (let i = patchLen - 1; i >= 0; i--) { for (let i = patchLen - 1; i >= 0; i--) {
component = patchQueue[i].component; component = patchQueue[i].component;
component.patched(); component.patched();
@@ -173,9 +221,6 @@ export class Fiber {
component.__owl__.patchedCB(); component.__owl__.patchedCB();
} }
} }
} catch (e) {
console.error(e);
}
} }
/** /**
@@ -186,7 +231,7 @@ export class Fiber {
if (!f.isRendered) { if (!f.isRendered) {
f.root.counter--; f.root.counter--;
} }
f.isCancelled = true; f.isCompleted = true;
return f.child; return f.child;
}); });
} }
@@ -200,10 +245,12 @@ export class Fiber {
* being in a corrupted state. * being in a corrupted state.
*/ */
handleError(error: Error) { handleError(error: Error) {
let canCatch = false;
let component = this.component; let component = this.component;
let qweb = component.env.qweb; this.vnode = component.__owl__.vnode || h("div");
const qweb = component.env.qweb;
let root = component; let root = component;
let canCatch = false;
while (component && !(canCatch = !!component.catchError)) { while (component && !(canCatch = !!component.catchError)) {
root = component; root = component;
component = component.__owl__.parent!; component = component.__owl__.parent!;
@@ -211,12 +258,13 @@ export class Fiber {
qweb.trigger("error", error); qweb.trigger("error", error);
if (canCatch) { if (canCatch) {
setTimeout(() => {
console.error(error);
component.catchError!(error); component.catchError!(error);
});
} else { } else {
// the 3 next lines aim to mark the root fiber as being in error, and
// to force it to end, without waiting for its children
this.root.counter = 0;
this.root.error = error; this.root.error = error;
scheduler.flush();
root.destroy(); root.destroy();
} }
} }
+19 -3
View File
@@ -40,7 +40,13 @@ QWeb.utils.validateProps = function(Widget, props: Object) {
break; break;
} }
} }
let isValid = isValidProp(props[propName], propsDef[propName]); let isValid;
try {
isValid = isValidProp(props[propName], propsDef[propName]);
} catch (e) {
e.message = `Invalid prop '${propName}' in component ${Widget.name} (${e.message})`;
throw e;
}
if (!isValid) { if (!isValid) {
throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`); throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`);
} }
@@ -80,17 +86,27 @@ function isValidProp(prop, propDef): boolean {
return result; return result;
} }
// propsDef is an object // propsDef is an object
if (propDef.optional && prop === undefined) {
return true;
}
let result = isValidProp(prop, propDef.type); let result = isValidProp(prop, propDef.type);
if (propDef.type === Array) { if (propDef.type === Array && propDef.element) {
for (let i = 0, iLen = prop.length; i < iLen; i++) { for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element); result = result && isValidProp(prop[i], propDef.element);
} }
} }
if (propDef.type === Object) { if (propDef.type === Object && propDef.shape) {
const shape = propDef.shape; const shape = propDef.shape;
for (let key in shape) { for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]); result = result && isValidProp(prop[key], shape[key]);
} }
if (result) {
for (let propName in prop) {
if (!(propName in shape)) {
throw new Error(`unknown prop '${propName}'`);
}
}
}
} }
return result; return result;
} }
+29 -6
View File
@@ -25,13 +25,25 @@ export class Scheduler {
this.requestAnimationFrame = requestAnimationFrame; this.requestAnimationFrame = requestAnimationFrame;
} }
addFiber(fiber, callback) { addFiber(fiber): Promise<void> {
this.tasks.push({ fiber, callback }); return new Promise((resolve, reject) => {
if (this.isRunning) { if (fiber.error) {
return; return reject(fiber.error);
} }
this.tasks.push({
fiber,
callback: () => {
if (fiber.error) {
return reject(fiber.error);
}
resolve();
}
});
if (!this.isRunning) {
this.scheduleTasks(); this.scheduleTasks();
} }
});
}
/** /**
* Process all current tasks. This only applies to the fibers that are ready. * Process all current tasks. This only applies to the fibers that are ready.
@@ -41,11 +53,19 @@ export class Scheduler {
let tasks = this.tasks; let tasks = this.tasks;
this.tasks = []; this.tasks = [];
tasks = tasks.filter(task => { tasks = tasks.filter(task => {
if (task.fiber.isCancelled) { if (task.fiber.isCompleted) {
task.callback();
return false; return false;
} }
if (task.fiber.counter === 0) { if (task.fiber.counter === 0) {
task.callback(task.fiber.error); if (!task.fiber.error) {
try {
task.fiber.complete();
} catch (e) {
task.fiber.handleError(e);
}
}
task.callback();
return false; return false;
} }
return true; return true;
@@ -65,3 +85,6 @@ export class Scheduler {
}); });
} }
} }
const raf = window.requestAnimationFrame.bind(window);
export const scheduler = new Scheduler(raf);
+30
View File
@@ -0,0 +1,30 @@
import { QWeb } from "./qweb/index";
/**
* This file creates and exports the OWL 'config' object, with keys:
* - 'mode': 'prod' or 'dev',
* - 'env': the environment to use in root components.
*/
interface Config {
mode: string;
}
export const config = {} as Config;
Object.defineProperty(config, "mode", {
get() {
return QWeb.dev ? "dev" : "prod";
},
set(mode: string) {
QWeb.dev = mode === "dev";
if (QWeb.dev) {
const url = `https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode`;
console.warn(
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
);
} else {
console.log(`Owl is now running in 'prod' mode.`);
}
}
});
+62 -25
View File
@@ -1,7 +1,8 @@
import { Component, scheduler } from "./component/component"; import { Component } from "./component/component";
import { scheduler } from "./component/scheduler";
import { EventBus } from "./core/event_bus"; import { EventBus } from "./core/event_bus";
import { Observer } from "./core/observer"; import { Observer } from "./core/observer";
import { onWillUnmount } from "./hooks";
/** /**
* The `Context` object provides a way to share data between an arbitrary number * The `Context` object provides a way to share data between an arbitrary number
* of component. Usually, data is passed from a parent to its children component, * of component. Usually, data is passed from a parent to its children component,
@@ -12,6 +13,28 @@ import { onWillUnmount } from "./hooks";
* With a `Context` object, each component can subscribe (with the `useContext` * With a `Context` object, each component can subscribe (with the `useContext`
* hook) to its state, and will be updated whenever the context state is updated. * hook) to its state, and will be updated whenever the context state is updated.
*/ */
function partitionBy<T>(arr: T[], fn: (t: T) => boolean) {
let lastGroup: T[] | false = false;
let lastValue;
return arr.reduce((acc: T[][], cur) => {
let curVal = fn(cur);
if (lastGroup) {
if (curVal === lastValue) {
lastGroup.push(cur);
} else {
lastGroup = false;
}
}
if (!lastGroup) {
lastGroup = [cur];
acc.push(lastGroup);
}
lastValue = curVal;
return acc;
}, []);
}
export class Context extends EventBus { export class Context extends EventBus {
state: any; state: any;
observer: Observer; observer: Observer;
@@ -22,40 +45,52 @@ export class Context extends EventBus {
constructor(state: Object = {}) { constructor(state: Object = {}) {
super(); super();
this.observer = new Observer(); this.observer = new Observer();
this.observer.notifyCB = this.__notifyComponents.bind(this); this.observer.notifyCB = () => {
// notify components in the next microtask tick to ensure that subscribers
// are notified only once for all changes that occur in the same micro tick
let rev = this.rev;
return Promise.resolve().then(() => {
if (rev === this.rev) {
this.__notifyComponents();
}
});
};
this.state = this.observer.observe(state); this.state = this.observer.observe(state);
this.subscriptions.update = [];
} }
/** /**
* Instead of using trigger to emit an update event, we actually implement * Instead of using trigger to emit an update event, we actually implement
* our own function to do that. The reason is that we need to be smarter than * our own function to do that. The reason is that we need to be smarter than
* a simple trigger function: we need to wait for parent components to be * a simple trigger function: we need to wait for parent components to be
* done before doing children components. The reason is that if an update * done before doing children components. More precisely, if an update
* as an effect of destroying a children, we do not want to call the * as an effect of destroying a children, we do not want to call any code
* mapStoreToProps function of the child, nor rendering it. * from the child, and certainly not render it.
* *
* This method is not optimal if we have a bunch of asynchronous components: * This method implements a simple grouping algorithm by depth. If we have
* we wait sequentially for each component to be completed before updating the * connected components of depths [2, 4,4,4,4, 3,8,8], the Context will notify
* next. However, the only things that matters is that children are updated * them in the following groups: [2], [4,4,4,4], [3], [8,8]. Each group will
* after their parents. So, this could be optimized by being smarter, and * be updated sequentially, but each components in a given group will be done in
* updating all widgets concurrently, except for parents/children. * parallel.
* *
* A potential cheap way to improve this situation is to keep track of the * This is a very simple algorithm, but it avoids checking if a given
* depth of a component in the component tree. A root component has a depth of * component is a child of another.
* 1, then its children of 2 and so on... Then, we can update all components
* with the same depth in parallel.
*/ */
async __notifyComponents() { async __notifyComponents() {
const rev = ++this.rev; const rev = ++this.rev;
const subs = this.subscriptions.update || []; const subscriptions = this.subscriptions.update;
for (let i = 0, iLen = subs.length; i < iLen; i++) { const groups = partitionBy(subscriptions, s => (s.owner ? s.owner.__owl__.depth : -1));
const sub = subs[i]; for (let group of groups) {
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true; const proms = group.map(sub => sub.callback.call(sub.owner, rev));
if (shouldCallback) { // at this point, each component in the current group has registered a
const render = sub.callback.call(sub.owner, rev); // top level fiber in the scheduler. It could happen that rendering these
// components is done (if they have no children). This is why we manually
// flush the scheduler. This will force the scheduler to check
// immediately if they are done, which will cause their rendering
// promise to resolve earlier, which means that there is a chance of
// processing the next group in the same frame.
scheduler.flush(); scheduler.flush();
await render; await Promise.all(proms);
}
} }
} }
} }
@@ -103,9 +138,11 @@ export function useContextWithCB(ctx: Context, component: Component<any, any>, m
await method(); await method();
} }
}); });
onWillUnmount(() => { const __destroy = component.__destroy;
component.__destroy = (parent) => {
ctx.off("update", component); ctx.off("update", component);
delete mapping[id]; delete mapping[id];
}); __destroy.call(component, parent);
}
return ctx.state; return ctx.state;
} }
+3 -17
View File
@@ -24,14 +24,6 @@ export class Observer {
weakMap: WeakMap<any, any> = new WeakMap(); weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {} notifyCB() {}
async notifyChange() {
this.dirty = true;
await Promise.resolve();
if (this.dirty) {
this.dirty = false;
this.notifyCB();
}
}
observe<T>(value: T, parent?: any): T { observe<T>(value: T, parent?: any): T {
if (value === null || typeof value !== "object" || value instanceof Date) { if (value === null || typeof value !== "object" || value instanceof Date) {
@@ -46,10 +38,6 @@ export class Observer {
const metadata = this.weakMap.get(value); const metadata = this.weakMap.get(value);
return metadata ? metadata.rev : 0; return metadata ? metadata.rev : 0;
} }
deepRevNumber(value): number {
const metadata = this.weakMap.get(value);
return metadata ? metadata.deepRev : 0;
}
_observe(value, parent) { _observe(value, parent) {
var self = this; var self = this;
@@ -69,7 +57,7 @@ export class Observer {
} }
self._updateRevNumber(target); self._updateRevNumber(target);
target[key] = newVal; target[key] = newVal;
self.notifyChange(); self.notifyCB();
} }
return true; return true;
}, },
@@ -77,7 +65,7 @@ export class Observer {
if (key in target) { if (key in target) {
delete target[key]; delete target[key];
self._updateRevNumber(target); self._updateRevNumber(target);
self.notifyChange(); self.notifyCB();
} }
return true; return true;
} }
@@ -87,7 +75,6 @@ export class Observer {
value, value,
proxy, proxy,
rev: this.rev, rev: this.rev,
deepRev: this.rev,
parent parent
}; };
@@ -99,11 +86,10 @@ export class Observer {
_updateRevNumber(target: any) { _updateRevNumber(target: any) {
this.rev++; this.rev++;
let metadata = this.weakMap.get(target); let metadata = this.weakMap.get(target);
metadata.rev!++;
let parent = target; let parent = target;
do { do {
metadata = this.weakMap.get(parent); metadata = this.weakMap.get(parent);
metadata.deepRev++; metadata.rev++;
} while ((parent = metadata.parent) && parent !== target); } while ((parent = metadata.parent) && parent !== target);
} }
} }
+15
View File
@@ -0,0 +1,15 @@
import { Component } from "../component/component";
/**
* We define here OwlEvent, a subclass of CustomEvent, with an additional
* attribute:
* - originalComponent: the component that triggered the event
*/
export class OwlEvent<T> extends CustomEvent<T> {
originalComponent: Component<any, any>;
constructor(component, eventType, options) {
super(eventType, options);
this.originalComponent = component;
}
}
+6 -1
View File
@@ -104,7 +104,12 @@ export function useRef(name: string): Ref {
return { return {
get el(): HTMLElement | null { get el(): HTMLElement | null {
const val = __owl__.refs && __owl__.refs[name]; const val = __owl__.refs && __owl__.refs[name];
return val instanceof HTMLElement ? val : null; if (val instanceof HTMLElement) {
return val;
} else if (val instanceof Component) {
return val.el;
}
return null;
}, },
get comp(): Component<any, any> | null { get comp(): Component<any, any> | null {
const val = __owl__.refs && __owl__.refs[name]; const val = __owl__.refs && __owl__.refs[name];
+2 -17
View File
@@ -7,6 +7,7 @@
import { EventBus } from "./core/event_bus"; import { EventBus } from "./core/event_bus";
import { Observer } from "./core/observer"; import { Observer } from "./core/observer";
import { QWeb } from "./qweb/index"; import { QWeb } from "./qweb/index";
import { config } from "./config";
import * as _store from "./store"; 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";
@@ -19,6 +20,7 @@ import { Router } from "./router/router";
export { Component } from "./component/component"; export { Component } from "./component/component";
export { QWeb }; export { QWeb };
export { config };
export const Context = _context.Context; export const Context = _context.Context;
export const useState = _hooks.useState; export const useState = _hooks.useState;
@@ -35,20 +37,3 @@ export const hooks = Object.assign({}, _hooks, {
useStore: _store.useStore useStore: _store.useStore
}); });
export const __info__ = {}; export const __info__ = {};
Object.defineProperty(__info__, "mode", {
get() {
return QWeb.dev ? "dev" : "prod";
},
set(mode: string) {
QWeb.dev = mode === "dev";
if (QWeb.dev) {
const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
console.warn(
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
);
} else {
console.log(`Owl is now running in 'prod' mode.`);
}
}
});
+18 -14
View File
@@ -1,5 +1,4 @@
import { CompilationContext } from "./compilation_context"; import { CompilationContext } from "./compilation_context";
import { QWebExprVar } from "./expression_parser";
import { QWeb } from "./qweb"; import { QWeb } from "./qweb";
import { htmlToVDOM } from "../vdom/html_to_vdom"; import { htmlToVDOM } from "../vdom/html_to_vdom";
@@ -34,7 +33,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
return; return;
} }
if (value.xml instanceof NodeList) { if (value.xml instanceof NodeList && !value.id) {
for (let node of Array.from(value.xml)) { for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx); qweb._compileNode(<ChildNode>node, ctx);
} }
@@ -71,6 +70,12 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
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();
} }
@@ -104,22 +109,21 @@ QWeb.addDirective({
atNodeEncounter({ node, ctx }): boolean { atNodeEncounter({ node, ctx }): boolean {
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] || {};
let qwebvar = ctx.variables[variable];
if (value) { if (value) {
const formattedValue = ctx.formatExpression(value); const formattedValue = ctx.formatExpression(value);
if (ctx.variables.hasOwnProperty(variable)) { if (ctx.variables.hasOwnProperty(variable) && qwebvar.id) {
ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`); ctx.addLine(`${qwebvar.id} = ${formattedValue}`);
} else { } else {
const varName = `_${ctx.generateID()}`; const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`); ctx.addLine(`var ${varName} = ${formattedValue};`);
ctx.variables[variable] = { qwebvar.id = varName;
id: varName, qwebvar.expr = formattedValue;
expr: formattedValue
};
} }
} else { } else {
ctx.variables[variable] = { qwebvar.xml = node.childNodes;
xml: node.childNodes
};
} }
return true; return true;
} }
@@ -133,7 +137,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) : cond.id!);
return false; return false;
}, },
finalize({ ctx }) { finalize({ ctx }) {
@@ -244,8 +248,8 @@ QWeb.addDirective({
// add new variables, if any // add new variables, if any
for (let key in tempCtx.variables) { for (let key in tempCtx.variables) {
const v = tempCtx.variables[key]; const v = tempCtx.variables[key];
if ((<QWebExprVar>v).expr) { if (v.expr) {
ctx.addLine(`let ${(<QWebExprVar>v).id} = ${(<QWebExprVar>v).expr};`); ctx.addLine(`let ${v.id} = ${v.expr};`);
} }
// todo: handle XML variables... // todo: handle XML variables...
} }
+26 -2
View File
@@ -1,4 +1,4 @@
import { compileExpr, QWebVar, QWebExprVar } from "./expression_parser"; import { compileExpr, QWebVar } from "./expression_parser";
export const INTERP_REGEXP = /\{\{.*?\}\}/g; export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -47,6 +47,30 @@ export class CompilationContext {
return id; return id;
} }
/**
* This method generates a "template key", which is basically a unique key
* which depends on the currently set keys, and on the iteration numbers (if
* we are in a loop).
*
* Such a key is necessary when we need to associate an id to some element
* generated by a template (for example, a component)
*/
generateTemplateKey(): string {
const id = this.generateID();
let locationExpr = `\`__${this.generateID()}__`;
for (let i = 0; i < this.loopNumber - 1; i++) {
locationExpr += `\${i${i + 1}}__`;
}
if (this.lastNodeKey || this.currentKey) {
const k = this.lastNodeKey || this.currentKey;
this.addLine(`let k${id} = ${locationExpr}\` + ${k};`);
} else {
locationExpr += this.loopNumber ? `\${i${this.loopNumber}}__\`` : "`";
this.addLine(`let k${id} = ${locationExpr};`);
}
return `k${id}`;
}
generateCode(): string[] { generateCode(): string[] {
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length; const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
if (shouldTrackScope) { if (shouldTrackScope) {
@@ -149,7 +173,7 @@ export class CompilationContext {
this.addLine("}"); this.addLine("}");
} }
getValue(val: any): QWebExprVar | 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;
} }
+11 -14
View File
@@ -25,7 +25,7 @@
// Misc types, constants and helpers // Misc types, constants and helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split( const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
"," ","
); );
@@ -38,17 +38,12 @@ const WORD_REPLACEMENT = {
lte: "<=" lte: "<="
}; };
export interface QWebExprVar { export interface QWebVar {
id: string; id?: string;
expr: string; expr?: string;
xml?: NodeList;
} }
export interface QWebXMLVar {
xml: NodeList;
}
export type QWebVar = QWebExprVar | QWebXMLVar;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Tokenizer // Tokenizer
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -82,7 +77,9 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
")": "RIGHT_PAREN" ")": "RIGHT_PAREN"
}; };
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(","); // note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ".split(",");
type Tokenizer = (expr: string) => Token | false; type Tokenizer = (expr: string) => Token | false;
@@ -165,9 +162,9 @@ const tokenizeOperator: Tokenizer = function(expr) {
const TOKENIZERS = [ const TOKENIZERS = [
tokenizeString, tokenizeString,
tokenizeNumber, tokenizeNumber,
tokenizeOperator,
tokenizeSymbol, tokenizeSymbol,
tokenizeStatic, tokenizeStatic
tokenizeOperator
]; ];
/** /**
@@ -252,7 +249,7 @@ export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): str
} }
if (isVar) { if (isVar) {
if (token.value in vars && "id" in vars[token.value]) { if (token.value in vars && "id" in vars[token.value]) {
token.value = (<QWebExprVar>vars[token.value]).id; token.value = vars[token.value].id!;
} else { } else {
token.value = `context['${token.value}']`; token.value = `context['${token.value}']`;
} }
+13 -5
View File
@@ -231,7 +231,17 @@ QWeb.addDirective({
const type = node.getAttribute("type"); const type = node.getAttribute("type");
let handler; let handler;
let event = fullName.includes(".lazy") ? "change" : "input"; let event = fullName.includes(".lazy") ? "change" : "input";
const expr = ctx.formatExpression(value);
// we keep here a reference to the "base expression" (if the expression
// is `t-model="some.expr.value", then the base expression is "some.expr").
// This is necessary so we can capture it in the handler closure.
let expr = ctx.formatExpression(value);
const index = expr.lastIndexOf(".");
const baseExpr = expr.slice(0, index);
ctx.addLine(`let expr${nodeID} = ${baseExpr};`);
expr = `expr${nodeID}.${expr.slice(index + 1)}`;
const key = ctx.generateTemplateKey();
if (node.tagName === "select") { if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`); ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
addNodeHook("create", `n.elm.value=${expr};`); addNodeHook("create", `n.elm.value=${expr};`);
@@ -255,10 +265,8 @@ QWeb.addDirective({
} }
handler = `(ev) => {${expr} = ${valueCode}}`; handler = `(ev) => {${expr} = ${valueCode}}`;
} }
ctx.addLine( ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`);
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});` ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`);
);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
} }
}); });
+2
View File
@@ -460,6 +460,8 @@ export class QWeb extends EventBus {
// this is a component, we modify in place the xml document to change // this is a component, we modify in place the xml document to change
// <SomeComponent ... /> to <t t-component="SomeComponent" ... /> // <SomeComponent ... /> to <t t-component="SomeComponent" ... />
node.setAttribute("t-component", node.tagName); node.setAttribute("t-component", node.tagName);
} else if (node.tagName !== 't' && node.hasAttribute('t-component')) {
throw new Error(`Directive 't-component' can only be used on <t> nodes (used on a <${node.tagName}>)`);
} }
const attributes = (<Element>node).attributes; const attributes = (<Element>node).attributes;
+7 -5
View File
@@ -84,8 +84,11 @@ const isStrictEqual = (a, b) => a === b;
export function useStore(selector, options: SelectorOptions = {}): any { export function useStore(selector, options: SelectorOptions = {}): any {
const component: Component<any, any> = Component.current!; const component: Component<any, any> = Component.current!;
const store = options.store || (component.env.store as Store); const store = options.store || (component.env.store as Store);
if (!(store instanceof Store)) {
throw new Error(`No store found when connecting '${component.constructor.name}'`);
}
let result = selector(store.state, component.props); let result = selector(store.state, component.props);
const hashFn = store.observer.deepRevNumber.bind(store.observer); const hashFn = store.observer.revNumber.bind(store.observer);
let revNumber = hashFn(result) || result; let revNumber = hashFn(result) || result;
const isEqual = options.isEqual || isStrictEqual; const isEqual = options.isEqual || isStrictEqual;
if (!store.updateFunctions[component.__owl__.id]) { if (!store.updateFunctions[component.__owl__.id]) {
@@ -108,15 +111,14 @@ export function useStore(selector, options: SelectorOptions = {}): any {
useContextWithCB(store, component, function(): Promise<void> | void { useContextWithCB(store, component, function(): Promise<void> | void {
let shouldRender = false; let shouldRender = false;
updateFunctions.forEach(function(updateFn) { for (let fn of updateFunctions) {
shouldRender = updateFn() || shouldRender; shouldRender = fn() || shouldRender;
}); }
if (shouldRender) { if (shouldRender) {
return component.render(); return component.render();
} }
}); });
onWillUpdateProps(props => { onWillUpdateProps(props => {
// FIXME: only do that if not keepalive + do it in destroy in that case
delete store.updateFunctions[component.__owl__.id]; delete store.updateFunctions[component.__owl__.id];
result = selector(store.state, props); result = selector(store.state, props);
}); });
-2
View File
@@ -20,10 +20,8 @@ function htmlToVNode(node: ChildNode): VNode {
attrs[attr.name] = attr.textContent; attrs[attr.name] = attr.textContent;
} }
const children: VNode[] = []; const children: VNode[] = [];
if (node.hasChildNodes) {
for (let c of node.childNodes) { for (let c of node.childNodes) {
children.push(htmlToVNode(c)); children.push(htmlToVNode(c));
} }
}
return h((node as Element).tagName, { attrs }, children); return h((node as Element).tagName, { attrs }, children);
} }
+12 -10
View File
@@ -12,8 +12,8 @@ exports[`animations t-transition combined with component 1`] = `
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
//COMPONENT //COMPONENT
let templateId3 = \`__4__\`; let k4 = \`__5__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false; let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props3 = {}; let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) { if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy(); w3.destroy();
@@ -28,17 +28,18 @@ exports[`animations t-transition combined with component 1`] = `
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child']; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3); w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id; parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => { let pvnode = h('dummy', {key: k4, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w3.destroy(); w3.destroy();
}; };
utils.transitionRemove(vn, 'chimay', finalize);}}}); utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber; const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode); c1.push(pvnode);
w3.__owl__.pvnode = pvnode; w3.__owl__.pvnode = pvnode;
} }
w3.__owl__.parentLastFiberId = extra.fiber.id;
sibling = w3.__owl__.currentFiber || sibling; sibling = w3.__owl__.currentFiber || sibling;
return vn1; return vn1;
}" }"
@@ -57,8 +58,8 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
if (context['state'].display) { if (context['state'].display) {
//COMPONENT //COMPONENT
let templateId3 = \`__4__\`; let k4 = \`__5__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false; let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props3 = {}; let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) { if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy(); w3.destroy();
@@ -73,17 +74,18 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child']; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3); w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id; parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => { let pvnode = h('dummy', {key: k4, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w3.destroy(); w3.destroy();
}; };
utils.transitionRemove(vn, 'chimay', finalize);}}}); utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber; const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode); c1.push(pvnode);
w3.__owl__.pvnode = pvnode; w3.__owl__.pvnode = pvnode;
} }
w3.__owl__.parentLastFiberId = extra.fiber.id;
sibling = w3.__owl__.currentFiber || sibling; sibling = w3.__owl__.currentFiber || sibling;
} }
return vn1; return vn1;
+9 -8
View File
@@ -29,6 +29,7 @@ let cssEl: HTMLElement;
beforeEach(() => { beforeEach(() => {
fixture = makeTestFixture(); fixture = makeTestFixture();
env = makeTestEnv(); env = makeTestEnv();
Component.env = env;
qweb = new QWeb(); qweb = new QWeb();
}); });
@@ -108,7 +109,7 @@ describe("animations", () => {
class TestWidget extends Widget { class TestWidget extends Widget {
state = useState({ hide: false }); state = useState({ hide: false });
} }
const widget = new TestWidget(env); const widget = new TestWidget();
// insert widget into the DOM // insert widget into the DOM
let def = makeDeferred(); let def = makeDeferred();
@@ -151,7 +152,7 @@ describe("animations", () => {
state = useState({ hide: false }); state = useState({ hide: false });
span = useRef("span"); span = useRef("span");
} }
const widget = new TestWidget(env); const widget = new TestWidget();
// insert widget into the DOM // insert widget into the DOM
let def = makeDeferred(); let def = makeDeferred();
@@ -180,7 +181,7 @@ describe("animations", () => {
class Parent extends Widget { class Parent extends Widget {
static components = { Child: Child }; static components = { Child: Child };
} }
const widget = new Parent(env); const widget = new Parent();
let def = makeDeferred(); let def = makeDeferred();
var spanNode; var spanNode;
@@ -220,7 +221,7 @@ describe("animations", () => {
static components = { Child: Child }; static components = { Child: Child };
state = useState({ display: true }); state = useState({ display: true });
} }
const widget = new Parent(env); const widget = new Parent();
let def = makeDeferred(); let def = makeDeferred();
var spanNode; var spanNode;
@@ -251,11 +252,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="__4__">blue</span></div>' '<div><span class="chimay-leave chimay-leave-active" data-owl-key="__5__">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="__4__">blue</span></div>' '<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__5__">blue</span></div>'
); );
def.resolve(); def.resolve();
}); });
@@ -283,7 +284,7 @@ describe("animations", () => {
} }
} }
const widget = new Parent(env); const widget = new Parent();
await widget.mount(fixture); await widget.mount(fixture);
let button = widget.el!.querySelector("button"); let button = widget.el!.querySelector("button");
@@ -341,7 +342,7 @@ describe("animations", () => {
} }
} }
const widget = new Parent(env); const widget = new Parent();
await widget.mount(fixture); await widget.mount(fixture);
let button = widget.el!.querySelector("button"); let button = widget.el!.querySelector("button");
File diff suppressed because it is too large Load Diff
@@ -12,8 +12,8 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
//COMPONENT //COMPONENT
let templateId3 = \`__4__\`; let k4 = \`__5__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false; let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props3 = {message:1}; let props3 = {message:1};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) { if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy(); w3.destroy();
@@ -28,14 +28,15 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child']; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3); w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id; parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); let pvnode = h('dummy', {key: k4, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});
const fiber = w3.__owl__.currentFiber; const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode); c1.push(pvnode);
w3.__owl__.pvnode = pvnode; w3.__owl__.pvnode = pvnode;
} }
w3.__owl__.parentLastFiberId = extra.fiber.id;
sibling = w3.__owl__.currentFiber || sibling; sibling = w3.__owl__.currentFiber || sibling;
return vn1; return vn1;
}" }"
File diff suppressed because it is too large Load Diff
+433 -101
View File
@@ -15,6 +15,7 @@ let dev: boolean = false;
beforeEach(() => { beforeEach(() => {
fixture = makeTestFixture(); fixture = makeTestFixture();
env = makeTestEnv(); env = makeTestEnv();
Component.env = env;
dev = QWeb.dev; dev = QWeb.dev;
QWeb.dev = true; QWeb.dev = true;
}); });
@@ -35,17 +36,32 @@ describe("props validation", () => {
static props = ["message"]; static props = ["message"];
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
} }
class Parent extends Widget {
static components = { TestWidget };
static template = xml`<div><TestWidget /></div>`;
}
let error;
QWeb.dev = true; QWeb.dev = true;
expect(() => { try {
new TestWidget(env); const p = new Parent();
}).toThrow(); await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'message' (component 'TestWidget')`);
error = undefined;
QWeb.dev = false; QWeb.dev = false;
try {
expect(() => { const p = new Parent();
new TestWidget(env); await p.mount(fixture);
}).not.toThrow(); } catch (e) {
error = e;
}
expect(error).toBeUndefined();
}); });
test("props: list of strings", async () => { test("props: list of strings", async () => {
@@ -53,10 +69,20 @@ describe("props validation", () => {
static props = ["message"]; static props = ["message"];
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
} }
class Parent extends Widget {
static components = { TestWidget };
static template = xml`<div><TestWidget /></div>`;
}
expect(() => { let error;
new TestWidget(env); try {
}).toThrow("Missing props 'message' (component 'TestWidget')"); const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'message' (component 'TestWidget')`);
}); });
test("validate simple types", async () => { test("validate simple types", async () => {
@@ -69,23 +95,50 @@ describe("props validation", () => {
{ type: Function, ok: () => {}, ko: "1" } { type: Function, ok: () => {}, ko: "1" }
]; ];
let props;
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
get p() {
return props.p;
}
}
for (let test of Tests) { for (let test of Tests) {
let TestWidget = class extends Widget { let TestWidget = class extends Widget {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
static props = { p: test.type }; static props = { p: test.type };
}; };
Parent.components = { TestWidget };
expect(() => { let error;
new TestWidget(env); props = {};
}).toThrow("Missing props 'p'"); try {
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'p' (component '_a')`);
expect(() => { error = undefined;
new TestWidget(env, { p: test.ok }); props = { p: test.ok };
}).not.toThrow(); try {
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => { props = { p: test.ko };
new TestWidget(env, { p: test.ko }); try {
}).toThrow("Props 'p' of invalid type in component"); const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component '_a'`);
} }
}); });
@@ -99,119 +152,306 @@ describe("props validation", () => {
{ type: Function, ok: () => {}, ko: "1" } { type: Function, ok: () => {}, ko: "1" }
]; ];
let props;
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
get p() {
return props.p;
}
}
for (let test of Tests) { for (let test of Tests) {
let TestWidget = class extends Widget { let TestWidget = class extends Component<any, any> {
static template = xml`<div>hey</div>`;
static props = { p: { type: test.type } }; static props = { p: { type: test.type } };
static template = xml`<div>hey</div>`;
}; };
Parent.components = { TestWidget };
expect(() => { let error;
new TestWidget(env); props = {};
}).toThrow("Missing props 'p'"); try {
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'p' (component '_a')`);
expect(() => { error = undefined;
new TestWidget(env, { p: test.ok }); props = { p: test.ok };
}).not.toThrow(); try {
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => { props = { p: test.ko };
new TestWidget(env, { p: test.ko }); try {
}).toThrow("Props 'p' of invalid type in component"); const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component '_a'`);
} }
}); });
test("can validate a prop with multiple types", async () => { test("can validate a prop with multiple types", async () => {
let TestWidget = class extends Widget { class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
static props = { p: [String, Boolean] }; static props = { p: [String, Boolean] };
}; }
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
}
expect(() => { let error;
new TestWidget(env, { p: "string" }); let props;
new TestWidget(env, { p: true }); try {
}).not.toThrow(); props = { p: "string" };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => { try {
new TestWidget(env, { p: 1 }); props = { p: true };
}).toThrow("Props 'p' of invalid type in component"); const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
try {
props = { p: 1 };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Props 'p' of invalid type in component 'TestWidget'");
}); });
test("can validate an optional props", async () => { test("can validate an optional props", async () => {
let TestWidget = class extends Widget { class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
static props = { p: { type: String, optional: true } }; static props = { p: { type: String, optional: true } };
}; }
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
}
expect(() => { let error;
new TestWidget(env, { p: "hey" }); let props;
new TestWidget(env, {}); try {
}).not.toThrow(); props = { p: "key" };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => { try {
new TestWidget(env, { p: 1 }); props = {};
}).toThrow(); const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
try {
props = { p: 1 };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`);
}); });
test("can validate an array with given primitive type", async () => { test("can validate an array with given primitive type", async () => {
let TestWidget = class extends Widget { class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
static props = { p: { type: Array, element: String } }; static props = { p: { type: Array, element: String } };
}; }
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
}
expect(() => { let error;
new TestWidget(env, { p: [] }); let props;
new TestWidget(env, { p: ["string"] }); try {
}).not.toThrow(); props = { p: [] };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => { try {
new TestWidget(env, { p: [1] }); props = { p: ["string"] };
}).toThrow(); const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => { try {
new TestWidget(env, { p: ["string", 1] }); props = { p: [1] };
}).toThrow(); const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
error = undefined;
try {
props = { p: ["string", 1] };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
}); });
test("can validate an array with multiple sub element types", async () => { test("can validate an array with multiple sub element types", async () => {
let TestWidget = class extends Widget { class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
static props = { p: { type: Array, element: [String, Boolean] } }; static props = { p: { type: Array, element: [String, Boolean] } };
}; }
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
}
expect(() => { let error;
new TestWidget(env, { p: [] }); let props;
new TestWidget(env, { p: ["string"] }); try {
new TestWidget(env, { p: [false, true, "string"] }); props = { p: [] };
}).not.toThrow(); const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => { try {
new TestWidget(env, { p: [true, 1] }); props = { p: ["string"] };
}).toThrow(); const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
try {
props = { p: [false, true, "string"] };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
try {
props = { p: [true, 1] };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`);
}); });
test("can validate an object with simple shape", async () => { test("can validate an object with simple shape", async () => {
let TestWidget = class extends Widget { class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
static props = { static props = {
p: { type: Object, shape: { id: Number, url: String } } p: { type: Object, shape: { id: Number, url: String } }
}; };
}; }
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
}
expect(() => { let error;
new TestWidget(env, { p: { id: 1, url: "url" } }); let props;
new TestWidget(env, { p: { id: 1, url: "url", extra: true } }); try {
}).not.toThrow(); props = { p: { id: 1, url: "url" } };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => { try {
new TestWidget(env, { p: { id: "1", url: "url" } }); props = { p: { id: 1, url: "url", extra: true } };
}).toThrow(); const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid prop 'p' in component TestWidget (unknown prop 'extra')");
expect(() => { try {
new TestWidget(env, { p: { id: 1 } }); props = { p: { id: "1", url: "url" } };
}).toThrow(); const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`);
error = undefined;
try {
props = { p: { id: 1 } };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`);
}); });
test("can validate recursively complicated prop def", async () => { test("can validate recursively complicated prop def", async () => {
let TestWidget = class extends Widget { class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
static props = { static props = {
p: { p: {
@@ -222,16 +462,77 @@ describe("props validation", () => {
} }
} }
}; };
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
}
let error;
let props;
try {
props = { p: { id: 1, url: true } };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
try {
props = { p: { id: 1, url: [12] } };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
try {
props = { p: { id: 1, url: [12, true] } };
const p = new Parent();
await p.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`);
});
test("can validate optional attributes in nested sub props", () => {
class TestComponent extends Component<any, any> {
static props = {
myprop: {
type: Array,
element: {
type: Object,
shape: {
num: { type: Number, optional: true }
}
}
}
}; };
}
let error;
try {
QWeb.utils.validateProps(TestComponent, { myprop: [{}] });
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(() => { try {
new TestWidget(env, { p: { id: 1, url: true } }); QWeb.utils.validateProps(TestComponent, { myprop: [{ a: 1 }] });
new TestWidget(env, { p: { id: 1, url: [12] } }); } catch (e) {
}).not.toThrow(); error = e;
}
expect(() => { expect(error).toBeDefined();
new TestWidget(env, { p: { id: 1, url: [12, true] } }); expect(error.message).toBe(
}).toThrow(); "Invalid prop 'myprop' in component TestComponent (unknown prop 'a')"
);
}); });
test("props are validated in dev mode (code snapshot)", async () => { test("props are validated in dev mode (code snapshot)", async () => {
@@ -249,7 +550,7 @@ describe("props validation", () => {
class App extends Widget { class App extends Widget {
static components = { Child }; static components = { Child };
} }
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>"); expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
// need to make sure there are 2 call to update props. one at component // need to make sure there are 2 call to update props. one at component
@@ -280,6 +581,32 @@ describe("props validation", () => {
}).toThrow(); }).toThrow();
}); });
test("props with type array, and no element", async () => {
class TestWidget extends Widget {
static props = { myprop: { type: Array } };
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { myprop: [1] });
}).not.toThrow();
expect(() => {
QWeb.utils.validateProps(TestWidget, { myprop: 1 });
}).toThrow(`Props 'myprop' of invalid type in component 'TestWidget'`);
});
test("props with type object, and no shape", async () => {
class TestWidget extends Widget {
static props = { myprop: { type: Object } };
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { myprop: { a: 3 } });
}).not.toThrow();
expect(() => {
QWeb.utils.validateProps(TestWidget, { myprop: false });
}).toThrow(`Props 'myprop' of invalid type in component 'TestWidget'`);
});
test("props: extra props cause an error", async () => { test("props: extra props cause an error", async () => {
class TestWidget extends Widget { class TestWidget extends Widget {
static props = ["message"]; static props = ["message"];
@@ -334,7 +661,7 @@ describe("props validation", () => {
static components = { TestWidget }; static components = { TestWidget };
} }
const w = new App(env, {}); const w = new App(undefined, {});
let error; let error;
try { try {
await w.mount(fixture); await w.mount(fixture);
@@ -365,7 +692,7 @@ describe("props validation", () => {
state: any = useState({ p: 1 }); state: any = useState({ p: 1 });
} }
const w = new Parent(env); const w = new Parent();
await w.mount(fixture); await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>"); expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
@@ -387,7 +714,7 @@ describe("props validation", () => {
state: any = useState({ p: 1 }); state: any = useState({ p: 1 });
} }
const w = new Parent(env); const w = new Parent();
await w.mount(fixture); await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>"); expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
@@ -399,13 +726,18 @@ describe("props validation", () => {
describe("default props", () => { describe("default props", () => {
test("can set default values", async () => { test("can set default values", async () => {
class TestWidget extends Widget { class TestWidget extends Component<any, any> {
static defaultProps = { p: 4 }; static defaultProps = { p: 4 };
static template = xml`<div>hey</div>`; static template = xml`<div><t t-esc="props.p"/></div>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget /></div>`;
static components = { TestWidget };
} }
const w = new TestWidget(env, {}); const w = new Parent();
expect(w.props.p).toBe(4); await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
}); });
test("default values are also set whenever component is updated", async () => { test("default values are also set whenever component is updated", async () => {
@@ -419,7 +751,7 @@ describe("default props", () => {
state: any = useState({ p: 1 }); state: any = useState({ p: 1 });
} }
const w = new Parent(env); const w = new Parent();
await w.mount(fixture); await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>"); expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
@@ -440,7 +772,7 @@ describe("default props", () => {
static components = { TestWidget }; static components = { TestWidget };
} }
const w = new App(env, {}); const w = new App(undefined, {});
await w.mount(fixture); await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>heyhey</span></div>"); expect(fixture.innerHTML).toBe("<div><span>heyhey</span></div>");
}); });
+126 -11
View File
@@ -1,5 +1,5 @@
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick } from "./helpers"; import { makeDeferred, makeTestEnv, makeTestFixture, nextTick } from "./helpers";
import { Component, Env } from "../src/component/component"; import { Component } from "../src/component/component";
import { Context, useContext } from "../src/context"; import { Context, useContext } from "../src/context";
import { xml } from "../src/tags"; import { xml } from "../src/tags";
import { useState } from "../src/hooks"; import { useState } from "../src/hooks";
@@ -11,14 +11,13 @@ import { useState } from "../src/hooks";
// We create before each test: // We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom // - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test. // manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components // - a test env, necessary to create components, that is set as env
let fixture: HTMLElement; let fixture: HTMLElement;
let env: Env;
beforeEach(() => { beforeEach(() => {
fixture = makeTestFixture(); fixture = makeTestFixture();
env = makeTestEnv(); Component.env = makeTestEnv();
}); });
afterEach(() => { afterEach(() => {
@@ -37,7 +36,7 @@ describe("Context", () => {
static template = xml`<div><t t-esc="contextObj.value"/></div>`; static template = xml`<div><t t-esc="contextObj.value"/></div>`;
contextObj = useContext(testContext); contextObj = useContext(testContext);
} }
const test = new Test(env); const test = new Test();
await test.mount(fixture); await test.mount(fixture);
expect(fixture.innerHTML).toBe("<div>123</div>"); expect(fixture.innerHTML).toBe("<div>123</div>");
}); });
@@ -49,7 +48,7 @@ describe("Context", () => {
static template = xml`<div><t t-esc="contextObj.value"/></div>`; static template = xml`<div><t t-esc="contextObj.value"/></div>`;
contextObj = useContext(testContext); contextObj = useContext(testContext);
} }
const test = new Test(env); const test = new Test();
await test.mount(fixture); await test.mount(fixture);
expect(fixture.innerHTML).toBe("<div>123</div>"); expect(fixture.innerHTML).toBe("<div>123</div>");
test.contextObj.value = 321; test.contextObj.value = 321;
@@ -68,7 +67,7 @@ describe("Context", () => {
static template = xml`<div><Child /><Child /></div>`; static template = xml`<div><Child /><Child /></div>`;
static components = { Child }; static components = { Child };
} }
const parent = new Parent(env); const parent = new Parent();
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>"); expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
testContext.state.value = 321; testContext.state.value = 321;
@@ -76,6 +75,93 @@ describe("Context", () => {
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>"); expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
}); });
test("two async components are updated in parallel", async () => {
const testContext = new Context({ value: 123 });
const def = makeDeferred();
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="contextObj.value"/></span>`;
contextObj = useContext(testContext);
async render() {
steps.push("render");
await def;
return super.render();
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child /><Child /></div>`;
static components = { Child };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
testContext.state.value = 321;
await nextTick();
expect(steps).toEqual(["render", "render"]);
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
});
test("two async components on two levels are updated (mostly) in parallel", async () => {
const testContext = new Context({ value: 123 });
const def = makeDeferred();
const steps: string[] = [];
class SlowComp extends Component<any, any> {
static template = xml`<p><t t-esc="props.value"/></p>`;
willUpdateProps() {
return def;
}
}
class Child extends Component<any, any> {
static template = xml`<span><SlowComp value="contextObj.value"/></span>`;
static components = { SlowComp };
contextObj = useContext(testContext);
render() {
steps.push("render");
return super.render();
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child /><Child /></div>`;
static components = { Child };
}
class App extends Component<any, any> {
static template = xml`<div><Child /><Parent /></div>`;
static components = { Child, Parent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><span><p>123</p></span><div><span><p>123</p></span><span><p>123</p></span></div></div>"
);
testContext.state.value = 321;
await nextTick();
expect(steps).toEqual(["render"]);
expect(fixture.innerHTML).toBe(
"<div><span><p>123</p></span><div><span><p>123</p></span><span><p>123</p></span></div></div>"
);
def.resolve();
await nextTick();
// we need to wait for an extra tick because it could happen (even though it
// is rare) that the second batch of renderings is not done yet, because
// the initial promise has been given to the macrotask queue, so a small
// delay happens.
await nextTick();
expect(steps).toEqual(["render", "render", "render"]);
expect(fixture.innerHTML).toBe(
"<div><span><p>321</p></span><div><span><p>321</p></span><span><p>321</p></span></div></div>"
);
});
test("one components can subscribe twice to same context", async () => { test("one components can subscribe twice to same context", async () => {
const testContext = new Context({ a: 1, b: 2 }); const testContext = new Context({ a: 1, b: 2 });
const steps: string[] = []; const steps: string[] = [];
@@ -93,7 +179,7 @@ describe("Context", () => {
static template = xml`<div><Child /></div>`; static template = xml`<div><Child /></div>`;
static components = { Child }; static components = { Child };
} }
const parent = new Parent(env); const parent = new Parent();
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>12</span></div>"); expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
expect(steps).toEqual(["child"]); expect(steps).toEqual(["child"]);
@@ -124,7 +210,7 @@ describe("Context", () => {
return super.__render(fiber); return super.__render(fiber);
} }
} }
const parent = new Parent(env); const parent = new Parent();
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span>321</div>"); expect(fixture.innerHTML).toBe("<div><span>123</span>321</div>");
expect(steps).toEqual(["parent", "child"]); expect(steps).toEqual(["parent", "child"]);
@@ -158,7 +244,7 @@ describe("Context", () => {
return super.__render(fiber); return super.__render(fiber);
} }
} }
const parent = new Parent(env); const parent = new Parent();
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span></div>"); expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
expect(steps).toEqual(["parent", "child"]); expect(steps).toEqual(["parent", "child"]);
@@ -174,6 +260,35 @@ describe("Context", () => {
expect(testContext.subscriptions.update.length).toBe(0); expect(testContext.subscriptions.update.length).toBe(0);
}); });
test("destroyed component before being mounted is inactive", async () => {
const testContext = new Context({ a: 123 });
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
contextObj = useContext(testContext);
willStart() {
return makeDeferred();
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: true });
}
const parent = new Parent();
const prom = parent.mount(fixture);
await nextTick(); // wait for Child to be instantiated
expect(testContext.subscriptions.update.length).toBe(1);
parent.state.flag = false;
await prom;
expect(fixture.innerHTML).toBe("<div></div>");
// kind of whitebox...
// we make sure we do not have any pending subscriptions to the 'update'
// event
expect(testContext.subscriptions.update.length).toBe(0);
});
test("concurrent renderings", async () => { test("concurrent renderings", async () => {
const testContext = new Context({ x: { n: 1 }, key: "x" }); const testContext = new Context({ x: { n: 1 }, key: "x" });
const def = makeDeferred(); const def = makeDeferred();
@@ -202,7 +317,7 @@ describe("Context", () => {
context = useContext(testContext); context = useContext(testContext);
} }
const component = new ComponentA(env); const component = new ComponentA();
await component.mount(fixture); await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>"); expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
+10 -44
View File
@@ -8,13 +8,11 @@ describe("observer", () => {
expect(typeof obj).toBe("object"); expect(typeof obj).toBe("object");
expect(observer.revNumber(obj)).toBe(1); expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1); expect(observer.rev).toBe(1);
const obj2: any = observer.observe({ a: 1 }); const obj2: any = observer.observe({ a: 1 });
expect(observer.revNumber(obj2)).toBe(1); expect(observer.revNumber(obj2)).toBe(1);
expect(observer.revNumber(obj)).toBe(1); expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1); expect(observer.rev).toBe(1);
obj2.a = 2; obj2.a = 2;
@@ -33,23 +31,19 @@ describe("observer", () => {
const obj: any = observer.observe({ a: null, b: undefined }); const obj: any = observer.observe({ a: null, b: undefined });
expect(observer.revNumber(obj)).toBe(1); expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1); expect(observer.rev).toBe(1);
obj.a = 3; obj.a = 3;
expect(observer.revNumber(obj)).toBe(2); expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
obj.b = 5; obj.b = 5;
expect(observer.revNumber(obj)).toBe(3); expect(observer.revNumber(obj)).toBe(3);
expect(observer.deepRevNumber(obj)).toBe(3);
expect(observer.rev).toBe(3); expect(observer.rev).toBe(3);
obj.a = null; obj.a = null;
obj.b = undefined; obj.b = undefined;
expect(observer.revNumber(obj)).toBe(5); expect(observer.revNumber(obj)).toBe(5);
expect(observer.deepRevNumber(obj)).toBe(5);
expect(observer.rev).toBe(5); expect(observer.rev).toBe(5);
expect(obj).toEqual({ expect(obj).toEqual({
a: null, a: null,
@@ -63,7 +57,6 @@ describe("observer", () => {
const obj: any = observer.observe({ date }); const obj: any = observer.observe({ date });
expect(observer.revNumber(obj)).toBe(1); expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1); expect(observer.rev).toBe(1);
expect(typeof obj.date.getFullYear()).toBe("number"); expect(typeof obj.date.getFullYear()).toBe("number");
expect(obj.date).toBe(date); expect(obj.date).toBe(date);
@@ -71,7 +64,6 @@ describe("observer", () => {
obj.date = new Date(); obj.date = new Date();
expect(observer.revNumber(obj)).toBe(2); expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
expect(obj.date).not.toBe(date); expect(obj.date).not.toBe(date);
}); });
@@ -82,14 +74,11 @@ describe("observer", () => {
expect(Array.isArray(obj.arr)).toBe(true); expect(Array.isArray(obj.arr)).toBe(true);
expect(observer.revNumber(obj.arr)).toBe(1); expect(observer.revNumber(obj.arr)).toBe(1);
expect(observer.deepRevNumber(obj.arr)).toBe(1);
expect(observer.rev).toBe(1); expect(observer.rev).toBe(1);
obj.arr[0] = "nope"; obj.arr[0] = "nope";
expect(observer.revNumber(obj.arr)).toBe(2); expect(observer.revNumber(obj.arr)).toBe(2);
expect(observer.deepRevNumber(obj.arr)).toBe(2); expect(observer.revNumber(obj)).toBe(2);
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
}); });
@@ -98,24 +87,20 @@ describe("observer", () => {
const obj: any = observer.observe({ a: 1 }); const obj: any = observer.observe({ a: 1 });
expect(observer.revNumber(obj)).toBe(1); expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1); expect(observer.rev).toBe(1);
obj.a = 2; obj.a = 2;
expect(observer.revNumber(obj)).toBe(2); expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
// same value again // same value again
obj.a = 2; obj.a = 2;
expect(observer.revNumber(obj)).toBe(2); expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
obj.a = 3; obj.a = 3;
expect(observer.revNumber(obj)).toBe(3); expect(observer.revNumber(obj)).toBe(3);
expect(observer.deepRevNumber(obj)).toBe(3);
expect(observer.rev).toBe(3); expect(observer.rev).toBe(3);
}); });
@@ -126,19 +111,16 @@ describe("observer", () => {
expect(Array.isArray(arr)).toBe(true); expect(Array.isArray(arr)).toBe(true);
expect(arr.length).toBe(0); expect(arr.length).toBe(0);
expect(observer.revNumber(arr)).toBe(1); expect(observer.revNumber(arr)).toBe(1);
expect(observer.deepRevNumber(arr)).toBe(1);
expect(observer.rev).toBe(1); expect(observer.rev).toBe(1);
arr.push(1); arr.push(1);
expect(observer.revNumber(arr)).toBe(2); expect(observer.revNumber(arr)).toBe(2);
expect(observer.deepRevNumber(arr)).toBe(2);
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
expect(arr.length).toBe(1); expect(arr.length).toBe(1);
expect(arr).toEqual([1]); expect(arr).toEqual([1]);
arr.splice(1, 0, "hey"); arr.splice(1, 0, "hey");
expect(observer.revNumber(arr)).toBe(3); expect(observer.revNumber(arr)).toBe(3);
expect(observer.deepRevNumber(arr)).toBe(3);
expect(observer.rev).toBe(3); expect(observer.rev).toBe(3);
expect(arr).toEqual([1, "hey"]); expect(arr).toEqual([1, "hey"]);
expect(arr.length).toBe(2); expect(arr.length).toBe(2);
@@ -146,7 +128,6 @@ describe("observer", () => {
arr.unshift("lindemans"); arr.unshift("lindemans");
//it generates 3 primitive operations //it generates 3 primitive operations
expect(observer.revNumber(arr)).toBe(6); expect(observer.revNumber(arr)).toBe(6);
expect(observer.deepRevNumber(arr)).toBe(6);
expect(observer.rev).toBe(6); expect(observer.rev).toBe(6);
expect(arr).toEqual(["lindemans", 1, "hey"]); expect(arr).toEqual(["lindemans", 1, "hey"]);
expect(arr.length).toBe(3); expect(arr.length).toBe(3);
@@ -154,21 +135,18 @@ describe("observer", () => {
arr.reverse(); arr.reverse();
//it generates 2 primitive operations //it generates 2 primitive operations
expect(observer.revNumber(arr)).toBe(8); expect(observer.revNumber(arr)).toBe(8);
expect(observer.deepRevNumber(arr)).toBe(8);
expect(observer.rev).toBe(8); expect(observer.rev).toBe(8);
expect(arr).toEqual(["hey", 1, "lindemans"]); expect(arr).toEqual(["hey", 1, "lindemans"]);
expect(arr.length).toBe(3); expect(arr.length).toBe(3);
arr.pop(); // one set, one delete arr.pop(); // one set, one delete
expect(observer.revNumber(arr)).toBe(10); expect(observer.revNumber(arr)).toBe(10);
expect(observer.deepRevNumber(arr)).toBe(10);
expect(observer.rev).toBe(10); expect(observer.rev).toBe(10);
expect(arr).toEqual(["hey", 1]); expect(arr).toEqual(["hey", 1]);
expect(arr.length).toBe(2); expect(arr.length).toBe(2);
arr.shift(); // 2 sets, 1 delete arr.shift(); // 2 sets, 1 delete
expect(observer.revNumber(arr)).toBe(13); expect(observer.revNumber(arr)).toBe(13);
expect(observer.deepRevNumber(arr)).toBe(13);
expect(observer.rev).toBe(13); expect(observer.rev).toBe(13);
expect(arr).toEqual([1]); expect(arr).toEqual([1]);
expect(arr.length).toBe(1); expect(arr.length).toBe(1);
@@ -187,8 +165,7 @@ describe("observer", () => {
arr[0].kriek = 6; arr[0].kriek = 6;
expect(observer.rev).toBe(3); expect(observer.rev).toBe(3);
expect(observer.revNumber(arr)).toBe(2); expect(observer.revNumber(arr)).toBe(3);
expect(observer.deepRevNumber(arr)).toBe(3);
expect(observer.revNumber(arr[0])).toBe(3); expect(observer.revNumber(arr[0])).toBe(3);
}); });
@@ -238,7 +215,6 @@ describe("observer", () => {
expect(observer.rev).toBe(1); expect(observer.rev).toBe(1);
expect(observer.revNumber(state)).toBe(1); expect(observer.revNumber(state)).toBe(1);
expect(observer.deepRevNumber(state)).toBe(1);
expect(observer.notifyCB).toBeCalledTimes(0); expect(observer.notifyCB).toBeCalledTimes(0);
state[1] = "b"; state[1] = "b";
@@ -247,7 +223,6 @@ describe("observer", () => {
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(2); expect(observer.revNumber(state)).toBe(2);
expect(observer.deepRevNumber(state)).toBe(2);
expect(observer.notifyCB).toBeCalledTimes(1); expect(observer.notifyCB).toBeCalledTimes(1);
expect(state).toEqual(["a", "b"]); expect(state).toEqual(["a", "b"]);
@@ -259,13 +234,11 @@ describe("observer", () => {
expect(observer.rev).toBe(1); expect(observer.rev).toBe(1);
expect(observer.revNumber(state.arr)).toBe(1); expect(observer.revNumber(state.arr)).toBe(1);
expect(observer.deepRevNumber(state.arr)).toBe(1);
expect(state.arr.length).toBe(0); expect(state.arr.length).toBe(0);
state.arr.push(1); state.arr.push(1);
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
expect(observer.revNumber(state.arr)).toBe(2); expect(observer.revNumber(state.arr)).toBe(2);
expect(observer.deepRevNumber(state.arr)).toBe(2);
expect(state.arr.length).toBe(1); expect(state.arr.length).toBe(1);
}); });
@@ -280,7 +253,7 @@ describe("observer", () => {
state.arr[0].something = 2; state.arr[0].something = 2;
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
expect(observer.revNumber(state.arr)).toBe(1); expect(observer.revNumber(state.arr)).toBe(2);
expect(observer.revNumber(state.arr[0])).toBe(2); expect(observer.revNumber(state.arr[0])).toBe(2);
}); });
@@ -294,7 +267,7 @@ describe("observer", () => {
state.a.b = 2; state.a.b = 2;
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(1); expect(observer.revNumber(state)).toBe(2);
expect(observer.revNumber(state.a)).toBe(2); expect(observer.revNumber(state.a)).toBe(2);
}); });
@@ -312,7 +285,7 @@ describe("observer", () => {
expect(observer.revNumber(obj.a)).toBe(2); expect(observer.revNumber(obj.a)).toBe(2);
obj.a.b = 3; obj.a.b = 3;
expect(observer.rev).toBe(3); expect(observer.rev).toBe(3);
expect(observer.revNumber(obj)).toBe(2); expect(observer.revNumber(obj)).toBe(3);
expect(observer.revNumber(obj.a)).toBe(3); expect(observer.revNumber(obj.a)).toBe(3);
}); });
@@ -320,22 +293,18 @@ describe("observer", () => {
const observer = new Observer(); const observer = new Observer();
const state: any = observer.observe({ o: { a: 1 }, arr: [1], n: 13 }); const state: any = observer.observe({ o: { a: 1 }, arr: [1], n: 13 });
expect(observer.revNumber(state)).toBe(1); expect(observer.revNumber(state)).toBe(1);
expect(observer.deepRevNumber(state)).toBe(1);
state.o.a = 2; state.o.a = 2;
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(1); expect(observer.revNumber(state)).toBe(2);
expect(observer.deepRevNumber(state)).toBe(2);
state.arr.push(2); state.arr.push(2);
expect(observer.rev).toBe(3); expect(observer.rev).toBe(3);
expect(observer.revNumber(state)).toBe(1); expect(observer.revNumber(state)).toBe(3);
expect(observer.deepRevNumber(state)).toBe(3);
state.n = 155; state.n = 155;
expect(observer.rev).toBe(4); expect(observer.rev).toBe(4);
expect(observer.revNumber(state)).toBe(2); expect(observer.revNumber(state)).toBe(4);
expect(observer.deepRevNumber(state)).toBe(4);
}); });
test("properly handle already observed state", () => { test("properly handle already observed state", () => {
@@ -361,18 +330,15 @@ describe("observer", () => {
const obj: any = observer.observe({}); const obj: any = observer.observe({});
expect(observer.revNumber(obj)).toBe(1); expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1); expect(observer.rev).toBe(1);
obj.aku = "always finds annoying problems"; obj.aku = "always finds annoying problems";
expect(observer.revNumber(obj)).toBe(2); expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
obj.aku = "always finds good problems"; obj.aku = "always finds good problems";
expect(observer.revNumber(obj)).toBe(3); expect(observer.revNumber(obj)).toBe(3);
expect(observer.deepRevNumber(obj)).toBe(3);
expect(observer.rev).toBe(3); expect(observer.rev).toBe(3);
}); });
@@ -416,7 +382,7 @@ describe("observer", () => {
expect(observer.revNumber(obj2)).toBe(1); expect(observer.revNumber(obj2)).toBe(1);
obj2.key = 3; obj2.key = 3;
expect(observer.revNumber(obj1)).toBe(1); expect(observer.revNumber(obj1)).toBe(2);
expect(observer.revNumber(obj2)).toBe(2); expect(observer.revNumber(obj2)).toBe(2);
}); });
@@ -442,7 +408,7 @@ describe("observer", () => {
obj.a = 111; obj.a = 111;
obj.f = 222; obj.f = 222;
await nextMicroTick(); await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(4); expect(observer.notifyCB).toBeCalledTimes(5);
}); });
test("throw error when state is mutated in object if allowMutation=false", async () => { test("throw error when state is mutated in object if allowMutation=false", async () => {
+11 -16
View File
@@ -23,7 +23,7 @@ interface MarkDownSection {
interface FileData { interface FileData {
name: string; name: string;
path: string[]; path: string[];
fullName: string fullName: string;
links: MarkDownLink[]; links: MarkDownLink[];
sections: MarkDownSection[]; sections: MarkDownSection[];
} }
@@ -31,11 +31,9 @@ interface FileData {
const LINK_REGEXP = /\[([^\[]+)\]\(([^\)]+)\)/g; const LINK_REGEXP = /\[([^\[]+)\]\(([^\)]+)\)/g;
const HEADING_REGEXP = /\n(#+\s*)(.*)/g; const HEADING_REGEXP = /\n(#+\s*)(.*)/g;
export function addMardownData(fileData): void { export function addMardownData(fileData): void {
const sep = fileData.path.length > 0 ? '/' : ''; const sep = fileData.path.length > 0 ? "/" : "";
const fullName = fileData.path.join('/') + sep + fileData.name; const fullName = fileData.path.join("/") + sep + fileData.name;
const content = fs.readFileSync(fullName, { encoding: "utf8" }); const content = fs.readFileSync(fullName, { encoding: "utf8" });
let m; let m;
// get links info // get links info
@@ -54,7 +52,6 @@ export function addMardownData(fileData): void {
} while (m); } while (m);
} }
/** /**
* Returns a list of FileData corresponding to all files that need to be * Returns a list of FileData corresponding to all files that need to be
* validated. * validated.
@@ -74,7 +71,7 @@ function getFiles(path: string[] = []): FileData[] {
if (f.isDirectory()) { if (f.isDirectory()) {
return getFiles(path.concat(f.name)); return getFiles(path.concat(f.name));
} }
const fullName = path.join('/') + (path.length > 0 ? '/' : '') + f.name; const fullName = path.join("/") + (path.length > 0 ? "/" : "") + f.name;
return [ return [
{ {
name: f.name, name: f.name,
@@ -88,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"];
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
@@ -99,19 +96,19 @@ export function isLinkValid(link: MarkDownLink, current: FileData, files: FileDa
// name = 'rendering.md' // name = 'rendering.md'
// hash = 'blabla' (or '' if no hash) // hash = 'blabla' (or '' if no hash)
const parts = link.link.split('#'); const parts = link.link.split("#");
const hash = parts[1] || ''; const hash = parts[1] || "";
let name; let name;
let path; let path;
if (parts[0]) { if (parts[0]) {
let temp = parts[0].split('/'); let temp = parts[0].split("/");
name = temp[temp.length - 1]; name = temp[temp.length - 1];
temp.splice(-1); temp.splice(-1);
path = current.path.slice(); path = current.path.slice();
for (let elem of temp) { for (let elem of temp) {
if (elem === '..') { if (elem === "..") {
path.splice(-1); path.splice(-1);
} else if (elem !== '.') { } else if (elem !== ".") {
path.push(elem); path.push(elem);
} }
} }
@@ -122,7 +119,7 @@ export function isLinkValid(link: MarkDownLink, current: FileData, files: FileDa
} }
// Step 2: build normalized link file name // Step 2: build normalized link file name
const linkFullName = path.join('/') + (path.length > 0 ? '/' : '') + name; const linkFullName = path.join("/") + (path.length > 0 ? "/" : "") + name;
// Step 3: check link name against white list of local files // Step 3: check link name against white list of local files
if (LOCAL_FILES.includes(linkFullName)) { if (LOCAL_FILES.includes(linkFullName)) {
@@ -164,8 +161,6 @@ function slugify(str) {
.replace(/-+$/, ""); // Trim - from end of text .replace(/-+$/, ""); // Trim - from end of text
} }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Test // Test
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
+2 -1
View File
@@ -1,4 +1,5 @@
import { Env, scheduler } from "../src/component/component"; import { Env } from "../src/component/component";
import { scheduler } from "../src/component/scheduler";
import { EvalContext, QWeb } from "../src/qweb/qweb"; import { EvalContext, QWeb } from "../src/qweb/qweb";
import { patch } from "../src/vdom"; import { patch } from "../src/vdom";
import "../src/qweb/base_directives"; import "../src/qweb/base_directives";
+144 -42
View File
@@ -28,6 +28,7 @@ let env: Env;
beforeEach(() => { beforeEach(() => {
fixture = makeTestFixture(); fixture = makeTestFixture();
env = makeTestEnv(); env = makeTestEnv();
Component.env = env;
}); });
afterEach(() => { afterEach(() => {
@@ -44,7 +45,7 @@ describe("hooks", () => {
static template = xml`<div><t t-esc="counter.value"/></div>`; static template = xml`<div><t t-esc="counter.value"/></div>`;
counter = useState({ value: 42 }); counter = useState({ value: 42 });
} }
const counter = new Counter(env); const counter = new Counter();
await counter.mount(fixture); await counter.mount(fixture);
expect(fixture.innerHTML).toBe("<div>42</div>"); expect(fixture.innerHTML).toBe("<div>42</div>");
counter.counter.value = 3; counter.counter.value = 3;
@@ -64,12 +65,12 @@ describe("hooks", () => {
} }
class MyComponent extends Component<any, any> { class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
constructor(env) { constructor() {
super(env); super();
useMyHook(); useMyHook();
} }
} }
const component = new MyComponent(env); const component = new MyComponent();
await component.mount(fixture); await component.mount(fixture);
expect(component).not.toHaveProperty("mounted"); expect(component).not.toHaveProperty("mounted");
expect(component).not.toHaveProperty("willUnmount"); expect(component).not.toHaveProperty("willUnmount");
@@ -92,8 +93,8 @@ describe("hooks", () => {
} }
class MyComponent extends Component<any, any> { class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
constructor(env) { constructor(parent, props) {
super(env); super(parent, props);
useMyHook(); useMyHook();
} }
} }
@@ -103,7 +104,7 @@ describe("hooks", () => {
static components = { MyComponent }; static components = { MyComponent };
state = useState({ flag: true }); state = useState({ flag: true });
} }
const parent = new Parent(env); const parent = new Parent();
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>"); expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
expect(steps).toEqual(["mounted"]); expect(steps).toEqual(["mounted"]);
@@ -126,8 +127,8 @@ describe("hooks", () => {
} }
class MyComponent extends Component<any, any> { class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
constructor(env) { constructor() {
super(env); super();
useMyHook(); useMyHook();
} }
mounted() { mounted() {
@@ -137,7 +138,7 @@ describe("hooks", () => {
steps.push("comp:willunmount"); steps.push("comp:willunmount");
} }
} }
const component = new MyComponent(env); const component = new MyComponent();
await component.mount(fixture); await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>"); expect(fixture.innerHTML).toBe("<div>hey</div>");
component.unmount(); component.unmount();
@@ -157,8 +158,8 @@ describe("hooks", () => {
} }
class MyComponent extends Component<any, any> { class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
constructor(env) { constructor(parent, props) {
super(env); super(parent, props);
useMyHook(); useMyHook();
} }
mounted() { mounted() {
@@ -175,7 +176,7 @@ describe("hooks", () => {
state = useState({ flag: true }); state = useState({ flag: true });
} }
const parent = new Parent(env); const parent = new Parent();
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>"); expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
parent.state.flag = false; parent.state.flag = false;
@@ -197,13 +198,13 @@ describe("hooks", () => {
} }
class MyComponent extends Component<any, any> { class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
constructor(env) { constructor() {
super(env); super();
useMyHook(1); useMyHook(1);
useMyHook(2); useMyHook(2);
} }
} }
const component = new MyComponent(env); const component = new MyComponent();
await component.mount(fixture); await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>"); expect(fixture.innerHTML).toBe("<div>hey</div>");
component.unmount(); component.unmount();
@@ -226,9 +227,12 @@ describe("hooks", () => {
(this.button.el as HTMLButtonElement).innerHTML = String(this.value); (this.button.el as HTMLButtonElement).innerHTML = String(this.value);
} }
} }
const counter = new Counter(env); const counter = new Counter();
expect(counter.button.el).toBe(null);
await counter.mount(fixture); await counter.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>0</button></div>"); expect(fixture.innerHTML).toBe("<div><button>0</button></div>");
expect(counter.button.el).not.toBe(null);
expect(counter.button.el).toBe(fixture.querySelector("button"));
counter.increment(); counter.increment();
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div><button>1</button></div>"); expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
@@ -247,7 +251,7 @@ describe("hooks", () => {
expect(this.spanRef.el).toBeNull(); expect(this.spanRef.el).toBeNull();
} }
} }
const component = new TestRef(env); const component = new TestRef();
await component.mount(fixture); await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>owl</span></div>"); expect(fixture.innerHTML).toBe("<div><span>owl</span></div>");
component.state.flag = false; component.state.flag = false;
@@ -255,6 +259,104 @@ describe("hooks", () => {
expect(fixture.innerHTML).toBe("<div></div>"); expect(fixture.innerHTML).toBe("<div></div>");
}); });
test("t-refs on widget are components", async () => {
class WidgetB extends Component<any, any> {
static template = xml`<div>b</div>`;
}
class WidgetC extends Component<any, any> {
static template = xml`<div class="outer-div">Hello<WidgetB t-ref="mywidgetb" /></div>`;
static components = { WidgetB };
ref = useRef("mywidgetb");
}
const widget = new WidgetC();
expect(widget.ref.comp).toBe(null);
expect(widget.ref.el).toBe(null);
await widget.mount(fixture);
expect(widget.ref.comp).toBeInstanceOf(WidgetB);
expect(widget.ref.el).toEqual(fixture.querySelector(".outer-div > div"));
});
test("t-refs are bound at proper timing", async () => {
expect.assertions(2);
class Widget extends Component<any, any> {
static template = xml`<div>widget</div>`;
}
class ParentWidget extends Component<any, any> {
static template = xml`
<div>
<t t-foreach="state.list" t-as="elem" t-ref="child" t-key="elem" t-component="Widget"/>
</div>
`;
static components = { Widget };
state = useState({ list: <any>[] });
child = useRef("child");
willPatch() {
expect(this.child.comp).toBeNull();
}
patched() {
expect(this.child.comp).not.toBeNull();
}
}
const parent = new ParentWidget();
await parent.mount(fixture);
parent.state.list.push(1);
await nextTick();
});
test("t-refs are bound at proper timing (2)", async () => {
expect.assertions(10);
class Widget extends Component<any, any> {
static template = xml`<div>widget</div>`;
}
class ParentWidget extends Component<any, any> {
static template = xml`
<div>
<t t-if="state.child1" t-ref="child1" t-component="Widget"/>
<t t-if="state.child2" t-ref="child2" t-component="Widget"/>
</div>`;
static components = { Widget };
state = useState({ child1: true, child2: false });
child1 = useRef("child1");
child2 = useRef("child2");
count = 0;
mounted() {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeNull();
}
willPatch() {
if (this.count === 0) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeNull();
}
if (this.count === 1) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeDefined();
}
}
patched() {
if (this.count === 0) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeDefined();
}
if (this.count === 1) {
expect(this.child1.comp).toBeNull();
expect(this.child2.comp).toBeDefined();
}
this.count++;
}
}
const parent = new ParentWidget();
await parent.mount(fixture);
parent.state.child2 = true;
await nextTick();
parent.state.child1 = false;
await nextTick();
});
test("can use onPatched, onWillPatch", async () => { test("can use onPatched, onWillPatch", async () => {
const steps: string[] = []; const steps: string[] = [];
function useMyHook() { function useMyHook() {
@@ -270,13 +372,13 @@ describe("hooks", () => {
static template = xml`<div><t t-if="state.flag">hey</t></div>`; static template = xml`<div><t t-if="state.flag">hey</t></div>`;
state = useState({ flag: true }); state = useState({ flag: true });
constructor(env) { constructor() {
super(env); super();
useMyHook(); useMyHook();
} }
} }
const component = new MyComponent(env); const component = new MyComponent();
await component.mount(fixture); await component.mount(fixture);
expect(component).not.toHaveProperty("patched"); expect(component).not.toHaveProperty("patched");
expect(component).not.toHaveProperty("willPatch"); expect(component).not.toHaveProperty("willPatch");
@@ -304,8 +406,8 @@ describe("hooks", () => {
static template = xml`<div><t t-if="state.flag">hey</t></div>`; static template = xml`<div><t t-if="state.flag">hey</t></div>`;
state = useState({ flag: true }); state = useState({ flag: true });
constructor(env) { constructor() {
super(env); super();
useMyHook(); useMyHook();
} }
willPatch() { willPatch() {
@@ -315,7 +417,7 @@ describe("hooks", () => {
steps.push("comp:patched"); steps.push("comp:patched");
} }
} }
const component = new MyComponent(env); const component = new MyComponent();
await component.mount(fixture); await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>"); expect(fixture.innerHTML).toBe("<div>hey</div>");
component.state.flag = false; component.state.flag = false;
@@ -337,13 +439,13 @@ describe("hooks", () => {
class MyComponent extends Component<any, any> { class MyComponent extends Component<any, any> {
static template = xml`<div>hey<t t-esc="state.value"/></div>`; static template = xml`<div>hey<t t-esc="state.value"/></div>`;
state = useState({ value: 1 }); state = useState({ value: 1 });
constructor(env) { constructor() {
super(env); super();
useMyHook(1); useMyHook(1);
useMyHook(2); useMyHook(2);
} }
} }
const component = new MyComponent(env); const component = new MyComponent();
await component.mount(fixture); await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey1</div>"); expect(fixture.innerHTML).toBe("<div>hey1</div>");
component.state.value++; component.state.value++;
@@ -377,13 +479,13 @@ describe("hooks", () => {
<input t-ref="input2"/> <input t-ref="input2"/>
</div>`; </div>`;
constructor(env) { constructor() {
super(env); super();
useAutofocus("input2"); useAutofocus("input2");
} }
} }
const component = new SomeComponent(env); const component = new SomeComponent();
await component.mount(fixture); await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><input></div>"); expect(fixture.innerHTML).toBe("<div><input><input></div>");
const input2 = fixture.querySelectorAll("input")[1]; const input2 = fixture.querySelectorAll("input")[1];
@@ -399,13 +501,13 @@ describe("hooks", () => {
</div>`; </div>`;
state = useState({ flag: false }); state = useState({ flag: false });
constructor(env) { constructor() {
super(env); super();
useAutofocus("input2"); useAutofocus("input2");
} }
} }
const component = new SomeComponent(env); const component = new SomeComponent();
await component.mount(fixture); await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input></div>"); expect(fixture.innerHTML).toBe("<div><input></div>");
expect(document.activeElement).toBe(document.body); expect(document.activeElement).toBe(document.body);
@@ -420,12 +522,12 @@ describe("hooks", () => {
test("can use sub env", async () => { test("can use sub env", async () => {
class TestComponent extends Component<any, any> { class TestComponent extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/></div>`; static template = xml`<div><t t-esc="env.val"/></div>`;
constructor(env) { constructor() {
super(env); super();
useSubEnv({ val: 3 }); useSubEnv({ val: 3 });
} }
} }
const component = new TestComponent(env); const component = new TestComponent();
await component.mount(fixture); await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3</div>"); expect(fixture.innerHTML).toBe("<div>3</div>");
expect(env).not.toHaveProperty("val"); expect(env).not.toHaveProperty("val");
@@ -435,8 +537,8 @@ describe("hooks", () => {
test("parent and child env", async () => { test("parent and child env", async () => {
class Child extends Component<any, any> { class Child extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/></div>`; static template = xml`<div><t t-esc="env.val"/></div>`;
constructor(env) { constructor(parent, props) {
super(env); super(parent, props);
useSubEnv({ val: 5 }); useSubEnv({ val: 5 });
} }
} }
@@ -444,12 +546,12 @@ describe("hooks", () => {
class Parent extends Component<any, any> { class Parent extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/><Child/></div>`; static template = xml`<div><t t-esc="env.val"/><Child/></div>`;
static components = { Child }; static components = { Child };
constructor(env) { constructor() {
super(env); super();
useSubEnv({ val: 3 }); useSubEnv({ val: 3 });
} }
} }
const component = new Parent(env); const component = new Parent();
await component.mount(fixture); await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3<div>5</div></div>"); expect(fixture.innerHTML).toBe("<div>3<div>5</div></div>");
}); });
@@ -478,7 +580,7 @@ describe("hooks", () => {
state = useState({ value: 1 }); state = useState({ value: 1 });
} }
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(app).not.toHaveProperty("willStart"); expect(app).not.toHaveProperty("willStart");
expect(app).not.toHaveProperty("willUpdateProps"); expect(app).not.toHaveProperty("willUpdateProps");
+6 -7
View File
@@ -2,7 +2,7 @@ import { AsyncRoot } from "../../src/misc/async_root";
import { useState } from "../../src/hooks"; import { useState } from "../../src/hooks";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { makeDeferred, makeTestFixture, makeTestEnv, nextTick } from "../helpers"; import { makeDeferred, makeTestFixture, makeTestEnv, nextTick } from "../helpers";
import { Env, Component } from "../../src/component/component"; import { Component } from "../../src/component/component";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Setup and helpers // Setup and helpers
@@ -11,14 +11,13 @@ import { Env, Component } from "../../src/component/component";
// We create before each test: // We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom // - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test. // manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components // - a test env, necessary to create components, that is set as env
let fixture: HTMLElement; let fixture: HTMLElement;
let env: Env;
beforeEach(() => { beforeEach(() => {
fixture = makeTestFixture(); fixture = makeTestFixture();
env = makeTestEnv(); Component.env = makeTestEnv();
}); });
afterEach(() => { afterEach(() => {
@@ -55,7 +54,7 @@ describe("Asyncroot", () => {
} }
} }
const parent = new Parent(env); const parent = new Parent();
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0</span><span>0</span>"); expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0</span><span>0</span>");
@@ -103,7 +102,7 @@ describe("Asyncroot", () => {
} }
} }
const parent = new Parent(env); const parent = new Parent();
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0</span><span>0</span>"); expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0</span><span>0</span>");
@@ -158,7 +157,7 @@ describe("Asyncroot", () => {
} }
} }
const parent = new Parent(env); const parent = new Parent();
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0/0</span><span>0/0</span>"); expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0/0</span><span>0/0</span>");
+101 -17
View File
@@ -699,25 +699,25 @@ exports[`misc global 1`] = `
} else { } else {
c17.push({text: \`foo default\`}); c17.push({text: \`foo default\`});
} }
var _19 = 'bbb'; _11 = 'bbb'
let c19 = [], p19 = {key:19};
var vn19 = h('span', p19, c19);
c14.push(vn19);
if (_11 || _11 === 0) {
c19.push({text: _11});
} else {
c19.push({text: \`foo default\`});
}
}
}
let c20 = [], p20 = {key:20}; let c20 = [], p20 = {key:20};
var vn20 = h('span', p20, c20); var vn20 = h('div', p20, c20);
c14.push(vn20); c1.push(vn20);
if (_19 || _19 === 0) { var _21 = context['toto'];
c20.push({text: _19}); if (_21 || _21 === 0) {
c20.push(...utils.htmlToVDOM(_21));
} else { } else {
c20.push({text: \`foo default\`}); c20.push({text: \`toto default\`});
}
}
}
let c21 = [], p21 = {key:21};
var vn21 = h('div', p21, c21);
c1.push(vn21);
var _22 = context['toto'];
if (_22 || _22 === 0) {
c21.push(...utils.htmlToVDOM(_22));
} else {
c21.push({text: \`toto default\`});
} }
return vn1; return vn1;
}" }"
@@ -2400,6 +2400,90 @@ exports[`t-set t-set should reuse variable if possible 1`] = `
}" }"
`; `;
exports[`t-set t-set, t-if, and mix of expression/body lookup, 1 1`] = `
"function anonymous(context,extra
) {
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['flag']) {
}
else {
var _2 = 0;
}
if (_2 || _2 === 0) {
c1.push({text: _2});
} else {
c1.push({text: \`1\`});
}
return vn1;
}"
`;
exports[`t-set t-set, t-if, and mix of expression/body lookup, 1 2`] = `
"function anonymous(context,extra
) {
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['flag']) {
}
else {
var _2 = 0;
}
if (_2 || _2 === 0) {
c1.push({text: _2});
} else {
c1.push({text: \`1\`});
}
return vn1;
}"
`;
exports[`t-set t-set, t-if, and mix of expression/body lookup, 2 1`] = `
"function anonymous(context,extra
) {
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['flag']) {
var _2 = 1;
}
else {
}
if (_2 || _2 === 0) {
c1.push({text: _2});
} else {
c1.push({text: \`0\`});
}
return vn1;
}"
`;
exports[`t-set t-set, t-if, and mix of expression/body lookup, 2 2`] = `
"function anonymous(context,extra
) {
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['flag']) {
var _2 = 1;
}
else {
}
if (_2 || _2 === 0) {
c1.push({text: _2});
} else {
c1.push({text: \`0\`});
}
return vn1;
}"
`;
exports[`t-set value priority 1`] = ` exports[`t-set value priority 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
+26
View File
@@ -242,6 +242,32 @@ describe("t-set", () => {
); );
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe("<div>45</div>"); expect(renderToString(qweb, "test", { somevariable: 43 })).toBe("<div>45</div>");
}); });
test("t-set, t-if, and mix of expression/body lookup, 1", () => {
qweb.addTemplate(
"test",
`<div>
<t t-if="flag" t-set="ourvar">1</t>
<t t-else="" t-set="ourvar" t-value="0"></t>
<t t-esc="ourvar"/>
</div>`
);
expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>");
expect(renderToString(qweb, "test", { flag: false })).toBe("<div>0</div>");
});
test("t-set, t-if, and mix of expression/body lookup, 2", () => {
qweb.addTemplate(
"test",
`<div>
<t t-if="flag" t-set="ourvar" t-value="1"></t>
<t t-else="" t-set="ourvar">0</t>
<t t-esc="ourvar"/>
</div>`
);
expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>");
expect(renderToString(qweb, "test", { flag: false })).toBe("<div>0</div>");
});
}); });
describe("t-if", () => { describe("t-if", () => {
+5
View File
@@ -43,6 +43,10 @@ describe("tokenizer", () => {
{ type: "OPERATOR", value: "!==" }, { type: "OPERATOR", value: "!==" },
{ type: "OPERATOR", value: "!=" } { type: "OPERATOR", value: "!=" }
]); ]);
expect(tokenize("typeof a")).toEqual([
{ type: "OPERATOR", value: "typeof " },
{ type: "SYMBOL", value: "a" }
]);
}); });
test("strings", () => { test("strings", () => {
@@ -112,6 +116,7 @@ describe("expression evaluation", () => {
expect(compileExpr("!flag", {})).toBe("!context['flag']"); expect(compileExpr("!flag", {})).toBe("!context['flag']");
expect(compileExpr("-3", {})).toBe("-3"); expect(compileExpr("-3", {})).toBe("-3");
expect(compileExpr("-a", {})).toBe("-context['a']"); expect(compileExpr("-a", {})).toBe("-context['a']");
expect(compileExpr("typeof a", {})).toBe("typeof context['a']");
}); });
test("various binary operators", () => { test("various binary operators", () => {
@@ -13,10 +13,10 @@ exports[`RouteComponent can render simple cases 1`] = `
if (context['routeComponent']) { if (context['routeComponent']) {
const nodeKey1 = context['env'].router.currentRouteName; const nodeKey1 = context['env'].router.currentRouteName;
//COMPONENT //COMPONENT
let templateId3 = \`__4__\` + nodeKey1; let k4 = \`__5__\` + nodeKey1;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false; let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let vn5 = {}; let vn6 = {};
result = vn5; result = vn6;
let props3 = Object.assign({}, context['env'].router.currentParams); let props3 = Object.assign({}, context['env'].router.currentParams);
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) { if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy(); w3.destroy();
@@ -25,20 +25,21 @@ exports[`RouteComponent can render simple cases 1`] = `
if (w3) { if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling); w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode; let pvnode = w3.__owl__.pvnode;
utils.defineProxy(vn5, pvnode); utils.defineProxy(vn6, pvnode);
} else { } else {
let componentKey3 = \`routeComponent\`; let componentKey3 = \`routeComponent\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['routeComponent']; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['routeComponent'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3); w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id; parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); let pvnode = h('dummy', {key: k4, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});
const fiber = w3.__owl__.currentFiber; const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
utils.defineProxy(vn5, pvnode); utils.defineProxy(vn6, pvnode);
w3.__owl__.pvnode = pvnode; w3.__owl__.pvnode = pvnode;
} }
w3.__owl__.parentLastFiberId = extra.fiber.id;
sibling = w3.__owl__.currentFiber || sibling; sibling = w3.__owl__.currentFiber || sibling;
} }
return result; return result;
+3 -2
View File
@@ -12,6 +12,7 @@ describe("Link component", () => {
beforeEach(() => { beforeEach(() => {
fixture = makeTestFixture(); fixture = makeTestFixture();
env = <RouterEnv>makeTestEnv(); env = <RouterEnv>makeTestEnv();
Component.env = env;
}); });
afterEach(() => { afterEach(() => {
@@ -38,7 +39,7 @@ describe("Link component", () => {
router = new TestRouter(env, routes, { mode: "history" }); router = new TestRouter(env, routes, { mode: "history" });
router.navigate({ to: "users" }); router.navigate({ to: "users" });
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe('<div><a href="/about">About</a></div>'); expect(fixture.innerHTML).toBe('<div><a href="/about">About</a></div>');
@@ -69,7 +70,7 @@ describe("Link component", () => {
router = new TestRouter(env, routes, { mode: "history" }); router = new TestRouter(env, routes, { mode: "history" });
router.navigate({ to: "users" }); router.navigate({ to: "users" });
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(window.location.pathname).toBe("/users"); expect(window.location.pathname).toBe("/users");
+4 -3
View File
@@ -12,6 +12,7 @@ describe("RouteComponent", () => {
beforeEach(() => { beforeEach(() => {
fixture = makeTestFixture(); fixture = makeTestFixture();
env = <RouterEnv>makeTestEnv(); env = <RouterEnv>makeTestEnv();
Component.env = env;
}); });
afterEach(() => { afterEach(() => {
@@ -45,7 +46,7 @@ describe("RouteComponent", () => {
router = new TestRouter(env, routes, { mode: "history" }); router = new TestRouter(env, routes, { mode: "history" });
await router.navigate({ to: "about" }); await router.navigate({ to: "about" });
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>About</span></div>"); expect(fixture.innerHTML).toBe("<div><span>About</span></div>");
@@ -72,7 +73,7 @@ describe("RouteComponent", () => {
const routes = [{ name: "book", path: "/book/{{title}}", component: Book }]; const routes = [{ name: "book", path: "/book/{{title}}", component: Book }];
router = new TestRouter(env, routes, { mode: "history" }); router = new TestRouter(env, routes, { mode: "history" });
await router.navigate({ to: "book", params: { title: "1984" } }); await router.navigate({ to: "book", params: { title: "1984" } });
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984</span></div>"); expect(fixture.innerHTML).toBe("<div><span>Book 1984</span></div>");
}); });
@@ -98,7 +99,7 @@ describe("RouteComponent", () => {
const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }]; const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }];
router = new TestRouter(env, routes, { mode: "history" }); router = new TestRouter(env, routes, { mode: "history" });
await router.navigate({ to: "book", params: { title: "1984", val: "123" } }); await router.navigate({ to: "book", params: { title: "1984", val: "123" } });
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>"); expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
}); });
+1 -2
View File
@@ -55,12 +55,11 @@ describe("router miscellaneous", () => {
test("navigate in hash mode preserve location", async () => { test("navigate in hash mode preserve location", async () => {
router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }], { mode: "hash" }); router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }], { mode: "hash" });
window.history.pushState({}, "title", window.location.origin + '/test.html'); window.history.pushState({}, "title", window.location.origin + "/test.html");
expect(window.location.href).toBe("http://localhost/test.html"); expect(window.location.href).toBe("http://localhost/test.html");
await router.navigate({ to: "users", params: { id: 3 } }); await router.navigate({ to: "users", params: { id: 3 } });
expect(window.location.href).toBe("http://localhost/test.html#/users/3"); expect(window.location.href).toBe("http://localhost/test.html#/users/3");
}); });
}); });
describe("routeToPath", () => { describe("routeToPath", () => {
+106 -22
View File
@@ -12,6 +12,7 @@ describe("connecting a component to store", () => {
beforeEach(() => { beforeEach(() => {
fixture = makeTestFixture(); fixture = makeTestFixture();
env = makeTestEnv(); env = makeTestEnv();
Component.env = env;
}); });
afterEach(() => { afterEach(() => {
@@ -37,7 +38,7 @@ describe("connecting a component to store", () => {
} }
(<any>env).store = store; (<any>env).store = store;
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>"); expect(fixture.innerHTML).toBe("<div></div>");
@@ -47,6 +48,22 @@ describe("connecting a component to store", () => {
expect(fixture.innerHTML).toBe("<div><span>hello</span></div>"); expect(fixture.innerHTML).toBe("<div><span>hello</span></div>");
}); });
test("throw error if no store is found", async () => {
class App extends Component<any, any> {
static template = xml`<div></div>`;
todos = useStore(state => state.todos);
}
let error;
try {
new App();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("No store found when connecting 'App'");
});
test("can use useStore twice in a component", async () => { test("can use useStore twice in a component", async () => {
const state = { a: 1, b: 2 }; const state = { a: 1, b: 2 };
const actions = { const actions = {
@@ -69,7 +86,7 @@ describe("connecting a component to store", () => {
App.prototype.__render = jest.fn(App.prototype.__render); App.prototype.__render = jest.fn(App.prototype.__render);
(<any>env).store = store; (<any>env).store = store;
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>1</span><span>2</span></div>"); expect(fixture.innerHTML).toBe("<div><span>1</span><span>2</span></div>");
@@ -101,7 +118,7 @@ describe("connecting a component to store", () => {
App.prototype.__render = jest.fn(App.prototype.__render); App.prototype.__render = jest.fn(App.prototype.__render);
(<any>env).store = store; (<any>env).store = store;
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>"); expect(fixture.innerHTML).toBe("<div></div>");
@@ -134,7 +151,7 @@ describe("connecting a component to store", () => {
} }
(<any>env).store = store; (<any>env).store = store;
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>0</div>"); expect(fixture.innerHTML).toBe("<div>0</div>");
@@ -163,7 +180,7 @@ describe("connecting a component to store", () => {
App.prototype.__render = jest.fn(App.prototype.__render); App.prototype.__render = jest.fn(App.prototype.__render);
(<any>env).store = store; (<any>env).store = store;
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>0</div>"); expect(fixture.innerHTML).toBe("<div>0</div>");
@@ -198,7 +215,7 @@ describe("connecting a component to store", () => {
todos = useStore(state => state.todos, { store }); todos = useStore(state => state.todos, { store });
} }
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>"); expect(fixture.innerHTML).toBe("<div></div>");
@@ -228,7 +245,7 @@ describe("connecting a component to store", () => {
storeState = useStore(state => state); storeState = useStore(state => state);
dispatch = useDispatch(); dispatch = useDispatch();
} }
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>Inc</button><span>1</span></div>"); expect(fixture.innerHTML).toBe("<div><button>Inc</button><span>1</span></div>");
@@ -257,7 +274,7 @@ describe("connecting a component to store", () => {
} }
(<any>env).store = store; (<any>env).store = store;
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>"); expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
@@ -294,7 +311,7 @@ describe("connecting a component to store", () => {
} }
(<any>env).store = store; (<any>env).store = store;
const app = new TodoList(env); const app = new TodoList();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>"); expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
@@ -347,7 +364,7 @@ describe("connecting a component to store", () => {
} }
(<any>env).store = store; (<any>env).store = store;
const app = new TodoList(env); const app = new TodoList();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
@@ -370,7 +387,7 @@ describe("connecting a component to store", () => {
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } }; const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
const store = new Store({ state }); const store = new Store({ state });
(<any>env).store = store; (<any>env).store = store;
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>"); expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
@@ -401,7 +418,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>"); expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
@@ -446,7 +463,7 @@ describe("connecting a component to store", () => {
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>"); expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>");
@@ -513,7 +530,7 @@ describe("connecting a component to store", () => {
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>"); expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>");
@@ -555,6 +572,31 @@ describe("connecting a component to store", () => {
); );
}); });
test("store changes occuring when mounting a component are notified", async () => {
const initialState = { x: { val: 1 } };
const actions = {
setValue({ state }, val) {
state.x.val = val;
}
};
class Parent extends Component<any, any> {
static template = xml`<div><t t-esc="x.val"/></div>`;
x = useStore(state => {
return Object.assign({}, state.x);
});
dispatch = useDispatch();
}
Parent.prototype.__render = jest.fn(Parent.prototype.__render);
Parent.env.store = new Store({ state: initialState, actions });
const parent = new Parent();
const prom = parent.mount(fixture);
parent.dispatch("setValue", 2);
await prom;
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(Parent.prototype.__render).toHaveBeenCalledTimes(1);
});
test("correct update order when parent/children are connected", async () => { test("correct update order when parent/children are connected", async () => {
const steps: string[] = []; const steps: string[] = [];
@@ -588,7 +630,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
const app = new Parent(env); const app = new Parent();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>a</span></div>"); expect(fixture.innerHTML).toBe("<div><span>a</span></div>");
@@ -639,7 +681,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
const app = new Parent(env); const app = new Parent();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>abc</span></div>"); expect(fixture.innerHTML).toBe("<div><span>abc</span></div>");
@@ -713,7 +755,7 @@ describe("connecting a component to store", () => {
} }
(<any>env).store = store; (<any>env).store = store;
const app = new TodoApp(env); const app = new TodoApp();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
@@ -787,7 +829,7 @@ describe("connecting a component to store", () => {
} }
(<any>env).store = store; (<any>env).store = store;
const app = new TodoApp(env); const app = new TodoApp();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
@@ -833,7 +875,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>a</div>"); expect(fixture.innerHTML).toBe("<div>a</div>");
@@ -870,7 +912,7 @@ describe("connecting a component to store", () => {
} }
const store = new TestStore({ state: { val: 1 } }); const store = new TestStore({ state: { val: 1 } });
(<any>env).store = store; (<any>env).store = store;
const parent = new Parent(env); const parent = new Parent();
await parent.mount(fixture); await parent.mount(fixture);
expect(steps).toEqual(["on:update"]); expect(steps).toEqual(["on:update"]);
@@ -882,6 +924,46 @@ describe("connecting a component to store", () => {
expect(steps).toEqual(["on:update", "off:update"]); expect(steps).toEqual(["on:update", "off:update"]);
}); });
test("connected child component destroyed by dispatched action", async () => {
let steps: any = [];
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="store.val"/></div>`;
store = useStore(s => {
steps.push("child selector");
return s;
});
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="store.child" /></div>`;
static components = { Child };
store = useStore(s => {
steps.push("parent selector");
return s;
});
dispatch = useDispatch();
}
const state = { child: true, val: 1 };
const actions = {
toggleChild({ state }) {
state.child = !state.child;
}
};
const store = new Store({ state, actions });
(<any>env).store = store;
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
expect(steps).toEqual(["parent selector", "child selector"]);
parent.dispatch("toggleChild");
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["parent selector", "child selector", "parent selector"]);
});
test("dispatch an action", async () => { test("dispatch an action", async () => {
class App extends Component<any, any> { class App extends Component<any, any> {
static template = xml`<div><t t-esc="store.counter"/></div>`; static template = xml`<div><t t-esc="store.counter"/></div>`;
@@ -901,7 +983,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, actions }); const store = new Store({ state, actions });
(<any>env).store = store; (<any>env).store = store;
const app = new App(env); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>0</div>"); expect(fixture.innerHTML).toBe("<div>0</div>");
@@ -920,6 +1002,7 @@ describe("various scenarios", () => {
beforeEach(() => { beforeEach(() => {
fixture = makeTestFixture(); fixture = makeTestFixture();
env = makeTestEnv(); env = makeTestEnv();
Component.env = env;
}); });
afterEach(() => { afterEach(() => {
@@ -978,7 +1061,8 @@ describe("various scenarios", () => {
} }
(<any>env).store = store; (<any>env).store = store;
const message = new Message(env); const message = new Message();
await message.mount(fixture); await message.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot(); expect(fixture.innerHTML).toMatchSnapshot();
+2 -4
View File
@@ -15,8 +15,6 @@ class Counter extends owl.Component {
// Message Widget // Message Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class Message extends owl.Component { class Message extends owl.Component {
static components = { Counter };
shouldUpdate(nextProps) { shouldUpdate(nextProps) {
return nextProps.message !== this.props.message; return nextProps.message !== this.props.message;
} }
@@ -26,12 +24,11 @@ class Message extends owl.Component {
}); });
} }
} }
Message.components = { Counter };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Root Widget // Root Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class App extends owl.Component { class App extends owl.Component {
static components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false }; state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() { mounted() {
@@ -137,6 +134,7 @@ class App extends owl.Component {
this.refs.log.innerHTML = ""; this.refs.log.innerHTML = "";
} }
} }
App.components = { Message };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Application initialization // Application initialization
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>OWL v0.24.0 Benchmark</title> <title>OWL v0.24.0 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>
<script src='app.js' type="module"></script> <script src='app.js' type="module"></script>
File diff suppressed because it is too large Load Diff
+4 -5
View File
@@ -16,8 +16,6 @@ class Counter extends owl.Component {
// Message Widget // Message Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class Message extends owl.Component { class Message extends owl.Component {
static components = { Counter };
shouldUpdate(nextProps) { shouldUpdate(nextProps) {
return nextProps.message !== this.props.message; return nextProps.message !== this.props.message;
} }
@@ -27,12 +25,12 @@ class Message extends owl.Component {
}); });
} }
} }
Message.components = { Counter };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Root Widget // Root Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class App extends owl.Component { class App extends owl.Component {
static components = { Message };
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false }); state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log"); logRef = useRef("log");
@@ -139,16 +137,17 @@ class App extends owl.Component {
this.logRef.el.innerHTML = ""; this.logRef.el.innerHTML = "";
} }
} }
App.components = { Message };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Application initialization // Application initialization
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
async function start() { async function start() {
const templates = await owl.utils.loadFile("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);
} }
+40 -56
View File
@@ -70,13 +70,9 @@ if __name__ == "__main__":
/** /**
* Make an iframe, with all the js, css and xml properly injected. * Make an iframe, with all the js, css and xml properly injected.
*/ */
function makeCodeIframe(js, css, xml, errorHandler) { function makeCodeIframe(js, css, xml) {
// check templates
var qweb = new owl.QWeb();
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, ""); const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "");
// will throw error if there is something wrong with xml
qweb.addTemplates(sanitizedXML);
// create iframe // create iframe
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
@@ -90,16 +86,15 @@ function makeCodeIframe(js, css, xml, errorHandler) {
owlScript.addEventListener("load", () => { owlScript.addEventListener("load", () => {
const script = doc.createElement("script"); const script = doc.createElement("script");
script.type = "text/javascript"; script.type = "text/javascript";
const content = `owl.__info__.mode = 'dev';\nwindow.TEMPLATES = \`${sanitizedXML}\`;\n${js}`; const content = `
script.innerHTML = content; {
iframe.contentWindow.addEventListener("error", errorHandler); owl.config.mode = 'dev';
iframe.contentWindow.addEventListener("unhandledrejection", errorHandler); let templates = \`${sanitizedXML}\`;
setTimeout(function() { const qweb = new owl.QWeb({ templates });
if (iframe.contentWindow) { owl.Component.env = { qweb };
iframe.contentWindow.removeEventListener("error", errorHandler);
iframe.contentWindow.removeEventListener("unhandledrejection", errorHandler);
} }
}, 200); ${js}`;
script.innerHTML = content;
doc.body.appendChild(script); doc.body.appendChild(script);
}); });
doc.head.appendChild(owlScript); doc.head.appendChild(owlScript);
@@ -125,20 +120,34 @@ async function makeApp(js, css, xml) {
.join("\n"); .join("\n");
const JS = ` const JS = `
async function loadTemplates() { /**
try { * This is the javascript code defined in the playground.
return owl.utils.loadFile('app.xml'); * In a larger application, this code should probably be moved in different
} catch(e) { * sub files.
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`); */
} function app() {
}
function start([TEMPLATES]) {
// Application code
${processedJS} ${processedJS}
} }
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start); /**
* Initialization code
* This code load templates, and make sure everything is properly connected.
*/
async function start() {
let templates;
try {
templates = await owl.utils.loadFile('app.xml');
} catch(e) {
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
return;
}
const env = { qweb: new owl.QWeb({templates})};
owl.Component.env = env;
await owl.utils.whenReady();
app();
}
start();
`; `;
zip.file("app.js", JS); zip.file("app.js", JS);
@@ -310,7 +319,6 @@ class App extends owl.Component {
js: this.SAMPLES[0].code, js: this.SAMPLES[0].code,
css: this.SAMPLES[0].css || "", css: this.SAMPLES[0].css || "",
xml: this.SAMPLES[0].xml || DEFAULT_XML, xml: this.SAMPLES[0].xml || DEFAULT_XML,
error: false,
displayWelcome: true, displayWelcome: true,
splitLayout: true, splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2), leftPaneWidth: Math.ceil(window.innerWidth / 2),
@@ -323,37 +331,12 @@ class App extends owl.Component {
this.content = useRef("content"); this.content = useRef("content");
} }
displayError(error) {
this.state.error = error;
if (error) {
setTimeout(() => {
this.content.el.innerHTML = "";
});
return;
}
}
runCode() { runCode() {
this.state.displayWelcome = false;
let subiframe;
let error = false;
const errorHandler = e => this.displayError(e.message || e.reason.message);
try {
const { js, css, xml } = this.state;
subiframe = makeCodeIframe(js, css, xml, errorHandler);
} catch (e) {
//probably problem with the templates
error = e;
// we still log the error, always useful to have it available
console.error(e);
}
if (error) {
this.displayError(error.message);
return;
} else {
this.state.error = false;
}
this.content.el.innerHTML = ""; this.content.el.innerHTML = "";
this.state.displayWelcome = false;
const { js, css, xml } = this.state;
const subiframe = makeCodeIframe(js, css, xml);
this.content.el.appendChild(subiframe); this.content.el.appendChild(subiframe);
} }
@@ -436,7 +419,8 @@ async function start() {
owl.utils.whenReady() owl.utils.whenReady()
]); ]);
const qweb = new owl.QWeb({ templates }); const qweb = new owl.QWeb({ templates });
const app = new App({ qweb }); owl.Component.env = { qweb };
const app = new App();
app.mount(document.body); app.mount(document.body);
} }
-13
View File
@@ -179,16 +179,3 @@ body {
padding: 5%; padding: 5%;
} }
.right-pane .error {
height: 100%;
width: 90%;
padding-top: 30%;
font-size: 18px;
color: darkred;
margin-left: 5%;
}
.right-pane .error pre {
overflow: auto;
width: 100%;
}
+20 -35
View File
@@ -16,9 +16,7 @@ class App extends Component {
App.components = { Greeter }; App.components = { Greeter };
// Application setup // Application setup
// Note that the xml templates are injected into the global TEMPLATES variable. const app = new App();
const qweb = new owl.QWeb({ templates: TEMPLATES});
const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
`; `;
@@ -70,8 +68,7 @@ class App extends Component {
} }
App.components = { Counter }; App.components = { Counter };
const qweb = new owl.QWeb({ templates: TEMPLATES}); const app = new App();
const app = new App({qweb});
app.mount(document.body); app.mount(document.body);
`; `;
@@ -228,8 +225,7 @@ class App extends Component {
} }
App.components = { DemoComponent }; App.components = { DemoComponent };
const qweb = new owl.QWeb({ templates: TEMPLATES}); const app = new App();
const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
`; `;
@@ -298,8 +294,7 @@ class App extends owl.Component {
} }
// Application setup // Application setup
const qweb = new owl.QWeb({ templates: TEMPLATES}); const app = new App();
const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
`; `;
@@ -349,11 +344,9 @@ const themeContext = new Context({
background: '#000', background: '#000',
foreground: '#fff', foreground: '#fff',
}); });
const env = { // Add the themeContext the environment to make it available to all components
qweb: new owl.QWeb({ templates: TEMPLATES}), App.env.themeContext = themeContext;
themeContext: themeContext, const app = new App();
};
const app = new App(env);
app.mount(document.body); app.mount(document.body);
`; `;
@@ -534,26 +527,25 @@ TodoApp.components = { TodoItem };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// App Initialization // App Initialization
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
function makeStore() {
function saveState(state) { function saveState(state) {
const str = JSON.stringify(state); const str = JSON.stringify(state);
window.localStorage.setItem(LOCALSTORAGE_KEY, str); window.localStorage.setItem(LOCALSTORAGE_KEY, str);
} }
function loadState() { function loadState() {
const localState = window.localStorage.getItem(LOCALSTORAGE_KEY); const localState = window.localStorage.getItem(LOCALSTORAGE_KEY);
return localState ? JSON.parse(localState) : initialState; return localState ? JSON.parse(localState) : initialState;
} }
function makeEnv() {
const state = loadState(); const state = loadState();
const store = new owl.Store({ state, actions }); const store = new owl.Store({ state, actions });
store.on("update", null, () => saveState(store.state)); store.on("update", null, () => saveState(store.state));
const qweb = new owl.QWeb({ templates: TEMPLATES}); return store;
return { qweb, store };
} }
const env = makeEnv(); TodoApp.env.store = makeStore();
const app = new TodoApp(env); const app = new TodoApp();
app.mount(document.body); app.mount(document.body);
`; `;
@@ -1040,12 +1032,9 @@ function setupResponsivePlugin(env) {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Application Startup // Application Startup
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const env = { setupResponsivePlugin(App.env);
qweb: new owl.QWeb({ templates: TEMPLATES}),
};
setupResponsivePlugin(env);
const app = new App(env); const app = new App();
app.mount(document.body); app.mount(document.body);
`; `;
@@ -1187,8 +1176,7 @@ class App extends Component {
App.components = {Card, Counter}; App.components = {Card, Counter};
// Application setup // Application setup
const qweb = new owl.QWeb({ templates: TEMPLATES}); const app = new App();
const app = new App({ qweb });
app.mount(document.body);`; app.mount(document.body);`;
const SLOTS_XML = `<templates> const SLOTS_XML = `<templates>
@@ -1301,10 +1289,9 @@ class App extends Component {
}, 3000); }, 3000);
} }
} }
App.components = {SlowComponent, NotificationList}; App.components = {SlowComponent, NotificationList, AsyncRoot};
const qweb = new owl.QWeb({ templates: TEMPLATES}); const app = new App();
const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
`; `;
@@ -1373,8 +1360,7 @@ class Form extends Component {
} }
// Application setup // Application setup
const qweb = new owl.QWeb({ templates: TEMPLATES}); const form = new Form();
const form = new Form({ qweb });
form.mount(document.body); form.mount(document.body);
`; `;
@@ -1540,7 +1526,6 @@ class App extends Component {
} }
App.components = { WindowManager }; App.components = { WindowManager };
const qweb = new owl.QWeb({ templates: TEMPLATES});
const windows = [ const windows = [
{ {
name: "Hello", name: "Hello",
@@ -1558,8 +1543,8 @@ const windows = [
} }
]; ];
const env = { qweb, windows }; App.env.windows = windows;
const app = new App(env); const app = new App();
app.mount(document.body); app.mount(document.body);
`; `;
+1 -5
View File
@@ -31,7 +31,7 @@
t-att-style="topEditorStyle"/> t-att-style="topEditorStyle"/>
<t t-if="state.splitLayout"> <t t-if="state.splitLayout">
<div class="separator horizontal"/> <div class="separator horizontal"/>
<TabbedEditor t-keepalive="1" <TabbedEditor
js="false" js="false"
css="state.css" css="state.css"
xml="state.xml" xml="state.xml"
@@ -51,10 +51,6 @@
</p> </p>
</div> </div>
</div> </div>
<div t-if="state.error" class="error">
<h3>Error</h3>
<pre t-esc="state.error"/>
</div>
<div class="content" t-ref="content"/> <div class="content" t-ref="content"/>
</div> </div>
</div> </div>