Compare commits

...

26 Commits

Author SHA1 Message Date
Géry Debongnie f0b5a55ad1 [REL] bump to v0.24.0 2019-10-25 17:07:51 +02:00
Géry Debongnie 690d8edf67 [FIX] context: solve tricky concurrency issue
part of #330
2019-10-25 16:01:52 +02:00
Géry Debongnie 3c38bbc076 [REF] component: large cleanup of concurrency branch
We remove here old comments, add some tests and documentation, and in
general, make sure the state of the code is in a good shape

part of #330
2019-10-25 16:01:52 +02:00
Aaron Bohy 9c5cad15c1 [IMP] component: refactor rendering pipeline
This commit introduces a brand new rendering system based on a fiber
class and a scheduler.

closes #330
2019-10-25 16:01:52 +02:00
Géry Debongnie 2bed1cfbd1 [REF] asyncroot: create asyncroot component
This component replaces the t-asyncroot directive.
2019-10-25 16:01:52 +02:00
Géry Debongnie caa9ac5cb2 [ADD] project: add prettier task to package.json 2019-10-25 16:01:52 +02:00
Géry Debongnie 67349eb0f0 [IMP] vdom: remove handling of combined selectors
this is done by qweb anyway
2019-10-25 16:01:52 +02:00
Géry Debongnie 1ca401811f [DOC] add example of inline statement in main doc 2019-10-25 14:56:09 +02:00
Aaron Bohy 2beb12678e [IMP] t-on directive: allow empty handler
Closes #377
2019-10-25 14:11:44 +02:00
Aaron Bohy 0ea1091692 [IMP] t-on directive: handle inline statements
Closes #265
2019-10-25 14:11:44 +02:00
Nicolas Bayet 71827c3ba8 [DOC] explain router history and hash mode 2019-10-25 13:36:18 +02:00
Géry Debongnie c2f42d3b82 [FIX] qweb: do not format expression twice in t-if
closes #392
2019-10-24 14:57:48 +02:00
Géry Debongnie a3317ab997 [FIX] qweb: handle variable expressions in t-if
closes #390
closes #362
2019-10-24 08:55:30 +02:00
Géry Debongnie 403935a41e [DOC] qweb: document t-att variant
closes #378
2019-10-22 22:03:10 +02:00
Géry Debongnie 39af9ec938 [DOC] improve store documentation on useGetters
closes #386
2019-10-22 22:00:41 +02:00
Géry Debongnie 13128ed425 [FIX] component: do not validate props twice
it is not useful to do it in the component directive, especially since
it is done in the constructor, and the default props are not applied.

closes #379
2019-10-22 21:58:51 +02:00
Géry Debongnie be556a970e [FIX] qweb: properly calls directive finalizers
closes #382
2019-10-22 21:56:29 +02:00
Aaron Bohy da6c24bbca [FIX] test: do not check external links in doc 2019-10-21 09:47:20 +02:00
Géry Debongnie f6d6da8393 [FIX] component: fix issues with component internal template key
closes #298
2019-10-21 09:41:27 +02:00
Géry Debongnie 918945c11e [DOC] improve the comparison documentation page
closes #365
closes #366
2019-10-20 14:46:01 +02:00
Géry Debongnie 29c2c5b9c9 [DOC] improve the documentation
closes #340
closes #344
closes #352
closes #355
2019-10-20 09:17:52 +02:00
Aaron Bohy 1bb4577ec1 [DOC] improve hooks and qweb doc
Closes #358
Closes #359
2019-10-18 09:42:03 +02:00
Géry Debongnie 0aeebd7b6e [REF] utils: rename loadTemplates into loadFile
closes #351
2019-10-18 08:05:26 +02:00
Géry Debongnie 4bc49e7241 [DOC] add a roadmap
closes #347
2019-10-17 22:36:44 +02:00
Géry Debongnie 2b5783d9bd [FIX] playground: update samples to make them work on firefox
Sadly, Firefox does not support yet static class fields, so we need to
use an equivalent, but slightly not as nice, syntax.

closes #335
2019-10-17 16:57:40 +02:00
Géry Debongnie 1f40f113aa [DOC] update the documentation
closes #334
closes #336
closes #338
closes #339
closes #341
closes #342
closes #343
closes #346
closes #348
2019-10-17 12:08:24 +02:00
50 changed files with 4036 additions and 2191 deletions
+12 -3
View File
@@ -4,7 +4,7 @@ _A no nonsense web framework for structured, dynamic and maintainable applicatio
## Project Overview
The Odoo Web Library (OWL) is a smallish (~17kb gzipped) UI framework intended to
The Odoo Web Library (OWL) is a smallish (~18kb gzipped) UI framework intended to
be the basis for the [Odoo](https://www.odoo.com/) Web Client. Owl is a modern
framework, written in Typescript, taking the best ideas from React and Vue in a
simple and consistent way. Owl's main features are:
@@ -20,6 +20,9 @@ virtual dom, integrates beautifully with hooks, and the rendering is asynchronou
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the Owl framework. There
are some code examples to showcase some interesting features.
Owl is currently mostly stable. Possible future changes are explained in the
[roadmap](roadmap.md).
## Example
Here is a short example to illustrate interactive components:
@@ -104,8 +107,8 @@ Submit a PR!
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-0.23.0.js](https://github.com/odoo/owl/releases/download/v0.23.0/owl.js)
- [owl-0.23.0.min.js](https://github.com/odoo/owl/releases/download/v0.23.0/owl.min.js)
- [owl-0.24.0.js](https://github.com/odoo/owl/releases/download/v0.24.0/owl.js)
- [owl-0.24.0.min.js](https://github.com/odoo/owl/releases/download/v0.24.0/owl.min.js)
Some npm scripts are available:
@@ -187,6 +190,12 @@ class Counter extends Component {
}
```
Note that the `t-on-click` handler can even be replaced by an inline statement:
```xml
<button t-on-click="state.value++">
```
**Props:** sub components often needs some information from their parents. This
is done by adding the required information to the template. This will then be
accessible by the sub component in the `props` object. Note that there is an
+4 -5
View File
@@ -30,7 +30,7 @@ btn {
}
```
will produce a nice flash effect whenever the user click (or activate with the
will produce a nice flash effect whenever the user clicks (or activates with the
keyboard) the button.
## CSS Transitions
@@ -47,7 +47,7 @@ the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
directive is there to help.
Whenever a node has a `t-transition` directive, with a `name` value, the following
will happen:
sequence of events will happen:
At node insertion:
@@ -91,6 +91,5 @@ The `t-transition` directive can be applied on a node element or on a component.
Notes:
- more information on animations are available [here](animations.md).
- Owl does not support more than one transition on a single node, so the
`t-transition` expression must be a single value (i.e. no space allowed)
Owl does not support more than one transition on a single node, so the
`t-transition` expression must be a single value (i.e. no space allowed).
+128 -20
View File
@@ -3,7 +3,7 @@
OWL, React and Vue have the same main feature: they allow developers to build
declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences.
In this page, we try to highlight some of these differences. Obviously, some
In this page, we try to highlight some of these differences. Obviously, a lot of
effort was done to be fair. However, if you disagree with some of the points
discussed, feel free to open an issue/submit a PR to correct this text.
@@ -16,6 +16,7 @@ discussed, feel free to open an issue/submit a PR to correct this text.
- [Asynchronous rendering](#asynchronous-rendering)
- [Reactiveness](#reactiveness)
- [State Management](#state-management)
- [Hooks](#hooks)
## Size
@@ -24,11 +25,16 @@ than React and Vue. Also, jQuery is not the same kind of framework, but it is in
| Framework | Size (minified, gzipped) |
| ------------------------ | ------------------------ |
| OWL | 16kb |
| OWL | 18kb |
| Vue + VueX | 30kb |
| Vue + VueX + Vue Router | 39kb |
| React + ReactDOM + Redux | 40kb |
| jQuery | 30kb |
Note that those comparisons are not entirely fair, because we do not compare
the same exact set of features. For example, VueX and Vue Router support more
advanced use cases.
## Class Based
Both React and Vue moved away from defining components with classes. They prefer
@@ -40,6 +46,17 @@ contrast, Owl has only one mechanism: class-based components. We believe that Ow
components are fast enough for all our usecases, and making it as simple as
possible for developers is more valuable (for us).
Also, functions or class based components are more than just syntax. Functions
comes with a mindset of composition and class are about inheritance. Clearly,
both of these are important mechanisms for reusing code. Also, one does not
exclude the other.
It certainly looks like the world of UI frameworks is moving toward composition,
for many very good reasons. Owl is still good at composition (for example,
Owl supports slots, which is the primary mechanism to make generic reusable
components). But it can also use inheritance (and this is very important since
templates can also be inherited with `xpaths` transformations).
## Tooling/Build step
OWL is designed to be easy to use in a standalone way. For various reasons,
@@ -50,13 +67,23 @@ be used by simply adding a script tag to a page.
<script src="owl.min.js" />
```
In comparison, React encourages using JSX,
which necessitate a build step, and most Vue applications uses single file
components, which also necessitate a build step.
In comparison, React encourages using JSX, which necessitate a build step, and
most Vue applications uses single file components, which also necessitate a build step.
On the flipside, external tooling may make it harder to use in some case, but it
also brings a lot of benefits. And React/Vue have both a large ecosystem.
Note that since Owl is not dependant on any external tool nor libraries, it is
very easy to integrate into any build toolchain. Also, since we cannot rely on
additional tools, we made a lot of effort to make the most of the web platform.
For example, Owl uses the standard `xml` parser that comes with every browser.
Because of that, Owl did not have to write its own template parser. Another
example is the [`xml`](tags.md#xml-tag) tag helper function, which makes use of
native template literals to allow in a natural way to write `xml` templates
directly in the javascript code. This can be easily integrated with editor
plugins to have autocompletion inside the template.
## Templating
OWL uses its own QWeb engine, which compiles templates on the
@@ -79,7 +106,8 @@ into javascript functions. Note that Vue has a separate build which includes the
template compiler.
In contrast, most React applications do not use a templating language, but write
some JSX code, which is precompiled into plain JavaScript by a build step.
some JSX code, which is precompiled into plain JavaScript by a build step. This
example is done with the (kind of outdated) React class system:
```jsx
class Clock extends React.Component {
@@ -98,6 +126,20 @@ This has the advantage of having the full power of Javascript, but is less
structured than a template language. Note that the tooling is quite impressive:
there is a syntax highlighter for jsx here on github!
By comparison, here is the equivalent Owl component, written with the
[`xml`](tags.md#xml-tag) tag helper:
```js
class Clock extends Component {
static template = xml`
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
`;
}
```
## Asynchronous Rendering
This is actually a big difference between OWL and React/Vue: components in OWL
@@ -127,12 +169,14 @@ This may be dangerous (to stop the rendering waiting for the network), but it is
extremely powerful as well, as demonstrated by the Odoo Web Client.
Lazy loading static libraries can obviously be done with React/Vue, but it is
more convoluted.
more convoluted. For example, in Vue, you need to use a dynamic import keyword
that needs to be transpiled at build time in order for the component to be loaded
asynchronously (see [the documentation](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components)).
## Reactiveness
React has a simple model: whenever the state changes, it is
replaced with a new state (via the setState method). Then, the DOM is patched.
replaced with a new state (via the `setState` method). Then, the DOM is patched.
This is simple, efficient, and a little bit awkward to write.
Vue is a little bit different: it replace magically the properties in the state
@@ -207,30 +251,94 @@ keeps track of who get data, and retrigger a render when it was changed.
Owl store is a little bit like a mix of redux and vuex: it has actions (but not
mutations), and like VueX, it keeps track of the state changes. However, it does
not notify a component when the state changes. Instead, components need to connect
to the store like in redux, by inheriting the `ConnectedComponent` class.
to the store like in redux, with the `useStore` hook (see the [store documentation](store.md#connecting-a-component)).
```javascript
const actions = {
increment({ state }, val) {
state.counter += val;
state.counter.value += val;
}
};
const state = {
counter: 0
counter: { value: 0 }
};
const store = new owl.Store({ state, actions });
class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
increment() {
this.env.store.dispatch("increment");
}
class Counter extends Component {
static template = xml`
<button t-name="Counter" t-on-click="dispatch('increment')">
Click Me! [<t t-esc="counter.value"/>]
</button>`;
counter = useStore(state => state.counter);
dispatch = useDispatch();
}
const counter = new Counter({ store, qweb });
```
## Hooks
[Hooks](https://reactjs.org/docs/hooks-intro.html#motivation) recently took over
the React world. They solve a lot of seemingly unconnected problems: attach
reusable behavior to a component, in a composable way, extract stateful logic
from a component or reuse stateful logic between component, without changing your
component hierarchy.
Here is an example of the React `useState` hook:
```js
import React, { useState } from "react";
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
```
Because of the way React designed the hooks API, they only work for functional
components. But in that case, they really are powerful. Every major React library
is in the process of redesigning their API with hooks (for example,
[Redux](https://react-redux.js.org/next/api/hooks)).
Vue 2 does not have hooks, but the Vue project is working on its next version,
which will feature its new [composition API](https://vue-composition-api-rfc.netlify.com/).
This work is based on the new ideas introduced by React hooks.
From the way React and Vue introduce their hooks, it may look like hooks are not
compatible with class components. However, this is not the case, as shown by
Owl [hooks](hooks.md). They are inspired by both React and Vue. For example,
the `useState` hook is named after React, but its API is closer to the `reactive`
Vue hook.
Here is what the `Counter` example above look like in Owl:
```js
import { Component, Owl } from "owl";
import { xml } from "owl/tags";
class Example extends Component {
static template = xml`
<div>
<p>You clicked {count.value} times</p>
<button t-on-click="increment">Click me</button>
</div>`;
count = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
Since the Owl framework had hooks from early in its life, its main APIs
are designed to be interacted with hooks from the start. For example, the
`Context` and `Store` abstractions.
+131 -38
View File
@@ -29,7 +29,7 @@
OWL components are the building blocks for user interface. They are designed to be:
1. **declarative:** the user interface should be described in term of the state
1. **declarative:** the user interface should be described in terms of the state
of the application, not as a sequence of imperative steps.
2. **composable:** each component can seamlessly be created in a parent component by
@@ -72,7 +72,7 @@ class ClickCounter extends owl.Component {
Note that this code is written in ESNext style, so it will only run on the
latest browsers without a transpilation step.
This example show how a component should be defined: it simply subclasses the
This example shows how a component should be defined: it simply subclasses the
Component class. If no static `template` key is defined, then
Owl will use the component's name as template name. Here,
a state object is defined, by using the `useState` hook. It is not mandatory to use the state object, but it is certainly encouraged. The result of the `useState` call is
@@ -80,33 +80,87 @@ a state object is defined, by using the `useState` hook. It is not mandatory to
## Reference
An Owl component is a small class which represent a component or some UI element.
An Owl component is a small class which represents a component or some UI element.
It exists in the context of an environment (`env`), which is propagated from a
parent to its children. The environment needs to have a QWeb instance, which
will be used to render the component template.
Be aware that the name of the component may be significant: if a component does
not define a `template` key, then Owl will lookup in QWeb to
find a template with the component name (or one of its ancestor).
find a template with the component name (or one of its ancestors).
### Reactive system
OWL components can be made reactive by observing some part of their state. See the [hooks](hooks.md) section for more details.
OWL components are normal javascript classes. So, changing a component internal
state does nothing more:
The main idea is that the `useState` hook generate a proxy version of an object
(this is done by an [observer](observer.md)), which allows the component to
react to any change.
```js
class Counter extends Component {
static template = xml`<div t-on-click="increment"><t t-esc="state.value"/></div>`;
state = { value: 0 };
```javascript
const { useState } = owl.hooks;
class SomeComponent extends owl.Component {
state = useState({ a: 0, b: 1 });
increment() {
this.state.value++;
}
}
```
Note that there is an important limitation: hooks need to be called in the
constructor.
Clicking on the `Counter` component defined above will call the `increment`
method, but it will not rerender the component. To fix that, one could add an
explicit call to `render` in `increment`:
```js
increment() {
this.state.value++;
this.render();
}
```
However, it may be simple in this case, but it quickly become cumbersome, as a
component get more complex, and its internal state is modified by more than one
method.
A better way is to use the reactive system: by using the `useState` hook (see the
[hooks](hooks.md) section for more details), one can make Owl react to state
changes. The `useState` hook generates a proxy version of an object
(this is done by an [observer](observer.md)), which allows the component to
react to any change. So, the `Counter` example above can be improved like this:
```js
const { useState } = owl.hooks;
class Counter extends Component {
static template = xml`<div t-on-click="increment"><t t-esc="state.value"/></div>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
Obviously, we can call the `useState` hook more than once:
```js
const { useState } = owl.hooks;
class Counter extends Component {
static template = xml`
<div>
<span t-on-click="increment(counter1)"><t t-esc="counter1.value"/></span>
<span t-on-click="increment(counter2)"><t t-esc="counter2.value"/></span>
</div>`;
counter1 = useState({ value: 0 });
counter2 = useState({ value: 0 });
increment(counter) {
counter.value++;
}
}
```
Note that hooks are subject to one important [rule](hooks.md#one-rule): they need
to be called in the constructor.
### Properties
@@ -115,10 +169,23 @@ constructor.
- **`env`** (Object): the component environment, which contains a QWeb instance.
- **`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,
in some case. Note that `props` are owned by the parent, not by the component.
As such, it should not ever be modified by the component!!
- **`props`** (Object): this is an object containing all the properties given by
the parent to a child component. For example, in the following situation,
the parent component gives a `user` and a `color` value to the `ChildComponent`.
```xml
<div>
<ChildComponent user="state.user" color="color">
</div>
```
Note that `props` are owned by the parent, not by the component.
As such, it should not ever be modified by the component (otherwise you risk
unintended effects, since the parent may not be aware of the change)!!
The `props` can be modified dynamically by the parent. In that case, the
component will go through the following lifecycle methods: `willUpdateProps`,
`willPatch` and `patched`.
### Static Properties
@@ -182,7 +249,7 @@ We explain here all the public methods of the `Component` class.
we know that its state (or something in the environment, or ...) has changed.
In that case, it should simply set to `true`.
- **`unmount()`**: in case a component need to be detached/removed from the DOM, this
- **`unmount()`**: in case a component needs to be detached/removed from the DOM, this
method can be used. Most applications should not call `unmount`, this is more
useful to the underlying component system.
@@ -338,9 +405,9 @@ with the DOM (for example, through an external library) whenever the
component was patched. Note that this hook will not be called if the compoent is
not in the DOM (this can happen with components with `t-keepalive`).
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
Updating the component state in this hook is possible, but not encouraged.
One needs to be careful, because updates here will create an additional rendering, which in
turn will cause other calls to the `patched` method. So, we need to be particularly
careful at avoiding endless cycles.
#### `willUnmount()`
@@ -363,7 +430,7 @@ This is the opposite method of `mounted`.
The `catchError` method is useful when we need to intercept and properly react
to (rendering) errors that occur in some sub components. See the section on
[error handling](#error-handling)
[error handling](#error-handling).
### Root Component
@@ -610,6 +677,29 @@ to event `menu-loaded` will receive the payload in its `someMethod` handler
By convention, we use KebabCase for the name of _business_ events.
The `t-on` directive allows to prebind some arguments. For example,
```xml
<button t-on-click="someMethod(expr)">Do something</button>
```
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context.
One can also directly specify inline statements. For example,
```xml
<button t-on-click="state.counter++">Increment counter</button>
```
Here, `state` must be defined in the rendering context (typically the component)
as it will be translated to:
```js
button.addEventListener("click", () => { component.state.counter++; });
```
In order to remove the DOM event details from the event handlers (like calls to
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
specified as additional suffixes of the `t-on` directive.
@@ -629,14 +719,14 @@ the order may matter. For instance `t-on-click.prevent.self` will prevent all
clicks while `t-on-click.self.prevent` will only prevent clicks on the element
itself.
The `t-on` directive also allows to prebind some arguments. For example,
Finally, empty handlers are tolerated as they could be defined only to apply
modifiers. For example,
```xml
<button t-on-click="someMethod(expr)">Do something</button>
<button t-on-click.stop="">Do something</button>
```
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context.
This will simply stop the propagation of the event.
### Form Input Bindings
@@ -717,7 +807,7 @@ Like event handling, the `t-model` directive accepts some modifiers:
| Modifier | Description |
| --------- | -------------------------------------------------------------------- |
| `.lazy` | update the value on the `change` event (default is on `input` event) |
| `.number` | tries to parse the value to a number (using `parseFloat`) |
| `.number` | try to parse the value to a number (using `parseFloat`) |
| `.trim` | trim the resulting value |
For example:
@@ -733,9 +823,10 @@ Note: the online playground has an example to show how it works.
### `t-key` Directive
Even though Owl tries to be as declarative as possible, some DOM state is still
locked inside the DOM: for example, the scrolling state, the current user selection,
the focused element or the state of an input. This is why we use a virtual dom
Even though Owl tries to be as declarative as possible, the DOM does not fully
expose its state declaratively in the DOM tree. For example, the scrolling state,
the current user selection, the focused element or the state of an input are not
set as attribute in the DOM tree. This is why we use a virtual dom
algorithm to keep the actual DOM node as much as possible. However, this is
sometimes not enough, and we need to help Owl decide if an element is actually
the same, or is different. The `t-key` directive is used to give an identity to an element.
@@ -847,7 +938,7 @@ Here is what Owl will do:
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
- hard to tell how a component should be used, by looking at its code.
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parent.
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
A props type system would solve both issues, by describing the types and shapes
of the props. Here is how it works in Owl:
@@ -1072,9 +1163,9 @@ Here are a few tips on how to work with asynchronous components:
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-component`) is there to help you. When this directive is met, a new rendering
second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs))
4. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When
this component is met, a new rendering
sub tree is created, such that the rendering of that component (and its
children) is not tied to the rendering of the rest of the interface. It can
be used on an asynchronous component, to prevent it from delaying the
@@ -1086,7 +1177,9 @@ Here are a few tips on how to work with asynchronous components:
```xml
<div t-name="ParentComponent">
<SyncChild />
<AsyncChild t-asyncroot="1"/>
<AsyncRoot>
<AsyncChild/>
</AsyncRoot>
</div>
```
@@ -1103,7 +1196,7 @@ from lifecycle hooks): the `catchError` hook.
Whenever the `catchError` lifecycle hook is implemented, all errors coming from
sub components rendering and/or lifecycle method calls will be caught and given
to the `catchError` method. This allow us to properly handle the error, and to
to the `catchError` method. This allows us to properly handle the error, and to
not break the application.
For example, here is how we could implement an `ErrorBoundary` component:
+1 -1
View File
@@ -11,7 +11,7 @@
## Overview
The `Context` object provides a way to share data between an arbitrary number
of component. Usually, data is passed from a parent to its children component,
of components. Usually, data is passed from a parent to its children component,
but when we have to deal with some mostly global information, this can be
annoying, since each component will need to pass the information to each children,
even though some or most of them will not use the information.
+40 -40
View File
@@ -35,7 +35,7 @@ Owl hooks serve the same purpose, except that they work for class components
there seems to be the misconception that hooks are in opposition to class. This
is clearly not true, as shown by Owl hooks).
Hooks works beautifully with Owl components: they solve the problems mentioned
Hooks work beautifully with Owl components: they solve the problems mentioned
above, and in particular, they are the perfect way to make your component
reactive.
@@ -127,7 +127,7 @@ class SomeComponent extends Component {
### One rule
There is only one rule: every hook for a component have to be called in the
There is only one rule: every hook for a component has to be called in the
constructor (or in class fields):
```js
@@ -152,14 +152,14 @@ class SomeComponent extends Component {
}
```
Hooks can get a reference to the currently being defined component with the
`Component.current` static property. This is why they need to be called in the
constructor.
In a hook, the `Component.current` static property is the reference to the
component instance that is currently being created. Hooks need to be called in
the constructor to ensure that this reference is properly set.
### `useState`
The `useState` hook is certainly the most important hooks for Owl components:
this is what enables component to be reactive, to react to state change.
The `useState` hook is certainly the most important hook for Owl components:
this is what allows a component to be reactive, to react to state change.
The `useState` hook has to be given an object or an array, and will return
an observed version of it (using a `Proxy`).
@@ -183,25 +183,25 @@ class Counter extends owl.Component {
### `onMounted`
`onMounted` is not an user hook, but is a building block designed to help make useful
`onMounted` is not a user hook, but is a building block designed to help make useful
abstractions. `onMounted` registers a callback, which will be called when the component
is mounted (see example on top of this page).
### `onWillUnmount`
`onWillUnmount` is not an user hook, but is a building block designed to help make useful
`onWillUnmount` is not a user hook, but is a building block designed to help make useful
abstractions. `onWillUnmount` registers a callback, which will be called when the component
is unmounted (see example on top of this page).
### `onWillPatch`
`onWillPatch` is not an user hook, but is a building block designed to help make useful
`onWillPatch` is not a user hook, but is a building block designed to help make useful
abstractions. `onWillPatch` registers a callback, which will be called just
before the component patched.
### `onPatched`
`onPatched` is not an user hook, but is a building block designed to help make useful
`onPatched` is not a user hook, but is a building block designed to help make useful
abstractions. `onPatched` registers a callback, which will be called just
after the component patched.
@@ -220,18 +220,18 @@ be ready whenever the component is rendered:
```js
function useLoader() {
const component = Component.current;
const record = useState({});
onWillStart(async () => {
const recordId = component.props.id;
Object.assign(record, await fetchSomeRecord(recordId));
});
return record;
const component = Component.current;
const record = useState({});
onWillStart(async () => {
const recordId = component.props.id;
Object.assign(record, await fetchSomeRecord(recordId));
});
return record;
}
```
Note that this example does not update the record value whenever props are
updated. For that situation, we need to use the `onWillUpdateProps` hook.
updated. For that situation, we need to use the `onWillUpdateProps` hook.
### `onWillUpdateProps`
@@ -241,17 +241,17 @@ useful to perform some asynchronous task such as fetching updated data.
```js
function useLoader() {
const component = Component.current;
const record = useState({});
const component = Component.current;
const record = useState({});
async function updateRecord(id) {
Object.assign(record, await fetchSomeRecord(id));
}
async function updateRecord(id) {
Object.assign(record, await fetchSomeRecord(id));
}
onWillStart(() => updateRecord(component.props.id));
onWillUpdateProps(nextProps => updateRecord(nextProps.id));
onWillStart(() => updateRecord(component.props.id));
onWillUpdateProps(nextProps => updateRecord(nextProps.id));
return record;
return record;
}
```
@@ -305,7 +305,7 @@ The `t-ref` directive also accepts dynamic values with string interpolation
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
```
Here, the references needs to be set like this:
Here, the references need to be set like this:
```js
this.ref1 = useRef("component_1");
@@ -321,8 +321,8 @@ The environment is sometimes useful to share some common information between
all components. But sometimes, we want to _scope_ that knowledge to a subtree.
For example, if we have a form view component, maybe we would like to make some
`model` object available to all sub component, but not to the whole application.
This is where the `useSubEnv` hook may be useful: it let a component add some
`model` object available to all sub components, but not to the whole application.
This is where the `useSubEnv` hook may be useful: it lets a component add some
information to the environment in a way that only the component and its children
can access it:
@@ -359,39 +359,39 @@ getters. See the [store documentation](store.md) for more information.
### Making customized hooks
Hooks are a wonderful way to organize the code of a complex component by feature
instead of by lifecycle methods. They are like mixins, except that they can be
instead of by lifecycle methods. They are like mixins, except that they can be
easily composed together.
But, like every good things in life, hooks should be used with moderation. They are
not the solution to every problem.
- they may be overkill: if your component needs to perform some action specific
to him (so, the specific code does not need to be shared), there is nothing
to itself (so, the specific code does not need to be shared), there is nothing
wrong with a simple class method:
```js
// maybe overkill
class A extends Component {
constructor(...args) {
super(...args);
useMySpecificHook()
super(...args);
useMySpecificHook();
}
}
// ok
class B extends Component {
constructor(...args) {
super(...args);
this.performSpecificTask()
super(...args);
this.performSpecificTask();
}
}
```
Note that the second solution is easier to extend in sub components.
- they may be harder to test: if a customized hook inject some external side
- they may be harder to test: if a customized hook injects some external side
effect dependency, then it is harder to test without doing some non obvious
manipulation. For example, assume that we want to give a reference to a
manipulation. For example, assume that we want to give a reference to a
router in a `useRouter` hook. We could do this:
```js
@@ -402,7 +402,7 @@ not the solution to every problem.
}
```
As you can see, this does not *hook* into the internal of the component. It
As you can see, this does not _hook_ into the internal of the component. It
simply returns a global object, which is difficult to mock.
A better way would be to do something like this: get the reference from the
@@ -410,7 +410,7 @@ not the solution to every problem.
```js
function useRouter() {
return Component.current.env.router;
return Component.current.env.router;
}
```
+23
View File
@@ -0,0 +1,23 @@
# 🦉 Miscellaneous 🦉
## `AsyncRoot`
When this component is used, a new rendering sub tree is created, such that the
rendering of that component (and its children) is not tied to the rendering of
the rest of the interface. It can be used on an asynchronous component, to
prevent it from delaying the rendering of the whole interface, or on a
synchronous one, such that its rendering isn't delayed by other (asynchronous)
components. Note that this directive has no effect on the first rendering, but
only on subsequent ones (triggered by state or props changes).
```xml
<div t-name="ParentComponent">
<SyncChild />
<AsyncRoot>
<AsyncChild/>
</AsyncRoot>
</div>
```
The `AsyncRoot` assumes that there is exactly one root node inside it. It can
be a dom node or a component.
+39 -7
View File
@@ -1,10 +1,16 @@
# 🦉 Observer 🦉
Owl need to be able to react to state changes. For example, whenever the state
of a component is changed, we need to rerender it. To help with that, we have
an Observer class. Its job is to observe some object state, and react to any
change. To do that, it recursively replace all keys of the observed state by
getters and setters.
Owl needs to be able to react to state changes. For example, whenever the state
of a component is changed, Owl needs to rerender it. To help with that, there is
an Observer class. Its job is to observe the state of an object (or array), and
to react to any change. The observer is implemented with the native `Proxy`
object. Note that this means that it will not work on older browsers.
Note that the `Observer` is used by the `useState` and `useContext` hooks. This
is the way most Owl applications will create observers. For the majority of
use cases, there is no need to directly instantiate an observer.
## Example
For example, this code will display `update` in the console:
@@ -16,5 +22,31 @@ const obj = observer.observe({ a: { b: 1 } });
obj.a.b = 2;
```
The observer is implemented with the native `Proxy` object. Note that this
means that it will not work on older browsers.
This example shows that an observer can observe nested properties.
## Reference
**observe** An observer can observe multiple values with the `observe` method.
This method takes an object or an array as its argument and will return a proxy
(which is mapped to the initial object/array). With this proxy, the observer
can detect whenever any internal value is changed.
**Registering a callback** Whenever an observer sees a state change, it will
call its `notifyCB` method. No additional information is given to the callback.
**deepRevNumber** Each observed value has an internal revision number, which
is incremented every time the value is observed. Sometimes, it can be useful
to obtain that number:
```js
const observer = new owl.Observer();
const obj = observer.observe({ a: { b: 1 } });
observer.deepRevNumber(obj.a); // 1
obj.a.b = 2;
observer.deepRevNumber(obj.a); // 2
```
The `deepRevNumber` can also return 0, which indicates that the value is not
observed.
+3 -6
View File
@@ -55,7 +55,7 @@ To build an application (or a sub-part of an application), we need two things:
- an environment: it is the global context in which we are working. It needs to
contain a QWeb instance (preloaded with templates), and anything else that we
need. In practice, it could context some user session information, some
need. In practice, it could be used to contain 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 component, which can
@@ -76,10 +76,7 @@ const useState = owl.hooks.useState;
class ClickCounter extends owl.Component {
static template = "clickcounter";
constructor() {
super(...arguments);
this.state = useState({ value: 0 });
}
state = useState({ value: 0 });
increment() {
this.state.value++;
@@ -90,7 +87,7 @@ class ClickCounter extends owl.Component {
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const templates = await owl.utils.loadFile("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
+29 -17
View File
@@ -20,7 +20,7 @@
## Overview
[QWeb](https://www.odoo.com/documentation/12.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the HTML.
@@ -33,7 +33,7 @@ To avoid element rendering, a placeholder element `<t>` is also available, which
<span t-if="somecondition">Some string</span>
<ul t-else="1">
<li t-foreach="messages" t-as="message">
<t t-esc="message">
<t t-esc="message"/>
</li>
</ul>
</div>
@@ -85,7 +85,7 @@ instantiated:
const qweb = new owl.QWeb();
```
It's API is quite simple:
Its API is quite simple:
- **`constructor(data)`**: constructor. Takes an optional string to add initial
templates (see `addTemplates` for more information on format of the string).
@@ -100,7 +100,8 @@ It's API is quite simple:
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.
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply
ignore templates added for a second time. Otherwise, `QWeb` will crash.
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute).
@@ -128,13 +129,13 @@ It's API is quite simple:
const str = qweb.renderToString("someTemplate", somecontext);
```
- **`registerTemplate(name, template)`**: static function to register an global
- **`registerTemplate(name, template)`**: static function to register a global
QWeb template. This is useful for commonly used components accross the
application, and for making a template available to an application without
having a reference to the actual QWeb instance.
```js
QWeb.registerTemplate("mytemplate", `<div>some template`);
QWeb.registerTemplate("mytemplate", `<div>some template</div>`);
```
- **`registerComponent(name, Component)`**: static function to register an OWL Component
@@ -154,7 +155,7 @@ It's API is quite simple:
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
has an extra responsibility: it can act as an event bus for internal communication
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
## Reference
@@ -165,7 +166,7 @@ specific extensions are documented in various other parts of the documentation.
### White Spaces
White spaces in a templates are handled in a special way:
White spaces in a template are handled in a special way:
- consecutive whitespaces are always condensed to a single whitespace
- if a whitespace-only text node contains a linebreak, it is ignored
@@ -207,7 +208,7 @@ component). For example, `a + b.c(d)` will be converted into:
context["a"] + context["b"].c(context["d"]);
```
It is useful to explain the various rules that applies on these expressions:
It is useful to explain the various rules that apply on these expressions:
1. it should be a simple expression which returns a value. It cannot be a statement.
@@ -389,14 +390,25 @@ If an expression evaluates to a falsy value, it will not be set at all:
<div t-att-foo="false"/> <!-- result: <div></div> -->
```
There is another way to format a string attribute: the `t-attf-` directive. With
it, you get string interpolation:
It is sometimes convenient to format an attribute with string interpolation. In
that case, the `t-attf-` directive can be used. It is useful when we need to mix
literal and dynamic elements, such as css classes.
```xml
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
```
If we need completely dynamic attribute names, then there is an additional
directive: `t-att`, which takes either an object (with keys mapping to their
values) or a pair `[key, value]`. For example:
```xml
<div t-att="{'a': 1, 'b': 2}"/> <!-- result: <div a="1" b="2"></div> -->
<div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->
```
### Loops
QWeb has an iteration directive `t-foreach` which take an expression returning the
@@ -449,16 +461,16 @@ the context of the `t-foreach`, the value is copied at the end of the foreach
into the global context.
```xml
<t t-set="existing_variable" t-value="False"/>
<t t-set="existing_variable" t-value="false"/>
<!-- existing_variable now False -->
<p t-foreach="Array(3)" t-as="i">
<t t-set="existing_variable" t-value="True"/>
<t t-set="new_variable" t-value="True"/>
<!-- existing_variable and new_variable now True -->
<t t-set="existing_variable" t-value="true"/>
<t t-set="new_variable" t-value="true"/>
<!-- existing_variable and new_variable now true -->
</p>
<!-- existing_variable always True -->
<!-- existing_variable always true -->
<!-- new_variable undefined -->
```
@@ -531,4 +543,4 @@ will stop execution if the browser dev tools are open.
<t t-log="foo"/>
```
will print 42 to the console
will print 42 to the console.
+5 -2
View File
@@ -29,6 +29,8 @@ owl
useStore
useDispatch
useGetters
misc
AsyncRoot
router
Link
RouteComponent
@@ -39,11 +41,11 @@ owl
debounce
escape
loadJS
loadTemplates
loadFile
shallowEqual
whenReady
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
## Reference
@@ -53,6 +55,7 @@ Note that for convenience, the `useState` hook is also exported at the root of t
- [Context](context.md)
- [Event Bus](event_bus.md)
- [Hooks](hooks.md)
- [Misc](misc.md)
- [Observer](observer.md)
- [QWeb](qweb.md)
- [Router](router.md)
+7
View File
@@ -97,6 +97,13 @@ The `Router` constructor takes three arguments:
- an optional object (with the only key `mode` which can be `history` (default
value) or `hash`).
`history` will use the browser [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) as the mechanism to manage URL.\
Example: `https://yourdomain.tld/my_custom_route`.\
For this mechanism to work, you need a way to configure your web server accordingly.
`hash` will manipulate the hash of the URL.\
Example: `https://yourdomain.tld/index.html#/my_custom_route`.
```js
const ROUTES = [...];
const router = new owl.router.Router(env, ROUTES, {mode: 'history'});
+55 -23
View File
@@ -10,6 +10,8 @@
- [Getters](#getters)
- [Connecting a Component](#connecting-a-component)
- [`useStore`](#usestore)
- [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters)
- [Semantics](#semantics)
- [Good Practices](#good-practices)
@@ -17,12 +19,12 @@
Managing the state in an application is not an easy task. In some cases, the
state of an application can be part of the component tree, in a natural way.
However, there are situations where some part of the state need to be displayed
However, there are situations where some parts of the state need to be displayed
in various parts of the user interface, and then, it is not obvious which
component should own which part of the state.
Owl's solution to this issue is a centralized store. It is a class that owns
some (or all) state, and let the developer update it in a structured way, with
some (or all) state, and lets the developer update it in a structured way, with
`actions`. Owl components can then connect to the store, and will be updated if
necessary.
@@ -113,9 +115,10 @@ const actions = {
```
The first argument to an action method is an object with four keys:
- `state`: the current state of the store content,
- `dispatch`: a function that can be used to dispatch other actions,
- `getters`: an object containing all getters defined in the store
- `getters`: an object containing all getters defined in the store,
- `env`: the current environment. This is useful sometimes, in particular if
an action needs to apply some side effects (such as performing an rpc), and
the `rpc` method is located in the environment.
@@ -132,7 +135,7 @@ call.
Also, it is important to be aware that we need to be careful with asynchronous
logic. Each state change will potentially trigger a rerendering, so we need to
make sure that we do not have a partial corrupted state. Here is an example that
make sure that we do not have a partially corrupted state. Here is an example that
is likely not a good idea:
```javascript
@@ -185,7 +188,7 @@ transform the data contained in the store.
const getters = {
getPost({ state }, id) {
const post = state.posts.find(p => p.id === id);
const author = state.authors.find(a => (a.id = post.id));
const author = state.authors.find(a => a.id === post.id);
return {
id,
author,
@@ -200,18 +203,16 @@ const post = store.getters.getPost(id);
Getters take _at most_ one argument.
Note that getters are cached if they don't take any argument, or their argument
is a string or a number.
Note that getters are not cached.
### Connecting a Component
At some point, we need a way to interact with the store from a component. This
can be done with the help of the three store hooks:
- `useStore` to subscribe a component to some part of the store state,
- `useDispatch` to get a reference to a dispatch function
- `useGetters` to get a reference to the getters defined in the store.
- [`useStore`](#usestore) to subscribe a component to some part of the store state,
- [`useDispatch`](#usedispatch) to get a reference to a dispatch function,
- [`useGetters`](#usegetters) to get a reference to the getters defined in the store.
Assume we have this store:
@@ -223,7 +224,7 @@ const actions = {
};
const state = {
counter: {value: 0}
counter: { value: 0 }
};
const store = new owl.Store({ state, actions });
```
@@ -245,46 +246,77 @@ const counter = new Counter({ store, qweb });
</button>
```
### `useStore``
### `useStore`
The `useStore` hook is used to select some part of the store state. It accepts
two arguments:
- a selector function, which takes the store state as first argument (and the
component props as second argument) and returns
an object or an array (which will be then observed)
an object or an array (which will be then observed),
- optionally, an object with a `store` key (if we want to override the default
store) and an equality function (if we want to specialize the comparison)
store) and an equality function (if we want to specialize the comparison).
If the `useStore` callback selects a sub part of the store state, the component
will only be rerendered whenever this part of the state changes. Otherwise, it
will perform a strict equality check and updates the component every time this
will perform a strict equality check and will update the component every time this
check fails.
### `useDispatch`
The `useDispatch` hook is useful when a component needs to be able to dispatch
actions. It takes an optional argument, which is a store. If not given, it will
use the store in the environment.
Note that a component does not need to be connected in any other way to the store.
For example:
```js
class DoSomethingButton extends Component {
static template = xml`<button t-on-click="dispatch('something')">Click</button>`;
dispatch = useDispatch();
}
```
### `useGetters`
The `useGetters` hook is useful when a component needs to be able to use the
getters defined in a store. It takes an optional argument, which is a store. If
not given, it will use the store in the environment.
Note that a component does not need to be connected in any other way to the store.
For example:
```js
class InfoButton extends Component {
static template = xml`<span><t t-esc="getters.somevalue()"></span>`;
getters = useGetters();
}
```
### Semantics
The `Store` class and the `useStore` hook 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
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.
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
a situation where some part of the UI is updated and some other part 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.
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 `useStore`
call and a `Message` component could get the data of its own
message
message,
- since the `useStore` 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.
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 Tags 🦉
Tags are very small helper to make it easy to write inline templates. There is
Tags are very small helpers to make it easy to write inline templates. There is
only one currently available tag: `xml`, but we plan to add other tags later,
such as a `css` tag, which will be used to write single file components.
+93 -8
View File
@@ -7,9 +7,10 @@ functions are all available in the `owl.utils` namespace.
- [`whenReady`](#whenready): executing code when DOM is ready
- [`loadJS`](#loadjs): loading script files
- [`loadTemplates`](#loadtemplates): loading xml files
- [`loadFile`](#loadfile): loading a file (useful for templates)
- [`escape`](#escape): sanitizing strings
- [`debounce`](#debounce): limiting rate of function calls
- [`shallowEqual`](#shallowequal): shallow object comparison
## `whenReady`
@@ -18,13 +19,15 @@ not ready yet, resolved directly otherwise). If called with a callback as
argument, it executes it as soon as the DOM ready (or directly).
```js
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([templates]) {
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function([templates]) {
const qweb = new owl.QWeb(templates);
const app = new App({ qweb });
app.mount(document.body);
});
```
or alternatively:
```js
owl.utils.whenReady(function() {
const qweb = new owl.QWeb();
@@ -35,10 +38,12 @@ owl.utils.whenReady(function() {
## `loadJS`
`loadJS` takes a url (string) for a javascript resource, and loads it. It returns
a promise, so the caller can properly react when it is ready. Also, it is smart:
it maintains a list of urls previously loaded (or currently being loaded), and
prevent doing twice the work.
`loadJS` takes a url (string) for a javascript resource, and loads it (by adding
a script tag in the document head). It returns a promise, so the caller can
properly reacts when it is ready. Also, it is smart: it maintains a list of urls
previously loaded (or currently being loaded), and prevent doing twice the work.
For example, it is useful for lazy loading external libraries:
```js
class MyComponent extends owl.Component {
@@ -48,16 +53,96 @@ class MyComponent extends owl.Component {
}
```
## `loadTemplates`
## `loadFile`
`loadFile` is a helper function to fetch a file. It simply
performs a `GET` request and returns the resulting string in a promise. The
initial usecase for this function is to load a template file. For example:
```js
async function makeEnv() {
const templates = await owl.utils.loadTemplates("templates.xml");
const templates = await owl.utils.loadFile("templates.xml");
const qweb = new owl.QWeb(templates);
return { qweb };
}
```
Note that unlike `loadJS`, this function returns the content of the file as a
string. It does not add a `script` tag or any other side effect.
## `escape`
Sometimes, we need to display dynamic data (for example user-generated data) in
the user interface. If this is done by a `QWeb` template, it is not an issue:
```xml
<div><t t-esc="user.data"/></div>
```
The `QWeb` engine will create a `div` node and add the content of the `user.data`
string as a text node, so the web browser will not parse it as html. However,
it may be a problem if this is done with some javascript code like this:
```js
class BadComponent extends Component {
// some template with a ref to a div
// some code ...
mounted() {
this.divRef.el.innerHTML = this.state.value;
}
}
```
In this case, the content of the `div` will be parsed as html, which may inject
unwanted behaviour. To fix this, the `escape` function will simply transform a
string into an escaped version of the same string, which will be properly displayed
by the browser, but which will not be parsed as html (for example, `"<ok>"` is
escaped to the string: `"&lt;ok&gt;"`). So, the bad example above can be fixed
with the following change:
```js
this.divRef.el.innerHTML = owl.utils.escape(this.state.value);
```
## `debounce`
The `debounce` function is useful when we want to limit the number of times some
function/action is perfomed. For example, this may be useful to prevent issue
with people double clicking on a button.
It takes three arguments:
- `func` (function): this is the function that will be rate limited
- `wait` (number): this is the number of milliseconds that we want to use to
rate limit the function `func`
- `immediate` (optional, boolean, default=false): if `immediate` is true, the
function will be triggered immediately (leading edge of the interval). If false,
the function will be triggered at the end (trailing edge).
It returns a function. For example:
```js
const debounce = owl.utils.debounce;
window.addEventListener("mousemove", debounce(doSomething, 100));
```
As this example shows, it is usualy useful for event handlers which are triggered
very quickly, such as `scroll` or `mousemove` events.
## `shallowEqual`
This function checks if two objects have the same values assigned to each keys:
```js
shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); // true
shallowEqual({ a: 1, b: 2 }, { a: 1, b: 3 }); // false
```
However, for performance reasons, it assumes that the two objects have the same
keys. If we are in a situation where this is not guaranteed, the following code
will work:
```js
const completeShallowEqual = (a, b) => shallowEqual(a, b) && shallowEqual(b, a);
```
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "owl-framework",
"version": "0.23.0",
"version": "0.24.0",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"scripts": {
@@ -14,7 +14,8 @@
"tools:serve": "python3 tools/server.py || python tools/server.py",
"tools": "npm run build && npm run tools:serve",
"pretools:watch": "npm run build",
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\""
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"",
"prettier": "prettier {src/**/*.ts,tests/**/*.ts,doc/**/*.md} --write"
},
"repository": {
"type": "git",
+46
View File
@@ -0,0 +1,46 @@
# 🦉 OWL Roadmap 🦉
- Current version: 0.24.0
- Status: mostly stable
This roadmap is only an attempt at predicting Owl's future. Everything may
change!
### October 2019
We plan to complete the following tasks:
- improve API for root widgets (issue #306),
- replace `t-keepalive`, and maybe `t-transition` by components (issue #295).
### November 2019
Once the previous tasks are done, release version 1.0alpha. This means that the
API should be stable. But it could change a little bit if we need it for our
work on Odoo.
### End of 2019
Release v1.0
- API should be stable,
- we will use semantic versioning,
- we will maintain a changelog and an upgrade guide.
### 1.x
- add chrome and firefox devtools,
- add support for single file components,
- fix every bugs,
- improve documentation,
- small backward compatible improvements.
### 2.x (2021? 2022?)
Maybe:
- reimplement vdom to use *block* system, like Vue 3,
- refactor `QWeb` to use an intermediate representation (some kind of AST) to
allow additional optimisations.
+100 -147
View File
@@ -2,7 +2,9 @@ import { Observer } from "../core/observer";
import { CompiledTemplate, QWeb } from "../qweb/index";
import { h, patch, VNode } from "../vdom/index";
import "./directive";
import { Fiber } from "./fiber";
import "./props_validation";
import { Scheduler } from "./scheduler";
/**
* Owl Component System
@@ -11,7 +13,6 @@ import "./props_validation";
* contains:
*
* - the Env interface (generic type for the environment)
* - the Fiber interface (owl metadata attached to a rendering)
* - the Internal interface (the owl specific metadata attached to a component)
* - the Component class
*/
@@ -19,6 +20,8 @@ import "./props_validation";
//------------------------------------------------------------------------------
// Types/helpers
//------------------------------------------------------------------------------
const raf = window.requestAnimationFrame.bind(window);
export const scheduler = new Scheduler(raf);
/**
* An Env (environment) is an object that will be (mostly) shared between all
@@ -34,27 +37,6 @@ export interface Env {
[key: string]: any;
}
/**
* Fibers are small abstractions designed to contain all the internal state
* associated to a "rendering work unit", relative to a specific component.
*
* A rendering will cause the creation of a fiber for each impacted components.
*/
export interface Fiber<Props> {
force: boolean;
rootFiber: Fiber<any> | null;
isCancelled: boolean;
scope: any;
vars: any;
patchQueue: Fiber<any>[];
component: Component<any, any>;
vnode: VNode | null;
props: Props;
promise: Promise<VNode> | null;
// handlers?: any;
// mountedHandlers?: any;
}
/**
* This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the
@@ -65,6 +47,7 @@ interface Internal<T extends Env, Props> {
// relationships
readonly id: number;
vnode: VNode | null;
pvnode: VNode | null;
isMounted: boolean;
isDestroyed: boolean;
@@ -77,11 +60,11 @@ interface Internal<T extends Env, Props> {
// the component instance back whenever the template is rerendered.
cmap: { [key: number]: number };
currentFiber: Fiber<Props> | null;
currentFiber: Fiber | null;
boundHandlers: { [key: number]: any };
observer: Observer | null;
render: CompiledTemplate;
renderFn: CompiledTemplate;
mountedCB: Function | null;
willUnmountCB: Function | null;
willPatchCB: Function | null;
@@ -183,6 +166,7 @@ export class Component<T extends Env, Props extends {}> {
this.__owl__ = {
id: id,
vnode: null,
pvnode: null,
isMounted: false,
isDestroyed: false,
parent: p,
@@ -197,7 +181,7 @@ export class Component<T extends Env, Props extends {}> {
willStartCB: null,
willUpdatePropsCB: null,
observer: null,
render: qweb.render.bind(qweb, this.__getTemplate(qweb)),
renderFn: qweb.render.bind(qweb, this.__getTemplate(qweb)),
classObj: null,
refs: null
};
@@ -296,28 +280,33 @@ export class Component<T extends Env, Props extends {}> {
async mount(target: HTMLElement, renderBeforeRemount: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return Promise.resolve();
}
if (__owl__.vnode && !renderBeforeRemount) {
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
}
return;
}
const fiber = this.__createFiber(false, undefined, undefined, undefined);
const fiber = new Fiber(null, this, this.props, undefined, undefined, false);
if (!__owl__.vnode) {
fiber.promise = this.__prepareAndRender(fiber);
const vnode = await fiber.promise;
if (__owl__.isDestroyed) {
// component was destroyed before we get here...
return;
}
this.__patch(vnode);
} else if (renderBeforeRemount) {
fiber.patchQueue.push(fiber);
fiber.promise = this.__render(fiber);
await fiber.promise;
this.__applyPatchQueue(fiber);
}
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
this.__prepareAndRender(fiber);
} else {
this.__render(fiber);
}
return new Promise(resolve => {
scheduler.addFiber(fiber, () => {
if (!__owl__.isDestroyed) {
this.__patch(fiber.vnode);
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
}
}
resolve();
});
});
}
/**
@@ -342,19 +331,22 @@ export class Component<T extends Env, Props extends {}> {
*/
async render(force: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
if (
(!__owl__.isMounted && !__owl__.currentFiber) ||
(__owl__.currentFiber && !__owl__.currentFiber.isRendered)
) {
return;
}
const fiber = this.__createFiber(force, undefined, undefined, undefined);
fiber.patchQueue.push(fiber);
fiber.promise = this.__render(fiber);
await fiber.promise;
if (__owl__.isMounted && fiber === __owl__.currentFiber) {
// 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(fiber);
}
const fiber = new Fiber(null, this, this.props, undefined, undefined, force);
this.__render(fiber);
return new Promise(resolve => {
scheduler.addFiber(fiber.root, () => {
if (__owl__.isMounted && fiber === fiber.root) {
fiber.patchComponents();
}
resolve();
});
});
}
/**
@@ -407,27 +399,6 @@ export class Component<T extends Env, Props extends {}> {
// Private
//--------------------------------------------------------------------------
/**
* This method is a helper to create a fiber element.
*/
__createFiber(force, scope, vars, parent?: Fiber<any>): Fiber<Props> {
const fiber: Fiber<Props> = {
force,
scope,
vars,
rootFiber: null,
isCancelled: false,
component: this,
vnode: null,
patchQueue: parent ? parent.patchQueue : [],
props: this.props,
promise: null
};
fiber.rootFiber = parent ? parent.rootFiber : fiber;
this.__owl__.currentFiber = fiber;
return fiber;
}
/**
* Private helper to perform a full destroy, from the point of view of an Owl
* component. It does not remove the el (this is done only once on the top
@@ -478,7 +449,7 @@ export class Component<T extends Env, Props extends {}> {
__owl__.mountedCB();
}
} catch (e) {
errorHandler(e, this);
console.error(e); // TODO : add a test
}
}
@@ -504,25 +475,35 @@ export class Component<T extends Env, Props extends {}> {
*/
async __updateProps(
nextProps: Props,
parentFiber: Fiber<any>,
scope?: any,
vars?: any
parentFiber: Fiber,
scope: any,
vars: any,
previousSibling?: Fiber | null
): Promise<void> {
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const __owl__ = this.__owl__;
const fiber = new Fiber(parentFiber, this, this.props, scope, vars, parentFiber.force);
if (!parentFiber.child) {
parentFiber.child = fiber;
} else {
previousSibling!.sibling = fiber;
}
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
}
await Promise.all([
this.willUpdateProps(nextProps),
this.__owl__.willUpdatePropsCB && this.__owl__.willUpdatePropsCB(nextProps)
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps)
]);
if (fiber.isCancelled) {
return;
}
this.props = nextProps;
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
fiber.patchQueue.push(fiber);
await this.__render(fiber);
this.__render(fiber);
}
}
@@ -534,6 +515,7 @@ export class Component<T extends Env, Props extends {}> {
const __owl__ = this.__owl__;
const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
__owl__.currentFiber = null;
}
/**
@@ -541,10 +523,15 @@ export class Component<T extends Env, Props extends {}> {
* subcomponent is created. It gets its scope and vars, if any, from the
* parent template.
*/
__prepare(parentFiber: Fiber<any>, scope: any, vars: any): Promise<VNode> {
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
fiber.promise = this.__prepareAndRender(fiber);
return fiber.promise;
__prepare(parentFiber: Fiber, scope: any, vars: any, previousSibling?: Fiber | null) {
const fiber = new Fiber(parentFiber, this, this.props, scope, vars, parentFiber.force);
fiber.shouldPatch = false;
if (!parentFiber.child) {
parentFiber.child = fiber;
} else {
previousSibling!.sibling = fiber;
}
return this.__prepareAndRender(fiber);
}
__getTemplate(qweb: QWeb): string {
@@ -570,68 +557,66 @@ export class Component<T extends Env, Props extends {}> {
}
return p._template;
}
async __prepareAndRender(fiber: Fiber<Props>): Promise<VNode> {
async __prepareAndRender(fiber: Fiber) {
try {
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
} catch (e) {
errorHandler(e, this);
return Promise.resolve(h("div"));
errorHandler(e, fiber);
fiber.vnode = h("div"); // -> we render this div at the end
return Promise.resolve();
}
const __owl__ = this.__owl__;
if (__owl__.isDestroyed) {
return Promise.resolve(h("div"));
if (this.__owl__.isDestroyed) {
return Promise.resolve();
}
if (!fiber.isCancelled) {
this.__render(fiber);
}
return this.__render(fiber);
}
__render(fiber: Fiber<Props>): Promise<VNode> {
__render(fiber: Fiber) {
const __owl__ = this.__owl__;
const promises: Promise<void>[] = [];
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
let vnode;
try {
vnode = __owl__.render!(this, {
promises,
vnode = __owl__.renderFn!(this, {
handlers: __owl__.boundHandlers,
fiber: fiber
});
} catch (e) {
vnode = __owl__.vnode || h("div");
errorHandler(e, this);
errorHandler(e, fiber);
}
fiber.vnode = vnode;
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
// this part is critical for the patching process to be done correctly. The
// 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 component. With this, we make sure that the parent component will be
// able to patch itself properly after
vnode.key = __owl__.id;
// we applly here the class information described on the component by the
// we apply here the class information described on the component by the
// template (so, something like <MyComponent class="..."/>) to the actual
// root vnode
if (__owl__.classObj) {
vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj);
}
return Promise.all(promises).then(() => vnode);
fiber.root.counter--;
fiber.isRendered = true;
}
/**
* Only called by qweb t-component directive
*/
__mount(vnode: VNode, elm: HTMLElement): VNode {
__mount(fiber: Fiber, elm: HTMLElement): VNode {
if (fiber !== this.__owl__.currentFiber) {
fiber = this.__owl__.currentFiber!; // TODO: check if we can remove fiber arg
}
const vnode = fiber.vnode!;
const __owl__ = this.__owl__;
if (__owl__.classObj) {
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, __owl__.classObj);
}
__owl__.vnode = patch(elm, vnode);
__owl__.currentFiber = null;
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
this.__callMounted();
}
@@ -664,47 +649,15 @@ export class Component<T extends Env, Props extends {}> {
}
return <Props>props;
}
/**
* Apply the given patch queue from a fiber.
* 1) Call 'willPatch' on the component of each patch
* 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in reverse order
*/
__applyPatchQueue(fiber: Fiber<Props>) {
const patchQueue = fiber.patchQueue;
let component: Component<any, any> = this;
try {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
component = patchQueue[i].component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
}
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode);
}
for (let i = patchLen - 1; i >= 0; i--) {
component = patchQueue[i].component;
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
}
} catch (e) {
errorHandler(e, component);
}
}
}
//------------------------------------------------------------------------------
// Error handling
//------------------------------------------------------------------------------
Fiber.prototype.handleError = function(error) {
errorHandler(error, this);
};
/**
* This is the global error handler for errors occurring in Owl main lifecycle
* methods. Caught errors are triggered on the QWeb instance, and are
@@ -713,8 +666,9 @@ export class Component<T extends Env, Props extends {}> {
* If there are no such component, we destroy everything. This is better than
* being in a corrupted state.
*/
function errorHandler(error: Error, component: Component<any, any>) {
function errorHandler(error: Error, fiber: Fiber) {
let canCatch = false;
let component = fiber.component;
let qweb = component.env.qweb;
let root = component;
while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
@@ -722,7 +676,6 @@ function errorHandler(error: Error, component: Component<any, any>) {
component = component.__owl__.parent!;
}
console.error(error);
// we trigger error on QWeb so it can be logged/handled
qweb.trigger("error", error);
if (canCatch) {
+114 -125
View File
@@ -186,7 +186,7 @@ QWeb.utils.defineProxy = function defineProxy(target, source) {
QWeb.addDirective({
name: "component",
extraNames: ["props", "keepalive", "asyncroot"],
extraNames: ["props", "keepalive"],
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//COMPONENT");
@@ -196,7 +196,6 @@ QWeb.addDirective({
ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
let async = node.getAttribute("t-asyncroot") ? true : false;
// t-on- events and t-transition
const events: [string, string[], string, string][] = [];
@@ -209,11 +208,11 @@ QWeb.addDirective({
if (name.startsWith("t-on-")) {
const [eventName, ...mods] = name.slice(5).split(".");
let extraArgs;
let handlerName = value.replace(/\(.*\)/, function(args) {
let handlerValue = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
events.push([eventName, mods, handlerName, extraArgs]);
events.push([eventName, mods, handlerValue, extraArgs]);
} else if (name === "t-transition") {
transition = value;
} else if (!name.startsWith("t-")) {
@@ -233,7 +232,6 @@ QWeb.addDirective({
let propStr = Object.keys(props)
.map(k => k + ":" + props[k])
.join(",");
let dummyID = ctx.generateID();
let defID = ctx.generateID();
let componentID = ctx.generateID();
let keyID = key && ctx.generateID();
@@ -242,22 +240,19 @@ QWeb.addDirective({
// want to evaluate it only once)
ctx.addLine(`let key${keyID} = 'key' + ${key};`);
}
ctx.addLine(`let def${defID};`);
let templateID = key
? `key${keyID}`
: ctx.inLoop
? ctx.currentKey
? `String(${ctx.currentKey} + '_k_' + i + '_c_' + ${componentID} )`
: `String(-${componentID} - i)`
: String(componentID);
if (ctx.allowMultipleRoots) {
templateID = `"_slot_${templateID}"`;
let locationExpr = `\`__${ctx.generateID()}__`;
for (let i = 0; i < ctx.loopNumber - 1; i++) {
locationExpr += `\${i${i + 1}}__`;
}
if (key || ctx.inLoop) {
let id = ctx.generateID();
ctx.addLine(`let templateId${id} = ${templateID};`);
templateID = `templateId${id}`;
if (key || ctx.currentKey) {
const k = key ? `key${keyID}` : ctx.currentKey;
ctx.addLine(`let templateId${componentID} = ${locationExpr}\` + ${k};`);
} else {
locationExpr += ctx.loopNumber ? `\${i${ctx.loopNumber}}__\`` : "`";
ctx.addLine(`let templateId${componentID} = ${locationExpr};`);
}
const templateId = `templateId${componentID}`;
let ref = node.getAttribute("t-ref");
let refExpr = "";
@@ -317,10 +312,10 @@ QWeb.addDirective({
}
}
let eventsCode = events
.map(function([eventName, mods, handlerName, extraArgs]) {
.map(function([eventName, mods, handlerValue, extraArgs]) {
let params = "owner";
if (extraArgs) {
if (ctx.inLoop) {
if (ctx.loopNumber) {
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
@@ -331,18 +326,17 @@ QWeb.addDirective({
params = `owner, ${ctx.formatExpression(extraArgs)}`;
}
}
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
handler += mods
.map(function(mod) {
return T_COMPONENT_MODS_CODE[mod];
})
.join("");
handler += `owner['${handlerName}'].call(${params}, e);}`;
} else {
handler = `owner['${handlerName}'].bind(${params})`;
let handler = `function (e) {`;
handler += mods
.map(function(mod) {
return T_COMPONENT_MODS_CODE[mod];
})
.join("");
if (handlerValue) {
handler += `const fn = owner['${handlerValue}'];`;
handler += `if (fn) { fn.call(${params}, e); } else { owner.${handlerValue}; }`;
}
handler += `}`;
return `vn.elm.addEventListener('${eventName}', ${handler});`;
})
.join("");
@@ -352,32 +346,21 @@ QWeb.addDirective({
}
ctx.addLine(
`let w${componentID} = ${templateID} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateID}]] : false;`
`let w${componentID} = ${templateId} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateId}]] : false;`
);
if (ctx.parentNode) {
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
}
let shouldProxy = false;
if (async || keepAlive) {
let shouldProxy = !ctx.parentNode;
if (keepAlive) {
ctx.addLine(
`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`
);
}
if (async) {
ctx.addLine(
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
);
} else {
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(null);`);
} else {
let id = ctx.generateID();
ctx.rootContext.rootNode = id;
shouldProxy = true;
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`let vn${id} = {};`);
ctx.addLine(`result = vn${id};`);
}
if (shouldProxy) {
let id = ctx.generateID();
ctx.rootContext.rootNode = id;
shouldProxy = true;
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`let vn${id} = {};`);
ctx.addLine(`result = vn${id};`);
}
if (hasDynamicProps) {
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
@@ -388,17 +371,73 @@ QWeb.addDirective({
ctx.addIf(
`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`
);
ctx.addIf(
`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.currentFiber.props)`
);
ctx.addLine(`def${defID} = w${componentID}.__owl__.currentFiber.promise;`);
ctx.addElse();
ctx.addLine(`w${componentID}.destroy();`);
ctx.addLine(`w${componentID} = false;`);
ctx.closeIf();
ctx.closeIf();
ctx.addIf(`!w${componentID}`);
let registerCode = "";
if (shouldProxy) {
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
}
// SLOTS
const varDefs: string[] = [];
const hasSlots = node.childNodes.length;
if (hasSlots) {
ctx.rootContext.shouldTrackScope = true;
for (let v of Object.values(ctx.variables)) {
if (v["id"]) {
varDefs.push(v["id"]);
}
}
}
let scopeVars;
if (hasSlots) {
let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
scopeVars = `${scope}, ${vars}`;
} else {
scopeVars = "undefined, undefined";
}
ctx.addIf(`w${componentID}`);
// need to update component
let patchQueueCode = keepAlive ? `fiber${componentID}` : "extra.fiber";
if (keepAlive) {
// if we have t-keepalive="1", the component could be unmounted, but then
// we __updateProps is called. This is ok, but we do not want to call
// the willPatch/patched hooks of the component in this case, so we
// disable the patch queue
patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`;
}
if (QWeb.dev) {
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
}
let styleCode = "";
if (tattStyle) {
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
}
ctx.addLine(
`w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars &&
", " + scopeVars}, sibling)${styleCode};`
);
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
let keepAliveCode = "";
if (keepAlive) {
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
ctx.addLine(keepAliveCode);
}
if (registerCode) {
ctx.addLine(registerCode);
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
}
ctx.addElse();
// new component
let dynamicFallback = "";
if (!value.match(INTERP_REGEXP)) {
@@ -414,23 +453,10 @@ QWeb.addDirective({
ctx.addLine(
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
);
if (QWeb.dev) {
ctx.addLine(`utils.validateProps(W${componentID}, props${componentID})`);
}
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
ctx.addLine(`parent.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`);
ctx.addLine(`parent.__owl__.cmap[${templateId}] = w${componentID}.__owl__.id;`);
// SLOTS
const varDefs: string[] = [];
const hasSlots = node.childNodes.length;
if (hasSlots) {
ctx.rootContext.shouldTrackScope = true;
for (let v of Object.values(ctx.variables)) {
if (v["id"]) {
varDefs.push(v["id"]);
}
}
const clone = <Element>node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]");
const slotId = QWeb.nextSlotId++;
@@ -455,67 +481,30 @@ QWeb.addDirective({
}
}
let scopeVars;
if (hasSlots) {
let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
scopeVars = `${scope}, ${vars}`;
} else {
scopeVars = "undefined, undefined";
}
ctx.addLine(`def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`);
ctx.addLine(`let def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars}, sibling);`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
if (shouldProxy) {
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
}
ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{if (w${componentID}.__owl__.isDestroyed){return}${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`
`let pvnode = h('dummy', {key: ${templateId}, hook: {insert(vn) { let nvn=w${componentID}.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});`
);
ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`);
ctx.addLine(
`def${defID}.then(function () {if (w${componentID}.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
if (registerCode) {
ctx.addLine(registerCode);
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
}
ctx.addLine(`w${componentID}.__owl__.pvnode = pvnode;`);
ctx.addElse();
// need to update component
let patchQueueCode = async || keepAlive ? `fiber${componentID}` : "extra.fiber";
if (keepAlive) {
// if we have t-keepalive="1", the component could be unmounted, but then
// we __updateProps is called. This is ok, but we do not want to call
// the willPatch/patched hooks of the component in this case, so we
// disable the patch queue
patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`;
}
if (QWeb.dev) {
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
}
ctx.addLine(
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars &&
", " + scopeVars});`
);
let keepAliveCode = "";
if (keepAlive) {
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
}
ctx.addLine(
`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${
tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""
}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}${registerCode}});`
);
ctx.closeIf();
if (classObj) {
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
}
if (async) {
ctx.addLine(
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, fiber${componentID}));`
);
} else {
ctx.addLine(`extra.promises.push(def${defID});`);
}
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
ctx.closeIf();
}
ctx.addLine(`sibling = w${componentID}.__owl__.currentFiber || sibling;`);
return true;
}
+199
View File
@@ -0,0 +1,199 @@
import { VNode } from "../vdom/index";
import { Component } from "./component";
/**
* Owl Fiber Class
*
* Fibers are small abstractions designed to contain all the internal state
* associated with a "rendering work unit", relative to a specific component.
*
* A rendering will cause the creation of a fiber for each impacted components.
*
* Fibers capture all that necessary information, which is critical to owl
* asynchronous rendering pipeline. Fibers can be cancelled, can be in different
* states and in general determine the state of the rendering.
*/
export class Fiber {
// The force attribute determines if a rendering should bypass the `shouldUpdate`
// method potentially implemented by a component. It is usually set to false.
force: boolean;
// isCancelled means that the rendering corresponding to this fiber and its
// children is cancelled. No extra work should be done.
isCancelled: boolean = false;
// the fibers corresponding to component updates (updateProps) need to call
// the willPatch and patched hooks from the corresponding component. However,
// fibers corresponding to a new component do not need to do that. So, the
// shouldPatch hook is the boolean that we check whenever we need to apply
// a patch.
shouldPatch: boolean = true;
// isRendered is the last state of a fiber. If true, this means that it has
// been rendered and is inert (so, it should not be taken into account when
// counting the number of active fibers).
isRendered: boolean = false;
// the counter number is a critical information. It is only necessary for a
// root fiber. For that fiber, this number counts the number of active sub
// fibers. When that number reaches 0, the fiber can be applied by the
// scheduler.
counter: number = 0;
scope: any;
vars: any;
props: any;
component: Component<any, any>;
vnode: VNode | null = null;
root: Fiber;
child: Fiber | null = null;
sibling: Fiber | null = null;
parent: Fiber | null = null;
constructor(parent: Fiber | null, component: Component<any, any>, props, scope, vars, force) {
this.force = force;
this.scope = scope;
this.vars = vars;
this.props = props;
this.component = component;
this.root = parent ? parent.root : this;
this.parent = parent;
let oldFiber = component.__owl__.currentFiber;
if (oldFiber && !oldFiber.isCancelled) {
this._remapFiber(oldFiber);
}
this.root.counter++;
component.__owl__.currentFiber = this;
}
/**
* In some cases, a rendering initiated at some component can detect that it
* should be part of a larger rendering initiated somewhere up the component
* tree. In that case, it needs to cancel the previous rendering and
* remap itself as a part of the current parent rendering.
*/
_remapFiber(oldFiber: Fiber) {
oldFiber.cancel();
if (oldFiber === oldFiber.root) {
oldFiber.root.counter++;
}
if (oldFiber.parent && !this.parent) {
// re-map links
this.parent = oldFiber.parent;
this.root = this.parent.root;
this.sibling = oldFiber.sibling;
if (this.parent.child === oldFiber) {
this.parent.child = this;
} else {
let current = this.parent.child!;
while (true) {
if (current.sibling === oldFiber) {
current.sibling = this;
break;
}
current = current.sibling!;
}
}
}
}
/**
* This function has been taken from
* https://medium.com/react-in-depth/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7
*/
_walk(doWork: (f: Fiber) => Fiber | null) {
let root = this;
let current: Fiber = this;
while (true) {
const child = doWork(current);
if (child) {
current = child;
continue;
}
if (current === root) {
return;
}
while (!current.sibling) {
if (!current.parent || current.parent === root) {
return;
}
current = current.parent;
}
current = current.sibling;
}
}
/**
* Apply the given patch queue from a fiber.
* 1) Call 'willPatch' on the component of each patch
* 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in reverse order
*/
patchComponents() {
const patchQueue: Fiber[] = [];
const doWork: (Fiber) => Fiber | null = function(f) {
if (f.shouldPatch) {
patchQueue.push(f);
}
return f.child;
};
this._walk(doWork);
let component: Component<any, any> = this.component;
this.shouldPatch = false;
const patchLen = patchQueue.length;
try {
for (let i = 0; i < patchLen; i++) {
component = patchQueue[i].component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
}
} catch (e) {
console.error(e);
}
try {
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode);
}
} catch (e) {
this.handleError(e);
}
try {
for (let i = patchLen - 1; i >= 0; i--) {
component = patchQueue[i].component;
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
}
} catch (e) {
console.error(e);
}
this.shouldPatch = true;
}
/**
* Cancel a fiber and all its children.
*/
cancel() {
this._walk(f => {
if (!f.isRendered) {
f.root.counter--;
}
f.isCancelled = true;
return f.child;
});
}
handleError(e: Error) {}
}
+67
View File
@@ -0,0 +1,67 @@
import { Fiber } from "./fiber";
/**
* Owl Scheduler Class
*
* The scheduler is the part of Owl that will effectively apply a rendering
* whenever a fiber is ready.
*
* Briefly, it can be used to register root fibers. Whenever there is an
* active root fiber, it will poll continuously each animation frame (so, about
* once every 16ms) and whenever a root fiber is ready, it will apply it.
*/
interface Task {
fiber: Fiber;
callback: () => void;
}
export class Scheduler {
tasks: Task[] = [];
isRunning: boolean = false;
requestAnimationFrame: typeof window.requestAnimationFrame;
constructor(requestAnimationFrame) {
this.requestAnimationFrame = requestAnimationFrame;
}
addFiber(fiber, callback) {
this.tasks.push({ fiber, callback });
if (this.isRunning) {
return;
}
this.scheduleTasks();
}
/**
* Process all current tasks. This only applies to the fibers that are ready.
* Other tasks are left unchanged.
*/
flush() {
let tasks = this.tasks;
this.tasks = [];
tasks = tasks.filter(task => {
if (task.fiber.isCancelled) {
return false;
}
if (task.fiber.counter === 0) {
task.callback();
return false;
}
return true;
});
this.tasks = tasks.concat(this.tasks);
}
scheduleTasks() {
this.isRunning = true;
this.requestAnimationFrame(() => {
this.flush();
if (this.tasks.length > 0) {
this.scheduleTasks();
} else {
this.isRunning = false;
}
});
}
}
+29 -13
View File
@@ -1,8 +1,7 @@
import { Component } from "./component/component";
import { Observer } from "./core/observer";
import { Component, scheduler } from "./component/component";
import { EventBus } from "./core/event_bus";
import { Observer } from "./core/observer";
import { onWillUnmount } from "./hooks";
/**
* The `Context` object provides a way to share data between an arbitrary number
* of component. Usually, data is passed from a parent to its children component,
@@ -16,7 +15,7 @@ import { onWillUnmount } from "./hooks";
export class Context extends EventBus {
state: any;
observer: Observer;
id: number = 1;
rev: number = 1;
// mapping from component id to last observed context id
mapping: { [componentId: number]: number } = {};
@@ -47,13 +46,15 @@ export class Context extends EventBus {
* with the same depth in parallel.
*/
async __notifyComponents() {
const id = ++this.id;
const rev = ++this.rev;
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, id);
const render = sub.callback.call(sub.owner, rev);
scheduler.flush();
await render;
}
}
}
@@ -68,22 +69,37 @@ export function useContext(ctx: Context): any {
return useContextWithCB(ctx, component, component.render.bind(component));
}
export function useContextWithCB(ctx: Context, component, method): any {
export function useContextWithCB(ctx: Context, component: Component<any, any>, method): any {
const __owl__ = component.__owl__;
const id = __owl__.id;
const mapping = ctx.mapping;
if (id in mapping) {
return ctx.state;
}
if (!__owl__.observer) {
__owl__.observer = new Observer();
__owl__.observer.notifyCB = component.render.bind(component);
}
const currentCB = __owl__.observer.notifyCB;
__owl__.observer.notifyCB = function () {
if (ctx.rev > mapping[id]) {
// in this case, the context has been updated since we were rendering
// last, and we do not need to render here with the observer. A
// rendering is coming anyway, with the correct props.
return;
}
currentCB();
}
mapping[id] = 0;
const renderFn = __owl__.render;
__owl__.render = function(comp, params) {
mapping[id] = ctx.id;
const renderFn = __owl__.renderFn;
__owl__.renderFn = function(comp, params) {
mapping[id] = ctx.rev;
return renderFn(comp, params);
};
ctx.on("update", component, async contextId => {
if (mapping[id] < contextId) {
mapping[id] = contextId;
ctx.on("update", component, async contextRev => {
if (mapping[id] < contextRev) {
mapping[id] = contextRev;
await method();
}
});
+2
View File
@@ -10,6 +10,7 @@ import { QWeb } from "./qweb/index";
import * as _store from "./store";
import * as _utils from "./utils";
import * as _tags from "./tags";
import {AsyncRoot} from "./misc/async_root";
import * as _hooks from "./hooks";
import * as _context from "./context";
import { Link } from "./router/link";
@@ -26,6 +27,7 @@ export const router = { Router, RouteComponent, Link };
export const Store = _store.Store;
export const utils = _utils;
export const tags = _tags;
export const misc = { AsyncRoot};
export const hooks = Object.assign({}, _hooks, {
useContext: _context.useContext,
useDispatch: _store.useDispatch,
+19
View File
@@ -0,0 +1,19 @@
import { Component } from "../component/component";
import { xml } from "../tags";
/**
* AsyncRoot
*
* Owl is by default asynchronous, and the user interface will wait for all its
* subcomponents to be rendered before updating the DOM. This is most of the
* time what we want, but in some cases, it makes sense to "detach" a component
* from this coordination. This is the goal of the AsyncRoot component.
*/
export class AsyncRoot extends Component<any, any> {
static template = xml`<t t-slot="default"/>`;
async __updateProps(nextProps, parentFiber) {
this.render(parentFiber.force);
}
}
+10 -13
View File
@@ -133,7 +133,7 @@ QWeb.addDirective({
priority: 20,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-if")!);
ctx.addIf(`${ctx.formatExpression(cond)}`);
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : cond.id);
return false;
},
finalize({ ctx }) {
@@ -146,7 +146,7 @@ QWeb.addDirective({
priority: 30,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-elif")!);
ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`);
ctx.addLine(`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : cond.id}) {`);
ctx.indent();
return false;
},
@@ -258,10 +258,6 @@ QWeb.addDirective({
ctx.addLine("}");
}
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
ctx.closeIf();
}
return true;
}
});
@@ -275,7 +271,7 @@ QWeb.addDirective({
priority: 10,
atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldProtectContext = true;
ctx = ctx.subContext("inLoop", true);
ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1);
const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID();
@@ -289,13 +285,14 @@ QWeb.addDirective({
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf();
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
const loopVar = `i${ctx.loopNumber}`;
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
ctx.indent();
ctx.addToScope(name + "_first", "i === 0");
ctx.addToScope(name + "_last", `i === _length${keysID} - 1`);
ctx.addToScope(name + "_index", "i");
ctx.addToScope(name, `_${keysID}[i]`);
ctx.addToScope(name + "_value", `_${valuesID}[i]`);
ctx.addToScope(name + "_first", `${loopVar} === 0`);
ctx.addToScope(name + "_last", `${loopVar} === _length${keysID} - 1`);
ctx.addToScope(name + "_index", loopVar);
ctx.addToScope(name, `_${keysID}[${loopVar}]`);
ctx.addToScope(name + "_value", `_${valuesID}[${loopVar}]`);
const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
if (!shouldWarn && node.tagName === "t") {
+7 -3
View File
@@ -1,4 +1,4 @@
import { compileExpr, QWebVar } from "./expression_parser";
import { compileExpr, QWebVar, QWebExprVar } from "./expression_parser";
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------
@@ -22,9 +22,10 @@ export class CompilationContext {
shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false;
shouldDefineResult: boolean = true;
shouldDefineSibling: boolean = true;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
inLoop: boolean = false;
loopNumber: number = 0;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
@@ -63,6 +64,9 @@ export class CompilationContext {
if (this.shouldDefineResult) {
this.code.unshift(" let result;");
}
if (this.shouldDefineSibling) {
this.code.unshift(" let sibling = null;");
}
if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
}
@@ -145,7 +149,7 @@ export class CompilationContext {
this.addLine("}");
}
getValue(val: any): any {
getValue(val: any): QWebExprVar | string {
return val in this.variables ? this.getValue(this.variables[val]) : val;
}
+21 -33
View File
@@ -40,44 +40,32 @@ QWeb.addDirective({
extraArgs = args.slice(1, -1);
return "";
});
ctx.addIf(`!context['${handlerName}']`);
ctx.addLine(
`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(
/`/g,
"'"
)}'\`)`
);
ctx.closeIf();
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
if (mods.length > 0) {
let handler = `function (e) {`;
handler += mods
.map(function(mod) {
return MODS_CODE[mod];
})
.join("");
let handler = `function (e) {`;
handler += mods
.map(function(mod) {
return MODS_CODE[mod];
})
.join("");
if (handlerName) {
if (!extraArgs) {
handler += `context['${handlerName}'].call(${params}, e);}`;
handler += `const fn = context['${handlerName}'];`;
handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerName}; }`;
handler += `}`;
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
} else {
const handlerKey = `handler${ctx.generateID()}`;
ctx.addLine(`const ${handlerKey} = context['${handlerName}'].bind(${params});`);
handler += `${handlerKey}(e);}`;
ctx.addLine(`const ${handlerKey} = context['${handlerName}'] && context['${handlerName}'].bind(${params});`);
handler += `if (${handlerKey}) { ${handlerKey}(e); } else { context.${value}; }`;
handler += `}`;
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
}
} else {
const handler = `context['${handlerName}'].bind(${params})`;
if (extraArgs) {
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
} else {
ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
}
handler += "}";
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
}
}
});
@@ -209,17 +197,17 @@ QWeb.addDirective({
ctx.addIf(`slot${slotKey}`);
let parentNode = `c${ctx.parentNode}`;
if (!ctx.parentNode) {
ctx.rootContext.shouldDefineResult = true;
ctx.rootContext.shouldDefineUtils = true;
parentNode = `children${ctx.nextID++}`;
ctx.addLine(`let ${parentNode}= []`);
ctx.addLine(`result = {}`);
ctx.rootContext.shouldDefineResult = true;
ctx.rootContext.shouldDefineUtils = true;
parentNode = `children${ctx.nextID++}`;
ctx.addLine(`let ${parentNode}= []`);
ctx.addLine(`result = {}`);
}
ctx.addLine(
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: owner}));`
);
if (!ctx.parentNode) {
ctx.addLine(`Promise.all(extra.promises).then(() => utils.defineProxy(result, ${parentNode}[0]))`);
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`);
}
ctx.closeIf();
return true;
+12 -5
View File
@@ -452,6 +452,8 @@ export class QWeb extends EventBus {
fullName: string;
}[] = [];
const finalizers: typeof validDirectives = [];
// maybe this is not optimal: we iterate on all attributes here, and again
// just after for each directive.
for (let i = 0; i < attributes.length; i++) {
@@ -496,7 +498,11 @@ export class QWeb extends EventBus {
}
}
}
for (let { directive, value, fullName } of validDirectives) {
if (directive.finalize) {
finalizers.push({ directive, value, fullName });
}
if (directive.atNodeEncounter) {
const isDone = directive.atNodeEncounter({
node,
@@ -506,6 +512,9 @@ export class QWeb extends EventBus {
value
});
if (isDone) {
for (let { directive, value, fullName } of finalizers) {
directive.finalize!({ node, qweb: this, ctx, fullName, value });
}
return;
}
}
@@ -564,10 +573,8 @@ export class QWeb extends EventBus {
ctx.addLine(`utils.addNameSpace(vn${ctx.parentNode});`);
}
for (let { directive, value, fullName } of validDirectives) {
if (directive.finalize) {
directive.finalize({ node, qweb: this, ctx, fullName, value });
}
for (let { directive, value, fullName } of finalizers) {
directive.finalize!({ node, qweb: this, ctx, fullName, value });
}
}
@@ -638,7 +645,7 @@ export class QWeb extends EventBus {
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
const v = ctx.getValue(value);
let formattedValue = v.id || ctx.formatExpression(v);
let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : v.id;
if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true;
+2 -2
View File
@@ -104,11 +104,11 @@ export function useStore(selector, options: SelectorOptions = {}): any {
return true;
}
return false;
})
});
useContextWithCB(store, component, function(): Promise<void> | void {
let shouldRender = false;
updateFunctions.forEach(function (updateFn) {
updateFunctions.forEach(function(updateFn) {
shouldRender = updateFn() || shouldRender;
});
if (shouldRender) {
+2 -2
View File
@@ -5,7 +5,7 @@
*
* - whenReady
* - loadJS
* - loadTemplates
* - loadFile
* - escape
* - debounce
*/
@@ -43,7 +43,7 @@ export function loadJS(url: string): Promise<void> {
return promise;
}
export async function loadTemplates(url: string): Promise<string> {
export async function loadFile(url: string): Promise<string> {
const result = await fetch(url);
if (!result.ok) {
throw new Error("Error while fetching xml templates");
+7 -13
View File
@@ -176,18 +176,10 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
}
vnode.elm = api.createComment(vnode.text as string);
} else if (sel !== undefined) {
// Parse selector
const hashIdx = sel.indexOf("#");
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 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, " "));
? api.createElementNS(i, sel)
: api.createElement(sel));
for (i = 0, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode);
if (array(children)) {
for (i = 0, iLen = children.length; i < iLen; ++i) {
@@ -565,13 +557,15 @@ type ArrayOrElement<T> = T | T[];
type VNodeChildren = ArrayOrElement<VNodeChildElement>;
export function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
if (sel === "dummy") {
// we do not need to add the namespace on dummy elements, they come from a
// subcomponent, which will handle the namespace itself
return;
}
data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) {
for (let i = 0, iLen = children.length; i < iLen; ++i) {
const child = children[i];
if (child === null) {
continue;
}
let childData = child.data;
if (childData !== undefined) {
addNS(childData, (child as VNode).children as VNodes, child.sel);
+54 -52
View File
@@ -7,39 +7,39 @@ exports[`animations t-transition combined with component 1`] = `
let QWeb = this.constructor;
let parent = context;
let owner = context;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
def3 = w4.__owl__.currentFiber.promise;
} else {
w4.destroy();
w4 = false;
}
let templateId3 = \`__4__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
}
if (!w4) {
let componentKey4 = \`Child\`;
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare(extra.fiber, undefined, undefined);
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}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;});
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode;
c1.push(pvnode);
} else {
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w3.destroy();
};
utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
extra.promises.push(def3);
sibling = w3.__owl__.currentFiber || sibling;
return vn1;
}"
`;
@@ -51,40 +51,40 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
let QWeb = this.constructor;
let parent = context;
let owner = context;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].display) {
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
def3 = w4.__owl__.currentFiber.promise;
} else {
w4.destroy();
w4 = false;
}
let templateId3 = \`__4__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
}
if (!w4) {
let componentKey4 = \`Child\`;
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare(extra.fiber, undefined, undefined);
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}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;});
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode;
c1.push(pvnode);
} else {
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w3.destroy();
};
utils.transitionRemove(vn, 'chimay', finalize);}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
extra.promises.push(def3);
sibling = w3.__owl__.currentFiber || sibling;
}
return vn1;
}"
@@ -94,6 +94,7 @@ exports[`animations t-transition with no delay/duration 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
@@ -114,6 +115,7 @@ exports[`animations t-transition, on a simple node (insert) 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
+2 -2
View File
@@ -251,11 +251,11 @@ describe("animations", () => {
widget.state.display = false;
patchNextFrame(cb => {
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="4">blue</span></div>'
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="__4__">blue</span></div>'
);
cb();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="4">blue</span></div>'
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__4__">blue</span></div>'
);
def.resolve();
});
File diff suppressed because it is too large Load Diff
@@ -1,9 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`default props default values are also set whenever component is updated 1`] = `"<div>1</div>"`;
exports[`default props default values are also set whenever component is updated 2`] = `"<div>4</div>"`;
exports[`props validation props are validated in dev mode (code snapshot) 1`] = `
"function anonymous(context,extra
) {
@@ -11,38 +7,37 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
let QWeb = this.constructor;
let parent = context;
let owner = context;
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {message:1};
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
def3 = w4.__owl__.currentFiber.promise;
} else {
w4.destroy();
w4 = false;
}
let templateId3 = \`__4__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let props3 = {message:1};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
w3 = false;
}
if (!w4) {
let componentKey4 = \`Child\`;
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
utils.validateProps(W4, props4)
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare(extra.fiber, undefined, undefined);
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
if (w3) {
utils.validateProps(w3.constructor, props3)
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode;
c1.push(pvnode);
} else {
utils.validateProps(w4.constructor, props4)
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
let componentKey3 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
extra.promises.push(def3);
sibling = w3.__owl__.currentFiber || sibling;
return vn1;
}"
`;
File diff suppressed because it is too large Load Diff
+31 -12
View File
@@ -1,5 +1,6 @@
import { Component, Env } from "../../src/component/component";
import { makeTestFixture, makeTestEnv } from "../helpers";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
import { useState } from "../../src/hooks";
import { QWeb } from "../../src/qweb";
import { xml } from "../../src/tags";
@@ -336,20 +337,38 @@ describe("default props", () => {
test("default values are also set whenever component is updated", async () => {
class TestWidget extends Widget {
static template = xml`<div><t t-esc="props.p"/></div>`;
static defaultProps = { p: 4 };
}
env.qweb.addTemplates(`
<templates>
<div t-name="TestWidget"><t t-esc="props.p"/></div>
</templates>`);
class Parent extends Widget {
static template = xml`<div><TestWidget p="state.p"/></div>`;
static components = { TestWidget };
state: any = useState({ p: 1 });
}
const w = new TestWidget(env, { p: 1 });
const w = new Parent(env);
await w.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
const fiber = w.__createFiber(false, undefined, undefined, undefined);
await w.__updateProps({}, fiber);
await w.render();
expect(w.props.p).toBe(4);
expect(fixture.innerHTML).toMatchSnapshot();
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
w.state.p = undefined;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
});
test("can set default required boolean values", async () => {
class TestWidget extends Widget {
static props = ["p", "q"];
static defaultProps = { p: true, q: false };
static template = xml`<span><t t-if="props.p">hey</t><t t-if="!props.q">hey</t></span>`;
}
class App extends Widget {
static template = xml`<div><TestWidget/></div>`;
static components = { TestWidget };
}
const w = new App(env, {});
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>heyhey</span></div>");
});
});
+49 -1
View File
@@ -1,4 +1,4 @@
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick } from "./helpers";
import { Component, Env } from "../src/component/component";
import { Context, useContext } from "../src/context";
import { xml } from "../src/tags";
@@ -173,4 +173,52 @@ describe("Context", () => {
// event
expect(testContext.subscriptions.update.length).toBe(0);
});
test("concurrent renderings", async () => {
const testContext = new Context({ x: { n: 1 }, key: "x" });
const def = makeDeferred();
let stateC;
class ComponentC extends Component<any, any> {
static template = xml`<span><t t-esc="context[props.key].n"/><t t-esc="state.x"/></span>`;
context = useContext(testContext);
state = useState({ x: "a" });
constructor(parent, props) {
super(parent, props);
stateC = this.state;
}
}
class ComponentB extends Component<any, any> {
static components = { ComponentC };
static template = xml`<p><ComponentC key="props.key"/></p>`;
willUpdateProps() {
return def;
}
}
class ComponentA extends Component<any, any> {
static components = { ComponentB };
static template = xml`<div><ComponentB key="context.key"/></div>`;
context = useContext(testContext);
}
const component = new ComponentA(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
testContext.state.key = "y";
testContext.state.y = {n: 2};
delete testContext.state.x;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
stateC.x = "b";
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span>2b</span></p></div>");
});
});
+6 -1
View File
@@ -13,7 +13,8 @@ const HEADING_REGEXP = /\n(#+\s*)(.*)/g;
function getFiles(): string[] {
const DOCFILES = fs.readdirSync("doc").map(f => `doc/${f}`);
const MAINREADME = "README.md";
return DOCFILES.concat(MAINREADME);
DOCFILES.push("roadmap.md", MAINREADME);
return DOCFILES;
}
test("All markdown links work", () => {
@@ -53,6 +54,10 @@ interface FileData {
}
function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean {
if (link.link.startsWith("http")) {
// no check on external links
return true;
}
const parts = link.link.split("#");
const currentParts = current.name.split("/");
const path = currentParts.length > 1 ? currentParts[0] + "/" : "";
+11 -3
View File
@@ -1,10 +1,16 @@
import { Env } from "../src/component/component";
import { Env, scheduler } from "../src/component/component";
import { EvalContext, QWeb } from "../src/qweb/qweb";
import { patch } from "../src/vdom";
import "../src/qweb/base_directives";
import "../src/qweb/extensions";
import "../src/component/directive";
// modifies scheduler to make it faster to test components
scheduler.requestAnimationFrame = function(callback: FrameRequestCallback) {
setTimeout(callback, 1);
return 1;
};
// Some static cleanup
let nextSlotId;
let slots;
@@ -30,8 +36,10 @@ export function nextMicroTick(): Promise<void> {
return Promise.resolve();
}
export function nextTick(): Promise<void> {
return new Promise(resolve => setTimeout(resolve));
export async function nextTick(): Promise<void> {
return new Promise(function(resolve) {
setTimeout(() => scheduler.requestAnimationFrame(() => resolve()));
});
}
export function makeTestFixture() {
+189
View File
@@ -0,0 +1,189 @@
import { AsyncRoot } from "../../src/misc/async_root";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeDeferred, makeTestFixture, makeTestEnv, nextTick } from "../helpers";
import { Env, Component } from "../../src/component/component";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
describe("Asyncroot", () => {
test("delayed component with AsyncRoot component", async () => {
let def;
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<Child val="state.val"/>
<AsyncRoot>
<AsyncChild val="state.val"/>
</AsyncRoot>
</div>
</div>`;
static components = { Child, AsyncChild, AsyncRoot };
state = useState({ val: 0 });
updateApp() {
this.state.val++;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0</span><span>0</span>");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>0</span>");
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>1</span>");
});
test("fast component with AsyncRoot", async () => {
let def;
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<AsyncRoot>
<Child val="state.val"/>
</AsyncRoot>
<AsyncChild val="state.val"/>
</div>
</div>`;
static components = { Child, AsyncChild, AsyncRoot };
state = useState({ val: 0 });
updateApp() {
this.state.val++;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0</span><span>0</span>");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>0</span>");
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>1</span>");
});
test("asyncroot component: mixed re-renderings", async () => {
let def;
class Child extends Component<any, any> {
static template = xml`
<span t-on-click="increment">
<t t-esc="state.val"/>/<t t-esc="props.val"/>
</span>`;
state = useState({ val: 0 });
increment() {
this.state.val++;
}
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<Child val="state.val"/>
<AsyncRoot>
<AsyncChild val="state.val"/>
</AsyncRoot>
</div>
</div>`;
static components = { Child, AsyncChild, AsyncRoot };
state = useState({ val: 0 });
updateApp() {
this.state.val++;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0/0</span><span>0/0</span>");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0/1</span><span>0/0</span>");
// click on each Child to increment their local counter
const children = parent.el!.querySelectorAll("span");
children[0]!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>0/0</span>");
children[1]!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>0/0</span>");
// finalize first re-rendering (coming from the props update)
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>1/1</span>");
});
});
File diff suppressed because it is too large Load Diff
+118 -8
View File
@@ -79,19 +79,12 @@ describe("error handling", () => {
expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined");
});
test("loadTemplates throw if parser error", () => {
test("addTemplates throw if parser error", () => {
expect(() => {
qweb.addTemplates("<templates><abc>></templates>");
}).toThrow("Invalid XML in template");
});
test("nice error when t-on-directive is evaluated with a missing handler", () => {
qweb.addTemplate("templatename", `<div t-on-click="somemethod"></div>`);
expect(() => qweb.render("templatename", {}, { handlers: [] })).toThrow(
"Missing handler 'somemethod' when evaluating template 'templatename'"
);
});
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(
@@ -328,6 +321,61 @@ describe("t-if", () => {
};
expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>");
});
test("t-esc with t-if", () => {
qweb.addTemplate("test", `<div><t t-if="true" t-esc="'x'"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>x</div>");
});
test("t-esc with t-elif", () => {
qweb.addTemplate("test", `<div><t t-if="false">abc</t><t t-else="1" t-esc="'x'"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>x</div>");
});
test("t-set, then t-if", () => {
qweb.addTemplate(
"test",
`
<div>
<t t-set="title" t-value="'test'"/>
<t t-if="title"><t t-esc="title"/></t>
</div>`
);
const result = renderToString(qweb, "test");
const expected = `<div>test</div>`;
expect(result).toBe(expected);
});
test("t-set, then t-if, part 2", () => {
qweb.addTemplate(
"test",
`
<div>
<t t-set="y" t-value="true"/>
<t t-set="x" t-value="y"/>
<span t-if="x">COUCOU</span>
</div>`
);
const result = renderToString(qweb, "test");
const expected = `<div><span>COUCOU</span></div>`;
expect(result).toBe(expected);
});
test("t-set, then t-elif, part 3", () => {
qweb.addTemplate(
"test",
`
<div>
<t t-set="y" t-value="false"/>
<t t-set="x" t-value="y"/>
<span t-if="x">AAA</span>
<span t-elif="!x">BBB</span>
</div>`
);
const result = renderToString(qweb, "test");
const expected = `<div><span>BBB</span></div>`;
expect(result).toBe(expected);
});
});
describe("attributes", () => {
@@ -749,6 +797,23 @@ describe("foreach", () => {
expect(Object.keys(context).length).toBe(0);
});
test("t-foreach in t-forach", () => {
qweb.addTemplate(
"test",
`<div>
<t t-foreach="numbers" t-as="number">
<t t-foreach="letters" t-as="letter">
[<t t-esc="number"/><t t-esc="letter"/>]
</t>
</t>
</div>`
);
const context = { numbers: [1, 2, 3], letters: ["a", "b"] };
expect(renderToString(qweb, "test", context)).toBe(
"<div> [1a] [1b] [2a] [2b] [3a] [3b] </div>"
);
});
test("throws error if invalid loop expression", () => {
qweb.addTemplate(
"test",
@@ -975,6 +1040,33 @@ describe("t-on", () => {
(<HTMLElement>node).click();
});
test("t-on with inline statement", () => {
qweb.addTemplate("test", `<button t-on-click="state.counter++">Click</button>`);
let owner = {
state: {
counter: 0,
},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.counter).toBe(0);
(<HTMLElement>node).click();
expect(owner.state.counter).toBe(1);
});
test("t-on with inline statement (function call)", () => {
qweb.addTemplate("test", `<button t-on-click="state.incrementCounter(2)">Click</button>`);
let owner = {
state: {
counter: 0,
incrementCounter: (inc) => { owner.state.counter += inc; },
},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
expect(owner.state.counter).toBe(0);
(<HTMLElement>node).click();
expect(owner.state.counter).toBe(2);
});
test("t-on with prevent and/or stop modifiers", async () => {
expect.assertions(7);
qweb.addTemplate(
@@ -1121,6 +1213,24 @@ describe("t-on", () => {
expect(steps).toEqual([1, 2]);
});
test("t-on with empty handler (only modifiers)", () => {
expect.assertions(2);
qweb.addTemplate(
"test",
`<div>
<button t-on-click.prevent="">Button</button>
</div>`
);
const node = renderToDOM(qweb, "test", {}, { handlers: [] });
node.addEventListener('click', (e) => {
expect(e.defaultPrevented).toBe(true);
});
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
button.click();
});
test("t-on combined with t-esc", async () => {
expect.assertions(3);
qweb.addTemplate("test", `<div><button t-on-click="onClick" t-esc="text"/></div>`);
+2 -4
View File
@@ -5,15 +5,13 @@ exports[`Link component can render simple cases 1`] = `
) {
let utils = this.constructor.utils;
let owner = context;
let sibling = null;
var h = this.h;
let _1 = utils.toObj({'router-link-active':context['isActive']});
var _2 = context['href'];
let c3 = [], p3 = {key:3,attrs:{href: _2},class:_1,on:{}};
var vn3 = h('a', p3, c3);
if (!context['navigate']) {
throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__template__1'\`)
}
extra.handlers['click' + 3] = extra.handlers['click' + 3] || context['navigate'].bind(owner);
extra.handlers['click' + 3] = extra.handlers['click' + 3] || function (e) {const fn = context['navigate'];if (fn) { fn.call(owner, e); } else { context.navigate; }};
p3.on['click'] = extra.handlers['click' + 3];
const slot4 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot4) {
@@ -7,38 +7,39 @@ exports[`RouteComponent can render simple cases 1`] = `
let QWeb = this.constructor;
let parent = context;
let owner = context;
let sibling = null;
let result;
var h = this.h;
if (context['routeComponent']) {
//COMPONENT
let key4 = 'key' + context['env'].router.currentRouteName;
let def2;
let templateId5 = key4;
let w3 = templateId5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId5]] : false;
let vn6 = {};
result = vn6;
let props3 = Object.assign({}, context['env'].router.currentParams);
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
if (utils.shallowEqual(props3, w3.__owl__.currentFiber.props)) {
def2 = w3.__owl__.currentFiber.promise;
} else {
w3.destroy();
w3 = false;
}
let key3 = 'key' + context['env'].router.currentRouteName;
let templateId2 = \`__4__\` + key3;
let w2 = templateId2 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId2]] : false;
let vn5 = {};
result = vn5;
let props2 = Object.assign({}, context['env'].router.currentParams);
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (!w3) {
let componentKey3 = \`routeComponent\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['routeComponent'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId5] = w3.__owl__.id;
def2 = w3.__prepare(extra.fiber, undefined, undefined);
def2 = def2.then(vnode=>{if (w3.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId5, hook: {insert(vn) {let nvn=w3.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});utils.defineProxy(vn6, pvnode);w3.__owl__.pvnode = pvnode;});
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined, undefined, sibling);
let pvnode = w2.__owl__.pvnode;
utils.defineProxy(vn5, pvnode);
} else {
def2 = def2 || w3.__updateProps(props3, extra.fiber, undefined, undefined);
def2 = def2.then(()=>{if (w3.__owl__.isDestroyed) {return};let pvnode=w3.__owl__.pvnode;utils.defineProxy(vn6, pvnode);});
let componentKey2 = \`routeComponent\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| context['routeComponent'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap[templateId2] = w2.__owl__.id;
let def1 = w2.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId2, hook: {insert(vn) { let nvn=w2.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w2.destroy();}}});
const fiber = w2.__owl__.currentFiber;
def1.then(function () {if (w2.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
utils.defineProxy(vn5, pvnode);
w2.__owl__.pvnode = pvnode;
}
extra.promises.push(def2);
sibling = w2.__owl__.currentFiber || sibling;
}
return result;
}"
+5 -5
View File
@@ -51,8 +51,8 @@ describe("connecting a component to store", () => {
const state = { a: 1, b: 2 };
const actions = {
doSomething({ state }) {
state.a = 2;
state.b = 3;
state.a = 2;
state.b = 3;
}
};
const store = new Store({ state, actions });
@@ -63,8 +63,8 @@ describe("connecting a component to store", () => {
<span t-esc="a.value"/>
<span t-esc="b.value"/>
</div>`;
a = useStore(state => ({value: state.a}));
b = useStore(state => ({value: state.b}));
a = useStore(state => ({ value: state.a }));
b = useStore(state => ({ value: state.b }));
}
App.prototype.__render = jest.fn(App.prototype.__render);
@@ -79,7 +79,7 @@ describe("connecting a component to store", () => {
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>2</span><span>3</span></div>");
expect(App.prototype.__render).toBeCalledTimes(2);
});
});
test("useStore: do not re-render if not changed", async () => {
let nextId = 1;
-48
View File
@@ -185,20 +185,6 @@ describe("snabbdom", function() {
expect(elm.tagName).toBe("DIV");
});
test("has different tag and id", function() {
let elm: any = document.createElement("div");
vnode0.appendChild(elm);
const vnode1 = h("span#id");
elm = patch(elm, vnode1).elm;
expect(elm.tagName).toBe("SPAN");
expect(elm.id).toBe("id");
});
test("has id", function() {
elm = patch(vnode0, h("div", [h("div#unique")])).elm;
expect(elm.firstChild.id).toBe("unique");
});
test("has correct namespace", function() {
const SVGNamespace = "http://www.w3.org/2000/svg";
const XHTMLNamespace = "http://www.w3.org/1999/xhtml";
@@ -218,20 +204,6 @@ describe("snabbdom", function() {
expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace);
});
test("receives classes in selector", function() {
elm = patch(vnode0, h("div", [h("i.am.a.class")])).elm;
expect(elm.firstChild.classList.contains("am")).toBeTruthy();
expect(elm.firstChild.classList.contains("a")).toBeTruthy();
expect(elm.firstChild.classList.contains("class")).toBeTruthy();
});
test("receives classes in selector when namespaced", function() {
elm = patch(vnode0, h("svg", [h("g.am.a.class.too")])).elm;
expect(elm.firstChild.classList.contains("am")).toBeTruthy();
expect(elm.firstChild.classList.contains("a")).toBeTruthy();
expect(elm.firstChild.classList.contains("class")).toBeTruthy();
});
test("can create elements with text content", function() {
elm = patch(vnode0, h("div", ["I am a string"])).elm;
expect(elm.innerHTML).toBe("I am a string");
@@ -268,26 +240,6 @@ describe("snabbdom", function() {
});
describe("patching an element", function() {
test("changes the elements classes", function() {
const vnode1 = h("i.i.am.horse");
const vnode2 = h("i.i.am");
patch(vnode0, vnode1);
elm = patch(vnode1, vnode2).elm;
expect(elm.classList.contains("i")).toBeTruthy();
expect(elm.classList.contains("am")).toBeTruthy();
expect(!elm.classList.contains("horse")).toBeTruthy();
});
test("removes missing classes", function() {
const vnode1 = h("i.i.am.horse");
const vnode2 = h("i.i.am");
patch(vnode0, vnode1);
elm = patch(vnode1, vnode2).elm;
expect(elm.classList.contains("i")).toBeTruthy();
expect(elm.classList.contains("am")).toBeTruthy();
expect(!elm.classList.contains("horse")).toBeTruthy();
});
test("changes an elements props", function() {
const vnode1 = h("a", { props: { src: "http://other/" } });
const vnode2 = h("a", { props: { src: "http://localhost/" } });
+1 -1
View File
@@ -144,7 +144,7 @@ class App extends owl.Component {
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const templates = await owl.utils.loadFile("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
+2 -2
View File
@@ -127,7 +127,7 @@ async function makeApp(js, css, xml) {
const JS = `
async function loadTemplates() {
try {
return owl.utils.loadTemplates('app.xml');
return owl.utils.loadFile('app.xml');
} catch(e) {
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
}
@@ -430,7 +430,7 @@ App.components = { TabbedEditor };
async function start() {
document.title = `${document.title} (v${owl.__info__.version})`;
const [templates] = await Promise.all([
owl.utils.loadTemplates("templates.xml"),
owl.utils.loadFile("templates.xml"),
owl.utils.whenReady()
]);
const qweb = new owl.QWeb(templates);
+23 -31
View File
@@ -11,9 +11,9 @@ class Greeter extends Component {
// Main root component
class App extends Component {
static components = { Greeter };
state = useState({ name: 'World'});
}
App.components = { Greeter };
// Application setup
// Note that the xml templates are injected into the global TEMPLATES variable.
@@ -58,7 +58,6 @@ class Counter extends Component {
class App extends Component {
state = useState({ flag: false, componentFlag: false, numbers: [] });
static components = { Counter };
toggle(key) {
this.state[key] = !this.state[key];
@@ -68,8 +67,8 @@ class App extends Component {
const n = this.state.numbers.length + 1;
this.state.numbers.push(n);
}
}
App.components = { Counter };
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({qweb});
@@ -217,7 +216,6 @@ class DemoComponent extends Component {
}
class App extends Component {
static components = { DemoComponent };
state = useState({ n: 0, flag: true });
increment() {
@@ -228,6 +226,7 @@ class App extends Component {
this.state.flag = !this.state.flag;
}
}
App.components = { DemoComponent };
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
@@ -332,20 +331,18 @@ class ToolbarButton extends Component {
}
}
class Toolbar extends Component {
static components = { ToolbarButton };
}
class Toolbar extends Component {}
Toolbar.components = { ToolbarButton };
// Main root component
class App extends Component {
static components = { Toolbar };
toggleTheme() {
const { background, foreground } = this.env.themeContext.state;
this.env.themeContext.state.background = foreground;
this.env.themeContext.state.foreground = background;
}
}
App.components = { Toolbar };
// Application setup
const themeContext = new Context({
@@ -469,10 +466,6 @@ class TodoItem extends Component {
useAutofocus("input");
}
editTodo() {
this.state.isEditing = true;
}
handleKeyup(ev) {
if (ev.keyCode === ENTER_KEY) {
this.updateTitle(ev.target.value);
@@ -497,7 +490,6 @@ class TodoItem extends Component {
// TodoApp
//------------------------------------------------------------------------------
class TodoApp extends Component {
static components = { TodoItem };
state = useState({ filter: "all" });
todos = useStore(state => state.todos);
dispatch = useDispatch();
@@ -537,6 +529,7 @@ class TodoApp extends Component {
this.state.filter = filter;
}
}
TodoApp.components = { TodoItem };
//------------------------------------------------------------------------------
// App Initialization
@@ -606,7 +599,7 @@ const TODO_APP_STORE_XML = `<templates>
<li t-name="TodoItem" class="todo" t-att-class="{completed: props.completed, editing: state.isEditing}">
<div class="view">
<input class="toggle" type="checkbox" t-on-change="dispatch('toggleTodo', props.id)" t-att-checked="props.completed"/>
<label t-on-dblclick="editTodo">
<label t-on-dblclick="state.isEditing = true">
<t t-esc="props.title"/>
</label>
<button class="destroy" t-on-click="dispatch('removeTodo', props.id)"></button>
@@ -1014,23 +1007,20 @@ class Navbar extends owl.Component {}
class MobileSearchView extends owl.Component {}
class ControlPanel extends owl.Component {
static components = { MobileSearchView };
}
class ControlPanel extends owl.Component {}
ControlPanel.components = { MobileSearchView };
class AdvancedComponent extends owl.Component {}
class FormView extends owl.Component {
static components = { AdvancedComponent };
}
class FormView extends owl.Component {}
FormView.components = { AdvancedComponent };
class Chatter extends owl.Component {
messages = Array.from(Array(100).keys());
}
class App extends owl.Component {
static components = { Navbar, ControlPanel, FormView, Chatter };
}
class App extends owl.Component {}
App.components = { Navbar, ControlPanel, FormView, Chatter };
//------------------------------------------------------------------------------
// Responsive plugin
@@ -1188,13 +1178,13 @@ class Counter extends Component {
// Main root component
class App extends Component {
static components = {Card, Counter};
state = useState({a: 1, b: 3});
inc(key, delta) {
this.state[key] += delta;
}
}
App.components = {Card, Counter};
// Application setup
const qweb = new owl.QWeb(TEMPLATES);
@@ -1283,9 +1273,10 @@ const ASYNC_COMPONENTS = `// This example will not work if your browser does not
// 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
// because of the slow component. We use the AsyncRoot component for this
// purpose. Try removing it to see the difference.
const { Component, useState } = owl;
const { AsyncRoot } = owl.misc;
class SlowComponent extends Component {
willUpdateProps() {
@@ -1298,7 +1289,6 @@ class SlowComponent extends Component {
class NotificationList extends Component {}
class App extends Component {
static components = {SlowComponent, NotificationList};
state = useState({ value: 0, notifs: [] });
increment() {
@@ -1311,6 +1301,7 @@ class App extends Component {
}, 3000);
}
}
App.components = {SlowComponent, NotificationList};
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
@@ -1321,9 +1312,10 @@ const ASYNC_COMPONENTS_XML = `<templates>
<div t-name="App" class="app">
<button t-on-click="increment">Increment</button>
<SlowComponent value="state.value"/>
<NotificationList t-asyncroot="1" notifications="state.notifs"/>
<AsyncRoot>
<NotificationList notifications="state.notifs"/>
</AsyncRoot>
</div>
<div t-name="SlowComponent" class="value" >
Current value: <t t-esc="props.value"/>
</div>
@@ -1492,7 +1484,6 @@ class Window extends Component {
}
class WindowManager extends Component {
static components = { Window };
windows = [];
nextId = 1;
currentZindex = 1;
@@ -1538,15 +1529,16 @@ class WindowManager extends Component {
ev.target.style["z-index"] = w.zindex;
}
}
WindowManager.components = { Window };
class App extends Component {
static components = { WindowManager };
wmRef = useRef("wm");
addWindow(name) {
this.wmRef.comp.addWindow(name);
}
}
App.components = { WindowManager };
const qweb = new owl.QWeb(TEMPLATES);
const windows = [