Compare commits

..

22 Commits

Author SHA1 Message Date
Géry Debongnie d381e85d94 [REL] bump to v0.16.0 2019-06-28 11:20:29 +02:00
Géry Debongnie af7520d869 [REF] *: reformat with prettier and printWidth=100
closes #214
2019-06-28 10:33:13 +02:00
Alexandre Kühn 0095bfa61f [IMP] store: positional arguments with dispatch/commit 2019-06-28 10:12:56 +02:00
Géry Debongnie 1b12cf9b91 [FIX] qweb/component: better handling of class attribute
This changes requires using the class module of vdom.

closes #192
2019-06-28 09:49:41 +02:00
Géry Debongnie dbfc7e4acd [FIX] store/connect: fix bugs with parent/child connected
There were big issues when we use parent/child connected widgets.

- children was rendered twice (and mapStoreToProps was called twice)
- if children was supposed to be destroyed, it was rendered once.

We solve them in this commit by waiting for parent widgets to be ready
before updating children.

closes #216
2019-06-28 09:45:43 +02:00
Pierre Paridans e838e879c0 [FIX] qweb: add missing not equals operators
Implement "!=" and "!==" operator support in QWeb.
2019-06-26 16:14:32 +02:00
Géry Debongnie ee63f6bb0f [IMP] playground: log error in console 2019-06-26 15:50:10 +02:00
Géry Debongnie 16bbb8bc9f [FIX] qweb/component: better handling for t-debug
- fix issue with t-debug on a t-set (defining a slot)
- put t-debug and t-log at a very low (high) priority to make sure they
  are executed before other directives

closes #201
2019-06-25 14:03:05 +02:00
Alexandre Kühn 4a5db0c283 [FIX] qweb: t-component + class with extra whitespaces
Crashed due to `DOMTokenList.add(string)` requiring non-empty string.
2019-06-25 13:08:14 +02:00
Géry Debongnie cc50a2e3bb [FIX] playground: fix some issues with tab handling
closes #200
2019-06-25 09:38:00 +02:00
Géry Debongnie 1e2b204fdd [FIX] component: default slot now works with textnodes
closes #209
2019-06-25 09:37:26 +02:00
Géry Debongnie 4a889b7b6b [IMP] qweb: add option to allow duplicate templates 2019-06-24 22:41:02 +02:00
Géry Debongnie 34695883c2 [REF] component: use __ for private methods
closes #202
2019-06-24 13:42:20 +02:00
Géry Debongnie 3d2e2a1873 [FIX] components: ignore slot if no definition is given
closes #203
2019-06-24 13:38:32 +02:00
Géry Debongnie af6aca83a2 [IMP] components: allow multiple roots in slots
closes #199
2019-06-24 13:36:11 +02:00
Géry Debongnie 8c8ffb6a6b [IMP] qweb,component: make QWeb an event bus
closes #197
2019-06-24 09:47:11 +02:00
Géry Debongnie 63a8fcd7e2 [FIX] component: scoping issue with t-on and t-foreach
closes #193
2019-06-21 16:31:24 +02:00
Géry Debongnie e6a5934162 [REF] component: use 'component' instead of 'widget' 2019-06-20 12:31:00 +02:00
Pierre Paridans 5524c2e323 [IMP] store: connected component reuse base component name
Connected component should have a unique name. To achieve this the
connected component currently have an id based generated name (like
`ConnectedComponent1`).

To make it clearer and ease debugging this commit reuse the extended
component's name to include it in the generated name in the form of
`Connected<ComponentName>`.
2019-06-20 12:26:13 +02:00
Géry Debongnie 35c1de26b8 [IMP] component: allow component name in templates
closes #186
2019-06-18 15:02:35 +02:00
Géry Debongnie d1cf6b1b8d [FIX] playground: sample was using old t-foreach syntax 2019-06-17 16:14:49 +02:00
Géry Debongnie bfa2c681cd [ADD] tools: add v0.15.0 benchmark 2019-06-17 16:07:06 +02:00
46 changed files with 6059 additions and 2017 deletions
+3 -3
View File
@@ -32,7 +32,7 @@ find some more information [here](doc/comparison.md).
## Example
Here is a short example to illustrate interactive widgets:
Here is a short example to illustrate interactive components:
```xml
<templates>
@@ -64,8 +64,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:
- [owl-0.15.0.js](https://github.com/odoo/owl/releases/download/v0.15.0/owl.js)
- [owl-0.15.0.min.js](https://github.com/odoo/owl/releases/download/v0.15.0/owl.min.js)
- [owl-0.16.0.js](https://github.com/odoo/owl/releases/download/v0.16.0/owl.js)
- [owl-0.16.0.min.js](https://github.com/odoo/owl/releases/download/v0.16.0/owl.min.js)
Some npm scripts are available:
+1 -1
View File
@@ -87,7 +87,7 @@ For example, a simple fade in/out effect can be done with this:
}
```
The `t-transition` directive can be combined with `t-widget`.
The `t-transition` directive can be applied on a node element or on a component.
Notes:
+4 -4
View File
@@ -91,17 +91,17 @@ there is a syntax highlighter for jsx here on github!
This is actually a big difference between OWL and React/Vue: components in OWL
are totally asynchronous. They have two asynchronous hooks in their lifecycle:
- `willStart` (before the widget starts rendering)
- `willStart` (before the component starts rendering)
- `willUpdateProps` (before new props are set)
Both these methods can be implemented and return a promise. The rendering will
then wait for these promises to be completed before patching the DOM. This is
useful for some use cases: for example, a widget may want to fetch an external
library (a calendar widget may need a specialized calendar rendering library),
useful for some use cases: for example, a component may want to fetch an external
library (a calendar component may need a specialized calendar rendering library),
in its willStart hook.
```javascript
class MyCalendarWidget extends owl.Component {
class MyCalendarComponent extends owl.Component {
...
willStart() {
+162 -151
View File
@@ -4,12 +4,13 @@
- [Overview](#overview)
- [Example](#example)
- [Root Widget And Environment](#root-widget-and-environment)
- [Reference](#reference)
- [Properties](#properties)
- [Static Properties](#static-properties)
- [Methods](#methods)
- [Lifecycle](#lifecycle)
- [Root Component](#root-component)
- [Environment](#environment)
- [Composition](#composition)
- [Event Handling](#event-handling)
- [Form Input Bindings](#form-input-bindings)
@@ -28,11 +29,11 @@ OWL components are the building blocks for user interface. They are designed to
1. **declarative:** the user interface should be described in term of the state
of the application, not as a sequence of imperative steps.
2. **composable:** each widget can seamlessly be created in a parent widget by
a simple directive in its template.
2. **composable:** each component can seamlessly be created in a parent component by
a simple tag or directive in its template.
3. **asynchronous rendering:** the framework will transparently wait for each
subwidgets to be ready before applying the rendering. It uses native promises
sub components to be ready before applying the rendering. It uses native promises
under the hood.
4. **uses QWeb as a template system:** the templates are described in XML
@@ -41,7 +42,7 @@ OWL components are the building blocks for user interface. They are designed to
OWL components are defined as a subclass of Component. The rendering is
exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in QWeb).
Rendering a component generates a virtual dom representation
of the widget, which is then patched to the DOM, in order to apply the changes in an efficient way.
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
OWL components observe their states, and rerender themselves whenever it is
changed. This is done by an [observer](observer.md).
@@ -76,33 +77,9 @@ a state object is defined. It is not mandatory to use the state object, but it
is certainly encouraged. The state object is [observed](observer.md), and any
change to it will cause a rerendering.
## Root Widget And Environment
Most of the time, Owl widget will be created automatically by the `t-widget`
directive in a template. There is however an obvious exception: the root widget
of an Owl application has to be created manually:
```js
class App extends owl.Component { ... }
const qweb = new owl.QWeb(TEMPLATES);
const env = { qweb: qweb };
const app = new App(env);
app.mount(document.body);
```
The root widget needs an environment. In Owl, an environment is an object with
a `qweb` key, which has to be a [QWeb](qweb.md) instance. This qweb instance will
be used to render everything.
The environment will be given to each child, unchanged, in the `env` property.
This can be very useful to share common information/methods. For example, all
rpcs can be made through a `rpc` method in the environment. This makes it very
easy to test a component.
## Reference
An Owl component is a small class which represent a widget or some UI element.
An Owl component is a small class which represent a component or some UI element.
It exists in the context of an environment (`env`), which is propagated from a
parent to its children. The environment needs to have a QWeb instance, which
will be used to render the component template.
@@ -123,7 +100,7 @@ find a template with the component name (or one of its ancestor).
- **`state`** (Object): this is the location of the component's state, if there is
any. After the willStart method, the `state` property is observed, and each
change will cause the widget to rerender itself.
change will cause the component to rerender itself.
- **`props`** (Object): this is an object given (in the constructor) by the parent
to configure the component. It can be dynamically changed later by the parent,
@@ -131,7 +108,7 @@ find a template with the component name (or one of its ancestor).
As such, it should not ever be modified by the component!!
- **`refs`** (Object): the `refs` object contains all references to sub DOM nodes
or sub widgets defined by a `t-ref` directive in the component's template.
or sub components defined by a `t-ref` directive in the component's template.
### Static Properties
@@ -146,6 +123,8 @@ find a template with the component name (or one of its ancestor).
### Methods
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
DOM: the root component is mounted to a target HTMLElement. Obviously, this
is asynchronous, since each children need to be created as well. Most applications
@@ -163,7 +142,7 @@ find a template with the component name (or one of its ancestor).
DOM in the same stack frame.
- **`shouldUpdate(nextProps)`**: this method is called each time a component's props
are updated. It returns a boolean, which indicates if the widget should
are updated. It returns a boolean, which indicates if the component should
ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
@@ -180,6 +159,11 @@ find a template with the component name (or one of its ancestor).
called directly (except maybe on the root component), but should be done by the
framework instead.
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
starting with `__`, in order to prevent possible future conflicts with user code
whenever Owl needs to change.
### Lifecycle
A solid and robust component system needs useful hooks/methods to help
@@ -239,7 +223,7 @@ perform some action before the initial rendering of a component.
It will be called exactly once before the initial rendering. It is useful
in some cases, for example, to load external assets (such as a JS library)
before the widget is rendered. Another use case is to load data from a server.
before the component is rendered. Another use case is to load data from a server.
```javascript
async willStart() {
@@ -251,7 +235,7 @@ 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
fast as possible.
The widget rendering will take place after `willStart` is completed.
The component rendering will take place after `willStart` is completed.
#### `mounted()`
@@ -296,8 +280,8 @@ scrollbar.
Note that modifying the state object is not allowed here. This method is called just
before an actual DOM patch, and is only intended to be used to save some local
DOM state. Also, it will not be called if the widget is not in the DOM (this can
happen with widgets with `t-keepalive`).
DOM state. Also, it will not be called if the component is not in the DOM (this can
happen with components with `t-keepalive`).
The return value of this method will be given as the first argument of the
corresponding `patched` call.
@@ -309,12 +293,12 @@ likely via a change in its state/props or environment).
This method is not called on the initial render. It is useful to interact
with the DOM (for example, through an external library) whenever the
component was patched. Note that this hook will not be called if the widget is
not in the DOM (this can happen with widgets with `t-keepalive`).
component was patched. Note that this hook will not be called if the compoent is
not in the DOM (this can happen with components with `t-keepalive`).
The `snapshot` parameter is the result of the previous `willPatch` call.
Updating the widget state in this hook is possible, but not encouraged.
Updating the compoent state in this hook is possible, but not encouraged.
One need to be careful, because updates here will cause rerender, which in
turn will cause other calls to patched. So, we need to be particularly
careful at avoiding endless cycles.
@@ -335,91 +319,137 @@ the DOM. This is a good place to remove some listeners, for example.
This is the opposite method of `mounted`.
### Root Component
Most of the time, an Owl component will be created automatically by a tag (or the `t-component`
directive) in a template. There is however an obvious exception: the root component
of an Owl application has to be created manually:
```js
class App extends owl.Component { ... }
const qweb = new owl.QWeb(TEMPLATES);
const env = { qweb: qweb };
const app = new App(env);
app.mount(document.body);
```
The root component needs an environment.
### Environment
In Owl, an environment is an object with a `qweb` key, which has to be a
[QWeb](qweb.md) instance. This qweb instance will be used to render everything.
The environment is meant to contain (mostly) static global information and
methods for the whole application. For example, settings keys (`mode` to determine
if we are in desktop or mobile mode, or `theme`: dark or light), `rpc` methods,
session information, ...
The environment will be given to each child, unchanged, in the `env` property.
This can be very useful to share common information/methods. For example, all
rpcs can be made through a `rpc` method in the environment. This makes it very
easy to test a component.
Updating the environment is not as simple as changing a component's state: its
content is not observed, so updates will not be reflected immediately in the
user interface. There is however a mechanism to force root widgets to rerender
themselves whenever the environment is modified: one only needs to trigger the
`update` event on the QWeb instance. For example, a responsive environment
could be programmed like this:
```js
function setupResponsivePlugin(env) {
const isMobile = () => window.innerWidth <= 768;
env.isMobile = isMobile();
const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile;
env.qweb.trigger('update');
}
}, 15);
window.addEventListener("resize", updateEnv);
}
```
### Composition
The example above shows a QWeb template with a `t-on-click` directive. Widget
templates are standard [QWeb](qweb.md) templates, but with an extra directive:
`t-widget`. With the `t-widget` directive, widget templates can declare sub
widgets:
The example above shows a QWeb template with a sub component. In a template,
components are declared with a tagname corresponding to the class name. It has
to be capitalized.
```xml
<div t-name="ParentWidget">
<div t-name="ParentComponent">
<span>some text</span>
<t t-widget="MyWidget" info="13"/>
<MyComponent info="13" />
</div>
```
```js
class ParentWidget extends owl.Component {
widgets = { MyWidget: MyWidget};
class ParentComponent extends owl.Component {
components = { MyComponent: MyComponent};
...
}
```
In this example, the `ParentWidget`'s template creates a widget `MyWidget` just
after the span. The `info` key will be added to the subwidget's props. Each
props is a string which represents a javascript (QWeb) expression, so it is
In this example, the `ParentComponent`'s template creates a component `MyComponent` just
after the span. The `info` key will be added to the subcomponent's `props`. Each
`props` is a string which represents a javascript (QWeb) expression, so it is
dynamic. If it is necessary to give a string, this can be done by quoting it:
`someString="'somevalue'"`. See the
[QWeb](qweb.md) documentation for more information on the `t-widget` directive.
`someString="'somevalue'"`.
Note that the rendering context for the template is the widget itself. This means
that the template can access `state`, `props`, `env`, or any methods defined in the widget.
The `t-widget` directive is the key to a declarative component
system. It allows a template to define where and how a sub widget is created
and/or updated. For example:
Note that the rendering context for the template is the component itself. This means
that the template can access `state`, `props`, `env`, or any methods defined in the component.
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget" count="state.val"/>
<div t-name="ParentComponent">
<ChildComponent count="state.val" />
</div>
```
```js
class ParentWidget {
widgets = { ChildWidget };
class ParentComponent {
components = { ChildComponent };
state = { val: 4 };
}
```
Whenever the template is rendered, it will automatically create the subwidget
`ChildWidget` at the correct place. It needs to find the reference to the
actual component class in the special `widgets` key, or the class registered in
Whenever the template is rendered, it will automatically create the subcomponent
`ChildComponent` at the correct place. It needs to find the reference to the
actual component class in the special `components` key, or the class registered in
QWeb's global registry (see `register` function of QWeb). It first looks inside
the local `widgets` key, then fallbacks on the global registry.
the local `components` key, then fallbacks on the global registry.
_Props_: In this example, the child widget will receive the object `{count: 4}` in its
_Props_: In this example, the child component will receive the object `{count: 4}` in its
constructor. This will be assigned to the `props` variable, which can be accessed
on the widget (and also, in the template). Whenever the state is updated, then
the subwidget will also be updated automatically.
on the component (and also, in the template). Whenever the state is updated, then
the sub component will also be updated automatically.
Note that there are some restrictions on prop names: `class`, `style` and any
string which starts with `t-` are not allowed.
The `t-widget` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
The `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget{{id}}"/>
<div t-name="ParentComponent">
<t t-component="ChildComponent{{id}}" />
</div>
```
```js
class ParentWidget {
widgets = { ChildWidget1, ChildWidget2 };
class ParentComponent {
components = { ChildComponent1, ChildComponent2 };
state = { id: 1 };
}
```
**CSS and style:** there is some specific support to allow the parent to declare
additional css classes or style for the sub widget: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
root widget element.
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
root component element.
```xml
<div t-name="ParentWidget">
<t t-widget="MyWidget" class="someClass" style="font-weight:bold;" info="13"/>
<div t-name="ParentComponent">
<MyComponent class="someClass" style="font-weight:bold;" info="13" />
</div>
```
@@ -431,7 +461,7 @@ class that need to be removed. This is why we only support the explicit syntax
with a class object:
```xml
<t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" />
<MyComponent t-att-class="{a: state.flagA, b: state.flagB}" />
```
### Event Handling
@@ -454,7 +484,7 @@ A _pure_ DOM event is directly triggered by a user interaction (e.g. a `click`).
This will be roughly translated in javascript like this:
```js
button.addEventListener("click", widget.someMethod.bind(widget));
button.addEventListener("click", component.someMethod.bind(component));
```
The suffix (`click` in this example) is simply the name of the actual DOM
@@ -463,11 +493,11 @@ event.
A _business_ DOM event is triggered by a call to `trigger` on a component.
```xml
<t t-widget="MyWidget" t-on-menu-loaded="someMethod"/>
<MyComponent t-on-menu-loaded="someMethod" />
```
```js
class MyWidget {
class MyComponent {
someWhere() {
const payload = ...;
this.trigger('menu-loaded', payload);
@@ -477,12 +507,12 @@ A _business_ DOM event is triggered by a call to `trigger` on a component.
The call to `trigger` generates a [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
of type `menu-loaded` and dispatches it on the component's DOM element
(`this.el`). The event bubbles and is cancelable. The parent widget listening
(`this.el`). The event bubbles and is cancelable. The parent component listening
to event `menu-loaded` will receive the payload in its `someMethod` handler
(in the `detail` property of the event), whenever the event is triggered.
```js
class ParentWidget {
class ParentComponent {
someMethod(ev) {
const payload = ev.detail;
...
@@ -538,8 +568,8 @@ class Form extends owl.Component {
```xml
<div>
<input t-on-input="_updateInputValue"/>
<span t-esc="state.text"/>
<input t-on-input="_updateInputValue" />
<span t-esc="state.text" />
</div>
```
@@ -559,8 +589,8 @@ class Form extends owl.Component {
```xml
<div>
<input t-model="text"/>
<span t-esc="state.text"/>
<input t-model="text" />
<span t-esc="state.text" />
</div>
```
@@ -586,7 +616,7 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
<label for="red">Red</label>
</span>
<span>
<input type="radio" name="color" id="blue" value="blue" t-model="color"/>
<input type="radio" name="color" id="blue" value="blue" t-model="color" />
<label for="blue">Blue</label>
</span>
</div>
@@ -604,7 +634,7 @@ Like event handling, the `t-model` directive accepts some modifiers:
For example:
```xml
<input t-model.lazy="someVal"/>
<input t-model.lazy="someVal" />
```
These modifiers can be combined. For instance, `t-model.lazy.number` will only
@@ -627,7 +657,7 @@ There are three main use cases:
```xml
<span t-foreach="todos" t-as="todo" t-key="todo.id">
<t t-esc="todo.text"/>
<t t-esc="todo.text" />
</span>
```
@@ -646,7 +676,7 @@ is inserted into the DOM.
```
```js
class MyWidget extends owl.Component {
class MyComponent extends owl.Component {
...
focusMe() {
this.refs.someInput.focus();
@@ -677,27 +707,27 @@ component (with some code like `app.mount(document.body)`).
2. when it is done, template `A` is rendered.
- widget `B` is created
- component `B` is created
1. `willStart` is called on `B`
2. template `B` is rendered
- widget `C` is created
- component `C` is created
1. `willStart` is called on `C`
2. template `C` is rendered
- widget `D` is created
- component `D` is created
1. `willStart` is called on `D`
2. template `D` is rendered
- widget `E` is created
- component `E` is created
1. `willStart` is called on `E`
2. template `E` is rendered
3. widget `A` is patched into a detached DOM element. This will create the actual
widget `A` DOM structure. The patching process will cause recursively the
3. component `A` is patched into a detached DOM element. This will create the actual
component `A` DOM structure. The patching process will cause recursively the
patching of the `B`, `C`, `D` and `E` DOM trees. (so the actual full DOM tree is created
in one pass)
4. the widget `A` root element is actually appended to `document.body`
4. the component `A` root element is actually appended to `document.body`
5. The method `mounted` is called recursively on all widgets in the following
5. The method `mounted` is called recursively on all components in the following
order: `B`, `D`, `E`, `C`, `A`.
**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some
@@ -705,7 +735,7 @@ button in `C`, and this results in a state update, which is supposed to:
- update `D`,
- remove `E`,
- add new widget `F`.
- add new component `F`.
So, the component tree should look like this:
@@ -722,17 +752,17 @@ Here is what Owl will do:
1. because of a state change, the method `render` is called on `C`
2. template `C` is rendered again
- widget `D` is updated:
- component `D` is updated:
1. hook `willUpdateProps` is called on `D` (async)
2. template `D` is rerendered
- widget `F` is created:
- component `F` is created:
1. hook `willStart` is called on `E` (async)
2. template `F` is rendered
3. `willPatch` hooks are called recursively on widgets `C`, `D` (not on `F`,
3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
because it is not mounted yet)
4. widget `C` is patched, which will cause recursively:
4. component `C` is patched, which will cause recursively:
2. `willUnmount` hook on `E`, then destruction of `E`,
3. (initial) patching of `F`, then hook `mounted` is called on `F`
@@ -836,42 +866,42 @@ Like the `t-on` directive, it can work either on a DOM node, or on a component:
```xml
<div>
<div t-ref="someDiv"/>
<t t-widget="SubWidget" t-ref="someWidget"/>
<SubComponent t-ref="someComponent"/>
</div>
```
In this example, the widget will be able to access the `div` and the component
In this example, the component will be able to access the `div` and the component
inside the special `refs` variable:
```js
this.refs.someDiv;
this.refs.someWidget;
this.refs.someComponent;
```
This is useful for various usecases: for example, integrating with an external
library that needs to render itself inside an actual DOM node. Or for calling
some method on a sub widget.
some method on a sub component.
Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
`t-widget` directives). For example, if we have
`t-component` directives). For example, if we have
`id` set to 44 in the rendering context,
```xml
<div t-ref="widget_{{id}}"/>
<div t-ref="component_{{id}}"/>
```
```js
this.refs.widget_44;
this.refs.component_44;
```
### Slots
To make generic components, it is useful to be able for a parent widget to _inject_
some sub template, but still be the owner. For example, a generic dialog widget
To make generic components, it is useful to be able for a parent component to _inject_
some sub template, but still be the owner. For example, a generic dialog component
will need to render some content, some footer, but with the parent as the
rendering context.
@@ -892,50 +922,31 @@ This is what _slots_ are for.
Slots are defined by the caller, with the `t-set` directive:
```xml
<div t-name="SomeWidget">
<div>some widget</div>
<t t-widget="Dialog" title="Some Dialog">
<div t-name="SomeComponent">
<div>some component</div>
<Dialog title="Some Dialog">
<t t-set="content">
<div>hey</div>
</t>
<t t-set="footer">
<button t-on-click="doSomething">ok</button>
</t>
</t>
</Dialog>
</div>
```
In this example, the widget `Dialog` will render the slots `content` and `footer`
In this example, the component `Dialog` will render the slots `content` and `footer`
with its parent as rendering context. This means that clicking on the button
will execute the `doSomething` method on the parent, not on the dialog.
Warning! Slots have a technical constraint: the result of the slot rendering
should have exactly one root node. So,
```xml
<t t-set="content">
<div>A</div>
<div>B</div>
</t>
```
is not allowed. A workaround could be to wrap the content in a div:
```xml
<div t-set="content">
<div>A</div>
<div>B</div>
</div>
```
Default slot: the first element inside the widget which is not a named slot will
Default slot: the first element inside the component which is not a named slot will
be considered the `default` slot. For example:
```xml
<div t-name="Parent">
<t t-widget="Child">
<Child>
<span>some content</span>
</t>
</Child>
</div>
<div t-name="Child">
@@ -952,23 +963,23 @@ components.
There are two different common problems with Owl asynchronous rendering model:
- any widget can delay the rendering (initial and subsequent) of the whole
- any component can delay the rendering (initial and subsequent) of the whole
application
- for a given widget, there are two independant situations that will trigger an
- for a given component, there are two independant situations that will trigger an
asynchronous rerendering: a change in the state, or a change in the props.
These changes may be done at different times, and Owl has no way of knowing
how to reconcile the resulting renderings.
Here are a few tips on how to work with asynchronous widgets:
Here are a few tips on how to work with asynchronous components:
1. Minimize the use of asynchronous widgets!
1. Minimize the use of asynchronous components!
2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
synchronous renderings
3. Lazy loading external libraries is a good use case for async rendering. This
is mostly fine, because we can assume that it will only takes a fraction of a
second, and only once (see `owl.utils.loadJS`)
4. For all the other cases, the `t-asyncroot` directive (to use alongside
`t-widget`) is there to help you. When this directive is met, a new rendering
`t-component`) is there to help you. When this directive is met, a new rendering
sub tree is created, such that the rendering of that component (and its
children) is not tied to the rendering of the rest of the interface. It can
be used on an asynchronous component, to prevent it from delaying the
@@ -978,8 +989,8 @@ Here are a few tips on how to work with asynchronous widgets:
(triggered by state or props changes).
```xml
<div t-name="ParentWidget">
<t t-widget="SyncChild"/>
<t t-widget="AsyncChild" t-asyncroot="1"/>
<div t-name="ParentComponent">
<SyncChild />
<AsyncChild t-asyncroot="1"/>
</div>
```
+4 -4
View File
@@ -58,16 +58,16 @@ To build an application (or a sub-part of an application), we need two things:
need. In practice, it could context some user session information, some
configuration keys (for example, isMobile = true/false if we are in mobile mode).
- a description of the user interface: there should be a root widget, which can
have sub widgets
- a description of the user interface: there should be a root component, which can
have sub components
Here are a few steps that we may take to get started:
- get the templates
- create a qweb engine, with the templates
- create an environment
- create an instance of the root widget
- mount the root widget to a DOM element
- create an instance of the root component
- mount the root component to a DOM element
Let us now add the javascript to make it work, in `app.js`:
+25 -18
View File
@@ -44,7 +44,7 @@ with a few interesting points:
- it compiles templates into functions that output a virtual DOM instead of a
string. This is necessary for the component system.
- it has a few extra directives: `t-widget`, `t-on`, ...
- it has a few extra directives: `t-component`, `t-on`, ...
## Directives
@@ -65,16 +65,16 @@ We present here a list of all standard QWeb directives:
The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
| Name | Description |
| -------------------------------------------- | ----------------------------------------------------------------------------------- |
| `t-widget`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
| `t-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
| Name | Description |
| ------------------------------------------- | ----------------------------------------------------------------------------------- |
| `t-component`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
| `t-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
## QWeb Engine
@@ -95,12 +95,14 @@ It's API is quite simple:
const qweb = new owl.QWeb(TEMPLATES);
```
- **`addTemplate(name, xmlStr)`**: add a specific template.
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
```js
qweb.addTemplate("mytemplate", "<div>hello</div>");
```
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply return whenever a template is added for a second time. Otherwise, `QWeb` will crash.
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute).
@@ -108,7 +110,7 @@ It's API is quite simple:
const TEMPLATES = `
<templates>
<div t-name="App" class="main">main</div>
<div t-name="OtherWidget">other widget</div>
<div t-name="OtherComponent">other component</div>
</templates>`;
qweb.addTemplates(TEMPLATES);
```
@@ -117,12 +119,12 @@ It's API is quite simple:
which is a virtual representation of the DOM (see [vdom doc](vdom.md)).
```js
const vnode = qweb.render("App", widget);
const vnode = qweb.render("App", component);
```
- **`register(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-widget` directive). This is useful for commonly used
templates (see the `t-component` directive). This is useful for commonly used
components accross the application.
```js
@@ -131,10 +133,15 @@ It's API is quite simple:
...
class ParentWidget extends owl.Component { ... }
qweb.addTemplate("ParentWidget", "<div><t t-widget='Dialog'/></div>");
class ParentComponent extends owl.Component { ... }
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
```
In some way, a `QWeb` instance is the core of an Owl application. It is the only
mandatory element of an [environment](component.md#environment). As such, it
has an extra responsability: it can act as an event bus for internal communication
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
## Reference
We define in this section the specification of how `QWeb` templates should be
@@ -179,7 +186,7 @@ root nodes.
QWeb expressions are strings that will be processed at compile time. Each variable in
the javascript expression will be replaced by a lookup in the context (so, the
widget). For example, `a + b.c(d)` will be converted into:
component). For example, `a + b.c(d)` will be converted into:
```js
context["a"] + context["b"].c(context["d"]);
+51 -2
View File
@@ -10,6 +10,8 @@
- [Actions](#actions)
- [Getters](#getters)
- [Connecting a Component](#connecting-a-component)
- [Semantics](#semantics)
- [Good Practices](#good-practices)
## Overview
@@ -84,6 +86,20 @@ object, or modifying an array with the `arr[i] = newValue` syntax). See the
Mutations are the only way to modify the state. Changing the state outside a
mutation is not allowed (and should throw an error). Mutations are synchronous.
```js
const mutations = {
setLoginState({ state }, loginState) {
state.loginState = loginState;
}
};
```
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 are used to coordinate state changes. It is also useful whenever some
@@ -92,10 +108,10 @@ in an action.
```js
const actions = {
async login({ commit }) {
async login({ commit }, info) {
commit("setLoginState", "pending");
try {
const loginInfo = await doSomeRPC("/login/", "someinfo");
const loginInfo = await doSomeRPC("/login/", info);
commit("setLoginState", loginInfo);
} catch {
commit("setLoginState", "error");
@@ -104,6 +120,13 @@ const actions = {
};
```
Actions are called with the `dispatch` method on the store, and can receive an
arbitrary number of arguments.
```js
store.dispatch("login", someInfo);
```
### Getters
Usually, data contained in the store will be stored in a normalized way. For
@@ -207,3 +230,29 @@ The arguments of `connect` are:
The `connect` function returns a sub class of the given `Component` which is
connected to the `store`.
### Semantics
The `Store` and the `connect` function try to be smart and to optimize as much
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
before children)
- they are updated only if they are in the DOM
- if a parent is asynchronous, the system will wait for it to complete its
update before updating other components.
- 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
a situation where some part of the UI is updated and other parts of the UI is
not updated.
### Good Practices
- avoid asynchronous components as much as possible. Asynchronous components
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
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
- since the `mapStoreToProps` function is called for each connected component,
for each state update, it is important to make sure that these functions are
as fast as possible.
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "owl",
"version": "0.15.0",
"version": "0.16.0",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"scripts": {
@@ -60,5 +60,8 @@
"json",
"node"
]
},
"prettier": {
"printWidth": 100
}
}
+84 -78
View File
@@ -42,8 +42,8 @@ export interface Meta<T extends Env, Props> {
isDestroyed: boolean;
parent: Component<T, any, any> | null;
children: { [key: number]: Component<T, any, any> };
// children mapping: from templateID to widgetID
// should it be a map number => Widget?
// children mapping: from templateID to componentID
// should it be a map number => Component?
cmap: { [key: number]: number };
renderId: number;
@@ -53,6 +53,7 @@ export interface Meta<T extends Env, Props> {
observer?: Observer;
render?: CompiledTemplate;
mountedHandlers: { [key: number]: Function };
classObj?: { [key: string]: boolean };
}
// If a component does not define explicitely a template
@@ -62,7 +63,7 @@ export interface Meta<T extends Env, Props> {
const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
//------------------------------------------------------------------------------
// Widget
// Component
//------------------------------------------------------------------------------
let nextId = 1;
@@ -71,8 +72,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
template?: string;
/**
* The `el` is the root element of the widget. Note that it could be null:
* this is the case if the widget is not mounted yet, or is destroyed.
* The `el` is the root element of the component. Note that it could be null:
* this is the case if the component is not mounted yet, or is destroyed.
*/
get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
@@ -97,34 +98,34 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
/**
* Creates an instance of Component.
*
* The root widget of a component tree needs an environment:
* The root component of a component tree needs an environment:
*
* ```javascript
* const root = new RootWidget(env, props);
* const root = new RootComponent(env, props);
* ```
*
* Every other widget simply needs a reference to its parent:
* Every other component simply needs a reference to its parent:
*
* ```javascript
* const child = new SomeWidget(parent, props);
* const child = new SomeComponent(parent, props);
* ```
*
* Note that most of the time, only the root widget needs to be created by
* hand. Other widgets should be created automatically by the framework (with
* the t-widget directive in a template)
* Note that most of the time, only the root component needs to be created by
* hand. Other components should be created automatically by the framework (with
* the t-component directive in a template)
*/
constructor(parent: Component<T, any, any> | T, props?: Props) {
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
props = this._applyDefaultProps(props, defaultProps);
props = this.__applyDefaultProps(props, defaultProps);
}
if (QWeb.dev) {
this._validateProps(props || {});
this.__validateProps(props || {});
}
// is this a good idea?
// Pro: if props is empty, we can create easily a widget
// Pro: if props is empty, we can create easily a component
// Con: this is not really safe
// Pro: but creating widget (by a template) is always unsafe anyway
// Pro: but creating component (by a template) is always unsafe anyway
this.props = <Props>props || <Props>{};
let id: number = nextId++;
let p: Component<T, any, any> | null = null;
@@ -134,6 +135,19 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
parent.__owl__.children[id] = this;
} else {
this.env = parent;
this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) {
this.render(true);
}
if (this.__owl__.isDestroyed) {
// this is unlikely to happen, but if a root widget is destroyed,
// we want to remove our subscription. The usual way to do that
// would be to perform some check in the destroy method, but since
// it is very performance sensitive, and since this is a rare event,
// we simply do it lazily
this.env.qweb.off("update", this);
}
});
}
this.__owl__ = {
id: id,
@@ -157,7 +171,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
*
* It will be called exactly once before the initial rendering. It is useful
* in some cases, for example, to load external assets (such as a JS library)
* before the widget is rendered.
* before the component is rendered.
*
* Note that a slow willStart method will slow down the rendering of the user
* interface. Therefore, some effort should be made to make this method as
@@ -207,7 +221,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* with the DOM (for example, through an external library) whenever the
* component was updated.
*
* Updating the widget state in this hook is possible, but not encouraged.
* Updating the component state in this hook is possible, but not encouraged.
* One need to be careful, because updates here will cause rerender, which in
* turn will cause other calls to updated. So, we need to be particularly
* careful at avoiding endless cycles.
@@ -238,22 +252,22 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* created declaratively in templates are managed by the Owl system.
*/
async mount(target: HTMLElement): Promise<void> {
const vnode = await this._prepare();
const vnode = await this.__prepare();
if (this.__owl__.isDestroyed) {
// component was destroyed before we get here...
return;
}
this._patch(vnode);
this.__patch(vnode);
target.appendChild(this.el!);
if (document.body.contains(target)) {
this._callMounted();
this.__callMounted();
}
}
unmount() {
if (this.__owl__.isMounted) {
this._callWillUnmount();
this.__callWillUnmount();
this.el!.remove();
}
}
@@ -267,14 +281,14 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (shouldPatch) {
patchQueue = [];
}
const renderVDom = this._render(force, patchQueue);
const renderVDom = this.__render(force, patchQueue);
const renderId = __owl__.renderId;
await renderVDom;
if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
// we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called.
this._applyPatchQueue(<any[]>patchQueue);
this.__applyPatchQueue(<any[]>patchQueue);
}
}
@@ -284,14 +298,14 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* - call the willUnmount hooks if necessary
* - remove the dom node from the dom
*
* This should only be called manually if you created the widget. Most widgets
* will be automatically destroyed.
* This should only be called manually if you created the component. Most
* components will be automatically destroyed.
*/
destroy() {
const __owl__ = this.__owl__;
if (!__owl__.isDestroyed) {
const el = this.el;
this._destroy(__owl__.parent);
this.__destroy(__owl__.parent);
if (el) {
el.remove();
}
@@ -308,8 +322,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
/**
* This method is the correct way to update the environment of a widget. Doing
* this will cause a full rerender of the widget and its children, so this is
* This method is the correct way to update the environment of a component. Doing
* this will cause a full rerender of the component and its children, so this is
* an operation that should not be done frequently.
*
* A good usecase for updating the environment would be to update some mostly
@@ -348,7 +362,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
// Private
//--------------------------------------------------------------------------
_destroy(parent: Component<any, any, any> | null) {
__destroy(parent: Component<any, any, any> | null) {
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
@@ -357,7 +371,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
const children = __owl__.children;
for (let key in children) {
children[key]._destroy(this);
children[key].__destroy(this);
}
if (parent) {
let id = __owl__.id;
@@ -368,13 +382,13 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
delete __owl__.vnode;
}
_callMounted() {
__callMounted() {
const __owl__ = this.__owl__;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
comp._callMounted();
comp.__callMounted();
}
}
__owl__.isMounted = true;
@@ -385,7 +399,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
this.mounted();
}
_callWillUnmount() {
__callWillUnmount() {
this.willUnmount();
const __owl__ = this.__owl__;
__owl__.isMounted = false;
@@ -393,12 +407,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
comp._callWillUnmount();
comp.__callWillUnmount();
}
}
}
async _updateProps(
async __updateProps(
nextProps: Props,
forceUpdate: boolean = false,
patchQueue?: any[]
@@ -407,10 +421,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (shouldUpdate) {
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
nextProps = this._applyDefaultProps(nextProps, defaultProps);
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
}
if (QWeb.dev) {
this._validateProps(nextProps);
this.__validateProps(nextProps);
}
await this.willUpdateProps(nextProps);
this.props = nextProps;
@@ -418,20 +432,24 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
}
_patch(vnode) {
__patch(vnode) {
const __owl__ = this.__owl__;
__owl__.renderPromise = null;
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);
}
_prepare(): Promise<VNode> {
__prepare(): Promise<VNode> {
const __owl__ = this.__owl__;
__owl__.renderProps = this.props;
__owl__.renderPromise = this._prepareAndRender();
__owl__.renderPromise = this.__prepareAndRender();
return __owl__.renderPromise;
}
async _prepareAndRender(): Promise<VNode> {
async __prepareAndRender(): Promise<VNode> {
await this.willStart();
const __owl__ = this.__owl__;
if (__owl__.isDestroyed) {
@@ -450,17 +468,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (template) {
this.template = template;
} else {
while (
(template = p.name) &&
!(template in qweb.templates) &&
p !== Component
) {
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
p = p.__proto__;
}
if (p === Component) {
throw new Error(
`Could not find template for component "${this.constructor.name}"`
);
throw new Error(`Could not find template for component "${this.constructor.name}"`);
} else {
tmap[name] = template;
this.template = template;
@@ -468,14 +480,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
}
__owl__.render = qweb.render.bind(qweb, this.template);
this._observeState();
return this._render();
this.__observeState();
return this.__render();
}
async _render(
force: boolean = false,
patchQueue: any[] = []
): Promise<VNode> {
async __render(force: boolean = false, patchQueue: any[] = []): Promise<VNode> {
const __owl__ = this.__owl__;
__owl__.renderId++;
const promises: Promise<void>[] = [];
@@ -499,9 +508,9 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
// this part is critical for the patching process to be done correctly. The
// tricky part is that a child widget can be rerendered on its own, which
// tricky part is that a child component can be rerendered on its own, which
// will update its own vnode representation without the knowledge of the
// parent widget. With this, we make sure that the parent widget will be
// parent component. With this, we make sure that the parent component will be
// able to patch itself properly after
vnode.key = __owl__.id;
__owl__.renderProps = this.props;
@@ -510,21 +519,24 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
/**
* Only called by qweb t-widget directive
* Only called by qweb t-component directive
*/
_mount(vnode: VNode, elm: HTMLElement): VNode {
__mount(vnode: VNode, elm: HTMLElement): VNode {
const __owl__ = this.__owl__;
if (__owl__.classObj) {
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, __owl__.classObj);
}
__owl__.vnode = patch(elm, vnode);
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
this._callMounted();
this.__callMounted();
}
return __owl__.vnode;
}
/**
* Only called by qweb t-widget directive (when t-keepalive is set)
* Only called by qweb t-component directive (when t-keepalive is set)
*/
_remount() {
__remount() {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
__owl__.isMounted = true;
@@ -532,7 +544,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
}
_observeState() {
__observeState() {
if (this.state) {
const __owl__ = this.__owl__;
__owl__.observer = new Observer();
@@ -547,7 +559,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* Note that this method does not modify in place the props, it returns a new
* prop object
*/
_applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
__applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
props = props ? Object.create(props) : {};
for (let propName in defaultProps) {
if (props![propName] === undefined) {
@@ -561,10 +573,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* Apply the given patch queue. A patch is a pair [c, vn], where c is a
* Component instance and vn a VNode.
* 1) Call 'willPatch' on the component of each patch
* 2) Call '_patch' on the component of each patch
* 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in inverse order
*/
_applyPatchQueue(patchQueue: any[]) {
__applyPatchQueue(patchQueue: any[]) {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
@@ -572,7 +584,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch[0]._patch(patch[1]);
patch[0].__patch(patch[1]);
}
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i];
@@ -586,15 +598,13 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* 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) {
__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]}' (widget '${this.constructor.name}')`
);
throw new Error(`Missing props '${propsDef[i]}' (component '${this.constructor.name}')`);
}
}
} else if (propsDef) {
@@ -602,9 +612,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
for (let propName in propsDef) {
if (!(propName in props)) {
if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(
`Missing props '${propName}' (widget '${this.constructor.name}')`
);
throw new Error(`Missing props '${propName}' (component '${this.constructor.name}')`);
} else {
break;
}
@@ -612,9 +620,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
let isValid = isValidProp(props[propName], propsDef[propName]);
if (!isValid) {
throw new Error(
`Props '${propName}' of invalid type in widget '${
this.constructor.name
}'`
`Props '${propName}' of invalid type in component '${this.constructor.name}'`
);
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ export class EventBus {
* Add a listener for the 'eventType' events.
*
* Note that the 'owner' of this event can be anything, but will more likely
* be a widget or a class. The idea is that the callback will be called with
* be a component or a class. The idea is that the callback will be called with
* the proper owner bound.
*
* Also, the owner should be kind of unique. This will be used to remove the
+1 -1
View File
@@ -32,7 +32,7 @@ Object.defineProperty(__info__, "mode", {
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
);
} else {
console.log(`Owl is now running in 'prod' mode.`)
console.log(`Owl is now running in 'prod' mode.`);
}
}
});
+5 -22
View File
@@ -21,15 +21,7 @@
// 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 methodsToPatch = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
const methodLen = methodsToPatch.length;
const ArrayProto = Array.prototype;
@@ -78,18 +70,14 @@ export class Observer {
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"
);
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"
);
throw Error("`Observer.delete()` can only be called with observed Objects");
}
target.__owl__.observer.delete(target, key);
}
@@ -127,8 +115,7 @@ export class Observer {
set(target: any, key: number | string, value: any) {
let alreadyDefined =
key in target &&
Object.getOwnPropertyDescriptor(target, key)!.configurable === false;
key in target && Object.getOwnPropertyDescriptor(target, key)!.configurable === false;
if (alreadyDefined) {
target[key] = value;
} else {
@@ -172,11 +159,7 @@ export class Observer {
}
}
_addProp<T extends { __owl__?: any }>(
obj: T,
key: string | number,
value: any
) {
_addProp<T extends { __owl__?: any }>(obj: T, key: string | number, value: any) {
var self = this;
Object.defineProperty(obj, key, {
configurable: true,
+111 -93
View File
@@ -1,5 +1,6 @@
import { VNode, h } from "./vdom";
import { QWebVar, compileExpr } from "./qweb_expressions";
import { EventBus } from "./event_bus";
/**
* Owl QWeb Engine
@@ -58,14 +59,7 @@ export interface Directive {
// Const/global stuff/helpers
//------------------------------------------------------------------------------
const DISABLED_TAGS = [
"input",
"textarea",
"button",
"select",
"option",
"optgroup"
];
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
@@ -87,21 +81,27 @@ const NODE_HOOKS_PARAMS = {
interface Utils {
h: typeof h;
objectToAttrString(obj: Object): string;
toObj(expr: any): Object;
shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any;
}
export const UTILS: Utils = {
h: h,
objectToAttrString(obj: Object): string {
let classes: string[] = [];
for (let k in obj) {
if (obj[k]) {
classes.push(k);
toObj(expr) {
if (typeof expr === "string") {
expr = expr.trim();
if (!expr) {
return {};
}
let words = expr.split(/\s+/);
let result = {};
for (let i = 0; i < words.length; i++) {
result[words[i]] = true;
}
return result;
}
return classes.join(" ");
return expr;
},
shallowEqual(p1, p2) {
for (let k in p1) {
@@ -127,10 +127,10 @@ let nextID = 1;
//------------------------------------------------------------------------------
// QWeb rendering engine
//------------------------------------------------------------------------------
export class QWeb {
export class QWeb extends EventBus {
templates: { [name: string]: Template } = {};
utils = UTILS;
static widgets = Object.create(null);
static components = Object.create(null);
// dev mode enables better error messages or more costly validations
static dev: boolean = false;
@@ -140,12 +140,13 @@ export class QWeb {
// able to map a qweb instance to a template name.
id = nextID++;
// slots contains sub templates defined with t-set inside t-widget nodes, and
// slots contains sub templates defined with t-set inside t-component nodes, and
// are meant to be used by the t-slot directive.
slots = {};
nextSlotId = 1;
constructor(data?: string) {
super();
if (data) {
this.addTemplates(data);
}
@@ -161,17 +162,20 @@ export class QWeb {
}
static register(name: string, Component: any) {
if (QWeb.widgets[name]) {
if (QWeb.components[name]) {
throw new Error(`Component '${name}' has already been registered`);
}
QWeb.widgets[name] = Component;
QWeb.components[name] = Component;
}
/**
* Add a template to the internal template map. Note that it is not
* immediately compiled.
*/
addTemplate(name: string, xmlString: string) {
addTemplate(name: string, xmlString: string, allowDuplicate?: boolean) {
if (allowDuplicate && name in this.templates) {
return;
}
const doc = parseXML(xmlString);
if (!doc.firstChild) {
throw new Error("Invalid template (should not be empty)");
@@ -234,9 +238,7 @@ export class QWeb {
return a + b;
}) > 1
) {
throw new Error(
"Only one conditional branching directive is allowed per node"
);
throw new Error("Only one conditional branching directive is allowed per node");
}
// All text nodes between branch nodes are removed
let textNode;
@@ -266,9 +268,15 @@ export class QWeb {
return template.fn.call(this, context, extra);
}
_compile(name: string, elem: Element): CompiledTemplate {
_compile(name: string, elem: Element, parentNode?: number): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new Context(name);
if (parentNode) {
ctx.nextID = parentNode + 1;
ctx.parentNode = parentNode;
ctx.allowMultipleRoots = true;
ctx.addLine(`let c${parentNode} = extra.parentNode;`);
}
this._compileNode(elem, ctx);
if (ctx.shouldProtectContext) {
@@ -286,33 +294,30 @@ export class QWeb {
ctx.code.unshift(" let utils = this.utils;");
}
if (!ctx.rootNode) {
throw new Error("A template should have one root node");
if (!parentNode) {
if (!ctx.rootNode) {
throw new Error("A template should have one root node");
}
ctx.addLine(`return vn${ctx.rootNode};`);
}
ctx.addLine(`return vn${ctx.rootNode};`);
let template;
try {
template = new Function(
"context",
"extra",
ctx.code.join("\n")
) as CompiledTemplate;
template = new Function("context", "extra", ctx.code.join("\n")) as CompiledTemplate;
} catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(ctx.code.join("\n"));
console.groupEnd();
throw new Error(
`Invalid generated code while compiling template '${templateName}': ${
e.message
}`
`Invalid generated code while compiling template '${templateName}': ${e.message}`
);
}
if (isDebug) {
console.log(
`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` +
template.toString()
);
const tpl = this.templates[name];
if (tpl) {
const msg = `Template: ${tpl.elem.outerHTML}\nCompiled code:\n${template.toString()}`;
console.log(msg);
}
}
return template;
}
@@ -346,6 +351,12 @@ export class QWeb {
return;
}
const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
node.setAttribute("t-component", node.tagName);
}
const attributes = (<Element>node).attributes;
const validDirectives: {
@@ -384,7 +395,7 @@ export class QWeb {
fullName = name;
value = attributes[j].textContent;
validDirectives.push({ directive, value, fullName });
if (directive.name === "on" || directive.name === 'model') {
if (directive.name === "on" || directive.name === "model") {
withHandlers = true;
}
}
@@ -453,11 +464,7 @@ export class QWeb {
}
}
_compileGenericNode(
node: ChildNode,
ctx: Context,
withHandlers: boolean = true
): number {
_compileGenericNode(node: ChildNode, ctx: Context, withHandlers: boolean = true): number {
// nodeType 1 is generic tag
if (node.nodeType !== 1) {
throw new Error("unsupported node type");
@@ -481,34 +488,39 @@ export class QWeb {
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
isProp = true;
}
if (
(key === "readonly" && node.nodeName === "input") ||
node.nodeName === "textarea"
) {
if ((key === "readonly" && node.nodeName === "input") || node.nodeName === "textarea") {
isProp = true;
}
if (isProp) {
props.push(`${key}: _${val}`);
}
}
let classObj = "";
for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name;
const value = attributes[i].textContent!;
// regular attributes
if (
!name.startsWith("t-") &&
!(<Element>node).getAttribute("t-attf-" + name)
) {
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
const attID = ctx.generateID();
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
if (name === "class") {
let classDef = value
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
} else {
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
}
attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID);
}
attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID);
}
// dynamic attributes
@@ -516,32 +528,35 @@ export class QWeb {
let attName = name.slice(6);
const v = ctx.getValue(value);
let formattedValue = v.id || ctx.formatExpression(v);
if (
formattedValue[0] === "{" &&
formattedValue[formattedValue.length - 1] === "}"
) {
formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
if (attName === "class") {
formattedValue = `this.utils.toObj(${formattedValue})`;
if (classObj) {
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${formattedValue};`);
}
} else {
const attID = ctx.generateID();
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
// we need to combine dynamic with non dynamic attributes:
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
const attID = ctx.generateID();
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
// we need to combine dynamic with non dynamic attributes:
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att =>
att.startsWith(attName + ":")
);
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
if (name.startsWith("t-attf-")) {
@@ -582,6 +597,9 @@ export class QWeb {
if (props.length > 0) {
parts.push(`props:{${props.join(",")}}`);
}
if (classObj) {
parts.push(`class:${classObj}`);
}
if (withHandlers) {
parts.push(`on:{}`);
}
@@ -598,9 +616,7 @@ export class QWeb {
ctx.addLine(`}`);
ctx.closeIf();
}
ctx.addLine(
`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`
);
ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
}
@@ -639,6 +655,7 @@ export class Context {
inLoop: boolean = false;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
constructor(name?: string) {
this.rootContext = this;
@@ -652,7 +669,11 @@ export class Context {
}
withParent(node: number): Context {
if (this === this.rootContext && (this.parentNode || this.parentTextNode)) {
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) {
@@ -724,10 +745,7 @@ export class Context {
return `(${this.formatExpression(s.slice(2, -2))})`;
}
let r = s.replace(
/\{\{.*?\}\}/g,
s => "${" + this.formatExpression(s.slice(2, -2)) + "}"
);
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`";
}
}
+8 -17
View File
@@ -60,9 +60,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`);
let tempNodeID = ctx.generateID();
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),`);
ctx.addLine(`}};`);
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
@@ -116,9 +114,7 @@ QWeb.addDirective({
if (value) {
const formattedValue = ctx.formatExpression(value);
if (ctx.variables.hasOwnProperty(variable)) {
ctx.addLine(
`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`
);
ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`);
} else {
const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`);
@@ -220,9 +216,7 @@ QWeb.addDirective({
}
// compile sub template
const subCtx = ctx
.subContext("caller", nodeCopy)
.subContext("variables", Object.create(vars));
const subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
qweb._compileNode(nodeTemplate.elem, subCtx);
@@ -250,9 +244,7 @@ QWeb.addDirective({
const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID();
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine(
`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`
);
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
let keysID = ctx.generateID();
let valuesID = ctx.generateID();
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
@@ -269,10 +261,9 @@ QWeb.addDirective({
ctx.addLine(`context.${name} = _${keysID}[i];`);
ctx.addLine(`context.${name}_value = _${valuesID}[i];`);
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 (node.hasAttribute("t-widget") && !node.hasAttribute("t-key")) {
if (node.hasAttribute("t-component") && !node.hasAttribute("t-key")) {
shouldWarn = true;
}
if (
@@ -304,7 +295,7 @@ QWeb.addDirective({
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "debug",
priority: 99,
priority: 1,
atNodeEncounter({ ctx }) {
ctx.addLine("debugger;");
}
@@ -315,7 +306,7 @@ QWeb.addDirective({
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "log",
priority: 99,
priority: 1,
atNodeEncounter({ ctx, value }) {
const expr = ctx.formatExpression(value);
ctx.addLine(`console.log(${expr})`);
+3 -9
View File
@@ -82,7 +82,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
")": "RIGHT_PAREN"
};
const OPERATORS = ".,===,==,+,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(',');
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(",");
type Tokenizer = (expr: string) => Token | false;
@@ -231,10 +231,7 @@ export function tokenize(expr: string): Token[] {
* - or if the previous token is a left brace or a comma, and the next token is
* a colon (in that case, this is an object key: `{a: b}`)
*/
export function compileExpr(
expr: string,
vars: { [key: string]: QWebVar }
): string {
export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): string {
const tokens = tokenize(expr);
let result = "";
for (let i = 0; i < tokens.length; i++) {
@@ -246,10 +243,7 @@ export function compileExpr(
if (prevToken) {
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false;
} else if (
prevToken.type === "LEFT_BRACE" ||
prevToken.type === "COMMA"
) {
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
let nextToken = tokens[i + 1];
if (nextToken && nextToken.type === "COLON") {
isVar = false;
+143 -138
View File
@@ -10,7 +10,7 @@ import { VNode } from "./vdom";
* - t-on
* - t-ref
* - t-transition
* - t-widget/t-keepalive
* - t-component/t-keepalive
* - t-mounted
* - t-slot
* - t-model
@@ -49,9 +49,7 @@ QWeb.addDirective({
)}'\`)`
);
ctx.closeIf();
let params = extraArgs
? `owner, ${ctx.formatExpression(extraArgs)}`
: "owner";
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
@@ -70,9 +68,7 @@ QWeb.addDirective({
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
);
ctx.addLine(
`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`
);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
}
}
});
@@ -101,9 +97,7 @@ 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}']`);
const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
if (dup) {
dup.remove();
}
@@ -164,9 +158,7 @@ function toMs(s: string): number {
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 durations: Array<string> = (styles.transitionDuration || "").split(", ");
const timeout: number = getTimeout(delays, durations);
if (timeout > 0) {
elm.addEventListener("transitionend", cb, { once: true });
@@ -191,24 +183,24 @@ QWeb.addDirective({
});
//------------------------------------------------------------------------------
// t-widget
// t-component
//------------------------------------------------------------------------------
const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
self: "if (e.target !== vn.elm) {return}"
});
/**
* The t-widget 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
* you this advice: Good luck...
*
* Since it is not 'direct' code, but rather code that generates other code, it
* is not easy to understand. To help you, here is a detailed and commented
* explanation of the code generated by the t-widget directive for the following
* explanation of the code generated by the t-component directive for the following
* situation:
* ```xml
* <t t-widget="child"
* <Child
* t-key="'somestring'"
* flag="state.flag"
* t-transition="fade"/>
@@ -216,23 +208,23 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
*
* ```js
* // we assign utils on top of the function because it will be useful for
* // each widgets
* // each components
* let utils = this.utils;
*
* // this is the virtual node representing the parent div
* let c1 = [], p1 = { key: 1 };
* var vn1 = h("div", p1, c1);
*
* // t-widget directive: we start by evaluating the expression given by t-key:
* // t-component directive: we start by evaluating the expression given by t-key:
* let key5 = "somestring";
*
* // def3 is the promise that will contain later either the new widget
* // def3 is the promise that will contain later either the new component
* // creation, or the props update...
* let def3;
*
* // this is kind of tricky: we need here to find if the widget was already
* // this is kind of tricky: we need here to find if the component was already
* // created by a previous rendering. This is done by checking the internal
* // `cmap` (children map) of the parent widget: it maps keys to widget ids,
* // `cmap` (children map) of the parent component: it maps keys to component ids,
* // and, then, if there is an id, we look into the children list to get the
* // instance
* let w4 =
@@ -240,9 +232,9 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* ? context.__owl__.children[context.__owl__.cmap[key5]]
* : false;
*
* // We keep the index of the position of the widget in the closure. We push
* // null to reserve the slot, and will replace it later by the widget vnode,
* // when it will be ready (do not forget that preparing/rendering a widget is
* // We keep the index of the position of the component in the closure. We push
* // null to reserve the slot, and will replace it later by the component vnode,
* // when it will be ready (do not forget that preparing/rendering a component is
* // asynchronous)
* let _2_index = c1.length;
* c1.push(null);
@@ -252,7 +244,7 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* // computation, so it is certainly better to do it only once
* let props4 = { flag: context["state"].flag };
*
* // If we have a widget, currently rendering, but not ready yet, we do not want
* // If we have a component, currently rendering, but not ready yet, we do not want
* // to wait for it to be ready if we can avoid it
* if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
* // we check if the props are the same. In that case, we can simply reuse
@@ -260,7 +252,7 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
* def3 = w4.__owl__.renderPromise;
* } else {
* // if the props are not the same, we destroy the widget and starts anew.
* // if the props are not the same, we destroy the component and starts anew.
* // this will be faster than waiting for its rendering, then updating it
* w4.destroy();
* w4 = false;
@@ -268,29 +260,29 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* }
*
* if (!w4) {
* // in this situation, we need to create a new widget. First step is
* // in this situation, we need to create a new component. First step is
* // to get a reference to the class, then create an instance with
* // current context as parent, and the props.
* let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
* let W4 = context.component && context.components[componentKey4] || QWeb.component[componentKey4];
* if (!W4) {
* throw new Error("Cannot find the definition of widget 'child'");
* throw new Error("Cannot find the definition of component 'child'");
* }
* w4 = new W4(owner, props4);
*
* // Whenever we rerender the parent widget, we need to be sure that we
* // are able to find the widget instance. To do that, we register it to
* // Whenever we rerender the parent component, we need to be sure that we
* // are able to find the component instance. To do that, we register it to
* // the parent cmap (children map). Note that the 'template' key is
* // used here, since this is what identify the widget from the template
* // used here, since this is what identify the component from the template
* // perspective.
* context.__owl__.cmap[key5] = w4.__owl__.id;
*
* // _prepare is called, to basically call willStart, then render the
* // widget
* def3 = w4._prepare();
* // __prepare is called, to basically call willStart, then render the
* // component
* def3 = w4.__prepare();
*
* def3 = def3.then(vnode => {
* // we create here a virtual node for the parent (NOT the widget). This
* // we create here a virtual node for the parent (NOT the component). This
* // means that the vdom of the parent will be stopped here, and from
* // the parent's perspective, it simply is a vnode with no children.
* // However, it shares the same dom element with the component root
@@ -298,16 +290,16 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* let pvnode = h(vnode.sel, { key: key5 });
*
* // we add hooks to the parent vnode so we can interact with the new
* // widget at the proper time
* // component at the proper time
* pvnode.data.hook = {
* insert(vn) {
* // the _mount method will patch the widget vdom into the elm vn.elm,
* // the __mount method will patch the component vdom into the elm vn.elm,
* // then call the mounted hooks. However, suprisingly, the snabbdom
* // patch method actually replace the elm by a new elm, so we need
* // to synchronise the pvnode elm with the resulting elm
* let nvn = w4._mount(vnode, vn.elm);
* let nvn = w4.__mount(vnode, vn.elm);
* pvnode.elm = nvn.elm;
* // what follows is only present if there are animations on the widget
* // what follows is only present if there are animations on the component
* utils.transitionInsert(vn, "fade");
* },
* remove() {
@@ -317,7 +309,7 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* },
* destroy() {
* // if there are animations, we delay the call to destroy on the
* // widget, if not, we call it directly.
* // component, if not, we call it directly.
* let finalize = () => {
* w4.destroy();
* };
@@ -328,19 +320,19 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* c1[_2_index] = pvnode;
*
* // we keep here a reference to the parent vnode (representing the
* // widget, so we can reuse it later whenever we update the widget
* // component, so we can reuse it later whenever we update the component
* w4.__owl__.pvnode = pvnode;
* });
* } else {
* // this is the 'update' path of the directive.
* // the call to _updateProps is the actual widget update
* // the call to __updateProps is the actual component update
* // Note that we only update the props if we cannot reuse the previous
* // rendering work (in the case it was rendered with the same props)
* def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
* def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
* def3 = def3.then(() => {
* // if widget was destroyed in the meantime, we do nothing (so, this
* // if component was destroyed in the meantime, we do nothing (so, this
* // means that the parent's element children list will have a null in
* // the widget's position, which will cause the pvnode to be removed
* // the component's position, which will cause the pvnode to be removed
* // when it is patched.
* if (w4.__owl__.isDestroyed) {
* return;
@@ -360,11 +352,11 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
*/
QWeb.addDirective({
name: "widget",
name: "component",
extraNames: ["props", "keepalive", "asyncroot"],
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//WIDGET");
ctx.addLine("//COMPONENT");
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineUtils = true;
@@ -408,7 +400,7 @@ QWeb.addDirective({
.join(",");
let dummyID = ctx.generateID();
let defID = ctx.generateID();
let widgetID = ctx.generateID();
let componentID = ctx.generateID();
let keyID = key && ctx.generateID();
if (key) {
// we bind a variable to the key (could be a complex expression, so we
@@ -419,8 +411,18 @@ QWeb.addDirective({
let templateID = key
? `key${keyID}`
: ctx.inLoop
? `String(-${widgetID} - i)`
: String(widgetID);
? `String(-${componentID} - i)`
: String(componentID);
if (ctx.allowMultipleRoots) {
// necessary to prevent collisions
if (!key && ctx.inLoop) {
let id = ctx.generateID();
ctx.addLine(`let template${id} = "_slot_" + String(-${componentID} - i)`);
templateID = `template${id}`;
} else {
templateID = `"_slot_${templateID}"`;
}
}
let ref = node.getAttribute("t-ref");
let refExpr = "";
@@ -428,21 +430,19 @@ QWeb.addDirective({
if (ref) {
refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.refs[${refKey}] = w${widgetID};`;
refExpr = `context.refs[${refKey}] = w${componentID};`;
}
let transitionsInsertCode = "";
if (transition) {
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
}
let finalizeWidgetCode = `w${widgetID}.${
keepAlive ? "unmount" : "destroy"
}();`;
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
if (ref && !keepAlive) {
finalizeWidgetCode += `delete context.refs[${refKey}];`;
finalizeComponentCode += `delete context.refs[${refKey}];`;
}
if (transition) {
finalizeWidgetCode = `let finalize = () => {
${finalizeWidgetCode}
finalizeComponentCode = `let finalize = () => {
${finalizeComponentCode}
};
utils.transitionRemove(vn, '${transition}', finalize);`;
}
@@ -457,37 +457,50 @@ QWeb.addDirective({
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
tattStyle = attVar;
}
let updateClassCode = "";
let classObj = "";
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
let classCode = "";
if (classAttr) {
classCode =
classAttr
.split(" ")
.map(c => `vn.elm.classList.add('${c}')`)
.join(";") + ";";
let classDef = classAttr
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
if (tattClass) {
const attVar = `_${ctx.generateID()}`;
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`);
classCode = `for (let k in ${attVar}) {
if (${attVar}[k]) {
vn.elm.classList.add(k);
}
}`;
updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`;
let tattExpr = ctx.formatExpression(tattClass);
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
tattExpr = `this.utils.toObj(${tattExpr})`;
}
if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${tattExpr};`);
}
}
let eventsCode = events
.map(function([eventName, mods, handlerName, extraArgs]) {
let params = extraArgs
? `owner, ${ctx.formatExpression(extraArgs)}`
: "owner";
let params = "owner";
if (extraArgs) {
if (ctx.inLoop) {
let argId = ctx.generateID();
// we need to evaluate the arguments now, because the handler will
// be set asynchronously later when the widget is ready, and the
// context might be different.
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
params = `owner, arg${argId}`;
} else {
params = `owner, ${ctx.formatExpression(extraArgs)}`;
}
}
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
handler += mods
.map(function(mod) {
return T_WIDGET_MODS_CODE[mod];
return T_COMPONENT_MODS_CODE[mod];
})
.join("");
handler += `owner['${handlerName}'].call(${params}, e);}`;
@@ -499,116 +512,113 @@ QWeb.addDirective({
.join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}${eventsCode}}};`;
createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`;
}
ctx.addLine(
`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`
`let w${componentID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`
);
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
if (async) {
ctx.addLine(`const patchQueue${widgetID} = [];`);
ctx.addLine(`const patchQueue${componentID} = [];`);
ctx.addLine(
`c${
ctx.parentNode
}.push(w${widgetID} && w${widgetID}.__owl__.pvnode || null);`
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
);
} else {
ctx.addLine(`c${ctx.parentNode}.push(null);`);
}
ctx.addLine(`let props${widgetID} = {${propStr}};`);
ctx.addLine(`let props${componentID} = {${propStr}};`);
ctx.addIf(
`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode`
`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`
);
ctx.addIf(
`utils.shallowEqual(props${widgetID}, w${widgetID}.__owl__.renderProps)`
);
ctx.addLine(`def${defID} = w${widgetID}.__owl__.renderPromise;`);
ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`);
ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`);
ctx.addElse();
ctx.addLine(`w${widgetID}.destroy();`);
ctx.addLine(`w${widgetID} = false;`);
ctx.addLine(`w${componentID}.destroy();`);
ctx.addLine(`w${componentID} = false;`);
ctx.closeIf();
ctx.closeIf();
ctx.addIf(`!w${widgetID}`);
// new widget
ctx.addLine(`let widgetKey${widgetID} = ${ctx.interpolate(value)};`);
ctx.addIf(`!w${componentID}`);
// new component
ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`);
ctx.addLine(
`let W${widgetID} = context.widgets && context.widgets[widgetKey${widgetID}] || QWeb.widgets[widgetKey${widgetID}];`
`let W${componentID} = context.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
);
// maybe only do this in dev mode...
ctx.addLine(
`if (!W${widgetID}) {throw new Error('Cannot find the definition of widget "' + widgetKey${widgetID} + '"')}`
);
ctx.addLine(`w${widgetID} = new W${widgetID}(owner, props${widgetID});`);
ctx.addLine(
`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
);
ctx.addLine(`w${componentID} = new W${componentID}(owner, props${componentID});`);
ctx.addLine(`context.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`);
// SLOTS
if (node.childElementCount) {
if (node.childNodes.length) {
const clone = <Element>node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]");
const slotId = qweb.nextSlotId++;
ctx.addLine(`w${widgetID}.__owl__.slotId = ${slotId};`);
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) {
const slotNode = slotNodes[i];
slotNode.parentElement!.removeChild(slotNode);
const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set");
const slotFn = qweb._compile(`slot_${key}_template`, slotNode);
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx.parentNode!);
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
}
}
if (clone.childElementCount) {
const content = clone.children[0];
const slotFn = qweb._compile(`slot_default_template`, content);
if (clone.childNodes.length) {
const t = clone.ownerDocument!.createElement("t");
for (let child of Object.values(clone.childNodes)) {
t.appendChild(child);
}
const slotFn = qweb._compile(`slot_default_template`, t, ctx.parentNode!);
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
}
}
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
ctx.addLine(`def${defID} = w${componentID}.__prepare();`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});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}}}});c${
ctx.parentNode
}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`
}[_${dummyID}_index]=pvnode;w${componentID}.__owl__.pvnode = pvnode;});`
);
ctx.addElse();
// need to update widget
const patchQueueCode = async ? `patchQueue${widgetID}` : "extra.patchQueue";
// need to update component
const patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
ctx.addLine(
`def${defID} = def${defID} || w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, ${patchQueueCode});`
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode});`
);
let keepAliveCode = "";
if (keepAlive) {
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}._remount();};`;
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
}
ctx.addLine(
`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${
tattStyle ? `w${widgetID}.el.style=${tattStyle};` : ""
}${updateClassCode}let pvnode=w${widgetID}.__owl__.pvnode;${keepAliveCode}c${
`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${
tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""
}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}c${
ctx.parentNode
}[_${dummyID}_index]=pvnode;});`
);
ctx.closeIf();
if (classObj) {
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
}
if (async) {
ctx.addLine(
`def${defID}.then(w${widgetID}._applyPatchQueue.bind(w${widgetID}, patchQueue${widgetID}));`
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`
);
} else {
ctx.addLine(`extra.promises.push(def${defID});`);
}
if (
node.hasAttribute("t-if") ||
node.hasAttribute("t-else") ||
node.hasAttribute("t-elif")
) {
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
ctx.closeIf();
}
@@ -648,10 +658,7 @@ QWeb.addDirective({
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
);
}
addNodeHook(
"insert",
`if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`
);
addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
}
});
@@ -663,12 +670,14 @@ QWeb.addDirective({
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(
`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`
);
ctx.addLine(
`c${ctx.parentNode}.push(slot${slotKey}(context.__owl__.parent, extra));`
`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
ctx.parentNode
}}));`
);
ctx.closeIf();
return true;
}
});
@@ -697,9 +706,7 @@ QWeb.addDirective({
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}'};`
);
ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`);
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
event = "click";
} else {
@@ -715,8 +722,6 @@ QWeb.addDirective({
ctx.addLine(
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
);
ctx.addLine(
`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`
);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
}
});
+56 -31
View File
@@ -21,8 +21,8 @@ import { Observer } from "./observer";
// Store Definition
//------------------------------------------------------------------------------
type Mutation = ({ state, commit, getters }, payload: any) => void;
type Action = ({ commit, state, dispatch, env, getters }, payload: any) => void;
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 {
@@ -48,6 +48,7 @@ export class Store extends EventBus {
observer: Observer;
getters: { [name: string]: (payload?) => any };
_gettersCache: { [name: string]: {} };
_updateId: number = 1;
constructor(config: StoreConfig, options: StoreOption = {}) {
super();
@@ -57,10 +58,7 @@ export class Store extends EventBus {
this.mutations = config.mutations;
this.env = config.env;
this.observer = new Observer();
this.observer.notifyCB = () => {
this._gettersCache = {};
this.trigger("update");
};
this.observer.notifyCB = this.__notifyComponents.bind(this);
this.observer.allowMutations = false;
this.observer.observe(this.state);
this.getters = {};
@@ -87,7 +85,7 @@ export class Store extends EventBus {
}
}
dispatch(action: string, payload?: any): Promise<void> | void {
dispatch(action: string, ...payload: any): Promise<void> | void {
if (!this.actions[action]) {
throw new Error(`[Error] action ${action} is undefined`);
}
@@ -99,7 +97,7 @@ export class Store extends EventBus {
state: this.state,
getters: this.getters
},
payload
...payload
);
if (result instanceof Promise) {
return new Promise((resolve, reject) => {
@@ -109,7 +107,7 @@ export class Store extends EventBus {
}
}
commit(type: string, payload?: any): any {
commit(type: string, ...payload: any): any {
if (!this.mutations[type]) {
throw new Error(`[Error] mutation ${type} is undefined`);
}
@@ -123,7 +121,7 @@ export class Store extends EventBus {
state: this.state,
getters: this.getters
},
payload
...payload
);
if (this._commitLevel === 1) {
@@ -132,13 +130,41 @@ export class Store extends EventBus {
this.history.push({
state: this.state,
mutation: type,
payload: payload
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);
}
}
}
}
//------------------------------------------------------------------------------
@@ -170,8 +196,6 @@ interface StoreOptions {
deep?: boolean;
}
let nextID = 1;
export function connect<E extends EnvWithStore, P, S>(
Comp: Constructor<Component<E, P, S>>,
mapStoreToProps,
@@ -232,16 +256,19 @@ export function connect<E extends EnvWithStore, P, S>(
* 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();
__callMounted() {
(<any>this.__owl__).store.on("update", this, this.__checkUpdate);
super.__callMounted();
}
willUnmount() {
(<any>this.__owl__).store.off("update", this);
super.willUnmount();
}
_checkUpdate() {
async __checkUpdate(updateId) {
if (updateId === (<any>this.__owl__).currentUpdateId) {
return;
}
const ownProps = (<any>this.__owl__).ownProps;
const storeProps = mapStoreToProps(
(<any>this.__owl__).store.state,
@@ -267,24 +294,22 @@ export function connect<E extends EnvWithStore, P, S>(
}
if (didChange) {
(<any>this.__owl__).currentStoreProps = storeProps;
this._updateProps(ownProps, false);
await this.__updateProps(ownProps, false);
}
}
_updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
if ((<any>this.__owl__).ownProps !== nextProps) {
(<any>this.__owl__).currentStoreProps = mapStoreToProps(
(<any>this.__owl__).store.state,
__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,
(<any>this.__owl__).store.getters
__owl__.store.getters
);
}
(<any>this.__owl__).ownProps = nextProps;
const mergedProps = Object.assign(
{},
nextProps,
(<any>this.__owl__).currentStoreProps
);
return super._updateProps(mergedProps, forceUpdate, patchQueue);
__owl__.ownProps = nextProps;
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
return super.__updateProps(mergedProps, forceUpdate, patchQueue);
}
};
@@ -292,7 +317,7 @@ export function connect<E extends EnvWithStore, P, S>(
// 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 = `ConnectedComponent${nextID++}`;
let name = `Connected${Comp.name}`;
Object.defineProperty(Result, "name", { value: name });
return Result;
}
+2 -6
View File
@@ -17,7 +17,7 @@ export function whenReady(fn) {
} else {
document.addEventListener("DOMContentLoaded", resolve, false);
}
}).then(fn || function () {});
}).then(fn || function() {});
}
const loadedScripts: { [key: string]: Promise<void> } = {};
@@ -76,11 +76,7 @@ export function escape(str: string | number | undefined): string {
*
* Inspired by https://davidwalsh.name/javascript-debounce-function
*/
export function debounce(
func: Function,
wait: number,
immediate?: boolean
): Function {
export function debounce(func: Function, wait: number, immediate?: boolean): Function {
let timeout;
return function(this: any) {
const context = this;
+61 -114
View File
@@ -59,7 +59,7 @@ function vnode(
elm: Element | Text | undefined
): VNode {
let key = data === undefined ? undefined : data.key;
return {sel, data, children, text, elm, key};
return { sel, data, children, text, elm, key };
}
//------------------------------------------------------------------------------
@@ -109,14 +109,7 @@ function createKeyToOldIdx(
return map;
}
const hooks: (keyof Module)[] = [
"create",
"update",
"remove",
"destroy",
"pre",
"post"
];
const hooks: (keyof Module)[] = ["create", "update", "remove", "destroy", "pre", "post"];
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number,
@@ -138,13 +131,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
function emptyNodeAt(elm: Element) {
const id = elm.id ? "#" + elm.id : "";
const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
return vnode(
api.tagName(elm).toLowerCase() + id + c,
{},
[],
undefined,
elm
);
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
}
function createRmCb(childElm: Node, listeners: number) {
@@ -179,19 +166,14 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
const dotIdx = sel.indexOf(".", hashIdx);
const hash = hashIdx > 0 ? hashIdx : sel.length;
const dot = dotIdx > 0 ? dotIdx : sel.length;
const tag =
hashIdx !== -1 || dotIdx !== -1
? sel.slice(0, Math.min(hash, dot))
: sel;
const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
const elm = (vnode.elm =
isDef(data) && isDef((i = (data as VNodeData).ns))
? api.createElementNS(i, tag)
: api.createElement(tag));
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
if (dotIdx > 0)
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
for (i = 0, iLen = cbs.create.length; i < iLen; ++i)
cbs.create[i](emptyNode, vnode);
if (dotIdx > 0) elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
for (i = 0, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode);
if (array(children)) {
for (i = 0, iLen = children.length; i < iLen; ++i) {
const ch = children[i];
@@ -237,8 +219,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
data = vnode.data;
if (data !== undefined) {
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode);
for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i)
cbs.destroy[i](vnode);
for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i) cbs.destroy[i](vnode);
if (vnode.children !== undefined) {
for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
i = vnode.children[j];
@@ -267,13 +248,8 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
invokeDestroyHook(ch);
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm as Node, listeners);
for (i = 0, iLen = cbs.remove.length; i < iLen; ++i)
cbs.remove[i](ch, rm);
if (
isDef((i = ch.data)) &&
isDef((i = i.hook)) &&
isDef((i = i.remove))
) {
for (i = 0, iLen = cbs.remove.length; i < iLen; ++i) cbs.remove[i](ch, rm);
if (isDef((i = ch.data)) && isDef((i = i.hook)) && isDef((i = i.remove))) {
i(ch, rm);
} else {
rm();
@@ -335,11 +311,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldEndVnode.elm as Node,
oldStartVnode.elm as Node
);
api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
@@ -366,11 +338,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(
parentElm,
elmToMove.elm as Node,
oldStartVnode.elm as Node
);
api.insertBefore(parentElm, elmToMove.elm as Node, oldStartVnode.elm as Node);
}
newStartVnode = newCh[++newStartIdx];
}
@@ -379,31 +347,16 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
let i: any, iLen: number, hook: any;
if (
isDef((i = vnode.data)) &&
isDef((hook = i.hook)) &&
isDef((i = hook.prepatch))
) {
if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) {
i(oldVnode, vnode);
}
const elm = (vnode.elm = oldVnode.elm as Node);
@@ -411,20 +364,14 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let ch = vnode.children;
if (oldVnode === vnode) return;
if (vnode.data !== undefined) {
for (i = 0, iLen = cbs.update.length; i < iLen; ++i)
cbs.update[i](oldVnode, vnode);
for (i = 0, iLen = cbs.update.length; i < iLen; ++i) cbs.update[i](oldVnode, vnode);
i = vnode.data.hook;
if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch)
updateChildren(
elm,
oldCh as Array<VNode>,
ch as Array<VNode>,
insertedVnodeQueue
);
updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
addVnodes(
@@ -436,23 +383,13 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
insertedVnodeQueue
);
} else if (isDef(oldCh)) {
removeVnodes(
elm,
oldCh as Array<VNode>,
0,
(oldCh as Array<VNode>).length - 1
);
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, "");
}
} else if (oldVnode.text !== vnode.text) {
if (isDef(oldCh)) {
removeVnodes(
elm,
oldCh as Array<VNode>,
0,
(oldCh as Array<VNode>).length - 1
);
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
}
api.setTextContent(elm, vnode.text as string);
}
@@ -510,11 +447,7 @@ interface DOMAPI {
createElementNS: (namespaceURI: string, qualifiedName: string) => Element;
createTextNode: (text: string) => Text;
createComment: (text: string) => Comment;
insertBefore: (
parentNode: Node,
newNode: Node,
referenceNode: Node | null
) => void;
insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void;
removeChild: (node: Node, child: Node) => void;
appendChild: (node: Node, child: Node) => void;
parentNode: (node: Node) => Node;
@@ -543,11 +476,7 @@ function createComment(text: string): Comment {
return document.createComment(text);
}
function insertBefore(
parentNode: Node,
newNode: Node,
referenceNode: Node | null
): void {
function insertBefore(parentNode: Node, newNode: Node, referenceNode: Node | null): void {
parentNode.insertBefore(newNode, referenceNode);
}
@@ -644,21 +573,13 @@ type VNodeChildElement = VNode | string | number | undefined | null;
type ArrayOrElement<T> = T | T[];
type VNodeChildren = ArrayOrElement<VNodeChildElement>;
function addNS(
data: any,
children: VNodes | undefined,
sel: string | undefined
): void {
function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) {
for (let i = 0, iLen = children.length; i < iLen; ++i) {
let childData = children[i].data;
if (childData !== undefined) {
addNS(
childData,
(children[i] as VNode).children as VNodes,
children[i].sel
);
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
}
}
}
@@ -697,13 +618,7 @@ export function h(sel: any, b?: any, c?: any): VNode {
if (children !== undefined) {
for (i = 0, iLen = children.length; i < iLen; ++i) {
if (primitive(children[i]))
children[i] = vnode(
undefined,
undefined,
undefined,
children[i],
undefined
);
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
if (
@@ -769,9 +684,7 @@ interface Module {
//------------------------------------------------------------------------------
// module/eventlisteners.ts
//------------------------------------------------------------------------------
type On = {
[N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void
} & {
type On = { [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void } & {
[event: string]: EventListener;
};
@@ -850,8 +763,7 @@ function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
// 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());
var listener = ((vnode as any).listener = (oldVnode as any).listener || createListener());
// update vnode for listener
listener.vnode = vnode;
@@ -938,4 +850,39 @@ export const attrsModule = {
update: updateAttrs
} as Module;
export const patch = init([eventListenersModule, attrsModule, propsModule]);
//------------------------------------------------------------------------------
// 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]);
+16 -16
View File
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`animations t-transition combined with t-widget 1`] = `
exports[`animations t-transition combined with component 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
@@ -9,7 +9,7 @@ exports[`animations t-transition combined with t-widget 1`] = `
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
//COMPONENT
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
@@ -24,18 +24,18 @@ exports[`animations t-transition combined with t-widget 1`] = `
}
}
if (!w4) {
let widgetKey4 = \`Child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
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 + '\\"')}
w4 = new W4(owner, props4);
context.__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;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
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 = () => {
w4.destroy();
};
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
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);
@@ -43,7 +43,7 @@ exports[`animations t-transition combined with t-widget 1`] = `
}"
`;
exports[`animations t-transition combined with t-widget and t-if 1`] = `
exports[`animations t-transition combined with t-component and t-if 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
@@ -53,7 +53,7 @@ exports[`animations t-transition combined with t-widget and t-if 1`] = `
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].display) {
//WIDGET
//COMPONENT
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let _2_index = c1.length;
@@ -68,18 +68,18 @@ exports[`animations t-transition combined with t-widget and t-if 1`] = `
}
}
if (!w4) {
let widgetKey4 = \`Child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
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 + '\\"')}
w4 = new W4(owner, props4);
context.__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;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
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 = () => {
w4.destroy();
};
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
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);
File diff suppressed because it is too large Load Diff
+14 -16
View File
@@ -128,8 +128,8 @@ exports[`attributes from object variables set previously 1`] = `
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = {a:'b'};
var _3 = _2.a;
let c4 = [], p4 = {key:4,attrs:{class: _3}};
let _3 = this.utils.toObj(_2.a);
let c4 = [], p4 = {key:4,class:_3};
var vn4 = h('span', p4, c4);
c1.push(vn4);
return vn1;
@@ -143,8 +143,8 @@ exports[`attributes from variables set previously 1`] = `
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 'def';
var _3 = _2;
let c4 = [], p4 = {key:4,attrs:{class: _3}};
let _3 = this.utils.toObj(_2);
let c4 = [], p4 = {key:4,class:_3};
var vn4 = h('span', p4, c4);
c1.push(vn4);
return vn1;
@@ -209,12 +209,11 @@ exports[`attributes t-att-class and class should combine together 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var _1 = 'hello';
var _3 = context['value'];
var _2 = 'hello' + (_3 ? ' ' + _3 : '');
let c4 = [], p4 = {key:4,attrs:{class: _2}};
var vn4 = h('div', p4, c4);
return vn4;
let _2 = {'hello':true};
Object.assign(_2, this.utils.toObj(context['value']))
let c3 = [], p3 = {key:3,class:_2};
var vn3 = h('div', p3, c3);
return vn3;
}"
`;
@@ -222,12 +221,11 @@ exports[`attributes t-att-class with object 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var _1 = 'static';
var _3 = this.utils.objectToAttrString({a:context['b'],c:context['d'],e:context['f']});
var _2 = 'static' + (_3 ? ' ' + _3 : '');
let c4 = [], p4 = {key:4,attrs:{class: _2}};
var vn4 = h('div', p4, c4);
return vn4;
let _2 = {'static':true};
Object.assign(_2, this.utils.toObj({a:context['b'],c:context['d'],e:context['f']}))
let c3 = [], p3 = {key:3,class:_2};
var vn3 = h('div', p3, c3);
return vn3;
}"
`;
-33
View File
@@ -1,33 +0,0 @@
function anonymous(context, extra) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].display) {
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let W4 = context.widgets['Child'];
if (!W4) {throw new Error(`Cannot find the definition of widget "Child"`)}
w4 = new W4(owner, props4);
context.__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;this.utils.transitionInsert(vn.elm, 'chimay');},remove: () => {},destroy: (vn) => {let finalize = () => {
w4.destroy();
};
this.utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
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);
}
}
+16 -31
View File
@@ -18,7 +18,7 @@ import {
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - qweb: a new QWeb instance
// - env: a WEnv, necessary to create new widgets
// - env: a WEnv, necessary to create new components
// - cssEl: a stylesheet injected into the dom
let fixture: HTMLElement;
@@ -170,16 +170,13 @@ describe("animations", () => {
expect(spanNode.className).toBe("");
});
test("t-transition combined with t-widget", async () => {
test("t-transition combined with component", async () => {
expect.assertions(5);
env.qweb.addTemplate(
"Parent",
`<div><t t-widget="Child" t-transition="chimay"/></div>`
);
env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`);
env.qweb.addTemplate("Child", `<span>blue</span>`);
class Parent extends Widget {
widgets = { Child: Child };
components = { Child: Child };
}
class Child extends Widget {}
const widget = new Parent(env);
@@ -209,16 +206,16 @@ describe("animations", () => {
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
});
test("t-transition combined with t-widget and t-if", async () => {
test("t-transition combined with t-component and t-if", async () => {
expect.assertions(8);
env.qweb.addTemplate(
"Parent",
`<div><t t-if="state.display" t-widget="Child" t-transition="chimay"/></div>`
`<div><t t-if="state.display" t-component="Child" t-transition="chimay"/></div>`
);
env.qweb.addTemplate("Child", `<span>blue</span>`);
class Parent extends Widget {
widgets = { Child: Child };
components = { Child: Child };
state = { display: true };
}
class Child extends Widget {}
@@ -320,9 +317,7 @@ describe("animations", () => {
expect(spans.length).toBe(1);
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
cb();
expect(spans[0].className).toBe(
`chimay-${phase}-active chimay-${phase}-to`
);
expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
def.resolve();
});
@@ -330,9 +325,7 @@ describe("animations", () => {
button!.click();
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe(
'<div><button>Toggle</button><span class="">blue</span></div>'
);
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
// click to remove the span, and click again to re-add it before transitionend
def = makeDeferred();
@@ -346,26 +339,24 @@ describe("animations", () => {
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe(
'<div><button>Toggle</button><span class="">blue</span></div>'
);
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
});
test("t-transition combined with t-widget, remove and re-add before transitionend", async () => {
test("t-transition combined with t-component, remove and re-add before transitionend", async () => {
expect.assertions(11);
env.qweb.addTemplates(
`<templates>
<div t-name="Parent">
<button t-on-click="toggle">Toggle</button>
<t t-if="state.flag" t-widget="Child" t-transition="chimay"/>
<t t-if="state.flag" t-component="Child" t-transition="chimay"/>
</div>
<span t-name="Child">blue</span>
</templates>`
);
class Child extends Widget {}
class Parent extends Widget {
widgets = { Child };
components = { Child };
constructor(parent) {
super(parent);
this.state = { flag: false };
@@ -386,9 +377,7 @@ describe("animations", () => {
expect(spans.length).toBe(1);
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
cb();
expect(spans[0].className).toBe(
`chimay-${phase}-active chimay-${phase}-to`
);
expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
def.resolve();
});
@@ -396,9 +385,7 @@ describe("animations", () => {
button!.click();
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe(
'<div><button>Toggle</button><span class="">blue</span></div>'
);
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
// click to remove the span, and click again to re-add it before transitionend
def = makeDeferred();
@@ -412,8 +399,6 @@ describe("animations", () => {
await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe(
'<div><button>Toggle</button><span class="">blue</span></div>'
);
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
});
});
+605 -430
View File
File diff suppressed because it is too large Load Diff
+4 -9
View File
@@ -52,15 +52,11 @@ interface FileData {
sections: MarkDownSection[];
}
function isLinkValid(
link: MarkDownLink,
current: FileData,
files: FileData[]
): boolean {
function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean {
const parts = link.link.split("#");
const currentParts = current.name.split("/");
const path = currentParts.length > 1 ? currentParts[0] + '/' : "";
const fullName = path + parts[0];
const path = currentParts.length > 1 ? currentParts[0] + "/" : "";
const fullName = path + parts[0];
if (parts.length === 1) {
// no # in url
if (parts[0].endsWith(".md")) {
@@ -70,8 +66,7 @@ function isLinkValid(
}
}
} else {
const file =
parts[0] === "" ? current : files.find(f => f.name === fullName);
const file = parts[0] === "" ? current : files.find(f => f.name === fullName);
if (!file) {
return false;
}
+1 -3
View File
@@ -23,9 +23,7 @@ describe("event bus behaviour", () => {
test("throw error if callback is undefined", () => {
expect.assertions(1);
const bus = new EventBus();
expect(() => bus.on("event", {}, <any>undefined)).toThrow(
`Missing callback`
);
expect(() => bus.on("event", {}, <any>undefined)).toThrow(`Missing callback`);
});
test("can unsubscribe", () => {
+2 -9
View File
@@ -68,11 +68,7 @@ export function renderToDOM(
return result.elm as HTMLElement;
}
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);
return node instanceof Text ? node.textContent! : node.outerHTML;
}
@@ -92,10 +88,7 @@ export function unpatchNextFrame() {
UTILS.nextFrame = nextFrame;
}
export async function editInput(
input: HTMLInputElement | HTMLTextAreaElement,
value: string
) {
export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) {
input.value = value;
input.dispatchEvent(new Event("input"));
input.dispatchEvent(new Event("change"));
+14 -19
View File
@@ -52,9 +52,9 @@ describe("props validation", () => {
}
const w = new TestWidget(env, { message: "bottle" });
try {
await w._updateProps({});
await w.__updateProps({});
} catch (e) {
expect(e.message).toBe("Missing props 'message' (widget 'TestWidget')");
expect(e.message).toBe("Missing props 'message' (component 'TestWidget')");
}
});
@@ -65,7 +65,7 @@ describe("props validation", () => {
expect(() => {
new TestWidget(env);
}).toThrow("Missing props 'message' (widget 'TestWidget')");
}).toThrow("Missing props 'message' (component 'TestWidget')");
});
test("validate simple types", async () => {
@@ -93,7 +93,7 @@ describe("props validation", () => {
expect(() => {
new TestWidget(env, { p: test.ko });
}).toThrow("Props 'p' of invalid type in widget");
}).toThrow("Props 'p' of invalid type in component");
}
});
@@ -122,11 +122,10 @@ describe("props validation", () => {
expect(() => {
new TestWidget(env, { p: test.ko });
}).toThrow("Props 'p' of invalid type in widget");
}).toThrow("Props 'p' of invalid type in component");
}
});
test("can validate a prop with multiple types", async () => {
let TestWidget = class extends Widget {
static props = { p: [String, Boolean] };
@@ -139,7 +138,7 @@ describe("props validation", () => {
expect(() => {
new TestWidget(env, { p: 1 });
}).toThrow("Props 'p' of invalid type in widget");
}).toThrow("Props 'p' of invalid type in component");
});
test("can validate an optional props", async () => {
@@ -149,16 +148,14 @@ describe("props validation", () => {
expect(() => {
new TestWidget(env, { p: "hey" });
new TestWidget(env, { });
new TestWidget(env, {});
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: 1 });
}).toThrow();
});
test("can validate an array with given primitive type", async () => {
let TestWidget = class extends Widget {
static props = { p: { type: Array, element: String } };
@@ -222,7 +219,7 @@ describe("props validation", () => {
type: Object,
shape: {
id: Number,
url: [Boolean, {type: Array, element: Number}],
url: [Boolean, { type: Array, element: Number }]
}
}
};
@@ -230,21 +227,19 @@ describe("props validation", () => {
expect(() => {
new TestWidget(env, { p: { id: 1, url: true } });
new TestWidget(env, { p: { id: 1, url: [12]} });
new TestWidget(env, { p: { id: 1, url: [12] } });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: { id: 1, url: [12, true]} });
new TestWidget(env, { p: { id: 1, url: [12, true] } });
}).toThrow();
});
});
describe("default props", () => {
test("can set default values", async () => {
class TestWidget extends Widget {
static defaultProps = {p: 4}
static defaultProps = { p: 4 };
}
const w = new TestWidget(env, {});
@@ -253,11 +248,11 @@ describe("default props", () => {
test("default values are also set whenever component is updated", async () => {
class TestWidget extends Widget {
static defaultProps = {p: 4}
static defaultProps = { p: 4 };
}
const w = new TestWidget(env, {p: 1});
await w._updateProps({});
const w = new TestWidget(env, { p: 1 });
await w.__updateProps({});
expect(w.props.p).toBe(4);
});
});
+38 -109
View File
@@ -25,13 +25,13 @@ describe("static templates", () => {
});
test("simple dynamic value", () => {
qweb.addTemplate("test", "<t><t t-esc=\"text\"/></t>");
expect(renderToString(qweb, "test", {text: "hello vdom"})).toBe("hello vdom");
qweb.addTemplate("test", '<t><t t-esc="text"/></t>');
expect(renderToString(qweb, "test", { text: "hello vdom" })).toBe("hello vdom");
});
test("simple string, with some dynamic value", () => {
qweb.addTemplate("test", "<t>hello <t t-esc=\"text\"/></t>");
expect(renderToString(qweb, "test", {text: "vdom"})).toBe("hello vdom");
qweb.addTemplate("test", '<t>hello <t t-esc="text"/></t>');
expect(renderToString(qweb, "test", { text: "vdom" })).toBe("hello vdom");
});
test("empty div", () => {
@@ -52,9 +52,7 @@ describe("static templates", () => {
describe("error handling", () => {
test("invalid xml", () => {
expect(() => qweb.addTemplate("test", "<div>")).toThrow(
"Invalid XML in template"
);
expect(() => qweb.addTemplate("test", "<div>")).toThrow("Invalid XML in template");
});
test("template with text node and tag", () => {
@@ -71,6 +69,7 @@ describe("error handling", () => {
test("cannot add twice the same template", () => {
qweb.addTemplate("test", `<t></t>`);
expect(() => qweb.addTemplate("test", "<div/>", true)).not.toThrow("already defined");
expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined");
});
@@ -89,19 +88,14 @@ describe("error handling", () => {
test("nice error when t-on is evaluated with a missing event", () => {
qweb.addTemplate("templatename", `<div t-on="somemethod"></div>`);
expect(() =>
qweb.render("templatename", { someMethod() {} }, { handlers: [] })
).toThrow("Missing event name with t-on directive");
expect(() => qweb.render("templatename", { someMethod() {} }, { handlers: [] })).toThrow(
"Missing event name with t-on directive"
);
});
test("error when unknown directive", () => {
qweb.addTemplate(
"templatename",
`<div t-best-beer="rochefort 10">test</div>`
);
expect(() => qweb.render("templatename")).toThrow(
"Unknown QWeb directive: 't-best-beer'"
);
qweb.addTemplate("templatename", `<div t-best-beer="rochefort 10">test</div>`);
expect(() => qweb.render("templatename")).toThrow("Unknown QWeb directive: 't-best-beer'");
});
});
@@ -118,9 +112,7 @@ describe("t-esc", () => {
test.skip("escaping", () => {
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<ok>" })).toBe(
"<span>&lt;ok&gt;</span>"
);
expect(renderToString(qweb, "test", { var: "<ok>" })).toBe("<span>&lt;ok&gt;</span>");
});
test("escaping on a node", () => {
@@ -152,9 +144,7 @@ describe("t-raw", () => {
test("not escaping", () => {
qweb.addTemplate("test", `<div><t t-raw="var"/></div>`);
expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe(
"<div><ok></ok></div>"
);
expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe("<div><ok></ok></div>");
});
test("t-raw and another sibling node", () => {
@@ -167,10 +157,7 @@ describe("t-raw", () => {
describe("t-set", () => {
test("set from attribute literal", () => {
qweb.addTemplate(
"test",
`<div><t t-set="value" t-value="'ok'"/><t t-esc="value"/></div>`
);
qweb.addTemplate("test", `<div><t t-set="value" t-value="'ok'"/><t t-esc="value"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>ok</div>");
});
@@ -182,24 +169,16 @@ describe("t-set", () => {
<t t-if="v === 'ok'">grimbergen</t>
</div>`
);
expect(renderToString(qweb, "test", { value: "ok" })).toBe(
"<div>grimbergen</div>"
);
expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>grimbergen</div>");
});
test("set from body literal", () => {
qweb.addTemplate(
"test",
`<t><t t-set="value">ok</t><t t-esc="value"/></t>`
);
qweb.addTemplate("test", `<t><t t-set="value">ok</t><t t-esc="value"/></t>`);
expect(renderToString(qweb, "test")).toBe("ok");
});
test("set from attribute lookup", () => {
qweb.addTemplate(
"test",
`<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`
);
qweb.addTemplate("test", `<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`);
expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>ok</div>");
});
@@ -231,18 +210,12 @@ describe("t-set", () => {
});
test("value priority", () => {
qweb.addTemplate(
"test",
`<div><t t-set="value" t-value="1">2</t><t t-esc="value"/></div>`
);
qweb.addTemplate("test", `<div><t t-set="value" t-value="1">2</t><t t-esc="value"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>1</div>");
});
test("evaluate value expression", () => {
qweb.addTemplate(
"test",
`<div><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></div>`
);
qweb.addTemplate("test", `<div><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>3</div>");
});
@@ -267,25 +240,19 @@ describe("t-set", () => {
"test",
`<div><t t-set="value" t-value="somevariable + 2"/><t t-esc="value"/></div>`
);
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe(
"<div>45</div>"
);
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe("<div>45</div>");
});
});
describe("t-if", () => {
test("boolean value true condition", () => {
qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`);
expect(renderToString(qweb, "test", { condition: true })).toBe(
"<div>ok</div>"
);
expect(renderToString(qweb, "test", { condition: true })).toBe("<div>ok</div>");
});
test("boolean value false condition", () => {
qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`);
expect(renderToString(qweb, "test", { condition: false })).toBe(
"<div></div>"
);
expect(renderToString(qweb, "test", { condition: false })).toBe("<div></div>");
});
test("boolean value condition missing", () => {
@@ -302,9 +269,7 @@ describe("t-if", () => {
<t t-else="">beer</t></div>
`
);
expect(renderToString(qweb, "test", { color: "red" })).toBe(
"<div>red is dead</div>"
);
expect(renderToString(qweb, "test", { color: "red" })).toBe("<div>red is dead</div>");
});
test("boolean value condition else", () => {
@@ -330,9 +295,7 @@ describe("t-if", () => {
`
);
const result = trim(renderToString(qweb, "test", { condition: false }));
expect(result).toBe(
"<div><span>begin</span>fail-else<span>end</span></div>"
);
expect(result).toBe("<div><span>begin</span>fail-else<span>end</span></div>");
});
test("can use some boolean operators in expressions", () => {
@@ -357,9 +320,7 @@ describe("t-if", () => {
m: 5,
n: 2
};
expect(normalize(renderToString(qweb, "test", context))).toBe(
"<div>andormgtnlt</div>"
);
expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>");
});
});
@@ -484,10 +445,7 @@ describe("attributes", () => {
});
test("format multiple", () => {
qweb.addTemplate(
"test",
`<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>`
);
qweb.addTemplate("test", `<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>`);
const result = renderToString(qweb, "test", {
value1: 0,
value2: 1,
@@ -523,19 +481,13 @@ describe("attributes", () => {
});
test("class and t-attf-class with ternary operation", () => {
qweb.addTemplate(
"test",
`<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`
);
qweb.addTemplate("test", `<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`);
const result = renderToString(qweb, "test", { value: true });
expect(result).toBe(`<div class="hello world"></div>`);
});
test("t-att-class with object", () => {
qweb.addTemplate(
"test",
`<div class="static" t-att-class="{a: b, c: d, e: f}"/>`
);
qweb.addTemplate("test", `<div class="static" t-att-class="{a: b, c: d, e: f}"/>`);
const result = renderToString(qweb, "test", { b: true, d: false, f: true });
expect(result).toBe(`<div class="static a e"></div>`);
});
@@ -564,10 +516,7 @@ describe("t-call (template calling", () => {
test("with unused setbody", () => {
qweb.addTemplate("_basic-callee", "<div>ok</div>");
qweb.addTemplate(
"caller",
'<t t-call="_basic-callee"><t t-set="qux" t-value="3"/></t>'
);
qweb.addTemplate("caller", '<t t-call="_basic-callee"><t t-set="qux" t-value="3"/></t>');
const expected = "<div>ok</div>";
expect(renderToString(qweb, "caller")).toBe(expected);
});
@@ -673,9 +622,7 @@ describe("foreach", () => {
</t>
</div>`
);
const result = trim(
renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } })
);
const result = trim(renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } }));
const expected = `<div>[0:a1][1:b2][2:c3]</div>`;
expect(result).toBe(expected);
});
@@ -722,14 +669,8 @@ describe("foreach", () => {
describe("misc", () => {
test("global", () => {
qweb.addTemplate("_callee-asc", `<Año t-att-falló="'agüero'" t-raw="0"/>`);
qweb.addTemplate(
"_callee-uses-foo",
`<span t-esc="foo">foo default</span>`
);
qweb.addTemplate(
"_callee-asc-toto",
`<div t-raw="toto">toto default</div>`
);
qweb.addTemplate("_callee-uses-foo", `<span t-esc="foo">foo default</span>`);
qweb.addTemplate("_callee-asc-toto", `<div t-raw="toto">toto default</div>`);
qweb.addTemplate(
"caller",
`
@@ -842,10 +783,7 @@ describe("t-on", () => {
});
test("can bind handlers with object arguments", () => {
qweb.addTemplate(
"test",
`<button t-on-click="add({val: 5})">Click</button>`
);
qweb.addTemplate("test", `<button t-on-click="add({val: 5})">Click</button>`);
let a = 1;
const node = renderToDOM(
qweb,
@@ -863,10 +801,7 @@ describe("t-on", () => {
test("can bind handlers with empty object", () => {
expect.assertions(2);
qweb.addTemplate(
"test",
`<button t-on-click="doSomething({})">Click</button>`
);
qweb.addTemplate("test", `<button t-on-click="doSomething({})">Click</button>`);
const node = renderToDOM(
qweb,
"test",
@@ -882,10 +817,7 @@ describe("t-on", () => {
test("can bind handlers with empty object (with non empty inner string)", () => {
expect.assertions(2);
qweb.addTemplate(
"test",
`<button t-on-click="doSomething({ })">Click</button>`
);
qweb.addTemplate("test", `<button t-on-click="doSomething({ })">Click</button>`);
const node = renderToDOM(
qweb,
"test",
@@ -1185,13 +1117,10 @@ describe("whitespace handling", () => {
describe("t-key", () => {
test("can use t-key directive on a node", () => {
qweb.addTemplate(
"test",
`<div t-key="beer.id"><t t-esc="beer.name"/></div>`
qweb.addTemplate("test", `<div t-key="beer.id"><t t-esc="beer.name"/></div>`);
expect(renderToString(qweb, "test", { beer: { id: 12, name: "Chimay Rouge" } })).toBe(
"<div>Chimay Rouge</div>"
);
expect(
renderToString(qweb, "test", { beer: { id: 12, name: "Chimay Rouge" } })
).toBe("<div>Chimay Rouge</div>");
});
test("t-key directive in a list", () => {
+19 -40
View File
@@ -2,9 +2,7 @@ import { compileExpr, tokenize } from "../src/qweb_expressions";
describe("tokenizer", () => {
test("simple tokens", () => {
expect(tokenize("1.3")).toEqual([
{ type: "VALUE", value: "1.3" }
]);
expect(tokenize("1.3")).toEqual([{ type: "VALUE", value: "1.3" }]);
expect(tokenize("{}")).toEqual([
{ type: "LEFT_BRACE", value: "{" },
@@ -29,10 +27,7 @@ describe("tokenizer", () => {
{ type: "VALUE", value: "2" },
{ type: "RIGHT_BRACE", value: "}" }
]);
expect(tokenize("a,")).toEqual([
{ type: "SYMBOL", value: "a" },
{ type: "COMMA", value: "," }
]);
expect(tokenize("a,")).toEqual([{ type: "SYMBOL", value: "a" }, { type: "COMMA", value: "," }]);
expect(tokenize("][")).toEqual([
{ type: "RIGHT_BRACKET", value: "]" },
{ type: "LEFT_BRACKET", value: "[" }
@@ -40,11 +35,13 @@ describe("tokenizer", () => {
});
test("various operators", () => {
expect(tokenize(">= <= < >")).toEqual([
expect(tokenize(">= <= < > !== !=")).toEqual([
{ type: "OPERATOR", value: ">=" },
{ type: "OPERATOR", value: "<=" },
{ type: "OPERATOR", value: "<" },
{ type: "OPERATOR", value: ">" },
{ type: "OPERATOR", value: "!==" },
{ type: "OPERATOR", value: "!=" }
]);
});
@@ -52,25 +49,17 @@ describe("tokenizer", () => {
expect(() => tokenize("'")).toThrow("Invalid expression");
expect(() => tokenize("'\\")).toThrow("Invalid expression");
expect(() => tokenize("'\\'")).toThrow("Invalid expression");
expect(tokenize("'hello ged'")).toEqual([
{ type: "VALUE", value: "'hello ged'" }
]);
expect(tokenize("'hello \\'ged\\''")).toEqual([
{ type: "VALUE", value: "'hello \\'ged\\''" }
]);
expect(tokenize("'hello ged'")).toEqual([{ type: "VALUE", value: "'hello ged'" }]);
expect(tokenize("'hello \\'ged\\''")).toEqual([{ type: "VALUE", value: "'hello \\'ged\\''" }]);
expect(() => tokenize('"')).toThrow("Invalid expression");
expect(() => tokenize('"\\"')).toThrow("Invalid expression");
expect(tokenize('"hello ged"')).toEqual([
{ type: "VALUE", value: '"hello ged"' }
]);
expect(tokenize('"hello ged"')).toEqual([{ type: "VALUE", value: '"hello ged"' }]);
expect(tokenize('"hello ged"}')).toEqual([
{ type: "VALUE", value: '"hello ged"' },
{ type: "RIGHT_BRACE", value: "}" }
]);
expect(tokenize('"hello \\"ged\\""')).toEqual([
{ type: "VALUE", value: '"hello \\"ged\\""' }
]);
expect(tokenize('"hello \\"ged\\""')).toEqual([{ type: "VALUE", value: '"hello \\"ged\\""' }]);
});
});
@@ -111,9 +100,7 @@ describe("expression evaluation", () => {
test("arrays and objects", () => {
expect(compileExpr("[{b:1}] ", {})).toBe("[{b:1}]");
expect(compileExpr("{a: []} ", {})).toBe("{a:[]}");
expect(compileExpr("[{b:1, c: [1, {d: {e: 3}} ]}] ", {})).toBe(
"[{b:1,c:[1,{d:{e:3}}]}]"
);
expect(compileExpr("[{b:1, c: [1, {d: {e: 3}} ]}] ", {})).toBe("[{b:1,c:[1,{d:{e:3}}]}]");
});
test("dot operator", () => {
@@ -128,13 +115,9 @@ describe("expression evaluation", () => {
});
test("various binary operators", () => {
expect(compileExpr("color == 'black'", {})).toBe(
"context['color']=='black'"
);
expect(compileExpr("color == 'black'", {})).toBe("context['color']=='black'");
expect(compileExpr("a || b", {})).toBe("context['a']||context['b']");
expect(compileExpr("color === 'black'", {})).toBe(
"context['color']==='black'"
);
expect(compileExpr("color === 'black'", {})).toBe("context['color']==='black'");
expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']");
expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1");
});
@@ -162,18 +145,14 @@ describe("expression evaluation", () => {
expect(compileExpr("a()", {})).toBe("context['a']()");
expect(compileExpr("a(1)", {})).toBe("context['a'](1)");
expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)");
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe(
"context['a'](1,2,{a:[context['a']]})"
);
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("context['a'](1,2,{a:[context['a']]})");
expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()");
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe(
"'x'.toUpperCase({a:3})"
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe("'x'.toUpperCase({a:3})");
expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })).toBe(
"'x'.toUpperCase(_v5)"
);
expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })).toBe(
"'x'.toUpperCase({b:_v5})"
);
expect(
compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })
).toBe("'x'.toUpperCase(_v5)");
expect(
compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })
).toBe("'x'.toUpperCase({b:_v5})");
});
});
+284 -54
View File
@@ -1,11 +1,7 @@
import { Component, Env } from "../src/component";
import { connect, Store } from "../src/store";
import {
makeTestFixture,
makeTestEnv,
nextMicroTick,
nextTick
} from "./helpers";
import { makeTestFixture, makeTestEnv, nextMicroTick, nextTick } from "./helpers";
import { Observer } from "../src";
describe("basic use", () => {
test("commit a mutation", () => {
@@ -41,6 +37,31 @@ describe("basic use", () => {
expect(store.state.n).toBe(15);
});
test("dispatch an action + commit a mutation with positional arguments", () => {
const state = { n1: 1, n2: 1, n3: 1 };
const mutations = {
batchInc({ state }, delta1, delta2, delta3) {
state.n1 += delta1;
state.n2 += delta2;
state.n3 += delta3;
}
};
const actions = {
batchInc({ commit }, delta1, delta2, delta3) {
commit("batchInc", delta1, delta2, delta3);
}
};
const store = new Store({ state, mutations, 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("modifying state outside of mutations trigger error", () => {
const state = { n: 1 };
const actions = {
@@ -207,7 +228,7 @@ describe("basic use", () => {
bestBeerName({ state }) {
n++;
return state.beers[1].name;
},
}
};
const store = new Store({ state, mutations: {}, actions: {}, getters });
expect((<any>store.getters).bestBeerName()).toBe("bertinchamps");
@@ -226,7 +247,7 @@ describe("basic use", () => {
name: "bertinchamps",
tasterID: 1
}
},
}
};
let n = 0;
const getters = {
@@ -265,7 +286,7 @@ describe("basic use", () => {
}
};
const mutations = {
changeTaster({ state }, {beerID, tasterID}) {
changeTaster({ state }, { beerID, tasterID }) {
state.beers[beerID].tasterID = tasterID;
}
};
@@ -274,15 +295,20 @@ describe("basic use", () => {
beerTasterName({ state }, beerID) {
n++;
return state.tasters[state.beers[beerID].tasterID].name;
},
}
};
const store = new Store({ state, mutations: mutations, actions: {}, getters });
const store = new Store({
state,
mutations: mutations,
actions: {},
getters
});
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
expect(n).toBe(1);
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
expect(n).toBe(1);
store.commit('changeTaster', {beerID: 1, tasterID: 2});
store.commit("changeTaster", { beerID: 1, tasterID: 2 });
await nextTick();
expect((<any>store.getters).beerTasterName(1)).toBe("gery");
@@ -295,14 +321,14 @@ describe("basic use", () => {
1: {
id: 1,
name: "bertinchamps"
},
}
}
};
const mutations = {
renameBeer({ state, getters }, beerID) {
expect(getters.beerName(beerID)).toBe('bertinchamps');
state.beers[1].name = 'chouffe';
expect(getters.beerName(beerID)).toBe('chouffe');
expect(getters.beerName(beerID)).toBe("bertinchamps");
state.beers[1].name = "chouffe";
expect(getters.beerName(beerID)).toBe("chouffe");
}
};
let n = 0;
@@ -310,12 +336,17 @@ describe("basic use", () => {
beerName({ state }, beerID) {
n++;
return state.beers[beerID].name;
},
}
};
const store = new Store({ state, mutations: mutations, actions: {}, getters });
const store = new Store({
state,
mutations: mutations,
actions: {},
getters
});
store.commit('renameBeer', 1);
expect((<any>store.getters).beerName(1)).toBe('chouffe');
store.commit("renameBeer", 1);
expect((<any>store.getters).beerName(1)).toBe("chouffe");
await nextTick();
expect(n).toBe(3);
@@ -534,14 +565,14 @@ describe("connecting a component to store", () => {
"App",
`
<div>
<t t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-widget="Todo" msg="todo.msg"/>
<t t-foreach="props.todos" t-as="todo" >
<Todo msg="todo.msg" t-key="todo"/>
</t>
</div>`
);
env.qweb.addTemplate("Todo", `<span><t t-esc="props.msg"/></span>`);
class App extends Component<any, any, any> {
widgets = { Todo };
components = { Todo };
}
class Todo extends Component<any, any, any> {}
const state = { todos: [] };
@@ -624,15 +655,15 @@ describe("connecting a component to store", () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<t t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-widget="Todo" msg="todo.msg"/>
<t t-foreach="props.todos" t-as="todo">
<Todo msg="todo.msg" t-key="todo" />
</t>
</div>
<span t-name="Todo"><t t-esc="props.msg"/></span>
</templates>
`);
class App extends Component<any, any, any> {
widgets = { Todo };
components = { Todo };
}
class Todo extends Component<any, any, any> {}
@@ -686,11 +717,11 @@ describe("connecting a component to store", () => {
"Parent",
`
<div>
<t t-if="state.child" t-widget="ConnectedChild"/>
<t t-if="state.child" t-component="ConnectedChild"/>
</div>`
);
class Parent extends Component<any, any, any> {
widgets = { ConnectedChild };
components = { ConnectedChild };
constructor(env: Env) {
super(env);
@@ -734,12 +765,12 @@ describe("connecting a component to store", () => {
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" id="todo.id"/>
<ConnectedTodo id="todo.id" t-key="todo.id"/>
</t>
</div>`
);
class TodoList extends Component<any, any, any> {
widgets = { ConnectedTodo };
components = { ConnectedTodo };
}
function mapStoreToProps(state) {
@@ -758,9 +789,7 @@ describe("connecting a component to store", () => {
store.commit("addTodo", "hoegaarden");
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><span>jupiler</span><span>hoegaarden</span></div>"
);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>hoegaarden</span></div>");
});
test("connect receives store getters as third argument", async () => {
@@ -801,12 +830,12 @@ describe("connecting a component to store", () => {
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" id="todo.id"/>
<ConnectedTodo id="todo.id" t-key="todo.id"/>
</t>
</div>`
);
class TodoList extends Component<any, any, any> {
widgets = { ConnectedTodo };
components = { ConnectedTodo };
}
function mapStoreToProps(state) {
@@ -839,11 +868,11 @@ describe("connecting a component to store", () => {
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" id="state.beerId"/>
<ConnectedBeer id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> {
widgets = { ConnectedBeer };
components = { ConnectedBeer };
state = { beerId: 1 };
}
@@ -894,9 +923,7 @@ describe("connecting a component to store", () => {
store.commit("addBeer", "kwak");
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><span>jupiler</span><span>kwak</span></div>"
);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>kwak</span></div>");
});
test("connected component with undefined, null and string props", async () => {
@@ -923,11 +950,11 @@ describe("connecting a component to store", () => {
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" id="state.beerId"/>
</div>`
<ConnectedBeer id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> {
widgets = { ConnectedBeer };
components = { ConnectedBeer };
state = { beerId: 0 };
}
@@ -948,9 +975,7 @@ describe("connecting a component to store", () => {
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span></div></div>"
);
expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>");
app.state.beerId = 1;
await nextTick();
@@ -995,11 +1020,11 @@ describe("connecting a component to store", () => {
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" id="state.beerId"/>
<ConnectedBeer id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> {
widgets = { ConnectedBeer };
components = { ConnectedBeer };
state = { beerId: 0 };
}
@@ -1026,9 +1051,7 @@ describe("connecting a component to store", () => {
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div><span>taster:aaron</span></div></div>"
);
expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>");
app.state.beerId = 1;
await nextTick();
@@ -1074,12 +1097,12 @@ describe("connecting a component to store", () => {
"Parent",
`
<div>
<t t-widget="Child" key="props.current"/>
<Child key="props.current"/>
</div>
`
);
class Parent extends Component<any, any, any> {
widgets = { Child: ConnectedChild };
components = { Child: ConnectedChild };
}
const ConnectedParent = connect(
Parent,
@@ -1117,7 +1140,188 @@ describe("connecting a component to store", () => {
store.commit("setCurrent", "b");
await nextTick();
expect(steps).toEqual(["parent", "child", "parent", "child", "child"]);
expect(fixture.innerHTML).toBe("<div><span>b</span></div>");
expect(steps).toEqual(["parent", "child", "parent", "child"]);
});
test("connected parent/children: no double rendering", async () => {
const mutations = {
editTodo({ state }) {
state.todos[1].title = "abc";
}
};
const todos = { 1: { id: 1, title: "kikoou" } };
const state = {
todos
};
const store = new Store({
state,
mutations
});
env.qweb.addTemplates(`
<templates>
<div t-name="TodoApp" class="todoapp">
<t t-foreach="Object.values(props.todos)" t-as="todo">
<ConnectedTodoItem t-key="todo.id" id="todo.id"/>
</t>
</div>
<div t-name="TodoItem" class="todo">
<t t-esc="props.todo.title"/>
<button class="destroy" t-on-click="editTodo">x</button>
</div>
</templates>
`);
function mapStoreToPropsTodoApp(state) {
return {
todos: state.todos
};
}
class TodoApp extends Component<any, any, any> {
components = { ConnectedTodoItem };
}
const ConnectedTodoApp = connect(
TodoApp,
mapStoreToPropsTodoApp
);
let renderCount = 0;
let fCount = 0;
function mapStoreToPropsTodoItem(state, ownProps) {
fCount++;
return {
todo: state.todos[ownProps.id]
};
}
class TodoItem extends Component<any, any, any> {
state = { isEditing: false };
editTodo() {
this.env.store.commit("editTodo");
}
__render(...args) {
renderCount++;
return super.__render(...args);
}
}
const ConnectedTodoItem = connect(
TodoItem,
mapStoreToPropsTodoItem
);
(<any>env).store = store;
const app = new ConnectedTodoApp(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div class="todoapp"><div class="todo">kikoou<button class="destroy">x</button></div></div>'
);
expect(renderCount).toBe(1);
expect(fCount).toBe(1);
fixture.querySelector("button")!.click();
await nextTick();
expect(renderCount).toBe(2);
expect(fCount).toBe(2);
expect(fixture.innerHTML).toBe(
'<div class="todoapp"><div class="todo">abc<button class="destroy">x</button></div></div>'
);
});
test("connected parent/children: no rendering if child is destroyed", async () => {
const mutations = {
removeTodo({ state }) {
Observer.delete(state.todos, 1);
}
};
const todos = { 1: { id: 1, title: "kikoou" } };
const state = {
todos
};
const store = new Store({
state,
mutations
});
env.qweb.addTemplates(`
<templates>
<div t-name="TodoApp" class="todoapp">
<t t-foreach="Object.values(props.todos)" t-as="todo">
<ConnectedTodoItem t-key="todo.id" id="todo.id"/>
</t>
</div>
<div t-name="TodoItem" class="todo">
<t t-esc="props.todo.title"/>
<button class="destroy" t-on-click="removeTodo">x</button>
</div>
</templates>
`);
function mapStoreToPropsTodoApp(state) {
return {
todos: state.todos
};
}
class TodoApp extends Component<any, any, any> {
components = { ConnectedTodoItem };
}
const ConnectedTodoApp = connect(
TodoApp,
mapStoreToPropsTodoApp
);
let renderCount = 0;
let fCount = 0;
function mapStoreToPropsTodoItem(state, ownProps) {
fCount++;
return {
todo: state.todos[ownProps.id]
};
}
class TodoItem extends Component<any, any, any> {
state = { isEditing: false };
removeTodo() {
this.env.store.commit("removeTodo");
}
__render(...args) {
renderCount++;
return super.__render(...args);
}
}
const ConnectedTodoItem = connect(
TodoItem,
mapStoreToPropsTodoItem
);
(<any>env).store = store;
const app = new ConnectedTodoApp(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div class="todoapp"><div class="todo">kikoou<button class="destroy">x</button></div></div>'
);
expect(renderCount).toBe(1);
expect(fCount).toBe(1);
fixture.querySelector("button")!.click();
await nextTick();
expect(renderCount).toBe(1);
expect(fCount).toBe(1);
expect(fixture.innerHTML).toBe('<div class="todoapp"></div>');
});
test("connected component willpatch/patch hooks are called on store updates", async () => {
@@ -1157,4 +1361,30 @@ describe("connecting a component to store", () => {
expect(fixture.innerHTML).toBe("<div>b</div>");
expect(steps).toEqual(["willpatch", "patched"]);
});
test("connected component has its own name", () => {
function mapStoreToProps() {}
class Named extends Component<any, any, any> {}
const namedConnected = connect(
Named,
mapStoreToProps
);
expect(namedConnected.name).toMatch("ConnectedNamed");
class ParentNamed extends Component<any, any, any> {}
class ChildNamed extends ParentNamed {}
const childConnected = connect(
ChildNamed,
mapStoreToProps
);
expect(childConnected.name).toMatch("ConnectedChildNamed");
const Anonymous = class extends Component<any, any, any> {};
const anonymousConnected = connect(
Anonymous,
mapStoreToProps
);
expect(anonymousConnected.name).toMatch(/^Connectedclass_\d+/);
});
});
+28 -143
View File
@@ -59,9 +59,7 @@ describe("attributes", function() {
test("are set correctly when namespaced", function() {
const vnode1 = h("div", { attrs: { "xlink:href": "#foo" } });
elm = patch(vnode0, vnode1).elm;
expect(elm.getAttributeNS("http://www.w3.org/1999/xlink", "href")).toBe(
"#foo"
);
expect(elm.getAttributeNS("http://www.w3.org/1999/xlink", "href")).toBe("#foo");
});
test("should not touch class nor id fields", function() {
@@ -207,12 +205,8 @@ describe("snabbdom", function() {
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
// verify that svg tag automatically gets svg namespace
elm = patch(
vnode0,
h("svg", [
h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])
])
).elm;
elm = patch(vnode0, h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]))
.elm;
expect(elm.namespaceURI).toBe(SVGNamespace);
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace);
@@ -352,13 +346,7 @@ describe("snabbdom", function() {
elm = patch(vnode0, vnode1).elm;
expect(elm.children.length).toBe(2);
elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"1",
"2",
"3",
"4",
"5"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
});
test("add elements in the middle", function() {
@@ -368,13 +356,7 @@ describe("snabbdom", function() {
expect(elm.children.length).toBe(4);
expect(elm.children.length).toBe(4);
elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"1",
"2",
"3",
"4",
"5"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
});
test("add elements at beginning and end", function() {
@@ -383,13 +365,7 @@ describe("snabbdom", function() {
elm = patch(vnode0, vnode1).elm;
expect(elm.children.length).toBe(3);
elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"1",
"2",
"3",
"4",
"5"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
});
test("adds children to parent with no children", function() {
@@ -588,35 +564,19 @@ describe("snabbdom", function() {
elm = patch(vnode0, vnode1).elm;
expect(elm.children.length).toBe(6);
elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"4",
"3",
"2",
"1",
"5",
"0"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["4", "3", "2", "1", "5", "0"]);
});
test("supports null/undefined children", function() {
const vnode1 = h("i", [0, 1, 2, 3, 4, 5].map(spanNum));
const vnode2 = h(
"i",
[null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(
spanNum
)
[null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(spanNum)
);
elm = patch(vnode0, vnode1).elm;
expect(elm.children.length).toBe(6);
elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"2",
"1",
"0",
"5",
"4",
"3"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["2", "1", "0", "5", "4", "3"]);
});
test("supports all null/undefined children", function() {
@@ -627,14 +587,7 @@ describe("snabbdom", function() {
elm = patch(vnode1, vnode2).elm;
expect(elm.children.length).toBe(0);
elm = patch(vnode2, vnode3).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"5",
"4",
"3",
"2",
"1",
"0"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["5", "4", "3", "2", "1", "0"]);
});
});
});
@@ -714,18 +667,10 @@ describe("snabbdom", function() {
});
test("removes elements", function() {
const vnode1 = h("div", [
h("span", "One"),
h("span", "Two"),
h("span", "Three")
]);
const vnode1 = h("div", [h("span", "One"), h("span", "Two"), h("span", "Three")]);
const vnode2 = h("div", [h("span", "One"), h("span", "Three")]);
elm = patch(vnode0, vnode1).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"One",
"Two",
"Three"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Two", "Three"]);
elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Three"]);
});
@@ -760,49 +705,19 @@ describe("snabbdom", function() {
});
test("reorders elements", function() {
const vnode1 = h("div", [
h("span", "One"),
h("div", "Two"),
h("b", "Three")
]);
const vnode2 = h("div", [
h("b", "Three"),
h("span", "One"),
h("div", "Two")
]);
const vnode1 = h("div", [h("span", "One"), h("div", "Two"), h("b", "Three")]);
const vnode2 = h("div", [h("b", "Three"), h("span", "One"), h("div", "Two")]);
elm = patch(vnode0, vnode1).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([
"One",
"Two",
"Three"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Two", "Three"]);
elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.tagName)).toEqual(["B", "SPAN", "DIV"]);
expect(map(elm.children, c => c.innerHTML)).toEqual([
"Three",
"One",
"Two"
]);
expect(map(elm.children, c => c.innerHTML)).toEqual(["Three", "One", "Two"]);
});
test("supports null/undefined children", function() {
const vnode1 = h("i", [null, h("i", "1"), h("i", "2"), null]);
const vnode2 = h("i", [
h("i", "2"),
undefined,
undefined,
h("i", "1"),
undefined
]);
const vnode3 = h("i", [
null,
h("i", "1"),
undefined,
null,
h("i", "2"),
undefined,
null
]);
const vnode2 = h("i", [h("i", "2"), undefined, undefined, h("i", "1"), undefined]);
const vnode3 = h("i", [null, h("i", "1"), undefined, null, h("i", "2"), undefined, null]);
elm = patch(vnode0, vnode1).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2"]);
elm = patch(vnode1, vnode2).elm;
@@ -835,10 +750,7 @@ describe("snabbdom", function() {
}
const vnode1 = h("div", [
h("span", "First sibling"),
h("div", { hook: { create: cb } }, [
h("span", "Child 1"),
h("span", "Child 2")
]),
h("div", { hook: { create: cb } }, [h("span", "Child 1"), h("span", "Child 2")]),
h("span", "Can't touch me")
]);
patch(vnode0, vnode1);
@@ -855,10 +767,7 @@ describe("snabbdom", function() {
}
const vnode1 = h("div", [
h("span", "First sibling"),
h("div", { hook: { insert: cb } }, [
h("span", "Child 1"),
h("span", "Child 2")
]),
h("div", { hook: { insert: cb } }, [h("span", "Child 1"), h("span", "Child 2")]),
h("span", "Can touch me")
]);
patch(vnode0, vnode1);
@@ -874,17 +783,11 @@ describe("snabbdom", function() {
}
const vnode1 = h("div", [
h("span", "First sibling"),
h("div", { hook: { prepatch: cb } }, [
h("span", "Child 1"),
h("span", "Child 2")
])
h("div", { hook: { prepatch: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
]);
const vnode2 = h("div", [
h("span", "First sibling"),
h("div", { hook: { prepatch: cb } }, [
h("span", "Child 1"),
h("span", "Child 2")
])
h("div", { hook: { prepatch: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
]);
patch(vnode0, vnode1);
patch(vnode1, vnode2);
@@ -965,10 +868,7 @@ describe("snabbdom", function() {
}
const vnode1 = h("div", [
h("span", "First sibling"),
h("div", { hook: { remove: cb } }, [
h("span", "Child 1"),
h("span", "Child 2")
])
h("div", { hook: { remove: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
]);
const vnode2 = h("div", [h("span", "First sibling")]);
patch(vnode0, vnode1);
@@ -981,9 +881,7 @@ describe("snabbdom", function() {
function cb(vnode) {
calls++;
}
const vnode1 = h("div", [
h("div", { hook: { destroy: cb } }, [h("span", "Child 1")])
]);
const vnode1 = h("div", [h("div", { hook: { destroy: cb } }, [h("span", "Child 1")])]);
const vnode2 = h("div", "Text node");
patch(vnode0, vnode1);
patch(vnode1, vnode2);
@@ -1053,10 +951,7 @@ describe("snabbdom", function() {
result.push(vnode);
rm();
}
const vnode1 = h("div", { hook: { remove: cb } }, [
h("b", "Child 1"),
h("i", "Child 2")
]);
const vnode1 = h("div", { hook: { remove: cb } }, [h("b", "Child 1"), h("i", "Child 2")]);
const vnode2 = h("span", [h("b", "Child 1"), h("i", "Child 2")]);
patch(vnode0, vnode1);
patch(vnode1, vnode2);
@@ -1091,10 +986,7 @@ describe("snabbdom", function() {
}
const vnode1 = h("div", [
h("span", "First sibling"),
h("div", [
h("span", { hook: { destroy: cb } }, "Child 1"),
h("span", "Child 2")
])
h("div", [h("span", { hook: { destroy: cb } }, "Child 1"), h("span", "Child 2")])
]);
const vnode2 = h("div");
patch(vnode0, vnode1);
@@ -1150,11 +1042,7 @@ describe("snabbdom", function() {
}
}
]);
const vnode1 = h("div", [
h("span", "First child"),
"",
h("span", "Third child")
]);
const vnode1 = h("div", [h("span", "First child"), "", h("span", "Third child")]);
const vnode2 = h("div");
patch(vnode0, vnode1);
patch(vnode1, vnode2);
@@ -1196,10 +1084,7 @@ describe("snabbdom", function() {
function cb(vnode) {
result.push(vnode);
}
const vnode1 = h("div", [
h("span", { hook: { update: cb } }, "Hello"),
h("span", "there")
]);
const vnode1 = h("div", [h("span", { hook: { update: cb } }, "Hello"), h("span", "there")]);
patch(vnode0, vnode1);
patch(vnode1, vnode1);
expect(result).toHaveLength(0);
+2 -11
View File
@@ -1,9 +1,4 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
@@ -40,11 +35,7 @@ class App extends owl.Component {
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(
`Benchmarking Owl v${owl.__info__.version} (build date: ${
owl.__info__.date
})`
);
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
+2 -11
View File
@@ -1,9 +1,4 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
@@ -40,11 +35,7 @@ class App extends owl.Component {
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(
`Benchmarking Owl v${owl.__info__.version} (build date: ${
owl.__info__.date
})`
);
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
+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 {
widgets = { 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 {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(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.15.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">
<t t-widget="Message" t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+5 -16
View File
@@ -1,9 +1,4 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
@@ -20,7 +15,7 @@ class Counter extends owl.Component {
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
@@ -36,15 +31,11 @@ class Message extends owl.Component {
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(
`Benchmarking Owl v${owl.__info__.version} (build date: ${
owl.__info__.date
})`
);
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
@@ -128,9 +119,7 @@ class App extends owl.Component {
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(
m => m.id === event.detail.id
);
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
+2 -2
View File
@@ -30,7 +30,7 @@
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" message="message"/>
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
@@ -40,7 +40,7 @@
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
<Counter/>
</div>
<div t-name="Counter">
+3 -3
View File
@@ -17,7 +17,7 @@ let nextId = 1;
export function buildData(n = 1000) {
const data = [];
for (let i = 0; i < n; i++) {
let id = nextId++;
let id = nextId++;
data.push({
id: id,
author: chooseRandomly(AUTHORS),
@@ -32,7 +32,7 @@ export function buildData(n = 1000) {
// Measuring helpers
//------------------------------------------------------------------------------
export function formatNumber(n) {
return Number(n).toFixed();
return Number(n).toFixed();
}
let startTime;
@@ -51,7 +51,7 @@ export function stopMeasure(cb) {
const msg = `[${last}] took ${formatNumber(delta)}ms`;
console.log(msg);
if (cb) {
cb({msg, delta});
cb({ msg, delta });
}
}, 0);
}
+1
View File
@@ -35,6 +35,7 @@
<li><a href="benchmarks/owl-0.12.0">OWL 0.12.0</a></li>
<li><a href="benchmarks/owl-0.13.0">OWL 0.13.0</a></li>
<li><a href="benchmarks/owl-0.14.0">OWL 0.14.0</a></li>
<li><a href="benchmarks/owl-0.15.0">OWL 0.15.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul>
<ul>
+28 -15
View File
@@ -97,10 +97,7 @@ function makeCodeIframe(js, css, xml, errorHandler) {
setTimeout(function() {
if (iframe.contentWindow) {
iframe.contentWindow.removeEventListener("error", errorHandler);
iframe.contentWindow.removeEventListener(
"unhandledrejection",
errorHandler
);
iframe.contentWindow.removeEventListener("unhandledrejection", errorHandler);
}
}, 200);
doc.body.appendChild(script);
@@ -161,7 +158,7 @@ class App extends owl.Component {
super(...args);
this.version = owl.__info__.version;
this.SAMPLES = SAMPLES;
this.widgets = { TabbedEditor };
this.components = { TabbedEditor };
this.state = {
js: SAMPLES[0].code,
@@ -200,6 +197,8 @@ class App extends owl.Component {
} catch (e) {
//probably problem with the templates
error = e;
// we still log the error, always useful to have it available
console.error(e);
}
if (error) {
this.displayError(error.message);
@@ -284,15 +283,7 @@ class TabbedEditor extends owl.Component {
this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {};
for (let tab of ["js", "xml", "css"]) {
if (props[tab]) {
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
this.sessions[tab].setOption("useWorker", false);
const tabSize = tab === "xml" ? 2 : 4;
this.sessions[tab].setOption("tabSize", tabSize);
this.sessions[tab].setUndoManager(new ace.UndoManager());
}
}
this._setupSessions(props);
this.editor = null;
}
@@ -317,9 +308,19 @@ class TabbedEditor extends owl.Component {
});
}
willUpdateProps(nextProps) {
this._setupSessions(nextProps);
}
patched() {
const session = this.sessions[this.state.currentTab];
session.setValue(this.props[this.state.currentTab], -1);
let content = this.props[this.state.currentTab];
if (content === false) {
const tab = this.props.js ? "js" : this.props.xml ? "xml" : "css";
content = this.props[tab];
this.state.currentTab = tab;
}
session.setValue(content, -1);
this.editor.setSession(session);
this.editor.resize();
}
@@ -347,6 +348,18 @@ class TabbedEditor extends owl.Component {
});
}
}
_setupSessions(props) {
for (let tab of ["js", "xml", "css"]) {
if (props[tab] && !this.sessions[tab]) {
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
this.sessions[tab].setOption("useWorker", false);
const tabSize = tab === "xml" ? 2 : 4;
this.sessions[tab].setOption("tabSize", tabSize);
this.sessions[tab].setUndoManager(new ace.UndoManager());
}
}
}
}
//------------------------------------------------------------------------------
+75 -74
View File
@@ -8,9 +8,9 @@ class Counter extends owl.Component {
}
}
// Main root widget
// Main root component
class App extends owl.Component {
widgets = { Counter };
components = { Counter };
}
// Application setup
@@ -26,8 +26,8 @@ const COMPONENTS_XML = `<templates>
</button>
<div t-name="App">
<t t-widget="Counter"/>
<t t-widget="Counter"/>
<Counter />
<Counter />
</div>
</templates>`;
@@ -49,8 +49,8 @@ class Counter extends owl.Component {
}
class App extends owl.Component {
state = { flag: false, widgetFlag: false, numbers: [] };
widgets = { Counter };
state = { flag: false, componentFlag: false, numbers: [] };
components = { Counter };
toggle(key) {
this.state[key] = !this.state[key];
@@ -83,12 +83,12 @@ const ANIMATION_XML = `<templates>
</div>
</div>
<h2>Transition on sub widgets</h2>
<h2>Transition on sub components</h2>
<div class="demo">
<button t-on-click="toggle('widgetFlag')">Toggle widget</button>
<button t-on-click="toggle('componentFlag')">Toggle component</button>
<div>
<t t-widget="Counter" t-if="state.widgetFlag" t-transition="fade"/>
<Counter t-if="state.componentFlag" t-transition="fade"/>
</div>
</div>
@@ -98,7 +98,7 @@ const ANIMATION_XML = `<templates>
<button t-on-click="addNumber">Add a number</button>
<div>
<t t-foreach="state.numbers" t-as="n">
<span t-transition="fade" class="numberspan"><t t-esc="n"/></span>
<span t-transition="fade" class="numberspan" t-key="n"><t t-esc="n"/></span>
</t>
</div>
</div>
@@ -173,12 +173,12 @@ const ANIMATION_CSS = `button {
const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
//
// The root widget controls a sub widget (DemoWidget). It logs all its lifecycle
// The root component controls a sub component (DemoComponent). It logs all its lifecycle
// methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what
// happens.
class DemoWidget extends owl.Component {
class DemoComponent extends owl.Component {
constructor() {
super(...arguments);
this.state = { n: 0 };
@@ -208,14 +208,14 @@ class DemoWidget extends owl.Component {
}
class App extends owl.Component {
widgets = { DemoWidget };
components = { DemoComponent };
state = { n: 0, flag: true };
increment() {
this.state.n++;
}
toggleSubWidget() {
toggleSubComponent() {
this.state.flag = !this.state.flag;
}
}
@@ -226,17 +226,17 @@ app.mount(document.body);
`;
const LIFECYCLE_DEMO_XML = `<templates>
<div t-name="DemoWidget" t-on-click="increment" class="demo">
<div>Demo Sub Widget</div>
<div t-name="DemoComponent" t-on-click="increment" class="demo">
<div>Demo Sub Component</div>
<div>(click on me to update me)</div>
<div>Props: <t t-esc="props.n"/>, State: <t t-esc="state.n"/>. </div>
</div>
<div t-name="App">
<button t-on-click="increment">Increment Parent State</button>
<button t-on-click="toggleSubWidget">Toggle SubWidget</button>
<button t-on-click="toggleSubComponent">Toggle SubComponent</button>
<div t-if="state.flag">
<t t-widget="DemoWidget" n="state.n"/>
<DemoComponent n="state.n"/>
</div>
</div>
</templates>`;
@@ -406,7 +406,7 @@ function mapStoreToProps(state) {
}
class TodoApp extends owl.Component {
widgets = { TodoItem };
components = { TodoItem };
state = { filter: "all" };
get visibleTodos() {
@@ -483,7 +483,7 @@ const TODO_APP_STORE_XML = `<templates>
<label for="toggle-all"></label>
<ul class="todo-list">
<t t-foreach="visibleTodos" t-as="todo">
<t t-widget="TodoItem" t-key="todo.id" id="todo.id" completed="todo.completed" title="todo.title"/>
<TodoItem t-key="todo.id" id="todo.id" completed="todo.completed" title="todo.title"/>
</t>
</ul>
</section>
@@ -910,7 +910,7 @@ const RESPONSIVE = `// In this example, we show how we can modify keys in the gl
//
// The main idea is to have a "isMobile" key in the environment, then listen
// to resize events and update the env if needed. Then, the whole interface
// will be updated, creating and destroying widgets as needed.
// will be updated, creating and destroying components as needed.
//
// To see this in action, try resizing the window. The application will switch
// to mobile mode whenever it has less than 768px.
@@ -921,50 +921,51 @@ const RESPONSIVE = `// In this example, we show how we can modify keys in the gl
class Navbar extends owl.Component {}
class ControlPanel extends owl.Component {
widgets = { MobileSearchView };
components = { MobileSearchView };
}
class FormView extends owl.Component {
widgets = { AdvancedWidget };
components = { AdvancedComponent };
}
class AdvancedWidget extends owl.Component {}
class AdvancedComponent extends owl.Component {}
class Chatter extends owl.Component {}
class Chatter extends owl.Component {
messages = Array.from(Array(100).keys());
}
class MobileSearchView extends owl.Component {}
class App extends owl.Component {
widgets = { Navbar, ControlPanel, FormView, Chatter };
components = { Navbar, ControlPanel, FormView, Chatter };
}
//------------------------------------------------------------------------------
// Responsive plugin
//------------------------------------------------------------------------------
function setupResponsivePlugin(env) {
const isMobile = () => window.innerWidth <= 768;
env.isMobile = isMobile();
const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile;
env.qweb.trigger('update');
}
}, 15);
window.addEventListener("resize", updateEnv);
}
//------------------------------------------------------------------------------
// Application Startup
//------------------------------------------------------------------------------
function isMobile() {
return window.innerWidth <= 768;
}
const env = {
qweb: new owl.QWeb(TEMPLATES),
isMobile: isMobile()
};
setupResponsivePlugin(env);
const app = new App(env);
app.mount(document.body);
function updateEnv() {
const _isMobile = isMobile();
if (_isMobile !== env.isMobile) {
app.updateEnv({
isMobile: _isMobile
});
}
}
window.addEventListener("resize", owl.utils.debounce(updateEnv, 20));
`;
const RESPONSIVE_XML = `<templates>
@@ -972,38 +973,38 @@ const RESPONSIVE_XML = `<templates>
<div t-name="ControlPanel" class="controlpanel">
<h2>Control Panel</h2>
<t t-if="env.isMobile" t-widget="MobileSearchView"/>
<MobileSearchView t-if="env.isMobile" />
</div>
<div t-name="FormView" class="formview">
<h2>Form View</h2>
<t t-if="!env.isMobile" t-widget="AdvancedWidget"/>
<AdvancedComponent t-if="!env.isMobile" />
</div>
<div t-name="Chatter" class="chatter">
<h2>Chatter</h2>
<t t-foreach="100" t-as="item"><div>Message <t t-esc="item"/></div></t>
<t t-foreach="messages" t-as="item"><div>Message <t t-esc="item"/></div></t>
</div>
<div t-name="MobileSearchView">Mobile searchview</div>
<div t-name="AdvancedWidget">
This widget is only created in desktop mode.
<div t-name="AdvancedComponent">
This component is only created in desktop mode.
<button>Button!</button>
</div>
<div t-name="App" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}">
<t t-widget="Navbar"/>
<t t-widget="ControlPanel"/>
<Navbar/>
<ControlPanel/>
<div class="content-wrapper" t-if="!env.isMobile">
<div class="content">
<t t-widget="FormView"/>
<t t-widget="Chatter"/>
<FormView />
<Chatter />
</div>
</div>
<t t-else="1">
<t t-widget="FormView"/>
<t t-widget="Chatter"/>
<FormView />
<Chatter />
</t>
</div>
</templates>
@@ -1070,7 +1071,7 @@ const RESPONSIVE_CSS = `body {
const SLOTS = `// We show here how slots can be used to create generic components.
// In this example, the Card component is basically only a container. It is not
// aware of its content. It just knows where it should be (with t-slot).
// The parent widget define the content with t-set.
// The parent component define the content with t-set.
//
// Note that the t-on-click event, defined in the App template, is executed in
// the context of the App component, even though it is inside the Card component
@@ -1091,9 +1092,9 @@ class Counter extends owl.Component {
}
}
// Main root widget
// Main root component
class App extends owl.Component {
widgets = {Card, Counter};
components = {Card, Counter};
state = {a: 1, b: 3};
inc(key, delta) {
@@ -1126,17 +1127,17 @@ const SLOTS_XML = `<templates>
</div>
<div t-name="App" class="main">
<t t-widget="Card" title="'Title card A'">
<Card title="'Title card A'">
<t t-set="content">Content of card 1... [<t t-esc="state.a"/>]</t>
<t t-set="footer"><button t-on-click="inc('a', 1)">Increment A</button></t>
</t>
<t t-widget="Card" title="'Title card B'">
<div t-set="content">
</Card>
<Card title="'Title card B'">
<t t-set="content">
<div>Card 2... [<t t-esc="state.b"/>]</div>
<t t-widget="Counter"/>
</div>
<Counter />
</t>
<t t-set="footer"><button t-on-click="inc('b', -1)">Decrement B</button></t>
</t>
</Card>
</div>
</templates>`;
@@ -1186,13 +1187,13 @@ const SLOTS_CSS = `.main {
const ASYNC_COMPONENTS = `// This example will not work if your browser does not support ESNext class fields
// In this example, we have 2 sub widgets, one of them being async (slow).
// However, we don't want renderings of the other sub widget to be delayed
// because of the slow widget. We use the 't-asyncroot' directive for this
// In this example, we have 2 sub components, one of them being async (slow).
// However, we don't want renderings of the other sub component to be delayed
// because of the slow component. We use the 't-asyncroot' directive for this
// purpose. Try removing it to see the difference.
class App extends owl.Component {
widgets = {SlowWidget, NotificationList};
components = {SlowComponent, NotificationList};
state = { value: 0, notifs: [] };
increment() {
@@ -1206,9 +1207,9 @@ class App extends owl.Component {
}
}
class SlowWidget extends owl.Component {
class SlowComponent extends owl.Component {
willUpdateProps() {
// simulate a widget that needs to perform async stuff (e.g. an RPC)
// simulate a component that needs to perform async stuff (e.g. an RPC)
// with the updated props before re-rendering itself
return new Promise(resolve => setTimeout(resolve, 1500));
}
@@ -1224,11 +1225,11 @@ app.mount(document.body);
const ASYNC_COMPONENTS_XML = `<templates>
<div t-name="App" class="app">
<button t-on-click="increment">Increment</button>
<t t-widget="SlowWidget" value="state.value"/>
<t t-widget="NotificationList" t-asyncroot="1" notifications="state.notifs"/>
<SlowComponent value="state.value"/>
<NotificationList t-asyncroot="1" notifications="state.notifs"/>
</div>
<div t-name="SlowWidget" class="value" >
<div t-name="SlowComponent" class="value" >
Current value: <t t-esc="props.value"/>
</div>
@@ -1350,7 +1351,7 @@ export const SAMPLES = [
description: "Lifecycle demo",
code: LIFECYCLE_DEMO,
xml: LIFECYCLE_DEMO_XML,
css: LIFECYCLE_CSS,
css: LIFECYCLE_CSS
},
{
description: "Todo List App (with store)",
+3 -3
View File
@@ -24,14 +24,14 @@
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
</div>
<t t-widget="TabbedEditor"
<TabbedEditor
js="state.js"
css="!state.splitLayout and state.css"
xml="!state.splitLayout and state.js"
xml="!state.splitLayout and state.xml"
t-att-style="topEditorStyle"/>
<t t-if="state.splitLayout">
<div class="separator horizontal"/>
<t t-widget="TabbedEditor" t-keepalive="1"
<TabbedEditor t-keepalive="1"
js="false"
css="state.css"
xml="state.xml"