Compare commits

..

65 Commits

Author SHA1 Message Date
Géry Debongnie f289cb17ec [REL] bump to v0.19.0 2019-08-28 11:55:36 +02:00
Géry Debongnie e6a3ede4a6 [FIX] component: improve unmount/remount behaviour 2019-08-28 11:41:10 +02:00
Alexandre Kühn 10cfe0b740 [IMP] store: dispatch method returns data from action 2019-08-27 22:26:16 +02:00
Géry Debongnie a3d563ebb4 [FIX] connected_component: some misc issues
- did not properly stop listening to the store after being destroyed
- did not properly synchronize store updates in some async cases
2019-08-27 17:11:54 +02:00
Géry Debongnie 3b1247d476 [REF] vdom: small cleanup 2019-08-27 16:01:01 +02:00
Géry Debongnie beb6c2bc82 [DOC] improve readme and comparison
closes #244
2019-08-27 14:48:02 +02:00
Géry Debongnie f0cd6f61cd [FIX] playground: fix crash in todo app example 2019-08-27 14:09:20 +02:00
Géry Debongnie c950b385a0 [ADD] playground: add window management system example
closes #231
2019-08-27 14:09:20 +02:00
Géry Debongnie c172b31481 [DOC] router: add documentation 2019-08-26 15:45:55 +02:00
Géry Debongnie 9b3a97e732 [DOC] component: improve internal documentation 2019-08-26 12:19:11 +02:00
Géry Debongnie 670c98916c [DOC] store: update documentation and add examples 2019-08-26 10:31:11 +02:00
Géry Debongnie c59abef374 [IMP] connected component: add dispatch method 2019-08-26 10:31:11 +02:00
Géry Debongnie bb4c2506c0 [REF] store: merge actions and mutations
closes #256
2019-08-26 10:31:11 +02:00
Géry Debongnie 9843d16585 [REF] component: improve render method 2019-08-26 10:31:11 +02:00
Géry Debongnie 34e463ad0a [REF] connected component: separate storeprops from props
closes #259
2019-08-26 10:31:11 +02:00
Géry Debongnie 2294c1967f [IMP] qweb: allow xml documents as template definition
Before this commit, the addTemplates method only accepts a string.  But
whenever we perform a $.get call, we get a parsed xml document. So, to
make Owl more convenient to use in that situation, we simply accept xml
documents in addition to strings.
2019-08-23 15:17:28 +02:00
Géry Debongnie 4b53e48e91 [ADD] tools: add benchmarks for 0.18.0 2019-08-22 15:26:17 +02:00
Géry Debongnie 70a70161e7 [REL] bump to v0.18.0 2019-08-22 14:56:39 +02:00
Géry Debongnie e690991a6a [FIX] component: better heuristic to determine if was mounted 2019-08-22 14:55:31 +02:00
Géry Debongnie a621a14028 [MERGE] router: add owl router
First implementation of a declarative router component/directive/plugin
2019-08-22 14:49:11 +02:00
Géry Debongnie 9754cc7521 [REF] small cleanup 2019-08-22 14:26:34 +02:00
Géry Debongnie aa268937a7 [IMP] router: navigate now returns more information 2019-08-22 14:26:34 +02:00
Géry Debongnie 3db6f0a643 [IMP] owl: add commonjs build script 2019-08-22 14:26:34 +02:00
Géry Debongnie 635a7fc72c [FIX] router: do not prevent default click on <a> in some cases 2019-08-22 14:26:34 +02:00
Géry Debongnie 6cd5ec10f0 [FIX] router: properly redirect if necessary when started 2019-08-22 14:26:34 +02:00
Géry Debongnie 0a276fbf1d [IMP] router: basic implementation of navigation guards 2019-08-22 14:26:34 +02:00
Géry Debongnie d593131cc1 [REF] router: remove useless check 2019-08-22 14:26:34 +02:00
Géry Debongnie 3c3dcb44a9 [IMP] router: add test for Link 2019-08-22 14:26:34 +02:00
Géry Debongnie 6ecd1d6ea2 [IMP] router: add validation for destinations 2019-08-22 14:26:34 +02:00
Géry Debongnie 1a931a2a08 [IMP] router: implement redirect feature 2019-08-22 14:26:34 +02:00
Géry Debongnie bf705fcd99 [REF] router: add TestRouter and improve tests 2019-08-22 14:26:34 +02:00
Géry Debongnie bb46346767 [REF] router: change Destination shape 2019-08-22 14:26:34 +02:00
Géry Debongnie cd0762b436 [REF] rework router into a class 2019-08-22 14:26:34 +02:00
Géry Debongnie ca920a0091 [ADD] router: initial implementation 2019-08-22 14:26:34 +02:00
Géry Debongnie e5b1ba24d8 [IMP] component: allow multiple mount/unmount
closes #258
2019-08-22 14:23:08 +02:00
Géry Debongnie eaf912bed7 [FIX] observer: properly handle dates 2019-08-22 14:22:20 +02:00
Géry Debongnie fd0fb5be00 [REF] observer: refactor observer using proxy
closes #253
2019-08-22 14:22:20 +02:00
Géry Debongnie f9f730ab7b [DOC] add some info about return value from actions 2019-08-22 13:05:54 +02:00
Géry Debongnie b30b72fc36 [FIX] update benchmark application 2019-08-22 13:05:37 +02:00
Alexandre Kühn ab08e84bff [IMP] store: return data from dispatching an action 2019-08-22 13:04:32 +02:00
Géry Debongnie cd9cac1246 [REF] qweb: add forceUpdate method
and make sure it only triggers an update once for multiple calls in same
call stack
2019-07-30 15:00:51 +02:00
Géry Debongnie f6369f9750 [REF] qweb: put directives in static QWeb prop
so they can be altered by outside code.  This is mostly useful for tests.
2019-07-30 14:37:39 +02:00
Géry Debongnie f18c73e59f [FIX] component: solve a tricky concurrency rendering issue
When components were initially rendered more than once, in consecutive
microtask ticks, the order of two actions was reversed: the assignation
of the vnode to the __owl__.vnode property, and the assignation of
__owl__.vnode to the final rendered vdom of the component.

As a result, the widget was rendered as null (so, not present).

The reason for this issue was that the renderPromise key was reassigned
at some point.
2019-07-30 08:54:55 +02:00
Géry Debongnie 93d0a7dd46 [FIX] store: properly handle defaultProps in connected components
Thanks to aku for issue + tests + first solution

closes #245
2019-07-16 20:51:57 +02:00
Géry Debongnie b1d022b583 [FIX] qweb: allow t-if with t-call directives
closes #246
2019-07-16 13:19:21 +02:00
Géry Debongnie 5382e824b1 [REF] owl: reorganize src code in sub files 2019-07-16 13:12:08 +02:00
Géry Debongnie 15a3f59744 [REF] owl: move some exported values around
This is a breaking change. Code using Observer, EventBus, Store or
ConnectedComponent need to update the way they import theses classes.

- Observer is now in owl.misc (so you can import it like this:
    const Observer = owl.misc.Observer;)
- EventBus is also now in owl.misc
- Store and ConnectedComponent are now together in store
2019-07-14 09:02:02 +02:00
Géry Debongnie 4a90ec7ff0 [ADD] tools: add v0.17.0 benchmark 2019-07-13 17:13:18 +02:00
Géry Debongnie 9f9b6b174e [REL] bump to v0.17.0 2019-07-13 17:06:43 +02:00
Géry Debongnie 545ceefc3d [REF] store: remove connect function, add ConnectedComponent
closes #238
closes #235
2019-07-13 17:04:36 +02:00
Géry Debongnie 591508e769 [IMP] component: allow top level widgets
closes #166
2019-07-12 14:31:34 +02:00
Géry Debongnie d9cbd23cd9 [IMP] component: add catchError hook 2019-07-12 09:52:09 +02:00
Géry Debongnie f4cd111cc8 [IMP] qweb: more information in an error message 2019-07-11 16:22:30 +02:00
Géry Debongnie ea015af741 [FIX] component: in slots, qweb was looking in wrong component
In slots, the actual parent is where the system should look for existing
widgets, not the rendering context.

closes #239
2019-07-11 15:00:05 +02:00
Géry Debongnie bfc7c81c0d [FIX] component: slots should preserve parented relation
closes #234
2019-07-10 11:01:15 +02:00
Géry Debongnie e24ff8f8aa [IMP] qweb: add renderToString method
closes #230
2019-07-09 10:16:27 +02:00
Géry Debongnie 9f1e64d399 [FIX] component: issue with t-slot,t-set,t-value, but no t-foreach
closes #232
2019-07-08 17:04:06 +02:00
Géry Debongnie 8e1aa71436 [FIX] component: scope issue with slots
Slot templates need to be able to access variables from the parent
scope.

closes #228
2019-07-08 16:24:57 +02:00
Géry Debongnie be2fc965b1 [IMP] playground: reduce duplication in responsive sample 2019-07-08 14:16:12 +02:00
Géry Debongnie 7245ccf8f9 [IMP] component: various prop validation improvements
- add optional form for list props: ['optionalField?']
- accept undefined values for optional props
- allow declaring props with only boolean true
- throw error if extra prop is given to component

closes #223
2019-06-28 16:53:14 +02:00
Géry Debongnie 1c8e1af86d [IMP] tools: improve error message display 2019-06-28 14:54:24 +02:00
Mathieu Duckerts-Antoine 60a6cca960 [IMP] qweb: better error messages for bad xml templates
closes #185
2019-06-28 14:53:19 +02:00
Géry Debongnie ad55b42ccb [IMP] playground: improve first example
Previous example did not illustrate props
2019-06-28 14:31:19 +02:00
Géry Debongnie 50c0a4b126 [DOC] improve component documentation 2019-06-28 13:00:38 +02:00
Géry Debongnie 2ca42e7470 [ADD] tools: add v0.16.0 benchmark 2019-06-28 11:51:37 +02:00
76 changed files with 19047 additions and 3787 deletions
+15 -8
View File
@@ -4,16 +4,20 @@ _A web framework for structured, dynamic and maintainable applications_
## Project Overview ## Project Overview
The Odoo Web Library (OWL) is a small The Odoo Web Library (OWL) is a small (~16kb gzipped) UI framework intended to be the basis for
UI framework intended to be the basis for the [Odoo](https://www.odoo.com/) Web Client, and hopefully many the [Odoo](https://www.odoo.com/) Web Client, and hopefully many other Odoo
other Odoo related projects. OWL's main feature is a _declarative component system_, with QWeb as a template engine, asynchronous rendering, and an underlying virtual dom. related projects. OWL's main features are:
- a _declarative component system_, (template based, with asynchronous rendering and a virtual dom)
- a store implementation (for state management)
- a small frontend router
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the OWL framework. **Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the OWL framework.
## OWL's Design Principles ## OWL's Design Principles
OWL is designed to be used in highly dynamic applications where changing OWL is designed to be used in highly dynamic applications where changing
requirements are common, code needs to be maintained by large teams. requirements are common and code needs to be maintained by large teams.
- **XML based**: templates are based on the XML format, which allows interesting - **XML based**: templates are based on the XML format, which allows interesting
applications. For example, they could be stored in a database and modified applications. For example, they could be stored in a database and modified
@@ -26,8 +30,11 @@ requirements are common, code needs to be maintained by large teams.
for various reasons (security/deployment/dynamic modules/specific assets tools), for various reasons (security/deployment/dynamic modules/specific assets tools),
it is not ok to use standard web tools based on `npm`. it is not ok to use standard web tools based on `npm`.
Owl is not designed to be fast or small (even though it is quite good on those Owl is not designed to be fast nor small (even though it is quite good on those
two topics). If you are interested in a comparison with React or Vue, you will two topics). It is a no nonsense framework to build applications. There is only
one way to define components (with classes).
If you are interested in a comparison with React or Vue, you will
find some more information [here](doc/comparison.md). find some more information [here](doc/comparison.md).
## Example ## Example
@@ -64,8 +71,8 @@ string. More interesting examples can be found on the
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.16.0.js](https://github.com/odoo/owl/releases/download/v0.16.0/owl.js) - [owl-0.19.0.js](https://github.com/odoo/owl/releases/download/v0.19.0/owl.js)
- [owl-0.16.0.min.js](https://github.com/odoo/owl/releases/download/v0.16.0/owl.min.js) - [owl-0.19.0.min.js](https://github.com/odoo/owl/releases/download/v0.19.0/owl.min.js)
Some npm scripts are available: Some npm scripts are available:
+53 -13
View File
@@ -10,6 +10,7 @@ discussed, feel free to open an issue/submit a PR to correct this text.
## Content ## Content
- [Size](#size) - [Size](#size)
- [Class Based](#class-based)
- [Tooling/Build Step](#toolingbuild-step) - [Tooling/Build Step](#toolingbuild-step)
- [Templating](#templating) - [Templating](#templating)
- [Asynchronous rendering](#asynchronous-rendering) - [Asynchronous rendering](#asynchronous-rendering)
@@ -21,12 +22,23 @@ discussed, feel free to open an issue/submit a PR to correct this text.
OWL is intended to be small and to work at a slightly lower level of abstraction OWL is intended to be small and to work at a slightly lower level of abstraction
than React and Vue. Also, jQuery is not the same kind of framework, but it is interesting to compare. than React and Vue. Also, jQuery is not the same kind of framework, but it is interesting to compare.
| Framework | Size (minified) | Size (minified, gzipped) | | Framework | Size (minified, gzipped) |
| ------------------------ | --------------- | ------------------------ | | ------------------------ | ------------------------ |
| OWL | 32kb | 11kb | | OWL | 16kb |
| Vue + VueX | | 30kb | | Vue + VueX | 30kb |
| React + ReactDOM + Redux | | 40kb | | React + ReactDOM + Redux | 40kb |
| jQuery | 86kb | 30kb | | jQuery | 30kb |
## Class Based
Both React and Vue moved away from defining components with classes. They prefer
a more functional approach, in particular, with the new `hooks` mechanisms.
This has some advantages and disadvantages. But the end result is that React
and Vue both offers multiple different ways of defining new components. In
contrast, Owl has only one mechanism: class-based components. We believe that Owl
components are fast enough for all our usecases, and making it as simple as
possible for developers is more valuable (for us).
## Tooling/Build step ## Tooling/Build step
@@ -45,7 +57,7 @@ components, which also necessitate a build step.
On the flipside, external tooling may make it harder to use in some case, but it On the flipside, external tooling may make it harder to use in some case, but it
also brings a lot of benefits. And React/Vue have both a large ecosystem. also brings a lot of benefits. And React/Vue have both a large ecosystem.
### Templating ## Templating
OWL uses its own QWeb engine, which compiles templates on the OWL uses its own QWeb engine, which compiles templates on the
frontend, as they are needed. This is extremely convenient for our use case, in frontend, as they are needed. This is extremely convenient for our use case, in
@@ -128,8 +140,10 @@ by getters/setters. With that, it can notify components whenever the state that
they read was changed. they read was changed.
Owl is closer to vue: it also tracks magically the state properties, but it does Owl is closer to vue: it also tracks magically the state properties, but it does
only increment a counter whenever it changes (and a _deep_ counter for each of only increment an internal counter whenever it changes. Note that it is done
its parents). This assumes that the state is actually a tree. with a `Proxy`, which means that it is totally transparent to the developers.
Adding new keys is supported. Once any part of the state has been changed, a
rendering is scheduled in the next microtask tick (promise queue).
## State Management ## State Management
@@ -190,7 +204,33 @@ keeps track of who get data, and retrigger a render when it was changed.
**Owl** **Owl**
Owl store is a little bit like a mix of redux and vuex: it has mutations and Owl store is a little bit like a mix of redux and vuex: it has actions (but not
actions, like VueX, it keeps track of the state changes, but it does not notify mutations), and like VueX, it keeps track of the state changes. However, it does
a component when the state changes. Instead, components need to connect to the not notify a component when the state changes. Instead, components need to connect
store like in redux, with a function that will listen to the relevant state. to the store like in redux, by inheriting the `ConnectedComponent` class.
```javascript
const actions = {
increment({ state }, val) {
state.counter += val;
}
};
const state = {
counter: 0
};
const store = new owl.Store({ state, actions });
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
increment() {
this.env.store.dispatch("increment");
}
}
const counter = new Counter({ store, qweb });
```
+131 -31
View File
@@ -21,6 +21,7 @@
- [References](#references) - [References](#references)
- [Slots](#slots) - [Slots](#slots)
- [Asynchronous Rendering](#asynchronous-rendering) - [Asynchronous Rendering](#asynchronous-rendering)
- [Error Handling](#error-handling)
## Overview ## Overview
@@ -116,19 +117,43 @@ find a template with the component name (or one of its ancestor).
type and shape of the (actual) props given to the component. If Owl mode is type and shape of the (actual) props given to the component. If Owl mode is
`dev`, this will be used to validate the props each time the component is `dev`, this will be used to validate the props each time the component is
created/updated. See [Props Validation](#props-validation) for more information. created/updated. See [Props Validation](#props-validation) for more information.
- **`defaultProps`** (Object, optional): if given, this object define default
```js
class Counter extends owl.Component {
static props = {
initialValue: Number,
optional: true
};
}
```
* **`defaultProps`** (Object, optional): if given, this object define default
values for (top-level) props. Whenever `props` are given to the object, they values for (top-level) props. Whenever `props` are given to the object, they
will be altered to add default value (if missing). Note that it does not will be altered to add default value (if missing). Note that it does not
change the initial object, a new object will be created instead. change the initial object, a new object will be created instead.
```js
class Counter extends owl.Component {
static defaultProps = {
initialValue: 0
};
}
```
### Methods ### Methods
We explain here all the public methods of the `Component` class. We explain here all the public methods of the `Component` class.
- **`mount(target)`** (async): this is the main way a component's hierarchy is added to the - **`mount(target, renderBeforeRemount=false)`** (async): this is the main way a
DOM: the root component is mounted to a target HTMLElement. Obviously, this component is added to the DOM: the root component is mounted to a target
is asynchronous, since each children need to be created as well. Most applications HTMLElement. Obviously, this is asynchronous, since each children need to be
will need to call `mount` exactly once, on the root component. created as well. Most applications will need to call `mount` exactly once, on
the root component.
The `renderBeforeRemount` argument is useful when a component is unmounted and remounted.
In that case, we may want to rerender the component _before_ it is remounted, if
we know that its state (or something in the environment, or ...) has changed.
In that case, it should simply set to `true`.
- **`unmount()`**: in case a component need to be detached/removed from the DOM, this - **`unmount()`**: in case a component need 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
@@ -160,7 +185,7 @@ We explain here all the public methods of the `Component` class.
framework instead. framework instead.
Obviously, these methods are reserved for Owl, and should not be used by Owl Obviously, these methods are reserved for Owl, and should not be used by Owl
users, unless they want to override them. Also, Owl reserves all method names users, unless they want to override them. Also, Owl reserves all method names
starting with `__`, in order to prevent possible future conflicts with user code starting with `__`, in order to prevent possible future conflicts with user code
whenever Owl needs to change. whenever Owl needs to change.
@@ -170,15 +195,16 @@ A solid and robust component system needs useful hooks/methods to help
developers write components. Here is a complete description of the lifecycle of developers write components. Here is a complete description of the lifecycle of
a owl component: a owl component:
| Method | Description | | Method | Description |
| ------------------------------------------------ | ----------------------------------------------------- | | ------------------------------------------------ | ------------------------------------------------------------ |
| **[constructor](#constructorparent-props)** | constructor | | **[constructor](#constructorparent-props)** | constructor |
| **[willStart](#willstart)** | async, before first rendering | | **[willStart](#willstart)** | async, before first rendering |
| **[mounted](#mounted)** | just after component is rendered and added to the DOM | | **[mounted](#mounted)** | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update | | **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
| **[willPatch](#willpatch)** | just before the DOM is patched | | **[willPatch](#willpatch)** | just before the DOM is patched |
| **[patched](#patchedsnapshot)** | just after the DOM is patched | | **[patched](#patchedsnapshot)** | just after the DOM is patched |
| **[willUnmount](#willunmount)** | just before removing component from DOM | | **[willUnmount](#willunmount)** | just before removing component from DOM |
| **[catchError](#catcherrorerror)** | catch errors (see [error handling section](#error-handling)) |
Notes: Notes:
@@ -235,7 +261,8 @@ At this point, the component is not yet rendered. Note that a slow `willStart` m
interface. Therefore, some care should be made to make this method as interface. Therefore, some care should be made to make this method as
fast as possible. fast as possible.
The component rendering will take place after `willStart` is completed. After the `willStart` method is completed, the state will be observed with a
new `Observer`. Then, the component will be rendered by `QWeb`.
#### `mounted()` #### `mounted()`
@@ -319,6 +346,12 @@ the DOM. This is a good place to remove some listeners, for example.
This is the opposite method of `mounted`. This is the opposite method of `mounted`.
#### `catchError(error)`
The `catchError` method is useful when we need to intercept and properly react
to (rendering) errors that occur in some sub components. See the section on
[error handling](#error-handling)
### Root Component ### Root Component
Most of the time, an Owl component will be created automatically by a tag (or the `t-component` Most of the time, an Owl component will be created automatically by a tag (or the `t-component`
@@ -353,22 +386,22 @@ easy to test a component.
Updating the environment is not as simple as changing a component's state: its 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 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 user interface. There is however a mechanism to force root widgets to rerender
themselves whenever the environment is modified: one only needs to trigger the themselves whenever the environment is modified: one only needs to trigger the
`update` event on the QWeb instance. For example, a responsive environment `update` event on the QWeb instance. For example, a responsive environment
could be programmed like this: could be programmed like this:
```js ```js
function setupResponsivePlugin(env) { function setupResponsivePlugin(env) {
const isMobile = () => window.innerWidth <= 768; const isMobile = () => window.innerWidth <= 768;
env.isMobile = isMobile(); env.isMobile = isMobile();
const updateEnv = owl.utils.debounce(() => { const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) { if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile; env.isMobile = !env.isMobile;
env.qweb.trigger('update'); env.qweb.trigger("update");
} }
}, 15); }, 15);
window.addEventListener("resize", updateEnv); window.addEventListener("resize", updateEnv);
} }
``` ```
@@ -786,7 +819,8 @@ of the props. Here is how it works in Owl:
- 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 [tooling page](tooling.md#development-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 only validates keys defined in (static) `props`. Additional keys in (component) `props` are not validated. - it validates keys defined in (static) `props`. Additional keys given by the
parent will cause an error.
For example: For example:
@@ -814,15 +848,16 @@ class ComponentB extends owl.Component {
- it is an object or a list of strings - it is an object or a list of strings
- a list of strings is a simplified props definition, which only lists the name - a list of strings is a simplified props definition, which only lists the name
of the props. of the props. Also, if the name ends with `?`, it is considered optional.
- all props are by default required, unless they are defined with `optional: true` - all props are by default required, unless they are defined with `optional: true`
(in that case, validation is only done if there is a value) (in that case, validation is only done if there is a value)
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all - valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
constructor functions (so, if you have a `Person` class, it can be used as a type) constructor functions (so, if you have a `Person` class, it can be used as a type)
- arrays are homogeneous (all elements have the same type/shape) - arrays are homogeneous (all elements have the same type/shape)
For each key, a `prop` definition is either a constructor, a list of constructors, or an object: For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
- a boolean: indicate that the props exists, and is mandatory.
- a constructor: this should describe the type, for example: `id: Number` describe - a constructor: this should describe the type, for example: `id: Number` describe
the props `id` as a number the props `id` as a number
- a list of constructors. In that case, this means that we allow more than one - a list of constructors. In that case, this means that we allow more than one
@@ -840,6 +875,11 @@ Examples:
static props = ['message', 'id', 'date']; static props = ['message', 'id', 'date'];
``` ```
```js
// size is optional
static props = ['message', 'size?'];
```
```js ```js
static props = { static props = {
messageIds: {type: Array, element: Number}, // list of number messageIds: {type: Array, element: Number}, // list of number
@@ -854,7 +894,8 @@ Examples:
url: String url: String
]}, // object, with keys id (number), name (string, optional) and url (string) ]}, // object, with keys id (number), name (string, optional) and url (string)
someFlag: Boolean, // a boolean, mandatory (even if `false`) someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date] // either a boolean or a date someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop
}; };
``` ```
@@ -994,3 +1035,62 @@ Here are a few tips on how to work with asynchronous components:
<AsyncChild t-asyncroot="1"/> <AsyncChild t-asyncroot="1"/>
</div> </div>
``` ```
### Error Handling
By default, whenever an error occurs in the rendering of an Owl application, we
destroy the whole application. Otherwise, we cannot offer any guarantee on the
state of the resulting component tree. It might be hopelessly corrupted, but
without any user-visible state.
Clearly, it sometimes is a little bit extreme to destroy the application. This
is why we have a builtin mechanism to handle rendering errors (and errors coming
from lifecycle hooks): the `catchError` hook.
Whenever the `catchError` lifecycle hook is implemented, all errors coming from
sub components rendering and/or lifecycle method calls will be caught and given
to the `catchError` method. This allow us to properly handle the error, and to
not break the application.
For example, here is how we could implement an `ErrorBoundary` component:
```xml
<div t-name="ErrorBoundary">
<t t-if="state.error">
Error handled
</t>
<t t-else="1">
<t t-slot="default" />
</t>
</div>
```
```js
class ErrorBoundary extends Widget {
state = { error: false };
catchError() {
this.state.error = true;
}
}
```
Using the `ErrorBoundary` is then extremely simple:
```xml
<ErrorBoundary><SomeOtherComponent/></ErrorBoundary>
```
Note that we need to be careful here: the fallback UI should not throw any
error, otherwise we risk going into an infinite loop.
Also, it may be useful to know that whenever an error is caught, it is then
broadcasted to the application by an event on the `qweb` instance. It may be
useful, for example, to log the error somewhere.
```js
env.qweb.on("error", null, function(error) {
// do something
// react to the error
});
```
+7
View File
@@ -122,6 +122,13 @@ It's API is quite simple:
const vnode = qweb.render("App", component); const vnode = qweb.render("App", component);
``` ```
- **`renderToString(name, context)`**: renders a template, but returns an html
string.
```js
const str = qweb.renderToString("someTemplate", somecontext);
```
- **`register(name, Component)`**: static function to register an OWL Component - **`register(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-component` directive). This is useful for commonly used templates (see the `t-component` directive). This is useful for commonly used
+28
View File
@@ -1,5 +1,31 @@
# 🦉 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
QWeb
core
EventBus
Observer
router
Link
Router
store
Store
ConnectedComponent
utils
debounce
escape
loadJS
loadTemplates
whenReady
```
## Reference ## Reference
- [Animations](animations.md) - [Animations](animations.md)
@@ -7,6 +33,7 @@
- [Event Bus](event_bus.md) - [Event Bus](event_bus.md)
- [Observer](observer.md) - [Observer](observer.md)
- [QWeb](qweb.md) - [QWeb](qweb.md)
- [Router](router.md)
- [Store](store.md) - [Store](store.md)
- [Utils](utils.md) - [Utils](utils.md)
- [Virtual DOM](vdom.md) - [Virtual DOM](vdom.md)
@@ -19,3 +46,4 @@
- [Comparison with React/Vue](comparison.md) - [Comparison with React/Vue](comparison.md)
- [Tooling](tooling.md) - [Tooling](tooling.md)
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
+172
View File
@@ -0,0 +1,172 @@
# 🦉 Router 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Route Definition](#route-definition)
- [Router](#router)
- [Navigation Guards](#navigation-guards)
- [`t-routecomponent`](#t-routecomponent)
- [Link](#link)
## Overview
It is often useful to organize an application around urls. If the application is
a single page application, then we need a way to manage those urls in the browser.
This is why there are many different routers for different frameworks. A generic
router can do the job just fine, but a specialized router for Owl can give a
better developer experience.
The Owl router support the following features:
- `history` or `hash` mode
- declarative routes
- route redirection
- navigation guards
- parameterized routes
- a `<Link>` component
- a `t-routecomponent` directive
Note that it is still in early stage of developments, and there are probably
still some issues.
## Example
To use the Owl router, there are some steps that needs to be done:
- declare some routes
- create a router
- add it to the environment
```js
async function protectRoute({ env, to }) {
if (!env.session.authUser) {
env.session.setNextRoute(to.name);
return { to: "SIGN_IN" };
}
return true;
}
export const ROUTES = [
{ name: "LANDING", path: "/", component: Landing },
{ name: "TASK", path: "/tasks/{{id}}", component: Task },
{ name: "SIGN_UP", path: "/signup", component: SignUp },
{ name: "SIGN_IN", path: "/signin", component: SignIn },
{ name: "ADMIN", path: "/admin", component: Admin, beforeRouteEnter: protectRoute },
{ name: "ACCOUNT", path: "/account", component: Account, beforeRouteEnter: protectRoute },
{ name: "UNKNOWN", path: "*", redirect: { to: "LANDING" } }
];
function makeEnvironment() {
...
const env = { qweb };
env.session = new Session(env);
env.router = new owl.router.Router(env, ROUTES);
await env.router.start();
return env;
}
```
Notice that the router needs to be started. This is an asynchronous operation
because it needs to apply the potential navigation guards on the current route
(which may or may not mean that the application is redirected to another route).
## Reference
### Route definition
A route need to be defined as an object with the following keys:
- `name` (optional): a (unique) string useful to identify the current route. If not
given, it will be assigned an automatic name,
- `path`: a string describing the url. It can be static: `/admin` or dynamic: `/users/{{id}}`.
It also can be `*`, to catch all remaining routes.
- `component` (optional): an Owl component that will be used by the `t-routecomponent`
directive if the route is active
- `redirect` (optional): should be destination object (with optional keys `path`, `to` and `params`) if given, the application will be redirected to the destination whenever we match this route
- `beforeRouteEnter`: defines a [navigation guard](#navigation-guards).
### `Router`
The `Router` constructor takes three arguments:
- `env`: a valid environment,
- a list of routes,
- an optional object (with the only key `mode` which can be `history` (default
value) or `hash`).
```js
const ROUTES = [...];
const router = new owl.router.Router(env, ROUTES, {mode: 'history'});
```
Note that the route are defined in a list, and the order matters: the router
tries to find a match by going down the list.
The router needs to be added to the environment in the `router` sub key.
Once a router is created, it needs to be started. This is necessary to initialize
its current state to the current URL (and also, to potentially apply any
navigation guards and/or redirecting).
```js
await router.start();
```
Once started, the router will keep track of the current url and reflect its
value in two keys:
- `router.currentRoute`
- `router.currentParams`
The router also has a `navigate` method, useful to programmatically change the
application to another state (and the url):
```js
router.navigate({to: 'USER', params: {id: 51}});
```
### Navigation Guards
Navigation guards are very useful to be able to execute some business logic/
perform some actions or redirect to other routes whenever the application is
entering a new route. For example, the following guard checks if there is an
authenticated user, and if it is not the case, redirect to the sign in route.
```js
async function protectRoute({ env, to }) {
if (!env.session.authUser) {
env.session.setNextRoute(to.name);
return { to: "SIGN_IN" };
}
return true;
}
```
A navigation guard is a function that returns a promise, which either resolves
to `true` (the navigation is accepted), or to another destination object.
### `t-routecomponent`
The `t-routecomponent` directive directs Owl to render the component associated
to the currently active route (if any):
```xml
<div t-name="App">
<NavBar />
<t t-routecomponent="1"/>
</div>
```
### `Link`
The `Link` component is a Owl component which render as a `<a>` tag with any
content. It will compute the proper href from its props, and allow Owl to
properly navigate to a given url if clicked on it.
```xml
<Link to="'HOME'">Home</Link>
```
+115 -84
View File
@@ -5,8 +5,7 @@
- [Overview](#overview) - [Overview](#overview)
- [Example](#example) - [Example](#example)
- [Reference](#reference) - [Reference](#reference)
- [Public API](#public-api) - [Store](#store)
- [Mutations](#mutations)
- [Actions](#actions) - [Actions](#actions)
- [Getters](#getters) - [Getters](#getters)
- [Connecting a Component](#connecting-a-component) - [Connecting a Component](#connecting-a-component)
@@ -22,11 +21,10 @@ in various parts of the user interface, and then, it is not obvious which
component should own which part of the state. component should own which part of the state.
Owl's solution to this issue is a centralized store. It is a class that owns Owl's solution to this issue is a centralized store. It is a class that owns
some state, and let the developer update it in a structured way (through some state, and let the developer update it in a structured way, with `actions`.
mutations and actions). Owl components can then connect to the store, and will Owl components can then connect to the store, and will be updated if necessary.
be updated if necessary.
Note: Owl's store is inspired by React Redux and VueX. Note: Owl store is inspired by React Redux and VueX.
## Example ## Example
@@ -34,19 +32,12 @@ Here is what a simple store looks like:
```js ```js
const actions = { const actions = {
addTodo({ commit }, message) {
commit("addTodo", message);
}
};
const mutations = {
addTodo({ state }, message) { addTodo({ state }, message) {
const todo = { state.todos.push({
id: state.nextId++, id: state.nextId++,
message, message,
isCompleted: false isCompleted: false
}; });
state.todos.push(todo);
} }
}; };
@@ -55,66 +46,65 @@ const state = {
nextId: 1 nextId: 1
}; };
const store = new owl.Store({ state, actions, mutations }); const store = new owl.Store({ state, actions });
store.on("update", () => console.log(store.state)); store.on("update", () => console.log(store.state));
// updating the state // updating the state
store.dispatch("addTodo", "fix all bugs"); store.dispatch("addTodo", "fix all bugs");
``` ```
This example shows how a store can be defined and used. Note that in most cases,
actions will be dispatched by connected components.
## Reference ## Reference
The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events whenever its ### `Store`
state is changed. Note that these events are triggered only after a microtask
tick, so only one event will be triggered for any number of state changes in a The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events
whenever its state is changed. Note that these events are triggered only after a
microtask tick, so only one event will be triggered for any number of state changes in a
call stack. call stack.
Also, it is important to mention that the state is observed (with an `owl.Observer`), Also, it is important to mention that the state is observed (with an `owl.Observer`),
which is the reason why it is able to know if it was changed. This implies that which is the reason why it is able to know if it was changed. See the
state changes need to be done carefully in some cases (adding a new key to an
object, or modifying an array with the `arr[i] = newValue` syntax). See the
[Observer](observer.md)'s documentation for more details. [Observer](observer.md)'s documentation for more details.
### Public API The `Store` class is quite small. It has two public methods:
1. `constructor` - its constructor
2. `commit` - `dispatch`
3. `dispatch`
### Mutations The constructor takes a configuration object with four (optional) keys:
Mutations are the only way to modify the state. Changing the state outside a - the initial state
mutation is not allowed (and should throw an error). Mutations are synchronous. - the actions
- the getters
- the environment
```js ```javascript
const mutations = { const config = {
setLoginState({ state }, loginState) { state,
state.loginState = loginState; actions,
} getters,
env
}; };
``` const store = new Store(config);
Mutations are called with the `commit` method on the store, and can receive an arbitrary number of arguments.
```js
store.commit("setLoginState", "error");
``` ```
### Actions ### Actions
Actions are used to coordinate state changes. It is also useful whenever some Actions are used to coordinate state changes. It can be used for both synchronous
asynchronous logic is necessary. For example, fetching data should be done and asynchronous logic.
in an action.
```js ```js
const actions = { const actions = {
async login({ commit }, info) { async login({ state }, info) {
commit("setLoginState", "pending"); state.loginState = "pending";
try { try {
const loginInfo = await doSomeRPC("/login/", info); const loginInfo = await doSomeRPC("/login/", info);
commit("setLoginState", loginInfo); state.loginState = loginInfo;
} catch { } catch {
commit("setLoginState", "error"); state.loginState = "error";
} }
} }
}; };
@@ -127,6 +117,39 @@ arbitrary number of arguments.
store.dispatch("login", someInfo); store.dispatch("login", someInfo);
``` ```
Note that anything returned by an action will also be returned by the `dispatch`
call.
Also, it is important to be aware that we need to be careful with asynchronous
logic. Each state change will potentially trigger a rerendering, so we need to
make sure that we do not have a partial corrupted state. Here is an example that
is likely not a good idea:
```javascript
const actions = {
async fetchSomeData({ state }, recordId) {
state.recordId = recordId;
const data = await doSomeRPC("/read/", recordId);
state.recordData = data;
}
};
```
In the previous example, there is a period of time in which the state has a
`recordId` which does not correspond to the `recordData`. It is more likely that
we want an atomic update: updating the `recordId` at the same time as the `recordData`
values:
```javascript
const actions = {
async fetchSomeData({ state }, recordId) {
const data = await doSomeRPC("/read/", recordId);
state.recordId = recordId;
state.recordData = data;
}
};
```
### Getters ### Getters
Usually, data contained in the store will be stored in a normalized way. For Usually, data contained in the store will be stored in a normalized way. For
@@ -165,47 +188,41 @@ const getters = {
const post = store.getters.getPost(id); const post = store.getters.getPost(id);
``` ```
Getters take *at most* one argument. Getters take _at most_ one argument.
Note that getters are cached if they don't take any argument, or their argument Note that getters are cached if they don't take any argument, or their argument
is a string or a number. is a string or a number.
### Connecting a Component ### Connecting a Component
By default, an Owl `Component` is not connected to any store. The `connect` At some point, we need a way to access the state in the store from a component.
function is there to create sub Components that are connected versions of By default, an Owl `Component` is not connected to any store. To do that, we
Components. need to create a component inheriting from `OwlComponent`:
```javascript ```javascript
const actions = { const actions = {
increment({ commit }) {
commit("increment", 1);
}
};
const mutations = {
increment({ state }, val) { increment({ state }, val) {
state.counter += val; state.counter += val;
} }
}; };
const state = { const state = {
counter: 0 counter: 0
}; };
const store = new owl.Store({ state, actions, mutations }); const store = new owl.Store({ state, actions });
class Counter extends owl.Component { class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
increment() { increment() {
this.env.store.dispatch("increment"); this.env.store.dispatch("increment");
} }
} }
function mapStoreToProps(state) {
return {
value: state.counter
};
}
const ConnectedCounter = owl.connect(Counter, mapStoreToProps);
const counter = new ConnectedCounter({ store, qweb }); const counter = new Counter({ store, qweb });
``` ```
```xml ```xml
@@ -214,45 +231,59 @@ const counter = new ConnectedCounter({ store, qweb });
</button> </button>
``` ```
The arguments of `connect` are: The `ConnectedComponent` class can be configured with the following fields:
- `Counter`: an owl `Component` to connect
- `mapStoreToProps`: a function that extracts the `props` of the Component - `mapStoreToProps`: a function that extracts the `props` of the Component
from the `state` of the `Store` and returns them as a dict from the `state` of the `Store` and returns them as a dict.
- `options`: dictionary of optional parameters that may contain - `getStore`: a function that takes the `env` in arguments and returns an
- `getStore`: a function that takes the `env` in arguments and returns an instance of `Store` to connect to (if not given, connects to `env.store`)
instance of `Store` to connect to (if not given, connects to `env.store`) - `hashFunction`: the function to use to detect changes in the state (if not
- `hashFunction`: the function to use to detect changes in the state (if not given, generates a function that uses revision numbers, incremented at
given, generates a function that uses revision numbers, incremented at each state change)
each state change) - `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch
- `deep`: [only useful if no hashFunction is given] if false, only watch for top level state changes (`true` by default)
for top level state changes (true by default)
The `connect` function returns a sub class of the given `Component` which is Note that the class `ConnectedComponent` has a `dispatch` method. This means
connected to the `store`. that the previous example could be simplified like this:
```javascript
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
}
```
```xml
<button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="props.value"/>]
</button>
```
### Semantics ### Semantics
The `Store` and the `connect` function try to be smart and to optimize as much The `Store` and the `ConnectedComponent` try to be smart and to optimize as much
as possible the rendering and update process. What is important to know is: as possible the rendering and update process. What is important to know is:
- components are always updated in the order of their creation (so, parent - components are always updated in the order of their creation (so, parent
before children) before children)
- they are updated only if they are in the DOM - they are updated only if they are in the DOM
- if a parent is asynchronous, the system will wait for it to complete its - if a parent is asynchronous, the system will wait for it to complete its
update before updating other components. update before updating other components.
- in general, updates are not coordinated. This is not a problem for synchronous - in general, updates are not coordinated. This is not a problem for synchronous
components, but if there are many asynchronous components, this could lead to components, but if there are many asynchronous components, this could lead to
a situation where some part of the UI is updated and other parts of the UI is a situation where some part of the UI is updated and other parts of the UI is
not updated. not updated.
### Good Practices ### Good Practices
- avoid asynchronous components as much as possible. Asynchronous components - avoid asynchronous components as much as possible. Asynchronous components
lead to situations where parts of the UI is not updated immediately. lead to situations where parts of the UI is not updated immediately.
- do not be afraid to connect many components, parent or children if needed. For - do not be afraid to connect many components, parent or children if needed. For
example, a `MessageList` component could get a list of ids in its `mapStoreToProps` and a `Message` component could get the data of its own example, a `MessageList` component could get a list of ids in its `mapStoreToProps` and a `Message` component could get the data of its own
message message
- since the `mapStoreToProps` function is called for each connected component, - since the `mapStoreToProps` function is called for each connected component,
for each state update, it is important to make sure that these functions are for each state update, it is important to make sure that these functions are
as fast as possible. as fast as possible.
+3
View File
@@ -37,6 +37,9 @@ owl.__info__.mode = "dev";
Note that templates compiled with the `prod` settings will not be recompiled. Note that templates compiled with the `prod` settings will not be recompiled.
So, changing this setting is best done at startup. 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
+5 -4
View File
@@ -1,12 +1,13 @@
{ {
"name": "owl", "name": "owl",
"version": "0.16.0", "version": "0.19.0",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "scripts": {
"build:js": "tsc --target esnext --module es6 --outDir dist/owl", "build:js": "tsc --target esnext --module es6 --outDir dist/owl",
"build:bundle": "rollup -c", "build:bundle": "rollup -c",
"build": "npm run build:js && npm run build:bundle", "build": "npm run build:js && npm run build:bundle",
"buildcommonjs": "npm run build:js && npm run build:bundle -- -f cjs",
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle", "minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
@@ -30,6 +31,7 @@
"cpx": "^1.5.0", "cpx": "^1.5.0",
"git-rev-sync": "^1.12.0", "git-rev-sync": "^1.12.0",
"jest": "^23.6.0", "jest": "^23.6.0",
"jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1", "live-server": "^1.2.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"rollup": "^1.6.0", "rollup": "^1.6.0",
@@ -38,8 +40,7 @@
"source-map-support": "^0.5.10", "source-map-support": "^0.5.10",
"ts-jest": "^23.10.5", "ts-jest": "^23.10.5",
"typescript": "^3.2.2", "typescript": "^3.2.2",
"uglify-es": "^3.3.9", "uglify-es": "^3.3.9"
"jest-environment-jsdom": "^24.7.1"
}, },
"dependencies": {}, "dependencies": {},
"jest": { "jest": {
@@ -62,6 +63,6 @@
] ]
}, },
"prettier": { "prettier": {
"printWidth": 100 "printWidth": 100
} }
} }
+192 -139
View File
@@ -1,6 +1,8 @@
import { Observer } from "./observer"; import { Observer } from "../core/observer";
import { QWeb, CompiledTemplate } from "./qweb_core"; import { CompiledTemplate, QWeb } from "../qweb/index";
import { h, patch, VNode } from "./vdom"; import { h, patch, VNode } from "../vdom/index";
import "./directive";
import "./props_validation";
/** /**
* Owl Component System * Owl Component System
@@ -28,6 +30,7 @@ import { h, patch, VNode } from "./vdom";
*/ */
export interface Env { export interface Env {
qweb: QWeb; qweb: QWeb;
[key: string]: any;
} }
/** /**
@@ -35,25 +38,36 @@ export interface Env {
* useful to typecheck and describe the internal keys used by Owl to manage the * useful to typecheck and describe the internal keys used by Owl to manage the
* component tree. * component tree.
*/ */
export interface Meta<T extends Env, Props> { interface Internal<T extends Env, Props> {
// each component has a unique id, useful mostly to handle parent/child
// relationships
readonly id: number; readonly id: number;
vnode: VNode | null; vnode: VNode | null;
isMounted: boolean; isMounted: boolean;
isDestroyed: boolean; isDestroyed: boolean;
// parent and children keys are obviously useful to setup the parent-children
// relationship.
parent: Component<T, any, any> | null; parent: Component<T, any, any> | null;
children: { [key: number]: Component<T, any, any> }; children: { [key: number]: Component<T, any, any> };
// children mapping: from templateID to componentID // children mapping: from templateID to componentID. templateID identifies a
// should it be a map number => Component? // place in a template. The t-component directive needs it to be able to get
// the component instance back whenever the template is rerendered.
cmap: { [key: number]: number }; cmap: { [key: number]: number };
renderId: number; renderId: number;
// the renderProps and renderPromise keys are only useful for the "prepare"
// step of the lifecycle of a component. Once a component has been rendered
// and patched, it is no longer useful.
renderProps: Props | null; renderProps: Props | null;
renderPromise: Promise<VNode> | null; renderPromise: Promise<VNode> | null;
boundHandlers: { [key: number]: any }; boundHandlers: { [key: number]: any };
observer?: Observer; observer: Observer | null;
render?: CompiledTemplate; render: CompiledTemplate | null;
mountedHandlers: { [key: number]: Function }; mountedHandlers: { [key: number]: Function };
classObj?: { [key: string]: boolean }; classObj: { [key: string]: boolean } | null;
} }
// If a component does not define explicitely a template // If a component does not define explicitely a template
@@ -68,7 +82,7 @@ const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
let nextId = 1; let nextId = 1;
export class Component<T extends Env, Props extends {}, State extends {}> { export class Component<T extends Env, Props extends {}, State extends {}> {
readonly __owl__: Meta<Env, Props>; readonly __owl__: Internal<Env, Props>;
template?: string; template?: string;
/** /**
@@ -119,9 +133,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (defaultProps) { if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps); props = this.__applyDefaultProps(props, defaultProps);
} }
if (QWeb.dev) {
this.__validateProps(props || {});
}
// is this a good idea? // is this a good idea?
// Pro: if props is empty, we can create easily a component // Pro: if props is empty, we can create easily a component
// Con: this is not really safe // Con: this is not really safe
@@ -135,6 +146,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
parent.__owl__.children[id] = this; parent.__owl__.children[id] = this;
} else { } else {
this.env = parent; this.env = parent;
if (QWeb.dev) {
// we only validate props for root widgets here. "Regular" widget
// props are validated by the t-component directive
QWeb.utils.validateProps(this.constructor, this.props);
}
this.env.qweb.on("update", this, () => { this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) { if (this.__owl__.isMounted) {
this.render(true); this.render(true);
@@ -161,7 +177,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
renderPromise: null, renderPromise: null,
renderProps: props || null, renderProps: props || null,
boundHandlers: {}, boundHandlers: {},
mountedHandlers: {} mountedHandlers: {},
observer: null,
render: null,
classObj: null
}; };
} }
@@ -241,6 +260,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
*/ */
willUnmount() {} willUnmount() {}
/**
* catchError is a method called whenever some error happens in the rendering or
* lifecycle hooks of a child.
*/
catchError(error: Error): void {}
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Public // Public
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -250,14 +275,26 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* *
* This should only be done if the component was created manually. Components * This should only be done if the component was created manually. Components
* created declaratively in templates are managed by the Owl system. * created declaratively in templates are managed by the Owl system.
*
* Note that a component can be mounted an unmounted several times
*/ */
async mount(target: HTMLElement): Promise<void> { async mount(target: HTMLElement, renderBeforeRemount: boolean = false): Promise<void> {
const vnode = await this.__prepare(); const __owl__ = this.__owl__;
if (this.__owl__.isDestroyed) { if (__owl__.isMounted) {
// component was destroyed before we get here...
return; return;
} }
this.__patch(vnode); if (!__owl__.vnode) {
const vnode = await this.__prepare();
if (__owl__.isDestroyed) {
// component was destroyed before we get here...
return;
}
this.__patch(vnode);
} else if (renderBeforeRemount) {
const patchQueue = [];
await this.__render(false, patchQueue, undefined, undefined);
this.__applyPatchQueue(<any[]>patchQueue);
}
target.appendChild(this.el!); target.appendChild(this.el!);
if (document.body.contains(target)) { if (document.body.contains(target)) {
@@ -265,6 +302,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
} }
/**
* The unmount method is the opposite of the mount method. It is useful
* to call willUnmount calls and remove the component from the DOM.
*/
unmount() { unmount() {
if (this.__owl__.isMounted) { if (this.__owl__.isMounted) {
this.__callWillUnmount(); this.__callWillUnmount();
@@ -272,20 +313,26 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
} }
async render(force: boolean = false, patchQueue?: any[]): Promise<void> { /**
* The render method is the main entry point to render a component (once it
* is ready. This method is not initially called when the component is
* rendered the first time).
*
* This method will cause all its sub components to potentially rerender
* themselves. Note that `render` is not called if a component is updated via
* its props.
*/
async render(force: boolean = false): Promise<void> {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (!__owl__.isMounted) { if (!__owl__.isMounted) {
return; return;
} }
const shouldPatch: boolean = !patchQueue; const patchQueue = [];
if (shouldPatch) {
patchQueue = [];
}
const renderVDom = this.__render(force, patchQueue);
const renderId = __owl__.renderId;
await renderVDom;
if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) { const renderId = ++__owl__.renderId;
await this.__render(force, patchQueue, undefined, undefined);
if (__owl__.isMounted && renderId === __owl__.renderId) {
// we only update the vnode and the actual DOM if no other rendering // we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called. // occurred between now and when the render method was initially called.
this.__applyPatchQueue(<any[]>patchQueue); this.__applyPatchQueue(<any[]>patchQueue);
@@ -362,6 +409,17 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
// Private // Private
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
/**
* Private helper to perform a full destroy, from the point of view of an Owl
* component. It does not remove the el (this is done only once on the top
* level destroyed component, for performance reasons).
*
* The job of this method is mostly to call willUnmount hooks, and to perform
* all necessary internal cleanup.
*
* Note that it does not call the __callWillUnmount method to avoid visiting
* all children many times.
*/
__destroy(parent: Component<any, any, any> | null) { __destroy(parent: Component<any, any, any> | null) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted; const isMounted = __owl__.isMounted;
@@ -396,7 +454,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
for (let key in handlers) { for (let key in handlers) {
handlers[key](); handlers[key]();
} }
this.mounted(); try {
this.mounted();
} catch (e) {
errorHandler(e, this);
}
} }
__callWillUnmount() { __callWillUnmount() {
@@ -412,10 +474,16 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
} }
/**
* The __updateProps method is called by the t-component directive whenever
* it updates a component (so, when the parent template is rerendered).
*/
async __updateProps( async __updateProps(
nextProps: Props, nextProps: Props,
forceUpdate: boolean = false, forceUpdate: boolean = false,
patchQueue?: any[] patchQueue?: any[],
scope?: any,
vars?: any
): Promise<void> { ): Promise<void> {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps); const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
if (shouldUpdate) { if (shouldUpdate) {
@@ -423,34 +491,36 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (defaultProps) { if (defaultProps) {
nextProps = this.__applyDefaultProps(nextProps, defaultProps); nextProps = this.__applyDefaultProps(nextProps, defaultProps);
} }
if (QWeb.dev) {
this.__validateProps(nextProps);
}
await this.willUpdateProps(nextProps); await this.willUpdateProps(nextProps);
this.props = nextProps; this.props = nextProps;
await this.render(forceUpdate, patchQueue); await this.__render(forceUpdate, patchQueue, scope, vars);
} }
} }
/**
* Main patching method. We call the virtual dom patch method here to convert
* a virtual dom vnode into some actual dom.
*/
__patch(vnode) { __patch(vnode) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
__owl__.renderPromise = null;
const target = __owl__.vnode || document.createElement(vnode.sel!); const target = __owl__.vnode || document.createElement(vnode.sel!);
if (this.__owl__.classObj) {
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, this.__owl__.classObj);
}
__owl__.vnode = patch(target, vnode); __owl__.vnode = patch(target, vnode);
} }
__prepare(): Promise<VNode> { __prepare(scope?: Object, vars?: any): Promise<VNode> {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
__owl__.renderProps = this.props; __owl__.renderProps = this.props;
__owl__.renderPromise = this.__prepareAndRender(); __owl__.renderPromise = this.__prepareAndRender(scope, vars);
return __owl__.renderPromise; return __owl__.renderPromise;
} }
async __prepareAndRender(): Promise<VNode> { async __prepareAndRender(scope?: Object, vars?: any): Promise<VNode> {
await this.willStart(); try {
await this.willStart();
} catch (e) {
errorHandler(e, this);
return Promise.resolve(h("div"));
}
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (__owl__.isDestroyed) { if (__owl__.isDestroyed) {
return Promise.resolve(h("div")); return Promise.resolve(h("div"));
@@ -481,27 +551,38 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
__owl__.render = qweb.render.bind(qweb, this.template); __owl__.render = qweb.render.bind(qweb, this.template);
this.__observeState(); this.__observeState();
return this.__render();
return this.__render(false, [], scope, vars);
} }
async __render(force: boolean = false, patchQueue: any[] = []): Promise<VNode> { __render(
force: boolean = false,
patchQueue: any[] = [],
scope?: Object,
vars?: any
): Promise<VNode> {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
__owl__.renderId++;
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
const patch: any[] = [this]; const patch: any[] = [this];
if (__owl__.isMounted) { patchQueue.push(patch);
patchQueue.push(patch);
}
if (__owl__.observer) { if (__owl__.observer) {
__owl__.observer.allowMutations = false; __owl__.observer.allowMutations = false;
} }
let vnode = __owl__.render!(this, { let vnode;
promises, try {
handlers: __owl__.boundHandlers, vnode = __owl__.render!(this, {
mountedHandlers: __owl__.mountedHandlers, promises,
forceUpdate: force, handlers: __owl__.boundHandlers,
patchQueue mountedHandlers: __owl__.mountedHandlers,
}); forceUpdate: force,
patchQueue,
scope,
vars
});
} catch (e) {
vnode = __owl__.vnode || h("div");
errorHandler(e, this);
}
patch.push(vnode); patch.push(vnode);
if (__owl__.observer) { if (__owl__.observer) {
__owl__.observer.allowMutations = true; __owl__.observer.allowMutations = true;
@@ -513,9 +594,15 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
// parent component. With this, we make sure that the parent component will be // parent component. With this, we make sure that the parent component will be
// able to patch itself properly after // able to patch itself properly after
vnode.key = __owl__.id; vnode.key = __owl__.id;
__owl__.renderProps = this.props;
__owl__.renderPromise = Promise.all(promises).then(() => vnode); // we applly here the class information described on the component by the
return __owl__.renderPromise; // template (so, something like <MyComponent class="..."/>) to the actual
// root vnode
if (__owl__.classObj) {
vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj);
}
return Promise.all(promises).then(() => vnode);
} }
/** /**
@@ -544,11 +631,15 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
} }
/**
* Enable the observe feature on the state. We only create an observer if
* there is some state to be observed.
*/
__observeState() { __observeState() {
if (this.state) { if (this.state) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
__owl__.observer = new Observer(); __owl__.observer = new Observer();
__owl__.observer.observe(this.state); this.state = __owl__.observer.observe(this.state);
__owl__.observer.notifyCB = this.render.bind(this); __owl__.observer.notifyCB = this.render.bind(this);
} }
} }
@@ -560,7 +651,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* prop object * prop object
*/ */
__applyDefaultProps(props: Object | undefined, defaultProps: Object): Props { __applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
props = props ? Object.create(props) : {}; 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];
@@ -577,96 +668,58 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* 3) Call 'patched' on the component of each patch, in inverse order * 3) Call 'patched' on the component of each patch, in inverse order
*/ */
__applyPatchQueue(patchQueue: any[]) { __applyPatchQueue(patchQueue: any[]) {
const patchLen = patchQueue.length; let component = this;
for (let i = 0; i < patchLen; i++) { try {
const patch = patchQueue[i]; const patchLen = patchQueue.length;
patch.push(patch[0].willPatch()); for (let i = 0; i < patchLen; i++) {
} const patch = patchQueue[i];
for (let i = 0; i < patchLen; i++) { component = patch[0];
const patch = patchQueue[i]; patch.push(patch[0].willPatch());
patch[0].__patch(patch[1]);
}
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i];
patch[0].patched(patch[2]);
}
}
/**
* Validate the component props (or next props) against the (static) props
* description. This is potentially an expensive operation: it may needs to
* visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode.
*/
__validateProps(props: Object) {
const propsDef = (<any>this.constructor).props;
if (propsDef instanceof Array) {
// list of strings (prop names)
for (let i = 0, l = propsDef.length; i < l; i++) {
if (!(propsDef[i] in props)) {
throw new Error(`Missing props '${propsDef[i]}' (component '${this.constructor.name}')`);
}
} }
} else if (propsDef) { for (let i = 0; i < patchLen; i++) {
// propsDef is an object now const patch = patchQueue[i];
for (let propName in propsDef) { patch[0].__patch(patch[1]);
if (!(propName in props)) {
if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propName}' (component '${this.constructor.name}')`);
} else {
break;
}
}
let isValid = isValidProp(props[propName], propsDef[propName]);
if (!isValid) {
throw new Error(
`Props '${propName}' of invalid type in component '${this.constructor.name}'`
);
}
} }
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i];
component = patch[0];
patch[0].patched(patch[2]);
}
} catch (e) {
errorHandler(e, component);
} }
} }
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Prop validation helper // Error handling
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
/** /**
* Check if an invidual prop value matches its (static) prop definition * This is the global error handler for errors occurring in Owl main lifecycle
* methods. Caught errors are triggered on the QWeb instance, and are
* potentially given to some parent component which implements `catchError`.
*
* If there are no such component, we destroy everything. This is better than
* being in a corrupted state.
*/ */
function isValidProp(prop, propDef): boolean { function errorHandler(error: Error, component: Component<any, any, any>) {
if (typeof propDef === "function") { let canCatch = false;
// Check if a value is constructed by some Constructor. Note that there is a let qweb = component.env.qweb;
// slight abuse of language: we want to consider primitive values as well. let root = component;
// while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
// So, even though 1 is not an instance of Number, we want to consider that root = component;
// it is valid. component = component.__owl__.parent!;
if (typeof prop === "object") {
return prop instanceof propDef;
}
return typeof prop === propDef.name.toLowerCase();
} else if (propDef instanceof Array) {
// If this code is executed, this means that we want to check if a prop
// matches at least one of its descriptor.
let result = false;
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
result = result || isValidProp(prop, propDef[i]);
}
return result;
} }
// propsDef is an object console.error(error);
let result = isValidProp(prop, propDef.type); // we trigger error on QWeb so it can be logged/handled
if (propDef.type === Array) { qweb.trigger("error", error);
for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element); if (canCatch) {
} setTimeout(() => {
component.catchError(error);
});
} else {
root.destroy();
} }
if (propDef.type === Object) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
}
}
return result;
} }
@@ -1,186 +1,5 @@
import { QWeb, UTILS } from "./qweb_core"; import { QWeb } from "../qweb/index";
import { VNode } from "./vdom"; import { MODS_CODE } from "../qweb/extensions";
/**
* Owl QWeb Extensions
*
* This file contains the implementation of non standard QWeb directives, added
* by Owl and that will only work on Owl projects:
*
* - t-on
* - t-ref
* - t-transition
* - t-component/t-keepalive
* - t-mounted
* - t-slot
* - t-model
*/
//------------------------------------------------------------------------------
// t-on
//------------------------------------------------------------------------------
// these are pieces of code that will be injected into the event handler if
// modifiers are specified
const MODS_CODE = {
prevent: "e.preventDefault();",
self: "if (e.target !== this.elm) {return}",
stop: "e.stopPropagation();"
};
QWeb.addDirective({
name: "on",
priority: 90,
atNodeCreation({ ctx, fullName, value, nodeID }) {
ctx.rootContext.shouldDefineOwner = true;
const [eventName, ...mods] = fullName.slice(5).split(".");
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
let handlerName = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
ctx.addIf(`!context['${handlerName}']`);
ctx.addLine(
`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(
/`/g,
"'"
)}'\`)`
);
ctx.closeIf();
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
handler += mods
.map(function(mod) {
return MODS_CODE[mod];
})
.join("");
handler += `context['${handlerName}'].call(${params}, e);}`;
} else {
handler = `context['${handlerName}'].bind(${params})`;
}
if (extraArgs) {
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
} else {
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
}
}
});
//------------------------------------------------------------------------------
// t-ref
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "ref",
priority: 95,
atNodeCreation({ ctx, value, addNodeHook }) {
const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
}
});
//------------------------------------------------------------------------------
// t-transition
//------------------------------------------------------------------------------
UTILS.nextFrame = function(cb: () => void) {
requestAnimationFrame(() => requestAnimationFrame(cb));
};
UTILS.transitionInsert = function(vn: VNode, name: string) {
const elm = <HTMLElement>vn.elm;
// remove potential duplicated vnode that is currently being removed, to
// prevent from having twice the same node in the DOM during an animation
const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
if (dup) {
dup.remove();
}
elm.classList.add(name + "-enter");
elm.classList.add(name + "-enter-active");
const finalize = () => {
elm.classList.remove(name + "-enter-active");
elm.classList.remove(name + "-enter-to");
};
this.nextFrame(() => {
elm.classList.remove(name + "-enter");
elm.classList.add(name + "-enter-to");
whenTransitionEnd(elm, finalize);
});
};
UTILS.transitionRemove = function(vn: VNode, name: string, rm: () => void) {
const elm = <HTMLElement>vn.elm;
elm.setAttribute("data-owl-key", vn.key!);
elm.classList.add(name + "-leave");
elm.classList.add(name + "-leave-active");
const finalize = () => {
elm.classList.remove(name + "-leave-active");
elm.classList.remove(name + "-leave-to");
rm();
};
this.nextFrame(() => {
elm.classList.remove(name + "-leave");
elm.classList.add(name + "-leave-to");
whenTransitionEnd(elm, finalize);
});
};
function getTimeout(delays: Array<string>, durations: Array<string>): number {
/* istanbul ignore next */
while (delays.length < durations.length) {
delays = delays.concat(delays);
}
return Math.max.apply(
null,
durations.map((d, i) => {
return toMs(d) + toMs(delays[i]);
})
);
}
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
// in a locale-dependent way, using a comma instead of a dot.
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
// as a floor function) causing unexpected behaviors
function toMs(s: string): number {
return Number(s.slice(0, -1).replace(",", ".")) * 1000;
}
function whenTransitionEnd(elm: HTMLElement, cb) {
const styles = window.getComputedStyle(elm);
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
const timeout: number = getTimeout(delays, durations);
if (timeout > 0) {
elm.addEventListener("transitionend", cb, { once: true });
} else {
cb();
}
}
QWeb.addDirective({
name: "transition",
priority: 96,
atNodeCreation({ value, addNodeHook }) {
let name = value;
const hooks = {
insert: `this.utils.transitionInsert(vn, '${name}');`,
remove: `this.utils.transitionRemove(vn, '${name}', rm);`
};
for (let hookName in hooks) {
addNodeHook(hookName, hooks[hookName]);
}
}
});
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-component // t-component
@@ -190,6 +9,19 @@ const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
self: "if (e.target !== vn.elm) {return}" self: "if (e.target !== vn.elm) {return}"
}); });
QWeb.utils.defineProxy = function defineProxy(target, source) {
for (let k in source) {
Object.defineProperty(target, k, {
get() {
return source[k];
},
set(val) {
source[k] = val;
}
});
}
};
/** /**
* The t-component directive is certainly a complicated and hard to maintain piece * The t-component directive is certainly a complicated and hard to maintain piece
* of code. To help you, fellow developer, if you have to maintain it, I offer * of code. To help you, fellow developer, if you have to maintain it, I offer
@@ -359,6 +191,7 @@ QWeb.addDirective({
ctx.addLine("//COMPONENT"); ctx.addLine("//COMPONENT");
ctx.rootContext.shouldDefineOwner = true; ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true; ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false; let keepAlive = node.getAttribute("t-keepalive") ? true : false;
let async = node.getAttribute("t-asyncroot") ? true : false; let async = node.getAttribute("t-asyncroot") ? true : false;
@@ -471,7 +304,7 @@ QWeb.addDirective({
if (tattClass) { if (tattClass) {
let tattExpr = ctx.formatExpression(tattClass); let tattExpr = ctx.formatExpression(tattClass);
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") { if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
tattExpr = `this.utils.toObj(${tattExpr})`; tattExpr = `utils.toObj(${tattExpr})`;
} }
if (classAttr) { if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`); ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
@@ -516,16 +349,28 @@ QWeb.addDirective({
} }
ctx.addLine( ctx.addLine(
`let w${componentID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;` `let w${componentID} = ${templateID} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateID}]] : false;`
); );
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); if (ctx.parentNode) {
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
}
let shouldProxy = false;
if (async) { if (async) {
ctx.addLine(`const patchQueue${componentID} = [];`); ctx.addLine(`const patchQueue${componentID} = [];`);
ctx.addLine( ctx.addLine(
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);` `c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
); );
} else { } else {
ctx.addLine(`c${ctx.parentNode}.push(null);`); if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(null);`);
} else {
let id = ctx.generateID();
ctx.rootContext.rootNode = id;
shouldProxy = true;
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`let vn${id} = {};`);
ctx.addLine(`result = vn${id};`);
}
} }
ctx.addLine(`let props${componentID} = {${propStr}};`); ctx.addLine(`let props${componentID} = {${propStr}};`);
ctx.addIf( ctx.addIf(
@@ -545,16 +390,27 @@ QWeb.addDirective({
ctx.addLine( ctx.addLine(
`let W${componentID} = context.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];` `let W${componentID} = context.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
); );
// maybe only do this in dev mode... // maybe only do this in dev mode...
ctx.addLine( ctx.addLine(
`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}(owner, props${componentID});`); if (QWeb.dev) {
ctx.addLine(`context.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`); ctx.addLine(`utils.validateProps(W${componentID}, props${componentID})`);
}
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
ctx.addLine(`parent.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`);
// SLOTS // SLOTS
if (node.childNodes.length) { const varDefs: string[] = [];
const hasSlots = node.childNodes.length;
if (hasSlots) {
ctx.rootContext.shouldTrackScope = true;
for (let v of Object.values(ctx.variables)) {
if (v["id"]) {
varDefs.push(v["id"]);
}
}
const clone = <Element>node.cloneNode(true); const clone = <Element>node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]"); const slotNodes = clone.querySelectorAll("[t-set]");
const slotId = qweb.nextSlotId++; const slotId = qweb.nextSlotId++;
@@ -565,7 +421,7 @@ QWeb.addDirective({
slotNode.parentElement!.removeChild(slotNode); slotNode.parentElement!.removeChild(slotNode);
const key = slotNode.getAttribute("t-set")!; const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set"); slotNode.removeAttribute("t-set");
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx.parentNode!); const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb); qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
} }
} }
@@ -574,24 +430,44 @@ QWeb.addDirective({
for (let child of Object.values(clone.childNodes)) { for (let child of Object.values(clone.childNodes)) {
t.appendChild(child); t.appendChild(child);
} }
const slotFn = qweb._compile(`slot_default_template`, t, ctx.parentNode!); const slotFn = qweb._compile(`slot_default_template`, t, ctx);
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb); qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
} }
} }
ctx.addLine(`def${defID} = w${componentID}.__prepare();`); let scopeVars = "";
if (hasSlots) {
scopeVars += ctx.scopeVars.length ? `Object.assign({}, scope)` : varDefs.length ? `{}` : "";
if (varDefs.length) {
scopeVars += `, {${varDefs.join(",")}}`;
}
}
ctx.addLine(`def${defID} = w${componentID}.__prepare(${scopeVars});`);
// 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
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
if (shouldProxy) {
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
}
ctx.addLine( ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});c${ `def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`
ctx.parentNode
}[_${dummyID}_index]=pvnode;w${componentID}.__owl__.pvnode = pvnode;});`
); );
ctx.addElse(); ctx.addElse();
// need to update component // need to update component
const patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue"; let patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
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 ? ${patchQueueCode} : []`;
}
if (QWeb.dev) {
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
}
ctx.addLine( ctx.addLine(
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode});` `def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode}${scopeVars &&
", " + scopeVars});`
); );
let keepAliveCode = ""; let keepAliveCode = "";
if (keepAlive) { if (keepAlive) {
@@ -600,9 +476,7 @@ QWeb.addDirective({
ctx.addLine( ctx.addLine(
`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${ `def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${
tattStyle ? `w${componentID}.el.style=${tattStyle};` : "" tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""
}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}c${ }let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}${registerCode}});`
ctx.parentNode
}[_${dummyID}_index]=pvnode;});`
); );
ctx.closeIf(); ctx.closeIf();
@@ -625,103 +499,3 @@ QWeb.addDirective({
return true; return true;
} }
}); });
//------------------------------------------------------------------------------
// t-mounted
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "mounted",
priority: 97,
atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
ctx.rootContext.shouldDefineOwner = true;
const eventName = fullName.slice(5);
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
let handler = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
/`/g,
"'"
)}'\`)})()`;
if (extraArgs) {
ctx.addLine(
`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
extraArgs
)});`
);
} else {
ctx.addLine(
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
);
}
addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
}
});
//------------------------------------------------------------------------------
// t-slot
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "slot",
priority: 80,
atNodeEncounter({ ctx, value }): boolean {
const slotKey = ctx.generateID();
ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`);
ctx.addIf(`slot${slotKey}`);
ctx.addLine(
`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
ctx.parentNode
}}));`
);
ctx.closeIf();
return true;
}
});
//------------------------------------------------------------------------------
// t-model
//------------------------------------------------------------------------------
UTILS.toNumber = function(val: string): number | string {
const n = parseFloat(val);
return isNaN(n) ? val : n;
};
QWeb.addDirective({
name: "model",
priority: 42,
atNodeCreation({ ctx, nodeID, value, node, fullName }) {
const type = node.getAttribute("type");
let handler;
let event = fullName.includes(".lazy") ? "change" : "input";
if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
event = "change";
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
} else if (type === "checkbox") {
ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`);
handler = `(ev) => {context.state['${value}'] = ev.target.checked}`;
} else if (type === "radio") {
const nodeValue = node.getAttribute("value")!;
ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`);
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
event = "click";
} else {
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
let valueCode = `ev.target.value${trimCode}`;
if (fullName.includes(".number")) {
ctx.rootContext.shouldDefineUtils = true;
valueCode = `utils.toNumber(${valueCode})`;
}
handler = `(ev) => {context.state['${value}'] = ${valueCode}}`;
}
ctx.addLine(
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
}
});
+96
View File
@@ -0,0 +1,96 @@
import { QWeb } from "../qweb/index";
//------------------------------------------------------------------------------
// Prop validation helper
//------------------------------------------------------------------------------
/**
* Validate the component props (or next props) against the (static) props
* description. This is potentially an expensive operation: it may needs to
* visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode.
*/
QWeb.utils.validateProps = function(Widget, props: Object) {
const propsDef = (<any>Widget).props;
if (propsDef instanceof Array) {
// list of strings (prop names)
for (let i = 0, l = propsDef.length; i < l; i++) {
const propName = propsDef[i];
if (propName[propName.length - 1] === "?") {
// optional prop
break;
}
if (!props[propName]) {
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
}
}
for (let key in props) {
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`);
}
}
} else if (propsDef) {
// propsDef is an object now
for (let propName in propsDef) {
if (props[propName] === undefined) {
if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
} else {
break;
}
}
let isValid = isValidProp(props[propName], propsDef[propName]);
if (!isValid) {
throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`);
}
}
for (let propName in props) {
if (!(propName in propsDef)) {
throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`);
}
}
}
};
/**
* Check if an invidual prop value matches its (static) prop definition
*/
function isValidProp(prop, propDef): boolean {
if (propDef === true) {
return true;
}
if (typeof propDef === "function") {
// Check if a value is constructed by some Constructor. Note that there is a
// slight abuse of language: we want to consider primitive values as well.
//
// So, even though 1 is not an instance of Number, we want to consider that
// it is valid.
if (typeof prop === "object") {
return prop instanceof propDef;
}
return typeof prop === propDef.name.toLowerCase();
} else if (propDef instanceof Array) {
// If this code is executed, this means that we want to check if a prop
// matches at least one of its descriptor.
let result = false;
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
result = result || isValidProp(prop, propDef[i]);
}
return result;
}
// propsDef is an object
let result = isValidProp(prop, propDef.type);
if (propDef.type === Array) {
for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element);
}
}
if (propDef.type === Object) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
}
}
return result;
}
+110
View File
@@ -0,0 +1,110 @@
/**
* Owl Observer
*
* This code contains the logic that allows Owl to observe and react to state
* changes.
*
* This is a Observer class that can observe any JS values. The way it works
* can be summarized thusly:
* - primitive values are not observed at all
* - Objects and arrays are observed by replacing them with a Proxy
* - each object/array metadata are tracked in a weakmap, and keep a revision
* number
*
* Note that this code is loosely inspired by Vue.
*/
//------------------------------------------------------------------------------
// Observer
//------------------------------------------------------------------------------
export class Observer {
rev: number = 1;
allowMutations: boolean = true;
dirty: boolean = false;
weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {}
async notifyChange() {
this.dirty = true;
await Promise.resolve();
if (this.dirty) {
this.dirty = false;
this.notifyCB();
}
}
observe(value: any, parent?: any): any {
if (value === null || typeof value !== "object" || value instanceof Date) {
// fun fact: typeof null === 'object'
return value;
}
let metadata = this.weakMap.get(value) || this._observe(value, parent);
return metadata.proxy;
}
revNumber(value): number {
const metadata = this.weakMap.get(value);
return metadata ? metadata.rev : 0;
}
deepRevNumber(value): number {
const metadata = this.weakMap.get(value);
return metadata ? metadata.deepRev : 0;
}
_observe(value, parent) {
var self = this;
const proxy = new Proxy(value, {
get(target, k) {
const targetValue = target[k];
return self.observe(targetValue, value);
},
set(target, key: string, newVal): boolean {
const value = target[key];
if (newVal !== value) {
if (!self.allowMutations) {
throw new Error(
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
);
}
self._updateRevNumber(target);
target[key] = newVal;
self.notifyChange();
}
return true;
},
deleteProperty(target, key) {
if (key in target) {
delete target[key];
self._updateRevNumber(target);
self.notifyChange();
}
return true;
}
});
const metadata = {
value,
proxy,
rev: this.rev,
deepRev: this.rev,
parent
};
this.weakMap.set(value, metadata);
this.weakMap.set(metadata.proxy, metadata);
return metadata;
}
_updateRevNumber(target: any) {
this.rev++;
let metadata = this.weakMap.get(target);
metadata.rev!++;
let parent = target;
do {
metadata = this.weakMap.get(parent);
metadata.deepRev++;
} while ((parent = metadata.parent) && parent !== target);
}
}
+14 -13
View File
@@ -4,19 +4,21 @@
* *
* Note that dynamic values, such as a date or a commit hash are added by rollup * Note that dynamic values, such as a date or a commit hash are added by rollup
*/ */
export { Component } from "./component"; import { EventBus } from "./core/event_bus";
export { EventBus } from "./event_bus"; import { Observer } from "./core/observer";
export { Observer } from "./observer"; import { QWeb } from "./qweb/index";
import { ConnectedComponent } from "./store/connected_component";
// we need to import manually the extra directives so they can register import { Store } from "./store/store";
// themselves in QWeb, otherwise these files will not even be loaded.
import "./qweb_directives";
import "./qweb_extensions";
import { QWeb } from "./qweb_core";
export { QWeb };
export { connect, Store } from "./store";
import * as _utils from "./utils"; import * as _utils from "./utils";
import { Link } from "./router/Link";
import { Router } from "./router/Router";
export { Component } from "./component/component";
export { QWeb };
export const core = { EventBus, Observer };
export const router = { Router, Link };
export const store = { Store, ConnectedComponent };
export const utils = _utils;
export const __info__ = {}; export const __info__ = {};
@@ -36,4 +38,3 @@ Object.defineProperty(__info__, "mode", {
} }
} }
}); });
export const utils = _utils;
-194
View File
@@ -1,194 +0,0 @@
/**
* Owl Observer
*
* This code contains the logic that allows Owl to observe and react to state
* changes.
*
* This is a Observer class that can observe any JS values. The way it works
* can be summarized thusly:
* - primitive values are not observed at all
* - Objects are observed by replacing all their keys with getters/setters
* (recursively)
* - Arrays are observed by replacing their prototype with a customized version,
* which wrap some methods to allow the tracking of each state change.
*
* Note that this code is inspired by Vue.
*/
//------------------------------------------------------------------------------
// Modified Array prototype
//------------------------------------------------------------------------------
// we define here a new modified Array prototype, which basically override all
// Array methods that change some state to be able to track their changes
const methodsToPatch = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
const methodLen = methodsToPatch.length;
const ArrayProto = Array.prototype;
const ModifiedArrayProto = Object.create(ArrayProto);
for (let i = 0; i < methodLen; i++) {
const method = methodsToPatch[i];
const initialMethod = ArrayProto[method];
ModifiedArrayProto[method] = function(...args) {
if (!this.__observer__.allowMutations) {
throw new Error(`Array cannot be changed here")`);
}
this.__observer__.rev++;
this.__observer__.notifyChange();
this.__owl__.rev++;
let parent = this;
do {
parent.__owl__.deepRev++;
} while ((parent = parent.__owl__.parent));
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
if (inserted) {
for (let i = 0, iLen = inserted.length; i < iLen; i++) {
this.__observer__.observe(inserted[i], this);
}
}
return initialMethod.call(this, ...args);
};
}
//------------------------------------------------------------------------------
// Observer
//------------------------------------------------------------------------------
export class Observer {
rev: number = 1;
allowMutations: boolean = true;
dirty: boolean = false;
static set(target: any, key: number | string, value: any) {
if (!target.__owl__) {
throw Error("`Observer.set()` can only be called with observed Objects or Arrays");
}
target.__owl__.observer.set(target, key, value);
}
static delete(target: any, key: number | string) {
if (!target.__owl__) {
throw Error("`Observer.delete()` can only be called with observed Objects");
}
target.__owl__.observer.delete(target, key);
}
notifyCB() {}
notifyChange() {
this.dirty = true;
Promise.resolve().then(() => {
if (this.dirty) {
this.dirty = false;
this.notifyCB();
}
});
}
observe(value: any, parent?: any) {
if (value === null) {
// fun fact: typeof null === 'object'
return;
}
if (typeof value !== "object") {
return;
}
if ("__owl__" in value) {
// already observed
value.__owl__.parent = parent;
return;
}
if (Array.isArray(value)) {
this._observeArr(value, parent);
} else {
this._observeObj(value, parent);
}
}
set(target: any, key: number | string, value: any) {
let alreadyDefined =
key in target && Object.getOwnPropertyDescriptor(target, key)!.configurable === false;
if (alreadyDefined) {
target[key] = value;
} else {
this._addProp(target, key, value);
this._updateRevNumber(target);
}
this.notifyChange();
}
delete(target: any, key: number | string) {
delete target[key];
this._updateRevNumber(target);
this.notifyChange();
}
_observeObj<T extends { __owl__?: any }>(obj: T, parent?: any) {
obj.__owl__ = {
rev: this.rev,
deepRev: this.rev,
parent,
observer: this
};
Object.defineProperty(obj, "__owl__", { enumerable: false });
for (let key in obj) {
this._addProp(obj, key, obj[key]);
}
}
_observeArr(arr: Array<any>, parent?: any) {
(<any>arr).__owl__ = {
rev: this.rev,
deepRev: this.rev,
parent,
observer: this
};
Object.defineProperty(arr, "__owl__", { enumerable: false });
(<any>arr).__proto__ = Object.create(ModifiedArrayProto);
(<any>arr).__proto__.__observer__ = this;
for (let i = 0, iLen = arr.length; i < iLen; i++) {
this.observe(arr[i], arr);
}
}
_addProp<T extends { __owl__?: any }>(obj: T, key: string | number, value: any) {
var self = this;
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
return value;
},
set(newVal) {
if (newVal !== value) {
if (!self.allowMutations) {
throw new Error(
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
);
}
self._updateRevNumber(obj);
value = newVal;
self.observe(newVal, obj);
self.notifyChange();
}
}
});
this.observe(value, obj);
}
_updateRevNumber(target: any) {
this.rev++;
target.__owl__.rev!++;
let parent = target;
do {
parent.__owl__.deepRev++;
} while ((parent = parent.__owl__.parent) && parent !== target);
}
}
@@ -1,5 +1,6 @@
import { Context, QWeb, UTILS } from "./qweb_core"; import { Context } from "./context";
import { QWebExprVar } from "./qweb_expressions"; import { QWebExprVar } from "./expression_parser";
import { QWeb } from "./qweb";
/** /**
* Owl QWeb Directives * Owl QWeb Directives
@@ -18,7 +19,7 @@ import { QWebExprVar } from "./qweb_expressions";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-esc and t-raw // t-esc and t-raw
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
(<any>UTILS).getFragment = function(str: string): DocumentFragment { QWeb.utils.getFragment = function(str: string): DocumentFragment {
const temp = document.createElement("template"); const temp = document.createElement("template");
temp.innerHTML = str; temp.innerHTML = str;
return temp.content; return temp.content;
@@ -57,7 +58,8 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
} }
} else { } else {
let fragID = ctx.generateID(); let fragID = ctx.generateID();
ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`); ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`var frag${fragID} = utils.getFragment(${exprID})`);
let tempNodeID = ctx.generateID(); let tempNodeID = ctx.generateID();
ctx.addLine(`var p${tempNodeID} = {hook: {`); ctx.addLine(`var p${tempNodeID} = {hook: {`);
ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`); ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
@@ -226,6 +228,10 @@ QWeb.addDirective({
ctx.addLine("}"); ctx.addLine("}");
} }
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
ctx.closeIf();
}
return true; return true;
} }
}); });
@@ -255,11 +261,11 @@ QWeb.addDirective({
ctx.addLine(`var _length${keysID} = _${keysID}.length;`); ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`); ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
ctx.indent(); ctx.indent();
ctx.addLine(`context.${name}_first = i === 0;`); ctx.addToScope(name + "_first", "i === 0");
ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`); ctx.addToScope(name + "_last", `i === _length${keysID} - 1`);
ctx.addLine(`context.${name}_index = i;`); ctx.addToScope(name + "_index", "i");
ctx.addLine(`context.${name} = _${keysID}[i];`); ctx.addToScope(name, `_${keysID}[i]`);
ctx.addLine(`context.${name}_value = _${valuesID}[i];`); ctx.addToScope(name + "_value", `_${valuesID}[i]`);
const nodeCopy = <Element>node.cloneNode(true); const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key"); let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
if (!shouldWarn && node.tagName === "t") { if (!shouldWarn && node.tagName === "t") {
+167
View File
@@ -0,0 +1,167 @@
import { compileExpr, QWebVar } from "./expression_parser";
//------------------------------------------------------------------------------
// Compilation Context
//------------------------------------------------------------------------------
export class Context {
nextID: number = 1;
code: string[] = [];
variables: { [key: string]: QWebVar } = {};
escaping: boolean = false;
parentNode: number | null = null;
parentTextNode: number | null = null;
rootNode: number | null = null;
indentLevel: number = 0;
rootContext: Context;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldDefineParent: boolean = false;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldDefineResult: boolean = false;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
inLoop: boolean = false;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false;
scopeVars: any[] = [];
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
this.addLine("var h = this.h;");
}
generateID(): number {
const id = this.rootContext.nextID++;
return id;
}
generateCode(): string[] {
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
if (shouldTrackScope) {
// add some vars to scope if needed
for (let scopeVar of this.scopeVars.reverse()) {
let { index, key, indent } = scopeVar;
const prefix = new Array(indent + 2).join(" ");
this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
}
this.code.unshift(" const scope = Object.create(null);");
}
if (this.shouldProtectContext) {
this.code.unshift(" context = Object.create(context);");
}
if (this.shouldDefineResult) {
this.code.unshift(" let result;");
}
if (this.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
this.code.unshift(" let owner = context;");
}
if (this.shouldDefineParent) {
if (this.hasParentWidget) {
this.code.unshift(" let parent = extra.parent;");
} else {
this.code.unshift(" let parent = context;");
}
}
if (this.shouldDefineQWeb) {
this.code.unshift(" let QWeb = this.constructor;");
}
if (this.shouldDefineUtils) {
this.code.unshift(" let utils = this.constructor.utils;");
}
return this.code;
}
withParent(node: number): Context {
if (
!this.allowMultipleRoots &&
this === this.rootContext &&
(this.parentNode || this.parentTextNode)
) {
throw new Error("A template should not have more than one root node");
}
if (!this.rootContext.rootNode) {
this.rootContext.rootNode = node;
}
return this.subContext("parentNode", node);
}
subContext(key: keyof Context, value: any): Context {
const newContext = Object.create(this);
newContext[key] = value;
return newContext;
}
indent() {
this.indentLevel++;
}
dedent() {
this.indentLevel--;
}
addLine(line: string): number {
const prefix = new Array(this.indentLevel + 2).join(" ");
this.code.push(prefix + line);
return this.code.length - 1;
}
addToScope(key: string, expr: string) {
const index = this.addLine(`context.${key} = ${expr};`);
this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
}
addIf(condition: string) {
this.addLine(`if (${condition}) {`);
this.indent();
}
addElse() {
this.dedent();
this.addLine("} else {");
this.indent();
}
closeIf() {
this.dedent();
this.addLine("}");
}
getValue(val: any): any {
return val in this.variables ? this.getValue(this.variables[val]) : val;
}
/**
* Prepare an expression for being consumed at render time. Its main job
* is to
* - replace unknown variables by a lookup in the context
* - replace already defined variables by their internal name
*/
formatExpression(expr: string): string {
return compileExpr(expr, this.variables);
}
/**
* Perform string interpolation on the given string. Note that if the whole
* string is an expression, it simply returns it (formatted and enclosed in
* parentheses).
* For instance:
* 'Hello {{x}}!' -> `Hello ${x}`
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
*/
interpolate(s: string): string {
let matches = s.match(/\{\{.*?\}\}/g);
if (matches && matches[0].length === s.length) {
return `(${this.formatExpression(s.slice(2, -2))})`;
}
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`";
}
}
+284
View File
@@ -0,0 +1,284 @@
import { VNode } from "../vdom/index";
import { QWeb } from "./qweb";
/**
* Owl QWeb Extensions
*
* This file contains the implementation of non standard QWeb directives, added
* by Owl and that will only work on Owl projects:
*
* - t-on
* - t-ref
* - t-transition
* - t-mounted
* - t-slot
* - t-model
*/
//------------------------------------------------------------------------------
// t-on
//------------------------------------------------------------------------------
// these are pieces of code that will be injected into the event handler if
// modifiers are specified
export const MODS_CODE = {
prevent: "e.preventDefault();",
self: "if (e.target !== this.elm) {return}",
stop: "e.stopPropagation();"
};
QWeb.addDirective({
name: "on",
priority: 90,
atNodeCreation({ ctx, fullName, value, nodeID }) {
ctx.rootContext.shouldDefineOwner = true;
const [eventName, ...mods] = fullName.slice(5).split(".");
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
let handlerName = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
ctx.addIf(`!context['${handlerName}']`);
ctx.addLine(
`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(
/`/g,
"'"
)}'\`)`
);
ctx.closeIf();
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
handler += mods
.map(function(mod) {
return MODS_CODE[mod];
})
.join("");
handler += `context['${handlerName}'].call(${params}, e);}`;
} else {
handler = `context['${handlerName}'].bind(${params})`;
}
if (extraArgs) {
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
} else {
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
}
}
});
//------------------------------------------------------------------------------
// t-ref
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "ref",
priority: 95,
atNodeCreation({ ctx, value, addNodeHook }) {
const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
}
});
//------------------------------------------------------------------------------
// t-transition
//------------------------------------------------------------------------------
QWeb.utils.nextFrame = function(cb: () => void) {
requestAnimationFrame(() => requestAnimationFrame(cb));
};
QWeb.utils.transitionInsert = function(vn: VNode, name: string) {
const elm = <HTMLElement>vn.elm;
// remove potential duplicated vnode that is currently being removed, to
// prevent from having twice the same node in the DOM during an animation
const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
if (dup) {
dup.remove();
}
elm.classList.add(name + "-enter");
elm.classList.add(name + "-enter-active");
const finalize = () => {
elm.classList.remove(name + "-enter-active");
elm.classList.remove(name + "-enter-to");
};
this.nextFrame(() => {
elm.classList.remove(name + "-enter");
elm.classList.add(name + "-enter-to");
whenTransitionEnd(elm, finalize);
});
};
QWeb.utils.transitionRemove = function(vn: VNode, name: string, rm: () => void) {
const elm = <HTMLElement>vn.elm;
elm.setAttribute("data-owl-key", vn.key!);
elm.classList.add(name + "-leave");
elm.classList.add(name + "-leave-active");
const finalize = () => {
elm.classList.remove(name + "-leave-active");
elm.classList.remove(name + "-leave-to");
rm();
};
this.nextFrame(() => {
elm.classList.remove(name + "-leave");
elm.classList.add(name + "-leave-to");
whenTransitionEnd(elm, finalize);
});
};
function getTimeout(delays: Array<string>, durations: Array<string>): number {
/* istanbul ignore next */
while (delays.length < durations.length) {
delays = delays.concat(delays);
}
return Math.max.apply(
null,
durations.map((d, i) => {
return toMs(d) + toMs(delays[i]);
})
);
}
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
// in a locale-dependent way, using a comma instead of a dot.
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
// as a floor function) causing unexpected behaviors
function toMs(s: string): number {
return Number(s.slice(0, -1).replace(",", ".")) * 1000;
}
function whenTransitionEnd(elm: HTMLElement, cb) {
const styles = window.getComputedStyle(elm);
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
const timeout: number = getTimeout(delays, durations);
if (timeout > 0) {
elm.addEventListener("transitionend", cb, { once: true });
} else {
cb();
}
}
QWeb.addDirective({
name: "transition",
priority: 96,
atNodeCreation({ ctx, value, addNodeHook }) {
ctx.rootContext.shouldDefineUtils = true;
let name = value;
const hooks = {
insert: `utils.transitionInsert(vn, '${name}');`,
remove: `utils.transitionRemove(vn, '${name}', rm);`
};
for (let hookName in hooks) {
addNodeHook(hookName, hooks[hookName]);
}
}
});
//------------------------------------------------------------------------------
// t-mounted
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "mounted",
priority: 97,
atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
ctx.rootContext.shouldDefineOwner = true;
const eventName = fullName.slice(5);
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
let handler = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
/`/g,
"'"
)}'\`)})()`;
if (extraArgs) {
ctx.addLine(
`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
extraArgs
)});`
);
} else {
ctx.addLine(
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
);
}
addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
}
});
//------------------------------------------------------------------------------
// t-slot
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "slot",
priority: 80,
atNodeEncounter({ ctx, value }): boolean {
const slotKey = ctx.generateID();
ctx.rootContext.shouldDefineOwner = true;
ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`);
ctx.addIf(`slot${slotKey}`);
ctx.addLine(
`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
ctx.parentNode
}, vars: extra.vars, parent: owner}));`
);
ctx.closeIf();
return true;
}
});
//------------------------------------------------------------------------------
// t-model
//------------------------------------------------------------------------------
QWeb.utils.toNumber = function(val: string): number | string {
const n = parseFloat(val);
return isNaN(n) ? val : n;
};
QWeb.addDirective({
name: "model",
priority: 42,
atNodeCreation({ ctx, nodeID, value, node, fullName }) {
const type = node.getAttribute("type");
let handler;
let event = fullName.includes(".lazy") ? "change" : "input";
if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
event = "change";
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
} else if (type === "checkbox") {
ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`);
handler = `(ev) => {context.state['${value}'] = ev.target.checked}`;
} else if (type === "radio") {
const nodeValue = node.getAttribute("value")!;
ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`);
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
event = "click";
} else {
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
let valueCode = `ev.target.value${trimCode}`;
if (fullName.includes(".number")) {
ctx.rootContext.shouldDefineUtils = true;
valueCode = `utils.toNumber(${valueCode})`;
}
handler = `(ev) => {context.state['${value}'] = ${valueCode}}`;
}
ctx.addLine(
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
}
});
+4
View File
@@ -0,0 +1,4 @@
import "./base_directives";
import "./extensions";
export { CompiledTemplate, QWeb } from "./qweb";
+117 -173
View File
@@ -1,6 +1,6 @@
import { VNode, h } from "./vdom"; import { EventBus } from "../core/event_bus";
import { QWebVar, compileExpr } from "./qweb_expressions"; import { h, patch, VNode } from "../vdom/index";
import { EventBus } from "./event_bus"; import { Context } from "./context";
/** /**
* Owl QWeb Engine * Owl QWeb Engine
@@ -64,15 +64,6 @@ const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgr
const lineBreakRE = /[\r\n]/; const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g; const whitespaceRE = /\s+/g;
const DIRECTIVE_NAMES = {
name: 1,
att: 1,
attf: 1,
key: 1
};
const DIRECTIVES: Directive[] = [];
const NODE_HOOKS_PARAMS = { const NODE_HOOKS_PARAMS = {
create: "(_, n)", create: "(_, n)",
insert: "vn", insert: "vn",
@@ -80,14 +71,12 @@ const NODE_HOOKS_PARAMS = {
}; };
interface Utils { interface Utils {
h: typeof h;
toObj(expr: any): Object; toObj(expr: any): Object;
shallowEqual(p1: Object, p2: Object): boolean; shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any; [key: string]: any;
} }
export const UTILS: Utils = { const UTILS: Utils = {
h: h,
toObj(expr) { toObj(expr) {
if (typeof expr === "string") { if (typeof expr === "string") {
expr = expr.trim(); expr = expr.trim();
@@ -117,7 +106,27 @@ function parseXML(xml: string): Document {
const parser = new DOMParser(); const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml"); const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) { if (doc.getElementsByTagName("parsererror").length) {
throw new Error("Invalid XML in template"); let msg = "Invalid XML in template.";
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
if (parsererrorText) {
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
const re = /\d+/g;
const firstMatch = re.exec(parsererrorText);
if (firstMatch) {
const lineNumber = Number(firstMatch[0]);
const line = xml.split("\n")[lineNumber - 1];
const secondMatch = re.exec(parsererrorText);
if (line && secondMatch) {
const columnIndex = Number(secondMatch[0]) - 1;
if (line[columnIndex]) {
msg +=
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
`${line}\n${"-".repeat(columnIndex - 1)}^`;
}
}
}
}
throw new Error(msg);
} }
return doc; return doc;
} }
@@ -129,9 +138,18 @@ let nextID = 1;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
export class QWeb extends EventBus { export class QWeb extends EventBus {
templates: { [name: string]: Template } = {}; templates: { [name: string]: Template } = {};
utils = UTILS; static utils = UTILS;
static components = Object.create(null); static components = Object.create(null);
static DIRECTIVE_NAMES: { [key: string]: 1 } = {
name: 1,
att: 1,
attf: 1,
key: 1
};
static DIRECTIVES: Directive[] = [];
h = h;
// dev mode enables better error messages or more costly validations // dev mode enables better error messages or more costly validations
static dev: boolean = false; static dev: boolean = false;
@@ -145,6 +163,8 @@ export class QWeb extends EventBus {
slots = {}; slots = {};
nextSlotId = 1; nextSlotId = 1;
isUpdating: boolean = false;
constructor(data?: string) { constructor(data?: string) {
super(); super();
if (data) { if (data) {
@@ -153,11 +173,14 @@ export class QWeb extends EventBus {
} }
static addDirective(directive: Directive) { static addDirective(directive: Directive) {
DIRECTIVES.push(directive); if (directive.name in QWeb.DIRECTIVE_NAMES) {
DIRECTIVE_NAMES[directive.name] = 1; throw new Error(`Directive "${directive.name} already registered`);
DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority); }
QWeb.DIRECTIVES.push(directive);
QWeb.DIRECTIVE_NAMES[directive.name] = 1;
QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
if (directive.extraNames) { if (directive.extraNames) {
directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1)); directive.extraNames.forEach(n => (QWeb.DIRECTIVE_NAMES[n] = 1));
} }
} }
@@ -184,12 +207,12 @@ export class QWeb extends EventBus {
} }
/** /**
* Load templates from a xml (as a string). This will look up for the first * Load templates from a xml (as a string or xml document). This will look up
* <templates> tag, and will consider each child of this as a template, with * for the first <templates> tag, and will consider each child of this as a
* the name given by the t-name attribute. * template, with the name given by the t-name attribute.
*/ */
addTemplates(xmlstr: string) { addTemplates(xmlstr: string | Document) {
const doc = parseXML(xmlstr); const doc = typeof xmlstr === 'string' ? parseXML(xmlstr) : xmlstr;
const templates = doc.getElementsByTagName("templates")[0]; const templates = doc.getElementsByTagName("templates")[0];
if (!templates) { if (!templates) {
return; return;
@@ -268,45 +291,82 @@ export class QWeb extends EventBus {
return template.fn.call(this, context, extra); return template.fn.call(this, context, extra);
} }
_compile(name: string, elem: Element, parentNode?: number): CompiledTemplate { /**
* Render a template to a html string.
*
* Note that this is more limited than the `render` method: it is not suitable
* to render a full component tree, since this is an asynchronous operation.
* This method can only render templates without components.
*/
renderToString(name: string, context: EvalContext = {}): string {
const vnode = this.render(name, context);
if (vnode.sel === undefined) {
return vnode.text!;
}
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
}
/**
* Force all widgets connected to this QWeb instance to rerender themselves.
*
* This method is mostly useful for external code that want to modify the
* application in some cases. For example, a router plugin.
*/
forceUpdate() {
this.isUpdating = true;
Promise.resolve().then(() => {
if (this.isUpdating) {
this.isUpdating = false;
this.trigger("update");
}
});
}
_compile(name: string, elem: Element, parentContext?: Context): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug"); const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new Context(name); const ctx = new Context(name);
if (parentNode) { if (parentContext) {
ctx.nextID = parentNode + 1; ctx.variables = Object.create(parentContext.variables);
ctx.parentNode = parentNode; ctx.nextID = parentContext.parentNode! + 1;
ctx.parentNode = parentContext.parentNode!;
ctx.allowMultipleRoots = true; ctx.allowMultipleRoots = true;
ctx.addLine(`let c${parentNode} = extra.parentNode;`); ctx.hasParentWidget = true;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
for (let v in parentContext.variables) {
let variable = <any>parentContext.variables[v];
if (variable.id) {
ctx.addLine(`let ${variable.id} = extra.vars.${variable.id}`);
}
}
}
if (parentContext) {
ctx.addLine(" Object.assign(context, extra.scope);");
} }
this._compileNode(elem, ctx); this._compileNode(elem, ctx);
if (ctx.shouldProtectContext) { if (!parentContext) {
ctx.code.unshift(" context = Object.create(context);"); if (ctx.shouldDefineResult) {
} ctx.addLine(`return result;`);
if (ctx.shouldDefineOwner) { } else {
// this is necessary to prevent some directives (t-forach for ex) to if (!ctx.rootNode) {
// pollute the rendering context by adding some keys in it. throw new Error(`A template should have one root node (${ctx.templateName})`);
ctx.code.unshift(" let owner = context;"); }
} ctx.addLine(`return vn${ctx.rootNode};`);
if (ctx.shouldDefineQWeb) { }
ctx.code.unshift(" let QWeb = this.constructor;");
}
if (ctx.shouldDefineUtils) {
ctx.code.unshift(" let utils = this.utils;");
} }
if (!parentNode) { let code = ctx.generateCode();
if (!ctx.rootNode) {
throw new Error("A template should have one root node");
}
ctx.addLine(`return vn${ctx.rootNode};`);
}
let template; let template;
try { try {
template = new Function("context", "extra", ctx.code.join("\n")) as CompiledTemplate; template = new Function("context", "extra", code.join("\n")) as CompiledTemplate;
} catch (e) { } catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'"); const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`); console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(ctx.code.join("\n")); console.warn(code.join("\n"));
console.groupEnd(); console.groupEnd();
throw new Error( throw new Error(
`Invalid generated code while compiling template '${templateName}': ${e.message}` `Invalid generated code while compiling template '${templateName}': ${e.message}`
@@ -373,16 +433,16 @@ export class QWeb extends EventBus {
let attrName = attributes[i].name; let attrName = attributes[i].name;
if (attrName.startsWith("t-")) { if (attrName.startsWith("t-")) {
let dName = attrName.slice(2).split(/-|\./)[0]; let dName = attrName.slice(2).split(/-|\./)[0];
if (!(dName in DIRECTIVE_NAMES)) { if (!(dName in QWeb.DIRECTIVE_NAMES)) {
throw new Error(`Unknown QWeb directive: '${attrName}'`); throw new Error(`Unknown QWeb directive: '${attrName}'`);
} }
} }
} }
const DIR_N = DIRECTIVES.length; const DIR_N = QWeb.DIRECTIVES.length;
const ATTR_N = attributes.length; const ATTR_N = attributes.length;
for (let i = 0; i < DIR_N; i++) { for (let i = 0; i < DIR_N; i++) {
let directive = DIRECTIVES[i]; let directive = QWeb.DIRECTIVES[i];
let fullName; let fullName;
let value; let value;
for (let j = 0; j < ATTR_N; j++) { for (let j = 0; j < ATTR_N; j++) {
@@ -530,7 +590,8 @@ export class QWeb extends EventBus {
let formattedValue = v.id || ctx.formatExpression(v); let formattedValue = v.id || ctx.formatExpression(v);
if (attName === "class") { if (attName === "class") {
formattedValue = `this.utils.toObj(${formattedValue})`; ctx.rootContext.shouldDefineUtils = true;
formattedValue = `utils.toObj(${formattedValue})`;
if (classObj) { if (classObj) {
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`); ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
} else { } else {
@@ -632,120 +693,3 @@ export class QWeb extends EventBus {
} }
} }
} }
//------------------------------------------------------------------------------
// Compilation Context
//------------------------------------------------------------------------------
export class Context {
nextID: number = 1;
code: string[] = [];
variables: { [key: string]: QWebVar } = {};
escaping: boolean = false;
parentNode: number | null = null;
parentTextNode: number | null = null;
rootNode: number | null = null;
indentLevel: number = 0;
rootContext: Context;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldProtectContext: boolean = false;
inLoop: boolean = false;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
this.addLine("var h = this.utils.h;");
}
generateID(): number {
const id = this.rootContext.nextID++;
return id;
}
withParent(node: number): Context {
if (
!this.allowMultipleRoots &&
this === this.rootContext &&
(this.parentNode || this.parentTextNode)
) {
throw new Error("A template should not have more than one root node");
}
if (!this.rootContext.rootNode) {
this.rootContext.rootNode = node;
}
return this.subContext("parentNode", node);
}
subContext(key: keyof Context, value: any): Context {
const newContext = Object.create(this);
newContext[key] = value;
return newContext;
}
indent() {
this.indentLevel++;
}
dedent() {
this.indentLevel--;
}
addLine(line: string) {
const prefix = new Array(this.indentLevel + 2).join(" ");
this.code.push(prefix + line);
}
addIf(condition: string) {
this.addLine(`if (${condition}) {`);
this.indent();
}
addElse() {
this.dedent();
this.addLine("} else {");
this.indent();
}
closeIf() {
this.dedent();
this.addLine("}");
}
getValue(val: any): any {
return val in this.variables ? this.getValue(this.variables[val]) : val;
}
/**
* Prepare an expression for being consumed at render time. Its main job
* is to
* - replace unknown variables by a lookup in the context
* - replace already defined variables by their internal name
*/
formatExpression(expr: string): string {
return compileExpr(expr, this.variables);
}
/**
* Perform string interpolation on the given string. Note that if the whole
* string is an expression, it simply returns it (formatted and enclosed in
* parentheses).
* For instance:
* 'Hello {{x}}!' -> `Hello ${x}`
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
*/
interpolate(s: string): string {
let matches = s.match(/\{\{.*?\}\}/g);
if (matches && matches[0].length === s.length) {
return `(${this.formatExpression(s.slice(2, -2))})`;
}
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`";
}
}
+48
View File
@@ -0,0 +1,48 @@
import { Component } from "../component/component";
import { Destination, RouterEnv } from "./Router";
export const LINK_TEMPLATE_NAME = "__owl__-router-link";
export const LINK_TEMPLATE = `
<a t-att-class="{'router-link-active': isActive }"
t-att-href="href"
t-on-click="navigate">
<t t-slot="default"/>
</a>`;
type Props = Destination;
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
template = LINK_TEMPLATE_NAME;
href: string = this.env.router.destToPath(this.props);
async willUpdateProps(nextProps) {
this.href = this.env.router.destToPath(nextProps);
}
get isActive() {
if (this.env.router.mode === "hash") {
return (<any>document.location).hash === this.href;
}
return (<any>document.location).pathname === this.href;
}
navigate(ev) {
// don't redirect with control keys
if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) {
return;
}
// don't redirect on right click
if (ev.button !== undefined && ev.button !== 0) {
return;
}
// don't redirect if `target="_blank"`
if (ev.currentTarget && ev.currentTarget.getAttribute) {
const target = ev.currentTarget.getAttribute("target");
if (/\b_blank\b/i.test(target)) {
return;
}
}
ev.preventDefault();
this.env.router.navigate(this.props);
}
}
+262
View File
@@ -0,0 +1,262 @@
import { Env } from "../component/component";
import { QWeb } from "../qweb/index";
import { makeDirective } from "./directive";
import { LINK_TEMPLATE, LINK_TEMPLATE_NAME } from "./Link";
type NavigationGuard = (info: {
env: Env;
to: Route | null;
from: Route | null;
}) => boolean | Destination;
export interface Route {
name: string;
path: string;
component?: any;
redirect?: Destination;
params: string[];
beforeRouteEnter?: NavigationGuard;
}
export type RouteParams = { [key: string]: string | number };
export interface RouterEnv extends Env {
router: Router;
}
export interface Destination {
path?: string;
to?: string;
params?: RouteParams;
}
interface PositiveMatchResult {
type: "match";
route: Route;
params: RouteParams;
}
interface NegativeMatchResult {
type: "nomatch";
}
interface CancelledMatch {
type: "cancelled";
}
type MatchResult = PositiveMatchResult | NegativeMatchResult | CancelledMatch;
interface Options {
mode: Router["mode"];
}
const paramRegexp = /\{\{(.*?)\}\}/;
export class Router {
currentRoute: Route | null = null;
currentParams: RouteParams | null = null;
mode: "history" | "hash";
routes: { [id: string]: Route };
routeIds: string[];
env: RouterEnv;
constructor(env: Env, routes: Partial<Route>[], options: Options = { mode: "history" }) {
env.router = this;
this.mode = options.mode;
this.env = env as RouterEnv;
this.routes = {};
this.routeIds = [];
let nextId = 1;
for (let partialRoute of routes) {
if (!partialRoute.name) {
partialRoute.name = "__route__" + nextId++;
}
if (partialRoute.component) {
QWeb.register("__component__" + partialRoute.name, partialRoute.component);
}
if (partialRoute.redirect) {
this.validateDestination(partialRoute.redirect);
}
partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : [];
this.routes[partialRoute.name] = partialRoute as Route;
this.routeIds.push(partialRoute.name);
}
(this as any)._listener = () => this.matchAndApplyRules(this.currentPath());
window.addEventListener("popstate", (this as any)._listener);
// setup link and directive
env.qweb.addTemplate(LINK_TEMPLATE_NAME, LINK_TEMPLATE);
QWeb.addDirective(makeDirective(<RouterEnv>env));
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
async start() {
const result = await this.matchAndApplyRules(this.currentPath());
if (result.type === "match") {
this.currentRoute = result.route;
this.currentParams = result.params;
const currentPath = this.routeToPath(result.route, result.params);
if (currentPath !== this.currentPath()) {
this.setUrlFromPath(currentPath);
}
}
}
async navigate(to: Destination): Promise<boolean> {
const path = this.destToPath(to);
const initialName = this.currentRouteName;
const result = await this.matchAndApplyRules(path);
if (result.type === "match") {
const finalPath = this.routeToPath(result.route, result.params);
this.setUrlFromPath(finalPath);
this.currentRoute = result.route;
this.currentParams = result.params;
} else if (result.type === "nomatch") {
this.currentRoute = null;
this.currentParams = null;
}
if (this.currentRouteName !== initialName) {
this.env.qweb.forceUpdate();
return true;
}
return false;
}
destToPath(dest: Destination): string {
this.validateDestination(dest);
return dest.path || this.routeToPath(this.routes[dest.to!], dest.params!);
}
get currentRouteName(): string | null {
return this.currentRoute && this.currentRoute.name;
}
//--------------------------------------------------------------------------
// Private helpers
//--------------------------------------------------------------------------
private setUrlFromPath(path: string) {
const url = location.origin + path;
window.history.pushState({}, path, url);
}
private validateDestination(dest: Destination) {
if ((!dest.path && !dest.to) || (dest.path && dest.to)) {
throw new Error(`Invalid destination: ${JSON.stringify(dest)}`);
}
}
private routeToPath(route: Route, params: RouteParams): string {
const path = route.path;
const parts = path.split("/");
const l = parts.length;
for (let i = 0; i < l; i++) {
const part = parts[i];
const match = part.match(paramRegexp);
if (match) {
const key = match[1].split(".")[0];
parts[i] = <string>params[key];
}
}
return parts.join("/");
}
private currentPath(): string {
let result = this.mode === "history" ? window.location.pathname : window.location.hash.slice(1);
return result || "/";
}
private match(path: string): MatchResult {
for (let routeId of this.routeIds) {
let route = this.routes[routeId];
let params = this.getRouteParams(route, path);
if (params) {
return {
type: "match",
route: route,
params: params
};
}
}
return { type: "nomatch" };
}
private async matchAndApplyRules(path: string): Promise<MatchResult> {
const result = this.match(path);
if (result.type === "match") {
return this.applyRules(result);
}
return result;
}
private async applyRules(matchResult: PositiveMatchResult): Promise<MatchResult> {
const route = matchResult.route;
if (route.redirect) {
const path = this.destToPath(route.redirect);
return this.matchAndApplyRules(path);
}
if (route.beforeRouteEnter) {
const result = await route.beforeRouteEnter({
env: this.env,
from: this.currentRoute,
to: route
});
if (result === false) {
return { type: "cancelled" };
} else if (result !== true) {
// we want to navigate to another destination
const path = this.destToPath(result);
return this.matchAndApplyRules(path);
}
}
return matchResult;
}
private getRouteParams(route: Route, path: string): RouteParams | false {
if (route.path === "*") {
return {};
}
const descrParts = route.path.split("/");
const targetParts = path.split("/");
const l = descrParts.length;
if (l !== targetParts.length) {
return false;
}
const result = {};
for (let i = 0; i < l; i++) {
const descr = descrParts[i];
let target: string | number = targetParts[i];
const match = descr.match(paramRegexp);
if (match) {
const [key, suffix] = match[1].split(".");
if (suffix === "number") {
target = parseInt(target, 10);
}
result[key] = target;
} else if (descr !== target) {
return false;
}
}
return result;
}
}
function findParams(str: string): string[] {
const globalParamRegexp = /\{\{(.*?)\}\}/g;
const result: string[] = [];
let m;
do {
m = globalParamRegexp.exec(str);
if (m) {
result.push(m[1].split(".")[0]);
}
} while (m);
return result;
}
+28
View File
@@ -0,0 +1,28 @@
import { RouterEnv } from "./Router";
export function makeDirective(env: RouterEnv) {
return {
name: "routecomponent",
priority: 13,
atNodeEncounter({ node }): boolean {
let first = true;
const router = env.router;
for (let name of router.routeIds) {
const route = router.routes[name];
if (route.component) {
// make new t t-component element
const comp = node.ownerDocument.createElement("t");
comp.setAttribute("t-component", "__component__" + route.name);
comp.setAttribute(first ? "t-if" : "t-elif", `env.router.currentRouteName === '${route.name}'`);
first = false;
for (let param of route.params) {
comp.setAttribute(param, `env.router.currentParams.${param}`);
}
node.appendChild(comp);
}
}
node.removeAttribute("t-routecomponent");
return false;
}
};
}
-323
View File
@@ -1,323 +0,0 @@
import { Component, Env } from "./component";
import { EventBus } from "./event_bus";
import { Observer } from "./observer";
/**
* Owl Store
*
* We have here:
* - a Store class
* - a connect function
*
* The Owl store is our answer to the problem of managing complex state across
* components. The main idea is that the store owns some state, allow external
* code to modify it through actions/mutations, and for each state changes,
* connected component will be notified, and updated if necessary.
*
* Note that this code is partly inspired by VueX and React/Redux
*/
//------------------------------------------------------------------------------
// Store Definition
//------------------------------------------------------------------------------
type Mutation = ({ state, commit, getters }, ...payload: any) => void;
type Action = ({ commit, state, dispatch, env, getters }, ...payload: any) => void;
type Getter = ({ state, getters }, payload) => any;
interface StoreConfig {
env?: Env;
state?: any;
actions?: { [name: string]: Action };
getters?: { [name: string]: Getter };
mutations?: { [name: string]: Mutation };
}
interface StoreOption {
debug?: boolean;
}
export class Store extends EventBus {
state: any;
actions: any;
mutations: any;
_commitLevel: number = 0;
history: any[] = [];
debug: boolean;
env: any;
observer: Observer;
getters: { [name: string]: (payload?) => any };
_gettersCache: { [name: string]: {} };
_updateId: number = 1;
constructor(config: StoreConfig, options: StoreOption = {}) {
super();
this.debug = options.debug || false;
this.state = config.state || {};
this.actions = config.actions;
this.mutations = config.mutations;
this.env = config.env;
this.observer = new Observer();
this.observer.notifyCB = this.__notifyComponents.bind(this);
this.observer.allowMutations = false;
this.observer.observe(this.state);
this.getters = {};
this._gettersCache = {};
if (this.debug) {
this.history.push({ state: this.state });
}
const cTypes = ["undefined", "number", "string"];
for (let entry of Object.entries(config.getters || {})) {
const name: string = entry[0];
const func: (...any) => any = entry[1];
this.getters[name] = payload => {
if (this._commitLevel === 0 && cTypes.indexOf(typeof payload) >= 0) {
this._gettersCache[name] = this._gettersCache[name] || {};
this._gettersCache[name][payload] =
this._gettersCache[name][payload] ||
func({ state: this.state, getters: this.getters }, payload);
return this._gettersCache[name][payload];
}
return func({ state: this.state, getters: this.getters }, payload);
};
}
}
dispatch(action: string, ...payload: any): Promise<void> | void {
if (!this.actions[action]) {
throw new Error(`[Error] action ${action} is undefined`);
}
const result = this.actions[action](
{
commit: this.commit.bind(this),
dispatch: this.dispatch.bind(this),
env: this.env,
state: this.state,
getters: this.getters
},
...payload
);
if (result instanceof Promise) {
return new Promise((resolve, reject) => {
result.then(() => resolve());
result.catch(reject);
});
}
}
commit(type: string, ...payload: any): any {
if (!this.mutations[type]) {
throw new Error(`[Error] mutation ${type} is undefined`);
}
this._commitLevel++;
this.observer.allowMutations = true;
const res = this.mutations[type].call(
null,
{
commit: this.commit.bind(this),
state: this.state,
getters: this.getters
},
...payload
);
if (this._commitLevel === 1) {
this.observer.allowMutations = false;
if (this.debug) {
this.history.push({
state: this.state,
mutation: type,
payload: [...payload]
});
}
}
this._commitLevel--;
return res;
}
/**
* Instead of using trigger to emit an update event, we actually implement
* our own function to do that. The reason is that we need to be smarter than
* a simple trigger function: we need to wait for parent components to be
* done before doing children components. The reason is that if an update
* as an effect of destroying a children, we do not want to call the
* mapStoreToProps function of the child, nor rendering it.
*
* This method is not optimal if we have a bunch of asynchronous components:
* we wait sequentially for each component to be completed before updating the
* next. However, the only things that matters is that children are updated
* after their parents. So, this could be optimized by being smarter, and
* updating all widgets concurrently, except for parents/children.
*/
async __notifyComponents() {
this._updateId++;
const current = this._updateId;
this._gettersCache = {};
const subs = this.subscriptions.update || [];
for (let i = 0, iLen = subs.length; i < iLen; i++) {
const sub = subs[i];
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
if (shouldCallback) {
await sub.callback.call(sub.owner, current);
}
}
}
}
//------------------------------------------------------------------------------
// Connect function
//------------------------------------------------------------------------------
function revNumber<T extends Object>(o: T): number {
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
return (<any>o).__owl__.rev;
}
return 0;
}
function deepRevNumber<T extends Object>(o: T): number {
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
return (<any>o).__owl__.deepRev;
}
return 0;
}
type Constructor<T> = new (...args: any[]) => T;
interface EnvWithStore extends Env {
store: Store;
}
type HashFunction = (a: any, b: any) => number;
interface StoreOptions {
getStore?(Env): Store;
hashFunction?: HashFunction;
deep?: boolean;
}
export function connect<E extends EnvWithStore, P, S>(
Comp: Constructor<Component<E, P, S>>,
mapStoreToProps,
options: StoreOptions = <StoreOptions>{}
) {
let hashFunction = options.hashFunction || null;
const getStore = options.getStore || (env => env.store);
if (!hashFunction) {
let deep = "deep" in options ? options.deep : true;
let defaultRevFunction = deep ? deepRevNumber : revNumber;
hashFunction = function({ storeProps }, options) {
const { currentStoreProps } = options;
if ("__owl__" in storeProps) {
return defaultRevFunction(storeProps);
}
let hash = 0;
for (let key in storeProps) {
const val = storeProps[key];
const hashVal = defaultRevFunction(val);
if (hashVal === 0) {
if (val !== currentStoreProps[key]) {
options.didChange = true;
}
} else {
hash += hashVal;
}
}
return hash;
};
}
const Result = class extends Comp {
constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent;
const store = getStore(env);
const ownProps = Object.assign({}, props || {});
const storeProps = mapStoreToProps(store.state, ownProps, store.getters);
const mergedProps = Object.assign({}, props || {}, storeProps);
super(parent, mergedProps);
(<any>this.__owl__).ownProps = ownProps;
(<any>this.__owl__).currentStoreProps = storeProps;
(<any>this.__owl__).store = store;
(<any>this.__owl__).storeHash = (<HashFunction>hashFunction)(
{
state: store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
{
currentStoreProps: storeProps
}
);
}
/**
* We do not use the mounted hook here for a subtle reason: we want the
* updates to be called for the parents before the children. However,
* if we use the mounted hook, this will be done in the reverse order.
*/
__callMounted() {
(<any>this.__owl__).store.on("update", this, this.__checkUpdate);
super.__callMounted();
}
willUnmount() {
(<any>this.__owl__).store.off("update", this);
super.willUnmount();
}
async __checkUpdate(updateId) {
if (updateId === (<any>this.__owl__).currentUpdateId) {
return;
}
const ownProps = (<any>this.__owl__).ownProps;
const storeProps = mapStoreToProps(
(<any>this.__owl__).store.state,
ownProps,
(<any>this.__owl__).store.getters
);
const options: any = {
currentStoreProps: (<any>this.__owl__).currentStoreProps
};
const storeHash = (<HashFunction>hashFunction)(
{
state: (<any>this.__owl__).store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
options
);
let didChange = options.didChange;
if (storeHash !== (<any>this.__owl__).storeHash) {
didChange = true;
(<any>this.__owl__).storeHash = storeHash;
}
if (didChange) {
(<any>this.__owl__).currentStoreProps = storeProps;
await this.__updateProps(ownProps, false);
}
}
__updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
const __owl__ = <any>this.__owl__;
__owl__.currentUpdateId = __owl__.store._updateId;
if (__owl__.ownProps !== nextProps) {
__owl__.currentStoreProps = mapStoreToProps(
__owl__.store.state,
nextProps,
__owl__.store.getters
);
}
__owl__.ownProps = nextProps;
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
return super.__updateProps(mergedProps, forceUpdate, patchQueue);
}
};
// we assign here a unique name to the resulting anonymous class.
// this is necessary for Owl to be able to properly deduce templates.
// Otherwise, all connected components would have the same name, and then
// each component after the first will necessarily have the same template.
let name = `Connected${Comp.name}`;
Object.defineProperty(Result, "name", { value: name });
return Result;
}
+119
View File
@@ -0,0 +1,119 @@
import { Component, Env } from "../component/component";
//------------------------------------------------------------------------------
// Connect function
//------------------------------------------------------------------------------
type HashFunction = (a: any, b: any) => number;
export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> {
deep: boolean = true;
getStore(env) {
return env.store;
}
storeProps: any;
hashFunction: HashFunction = (storeProps, options) => {
const revFn = (this.__owl__ as any).revFn;
const rev = revFn(storeProps);
if (rev > 0) {
return rev;
}
let hash = 0;
for (let key in storeProps) {
const val = storeProps[key];
const hashVal = revFn(val);
if (hashVal === 0) {
if (val !== options.prevStoreProps[key]) {
options.didChange = true;
}
} else {
hash += hashVal;
}
}
return hash;
};
static mapStoreToProps(storeState, ownProps, getters) {
return {};
}
dispatch(name, ...payload) {
return (this.__owl__ as any).store.dispatch(name, ...payload);
}
/**
* Need to do this here so 'deep' can be overrided by subcomponent easily
*/
async __prepareAndRender(
scope?: Object,
vars?: any
): ReturnType<Component<any, any, any>["__prepareAndRender"]> {
const store = this.getStore(this.env);
const ownProps = this.props || {};
this.storeProps = (<any>this.constructor).mapStoreToProps(store.state, ownProps, store.getters);
const observer = store.observer;
const revFn = this.deep ? observer.deepRevNumber : observer.revNumber;
(this.__owl__ as any).store = store;
(this.__owl__ as any).revFn = revFn.bind(observer);
(this.__owl__ as any).storeHash = this.hashFunction(this.storeProps, {
prevStoreProps: this.storeProps
});
(this.__owl__ as any).rev = observer.rev;
return super.__prepareAndRender(scope, vars);
}
/**
* We do not use the mounted hook here for a subtle reason: we want the
* updates to be called for the parents before the children. However,
* if we use the mounted hook, this will be done in the reverse order.
*/
__callMounted() {
(this.__owl__ as any).store.on("update", this, this.__checkUpdate);
super.__callMounted();
}
__callWillUnmount() {
(this.__owl__ as any).store.off("update", this);
super.__callWillUnmount();
}
__destroy(parent: any) {
(this.__owl__ as any).store.off("update", this);
super.__destroy(parent);
}
async __updateProps(nextProps: P, f, p, s, v) {
this.__updateStoreProps(nextProps);
return super.__updateProps(nextProps, f, p, s, v);
}
__updateStoreProps(nextProps): boolean {
const store = (this.__owl__ as any).store;
const storeProps = (<any>this.constructor).mapStoreToProps(
store.state,
nextProps,
store.getters
);
const options = { prevStoreProps: this.storeProps, didChange: false };
const storeHash = this.hashFunction(storeProps, options);
this.storeProps = storeProps;
let didChange = options.didChange;
if (storeHash !== (this.__owl__ as any).storeHash) {
(this.__owl__ as any).storeHash = storeHash;
didChange = true;
}
(this.__owl__ as any).rev = store.observer.rev;
return didChange;
}
async __checkUpdate() {
const observer = (this.__owl__ as any).store.observer;
if (observer.rev === (this.__owl__ as any).rev) {
// update was already done by updateProps, from parent
return;
}
const didChange = this.__updateStoreProps(this.props);
if (didChange) {
return this.render();
}
}
}
+107
View File
@@ -0,0 +1,107 @@
import { Env } from "../component/component";
import { EventBus } from "../core/event_bus";
import { Observer } from "../core/observer";
/**
* Owl Store
*
* We have here:
* - a Store class
* - the ConnectedComponent class
*
* The Owl store is our answer to the problem of managing complex state across
* components. The main idea is that the store owns some state, allow external
* code to modify it through actions, and for each state changes,
* connected component will be notified, and updated if necessary.
*
* Note that this code is partly inspired by VueX and React/Redux
*/
//------------------------------------------------------------------------------
// Store Definition
//------------------------------------------------------------------------------
export type Action = ({ state, dispatch, env, getters }, ...payload: any) => any;
export type Getter = ({ state: any, getters }, payload?) => any;
interface StoreConfig {
env?: Env;
state?: any;
actions?: { [name: string]: Action };
getters?: { [name: string]: Getter };
}
interface StoreOption {
debug?: boolean;
}
export class Store extends EventBus {
state: any;
actions: any;
mutations: any;
debug: boolean;
env: any;
observer: Observer;
getters: { [name: string]: (payload?) => any };
constructor(config: StoreConfig, options: StoreOption = {}) {
super();
this.debug = options.debug || false;
this.actions = config.actions;
this.env = config.env;
this.observer = new Observer();
this.observer.notifyCB = this.__notifyComponents.bind(this);
this.state = this.observer.observe(config.state || {});
this.getters = {};
if (config.getters) {
const firstArg = {
state: this.state,
getters: this.getters,
};
for (let g in config.getters) {
this.getters[g] = config.getters[g].bind(this, firstArg);
}
}
}
dispatch(action: string, ...payload: any): Promise<void> | void {
if (!this.actions[action]) {
throw new Error(`[Error] action ${action} is undefined`);
}
const result = this.actions[action](
{
dispatch: this.dispatch.bind(this),
env: this.env,
state: this.state,
getters: this.getters
},
...payload
);
return result;
}
/**
* Instead of using trigger to emit an update event, we actually implement
* our own function to do that. The reason is that we need to be smarter than
* a simple trigger function: we need to wait for parent components to be
* done before doing children components. The reason is that if an update
* as an effect of destroying a children, we do not want to call the
* mapStoreToProps function of the child, nor rendering it.
*
* This method is not optimal if we have a bunch of asynchronous components:
* we wait sequentially for each component to be completed before updating the
* next. However, the only things that matters is that children are updated
* after their parents. So, this could be optimized by being smarter, and
* updating all widgets concurrently, except for parents/children.
*/
async __notifyComponents() {
const subs = this.subscriptions.update || [];
for (let i = 0, iLen = subs.length; i < iLen; i++) {
const sub = subs[i];
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
if (shouldCallback) {
await sub.callback.call(sub.owner);
}
}
}
}
+1 -1
View File
@@ -10,7 +10,7 @@
* - debounce * - debounce
*/ */
export function whenReady(fn) { export function whenReady(fn?: any) {
return new Promise(function(resolve) { return new Promise(function(resolve) {
if (document.readyState !== "loading") { if (document.readyState !== "loading") {
resolve(); resolve();
+9
View File
@@ -0,0 +1,9 @@
import { attrsModule, classModule, eventListenersModule, propsModule } from "./modules";
import { init } from "./vdom";
//------------------------------------------------------------------------------
// patch
//------------------------------------------------------------------------------
export { h, VNode } from "./vdom";
export const patch = init([eventListenersModule, attrsModule, propsModule, classModule]);
+234
View File
@@ -0,0 +1,234 @@
import { Module, VNode, VNodeData } from "./vdom";
//------------------------------------------------------------------------------
// module/props.ts
//------------------------------------------------------------------------------
function updateProps(oldVnode: VNode, vnode: VNode): void {
var key: string,
cur: any,
old: any,
elm = vnode.elm,
oldProps = (oldVnode.data as VNodeData).props,
props = (vnode.data as VNodeData).props;
if (!oldProps && !props) return;
if (oldProps === props) return;
oldProps = oldProps || {};
props = props || {};
for (key in oldProps) {
if (!props[key]) {
delete (elm as any)[key];
}
}
for (key in props) {
cur = props[key];
old = oldProps[key];
if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) {
(elm as any)[key] = cur;
}
}
}
export const propsModule = {
create: updateProps,
update: updateProps
} as Module;
//------------------------------------------------------------------------------
// module/eventlisteners.ts
//------------------------------------------------------------------------------
function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
if (typeof handler === "function") {
// call function handler
handler.call(vnode, event, vnode);
} else if (typeof handler === "object") {
// call handler with arguments
if (typeof handler[0] === "function") {
// special case for single argument for performance
if (handler.length === 2) {
handler[0].call(vnode, handler[1], event, vnode);
} else {
var args = handler.slice(1);
args.push(event);
args.push(vnode);
handler[0].apply(vnode, args);
}
} else {
// call multiple handlers
for (let i = 0, iLen = handler.length; i < iLen; i++) {
invokeHandler(handler[i], vnode, event);
}
}
}
}
function handleEvent(event: Event, vnode: VNode) {
var name = event.type,
on = (vnode.data as VNodeData).on;
// call event handler(s) if exists
if (on && on[name]) {
invokeHandler(on[name], vnode, event);
}
}
function createListener() {
return function handler(event: Event) {
handleEvent(event, (handler as any).vnode);
};
}
function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
var oldOn = (oldVnode.data as VNodeData).on,
oldListener = (oldVnode as any).listener,
oldElm: Element = oldVnode.elm as Element,
on = vnode && (vnode.data as VNodeData).on,
elm: Element = (vnode && vnode.elm) as Element,
name: string;
// optimization for reused immutable handlers
if (oldOn === on) {
return;
}
// remove existing listeners which no longer used
if (oldOn && oldListener) {
// if element changed or deleted we remove all existing listeners unconditionally
if (!on) {
for (name in oldOn) {
// remove listener if element was changed or existing listeners removed
oldElm.removeEventListener(name, oldListener, false);
}
} else {
for (name in oldOn) {
// remove listener if existing listener removed
if (!on[name]) {
oldElm.removeEventListener(name, oldListener, false);
}
}
}
}
// add new listeners which has not already attached
if (on) {
// reuse existing listener or create new
var listener = ((vnode as any).listener = (oldVnode as any).listener || createListener());
// update vnode for listener
listener.vnode = vnode;
// if element changed or added we add all needed listeners unconditionally
if (!oldOn) {
for (name in on) {
// add listener if element was changed or new listeners added
elm.addEventListener(name, listener, false);
}
} else {
for (name in on) {
// add listener if new listener added
if (!oldOn[name]) {
elm.addEventListener(name, listener, false);
}
}
}
}
}
export const eventListenersModule = {
create: updateEventListeners,
update: updateEventListeners,
destroy: updateEventListeners
} as Module;
//------------------------------------------------------------------------------
// attributes.ts
//------------------------------------------------------------------------------
const xlinkNS = "http://www.w3.org/1999/xlink";
const xmlNS = "http://www.w3.org/XML/1998/namespace";
const colonChar = 58;
const xChar = 120;
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
var key: string,
elm: Element = vnode.elm as Element,
oldAttrs = (oldVnode.data as VNodeData).attrs,
attrs = (vnode.data as VNodeData).attrs;
if (!oldAttrs && !attrs) return;
if (oldAttrs === attrs) return;
oldAttrs = oldAttrs || {};
attrs = attrs || {};
// update modified attributes, add new attributes
for (key in attrs) {
const cur = attrs[key];
const old = oldAttrs[key];
if (old !== cur) {
if (cur === true) {
elm.setAttribute(key, "");
} else if (cur === false) {
elm.removeAttribute(key);
} else {
if (key.charCodeAt(0) !== xChar) {
elm.setAttribute(key, cur);
} else if (key.charCodeAt(3) === colonChar) {
// Assume xml namespace
elm.setAttributeNS(xmlNS, key, cur);
} else if (key.charCodeAt(5) === colonChar) {
// Assume xlink namespace
elm.setAttributeNS(xlinkNS, key, cur);
} else {
elm.setAttribute(key, cur);
}
}
}
}
// remove removed attributes
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
// the other option is to remove all attributes with value == undefined
for (key in oldAttrs) {
if (!(key in attrs)) {
elm.removeAttribute(key);
}
}
}
export const attrsModule = {
create: updateAttrs,
update: updateAttrs
} as Module;
//------------------------------------------------------------------------------
// class.ts
//------------------------------------------------------------------------------
function updateClass(oldVnode: VNode, vnode: VNode): void {
var cur: any,
name: string,
elm: Element,
oldClass = (oldVnode.data as VNodeData).class,
klass = (vnode.data as VNodeData).class;
if (!oldClass && !klass) return;
if (oldClass === klass) return;
oldClass = oldClass || {};
klass = klass || {};
elm = vnode.elm as Element;
for (name in oldClass) {
if (!klass[name]) {
elm.classList.remove(name);
}
}
for (name in klass) {
cur = klass[name];
if (cur !== oldClass[name]) {
(elm.classList as any)[cur ? "add" : "remove"](name);
}
}
}
export const classModule = { create: updateClass, update: updateClass } as Module;
+16 -280
View File
@@ -27,6 +27,21 @@ declare global {
} }
} }
type Props = Record<string, any>;
type Attrs = Record<string, string | number | boolean>;
type On = { [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void } & {
[event: string]: EventListener;
};
export interface Module {
pre: PreHook;
create: CreateHook;
update: UpdateHook;
destroy: DestroyHook;
remove: RemoveHook;
post: PostHook;
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// vnode.ts // vnode.ts
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -41,7 +56,7 @@ export interface VNode {
key: Key | undefined; key: Key | undefined;
} }
interface VNodeData { export interface VNodeData {
props?: Props; props?: Props;
attrs?: Attrs; attrs?: Attrs;
on?: On; on?: On;
@@ -454,10 +469,6 @@ interface DOMAPI {
nextSibling: (node: Node) => Node; nextSibling: (node: Node) => Node;
tagName: (elm: Element) => string; tagName: (elm: Element) => string;
setTextContent: (node: Node, text: string | null) => void; setTextContent: (node: Node, text: string | null) => void;
getTextContent: (node: Node) => string | null;
isElement: (node: Node) => node is Element;
isText: (node: Node) => node is Text;
isComment: (node: Node) => node is Comment;
} }
function createElement(tagName: any): HTMLElement { function createElement(tagName: any): HTMLElement {
@@ -504,22 +515,6 @@ function setTextContent(node: Node, text: string | null): void {
node.textContent = text; node.textContent = text;
} }
function getTextContent(node: Node): string | null {
return node.textContent;
}
function isElement(node: Node): node is Element {
return node.nodeType === 1;
}
function isText(node: Node): node is Text {
return node.nodeType === 3;
}
function isComment(node: Node): node is Comment {
return node.nodeType === 8;
}
const htmlDomApi = { const htmlDomApi = {
createElement, createElement,
createElementNS, createElementNS,
@@ -532,10 +527,6 @@ const htmlDomApi = {
nextSibling, nextSibling,
tagName, tagName,
setTextContent, setTextContent,
getTextContent,
isElement,
isText,
isComment
} as DOMAPI; } as DOMAPI;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -631,258 +622,3 @@ export function h(sel: any, b?: any, c?: any): VNode {
} }
return vnode(sel, data, children, text, undefined); return vnode(sel, data, children, text, undefined);
} }
//------------------------------------------------------------------------------
// module/props.ts
//------------------------------------------------------------------------------
type Props = Record<string, any>;
function updateProps(oldVnode: VNode, vnode: VNode): void {
var key: string,
cur: any,
old: any,
elm = vnode.elm,
oldProps = (oldVnode.data as VNodeData).props,
props = (vnode.data as VNodeData).props;
if (!oldProps && !props) return;
if (oldProps === props) return;
oldProps = oldProps || {};
props = props || {};
for (key in oldProps) {
if (!props[key]) {
delete (elm as any)[key];
}
}
for (key in props) {
cur = props[key];
old = oldProps[key];
if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) {
(elm as any)[key] = cur;
}
}
}
export const propsModule = {
create: updateProps,
update: updateProps
} as Module;
//------------------------------------------------------------------------------
// module/module.ts
//------------------------------------------------------------------------------
interface Module {
pre: PreHook;
create: CreateHook;
update: UpdateHook;
destroy: DestroyHook;
remove: RemoveHook;
post: PostHook;
}
//------------------------------------------------------------------------------
// module/eventlisteners.ts
//------------------------------------------------------------------------------
type On = { [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void } & {
[event: string]: EventListener;
};
function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
if (typeof handler === "function") {
// call function handler
handler.call(vnode, event, vnode);
} else if (typeof handler === "object") {
// call handler with arguments
if (typeof handler[0] === "function") {
// special case for single argument for performance
if (handler.length === 2) {
handler[0].call(vnode, handler[1], event, vnode);
} else {
var args = handler.slice(1);
args.push(event);
args.push(vnode);
handler[0].apply(vnode, args);
}
} else {
// call multiple handlers
for (let i = 0, iLen = handler.length; i < iLen; i++) {
invokeHandler(handler[i], vnode, event);
}
}
}
}
function handleEvent(event: Event, vnode: VNode) {
var name = event.type,
on = (vnode.data as VNodeData).on;
// call event handler(s) if exists
if (on && on[name]) {
invokeHandler(on[name], vnode, event);
}
}
function createListener() {
return function handler(event: Event) {
handleEvent(event, (handler as any).vnode);
};
}
function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
var oldOn = (oldVnode.data as VNodeData).on,
oldListener = (oldVnode as any).listener,
oldElm: Element = oldVnode.elm as Element,
on = vnode && (vnode.data as VNodeData).on,
elm: Element = (vnode && vnode.elm) as Element,
name: string;
// optimization for reused immutable handlers
if (oldOn === on) {
return;
}
// remove existing listeners which no longer used
if (oldOn && oldListener) {
// if element changed or deleted we remove all existing listeners unconditionally
if (!on) {
for (name in oldOn) {
// remove listener if element was changed or existing listeners removed
oldElm.removeEventListener(name, oldListener, false);
}
} else {
for (name in oldOn) {
// remove listener if existing listener removed
if (!on[name]) {
oldElm.removeEventListener(name, oldListener, false);
}
}
}
}
// add new listeners which has not already attached
if (on) {
// reuse existing listener or create new
var listener = ((vnode as any).listener = (oldVnode as any).listener || createListener());
// update vnode for listener
listener.vnode = vnode;
// if element changed or added we add all needed listeners unconditionally
if (!oldOn) {
for (name in on) {
// add listener if element was changed or new listeners added
elm.addEventListener(name, listener, false);
}
} else {
for (name in on) {
// add listener if new listener added
if (!oldOn[name]) {
elm.addEventListener(name, listener, false);
}
}
}
}
}
export const eventListenersModule = {
create: updateEventListeners,
update: updateEventListeners,
destroy: updateEventListeners
} as Module;
//------------------------------------------------------------------------------
// attributes.ts
//------------------------------------------------------------------------------
type Attrs = Record<string, string | number | boolean>;
const xlinkNS = "http://www.w3.org/1999/xlink";
const xmlNS = "http://www.w3.org/XML/1998/namespace";
const colonChar = 58;
const xChar = 120;
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
var key: string,
elm: Element = vnode.elm as Element,
oldAttrs = (oldVnode.data as VNodeData).attrs,
attrs = (vnode.data as VNodeData).attrs;
if (!oldAttrs && !attrs) return;
if (oldAttrs === attrs) return;
oldAttrs = oldAttrs || {};
attrs = attrs || {};
// update modified attributes, add new attributes
for (key in attrs) {
const cur = attrs[key];
const old = oldAttrs[key];
if (old !== cur) {
if (cur === true) {
elm.setAttribute(key, "");
} else if (cur === false) {
elm.removeAttribute(key);
} else {
if (key.charCodeAt(0) !== xChar) {
elm.setAttribute(key, cur);
} else if (key.charCodeAt(3) === colonChar) {
// Assume xml namespace
elm.setAttributeNS(xmlNS, key, cur);
} else if (key.charCodeAt(5) === colonChar) {
// Assume xlink namespace
elm.setAttributeNS(xlinkNS, key, cur);
} else {
elm.setAttribute(key, cur);
}
}
}
}
// remove removed attributes
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
// the other option is to remove all attributes with value == undefined
for (key in oldAttrs) {
if (!(key in attrs)) {
elm.removeAttribute(key);
}
}
}
export const attrsModule = {
create: updateAttrs,
update: updateAttrs
} as Module;
//------------------------------------------------------------------------------
// class.ts
//------------------------------------------------------------------------------
function updateClass(oldVnode: VNode, vnode: VNode): void {
var cur: any,
name: string,
elm: Element,
oldClass = (oldVnode.data as VNodeData).class,
klass = (vnode.data as VNodeData).class;
if (!oldClass && !klass) return;
if (oldClass === klass) return;
oldClass = oldClass || {};
klass = klass || {};
elm = vnode.elm as Element;
for (name in oldClass) {
if (!klass[name]) {
elm.classList.remove(name);
}
}
for (name in klass) {
cur = klass[name];
if (cur !== oldClass[name]) {
(elm.classList as any)[cur ? "add" : "remove"](name);
}
}
}
const classModule = { create: updateClass, update: updateClass } as Module;
//------------------------------------------------------------------------------
// patch
//------------------------------------------------------------------------------
export const patch = init([eventListenersModule, attrsModule, propsModule, classModule]);
+20 -16
View File
@@ -3,15 +3,16 @@
exports[`animations t-transition combined with component 1`] = ` exports[`animations t-transition combined with component 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let utils = this.utils; let utils = this.constructor.utils;
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context;
let owner = context; let owner = context;
var h = this.utils.h; var h = this.h;
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 def3; let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false; let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length; let _2_index = c1.length;
c1.push(null); c1.push(null);
let props4 = {}; let props4 = {};
@@ -27,8 +28,8 @@ exports[`animations t-transition combined with component 1`] = `
let componentKey4 = \`Child\`; let componentKey4 = \`Child\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4]; let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')} if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(owner, props4); w4 = new W4(parent, props4);
context.__owl__.cmap[4] = w4.__owl__.id; parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare(); def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => { def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy(); w4.destroy();
@@ -46,16 +47,17 @@ exports[`animations t-transition combined with component 1`] = `
exports[`animations t-transition combined with t-component and t-if 1`] = ` exports[`animations t-transition combined with t-component and t-if 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let utils = this.utils; let utils = this.constructor.utils;
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context;
let owner = context; let owner = context;
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
if (context['state'].display) { if (context['state'].display) {
//COMPONENT //COMPONENT
let def3; let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false; let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length; let _2_index = c1.length;
c1.push(null); c1.push(null);
let props4 = {}; let props4 = {};
@@ -71,8 +73,8 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
let componentKey4 = \`Child\`; let componentKey4 = \`Child\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4]; let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')} if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(owner, props4); w4 = new W4(parent, props4);
context.__owl__.cmap[4] = w4.__owl__.id; parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare(); def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => { def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy(); w4.destroy();
@@ -91,15 +93,16 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
exports[`animations t-transition with no delay/duration 1`] = ` exports[`animations t-transition with no delay/duration 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; let utils = this.constructor.utils;
var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
p1.hook = { p1.hook = {
insert: vn => { insert: vn => {
this.utils.transitionInsert(vn, 'jupiler'); utils.transitionInsert(vn, 'jupiler');
}, },
remove: (vn, rm) => { remove: (vn, rm) => {
this.utils.transitionRemove(vn, 'jupiler', rm); utils.transitionRemove(vn, 'jupiler', rm);
}, },
}; };
c1.push({text: \`blue\`}); c1.push({text: \`blue\`});
@@ -110,15 +113,16 @@ exports[`animations t-transition with no delay/duration 1`] = `
exports[`animations t-transition, on a simple node (insert) 1`] = ` exports[`animations t-transition, on a simple node (insert) 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; let utils = this.constructor.utils;
var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
p1.hook = { p1.hook = {
insert: vn => { insert: vn => {
this.utils.transitionInsert(vn, 'chimay'); utils.transitionInsert(vn, 'chimay');
}, },
remove: (vn, rm) => { remove: (vn, rm) => {
this.utils.transitionRemove(vn, 'chimay', rm); utils.transitionRemove(vn, 'chimay', rm);
}, },
}; };
c1.push({text: \`blue\`}); c1.push({text: \`blue\`});
+2 -2
View File
@@ -1,5 +1,5 @@
import { Component, Env } from "../src/component"; import { Component, Env } from "../src/component/component";
import { QWeb } from "../src/qweb_core"; import { QWeb } from "../src/qweb/index";
import { import {
makeDeferred, makeDeferred,
makeTestFixture, makeTestFixture,
@@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`default props default values are also set whenever component is updated 1`] = `"<div>1</div>"`;
exports[`default props default values are also set whenever component is updated 2`] = `"<div>4</div>"`;
exports[`props validation props are validated in dev mode (code snapshot) 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {message:1};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let componentKey4 = \`Child\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
utils.validateProps(W4, props4)
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
utils.validateProps(w4.constructor, props4)
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
@@ -1,6 +1,6 @@
import { Component, Env } from "../src/component"; import { Component, Env } from "../../src/component/component";
import { QWeb } from "../src/qweb_core"; import { QWeb } from "../../src/qweb/qweb";
import { EventBus } from "../src/event_bus"; import { EventBus } from "../../src/core/event_bus";
import { import {
makeDeferred, makeDeferred,
makeTestFixture, makeTestFixture,
@@ -9,7 +9,7 @@ import {
nextTick, nextTick,
normalize, normalize,
editInput editInput
} from "./helpers"; } from "../helpers";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Setup and helpers // Setup and helpers
@@ -733,15 +733,16 @@ describe("lifecycle hooks", () => {
// we make sure here that willPatch/patched is only called if widget is in // we make sure here that willPatch/patched is only called if widget is in
// dom, mounted // dom, mounted
const steps: string[] = []; const steps: string[] = [];
env.qweb.addTemplate( env.qweb.addTemplates(`
"ParentWidget", <templates>
` <div t-name="ParentWidget">
<div> <ChildWidget t-if="state.flag" v="state.n" t-keepalive="1"/>
<t t-if="state.flag" t-component="child" v="state.n" t-keepalive="1"/> </div>
</div>` </templates>
); `);
class ParentWidget extends Widget { class ParentWidget extends Widget {
components = { child: ChildWidget }; components = { ChildWidget };
state = { n: 1, flag: true }; state = { n: 1, flag: true };
} }
@@ -761,6 +762,8 @@ describe("lifecycle hooks", () => {
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
expect(steps).toEqual(["child:mounted"]); expect(steps).toEqual(["child:mounted"]);
widget.state.flag = false; widget.state.flag = false;
await nextTick(); await nextTick();
@@ -879,18 +882,22 @@ describe("composition", () => {
expect(fixture.innerHTML).toBe("<div><span>1</span></div>"); expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
}); });
test("throw a nice error if it cannot find component", async () => { test("display a nice error if it cannot find component", async () => {
expect.assertions(1); const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplate("Parent", `<div><SomeMispelledWidget /></div>`); env.qweb.addTemplate("Parent", `<div><SomeMispelledWidget /></div>`);
class Parent extends Widget { class Parent extends Widget {
components = { SomeWidget: Widget }; components = { SomeWidget: Widget };
} }
const parent = new Parent(env); const parent = new Parent(env);
try { await parent.mount(fixture);
await parent.mount(fixture);
} catch (e) { expect(console.error).toBeCalledTimes(1);
expect(e.message).toBe('Cannot find the definition of component "SomeMispelledWidget"'); expect((<any>console.error).mock.calls[0][0].message).toMatch(
} 'Cannot find the definition of component "SomeMispelledWidget"'
);
console.error = consoleError;
}); });
test("t-refs on widget are components", async () => { test("t-refs on widget are components", async () => {
@@ -1120,6 +1127,7 @@ describe("composition", () => {
const input2 = fixture.getElementsByTagName("input")[0]; const input2 = fixture.getElementsByTagName("input")[0];
expect(input).toBe(input2); expect(input).toBe(input2);
expect(input2.value).toBe("test"); expect(input2.value).toBe("test");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
}); });
test("sub widget with t-ref and t-keepalive", async () => { test("sub widget with t-ref and t-keepalive", async () => {
@@ -2604,6 +2612,39 @@ describe("async rendering", () => {
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>1/1</span>"); expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>1/1</span>");
}); });
test("rendering component again in next microtick", async () => {
class Child extends Widget {}
class App extends Widget {
components = { Child };
async onClick() {
(env as any).flag = true;
this.render();
await Promise.resolve();
this.render();
}
}
env.qweb.addTemplates(`
<templates>
<div t-name="Child">Child</div>
<div t-name="App">
<button t-on-click="onClick">Click</button>
<t t-if="env.flag"><Child/></t>
</div>
</templates>
`);
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>Click</button></div>");
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>Click</button><div>Child</div></div>");
});
}); });
describe("updating environment", () => { describe("updating environment", () => {
@@ -2726,7 +2767,8 @@ describe("widget and observable state", () => {
}); });
test("subcomponents cannot change observable state received from parent", async () => { test("subcomponents cannot change observable state received from parent", async () => {
expect.assertions(1); const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplate("Parent", `<div><Child obj="state.obj"/></div>`); env.qweb.addTemplate("Parent", `<div><Child obj="state.obj"/></div>`);
class Parent extends Widget { class Parent extends Widget {
state = { obj: { coffee: 1 } }; state = { obj: { coffee: 1 } };
@@ -2739,11 +2781,13 @@ describe("widget and observable state", () => {
} }
} }
const parent = new Parent(env); const parent = new Parent(env);
try { await parent.mount(fixture);
await parent.mount(fixture);
} catch (e) { expect(console.error).toBeCalledTimes(1);
expect(e.message).toBe('Observed state cannot be changed here! (key: "coffee", val: "2")'); expect((<any>console.error).mock.calls[0][0].message).toMatch(
} 'Observed state cannot be changed here! (key: "coffee", val: "2")'
);
console.error = consoleError;
}); });
}); });
@@ -2920,6 +2964,113 @@ describe("t-slot directive", () => {
); );
}); });
test("slots are rendered with proper context, part 2", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<u><li t-foreach="state.users" t-as="user" t-key="user.id">
<Link to="'/user/' + user.id">User <t t-esc="user.name"/></Link>
</li></u>
</div>
</templates>
`);
class Link extends Widget {}
class App extends Widget {
state = { users: [{ id: 1, name: "Aaron" }, { id: 2, name: "David" }] };
components = { Link };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User David</a></li></u></div>'
);
expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.users[1].name = "Mathieu";
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User Mathieu</a></li></u></div>'
);
});
test("slots are rendered with proper context, part 3", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<u><li t-foreach="state.users" t-as="user" t-key="user.id" >
<t t-set="userdescr" t-value="'User ' + user.name"/>
<Link to="'/user/' + user.id"><t t-esc="userdescr"/></Link>
</li></u>
</div>
</templates>
`);
class Link extends Widget {}
class App extends Widget {
state = { users: [{ id: 1, name: "Aaron" }, { id: 2, name: "David" }] };
components = { Link };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User David</a></li></u></div>'
);
expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.users[1].name = "Mathieu";
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User Mathieu</a></li></u></div>'
);
});
test("slots are rendered with proper context, part 4", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<t t-set="userdescr" t-value="'User ' + state.user.name"/>
<Link to="'/user/' + state.user.id"><t t-esc="userdescr"/></Link>
</div>
</templates>
`);
class Link extends Widget {}
class App extends Widget {
state = { user: { id: 1, name: "Aaron" } };
components = { Link };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe('<div><a href="/user/1">User Aaron</a></div>');
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.user.name = "David";
await nextTick();
expect(fixture.innerHTML).toBe('<div><a href="/user/1">User David</a></div>');
});
test("refs are properly bound in slots", async () => { test("refs are properly bound in slots", async () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
@@ -3087,6 +3238,76 @@ describe("t-slot directive", () => {
expect(console.log).toHaveBeenCalledTimes(0); expect(console.log).toHaveBeenCalledTimes(0);
console.log = consoleLog; console.log = consoleLog;
}); });
test("slot preserves properly parented relationship", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Child>
<GrandChild/>
</Child>
</div>
<div t-name="Child"><t t-slot="default"/></div>
<div t-name="GrandChild">Grand Child</div>
</templates>
`);
class Child extends Widget {}
class GrandChild extends Widget {}
class Parent extends Widget {
components = { Child, GrandChild };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div>Grand Child</div></div></div>");
const parentChildren = children(parent);
expect(parentChildren.length).toBe(1);
expect(parentChildren[0]).toBeInstanceOf(Child);
const childrenChildren = children(parentChildren[0]);
expect(childrenChildren.length).toBe(1);
expect(childrenChildren[0]).toBeInstanceOf(GrandChild);
});
test("slot are properly rendered if inner props are changed", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
SC:<t t-esc="props.val"/>
</div>
<div t-name="GenericComponent">
<t t-slot="default" />
</div>
<div t-name="App">
<button t-on-click="inc">Inc[<t t-esc="state.val"/>]</button>
<GenericComponent>
<SomeComponent val="state.val"/>
</GenericComponent>
</div>
</templates>
`);
class SomeComponent extends Widget {}
class GenericComponent extends Widget {}
class App extends Widget {
components = { GenericComponent, SomeComponent };
state = { val: 4 };
inc() {
this.state.val++;
}
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>Inc[4]</button><div><div> SC:4</div></div></div>");
(<any>fixture.querySelector("button")).click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>Inc[5]</button><div><div> SC:5</div></div></div>");
});
}); });
describe("t-model directive", () => { describe("t-model directive", () => {
@@ -3322,7 +3543,7 @@ describe("environment and plugins", () => {
env.someFlag = true; env.someFlag = true;
bus.on("some-event", null, () => { bus.on("some-event", null, () => {
env.someFlag = !env.someFlag; env.someFlag = !env.someFlag;
env.qweb.trigger("update"); env.qweb.forceUpdate();
}); });
}; };
@@ -3347,3 +3568,520 @@ describe("environment and plugins", () => {
expect(fixture.innerHTML).toBe("<div>Blue</div>"); expect(fixture.innerHTML).toBe("<div>Blue</div>");
}); });
}); });
describe("component error handling (catchError)", () => {
/**
* This test suite requires often to wait for 3 ticks. Here is why:
* - First tick is to let the app render and crash.
* - When we crash, we call the catchError handler in a setTimeout (because we
* need to wait for the previous rendering to be completely stopped). So, we
* need to wait for the second tick.
* - Then, when the handler changes the state, we need to wait for the interface
* to be rerendered.
* */
test("can catch an error in a component render function", async () => {
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else="1"><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">hey<t t-esc="props.flag and state.this.will.crash"/>
</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent flag="state.flag"/></ErrorBoundary>
</div>
</templates>`);
const handler = jest.fn();
env.qweb.on("error", null, handler);
class ErrorComponent extends Widget {}
class ErrorBoundary extends Widget {
state = { error: false };
catchError() {
this.state.error = true;
}
}
class App extends Widget {
state = { flag: false };
components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div>hey</div></div></div>");
app.state.flag = true;
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(1);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("no component catching error lead to full app destruction", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorComponent">hey<t t-esc="props.flag and state.this.will.crash"/>
</div>
<div t-name="App">
<ErrorComponent flag="state.flag"/>
</div>
</templates>`);
class ErrorComponent extends Widget {}
class App extends Widget {
state = { flag: false };
components = { ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
app.state.flag = true;
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(1);
console.error = consoleError;
expect(app.__owl__.isDestroyed).toBe(true);
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the initial call of a component render function", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else="1"><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">hey<t t-esc="state.this.will.crash"/>
</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Widget {}
class ErrorBoundary extends Widget {
state = { error: false };
catchError() {
this.state.error = true;
}
}
class App extends Widget {
components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(1);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the constructor call of a component render function", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else="1"><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">Some text</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Widget {
constructor(parent) {
super(parent);
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Widget {
state = { error: false };
catchError() {
this.state.error = true;
}
}
class App extends Widget {
components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(1);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the willStart call", async () => {
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else="1"><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">Some text</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Widget {
async willStart() {
// we wait a little bit to be in a different stack frame
await nextTick();
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Widget {
state = { error: false };
catchError() {
this.state.error = true;
}
}
class App extends Widget {
components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(1);
console.error = consoleError;
});
test("can catch an error in the mounted call", async () => {
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else="1"><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">Some text</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Widget {
mounted() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Widget {
state = { error: false };
catchError() {
this.state.error = true;
}
}
class App extends Widget {
components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
});
test("can catch an error in the willPatch call", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else="1"><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent"><t t-esc="props.message"/></div>
<div t-name="App">
<ErrorBoundary><ErrorComponent message="state.message" /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Widget {
willPatch() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Widget {
state = { error: false };
catchError() {
this.state.error = true;
}
}
class App extends Widget {
state = { message: "abc" };
components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div>abc</div></div></div>");
app.state.message = "def";
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
});
});
describe("top level sub widgets", () => {
test("basic use", async () => {
env.qweb.addTemplates(`
<templates>
<t t-name="Parent">
<Child p="1"/>
</t>
<span t-name="Child">child<t t-esc="props.p"/></span>
</templates>`);
class Child extends Widget {}
class Parent extends Widget {
components = { Child };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<span>child1</span>");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
});
test("sub widget is interactive", async () => {
env.qweb.addTemplates(`
<templates>
<t t-name="Parent">
<Child p="1"/>
</t>
<span t-name="Child"><button t-on-click="inc">click</button>child<t t-esc="state.val"/></span>
</templates>`);
class Child extends Widget {
state = { val: 1 };
inc() {
this.state.val++;
}
}
class Parent extends Widget {
components = { Child };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<span><button>click</button>child1</span>");
const button = fixture.querySelector("button")!;
button.click();
await nextTick();
expect(fixture.innerHTML).toBe("<span><button>click</button>child2</span>");
});
test("can select a sub widget ", async () => {
env.qweb.addTemplates(`
<templates>
<t t-name="Parent">
<t t-if="env.flag"><Child /></t>
<t t-if="!env.flag"><OtherChild /></t>
</t>
<span t-name="Child">CHILD 1</span>
<div t-name="OtherChild">CHILD 2</div>
</templates>`);
class Child extends Widget {}
class OtherChild extends Widget {}
class Parent extends Widget {
components = { Child, OtherChild };
}
(<any>env).flag = true;
let parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<span>CHILD 1</span>");
parent.destroy();
(<any>env).flag = false;
parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div>CHILD 2</div>");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
});
test("can select a sub widget, part 2", async () => {
env.qweb.addTemplates(`
<templates>
<t t-name="Parent">
<t t-if="state.flag"><Child /></t>
<t t-if="!state.flag"><OtherChild /></t>
</t>
<span t-name="Child">CHILD 1</span>
<div t-name="OtherChild">CHILD 2</div>
</templates>`);
class Child extends Widget {}
class OtherChild extends Widget {}
class Parent extends Widget {
state = { flag: true };
components = { Child, OtherChild };
}
let parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<span>CHILD 1</span>");
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div>CHILD 2</div>");
});
});
describe("unmounting and remounting", () => {
test("widget can be unmounted and remounted", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="MyWidget">Hey</div>
</templates>`);
const steps: string[] = [];
class MyWidget extends Widget {
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
}
const w = new MyWidget(env);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]);
w.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual(["willstart", "mounted", "willunmount"]);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted", "willunmount", "mounted"]);
});
test("widget can be mounted twice without ill effect", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="MyWidget">Hey</div>
</templates>`);
const steps: string[] = [];
class MyWidget extends Widget {
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
}
const w = new MyWidget(env);
await w.mount(fixture);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]);
});
test("state changes in willUnmount do not trigger rerender", async () => {
const steps: string[] = [];
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Child t-if="state.flag" val="state.val"/>
</div>
<span t-name="Child"><t t-esc="props.val"/><t t-esc="state.n"/></span>
</templates>
`);
class Child extends Widget {
state = { n: 2 };
__render(a, b, c, d) {
steps.push("render");
return super.__render(a, b, c, d);
}
willPatch() {
steps.push("willPatch");
}
patched() {
steps.push("patched");
}
willUnmount() {
steps.push("willUnmount");
this.state.n = 3;
}
}
class Parent extends Widget {
components = { Child };
state = { val: 1, flag: true };
}
const widget = new Parent(env);
await widget.mount(fixture);
expect(steps).toEqual(["render"]);
expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
widget.state.flag = false;
await nextTick();
// we make sure here that no call to __render is done
expect(steps).toEqual(["render", "willUnmount"]);
});
test("state changes in willUnmount will be applied on remount", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="TestWidget"><t t-esc="state.val"/></div>
</templates>
`);
class TestWidget extends Widget {
state = { val: 1 };
willUnmount() {
this.state.val = 3;
}
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>1</div>");
widget.unmount();
expect(fixture.innerHTML).toBe("");
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>1</div>");
widget.unmount();
await widget.mount(fixture, true);
expect(fixture.innerHTML).toBe("<div>3</div>");
});
});
@@ -1,6 +1,6 @@
import { Component, Env } from "../src/component"; import { Component, Env } from "../../src/component/component";
import { makeTestFixture, makeTestEnv } from "./helpers"; import { makeTestFixture, makeTestEnv } from "../helpers";
import { QWeb } from "../src"; import { QWeb } from "../../src/qweb";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Setup and helpers // Setup and helpers
@@ -45,19 +45,6 @@ describe("props validation", () => {
}).not.toThrow(); }).not.toThrow();
}); });
test("props validation is also done on update props", async () => {
expect.assertions(1);
class TestWidget extends Widget {
static props = ["message"];
}
const w = new TestWidget(env, { message: "bottle" });
try {
await w.__updateProps({});
} catch (e) {
expect(e.message).toBe("Missing props 'message' (component 'TestWidget')");
}
});
test("props: list of strings", async () => { test("props: list of strings", async () => {
class TestWidget extends Widget { class TestWidget extends Widget {
static props = ["message"]; static props = ["message"];
@@ -234,6 +221,96 @@ describe("props validation", () => {
new TestWidget(env, { p: { id: 1, url: [12, true] } }); new TestWidget(env, { p: { id: 1, url: [12, true] } });
}).toThrow(); }).toThrow();
}); });
test("props are validated in dev mode (code snapshot)", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<Child message="1"/>
</div>
<div t-name="Child"><t t-esc="props.message"/></div>
</templates>`);
class Child extends Widget {
static props = ["message"];
}
class App extends Widget {
components = { Child };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
// need to make sure there are 2 call to update props. one at component
// creation, and one at update time.
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
});
test("props: list of strings with optional props", async () => {
class TestWidget extends Widget {
static props = ["message", "someProp?"];
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { someProp: 1 });
}).toThrow();
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: 1 });
}).not.toThrow();
});
test("props: can be defined with a boolean", async () => {
class TestWidget extends Widget {
static props = { message: true };
}
expect(() => {
QWeb.utils.validateProps(TestWidget, {});
}).toThrow();
});
test("props: extra props cause an error", async () => {
class TestWidget extends Widget {
static props = ["message"];
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: 1, flag: true });
}).toThrow();
});
test("props: extra props cause an error, part 2", async () => {
class TestWidget extends Widget {
static props = {message: true};
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: 1, flag: true });
}).toThrow();
});
test("props: optional prop do not cause an error", async () => {
class TestWidget extends Widget {
static props = ["message?"];
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: 1});
}).not.toThrow();
});
test("optional prop do not cause an error if value is undefined", async () => {
class TestWidget extends Widget {
static props = {message: {type: String, optional: true}};
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: undefined});
}).not.toThrow();
expect(() => {
QWeb.utils.validateProps(TestWidget, { message: null});
}).toThrow();
});
}); });
describe("default props", () => { describe("default props", () => {
@@ -250,9 +327,17 @@ describe("default props", () => {
class TestWidget extends Widget { class TestWidget extends Widget {
static defaultProps = { p: 4 }; static defaultProps = { p: 4 };
} }
env.qweb.addTemplates(`
<templates>
<div t-name="TestWidget"><t t-esc="props.p"/></div>
</templates>`);
const w = new TestWidget(env, { p: 1 }); const w = new TestWidget(env, { p: 1 });
await w.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
await w.__updateProps({}); await w.__updateProps({});
await w.render();
expect(w.props.p).toBe(4); expect(w.props.p).toBe(4);
expect(fixture.innerHTML).toMatchSnapshot();
}); });
}); });
@@ -1,4 +1,4 @@
import { EventBus } from "../src/event_bus"; import { EventBus } from "../../src/core/event_bus";
describe("event bus behaviour", () => { describe("event bus behaviour", () => {
test("can subscribe and be notified", () => { test("can subscribe and be notified", () => {
+466
View File
@@ -0,0 +1,466 @@
import { Observer } from "../../src/core/observer";
import { nextMicroTick } from "../helpers";
describe("observer", () => {
test("properly observe objects", () => {
const observer = new Observer();
const obj: any = observer.observe({});
expect(typeof obj).toBe("object");
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
const obj2: any = observer.observe({ a: 1 });
expect(observer.revNumber(obj2)).toBe(1);
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
obj2.a = 2;
expect(observer.revNumber(obj)).toBe(1);
expect(observer.revNumber(obj2)).toBe(2);
expect(observer.rev).toBe(2);
expect(obj2).toEqual({
a: 2
});
});
test("properly handle null or undefined", () => {
const observer = new Observer();
const obj: any = observer.observe({ a: null, b: undefined });
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
obj.a = 3;
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
obj.b = 5;
expect(observer.revNumber(obj)).toBe(3);
expect(observer.deepRevNumber(obj)).toBe(3);
expect(observer.rev).toBe(3);
obj.a = null;
obj.b = undefined;
expect(observer.revNumber(obj)).toBe(5);
expect(observer.deepRevNumber(obj)).toBe(5);
expect(observer.rev).toBe(5);
expect(obj).toEqual({
a: null,
b: undefined
});
});
test("properly handle dates", () => {
const observer = new Observer();
const date = new Date();
const obj: any = observer.observe({ date });
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
expect(typeof obj.date.getFullYear()).toBe('number');
expect(obj.date).toBe(date);
obj.date = new Date();
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
expect(obj.date).not.toBe(date);
});
test("can change values in array", () => {
const observer = new Observer();
const obj: any = observer.observe({ arr: [1, 2] });
expect(Array.isArray(obj.arr)).toBe(true);
expect(observer.revNumber(obj.arr)).toBe(1);
expect(observer.deepRevNumber(obj.arr)).toBe(1);
expect(observer.rev).toBe(1);
obj.arr[0] = "nope";
expect(observer.revNumber(obj.arr)).toBe(2);
expect(observer.deepRevNumber(obj.arr)).toBe(2);
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
});
test("various object property changes", () => {
const observer = new Observer();
const obj: any = observer.observe({ a: 1 });
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
obj.a = 2;
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
// same value again
obj.a = 2;
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
obj.a = 3;
expect(observer.revNumber(obj)).toBe(3);
expect(observer.deepRevNumber(obj)).toBe(3);
expect(observer.rev).toBe(3);
});
test("properly observe arrays", () => {
const observer = new Observer();
const arr: any = observer.observe([]);
expect(Array.isArray(arr)).toBe(true);
expect(arr.length).toBe(0);
expect(observer.revNumber(arr)).toBe(1);
expect(observer.deepRevNumber(arr)).toBe(1);
expect(observer.rev).toBe(1);
arr.push(1);
expect(observer.revNumber(arr)).toBe(2);
expect(observer.deepRevNumber(arr)).toBe(2);
expect(observer.rev).toBe(2);
expect(arr.length).toBe(1);
expect(arr).toEqual([1]);
arr.splice(1, 0, "hey");
expect(observer.revNumber(arr)).toBe(3);
expect(observer.deepRevNumber(arr)).toBe(3);
expect(observer.rev).toBe(3);
expect(arr).toEqual([1, "hey"]);
expect(arr.length).toBe(2);
arr.unshift("lindemans");
//it generates 3 primitive operations
expect(observer.revNumber(arr)).toBe(6);
expect(observer.deepRevNumber(arr)).toBe(6);
expect(observer.rev).toBe(6);
expect(arr).toEqual(["lindemans", 1, "hey"]);
expect(arr.length).toBe(3);
arr.reverse();
//it generates 2 primitive operations
expect(observer.revNumber(arr)).toBe(8);
expect(observer.deepRevNumber(arr)).toBe(8);
expect(observer.rev).toBe(8);
expect(arr).toEqual(["hey", 1, "lindemans"]);
expect(arr.length).toBe(3);
arr.pop(); // one set, one delete
expect(observer.revNumber(arr)).toBe(10);
expect(observer.deepRevNumber(arr)).toBe(10);
expect(observer.rev).toBe(10);
expect(arr).toEqual(["hey", 1]);
expect(arr.length).toBe(2);
arr.shift(); // 2 sets, 1 delete
expect(observer.revNumber(arr)).toBe(13);
expect(observer.deepRevNumber(arr)).toBe(13);
expect(observer.rev).toBe(13);
expect(arr).toEqual([1]);
expect(arr.length).toBe(1);
});
test("object pushed into arrays are observed", () => {
const observer = new Observer();
const arr: any = observer.observe([]);
expect(observer.rev).toBe(1);
arr.push({ kriek: 5 });
expect(observer.rev).toBe(2);
expect(observer.revNumber(arr)).toBe(2);
expect(observer.revNumber(arr[0])).toBe(2);
arr[0].kriek = 6;
expect(observer.rev).toBe(3);
expect(observer.revNumber(arr)).toBe(2);
expect(observer.deepRevNumber(arr)).toBe(3);
expect(observer.revNumber(arr[0])).toBe(3);
});
test("set new property on observed object", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const state: any = observer.observe({ a: 1 });
expect(observer.notifyCB).toBeCalledTimes(0);
expect(observer.rev).toBe(1);
expect(observer.revNumber(state)).toBe(1);
state.b = 8;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(1);
expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(2);
expect(state.b).toBe(8);
});
test("delete property from observed object", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const state: any = observer.observe({ a: 1, b: 8 });
observer.observe(state);
expect(observer.notifyCB).toBeCalledTimes(0);
expect(observer.rev).toBe(1);
expect(observer.revNumber(state)).toBe(1);
delete state.b;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(1);
expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(2);
expect(state).toEqual({ a: 1 });
});
test("set element in observed array", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const state: any = observer.observe(["a"]);
expect(observer.rev).toBe(1);
expect(observer.revNumber(state)).toBe(1);
expect(observer.deepRevNumber(state)).toBe(1);
expect(observer.notifyCB).toBeCalledTimes(0);
state[1] = "b";
await nextMicroTick();
expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(2);
expect(observer.deepRevNumber(state)).toBe(2);
expect(observer.notifyCB).toBeCalledTimes(1);
expect(state).toEqual(["a", "b"]);
});
test("properly observe arrays in object", () => {
const observer = new Observer();
const state: any = observer.observe({ arr: [] });
expect(observer.rev).toBe(1);
expect(observer.revNumber(state.arr)).toBe(1);
expect(observer.deepRevNumber(state.arr)).toBe(1);
expect(state.arr.length).toBe(0);
state.arr.push(1);
expect(observer.rev).toBe(2);
expect(observer.revNumber(state.arr)).toBe(2);
expect(observer.deepRevNumber(state.arr)).toBe(2);
expect(state.arr.length).toBe(1);
});
test("properly observe objects in array", () => {
const observer = new Observer();
const state: any = observer.observe({ arr: [{ something: 1 }] });
observer.observe(state);
expect(observer.rev).toBe(1);
expect(observer.revNumber(state.arr)).toBe(1);
expect(observer.revNumber(state.arr[0])).toBe(1);
state.arr[0].something = 2;
expect(observer.rev).toBe(2);
expect(observer.revNumber(state.arr)).toBe(1);
expect(observer.revNumber(state.arr[0])).toBe(2);
});
test("properly observe objects in object", () => {
const observer = new Observer();
const state: any = observer.observe({ a: { b: 1 } });
expect(observer.rev).toBe(1);
expect(observer.revNumber(state)).toBe(1);
expect(observer.revNumber(state.a)).toBe(1);
state.a.b = 2;
expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(1);
expect(observer.revNumber(state.a)).toBe(2);
});
test("reobserve new object values", () => {
const observer = new Observer();
const obj: any = observer.observe({ a: 1 });
expect(observer.rev).toBe(1);
expect(observer.revNumber(obj)).toBe(1);
obj.a = { b: 2 };
expect(observer.rev).toBe(2);
expect(observer.revNumber(obj)).toBe(2);
expect(observer.revNumber(obj.a)).toBe(2);
obj.a.b = 3;
expect(observer.rev).toBe(3);
expect(observer.revNumber(obj)).toBe(2);
expect(observer.revNumber(obj.a)).toBe(3);
});
test("deep observe misc changes", () => {
const observer = new Observer();
const state: any = observer.observe({ o: { a: 1 }, arr: [1], n: 13 });
expect(observer.revNumber(state)).toBe(1);
expect(observer.deepRevNumber(state)).toBe(1);
state.o.a = 2;
expect(observer.rev).toBe(2);
expect(observer.revNumber(state)).toBe(1);
expect(observer.deepRevNumber(state)).toBe(2);
state.arr.push(2);
expect(observer.rev).toBe(3);
expect(observer.revNumber(state)).toBe(1);
expect(observer.deepRevNumber(state)).toBe(3);
state.n = 155;
expect(observer.rev).toBe(4);
expect(observer.revNumber(state)).toBe(2);
expect(observer.deepRevNumber(state)).toBe(4);
});
test("properly handle already observed state", () => {
const observer = new Observer();
const obj1: any = observer.observe({ a: 1 });
const obj2: any = observer.observe({ b: 1 });
expect(observer.revNumber(obj1)).toBe(1);
expect(observer.revNumber(obj2)).toBe(1);
obj1.a = 2;
obj2.b = 3;
expect(observer.revNumber(obj1)).toBe(2);
expect(observer.revNumber(obj2)).toBe(2);
obj2.b = obj1;
expect(observer.revNumber(obj1)).toBe(2);
expect(observer.revNumber(obj2)).toBe(3);
});
test("can set a property more than once", () => {
const observer = new Observer();
const obj: any = observer.observe({});
expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1);
obj.aku = "always finds annoying problems";
expect(observer.revNumber(obj)).toBe(2);
expect(observer.deepRevNumber(obj)).toBe(2);
expect(observer.rev).toBe(2);
obj.aku = "always finds good problems";
expect(observer.revNumber(obj)).toBe(3);
expect(observer.deepRevNumber(obj)).toBe(3);
expect(observer.rev).toBe(3);
});
test("properly handle swapping elements", () => {
const observer = new Observer();
const obj: any = observer.observe({ a: { arr: [] }, b: 1 });
// swap a and b
const b = obj.b;
obj.b = obj.a;
obj.a = b;
expect(observer.rev).toBe(3);
// push something into array to make sure it works
obj.b.arr.push("blanche");
expect(observer.rev).toBe(4);
});
test("properly handle assigning observed obj containing array", () => {
const observer = new Observer();
const obj: any = observer.observe({ a: { arr: [], val: "test" } });
expect(observer.rev).toBe(1);
obj.a = { ...obj.a, val: "test2" };
expect(observer.rev).toBe(2);
// push something into array to make sure it works
obj.a.arr.push("blanche");
expect(observer.rev).toBe(3);
});
test("accept cycles in observed state", () => {
const observer = new Observer();
let obj1: any = {};
let obj2: any = { b: obj1, key: 1 };
obj1.a = obj2;
obj1 = observer.observe(obj1);
obj2 = obj1.a;
expect(observer.revNumber(obj1)).toBe(1);
expect(observer.revNumber(obj2)).toBe(1);
obj2.key = 3;
expect(observer.revNumber(obj1)).toBe(1);
expect(observer.revNumber(obj2)).toBe(2);
});
test("call callback when state is changed", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const obj: any = observer.observe({ a: 1, b: { c: 2 }, d: [{ e: 3 }], f: 4 });
expect(observer.notifyCB).toBeCalledTimes(0);
obj.a = 2;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(1);
obj.b.c = 3;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(2);
obj.d[0].e = 5;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(3);
obj.a = 111;
obj.f = 222;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(4);
});
test("throw error when state is mutated in object if allowMutation=false", async () => {
const observer = new Observer();
observer.allowMutations = false;
const obj: any = observer.observe({ a: 1 });
expect(() => {
obj.a = 2;
}).toThrow('Observed state cannot be changed here! (key: "a", val: "2")');
});
test("throw error when state is mutated in array if allowMutation=false", async () => {
const observer = new Observer();
observer.allowMutations = false;
const obj: any = observer.observe({ a: [1] });
expect(() => {
obj.a.push(2);
}).toThrow('Observed state cannot be changed here! (key: "1", val: "2")');
});
});
+22 -8
View File
@@ -1,8 +1,9 @@
import { Env } from "../src/component"; import { Env } from "../src/component/component";
import { EvalContext, QWeb, UTILS } from "../src/qweb_core"; import { EvalContext, QWeb } from "../src/qweb/qweb";
import { patch } from "../src/vdom"; import { patch } from "../src/vdom";
import "../src/qweb_directives"; import "../src/qweb/base_directives";
import "../src/qweb_extensions"; import "../src/qweb/extensions";
import "../src/component/directive";
export function nextMicroTick(): Promise<void> { export function nextMicroTick(): Promise<void> {
return Promise.resolve(); return Promise.resolve();
@@ -68,24 +69,37 @@ export function renderToDOM(
return result.elm as HTMLElement; return result.elm as HTMLElement;
} }
/**
* Render a template to an html string. The big difference with the
* renderToString method in QWeb is that we use the renderToDom method, which
* snapshots the resulting template function. Doing so gives us a large body
* of reference to evaluate changes in the generated QWeb code.
*
* Note that the result of renderToString is guaranteed to be the same as the
* one from QWeb.
*/
export function renderToString(qweb: QWeb, t: string, context: EvalContext = {}): string { export function renderToString(qweb: QWeb, t: string, context: EvalContext = {}): string {
const node = renderToDOM(qweb, t, context); const node = renderToDOM(qweb, t, context);
return node instanceof Text ? node.textContent! : node.outerHTML; const result = node instanceof Text ? node.textContent! : node.outerHTML;
if (result !== qweb.renderToString(t, context)) {
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
}
return result;
} }
// hereafter, we define two helpers to patch/unpatch the nextFrame utils. This // hereafter, we define two helpers to patch/unpatch the nextFrame utils. This
// is useful for animations tests, as we hook before repaints to trigger // is useful for animations tests, as we hook before repaints to trigger
// animations (thanks to requestAnimationFrame). Patching nextFrame allows to // animations (thanks to requestAnimationFrame). Patching nextFrame allows to
// simulate calls to this hook. One must not forget to unpatch afterwards. // simulate calls to this hook. One must not forget to unpatch afterwards.
let nextFrame = UTILS.nextFrame; let nextFrame = QWeb.utils.nextFrame;
export function patchNextFrame(f: Function) { export function patchNextFrame(f: Function) {
UTILS.nextFrame = (cb: () => void) => { QWeb.utils.nextFrame = (cb: () => void) => {
setTimeout(() => f(cb)); setTimeout(() => f(cb));
}; };
} }
export function unpatchNextFrame() { export function unpatchNextFrame() {
UTILS.nextFrame = nextFrame; QWeb.utils.nextFrame = nextFrame;
} }
export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) { export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) {
-394
View File
@@ -1,394 +0,0 @@
import { Observer } from "../src/observer";
import { nextMicroTick } from "./helpers";
describe("observer", () => {
test("properly observe objects", () => {
const observer = new Observer();
const obj: any = {};
observer.observe(obj);
expect(obj.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
const ob2: any = { a: 1 };
observer.observe(ob2);
expect(ob2.__owl__.rev).toBe(1);
ob2.a = 2;
expect(observer.rev).toBe(2);
expect(ob2.__owl__.rev).toBe(2);
ob2.b = 3;
expect(observer.rev).toBe(2);
expect(ob2.__owl__.rev).toBe(2);
observer.set(ob2, "b", 4);
expect(observer.rev).toBe(3);
expect(ob2.__owl__.rev).toBe(3);
});
test("properly handle null or undefined", () => {
const observer = new Observer();
const obj: any = { a: null, b: undefined };
observer.observe(obj);
expect(obj.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
obj.a = 3;
expect(obj.__owl__.rev).toBe(2);
obj.b = 5;
expect(obj.__owl__.rev).toBe(3);
obj.a = null;
obj.b = undefined;
expect(obj.__owl__.rev).toBe(5);
});
test("can change values in array", () => {
const observer = new Observer();
const obj: any = { arr: [1, 2] };
observer.observe(obj);
expect(obj.arr.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
obj.arr[0] = "nope";
expect(obj.arr.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
observer.set(obj.arr, 0, "yep");
expect(obj.arr.__owl__.rev).toBe(2);
expect(observer.rev).toBe(2);
});
test("various object property changes", () => {
const observer = new Observer();
const obj: any = { a: 1 };
observer.observe(obj);
expect(obj.__owl__.rev).toBe(1);
obj.a = 2;
expect(observer.rev).toBe(2);
expect(obj.__owl__.rev).toBe(2);
// same value again
obj.a = 2;
expect(observer.rev).toBe(2);
expect(obj.__owl__.rev).toBe(2);
obj.a = 3;
expect(observer.rev).toBe(3);
expect(obj.__owl__.rev).toBe(3);
});
test("properly observe arrays", () => {
const observer = new Observer();
const arr: any = [];
observer.observe(arr);
expect(arr.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
expect(arr.length).toBe(0);
arr.push(1);
expect(arr.__owl__.rev).toBe(2);
expect(observer.rev).toBe(2);
expect(arr.length).toBe(1);
arr.splice(1, 0, "hey");
expect(arr.__owl__.rev).toBe(3);
expect(observer.rev).toBe(3);
expect(arr.length).toBe(2);
arr.unshift("lindemans");
expect(arr.__owl__.rev).toBe(4);
arr.reverse();
expect(arr.__owl__.rev).toBe(5);
arr.pop();
expect(arr.__owl__.rev).toBe(6);
arr.shift();
expect(arr.__owl__.rev).toBe(7);
arr.sort();
expect(arr.__owl__.rev).toBe(8);
expect(arr).toEqual([1]);
});
test("object pushed into arrays are observed", () => {
const observer = new Observer();
const arr: any = [];
observer.observe(arr);
expect(observer.rev).toBe(1);
arr.push({ kriek: 5 });
expect(observer.rev).toBe(2);
expect(arr.__owl__.rev).toBe(2);
expect(arr[0].__owl__.rev).toBe(2);
arr[0].kriek = 6;
expect(observer.rev).toBe(3);
expect(arr.__owl__.rev).toBe(2);
expect(arr[0].__owl__.rev).toBe(3);
});
test("set new property on observed object", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const state: any = { a: 1 };
observer.observe(state);
expect(state.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
expect(observer.notifyCB).toBeCalledTimes(0);
Observer.set(state, "b", 8);
await nextMicroTick();
expect(state.__owl__.rev).toBe(2);
expect(observer.rev).toBe(2);
expect(observer.notifyCB).toBeCalledTimes(1);
expect(state.b).toBe(8);
});
test("delete property from observed object", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const state: any = { a: 1, b: 8 };
observer.observe(state);
expect(state.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
expect(observer.notifyCB).toBeCalledTimes(0);
Observer.delete(state, "b");
await nextMicroTick();
expect(state.__owl__.rev).toBe(2);
expect(observer.rev).toBe(2);
expect(observer.notifyCB).toBeCalledTimes(1);
expect(state).toEqual({ a: 1 });
});
test("set element in observed array", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const state: any = ["a"];
observer.observe(state);
expect(state.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
expect(observer.notifyCB).toBeCalledTimes(0);
Observer.set(state, 1, "b");
await nextMicroTick();
expect(state.__owl__.rev).toBe(2);
expect(observer.rev).toBe(2);
expect(observer.notifyCB).toBeCalledTimes(1);
expect(state).toEqual(["a", "b"]);
});
test("properly observe arrays in object", () => {
const observer = new Observer();
const state: any = { arr: [] };
observer.observe(state);
expect(state.arr.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
expect(state.arr.length).toBe(0);
state.arr.push(1);
expect(state.arr.__owl__.rev).toBe(2);
expect(observer.rev).toBe(2);
expect(state.arr.length).toBe(1);
});
test("properly observe objects in array", () => {
const observer = new Observer();
const state: any = { arr: [{ something: 1 }] };
observer.observe(state);
expect(state.arr.__owl__.rev).toBe(1);
expect(state.arr[0].__owl__.rev).toBe(1);
state.arr[0].something = 2;
expect(state.arr.__owl__.rev).toBe(1);
expect(state.arr[0].__owl__.rev).toBe(2);
});
test("properly observe objects in object", () => {
const observer = new Observer();
const state: any = { a: { b: 1 } };
observer.observe(state);
expect(state.__owl__.rev).toBe(1);
expect(state.a.__owl__.rev).toBe(1);
state.a.b = 2;
expect(state.__owl__.rev).toBe(1);
expect(state.a.__owl__.rev).toBe(2);
});
test("reobserve new object values", () => {
const observer = new Observer();
const obj: any = { a: 1 };
observer.observe(obj);
expect(obj.__owl__.rev).toBe(1);
obj.a = { b: 2 };
expect(observer.rev).toBe(2);
expect(obj.__owl__.rev).toBe(2);
// we start at 2 because it is the new observer rev number
expect(obj.a.__owl__.rev).toBe(2);
obj.a.b = 3;
expect(observer.rev).toBe(3);
expect(obj.__owl__.rev).toBe(2);
expect(obj.a.__owl__.rev).toBe(3);
});
test("deep observe misc changes", () => {
const observer = new Observer();
const state: any = { o: { a: 1 }, arr: [1], n: 13 };
observer.observe(state);
expect(state.__owl__.rev).toBe(1);
expect(state.__owl__.deepRev).toBe(1);
state.o.a = 2;
expect(observer.rev).toBe(2);
expect(state.__owl__.rev).toBe(1);
expect(state.__owl__.deepRev).toBe(2);
state.arr.push(2);
expect(state.__owl__.rev).toBe(1);
expect(state.__owl__.deepRev).toBe(3);
state.n = 155;
expect(state.__owl__.rev).toBe(2);
expect(state.__owl__.deepRev).toBe(4);
});
test("properly handle already observed state", () => {
const observer = new Observer();
const obj1: any = { a: 1 };
const obj2: any = { b: 1 };
observer.observe(obj1);
observer.observe(obj2);
expect(obj1.__owl__.rev).toBe(1);
expect(obj2.__owl__.rev).toBe(1);
obj1.a = 2;
obj2.b = 3;
expect(obj1.__owl__.rev).toBe(2);
expect(obj2.__owl__.rev).toBe(2);
obj2.b = obj1;
expect(obj1.__owl__.rev).toBe(2);
expect(obj2.__owl__.rev).toBe(3);
});
test("can set a property more than once", () => {
const observer = new Observer();
const obj: any = {};
observer.observe(obj);
expect(obj.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
expect(obj.__owl__.deepRev).toBe(1);
observer.set(obj, "aku", "always finds annoying problems");
expect(observer.rev).toBe(2);
expect(obj.__owl__.rev).toBe(2);
expect(obj.__owl__.deepRev).toBe(2);
observer.set(obj, "aku", "always finds good problems");
expect(observer.rev).toBe(3);
expect(obj.__owl__.rev).toBe(3);
expect(obj.__owl__.deepRev).toBe(3);
});
test("properly handle swapping elements", () => {
const observer = new Observer();
const obj: any = { a: { arr: [] }, b: 1 };
observer.observe(obj);
// swap a and b
const b = obj.b;
obj.b = obj.a;
obj.a = b;
expect(observer.rev).toBe(3);
// push something into array to make sure it works
obj.b.arr.push("blanche");
expect(observer.rev).toBe(4);
});
test("properly handle assigning observed obj containing array", () => {
const observer = new Observer();
const obj: any = { a: { arr: [], val: "test" } };
observer.observe(obj);
obj.a = { ...obj.a, val: "test2" };
expect(observer.rev).toBe(2);
// push something into array to make sure it works
obj.a.arr.push("blanche");
expect(observer.rev).toBe(3);
});
test("accept cycles in observed state", () => {
const observer = new Observer();
const obj1: any = {};
const obj2: any = { b: obj1, key: 1 };
obj1.a = obj2;
observer.observe(obj1);
expect(obj1.__owl__.rev).toBe(1);
expect(obj2.__owl__.rev).toBe(1);
obj2.key = 3;
expect(obj1.__owl__.rev).toBe(1);
expect(obj2.__owl__.rev).toBe(2);
});
test("call callback when state is changed", async () => {
const observer = new Observer();
observer.notifyCB = jest.fn();
const obj: any = { a: 1, b: { c: 2 }, d: [{ e: 3 }], f: 4 };
observer.observe(obj);
expect(observer.notifyCB).toBeCalledTimes(0);
obj.a = 2;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(1);
obj.b.c = 3;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(2);
obj.d[0].e = 5;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(3);
obj.a = 111;
obj.f = 222;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(4);
});
test("throw error when state is mutated in object if allowMutation=false", async () => {
const observer = new Observer();
observer.allowMutations = false;
const obj: any = { a: 1 };
observer.observe(obj);
expect(() => {
obj.a = 2;
}).toThrow('Observed state cannot be changed here! (key: "a", val: "2")');
});
test("throw error when state is mutated in array if allowMutation=false", async () => {
const observer = new Observer();
observer.allowMutations = false;
const obj: any = { a: [1] };
observer.observe(obj);
expect(() => {
obj.a.push(2);
}).toThrow("Array cannot be changed here");
});
});
@@ -3,7 +3,7 @@
exports[`attributes class and t-attf-class with ternary operation 1`] = ` exports[`attributes class and t-attf-class with ternary operation 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = 'hello ' + (context['value']?'world':''); var _1 = 'hello ' + (context['value']?'world':'');
let c2 = [], p2 = {key:2,attrs:{class: _1}}; let c2 = [], p2 = {key:2,attrs:{class: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
@@ -14,7 +14,7 @@ exports[`attributes class and t-attf-class with ternary operation 1`] = `
exports[`attributes dynamic attribute falsy variable 1`] = ` exports[`attributes dynamic attribute falsy variable 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = context['value']; var _1 = context['value'];
let c2 = [], p2 = {key:2,attrs:{foo: _1}}; let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
@@ -25,7 +25,7 @@ exports[`attributes dynamic attribute falsy variable 1`] = `
exports[`attributes dynamic attribute with a dash 1`] = ` exports[`attributes dynamic attribute with a dash 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = context['id']; var _1 = context['id'];
let c2 = [], p2 = {key:2,attrs:{\\"data-action-id\\": _1}}; let c2 = [], p2 = {key:2,attrs:{\\"data-action-id\\": _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
@@ -36,7 +36,7 @@ exports[`attributes dynamic attribute with a dash 1`] = `
exports[`attributes dynamic attributes 1`] = ` exports[`attributes dynamic attributes 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = 'bar'; var _1 = 'bar';
let c2 = [], p2 = {key:2,attrs:{foo: _1}}; let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
@@ -47,7 +47,7 @@ exports[`attributes dynamic attributes 1`] = `
exports[`attributes dynamic formatted attributes with a dash 1`] = ` exports[`attributes dynamic formatted attributes with a dash 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = \`Some text \${context['id']}\`; var _1 = \`Some text \${context['id']}\`;
let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}}; let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
@@ -58,7 +58,7 @@ exports[`attributes dynamic formatted attributes with a dash 1`] = `
exports[`attributes fixed variable 1`] = ` exports[`attributes fixed variable 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = context['value']; var _1 = context['value'];
let c2 = [], p2 = {key:2,attrs:{foo: _1}}; let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
@@ -69,7 +69,7 @@ exports[`attributes fixed variable 1`] = `
exports[`attributes format expression 1`] = ` exports[`attributes format expression 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = (context['value']+37); var _1 = (context['value']+37);
let c2 = [], p2 = {key:2,attrs:{foo: _1}}; let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
@@ -80,7 +80,7 @@ exports[`attributes format expression 1`] = `
exports[`attributes format expression, other format 1`] = ` exports[`attributes format expression, other format 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = (context['value']+37); var _1 = (context['value']+37);
let c2 = [], p2 = {key:2,attrs:{foo: _1}}; let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
@@ -91,7 +91,7 @@ exports[`attributes format expression, other format 1`] = `
exports[`attributes format literal 1`] = ` exports[`attributes format literal 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = \`bar\`; var _1 = \`bar\`;
let c2 = [], p2 = {key:2,attrs:{foo: _1}}; let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
@@ -102,7 +102,7 @@ exports[`attributes format literal 1`] = `
exports[`attributes format multiple 1`] = ` exports[`attributes format multiple 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = \`a \${context['value1']} is \${context['value2']} of \${context['value3']} ]\`; var _1 = \`a \${context['value1']} is \${context['value2']} of \${context['value3']} ]\`;
let c2 = [], p2 = {key:2,attrs:{foo: _1}}; let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
@@ -113,7 +113,7 @@ exports[`attributes format multiple 1`] = `
exports[`attributes format value 1`] = ` exports[`attributes format value 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = \`b\${context['value']}r\`; var _1 = \`b\${context['value']}r\`;
let c2 = [], p2 = {key:2,attrs:{foo: _1}}; let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
@@ -124,11 +124,12 @@ exports[`attributes format value 1`] = `
exports[`attributes from object variables set previously 1`] = ` exports[`attributes from object variables set previously 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; let utils = this.constructor.utils;
var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = {a:'b'}; var _2 = {a:'b'};
let _3 = this.utils.toObj(_2.a); let _3 = utils.toObj(_2.a);
let c4 = [], p4 = {key:4,class:_3}; let c4 = [], p4 = {key:4,class:_3};
var vn4 = h('span', p4, c4); var vn4 = h('span', p4, c4);
c1.push(vn4); c1.push(vn4);
@@ -139,11 +140,12 @@ exports[`attributes from object variables set previously 1`] = `
exports[`attributes from variables set previously 1`] = ` exports[`attributes from variables set previously 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; let utils = this.constructor.utils;
var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = 'def'; var _2 = 'def';
let _3 = this.utils.toObj(_2); let _3 = utils.toObj(_2);
let c4 = [], p4 = {key:4,class:_3}; let c4 = [], p4 = {key:4,class:_3};
var vn4 = h('span', p4, c4); var vn4 = h('span', p4, c4);
c1.push(vn4); c1.push(vn4);
@@ -154,7 +156,7 @@ exports[`attributes from variables set previously 1`] = `
exports[`attributes object 1`] = ` exports[`attributes object 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = context['value']; var _1 = context['value'];
let c2 = [], p2 = {key:2,attrs:{}}; let c2 = [], p2 = {key:2,attrs:{}};
if (_1 instanceof Array) { if (_1 instanceof Array) {
@@ -172,7 +174,7 @@ exports[`attributes object 1`] = `
exports[`attributes static attributes 1`] = ` exports[`attributes static attributes 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = 'a'; var _1 = 'a';
var _2 = 'b'; var _2 = 'b';
var _3 = 'c'; var _3 = 'c';
@@ -185,7 +187,7 @@ exports[`attributes static attributes 1`] = `
exports[`attributes static attributes on void elements 1`] = ` exports[`attributes static attributes on void elements 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = '/test.jpg'; var _1 = '/test.jpg';
var _2 = 'Test'; var _2 = 'Test';
let c3 = [], p3 = {key:3,attrs:{src: _1,alt: _2}}; let c3 = [], p3 = {key:3,attrs:{src: _1,alt: _2}};
@@ -197,7 +199,7 @@ exports[`attributes static attributes on void elements 1`] = `
exports[`attributes static attributes with dashes 1`] = ` exports[`attributes static attributes with dashes 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = 'Close'; var _1 = 'Close';
let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}}; let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
@@ -208,9 +210,10 @@ exports[`attributes static attributes with dashes 1`] = `
exports[`attributes t-att-class and class should combine together 1`] = ` exports[`attributes t-att-class and class should combine together 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; let utils = this.constructor.utils;
var h = this.h;
let _2 = {'hello':true}; let _2 = {'hello':true};
Object.assign(_2, this.utils.toObj(context['value'])) Object.assign(_2, utils.toObj(context['value']))
let c3 = [], p3 = {key:3,class:_2}; let c3 = [], p3 = {key:3,class:_2};
var vn3 = h('div', p3, c3); var vn3 = h('div', p3, c3);
return vn3; return vn3;
@@ -220,9 +223,10 @@ exports[`attributes t-att-class and class should combine together 1`] = `
exports[`attributes t-att-class with object 1`] = ` exports[`attributes t-att-class with object 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; let utils = this.constructor.utils;
var h = this.h;
let _2 = {'static':true}; let _2 = {'static':true};
Object.assign(_2, this.utils.toObj({a:context['b'],c:context['d'],e:context['f']})) Object.assign(_2, utils.toObj({a:context['b'],c:context['d'],e:context['f']}))
let c3 = [], p3 = {key:3,class:_2}; let c3 = [], p3 = {key:3,class:_2};
var vn3 = h('div', p3, c3); var vn3 = h('div', p3, c3);
return vn3; return vn3;
@@ -232,7 +236,7 @@ exports[`attributes t-att-class with object 1`] = `
exports[`attributes t-attf-class should combine with class 1`] = ` exports[`attributes t-attf-class should combine with class 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = 'hello ' + \`world\`; var _1 = 'hello ' + \`world\`;
let c2 = [], p2 = {key:2,attrs:{class: _1}}; let c2 = [], p2 = {key:2,attrs:{class: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
@@ -243,7 +247,7 @@ exports[`attributes t-attf-class should combine with class 1`] = `
exports[`attributes tuple literal 1`] = ` exports[`attributes tuple literal 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = ['foo','bar']; var _1 = ['foo','bar'];
let c2 = [], p2 = {key:2,attrs:{}}; let c2 = [], p2 = {key:2,attrs:{}};
if (_1 instanceof Array) { if (_1 instanceof Array) {
@@ -261,7 +265,7 @@ exports[`attributes tuple literal 1`] = `
exports[`attributes tuple variable 1`] = ` exports[`attributes tuple variable 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = context['value']; var _1 = context['value'];
let c2 = [], p2 = {key:2,attrs:{}}; let c2 = [], p2 = {key:2,attrs:{}};
if (_1 instanceof Array) { if (_1 instanceof Array) {
@@ -279,7 +283,7 @@ exports[`attributes tuple variable 1`] = `
exports[`debugging t-debug 1`] = ` exports[`debugging t-debug 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
debugger; debugger;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
@@ -297,7 +301,7 @@ exports[`debugging t-debug 1`] = `
exports[`debugging t-log 1`] = ` exports[`debugging t-log 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = 42; var _2 = 42;
@@ -310,7 +314,7 @@ exports[`foreach does not pollute the rendering context 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
context = Object.create(context); context = Object.create(context);
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = [1]; var _2 = [1];
@@ -340,7 +344,7 @@ exports[`foreach iterate on items (on a element node) 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
context = Object.create(context); context = Object.create(context);
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = [1,2]; var _2 = [1,2];
@@ -373,7 +377,7 @@ exports[`foreach iterate on items 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
context = Object.create(context); context = Object.create(context);
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = [3,2,1]; var _2 = [3,2,1];
@@ -415,7 +419,7 @@ exports[`foreach iterate, dict param 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
context = Object.create(context); context = Object.create(context);
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = context['value']; var _2 = context['value'];
@@ -457,7 +461,7 @@ exports[`foreach iterate, position 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
context = Object.create(context); context = Object.create(context);
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = Array(5); var _2 = Array(5);
@@ -496,7 +500,7 @@ exports[`foreach warn if no key in some case 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
context = Object.create(context); context = Object.create(context);
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = [1,2]; var _2 = [1,2];
@@ -528,7 +532,7 @@ exports[`foreach warn if no key in some case 1`] = `
exports[`loading templates can initialize qweb with a string 1`] = ` exports[`loading templates can initialize qweb with a string 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
c1.push({text: \`jupiler\`}); c1.push({text: \`jupiler\`});
@@ -539,7 +543,7 @@ exports[`loading templates can initialize qweb with a string 1`] = `
exports[`loading templates can load a few templates from a xml string 1`] = ` exports[`loading templates can load a few templates from a xml string 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('ul', p1, c1); var vn1 = h('ul', p1, c1);
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
@@ -557,8 +561,9 @@ exports[`loading templates can load a few templates from a xml string 1`] = `
exports[`misc global 1`] = ` exports[`misc global 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let utils = this.constructor.utils;
context = Object.create(context); context = Object.create(context);
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = [4,5,6]; var _2 = [4,5,6];
@@ -624,7 +629,7 @@ exports[`misc global 1`] = `
c1.push(vn21); c1.push(vn21);
var _22 = context['toto']; var _22 = context['toto'];
if (_22 || _22 === 0) { if (_22 || _22 === 0) {
var frag23 = this.utils.getFragment(_22) var frag23 = utils.getFragment(_22)
var p24 = {hook: { var p24 = {hook: {
insert: n => n.elm.parentNode.replaceChild(frag23, n.elm), insert: n => n.elm.parentNode.replaceChild(frag23, n.elm),
}}; }};
@@ -640,7 +645,7 @@ exports[`misc global 1`] = `
exports[`special cases for some boolean html attributes/properties input type= checkbox, with t-att-checked 1`] = ` exports[`special cases for some boolean html attributes/properties input type= checkbox, with t-att-checked 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = 'checkbox'; var _1 = 'checkbox';
var _2 = context['flag']; var _2 = context['flag'];
let c3 = [], p3 = {key:3,attrs:{type: _1,checked: _2},props:{checked: _2}}; let c3 = [], p3 = {key:3,attrs:{type: _1,checked: _2},props:{checked: _2}};
@@ -652,7 +657,7 @@ exports[`special cases for some boolean html attributes/properties input type= c
exports[`special cases for some boolean html attributes/properties various boolean html attributes 1`] = ` exports[`special cases for some boolean html attributes/properties various boolean html attributes 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = 'checkbox'; var _2 = 'checkbox';
@@ -692,7 +697,7 @@ exports[`special cases for some boolean html attributes/properties various boole
exports[`static templates div with a span child node 1`] = ` exports[`static templates div with a span child node 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
@@ -706,7 +711,7 @@ exports[`static templates div with a span child node 1`] = `
exports[`static templates div with a text node 1`] = ` exports[`static templates div with a text node 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
c1.push({text: \`word\`}); c1.push({text: \`word\`});
@@ -717,7 +722,7 @@ exports[`static templates div with a text node 1`] = `
exports[`static templates empty div 1`] = ` exports[`static templates empty div 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
return vn1; return vn1;
@@ -727,7 +732,7 @@ exports[`static templates empty div 1`] = `
exports[`static templates simple dynamic value 1`] = ` exports[`static templates simple dynamic value 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var _1 = context['text']; var _1 = context['text'];
if (_1 || _1 === 0) { if (_1 || _1 === 0) {
var vn2 = {text: _1}; var vn2 = {text: _1};
@@ -739,7 +744,7 @@ exports[`static templates simple dynamic value 1`] = `
exports[`static templates simple string 1`] = ` exports[`static templates simple string 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var vn1 = {text: \`hello vdom\`}; var vn1 = {text: \`hello vdom\`};
return vn1; return vn1;
}" }"
@@ -748,7 +753,7 @@ exports[`static templates simple string 1`] = `
exports[`static templates simple string, with some dynamic value 1`] = ` exports[`static templates simple string, with some dynamic value 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var vn1 = {text: \`hello \`}; var vn1 = {text: \`hello \`};
var _2 = context['text']; var _2 = context['text'];
if (_2 || _2 === 0) { if (_2 || _2 === 0) {
@@ -761,7 +766,7 @@ exports[`static templates simple string, with some dynamic value 1`] = `
exports[`t-call (template calling basic caller 1`] = ` exports[`t-call (template calling basic caller 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
c1.push({text: \`ok\`}); c1.push({text: \`ok\`});
@@ -772,7 +777,7 @@ exports[`t-call (template calling basic caller 1`] = `
exports[`t-call (template calling inherit context 1`] = ` exports[`t-call (template calling inherit context 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = 1; var _2 = 1;
@@ -786,7 +791,7 @@ exports[`t-call (template calling inherit context 1`] = `
exports[`t-call (template calling scoped parameters 1`] = ` exports[`t-call (template calling scoped parameters 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
{ {
@@ -801,10 +806,26 @@ exports[`t-call (template calling scoped parameters 1`] = `
}" }"
`; `;
exports[`t-call (template calling t-call with t-if 1`] = `
"function anonymous(context,extra
) {
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['flag']) {
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
c2.push({text: \`ok\`});
}
return vn1;
}"
`;
exports[`t-call (template calling with unused body 1`] = ` exports[`t-call (template calling with unused body 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
c2.push({text: \`ok\`}); c2.push({text: \`ok\`});
@@ -815,7 +836,7 @@ exports[`t-call (template calling with unused body 1`] = `
exports[`t-call (template calling with unused setbody 1`] = ` exports[`t-call (template calling with unused setbody 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
{ {
let _1 = 3; let _1 = 3;
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
@@ -829,7 +850,7 @@ exports[`t-call (template calling with unused setbody 1`] = `
exports[`t-call (template calling with used body 1`] = ` exports[`t-call (template calling with used body 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
var vn2 = h('h1', p2, c2); var vn2 = h('h1', p2, c2);
c2.push({text: \`ok\`}); c2.push({text: \`ok\`});
@@ -840,7 +861,7 @@ exports[`t-call (template calling with used body 1`] = `
exports[`t-call (template calling with used set body 1`] = ` exports[`t-call (template calling with used set body 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
{ {
@@ -856,7 +877,7 @@ exports[`t-call (template calling with used set body 1`] = `
exports[`t-esc escaping on a node 1`] = ` exports[`t-esc escaping on a node 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
var _2 = 'ok'; var _2 = 'ok';
@@ -870,7 +891,7 @@ exports[`t-esc escaping on a node 1`] = `
exports[`t-esc escaping on a node with a body 1`] = ` exports[`t-esc escaping on a node with a body 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
var _2 = 'ok'; var _2 = 'ok';
@@ -886,7 +907,7 @@ exports[`t-esc escaping on a node with a body 1`] = `
exports[`t-esc escaping on a node with a body, as a default 1`] = ` exports[`t-esc escaping on a node with a body, as a default 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
var _2 = context['var']; var _2 = context['var'];
@@ -902,7 +923,7 @@ exports[`t-esc escaping on a node with a body, as a default 1`] = `
exports[`t-esc literal 1`] = ` exports[`t-esc literal 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
var _2 = 'ok'; var _2 = 'ok';
@@ -916,7 +937,7 @@ exports[`t-esc literal 1`] = `
exports[`t-esc variable 1`] = ` exports[`t-esc variable 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
var _2 = context['var']; var _2 = context['var'];
@@ -930,7 +951,7 @@ exports[`t-esc variable 1`] = `
exports[`t-if boolean value condition elif 1`] = ` exports[`t-if boolean value condition elif 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
if (context['color']=='black') { if (context['color']=='black') {
@@ -952,7 +973,7 @@ exports[`t-if boolean value condition elif 1`] = `
exports[`t-if boolean value condition else 1`] = ` exports[`t-if boolean value condition else 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
@@ -976,7 +997,7 @@ exports[`t-if boolean value condition else 1`] = `
exports[`t-if boolean value condition false else 1`] = ` exports[`t-if boolean value condition false else 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
@@ -1000,7 +1021,7 @@ exports[`t-if boolean value condition false else 1`] = `
exports[`t-if boolean value condition missing 1`] = ` exports[`t-if boolean value condition missing 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
if (context['condition']) { if (context['condition']) {
@@ -1013,7 +1034,7 @@ exports[`t-if boolean value condition missing 1`] = `
exports[`t-if boolean value false condition 1`] = ` exports[`t-if boolean value false condition 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
if (context['condition']) { if (context['condition']) {
@@ -1026,7 +1047,7 @@ exports[`t-if boolean value false condition 1`] = `
exports[`t-if boolean value true condition 1`] = ` exports[`t-if boolean value true condition 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
if (context['condition']) { if (context['condition']) {
@@ -1039,7 +1060,7 @@ exports[`t-if boolean value true condition 1`] = `
exports[`t-if can use some boolean operators in expressions 1`] = ` exports[`t-if can use some boolean operators in expressions 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
if (context['cond1']&&context['cond2']) { if (context['cond1']&&context['cond2']) {
@@ -1073,7 +1094,7 @@ exports[`t-if can use some boolean operators in expressions 1`] = `
exports[`t-key can use t-key directive on a node 1`] = ` exports[`t-key can use t-key directive on a node 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:context['beer'].id}; let c1 = [], p1 = {key:context['beer'].id};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = context['beer'].name; var _2 = context['beer'].name;
@@ -1088,7 +1109,7 @@ exports[`t-key t-key directive in a list 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
context = Object.create(context); context = Object.create(context);
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('ul', p1, c1); var vn1 = h('ul', p1, c1);
var _2 = context['beers']; var _2 = context['beers'];
@@ -1123,7 +1144,7 @@ exports[`t-on can bind event handler 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let owner = context; let owner = context;
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('button', p1, c1); var vn1 = h('button', p1, c1);
if (!context['add']) { if (!context['add']) {
@@ -1140,7 +1161,7 @@ exports[`t-on can bind handlers with arguments 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let owner = context; let owner = context;
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('button', p1, c1); var vn1 = h('button', p1, c1);
if (!context['add']) { if (!context['add']) {
@@ -1156,7 +1177,7 @@ exports[`t-on can bind handlers with empty object (with non empty inner string)
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let owner = context; let owner = context;
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('button', p1, c1); var vn1 = h('button', p1, c1);
if (!context['doSomething']) { if (!context['doSomething']) {
@@ -1172,7 +1193,7 @@ exports[`t-on can bind handlers with empty object 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let owner = context; let owner = context;
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('button', p1, c1); var vn1 = h('button', p1, c1);
if (!context['doSomething']) { if (!context['doSomething']) {
@@ -1189,7 +1210,7 @@ exports[`t-on can bind handlers with loop variable as argument 1`] = `
) { ) {
let owner = context; let owner = context;
context = Object.create(context); context = Object.create(context);
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('ul', p1, c1); var vn1 = h('ul', p1, c1);
var _2 = ['someval']; var _2 = ['someval'];
@@ -1226,7 +1247,7 @@ exports[`t-on can bind handlers with object arguments 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let owner = context; let owner = context;
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('button', p1, c1); var vn1 = h('button', p1, c1);
if (!context['add']) { if (!context['add']) {
@@ -1242,7 +1263,7 @@ exports[`t-on can bind two event handlers 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let owner = context; let owner = context;
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('button', p1, c1); var vn1 = h('button', p1, c1);
if (!context['handleClick']) { if (!context['handleClick']) {
@@ -1264,7 +1285,7 @@ exports[`t-on handler is bound to proper owner 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let owner = context; let owner = context;
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('button', p1, c1); var vn1 = h('button', p1, c1);
if (!context['add']) { if (!context['add']) {
@@ -1281,7 +1302,7 @@ exports[`t-on t-on with prevent and self modifiers (order matters) 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let owner = context; let owner = context;
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}}; let c2 = [], p2 = {key:2,on:{}};
@@ -1304,7 +1325,7 @@ exports[`t-on t-on with prevent and/or stop modifiers 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let owner = context; let owner = context;
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}}; let c2 = [], p2 = {key:2,on:{}};
@@ -1342,7 +1363,7 @@ exports[`t-on t-on with self and prevent modifiers (order matters) 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let owner = context; let owner = context;
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}}; let c2 = [], p2 = {key:2,on:{}};
@@ -1365,7 +1386,7 @@ exports[`t-on t-on with self modifier 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let owner = context; let owner = context;
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}}; let c2 = [], p2 = {key:2,on:{}};
@@ -1399,12 +1420,13 @@ exports[`t-on t-on with self modifier 1`] = `
exports[`t-raw literal 1`] = ` exports[`t-raw literal 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; let utils = this.constructor.utils;
var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
var _2 = 'ok'; var _2 = 'ok';
if (_2 || _2 === 0) { if (_2 || _2 === 0) {
var frag3 = this.utils.getFragment(_2) var frag3 = utils.getFragment(_2)
var p4 = {hook: { var p4 = {hook: {
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm), insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
}}; }};
@@ -1418,12 +1440,13 @@ exports[`t-raw literal 1`] = `
exports[`t-raw not escaping 1`] = ` exports[`t-raw not escaping 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; let utils = this.constructor.utils;
var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = context['var']; var _2 = context['var'];
if (_2 || _2 === 0) { if (_2 || _2 === 0) {
var frag3 = this.utils.getFragment(_2) var frag3 = utils.getFragment(_2)
var p4 = {hook: { var p4 = {hook: {
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm), insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
}}; }};
@@ -1437,7 +1460,8 @@ exports[`t-raw not escaping 1`] = `
exports[`t-raw t-raw and another sibling node 1`] = ` exports[`t-raw t-raw and another sibling node 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; let utils = this.constructor.utils;
var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
@@ -1446,7 +1470,7 @@ exports[`t-raw t-raw and another sibling node 1`] = `
c2.push({text: \`hello\`}); c2.push({text: \`hello\`});
var _3 = context['var']; var _3 = context['var'];
if (_3 || _3 === 0) { if (_3 || _3 === 0) {
var frag4 = this.utils.getFragment(_3) var frag4 = utils.getFragment(_3)
var p5 = {hook: { var p5 = {hook: {
insert: n => n.elm.parentNode.replaceChild(frag4, n.elm), insert: n => n.elm.parentNode.replaceChild(frag4, n.elm),
}}; }};
@@ -1460,12 +1484,13 @@ exports[`t-raw t-raw and another sibling node 1`] = `
exports[`t-raw variable 1`] = ` exports[`t-raw variable 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; let utils = this.constructor.utils;
var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
var _2 = context['var']; var _2 = context['var'];
if (_2 || _2 === 0) { if (_2 || _2 === 0) {
var frag3 = this.utils.getFragment(_2) var frag3 = utils.getFragment(_2)
var p4 = {hook: { var p4 = {hook: {
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm), insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
}}; }};
@@ -1479,7 +1504,7 @@ exports[`t-raw variable 1`] = `
exports[`t-ref can get a dynamic ref on a node 1`] = ` exports[`t-ref can get a dynamic ref on a node 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
@@ -1498,7 +1523,7 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
exports[`t-ref can get a ref on a node 1`] = ` exports[`t-ref can get a ref on a node 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
@@ -1518,7 +1543,7 @@ exports[`t-ref refs in a loop 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
context = Object.create(context); context = Object.create(context);
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = context['items']; var _2 = context['items'];
@@ -1556,7 +1581,7 @@ exports[`t-ref refs in a loop 1`] = `
exports[`t-set evaluate value expression 1`] = ` exports[`t-set evaluate value expression 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = 1+2; var _2 = 1+2;
@@ -1570,7 +1595,7 @@ exports[`t-set evaluate value expression 1`] = `
exports[`t-set evaluate value expression, part 2 1`] = ` exports[`t-set evaluate value expression, part 2 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = context['somevariable']+2; var _2 = context['somevariable']+2;
@@ -1584,7 +1609,7 @@ exports[`t-set evaluate value expression, part 2 1`] = `
exports[`t-set set from attribute literal 1`] = ` exports[`t-set set from attribute literal 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = 'ok'; var _2 = 'ok';
@@ -1598,7 +1623,7 @@ exports[`t-set set from attribute literal 1`] = `
exports[`t-set set from attribute lookup 1`] = ` exports[`t-set set from attribute lookup 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = context['value']; var _2 = context['value'];
@@ -1612,7 +1637,7 @@ exports[`t-set set from attribute lookup 1`] = `
exports[`t-set set from body literal 1`] = ` exports[`t-set set from body literal 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
var vn1 = {text: \`ok\`}; var vn1 = {text: \`ok\`};
return vn1; return vn1;
}" }"
@@ -1621,7 +1646,7 @@ exports[`t-set set from body literal 1`] = `
exports[`t-set set from body lookup 1`] = ` exports[`t-set set from body lookup 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = context['value']; var _2 = context['value'];
@@ -1635,7 +1660,7 @@ exports[`t-set set from body lookup 1`] = `
exports[`t-set set from empty body 1`] = ` exports[`t-set set from empty body 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
return vn1; return vn1;
@@ -1645,7 +1670,7 @@ exports[`t-set set from empty body 1`] = `
exports[`t-set t-set and t-if 1`] = ` exports[`t-set t-set and t-if 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = context['value']; var _2 = context['value'];
@@ -1659,7 +1684,7 @@ exports[`t-set t-set and t-if 1`] = `
exports[`t-set t-set evaluates an expression only once 1`] = ` exports[`t-set t-set evaluates an expression only once 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = context['value']+' artois'; var _2 = context['value']+' artois';
@@ -1677,7 +1702,7 @@ exports[`t-set t-set should reuse variable if possible 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
context = Object.create(context); context = Object.create(context);
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = 1; var _2 = 1;
@@ -1714,7 +1739,7 @@ exports[`t-set t-set should reuse variable if possible 1`] = `
exports[`t-set value priority 1`] = ` exports[`t-set value priority 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = 1; var _2 = 1;
@@ -1728,7 +1753,7 @@ exports[`t-set value priority 1`] = `
exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = ` exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
c1.push({text: \` abc \`}); c1.push({text: \` abc \`});
@@ -1739,7 +1764,7 @@ exports[`whitespace handling consecutives whitespaces are condensed into a singl
exports[`whitespace handling nothing is done in pre tags 1`] = ` exports[`whitespace handling nothing is done in pre tags 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('pre', p1, c1); var vn1 = h('pre', p1, c1);
c1.push({text: \` \`}); c1.push({text: \` \`});
@@ -1750,7 +1775,7 @@ exports[`whitespace handling nothing is done in pre tags 1`] = `
exports[`whitespace handling nothing is done in pre tags 2`] = ` exports[`whitespace handling nothing is done in pre tags 2`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('pre', p1, c1); var vn1 = h('pre', p1, c1);
c1.push({text: \` c1.push({text: \`
@@ -1763,7 +1788,7 @@ exports[`whitespace handling nothing is done in pre tags 2`] = `
exports[`whitespace handling nothing is done in pre tags 3`] = ` exports[`whitespace handling nothing is done in pre tags 3`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('pre', p1, c1); var vn1 = h('pre', p1, c1);
c1.push({text: \` c1.push({text: \`
@@ -1776,7 +1801,7 @@ exports[`whitespace handling nothing is done in pre tags 3`] = `
exports[`whitespace handling white space only text nodes are condensed into a single space 1`] = ` exports[`whitespace handling white space only text nodes are condensed into a single space 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
c1.push({text: \` \`}); c1.push({text: \` \`});
@@ -1787,7 +1812,7 @@ exports[`whitespace handling white space only text nodes are condensed into a si
exports[`whitespace handling whitespace only text nodes with newlines are removed 1`] = ` exports[`whitespace handling whitespace only text nodes with newlines are removed 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
+21 -2
View File
@@ -1,5 +1,5 @@
import { QWeb } from "../src/qweb_core"; import { QWeb } from "../../src/qweb/index";
import { normalize, renderToDOM, renderToString, trim } from "./helpers"; import { normalize, renderToDOM, renderToString, trim, nextTick } from "../helpers";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Setup and helpers // Setup and helpers
@@ -501,6 +501,13 @@ describe("t-call (template calling", () => {
expect(renderToString(qweb, "caller")).toBe(expected); expect(renderToString(qweb, "caller")).toBe(expected);
}); });
test("t-call with t-if", () => {
qweb.addTemplate("sub", "<span>ok</span>");
qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>');
const expected = "<div><span>ok</span></div>";
expect(renderToString(qweb, "caller", { flag: true })).toBe(expected);
});
test("t-call not allowed on a non t node", () => { test("t-call not allowed on a non t node", () => {
qweb.addTemplate("_basic-callee", "<t>ok</t>"); qweb.addTemplate("_basic-callee", "<t>ok</t>");
qweb.addTemplate("caller", '<div t-call="_basic-callee"/>'); qweb.addTemplate("caller", '<div t-call="_basic-callee"/>');
@@ -1171,3 +1178,15 @@ describe("debugging", () => {
console.log = consoleLog; console.log = consoleLog;
}); });
}); });
describe("update on event bus", () => {
test("two consecutive forceUpdates only causes one listener update", async () => {
const fn = jest.fn();
qweb.on("update", null, fn);
qweb.forceUpdate();
qweb.forceUpdate();
expect(fn).toBeCalledTimes(0);
await nextTick();
expect(fn).toBeCalledTimes(1);
});
});
@@ -1,4 +1,4 @@
import { compileExpr, tokenize } from "../src/qweb_expressions"; import { compileExpr, tokenize } from "../../src/qweb/expression_parser";
describe("tokenizer", () => { describe("tokenizer", () => {
test("simple tokens", () => { test("simple tokens", () => {
+84
View File
@@ -0,0 +1,84 @@
import { Component } from "../../src/component/component";
import { Link, LINK_TEMPLATE_NAME } from "../../src/router/Link";
import { RouterEnv } from "../../src/router/Router";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
import { TestRouter } from "./TestRouter";
describe("Link component", () => {
let fixture: HTMLElement;
let env: RouterEnv;
let router: TestRouter | null = null;
beforeEach(() => {
fixture = makeTestFixture();
env = <RouterEnv>makeTestEnv();
});
afterEach(() => {
fixture.remove();
if (router) {
router.destroy();
}
router = null;
});
test("can render simple cases", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<Link to="'about'">About</Link>
</div>
</templates>
`);
class App extends Component<any, any, any> {
components = { Link: Link };
}
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
router = new TestRouter(env, routes, { mode: "history" });
router.navigate({ to: "users" });
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe('<div><a href="/about">About</a></div>');
expect(window.location.pathname).toBe("/users");
fixture.querySelector("a")!.click();
await nextTick();
expect(window.location.pathname).toBe("/about");
expect(fixture.innerHTML).toBe(
'<div><a href="/about" class="router-link-active">About</a></div>'
);
expect(env.qweb.templates[LINK_TEMPLATE_NAME].fn.toString()).toMatchSnapshot();
});
test("do not redirect if right clicking", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<Link to="'about'">About</Link>
</div>
</templates>
`);
class App extends Component<any, any, any> {
components = { Link: Link };
}
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
router = new TestRouter(env, routes, { mode: "history" });
router.navigate({ to: "users" });
const app = new App(env);
await app.mount(fixture);
expect(window.location.pathname).toBe("/users");
var evt = new MouseEvent("click", {
button: 1
});
fixture.querySelector("a")!.dispatchEvent(evt);
await nextTick();
expect(window.location.pathname).toBe("/users");
});
});
+17
View File
@@ -0,0 +1,17 @@
import { Router } from "../../src/router/Router";
import { QWeb } from "../../src/qweb/index";
export class TestRouter extends Router {
destroy() {
window.removeEventListener("popstate", (this as any)._listener);
delete QWeb.DIRECTIVE_NAMES.routecomponent;
QWeb.DIRECTIVES = QWeb.DIRECTIVES.filter(d => d.name !== "routecomponent");
// remove component defined inroutes
for (let key in QWeb.components) {
if (key.startsWith("__component__")) {
delete QWeb.components[key];
}
}
}
}
@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Link component can render simple cases 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let owner = context;
var h = this.h;
let _1 = utils.toObj({'router-link-active':context['isActive']});
var _2 = context['href'];
let c3 = [], p3 = {key:3,attrs:{href: _2},class:_1,on:{}};
var vn3 = h('a', p3, c3);
if (!context['navigate']) {
throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__owl__-router-link'\`)
}
extra.handlers['click' + 3] = extra.handlers['click' + 3] || context['navigate'].bind(owner);
p3.on['click'] = extra.handlers['click' + 3];
const slot4 = this.slots[context.__owl__.slotId + '_' + 'default'];
if (slot4) {
slot4(context.__owl__.parent, Object.assign({}, extra, {parentNode: c3, vars: extra.vars, parent: owner}));
}
return vn3;
}"
`;
@@ -0,0 +1,159 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`router directive t-routecomponent can render parameterized route 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['env'].router.currentRouteName==='book') {
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {title:context['env'].router.currentParams.title};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let componentKey4 = \`__component__book\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
}
return vn1;
}"
`;
exports[`router directive t-routecomponent can render parameterized route with suffixes 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['env'].router.currentRouteName==='book') {
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {title:context['env'].router.currentParams.title,val:context['env'].router.currentParams.val};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let componentKey4 = \`__component__book\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
}
return vn1;
}"
`;
exports[`router directive t-routecomponent can render simple cases 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['env'].router.currentRouteName==='about') {
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let componentKey4 = \`__component__about\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
}
else if (context['env'].router.currentRouteName==='users') {
//COMPONENT
let def6;
let w7 = 7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[7]] : false;
let _5_index = c1.length;
c1.push(null);
let props7 = {};
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
def6 = w7.__owl__.renderPromise;
} else {
w7.destroy();
w7 = false;
}
}
if (!w7) {
let componentKey7 = \`__component__users\`;
let W7 = context.components && context.components[componentKey7] || QWeb.components[componentKey7];
if (!W7) {throw new Error('Cannot find the definition of component \\"' + componentKey7 + '\\"')}
w7 = new W7(parent, props7);
parent.__owl__.cmap[7] = w7.__owl__.id;
def6 = w7.__prepare();
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: 7, hook: {insert(vn) {let nvn=w7.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
} else {
def6 = def6 || w7.__updateProps(props7, extra.forceUpdate, extra.patchQueue);
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c1[_5_index]=pvnode;});
}
extra.promises.push(def6);
}
return vn1;
}"
`;
+106
View File
@@ -0,0 +1,106 @@
import { Component } from "../../src/component/component";
import { RouterEnv } from "../../src/router/Router";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
import { TestRouter } from "./TestRouter";
describe("router directive t-routecomponent", () => {
let fixture: HTMLElement;
let env: RouterEnv;
let router: TestRouter | null = null;
beforeEach(() => {
fixture = makeTestFixture();
env = <RouterEnv>makeTestEnv();
});
afterEach(() => {
fixture.remove();
if (router) {
router.destroy();
}
router = null;
});
test("can render simple cases", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<t t-routecomponent="1" />
</div>
<span t-name="About">About</span>
<span t-name="Users">Users</span>
</templates>
`);
class About extends Component<any, any, any> {}
class Users extends Component<any, any, any> {}
class App extends Component<any, any, any> {
components = { About, Users };
}
const routes = [
{ name: "about", path: "/about", component: About },
{ name: "users", path: "/users", component: Users }
];
router = new TestRouter(env, routes, { mode: "history" });
await router.navigate({ to: "about" });
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>About</span></div>");
await router.navigate({ to: "users" });
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>Users</span></div>");
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
});
test("can render parameterized route", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<t t-routecomponent="1" />
</div>
<span t-name="Book">Book <t t-esc="props.title"/></span>
</templates>
`);
class Book extends Component<any, any, any> {}
class App extends Component<any, any, any> {
components = { Book };
}
const routes = [{ name: "book", path: "/book/{{title}}", component: Book }];
router = new TestRouter(env, routes, { mode: "history" });
await router.navigate({ to: "book", params: { title: "1984" } });
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984</span></div>");
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
});
test("can render parameterized route with suffixes", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<t t-routecomponent="1" />
</div>
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
</templates>
`);
class Book extends Component<any, any, any> {
get incVal() {
return this.props.val + 1;
}
}
class App extends Component<any, any, any> {
components = { Book };
}
const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }];
router = new TestRouter(env, routes, { mode: "history" });
await router.navigate({ to: "book", params: { title: "1984", val: "123" } });
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
});
});
+180
View File
@@ -0,0 +1,180 @@
import { Destination, Router, RouterEnv, Route } from "../../src/router/Router";
import { makeTestEnv } from "../helpers";
import { TestRouter } from "./TestRouter";
let env: RouterEnv;
let router: TestRouter | null = null;
beforeEach(() => {
env = <RouterEnv>makeTestEnv();
window.history.pushState({}, "/", "/");
});
afterEach(() => {
if (router) {
router.destroy();
}
router = null;
});
describe("router miscellaneous", () => {
test("validate routes shape", () => {
expect(() => {
router = new TestRouter(env, [
{ name: "someroute", path: "/some/path", redirect: { abc: "hey" } as Destination }
]);
}).toThrow(`Invalid destination: {"abc":"hey"}`);
});
});
describe("routeToPath", () => {
const routeToPath = Router.prototype["routeToPath"];
test("simple non parameterized path", () => {
expect(routeToPath({path: "/abc"} as Route, {})).toBe("/abc");
expect(routeToPath({path: "/abc/def"} as Route, {})).toBe("/abc/def");
expect(routeToPath({path: "/abc"} as Route, { val: 12 })).toBe("/abc");
});
test("simple parameterized path", () => {
expect(routeToPath({path: "/abc/{{def}}"} as Route, { def: 34 })).toBe("/abc/34");
});
});
describe("destToPath", () => {
test("validate destination shape", () => {
router = new TestRouter(env, [{ name: "someroute", path: "/some/path" }]);
expect(() => {
router!.destToPath({ abc: 123 } as Destination);
}).toThrow('Invalid destination: {"abc":123}');
expect(() => {
router!.destToPath({ to: "someroute" } as Destination);
}).not.toThrow();
expect(() => {
router!.destToPath({ path: "/someroute", to: "otherroute" } as Destination);
}).toThrow();
});
});
describe("getRouteParams", () => {
const getRouteParams = Router.prototype["getRouteParams"];
test("properly match simple routes", () => {
// simple route
expect(getRouteParams({path: "/home"} as Route, "/home")).toEqual({});
// no match
expect(getRouteParams({path: "/home"} as Route, "/otherpath")).toEqual(false);
// fallback route
expect(getRouteParams({path: "*"} as Route, "somepath")).toEqual({});
});
test("match some parameterized routes", () => {
expect(getRouteParams({path: "/invoices/{{id}}"} as Route, "/invoices/3")).toEqual({
id: "3"
});
});
test("can convert to number if needed", () => {
expect(getRouteParams({path: "/invoices/{{id.number}}"} as Route, "/invoices/3")).toEqual({
id: 3
});
});
});
describe("redirect", () => {
test("can redirect to other route", async () => {
router = new TestRouter(env, [
{ name: "routea", path: "/some/path" },
{ name: "routeb", path: "/some/other/path", redirect: { to: "routea" } }
]);
await router.start();
expect(window.location.pathname).toBe("/");
await router.navigate({ to: "routeb" });
expect(window.location.pathname).toBe("/some/path");
expect(router.currentRouteName).toBe("routea");
});
test("can redirect to other path", async () => {
router = new TestRouter(env, [
{ name: "routea", path: "/some/path" },
{ name: "routeb", path: "/some/other/path", redirect: { path: "/some/path" } }
]);
router.navigate = jest.fn(router.navigate);
await router.start();
await router.navigate({ to: "routeb" });
// once because we ask for it, once because it is redirected
// expect(router.navigate).toBeCalledTimes(2);
expect(window.location.pathname).toBe("/some/path");
expect(router.currentRouteName).toBe("routea");
});
});
describe("beforeRouteEnter", () => {
test("navigation guard is called and properly handle return true", async () => {
expect(window.location.pathname).not.toBe("/some/path");
const guard = jest.fn(() => true);
router = new TestRouter(env, [{ name: "route", path: "/some/path", beforeRouteEnter: guard }]);
await router.start();
await router.navigate({ to: "route" });
expect(guard).toBeCalledTimes(1);
expect(window.location.pathname).toBe("/some/path");
expect(router.currentRouteName).toBe("route");
});
test("navigation is cancelled if guard return false", async () => {
expect(window.location.pathname).toBe("/");
const guard = jest.fn(() => false);
router = new TestRouter(env, [
{ name: "routea", path: "/some/patha"},
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard }
]);
await router.start();
await router.navigate({ to: "routea" });
expect(window.location.pathname).toBe("/some/patha");
const result = await router.navigate({ to: "routeb" });
expect(result).toBe(false);
expect(guard).toBeCalledTimes(1);
expect(window.location.pathname).toBe("/some/patha");
expect(router.currentRouteName).toBe("routea");
});
test("navigation is redirected if guard decides so", async () => {
expect(window.location.pathname).toBe("/");
const guard = jest.fn(() => {return {to: "routec"}});
router = new TestRouter(env, [
{ name: "routea", path: "/some/patha"},
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard },
{ name: "routec", path: "/some/pathc"},
]);
await router.start();
const result = await router.navigate({ to: "routea" });
expect(result).toBe(true);
expect(window.location.pathname).toBe("/some/patha");
await router.navigate({ to: "routeb" });
expect(guard).toBeCalledTimes(1);
expect(window.location.pathname).toBe("/some/pathc");
expect(router.currentRouteName).toBe("routec");
});
test("navigation is initially redirected if guard decides so", async () => {
expect(window.location.pathname).toBe("/");
const guard = jest.fn(() => {return {to: "otherroute"}});
router = new TestRouter(env, [
{ name: "landing", path: "/", beforeRouteEnter: guard},
{ name: "otherroute", path: "/some/other/route"}
]);
expect(window.location.pathname).toBe("/");
await router.start();
expect(window.location.pathname).toBe("/some/other/route");
});
});
-1390
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+328
View File
@@ -0,0 +1,328 @@
import { Env } from "../../src/component/component";
import { Store, Getter } from "../../src/store/store";
import { nextTick, nextMicroTick } from "../helpers";
describe("basic use", () => {
test("dispatch an action", () => {
const state = { n: 1 };
const actions = {
inc({ state }, delta) {
state.n += delta;
}
};
const store = new Store({ state, actions });
expect(store.state.n).toBe(1);
store.dispatch("inc", 14);
expect(store.state.n).toBe(15);
});
test("dispatch an action with positional arguments", () => {
const state = { n1: 1, n2: 1, n3: 1 };
const actions = {
batchInc({ state }, delta1, delta2, delta3) {
state.n1 += delta1;
state.n2 += delta2;
state.n3 += delta3;
}
};
const store = new Store({ state, actions });
expect(store.state.n1).toBe(1);
expect(store.state.n2).toBe(1);
expect(store.state.n3).toBe(1);
store.dispatch("batchInc", 14, 30, 88);
expect(store.state.n1).toBe(15);
expect(store.state.n2).toBe(31);
expect(store.state.n3).toBe(89);
});
test("can dispatch an action in an action", () => {
const state = { n: 1 };
const actions = {
inc({ state }, delta) {
state.n += delta;
},
inc100({ dispatch }) {
dispatch("inc", 100);
}
};
const store = new Store({ state, actions });
expect(store.state.n).toBe(1);
store.dispatch("inc100");
expect(store.state.n).toBe(101);
});
test("return data from dispatching an action", () => {
const state = { n: 1 };
const actions = {
inc({ state }) {
return ++state.n;
}
};
const store = new Store({ state, actions });
expect(store.state.n).toBe(1);
const res = store.dispatch("inc");
expect(store.state.n).toBe(2);
expect(res).toBe(2);
});
test("dispatch allow synchronizing between actions", async () => {
const state = { n: 1 };
const actions = {
inc({ state }, delta) {
state.n += delta;
},
setN({ state }, n) {
state.n = n;
},
async dosomething({ dispatch }) {
await dispatch("setTo10");
dispatch("inc", 3);
},
async setTo10({ dispatch }) {
await Promise.resolve();
dispatch("setN", 10);
}
};
const store = new Store({ state, actions });
expect(store.state.n).toBe(1);
store.dispatch("dosomething");
expect(store.state.n).toBe(1);
await nextTick();
expect(store.state.n).toBe(13);
});
test("return data from dispatching an action", async () => {
const state = { n: 1 };
const actions = {
inc({ state }, delta) {
state.n += delta;
},
setN({ state }, n) {
state.n = n;
},
async dosomething({ dispatch }) {
const val = await dispatch("setTo10");
dispatch("inc", val);
},
async setTo10({ dispatch }) {
await Promise.resolve();
dispatch("setN", 10);
return 5;
}
};
const store = new Store({ state, actions });
expect(store.state.n).toBe(1);
store.dispatch("dosomething");
expect(store.state.n).toBe(1);
await nextTick();
expect(store.state.n).toBe(15);
});
test("env is given to actions", () => {
expect.assertions(1);
const someEnv = <Env>{};
const actions = {
someaction({ env }) {
expect(env).toBe(someEnv);
}
};
const store = new Store({ state: {}, actions, env: someEnv });
store.dispatch("someaction");
});
test("can have getters from store", async () => {
const state = {
beers: {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1
}
},
tasters: {
1: {
id: 1,
name: "aaron"
}
}
};
const getters: { [key: string]: Getter } = {
beerTasterName({ state }, beerID) {
return state.tasters[state.beers[beerID].tasterID].name;
},
bestBeerName({ state }) {
return state.beers[1].name;
}
};
const store = new Store({ state, actions: {}, getters });
expect(store.getters).toBeDefined();
expect((<any>store.getters).bestBeerName()).toBe("bertinchamps");
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
});
test("getters given to actions", async () => {
expect.assertions(3);
const state = {
beers: {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1
}
},
tasters: {
1: {
id: 1,
name: "aaron"
}
}
};
const getters = {
beerTasterName({ state }, beerID) {
return state.tasters[state.beers[beerID].tasterID].name;
},
bestBeerName({ state }) {
return state.beers[1].name;
}
};
const actions = {
action({ getters }) {
expect(getters).toBeDefined();
expect(getters.bestBeerName()).toBe("bertinchamps");
expect(getters.beerTasterName(1)).toBe("aaron");
}
};
const store = new Store({ state, actions, getters });
store.dispatch("action");
});
test("can use getters inside a getter", () => {
const getters = {
a({ getters }) {
return `${getters.b()}${getters.c(1)}`;
},
b() {
return "b";
},
c({}, i) {
return `c${i}`;
}
};
const store = new Store({ getters, state: {}, actions: {} });
expect(store.getters.a()).toBe("bc1");
});
});
describe("advanced state properties", () => {
test("state in the store is reference equal after mutation", async () => {
const actions = {
donothing() {}
};
const store = new Store({ state: {}, actions });
const state = store.state;
store.dispatch("donothing");
expect(store.state).toBe(state);
});
test("can use array properties in mutations", () => {
expect.assertions(3);
const state = { a: [1, 2, 3] };
const actions = {
m({ state }) {
expect(state.a.length).toBe(3);
const l = state.a.push(53);
expect(l).toBe(4);
}
};
const store = new Store({ state, actions });
store.dispatch("m");
expect(store.state.a).toEqual([1, 2, 3, 53]);
});
test("can use object assign in store", async () => {
const actions = {
dosomething({ state }) {
Object.assign(state.westmalle, { a: 3, b: 4 });
}
};
const store = new Store({
state: { westmalle: { a: 1, b: 2 } },
actions
});
store.dispatch("dosomething");
expect(store.state.westmalle).toEqual({ a: 3, b: 4 });
});
test("aku reactive store state 1", async () => {
const actions = {
inc({ state }) {
state.counter++;
}
};
const state = { counter: 0 };
const store = new Store({ state, actions });
expect(store.state.counter).toBe(0);
store.dispatch("inc", {});
expect(store.state.counter).toBe(1);
});
});
describe("updates triggered by the store", () => {
test("multiple commits trigger one update", async () => {
let updateCounter = 0;
const state = { n: 1 };
const actions = {
inc({ state }, delta) {
state.n += delta;
}
};
const store = new Store({ state, actions });
store.on("update", null, () => updateCounter++);
store.dispatch("inc", 14);
expect(updateCounter).toBe(0);
store.dispatch("inc", 50);
expect(updateCounter).toBe(0);
await nextMicroTick();
expect(updateCounter).toBe(1);
});
test("empty commits do not trigger updates", async () => {
let updateCounter = 0;
const state = { n: 1 };
const actions = {
inc({ state }, delta) {
state.n += delta;
},
noop() {},
noop2({ state }) {
const val = state.n;
state.n = val;
}
};
const store = new Store({ state, actions });
store.on("update", null, () => updateCounter++);
store.dispatch("noop");
await nextMicroTick();
expect(updateCounter).toBe(0);
store.dispatch("inc", 50);
await nextMicroTick();
expect(updateCounter).toBe(1);
store.dispatch("noop2");
await nextMicroTick();
expect(updateCounter).toBe(1);
});
});
+2 -1
View File
@@ -1,4 +1,5 @@
import { h, patch, init } from "../src/vdom"; import { h, patch } from "../src/vdom";
import { init } from "../src/vdom/vdom";
function map(list, fn) { function map(list, fn) {
var ret: any[] = []; var ret: any[] = [];
+161
View File
@@ -0,0 +1,161 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.16.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+161
View File
@@ -0,0 +1,161 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
owl.Observer.set(messages, i, msg);
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.17.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+161
View File
@@ -0,0 +1,161 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
messages[i] = msg;
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.18.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+2 -3
View File
@@ -107,12 +107,11 @@ class App extends owl.Component {
updateSomeMessages() { updateSomeMessages() {
this.benchmark("update every 10th", () => { this.benchmark("update every 10th", () => {
const setState = owl.Observer.set;
const messages = this.state.messages; const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) { for (let i = 0; i < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]); const msg = Object.assign({}, messages[i]);
msg.author += "!!!"; msg.author += "!!!";
setState(messages, i, msg); messages[i] = msg;
} }
}); });
} }
+1 -3
View File
@@ -1,9 +1,7 @@
html, html,
body { body {
height: 100%; height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
} }
body { body {
+3
View File
@@ -36,6 +36,9 @@
<li><a href="benchmarks/owl-0.13.0">OWL 0.13.0</a></li> <li><a href="benchmarks/owl-0.13.0">OWL 0.13.0</a></li>
<li><a href="benchmarks/owl-0.14.0">OWL 0.14.0</a></li> <li><a href="benchmarks/owl-0.14.0">OWL 0.14.0</a></li>
<li><a href="benchmarks/owl-0.15.0">OWL 0.15.0</a></li> <li><a href="benchmarks/owl-0.15.0">OWL 0.15.0</a></li>
<li><a href="benchmarks/owl-0.16.0">OWL 0.16.0</a></li>
<li><a href="benchmarks/owl-0.17.0">OWL 0.17.0</a></li>
<li><a href="benchmarks/owl-0.18.0">OWL 0.18.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li> <li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul> </ul>
<ul> <ul>
+1 -3
View File
@@ -1,9 +1,7 @@
html, html,
body { body {
height: 100%; height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
} }
.title, .title,
+10 -8
View File
@@ -2,9 +2,7 @@
html, html,
body { body {
height: 100%; height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
} }
body { body {
@@ -181,10 +179,14 @@ body {
.right-pane .error { .right-pane .error {
height: 100%; height: 100%;
padding-top: 40%; width: 90%;
font-size: 30px; padding-top: 30%;
font-size: 18px;
color: darkred; color: darkred;
text-align: center; margin-left: 5%;
padding-left: 30px; }
padding-right: 30px;
.right-pane .error pre {
overflow: auto;
width: 100%;
} }
+326 -102
View File
@@ -1,16 +1,17 @@
const COMPONENTS = `// In this example, we show how components can be defined and created. const COMPONENTS = `// In this example, we show how components can be defined and created.
class Counter extends owl.Component { class Greeter extends owl.Component {
state = { value: 0 }; state = { word: 'Hello' };
increment() { toggle() {
this.state.value++; this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'
} }
} }
// Main root component // Main root component
class App extends owl.Component { class App extends owl.Component {
components = { Counter }; components = { Greeter };
state = { name: 'World'};
} }
// Application setup // Application setup
@@ -21,20 +22,25 @@ app.mount(document.body);
`; `;
const COMPONENTS_XML = `<templates> const COMPONENTS_XML = `<templates>
<button t-name="Counter" t-on-click="increment"> <div t-name="Greeter" class="greeter" t-on-click="toggle">
Click Me! [<t t-esc="state.value"/>] <t t-esc="state.word"/>, <t t-esc="props.name"/>
</button> </div>
<div t-name="App"> <div t-name="App">
<Counter /> <Greeter name="state.name"/>
<Counter />
</div> </div>
</templates>`; </templates>
`;
const COMPONENTS_CSS = `button { const COMPONENTS_CSS = `.greeter {
font-size: 20px; font-size: 20px;
width: 220px; width: 300px;
height: 100px;
margin: 5px; margin: 5px;
text-align: center;
line-height: 100px;
background-color: #eeeeee;
user-select: none;
}`; }`;
const ANIMATION = `// The goal of this component is to see how the t-transition directive can be const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
@@ -262,56 +268,42 @@ const TODO_APP_STORE = `// This example is an implementation of the TodoList app
const ENTER_KEY = 13; const ENTER_KEY = 13;
const ESC_KEY = 27; const ESC_KEY = 27;
const LOCALSTORAGE_KEY = "todos-odoo"; const LOCALSTORAGE_KEY = "todomvc";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Store Definition // Store Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const actions = { const actions = {
addTodo({ commit }, title) {
commit("addTodo", title);
},
removeTodo({ commit }, id) {
commit("removeTodo", id);
},
toggleTodo({ state, commit }, id) {
const todo = state.todos.find(t => t.id === id);
commit("editTodo", { id, completed: !todo.completed });
},
clearCompleted({ state, commit }) {
state.todos
.filter(todo => todo.completed)
.forEach(todo => {
commit("removeTodo", todo.id);
});
},
toggleAll({ state, commit }, completed) {
state.todos.forEach(todo => {
commit("editTodo", {
id: todo.id,
completed
});
});
},
editTodo({ commit }, { id, title }) {
commit("editTodo", { id, title });
}
};
const mutations = {
addTodo({ state }, title) { addTodo({ state }, title) {
const id = state.nextId++; state.todos.push({
const todo = { id: state.nextId++,
id,
title, title,
completed: false completed: false
}; });
state.todos.push(todo);
}, },
removeTodo({ state }, id) { removeTodo({ state }, id) {
const index = state.todos.findIndex(t => t.id === id); const index = state.todos.findIndex(t => t.id === id);
state.todos.splice(index, 1); state.todos.splice(index, 1);
}, },
toggleTodo({ state, dispatch }, id) {
const todo = state.todos.find(t => t.id === id);
dispatch("editTodo", { id, completed: !todo.completed });
},
clearCompleted({ state, dispatch }) {
state.todos
.filter(todo => todo.completed)
.forEach(todo => {
dispatch("removeTodo", todo.id);
});
},
toggleAll({ state, dispatch }, completed) {
state.todos.forEach(todo => {
dispatch("editTodo", {
id: todo.id,
completed
});
});
},
editTodo({ state }, { id, title, completed }) { editTodo({ state }, { id, title, completed }) {
const todo = state.todos.find(t => t.id === id); const todo = state.todos.find(t => t.id === id);
if (title !== undefined) { if (title !== undefined) {
@@ -323,24 +315,20 @@ const mutations = {
} }
}; };
function saveState(state) {
const str = JSON.stringify(state);
window.localStorage.setItem(LOCALSTORAGE_KEY, str);
}
function loadState() {
const localState = window.localStorage.getItem(LOCALSTORAGE_KEY);
return localState ? JSON.parse(localState) : { todos: [], nextId: 1};
}
function makeStore() { function makeStore() {
const todos = JSON.parse( const state = loadState();
window.localStorage.getItem(LOCALSTORAGE_KEY) || "[]" const store = new owl.store.Store({ state, actions });
); store.on("update", null, () => saveState(store.state));
const nextId = Math.max(0, ...todos.map(t => t.id || 0)) + 1;
const state = {
todos,
nextId
};
const store = new owl.Store({
state,
actions,
mutations
});
store.on("update", null, () => {
const state = JSON.stringify(store.state.todos);
window.localStorage.setItem(LOCALSTORAGE_KEY, state);
});
return store; return store;
} }
@@ -351,11 +339,11 @@ class TodoItem extends owl.Component {
state = { isEditing: false }; state = { isEditing: false };
removeTodo() { removeTodo() {
this.env.dispatch("removeTodo", this.props.id); this.env.store.dispatch("removeTodo", this.props.id);
} }
toggleTodo() { toggleTodo() {
this.env.dispatch("toggleTodo", this.props.id); this.env.store.dispatch("toggleTodo", this.props.id);
} }
async editTodo() { async editTodo() {
@@ -387,7 +375,7 @@ class TodoItem extends owl.Component {
if (!value) { if (!value) {
this.removeTodo(this.props.id); this.removeTodo(this.props.id);
} else { } else {
this.env.dispatch("editTodo", { this.env.store.dispatch("editTodo", {
id: this.props.id, id: this.props.id,
title: value title: value
}); });
@@ -399,18 +387,17 @@ class TodoItem extends owl.Component {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// TodoApp // TodoApp
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
function mapStoreToProps(state) { class TodoApp extends owl.store.ConnectedComponent {
return {
todos: state.todos
};
}
class TodoApp extends owl.Component {
components = { TodoItem }; components = { TodoItem };
state = { filter: "all" }; state = { filter: "all" };
static mapStoreToProps(state) {
return {
todos: state.todos
};
}
get visibleTodos() { get visibleTodos() {
let todos = this.props.todos; let todos = this.storeProps.todos;
if (this.state.filter === "active") { if (this.state.filter === "active") {
todos = todos.filter(t => !t.completed); todos = todos.filter(t => !t.completed);
} }
@@ -421,11 +408,11 @@ class TodoApp extends owl.Component {
} }
get allChecked() { get allChecked() {
return this.props.todos.every(todo => todo.completed); return this.storeProps.todos.every(todo => todo.completed);
} }
get remaining() { get remaining() {
return this.props.todos.filter(todo => !todo.completed).length; return this.storeProps.todos.filter(todo => !todo.completed).length;
} }
get remainingText() { get remainingText() {
@@ -437,27 +424,17 @@ class TodoApp extends owl.Component {
if (ev.keyCode === ENTER_KEY) { if (ev.keyCode === ENTER_KEY) {
const title = ev.target.value; const title = ev.target.value;
if (title.trim()) { if (title.trim()) {
this.env.dispatch("addTodo", title); this.dispatch("addTodo", title);
} }
ev.target.value = ""; ev.target.value = "";
} }
} }
clearCompleted() {
this.env.dispatch("clearCompleted");
}
toggleAll() {
this.env.dispatch("toggleAll", !this.allChecked);
}
setFilter(filter) { setFilter(filter) {
this.state.filter = filter; this.state.filter = filter;
} }
} }
const ConnectedTodoApp = owl.connect(TodoApp, mapStoreToProps);
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// App Initialization // App Initialization
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -466,9 +443,8 @@ const qweb = new owl.QWeb(TEMPLATES);
const env = { const env = {
qweb, qweb,
store, store,
dispatch: store.dispatch.bind(store),
}; };
const app = new ConnectedTodoApp(env); const app = new TodoApp(env);
app.mount(document.body); app.mount(document.body);
`; `;
@@ -478,8 +454,8 @@ const TODO_APP_STORE_XML = `<templates>
<h1>todos</h1> <h1>todos</h1>
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/> <input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
</header> </header>
<section class="main" t-if="props.todos.length"> <section class="main" t-if="storeProps.todos.length">
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="toggleAll"/> <input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="dispatch('toggleAll', !allChecked)"/>
<label for="toggle-all"></label> <label for="toggle-all"></label>
<ul class="todo-list"> <ul class="todo-list">
<t t-foreach="visibleTodos" t-as="todo"> <t t-foreach="visibleTodos" t-as="todo">
@@ -487,7 +463,7 @@ const TODO_APP_STORE_XML = `<templates>
</t> </t>
</ul> </ul>
</section> </section>
<footer class="footer" t-if="props.todos.length"> <footer class="footer" t-if="storeProps.todos.length">
<span class="todo-count"> <span class="todo-count">
<strong> <strong>
<t t-esc="remaining"/> <t t-esc="remaining"/>
@@ -505,7 +481,7 @@ const TODO_APP_STORE_XML = `<templates>
<a t-on-click="setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a> <a t-on-click="setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
</li> </li>
</ul> </ul>
<button class="clear-completed" t-if="props.todos.length gt remaining" t-on-click="clearCompleted"> <button class="clear-completed" t-if="storeProps.todos.length gt remaining" t-on-click="dispatch('clearCompleted')">
Clear completed Clear completed
</button> </button>
</footer> </footer>
@@ -950,7 +926,7 @@ function setupResponsivePlugin(env) {
const updateEnv = owl.utils.debounce(() => { const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) { if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile; env.isMobile = !env.isMobile;
env.qweb.trigger('update'); env.qweb.forceUpdate();
} }
}, 15); }, 15);
window.addEventListener("resize", updateEnv); window.addEventListener("resize", updateEnv);
@@ -994,17 +970,19 @@ const RESPONSIVE_XML = `<templates>
</div> </div>
<div t-name="App" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}"> <div t-name="App" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}">
<t t-set="maincontent">
<FormView />
<Chatter />
</t>
<Navbar/> <Navbar/>
<ControlPanel/> <ControlPanel/>
<div class="content-wrapper" t-if="!env.isMobile"> <div class="content-wrapper" t-if="!env.isMobile">
<div class="content"> <div class="content">
<FormView /> <t t-raw="maincontent"/>
<Chatter />
</div> </div>
</div> </div>
<t t-else="1"> <t t-else="1">
<FormView /> <t t-raw="maincontent"/>
<Chatter />
</t> </t>
</div> </div>
</templates> </templates>
@@ -1329,6 +1307,246 @@ const FORM_XML = `<templates>
</templates> </templates>
`; `;
const WMS = `// This example is slightly more complex than usual. We demonstrate
// here a way to manage sub windows in Owl, declaratively. This is still just a
// demonstration. Managing windows can be as complex as we want. For example,
// we could implement the following features:
// - resizing windows
// - minimizing windows
// - configuration options for windows to make a window non resizeable
// - minimal width/height
// - better heuristic for initial window position
// - ...
class HelloWorld extends owl.Component {}
class Counter extends owl.Component {
state = { value: 0 };
inc() {
this.state.value++;
}
}
class Window extends owl.Component {
get style() {
let { width, height, top, left, zindex } = this.props.info;
return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`;
}
close() {
this.trigger("close-window", { id: this.props.info.id });
}
startDragAndDrop(ev) {
this.updateZIndex();
this.el.classList.add('dragging');
const offsetX = this.props.info.left - ev.pageX;
const offsetY = this.props.info.top - ev.pageY;
let left, top;
const el = this.el;
const self = this;
window.addEventListener("mousemove", moveWindow);
window.addEventListener("mouseup", stopDnD, { once: true });
function moveWindow(ev) {
left = Math.max(offsetX + ev.pageX, 0);
top = Math.max(offsetY + ev.pageY, 0);
el.style.left = \`\${left}px\`;
el.style.top = \`\${top}px\`;
}
function stopDnD() {
window.removeEventListener("mousemove", moveWindow);
const options = { id: self.props.info.id, left, top };
self.el.classList.remove('dragging');
self.trigger("set-window-position", options);
}
}
updateZIndex() {
this.trigger("update-z-index", { id: this.props.info.id });
}
}
class WindowManager extends owl.Component {
components = { Window };
windows = [];
nextId = 1;
currentZindex = 1;
addWindow(name) {
const info = this.env.windows.find(w => w.name === name);
const id = \`w\${this.nextId++}\`;
this.windows.push({
id: id,
title: info.title,
width: info.defaultWidth,
height: info.defaultHeight,
top: 0,
left: 0,
zindex: this.currentZindex++
});
this.components[id] = info.component;
this.render();
}
closeWindow(ev) {
const id = ev.detail.id;
delete this.components[id];
const index = this.windows.findIndex(w => w.id === id);
this.windows.splice(index, 1);
this.render();
}
setWindowPosition(ev) {
const id = ev.detail.id;
const w = this.windows.find(w => w.id === id);
w.top = ev.detail.top;
w.left = ev.detail.left;
}
updateZIndex(ev) {
const id = ev.detail.id;
const w = this.windows.find(w => w.id === id);
w.zindex = this.currentZindex++;
ev.target.style["z-index"] = w.zindex;
}
}
class App extends owl.Component {
components = { WindowManager };
addWindow(name) {
this.refs.wm.addWindow(name);
}
}
const qweb = new owl.QWeb(TEMPLATES);
const windows = [
{
name: "Hello",
title: "Hello",
component: HelloWorld,
defaultWidth: 200,
defaultHeight: 100
},
{
name: "Counter",
title: "Click Counter",
component: Counter,
defaultWidth: 300,
defaultHeight: 120
}
];
const env = { qweb, windows };
const app = new App(env);
app.mount(document.body);
`;
const WMS_XML = `<templates>
<div t-name="Window" class="window" t-att-style="style" t-on-click="updateZIndex">
<div class="header">
<span t-on-mousedown="startDragAndDrop"><t t-esc="props.info.title"/></span>
<span class="close" t-on-click="close">×</span>
</div>
<t t-slot="default"/>
</div>
<div t-name="WindowManager" class="window-manager"
t-on-close-window="closeWindow"
t-on-update-z-index="updateZIndex"
t-on-set-window-position="setWindowPosition">
<Window t-foreach="windows" t-as="w" t-key="w.id" info="w">
<t t-component="{{w.id}}"/>
</Window>
</div>
<div t-name="App" class="app">
<WindowManager t-ref="wm"/>
<div class="menubar">
<button t-on-click="addWindow('Hello')">Say Hello</button>
<button t-on-click="addWindow('Counter')">Counter</button>
</div>
</div>
<div t-name="HelloWorld">
World
</div>
<div t-name="Counter" class="counter">
<button t-on-click="inc">Inc</button>
<span><t t-esc="state.value"/></span>
</div>
</templates>
`;
const WMS_CSS = `body {
margin: 0;
}
.app {
width: 100%;
height: 100%;
display: grid;
grid-template-rows: auto 50px;
}
.window-manager {
position: relative;
width: 100%;
height: 100%;
background-color: #eeeeee;
overflow: hidden;
}
.menubar {
background-color: #875a7b;
color: white;
}
.menubar button {
height: 40px;
font-size: 18px;
margin: 5px;
}
.window {
display: grid;
grid-template-rows: 30px auto;
border: 1px solid gray;
background-color: white;
position: absolute;
box-shadow: 1px 1px 2px 1px grey;
}
.window.dragging {
opacity: 0.75;
}
.window .header {
background-color: #875a7b;
display: grid;
grid-template-columns: auto 24px;
color: white;
line-height: 30px;
padding-left: 5px;
cursor: default;
user-select: none;
}
.window .header .close {
cursor: pointer;
font-size: 22px;
padding-left: 4px;
padding-right: 4px;
font-weight: bold;
}
.counter {
font-size: 20px;
}
.counter button {
width: 80px;
height:40px;
font-size: 20px;
}`;
export const SAMPLES = [ export const SAMPLES = [
{ {
description: "Components", description: "Components",
@@ -1371,6 +1589,12 @@ export const SAMPLES = [
xml: SLOTS_XML, xml: SLOTS_XML,
css: SLOTS_CSS css: SLOTS_CSS
}, },
{
description: "Window Management System",
code: WMS,
xml: WMS_XML,
css: WMS_CSS,
},
{ {
description: "Asynchronous components", description: "Asynchronous components",
code: ASYNC_COMPONENTS, code: ASYNC_COMPONENTS,
+2 -1
View File
@@ -48,7 +48,8 @@
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div> <div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
</div> </div>
<div t-if="state.error" class="error"> <div t-if="state.error" class="error">
<t t-esc="state.error"/> <h3>Error</h3>
<pre t-esc="state.error"/>
</div> </div>
<div class="content" t-ref="content"/> <div class="content" t-ref="content"/>
</div> </div>
+2 -2
View File
@@ -5,7 +5,7 @@
"noImplicitThis": true, "noImplicitThis": true,
"removeComments": false, "removeComments": false,
"declaration": true, "declaration": true,
"target": "es5", "target": "esnext",
"outDir": "dist", "outDir": "dist",
"alwaysStrict": true, "alwaysStrict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
@@ -15,5 +15,5 @@
"strictPropertyInitialization": true, "strictPropertyInitialization": true,
"strictNullChecks": true "strictNullChecks": true
}, },
"include": ["src/*.ts"] "include": ["src/**/*.ts","src/*.ts"]
} }