Compare commits

...

45 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Anyway, I could not find the cause of the issue, but
simply waiting an extra microtask tick seems to work.
2019-10-31 12:32:26 +01:00
60 changed files with 7926 additions and 1313 deletions
+4 -3
View File
@@ -92,7 +92,8 @@ A complete documentation for Owl can be found here:
The most important sections are:
- [Quick Start](doc/learning/quick_start.md)
- [Tutorial: TodoList application](doc/learning/tutorial_todoapp.md)
- [QWeb templating language](doc/reference/qweb_templating_language.md)
- [Component](doc/reference/component.md)
- [Hooks](doc/reference/hooks.md)
@@ -103,8 +104,8 @@ Submit a PR!
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.0.0-alpha.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha/owl.js)
- [owl-1.0.0-alpha.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha/owl.min.js)
- [owl-1.0.0-alpha4.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha3/owl.js)
- [owl-1.0.0-alpha4.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-alpha3/owl.min.js)
Some npm scripts are available:
+1 -1
View File
@@ -274,7 +274,7 @@ class Counter extends Component {
dispatch = useDispatch();
}
owl.config.env.store = store;
Counter.env.store = store;
const counter = new Counter();
```
-48
View File
@@ -1,48 +0,0 @@
# 🦉 Environment 🦉
An environment is an object which contains a [`QWeb` instance](../reference/qweb.md).
Whenever a root component is created, it is assigned an environment (see the
reference section on [environment](../reference/environment.md). This environment
is then automatically given to each sub components (and accessible in the `this.env` property).
The environment is mostly static. Each application is free to add anything to
the environment, which is very useful, since this can be accessed by each sub
component.
Some good use cases for the environment is:
- some configuration keys,
- session information,
- generic services (such as doing rpcs, or accessing local storage).
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
For example:
```js
async function myEnv() {
const templates = await loadTemplates();
const qweb = new QWeb({templates});
const session = getSession();
return {
_t: myTranslateFunction,
session: session,
qweb: qweb,
services: {
localStorage: localStorage,
rpc: rpc,
},
debug: false,
inMobileMode: true,
};
}
async function start() {
owl.config.env = await myEnv();
const app = new App();
await app.mount(document.body);
}
```
+1 -2
View File
@@ -97,8 +97,7 @@ class ClickCounter extends owl.Component {
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadFile("templates.xml");
const qweb = new owl.QWeb({ templates });
owl.config.env.qweb = qweb;
ClickCounter.env = { qweb: new owl.QWeb({ templates }) };
const counter = new ClickCounter();
const target = document.getElementById("main");
await counter.mount(target);
File diff suppressed because it is too large Load Diff
+46 -60
View File
@@ -1,61 +1,5 @@
# 🦉 OWL Documentation 🦉
## Owl Content
Owl is a javascript library that contains some core classes and function to help
build applications. Here is a complete representation of its content:
```
owl
Component
Context
QWeb
Store
useState
config
mode
env
core
EventBus
Observer
hooks
onWillStart
onMounted
onWillUpdateProps
onWillPatch
onPatched
onWillUnmount
useContext
useState
useRef
useSubEnv
useStore
useDispatch
useGetters
misc
AsyncRoot
router
Link
RouteComponent
Router
tags
xml
utils
debounce
escape
loadJS
loadFile
shallowEqual
whenReady
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
## Learning Resources
- [Quick Start: create an (almost) empty Owl application](learning/quick_start.md)
- [Environment: what it is and what it should contain](learning/environment.md)
## Reference
- [Animations](reference/animations.md)
@@ -65,14 +9,21 @@ Note that for convenience, the `useState` hook is also exported at the root of t
- [Environment](reference/environment.md)
- [Event Bus](reference/event_bus.md)
- [Hooks](reference/hooks.md)
- [Misc](reference/misc.md)
- [Miscellaneous Components](reference/misc.md)
- [Observer](reference/observer.md)
- [QWeb](reference/qweb.md)
- [Props](reference/props.md)
- [QWeb Templating Language](reference/qweb_templating_language.md)
- [QWeb Engine](reference/qweb_engine.md)
- [Router](reference/router.md)
- [Store](reference/store.md)
- [Tags](reference/tags.md)
- [Utils](reference/utils.md)
## Learning Resources
- [Quick Start: create an (almost) empty Owl application](learning/quick_start.md)
- [Tutorial: create a TodoList application](learning/tutorial_todoapp.md)
## Miscellaneous
- [Comparison with React/Vue](comparison.md)
@@ -81,8 +32,43 @@ Note that for convenience, the `useState` hook is also exported at the root of t
## Architecture
This section explains in more detail the inner workings of Owl. It is more
useful for people working on Owl code.
This section explains in more detail the inner workings of Owl. It is targeted
for developers working on Owl itself.
- [Virtual DOM](architecture/vdom.md)
- [Rendering](architecture/rendering.md)
## Owl Content
Here is a complete visual representation of everything exported by the `owl`
global object (so, for example, `Component` is available at `owl.Component`,
and `EventBus` is exported as `owl.core.EventBus`):
```
Component misc
Context AsyncRoot
QWeb router
Store Link
useState RouteComponent
config Router
mode tags
core xml
EventBus utils
Observer debounce
hooks escape
onWillStart loadJS
onMounted loadFile
onWillUpdateProps shallowEqual
onWillPatch whenReady
onPatched
onWillUnmount
useContext
useState
useRef
useSubEnv
useStore
useDispatch
useGetters
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
+70 -141
View File
@@ -14,11 +14,11 @@
- [Composition](#composition)
- [Event Handling](#event-handling)
- [Form Input Bindings](#form-input-bindings)
- [`t-key` Directive](#t-key-directive)
- [Semantics](#semantics)
- [Props Validation](#props-validation)
- [References](#references)
- [Slots](#slots)
- [Dynamic sub components](#dynamic-sub-components)
- [Asynchronous Rendering](#asynchronous-rendering)
- [Error Handling](#error-handling)
- [Functional Components](#functional-components)
@@ -42,7 +42,7 @@ OWL components are the building blocks for user interface. They are designed to
and follow the QWeb specification. This is a requirement for Odoo.
OWL components are defined as a subclass of Component. The rendering is
exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in QWeb).
exclusively done by a [QWeb](qweb_templating_language.md) template (which needs to be preloaded in QWeb).
Rendering a component generates a virtual dom representation
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
@@ -81,7 +81,7 @@ a state object is defined, by using the `useState` hook. It is not mandatory to
An Owl component is a small class which represents a component or some UI element.
It exists in the context of an [environment](environment.md) (`env`), which is propagated from a
parent to its children. The environment needs to have a [QWeb](qweb.md) instance, which
parent to its children. The environment needs to have a [QWeb](qweb_templating_language.md) instance, which
will be used to render the component template.
Be aware that the name of the component may be significant: if a component does
@@ -166,7 +166,7 @@ to be called in the constructor.
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
component is not mounted.
- **`env`** (Object): the component environment, which contains a QWeb instance.
- **`env`** (Object): the component [environment](environment.md), which contains a QWeb instance.
- **`props`** (Object): this is an object containing all the properties given by
the parent to a child component. For example, in the following situation,
@@ -237,16 +237,15 @@ component.
We explain here all the public methods of the `Component` class.
- **`mount(target, renderBeforeRemount=false)`** (async): this is the main way a
- **`mount(target)`** (async): this is the main way a
component is added to the DOM: the root component is mounted to a target
HTMLElement. Obviously, this is asynchronous, since each children need to be
created as well. Most applications will need to call `mount` exactly once, on
the root component.
The `renderBeforeRemount` argument is useful when a component is unmounted and remounted.
In that case, we may want to rerender the component _before_ it is remounted, if
we know that its state (or something in the environment, or ...) has changed.
In that case, it should simply set to `true`.
Note that if a component is mounted, unmounted and remounted, it will be
automatically re-rendered to ensure that changes in its state (or something
in the environment, or in the store, or ...) will be taken into account.
- **`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
@@ -443,8 +442,13 @@ const app = new App();
app.mount(document.body);
```
The root component does not have a parent nor props. It will be setup with an
[environment](environment.md) (located in `owl.config.env`).
The root component does not have a parent nor `props` (see note below). It will be setup with an
[environment](environment.md) (either the `env` defined on its class, or a
default empty environment).
Note: a root component can however be given a `props` object in its constructor,
like this: `new App(null, {some: 'object'});`. It will not be a true `props`
object, managed by Owl (so, for example, it will never be updated).
### Composition
@@ -498,66 +502,8 @@ the static `components` key, then fallbacks on the global registry.
_Props_: In this example, the child component will receive the object `{count: 4}` in its
constructor. This will be assigned to the `props` variable, which can be accessed
on the component (and also, in the template). Whenever the state is updated, then
the sub component will also be updated automatically.
Note that there are restrictions on valid prop names: `class`, `style` and any
string which starts with `t-` are not allowed.
It is not common, but sometimes we need a dynamic component name and/or dynamic props. In this case,
the `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
```xml
<div t-name="ParentComponent">
<t t-component="ChildComponent{{id}}" />
</div>
```
```js
class ParentComponent {
static components = { ChildComponent1, ChildComponent2 };
state = { id: 1 };
}
```
And the `t-props` directive can be used to specify totally dynamic props:
```xml
<div t-name="ParentComponent">
<Child t-props="some.obj"/>
</div>
```
```js
class ParentComponent {
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
```
There is an even more dynamic way to use `t-component`: its value can be an
expression evaluating to an actual component class. In that case, this is the
class that will be used to create the component:
```js
class A extends Component<any, any, any> {
static template = xml`<span>child a</span>`;
}
class B extends Component<any, any, any> {
static template = xml`<span>child b</span>`;
}
class App extends Component<any, any, any> {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = { child: "a" };
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
```
In this example, the component `App` selects dynamically the concrete sub
component class.
the sub component will also be updated automatically. See the [props section](props.md)
for more information.
**CSS and style:** Owl allows the parent to declare
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
@@ -621,11 +567,13 @@ A _business_ DOM event is triggered by a call to `trigger` on a component.
}
```
The call to `trigger` generates a [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
of type `menu-loaded` and dispatches it on the component's DOM element
(`this.el`). The event bubbles and is cancelable. The parent component listening
to event `menu-loaded` will receive the payload in its `someMethod` handler
(in the `detail` property of the event), whenever the event is triggered.
The call to `trigger` generates an `OwlEvent`, a subclass of [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
with an additional attribute `originalComponent` (the component that triggered
the event). The generated event is of type `menu-loaded` and dispatches it on
the component's DOM element (`this.el`). The event bubbles and is cancelable.
The parent component listening to event `menu-loaded` will receive the payload
in its `someMethod` handler (in the `detail` property of the event), whenever
the event is triggered.
```js
class ParentComponent {
@@ -783,70 +731,6 @@ update a number whenever the change is done.
Note: the online playground has an example to show how it works.
### `t-key` Directive
Even though Owl tries to be as declarative as possible, the DOM does not fully
expose its state declaratively in the DOM tree. For example, the scrolling state,
the current user selection, the focused element or the state of an input are not
set as attribute in the DOM tree. This is why we use a virtual dom
algorithm to keep the actual DOM node as much as possible.
However, in some situations, this is not enough, and we need to help Owl decide
if an element is actually the same, or is a different element with the same
properties.
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
and we render them in this template:
```xml
<p t-foreach="items" t-as="item"><t t-esc="item.text"/></p>
```
The result will be two `<p>` tags with text `a` and `b`. Now, if we swap them,
and rerender the template, Owl needs to know what the intent is:
- should Owl actually swap the DOM nodes,
- or should it keep the DOM nodes, but with an updated text content?
This might look trivial, but it actually matters. These two possibilities lead
to different results in some cases. For example, if the user selected the text
of the first `p`, swapping them will keep the selection while updating the
text content will not.
There are many other cases where this is important: `input` tags with their
value, css classes and animations, scroll position...
So, the `t-key` directive is used to give an identity to an element. It allows
Owl to understand if different elements of a list are actually different or not.
The above example could be modified by adding an ID: `[{id: 1, text: "a"}, {id: 2, text: "b"}]`.
Then, the template could look like this:
```xml
<p t-foreach="items" t-as="item" t-key="item.id"><t t-esc="item.text"/></p>
```
The `t-key` directive is useful for lists (`t-foreach`). A key should be
a unique number or string (objects will not work: they will be cast to the
`"[object Object]"` string, which is obviously not unique).
Also, the key can be set on a `t` tag or on its children. The following variations
are all equivalent:
```xml
<p t-foreach="items" t-as="item" t-key="item.id">
<t t-esc="item.text"/>
</p>
<t t-foreach="items" t-as="item" t-key="item.id">
<p t-esc="item.text"/>
</t>
<t t-foreach="items" t-as="item">
<p t-key="item.id" t-esc="item.text"/>
</t>
```
### Semantics
We give here an informal description of the way components are created/updated
@@ -941,7 +825,7 @@ As an application becomes complex, it may be quite unsafe to define props in an
- hard to tell how a component should be used, by looking at its code.
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
A props type system would solve both issues, by describing the types and shapes
A props type system solves both issues, by describing the types and shapes
of the props. Here is how it works in Owl:
- `props` key is a static key (so, different from `this.props` in a component instance)
@@ -1141,6 +1025,51 @@ be considered the `default` slot. For example:
</div>
```
### Dynamic sub components
It is not common, but sometimes we need a dynamic component name. In this case,
the `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb_templating_language.md#dynamic-attributes) directive):
```xml
<div t-name="ParentComponent">
<t t-component="ChildComponent{{id}}" />
</div>
```
```js
class ParentComponent {
static components = { ChildComponent1, ChildComponent2 };
state = { id: 1 };
}
```
There is an even more dynamic way to use `t-component`: its value can be an
expression evaluating to an actual component class. In that case, this is the
class that will be used to create the component:
```js
class A extends Component<any, any, any> {
static template = xml`<span>child a</span>`;
}
class B extends Component<any, any, any> {
static template = xml`<span>child b</span>`;
}
class App extends Component<any, any, any> {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = { child: "a" };
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
```
In this example, the component `App` selects dynamically the concrete sub
component class.
Note that the `t-component` directive can only be used on `<t>` nodes.
### Asynchronous Rendering
Working with asynchronous code always adds a lot of complexity to a system. Whenever
+4 -33
View File
@@ -1,11 +1,10 @@
# 🦉 Config 🦉
The Owl framework is designed to work in many situations. However, it is
sometimes necessary to customize some behaviour. This is done by using the
global `config` object. It currently has two keys:
The Owl framework is designed to work in many situations. However, it is
sometimes necessary to customize some behaviour. This is done by using the
global `config` object. It currently has one key:
- [`mode`](#mode),
- [`env`](#env).
- [`mode`](#mode).
## Mode
@@ -26,31 +25,3 @@ So, changing this setting is best done at startup.
An important job done by the `dev` mode is to validate props for each component
creation and update. Also, extra props will cause an error.
## Env
An Owl application needs an [environment](environment.md) to be executed. The
environment has an important key: the [QWeb](qweb.md) instance, which will render
all templates.
Whenever a root component is mounted, Owl will take the environment from
`owl.config.env` and use it to setup the component (and its children).
- if no environment was setup, an empty environment will be generated,
- if an environment exists, but does not have a QWeb key, a new QWeb instance
will then be added to the environment.
The correct way to customize an environment is to simply modify `owl.config.env`
before the first component is created:
```js
owl.config.env = {
_t: myTranslateFunction,
user: {...},
services: {
...
},
};
const app = new App();
app.mount(document.body);
```
+9 -9
View File
@@ -28,7 +28,7 @@ context, and add it to the environment:
```js
const deviceContext = new Context({ isMobile: true });
owl.config.env.deviceContext = deviceContext;
App.env.deviceContext = deviceContext;
```
If we want to make it completely responsive, we need to update its value whenever
@@ -53,14 +53,14 @@ fact that we are in a mobile or desktop mode.
```js
class SomeComponent extends Component {
static template = xml`
<div>
<t t-if=device.isMobile>
some simplified user interface
</t>
<t t-else="1">
some more sopthisticated user interface
</t>
`;
<div>
<t t-if=device.isMobile>
some simplified user interface
</t>
<t t-else="1">
a more advanced user interface
</t>
</div>`;
device = useContext(this.env.deviceContext);
}
```
+93 -6
View File
@@ -1,7 +1,18 @@
# 🦉 Environment 🦉
An environment is an object which contains a [`QWeb` instance](qweb.md). Whenever a root component is created, it is assigned an environment. This environment
is then automatically given to each sub component (and accessible in the `this.env` property).
## Content
- [Overview](#overview)
- [Setting an Environment](#setting-an-environment)
- [Content of an Environment](#content-of-an-environment)
## Overview
An environment is an object which contains a [`QWeb` instance](qweb_engine.md). Whenever
a root component is created, it is assigned an environment (see
[below](#setting-an-environment) for more info on this). This environment is
then automatically given to each sub component (and accessible in the `this.env`
property).
```
Root
@@ -9,10 +20,86 @@ is then automatically given to each sub component (and accessible in the `this.e
A B
```
This way, all components share the same `QWeb` instance.
This way, all components share the same `QWeb` instance. Owl internally requires
that the environment has a `qweb` key which maps to a
[`QWeb`](qweb_engine.md) instance. This is the QWeb instance that will be used to
render each templates in this specific component tree. Note that if no `QWeb`
instance is provided, Owl will simply generate it on the fly.
Note: some additional information can be found here:
The environment is mostly static. Each application is free to add anything to
the environment, which is very useful, since this can be accessed by each sub
component.
- [What should go into an environment?](../learning/environment.md)
- [Customizing an environment](config.md#env)
## Setting an environment
An Owl application needs an [environment](environment.md) to be executed. The
environment has an important key: the [QWeb](qweb_engine.md) instance, which will render
all templates.
Whenever a root component `App` is mounted, Owl will setup a valid environment by
following the next steps:
- take the `env` object defined on `App.env` (if no `env` was explicitely setup,
this will be return the empty `env` object defined on `Component`)
- if `env.qweb` is not set, then Owl will create a `QWeb` instance.
The correct way to customize an environment is to simply set it up on the root
component class, before the first component is created:
```js
App.env = {
_t: myTranslateFunction,
user: {...},
services: {
...
},
};
const app = new App();
app.mount(document.body);
```
It is also possible to simply share an environment between all root components,
by simply doing this:
```js
Component.env = myEnv; // will be the default env for all components
```
## Content of an Environment
Some good use cases for additional keys in the environment are:
- some configuration keys,
- session information,
- generic services (such as doing rpcs, or accessing local storage).
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
For example:
```js
async function myEnv() {
const templates = await loadTemplates();
const qweb = new QWeb({ templates });
const session = getSession();
return {
_t: myTranslateFunction,
session: session,
qweb: qweb,
services: {
localStorage: localStorage,
rpc: rpc
},
debug: false,
inMobileMode: true
};
}
async function start() {
App.env = await myEnv();
const app = new App();
await app.mount(document.body);
}
```
+7 -3
View File
@@ -290,6 +290,7 @@ class Parent extends Component {
// here, if component is mounted, refs are active:
// - this.divRef.el is the div HTMLElement
// - this.subRef.comp is the instance of the sub component
// - this.subRef.el is the root HTML node of the sub component (i.e. this.subRef.comp.el)
}
}
```
@@ -297,11 +298,14 @@ class Parent extends Component {
As shown by the example above, html elements are accessed by using the `el`
key, and components references are accessed with `comp`.
Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`.
Notes:
- if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`,
- on a component, accessing `ref.el` will get the root node of the component.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
(like the [`t-attf-`](qweb_templating_language.md#dynamic-attributes) and
`t-component` directives). For example,
```xml
+97
View File
@@ -0,0 +1,97 @@
# 🦉 Props 🦉
## Content
- [Overview](#overview)
- [Definition](#definition)
- [Good Practices](#good-practices)
- [Dynamic Props](#dynamic-props)
## Overview
In Owl, `props` (short for _properties_) is an object which contains every piece
of data given to a component by its parent.
```js
class Child extends Component {
static template = xml`<div><t t-esc="props.a"/><t t-esc="props.b"/></div>`;
}
class Parent extends Component {
static template = xml`<div><ComponentA a="state.a" b="'string'"/></div>`;
static components = { Child };
state = useState({ a: "fromparent" });
}
```
In this example, the `Child` component receives two props from its parent: `a`
and `b`. They are collected into a `props` object by Owl, with each value being
evaluated in the context of the parent. So, `props.a` is equal to `'fromparent'` and
`props.b` is equal to `'string'`.
Note that `props` is an object that only makes sense from the perspective of the
child component.
## Definition
The `props` object is made of every attributes defined on the template, with the
following exceptions:
- every attribute starting with `t-` are not props (they are QWeb directives),
- `style` and `class` attributes are excluded as well (they are applied by Owl on
the root element of the component).
In the following example:
```xml
<div>
<ComponentA a="state.a" b="'string'"/>
<ComponentB t-if="state.flag" model="model"/>
<ComponentC style="color:red;" class="left-pane" />
</div>
```
the `props` object contains the following keys:
- for `ComponentA`: `a` and `b`,
- for `ComponentB`: `model`,
- for `ComponentC`: empty object
## Good Practices
A `props` object is a collection of values that come from the parent. As such,
they are owned by the parent, and should never be modified by the child:
```js
class MyComponent extends Component {
constructor(parent, props) {
super(parent, props);
props.a.b = 43; // Never do that!!!
}
}
```
Props should be considered readonly, from the perspective of the child component.
If there is a need to modify them, then the request to update them should be
sent to the parent (for example, with an event).
Any value can go in a props. Strings, objects, classes, or even callbacks could
be given to a child component (but then, in the case of callbacks, communicating
with events seems more appropriate).
## Dynamic Props
The `t-props` directive can be used to specify totally dynamic props:
```xml
<div t-name="ParentComponent">
<Child t-props="some.obj"/>
</div>
```
```js
class ParentComponent {
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
```
+151
View File
@@ -0,0 +1,151 @@
# 🦉 QWeb Engine 🦉
## Content
- [Overview](#overview)
- [Reference](#reference)
## Overview
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
templating engine used by Odoo. The QWeb class in the OWL project is an
implementation of that specification with a few interesting points:
- it compiles templates into functions that output a virtual DOM instead of a
string. This is necessary for the component system.
- it has a few extra directives: `t-component`, `t-on`, ...
We present in this section the engine, not the templating language.
## Reference
This section is about the javascript code that implements the `QWeb` specification.
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
instantiated:
```js
const qweb = new owl.QWeb();
```
Its API is quite simple:
- **`constructor(config)`**: constructor. Takes an optional configuration object
with an optional `templates` string to add initial
templates (see `addTemplates` for more information on format of the string)
and an optional `translateFn` translate function (see the section on
[translations](#translations)).
```js
const qweb = new owl.QWeb({ templates: TEMPLATES, translateFn: _t });
```
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
```js
qweb.addTemplate("mytemplate", "<div>hello</div>");
```
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply
ignore templates added for a second time. Otherwise, `QWeb` will crash.
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute).
```js
const TEMPLATES = `
<templates>
<div t-name="App" class="main">main</div>
<div t-name="OtherComponent">other component</div>
</templates>`;
qweb.addTemplates(TEMPLATES);
```
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
which is a virtual representation of the DOM (see [vdom doc](../architecture/vdom.md)).
```js
const vnode = qweb.render("App", component);
```
- **`renderToString(name, context)`**: renders a template, but returns an html
string.
```js
const str = qweb.renderToString("someTemplate", somecontext);
```
- **`registerTemplate(name, template)`**: static function to register a global
QWeb template. This is useful for commonly used components accross the
application, and for making a template available to an application without
having a reference to the actual QWeb instance.
```js
QWeb.registerTemplate("mytemplate", `<div>some template</div>`);
```
- **`registerComponent(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-component` directive). This is useful for commonly used
components accross the application.
```js
class Dialog extends owl.Component { ... }
QWeb.registerComponent("Dialog", Dialog);
...
class ParentComponent extends owl.Component { ... }
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
```
In some way, a `QWeb` instance is the core of an Owl application. It is the only
mandatory element of an [environment](environment.md). As such, it
has an extra responsibility: it can act as an event bus for internal communication
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
### Translations
If properly setup, Owl QWeb engine can translate all rendered templates. To do
so, it needs a translate function, which takes a string and returns a string.
For example:
```js
const translations = {
hello: "bonjour",
yes: "oui",
no: "non"
};
const translateFn = str => translations[str] || str;
const qweb = new QWeb({ translateFn });
```
Once setup, all rendered templates will be translated using `translateFn`:
- each text node will be replaced with its translation,
- each of the following attribute values will be translated as well: `title`,
`placeholder`, `label` and `alt`,
- translating text nodes can be disabled with the special attribute `t-translation`,
if its value is `off`.
So, with the above `translateFn`, the following templates:
```xml
<div>hello</div>
<div t-translation="off">hello</div>
<div>Are you sure?</div>
<input placeholder="hello" other="yes"/>
```
will be rendered as:
```xml
<div>bonjour</div>
<div>hello</div>
<div>Are you sure?</div>
<input placeholder="bonjour" other="yes"/>
```
Note that the translation is done during the compilation of the template, not
when it is rendered.
@@ -1,10 +1,9 @@
# 🦉 QWeb 🦉
# 🦉 QWeb Templating Language🦉
## Content
- [Overview](#overview)
- [Directives](#directives)
- [QWeb Engine](#qweb-engine)
- [Reference](#reference)
- [White Spaces](#white-spaces)
- [Root Nodes](#root-nodes)
@@ -21,14 +20,11 @@
## Overview
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
templating engine used by Odoo. It is based on the XML format, and used
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the HTML.
Template directives are specified as XML attributes prefixed with `t-`, for instance `t-if` for conditionals, with elements and other attributes being rendered directly.
To avoid element rendering, a placeholder element `<t>` is also available, which executes its directive but doesn’t generate any output in and of itself.
```xml
<div>
<span t-if="somecondition">Some string</span>
@@ -40,135 +36,48 @@ To avoid element rendering, a placeholder element `<t>` is also available, which
</div>
```
The QWeb class in the OWL project is an implementation of that specification
with a few interesting points:
Template directives are specified as XML attributes prefixed with `t-`, for
instance `t-if` for conditionals, with elements and other attributes being
rendered directly.
- it compiles templates into functions that output a virtual DOM instead of a
string. This is necessary for the component system.
- it has a few extra directives: `t-component`, `t-on`, ...
To avoid element rendering, a placeholder element `<t>` is also available, which
executes its directive but doesn’t generate any output in and of itself.
We present in this section the templating language, including its Owl specific
extensions.
## Directives
We present here a list of all standard QWeb directives:
For reference, here is a list of all standard QWeb directives:
| Name | Description |
| ------------------------------ | ------------------------------------------------------------ |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-translation` | [Disabling the translation of a node](#translations) |
| `t-name` | [Defining a template (not really a directive)](#qweb-engine) |
| Name | Description |
| ------------------------------ | -------------------------------------------------------------- |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-translation` | [Disabling the translation of a node](#translations) |
| `t-name` | [Defining a template (not really a directive)](qweb_engine.md) |
The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
| Name | Description |
| ------------------------ | ----------------------------------------------------------------------------------- |
| `t-component`, `t-props` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
## QWeb Engine
This section is about the javascript code that implements the `QWeb` specification.
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
instantiated:
```js
const qweb = new owl.QWeb();
```
Its API is quite simple:
- **`constructor(config)`**: constructor. Takes an optional configuration object
with an optional `templates` string to add initial
templates (see `addTemplates` for more information on format of the string)
and an optional `translateFn` translate function (see the section on
[translations](#translations)).
```js
const qweb = new owl.QWeb({ templates: TEMPLATES, translateFn: _t });
```
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
```js
qweb.addTemplate("mytemplate", "<div>hello</div>");
```
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply
ignore templates added for a second time. Otherwise, `QWeb` will crash.
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute).
```js
const TEMPLATES = `
<templates>
<div t-name="App" class="main">main</div>
<div t-name="OtherComponent">other component</div>
</templates>`;
qweb.addTemplates(TEMPLATES);
```
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
which is a virtual representation of the DOM (see [vdom doc](../architecture/vdom.md)).
```js
const vnode = qweb.render("App", component);
```
- **`renderToString(name, context)`**: renders a template, but returns an html
string.
```js
const str = qweb.renderToString("someTemplate", somecontext);
```
- **`registerTemplate(name, template)`**: static function to register a global
QWeb template. This is useful for commonly used components accross the
application, and for making a template available to an application without
having a reference to the actual QWeb instance.
```js
QWeb.registerTemplate("mytemplate", `<div>some template</div>`);
```
- **`registerComponent(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-component` directive). This is useful for commonly used
components accross the application.
```js
class Dialog extends owl.Component { ... }
QWeb.registerComponent("Dialog", Dialog);
...
class ParentComponent extends owl.Component { ... }
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
```
In some way, a `QWeb` instance is the core of an Owl application. It is the only
mandatory element of an [environment](environment.md). As such, it
has an extra responsibility: it can act as an event bus for internal communication
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
| Name | Description |
| ------------------------ | ------------------------------------------------------------------------------- |
| `t-component`, `t-props` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
## Reference
We define in this section the specification of how `QWeb` templates should be
rendered. Note that we only document here the standard QWeb specification. Owl
specific extensions are documented in various other parts of the documentation.
### White Spaces
White spaces in a template are handled in a special way:
@@ -479,16 +388,66 @@ into the global context.
<!-- new_variable undefined -->
```
Owl QWeb is used as the template engine for components. Components are frequently
updated, and reuse as much of the previous DOM as possible. Loops offer a specific
problem for this usecase: how does the template engine know if two rows have
been swapped, or if the content of these rows was changed? To help Owl with that,
there is an additional directive: [`t-key`](component.md#t-key-directive).
Even though Owl tries to be as declarative as possible, the DOM does not fully
expose its state declaratively in the DOM tree. For example, the scrolling state,
the current user selection, the focused element or the state of an input are not
set as attribute in the DOM tree. This is why we use a virtual dom
algorithm to keep the actual DOM node as much as possible.
However, in some situations, this is not enough, and we need to help Owl decide
if an element is actually the same, or is a different element with the same
properties.
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
and we render them in this template:
```xml
<p t-foreach="state.things" t-as="thing" t-key="thing.id">
<t t-esc="thing.content"/>
<p t-foreach="items" t-as="item"><t t-esc="item.text"/></p>
```
The result will be two `<p>` tags with text `a` and `b`. Now, if we swap them,
and rerender the template, Owl needs to know what the intent is:
- should Owl actually swap the DOM nodes,
- or should it keep the DOM nodes, but with an updated text content?
This might look trivial, but it actually matters. These two possibilities lead
to different results in some cases. For example, if the user selected the text
of the first `p`, swapping them will keep the selection while updating the
text content will not.
There are many other cases where this is important: `input` tags with their
value, css classes and animations, scroll position...
So, the `t-key` directive is used to give an identity to an element. It allows
Owl to understand if different elements of a list are actually different or not.
The above example could be modified by adding an ID: `[{id: 1, text: "a"}, {id: 2, text: "b"}]`.
Then, the template could look like this:
```xml
<p t-foreach="items" t-as="item" t-key="item.id"><t t-esc="item.text"/></p>
```
The `t-key` directive is useful for lists (`t-foreach`). A key should be
a unique number or string (objects will not work: they will be cast to the
`"[object Object]"` string, which is obviously not unique).
Also, the key can be set on a `t` tag or on its children. The following variations
are all equivalent:
```xml
<p t-foreach="items" t-as="item" t-key="item.id">
<t t-esc="item.text"/>
</p>
<t t-foreach="items" t-as="item" t-key="item.id">
<p t-esc="item.text"/>
</t>
<t t-foreach="items" t-as="item">
<p t-key="item.id" t-esc="item.text"/>
</t>
```
If there is no `t-key` directive, Owl will use the index as a default key.
@@ -543,23 +502,9 @@ will result in :
### Translations
If properly setup, Owl QWeb engine can translate all rendered templates. To do
so, it needs a translate function, which takes a string and returns a string.
For example:
```js
const translations = {
hello: "bonjour",
yes: "oui",
no: "non"
};
const translateFn = str => translations[str] || str;
const qweb = new QWeb({ translateFn });
```
Once setup, all rendered templates will be translated using `translateFn`:
By default, QWeb specify that templates should be translated. If this behaviour
is not wanted, there is a `t-translation` directive which can turn off
translations (if it is set to the `off` value), with the following rules:
- each text node will be replaced with its translation,
- each of the following attribute values will be translated as well: `title`,
@@ -567,26 +512,8 @@ Once setup, all rendered templates will be translated using `translateFn`:
- translating text nodes can be disabled with the special attribute `t-translation`,
if its value is `off`.
So, with the above `translateFn`, the following templates:
```xml
<div>hello</div>
<div t-translation="off">hello</div>
<div>Are you sure?</div>
<input placeholder="hello" other="yes"/>
```
will be rendered as:
```xml
<div>bonjour</div>
<div>hello</div>
<div>Are you sure?</div>
<input placeholder="bonjour" other="yes"/>
```
Note that the translation is done during the compilation of the template, not
when it is rendered.
See [here](qweb_engine.md#translations) for more information on how to setup a
translate function in Owl QWeb.
### Debugging
+1 -1
View File
@@ -68,7 +68,7 @@ function makeEnvironment() {
return env;
}
owl.config.env = makeEnvironment();
App.env = makeEnvironment();
// create root component here
```
+1 -1
View File
@@ -44,7 +44,7 @@ it easier to scale application to larger size.
To do so, Owl currently has a small helper that makes it easy to define a
template inside a javascript (or typescript) file: the [`xml`](reference/tags.md#xml-tag)
helper. With this, a template is automatically registered to [QWeb](reference/qweb.md).
helper. With this, a template is automatically registered to [QWeb](reference/qweb_engine.md).
This means that the template and the javascript code can be defined in the same
file. It is not currently possible to add css to the same file, but Owl may
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "owl-framework",
"version": "1.0.0-alpha",
"version": "1.0.0-alpha4",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"engines": {
@@ -44,7 +44,7 @@
"sass": "^1.16.1",
"source-map-support": "^0.5.10",
"ts-jest": "^23.10.5",
"typescript": "^3.6.4",
"typescript": "^3.7.2",
"uglify-es": "^3.3.9"
},
"jest": {
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.0.0-alpha
- Current version: 1.0.0-alpha4
- Status: mostly stable
This roadmap is only an attempt at predicting Owl's future. Everything may
+118 -108
View File
@@ -1,11 +1,11 @@
import { Observer } from "../core/observer";
import { OwlEvent } from "../core/owl_event";
import { CompiledTemplate, QWeb } from "../qweb/index";
import { h, patch, VNode } from "../vdom/index";
import { config } from "../config";
import { patch, VNode } from "../vdom/index";
import "./directive";
import { Fiber } from "./fiber";
import "./props_validation";
import { scheduler } from "./scheduler";
import { Scheduler, scheduler } from "./scheduler";
/**
* Owl Component System
@@ -61,6 +61,9 @@ interface Internal<T extends Env, Props> {
cmap: { [key: number]: number };
currentFiber: Fiber | null;
// parentLastFiberId is there to help the parent component to detect, among
// its children, those that are not used anymore and thus can be destroyed
parentLastFiberId: number;
boundHandlers: { [key: number]: any };
observer: Observer | null;
@@ -88,6 +91,9 @@ export class Component<T extends Env, Props extends {}> {
static components = {};
static props?: any;
static defaultProps?: any;
static env: any = {};
// expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler;
/**
* The `el` is the root element of the component. Note that it could be null:
@@ -111,28 +117,33 @@ export class Component<T extends Env, Props extends {}> {
* hand. Other components should be created automatically by the framework (with
* the t-component directive in a template)
*/
constructor(parent?: Component<T, any>, props?: Props) {
constructor(parent?: Component<T, any> | null, props?: Props) {
Component.current = this;
let constr = this.constructor as any;
const defaultProps = constr.defaultProps;
if (defaultProps) {
props = props || ({} as Props);
this.__applyDefaultProps(props, defaultProps);
}
this.props = <Props>props;
if (QWeb.dev) {
QWeb.utils.validateProps(constr, this.props);
}
const id: number = nextId++;
let depth;
if (parent) {
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps);
}
this.props = <Props>props;
if (QWeb.dev) {
QWeb.utils.validateProps(this.constructor, this.props);
}
this.env = parent.env;
const __powl__ = parent.__owl__;
__powl__.children[id] = this;
depth = __powl__.depth + 1;
} else {
// we are the root component
this.env = config.env as T;
this.props = undefined as unknown as Props;
this.env = (this.constructor as any).env;
if (!this.env.qweb) {
this.env.qweb = new QWeb();
}
this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) {
this.render(true);
@@ -150,7 +161,7 @@ export class Component<T extends Env, Props extends {}> {
}
const qweb = this.env.qweb;
const template = constr.template || this.__getTemplate(qweb);
this.__owl__ = {
id: id,
depth: depth,
@@ -162,6 +173,7 @@ export class Component<T extends Env, Props extends {}> {
children: {},
cmap: {},
currentFiber: null,
parentLastFiberId: 0,
boundHandlers: {},
mountedCB: null,
willUnmountCB: null,
@@ -170,7 +182,7 @@ export class Component<T extends Env, Props extends {}> {
willStartCB: null,
willUpdatePropsCB: null,
observer: null,
renderFn: qweb.render.bind(qweb, this.__getTemplate(qweb)),
renderFn: qweb.render.bind(qweb, template),
classObj: null,
refs: null
};
@@ -269,40 +281,23 @@ export class Component<T extends Env, Props extends {}> {
*
* Note that a component can be mounted an unmounted several times
*/
async mount(target: HTMLElement, renderBeforeRemount: boolean = false): Promise<void> {
async mount(target: HTMLElement): Promise<void> {
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return Promise.resolve();
}
if (__owl__.vnode && !renderBeforeRemount) {
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
}
return;
if (!(target instanceof HTMLElement)) {
let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`;
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message);
}
return new Promise((resolve, reject) => {
const fiber = new Fiber(null, this, undefined, undefined, false);
scheduler.addFiber(fiber, err => {
if (err) {
reject(err);
return;
}
if (!__owl__.isDestroyed) {
this.__patch(fiber.vnode);
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
}
}
resolve();
});
if (!__owl__.vnode) {
this.__prepareAndRender(fiber);
} else {
this.__render(fiber);
}
});
const fiber = new Fiber(null, this, undefined, undefined, false, target);
if (!__owl__.vnode) {
this.__prepareAndRender(fiber);
} else {
this.__render(fiber);
}
return scheduler.addFiber(fiber);
}
/**
@@ -327,26 +322,34 @@ export class Component<T extends Env, Props extends {}> {
*/
async render(force: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
if (
(!__owl__.isMounted && !__owl__.currentFiber) ||
(__owl__.currentFiber && !__owl__.currentFiber.isRendered)
) {
if (!__owl__.isMounted && !__owl__.currentFiber) {
// if we get here, this means that the component was either never mounted,
// or was unmounted and some state change triggered a render. Either way,
// we do not want to actually render anything in this case.
return;
}
return new Promise((resolve, reject) => {
const fiber = new Fiber(null, this, undefined, undefined, force);
scheduler.addFiber(fiber.root, err => {
if (err) {
reject(err);
return;
}
if (__owl__.isMounted && fiber === fiber.root) {
fiber.patchComponents();
}
resolve();
});
this.__render(fiber);
if (__owl__.currentFiber && !__owl__.currentFiber.isRendered) {
return scheduler.addFiber(__owl__.currentFiber.root);
}
// if we aren't mounted at this point, it implies that there is a
// currentFiber that is already rendered (isRendered is true), so we are
// about to be mounted
const isMounted = __owl__.isMounted;
const fiber = new Fiber(null, this, undefined, undefined, force, null);
Promise.resolve().then(() => {
if (__owl__.isMounted || !isMounted) {
// we are mounted (__owl__.isMounted), or if we are currently being
// mounted (!isMounted), so we call __render
this.__render(fiber);
} else {
// we were mounted when render was called, but we aren't anymore, so we
// were actually about to be unmounted ; we can thus forget about this
// fiber
fiber.isCompleted = true;
__owl__.currentFiber = null;
}
});
return scheduler.addFiber(fiber);
}
/**
@@ -386,7 +389,7 @@ export class Component<T extends Env, Props extends {}> {
*/
trigger(eventType: string, payload?: any) {
if (this.el) {
const ev = new CustomEvent(eventType, {
const ev = new OwlEvent(this, eventType, {
bubbles: true,
cancelable: true,
detail: payload
@@ -431,6 +434,9 @@ export class Component<T extends Env, Props extends {}> {
}
__owl__.isDestroyed = true;
delete __owl__.vnode;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true;
}
}
__callMounted() {
@@ -443,13 +449,10 @@ export class Component<T extends Env, Props extends {}> {
}
}
__owl__.isMounted = true;
try {
this.mounted();
if (__owl__.mountedCB) {
__owl__.mountedCB();
}
} catch (e) {
console.error(e); // TODO : add a test
__owl__.currentFiber = null;
this.mounted();
if (__owl__.mountedCB) {
__owl__.mountedCB();
}
}
@@ -483,7 +486,7 @@ export class Component<T extends Env, Props extends {}> {
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const __owl__ = this.__owl__;
const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force);
const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force, null);
if (!parentFiber.child) {
parentFiber.child = fiber;
} else {
@@ -492,7 +495,7 @@ export class Component<T extends Env, Props extends {}> {
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
this.__applyDefaultProps(nextProps, defaultProps);
}
if (QWeb.dev) {
QWeb.utils.validateProps(this.constructor, nextProps);
@@ -501,7 +504,7 @@ export class Component<T extends Env, Props extends {}> {
this.willUpdateProps(nextProps),
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps)
]);
if (fiber.isCancelled) {
if (fiber.isCompleted) {
return;
}
this.props = nextProps;
@@ -518,7 +521,6 @@ 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;
}
/**
@@ -527,7 +529,7 @@ export class Component<T extends Env, Props extends {}> {
* parent template.
*/
__prepare(parentFiber: Fiber, scope: any, vars: any, previousSibling?: Fiber | null) {
const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force);
const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force, null);
fiber.shouldPatch = false;
if (!parentFiber.child) {
parentFiber.child = fiber;
@@ -540,22 +542,18 @@ export class Component<T extends Env, Props extends {}> {
__getTemplate(qweb: QWeb): string {
let p = (<any>this).constructor;
if (!p.hasOwnProperty("_template")) {
if (p.template) {
p._template = p.template;
} else {
// here, the component and none of its superclasses defines a static `template`
// key. So we fall back on looking for a template matching its name (or
// one of its subclass).
// here, the component and none of its superclasses defines a static `template`
// key. So we fall back on looking for a template matching its name (or
// one of its subclass).
let template: string;
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
p = p.__proto__;
}
if (p === Component) {
throw new Error(`Could not find template for component "${this.constructor.name}"`);
} else {
p._template = template;
}
let template: string;
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
p = p.__proto__;
}
if (p === Component) {
throw new Error(`Could not find template for component "${this.constructor.name}"`);
} else {
p._template = template;
}
}
return p._template;
@@ -565,13 +563,12 @@ export class Component<T extends Env, Props extends {}> {
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
} catch (e) {
fiber.handleError(e);
fiber.vnode = h("div"); // -> we render this div at the end
return Promise.resolve();
}
if (this.__owl__.isDestroyed) {
return Promise.resolve();
}
if (!fiber.isCancelled) {
if (!fiber.isCompleted) {
this.__render(fiber);
}
}
@@ -581,29 +578,45 @@ export class Component<T extends Env, Props extends {}> {
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
let vnode;
let error;
try {
vnode = __owl__.renderFn!(this, {
let vnode = __owl__.renderFn!(this, {
handlers: __owl__.boundHandlers,
fiber: fiber
});
// we iterate over the children to detect those that no longer belong to the
// current rendering: those ones, if not mounted yet, can (and have to) be
// destroyed right now, because they are not in the DOM, and thus we won't
// be notified later on (when patching), that they are removed from the DOM
for (let childKey in __owl__.children) {
let child = __owl__.children[childKey];
if (!child.__owl__.isMounted && child.__owl__.parentLastFiberId < fiber.id) {
child.destroy();
}
}
if (!vnode) {
throw new Error(`Rendering '${this.constructor.name}' did not return anything`);
}
fiber.vnode = vnode;
// 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) {
const data = vnode.data!;
data.class = Object.assign(data.class || {}, __owl__.classObj);
}
} catch (e) {
vnode = __owl__.vnode || h("div");
fiber.handleError(e);
error = e;
}
fiber.vnode = vnode;
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
// 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);
}
fiber.root.counter--;
fiber.isRendered = true;
if (error) {
fiber.handleError(error);
}
}
/**
@@ -640,16 +653,13 @@ export class Component<T extends Env, Props extends {}> {
/**
* Apply default props (only top level).
*
* Note that this method does not modify in place the props, it returns a new
* prop object
* Note that this method does modify in place the props
*/
__applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
props = props ? Object.assign({}, props) : {};
__applyDefaultProps(props: Object, defaultProps: Object) {
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
}
}
return <Props>props;
}
}
+6 -17
View File
@@ -229,19 +229,7 @@ QWeb.addDirective({
let defID = ctx.generateID();
let componentID = ctx.generateID();
let locationExpr = `\`__${ctx.generateID()}__`;
for (let i = 0; i < ctx.loopNumber - 1; i++) {
locationExpr += `\${i${i + 1}}__`;
}
if (ctx.lastNodeKey || ctx.currentKey) {
const k = ctx.lastNodeKey || ctx.currentKey;
ctx.addLine(`let templateId${componentID} = ${locationExpr}\` + ${k};`);
} else {
locationExpr += ctx.loopNumber ? `\${i${ctx.loopNumber}}__\`` : "`";
ctx.addLine(`let templateId${componentID} = ${locationExpr};`);
}
const templateId = `templateId${componentID}`;
const templateKey = ctx.generateTemplateKey();
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey: string = "";
@@ -334,7 +322,7 @@ QWeb.addDirective({
}
ctx.addLine(
`let w${componentID} = ${templateId} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateId}]] : false;`
`let w${componentID} = ${templateKey} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateKey}]] : false;`
);
let shouldProxy = !ctx.parentNode;
if (shouldProxy) {
@@ -421,7 +409,7 @@ QWeb.addDirective({
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
);
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
ctx.addLine(`parent.__owl__.cmap[${templateId}] = w${componentID}.__owl__.id;`);
ctx.addLine(`parent.__owl__.cmap[${templateKey}] = w${componentID}.__owl__.id;`);
if (hasSlots) {
const clone = <Element>node.cloneNode(true);
@@ -451,11 +439,11 @@ QWeb.addDirective({
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
ctx.addLine(
`let pvnode = h('dummy', {key: ${templateId}, hook: {insert(vn) { let nvn=w${componentID}.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});`
`let pvnode = h('dummy', {key: ${templateKey}, hook: {insert(vn) { let nvn=w${componentID}.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});`
);
ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`);
ctx.addLine(
`def${defID}.then(function () {if (w${componentID}.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
`def${defID}.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
if (registerCode) {
ctx.addLine(registerCode);
@@ -471,6 +459,7 @@ QWeb.addDirective({
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
}
ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`);
ctx.addLine(`sibling = w${componentID}.__owl__.currentFiber || sibling;`);
return true;
+80 -35
View File
@@ -1,4 +1,4 @@
import { VNode } from "../vdom/index";
import { h, VNode } from "../vdom/index";
import { Component } from "./component";
import { scheduler } from "./scheduler";
@@ -16,13 +16,17 @@ import { scheduler } from "./scheduler";
*/
export class Fiber {
static nextId: number = 1;
id: number = Fiber.nextId++;
// 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;
// isCompleted means that the rendering corresponding to this fiber's work is
// done, either because the component has been mounted or patched, or because
// fiber has been cancelled.
isCompleted: boolean = false;
// the fibers corresponding to component updates (updateProps) need to call
// the willPatch and patched hooks from the corresponding component. However,
@@ -42,6 +46,8 @@ export class Fiber {
// scheduler.
counter: number = 0;
target: HTMLElement | null;
scope: any;
vars: any;
@@ -55,18 +61,25 @@ export class Fiber {
error?: Error;
constructor(parent: Fiber | null, component: Component<any, any>, scope, vars, force) {
constructor(parent: Fiber | null, component: Component<any, any>, scope, vars, force, target) {
this.force = force;
this.scope = scope;
this.vars = vars;
this.component = component;
this.target = target;
this.root = parent ? parent.root : this;
this.parent = parent;
let oldFiber = component.__owl__.currentFiber;
if (oldFiber && !oldFiber.isCancelled) {
this._remapFiber(oldFiber);
if (oldFiber && !oldFiber.isCompleted) {
if (oldFiber.root === oldFiber && !parent) {
// both oldFiber and this fiber are root fibers
this._reuseFiber(oldFiber);
return oldFiber;
} else {
this._remapFiber(oldFiber);
}
}
this.root.counter++;
@@ -74,6 +87,27 @@ export class Fiber {
component.__owl__.currentFiber = this;
}
/**
* When the oldFiber is not completed yet, and both oldFiber and this fiber
* are root fibers, we want to reuse the oldFiber instead of creating a new
* one. Doing so will guarantee that the initiator(s) of those renderings will
* be notified (the promise will resolve) when the last rendering will be done.
*
* This function thus assumes that oldFiber is a root fiber.
*/
_reuseFiber(oldFiber: Fiber) {
oldFiber.cancel(); // cancel children fibers
oldFiber.isCompleted = false; // keep the root fiber alive
oldFiber.isRendered = false; // the fiber has to be re-rendered
if (oldFiber.child) {
// remove relation to children
oldFiber.child.parent = null;
oldFiber.child = null;
}
oldFiber.counter = 1; // re-initialize counter
oldFiber.id = Fiber.nextId++;
}
/**
* In some cases, a rendering initiated at some component can detect that it
* should be part of a larger rendering initiated somewhere up the component
@@ -83,7 +117,7 @@ export class Fiber {
_remapFiber(oldFiber: Fiber) {
oldFiber.cancel();
if (oldFiber === oldFiber.root) {
oldFiber.root.counter++;
oldFiber.counter++;
}
if (oldFiber.parent && !this.parent) {
// re-map links
@@ -132,7 +166,26 @@ export class Fiber {
}
/**
* Apply the given patch queue from a fiber.
* Successfully complete the work of the fiber: call the mount or patch hooks
* and patch the DOM. This function is called once the fiber and its children
* are ready, and the scheduler decides to process it.
*/
complete() {
const component = this.component;
if (this.target) {
component.__patch(this.vnode);
this.target.appendChild(component.el!);
if (document.body.contains(this.target)) {
component.__callMounted();
}
} else if (component.__owl__.isMounted && this === this.root) {
this.patchComponents();
}
this.isCompleted = true;
}
/**
* Compute and apply the patch queue of the fiber.
* 1) Call 'willPatch' on the component of each patch
* 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in reverse order
@@ -142,38 +195,31 @@ export class Fiber {
const doWork: (Fiber) => Fiber | null = function(f) {
if (f.shouldPatch) {
patchQueue.push(f);
return f.child;
}
return f.child;
};
this._walk(doWork);
let component: Component<any, any> = this.component;
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();
for (let i = 0; i < patchLen; i++) {
component = patchQueue[i].component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
} catch (e) {
console.error(e);
component.willPatch();
}
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode);
component.__owl__.currentFiber = null;
}
try {
for (let i = patchLen - 1; i >= 0; i--) {
component = patchQueue[i].component;
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
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);
}
}
@@ -185,7 +231,7 @@ export class Fiber {
if (!f.isRendered) {
f.root.counter--;
}
f.isCancelled = true;
f.isCompleted = true;
return f.child;
});
}
@@ -199,10 +245,12 @@ export class Fiber {
* being in a corrupted state.
*/
handleError(error: Error) {
let canCatch = false;
let component = this.component;
let qweb = component.env.qweb;
this.vnode = component.__owl__.vnode || h("div");
const qweb = component.env.qweb;
let root = component;
let canCatch = false;
while (component && !(canCatch = !!component.catchError)) {
root = component;
component = component.__owl__.parent!;
@@ -210,10 +258,7 @@ export class Fiber {
qweb.trigger("error", error);
if (canCatch) {
setTimeout(() => {
console.error(error);
component.catchError!(error);
});
component.catchError!(error);
} else {
// the 3 next lines aim to mark the root fiber as being in error, and
// to force it to end, without waiting for its children
+19 -3
View File
@@ -40,7 +40,13 @@ QWeb.utils.validateProps = function(Widget, props: Object) {
break;
}
}
let isValid = isValidProp(props[propName], propsDef[propName]);
let isValid;
try {
isValid = isValidProp(props[propName], propsDef[propName]);
} catch (e) {
e.message = `Invalid prop '${propName}' in component ${Widget.name} (${e.message})`;
throw e;
}
if (!isValid) {
throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`);
}
@@ -80,17 +86,27 @@ function isValidProp(prop, propDef): boolean {
return result;
}
// propsDef is an object
if (propDef.optional && prop === undefined) {
return true;
}
let result = isValidProp(prop, propDef.type);
if (propDef.type === Array) {
if (propDef.type === Array && propDef.element) {
for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element);
}
}
if (propDef.type === Object) {
if (propDef.type === Object && propDef.shape) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
}
if (result) {
for (let propName in prop) {
if (!(propName in shape)) {
throw new Error(`unknown prop '${propName}'`);
}
}
}
}
return result;
}
+28 -8
View File
@@ -25,12 +25,24 @@ export class Scheduler {
this.requestAnimationFrame = requestAnimationFrame;
}
addFiber(fiber, callback) {
this.tasks.push({ fiber, callback });
if (this.isRunning) {
return;
}
this.scheduleTasks();
addFiber(fiber): Promise<void> {
return new Promise((resolve, reject) => {
if (fiber.error) {
return reject(fiber.error);
}
this.tasks.push({
fiber,
callback: () => {
if (fiber.error) {
return reject(fiber.error);
}
resolve();
}
});
if (!this.isRunning) {
this.scheduleTasks();
}
});
}
/**
@@ -41,11 +53,19 @@ export class Scheduler {
let tasks = this.tasks;
this.tasks = [];
tasks = tasks.filter(task => {
if (task.fiber.isCancelled) {
if (task.fiber.isCompleted) {
task.callback();
return false;
}
if (task.fiber.counter === 0) {
task.callback(task.fiber.error);
if (!task.fiber.error) {
try {
task.fiber.complete();
} catch (e) {
task.fiber.handleError(e);
}
}
task.callback();
return false;
}
return true;
+2 -20
View File
@@ -1,5 +1,4 @@
import { QWeb } from "./qweb/index";
import { Env } from "./component/component";
/**
* This file creates and exports the OWL 'config' object, with keys:
@@ -8,7 +7,6 @@ import { Env } from "./component/component";
*/
interface Config {
env: Env;
mode: string;
}
@@ -21,28 +19,12 @@ Object.defineProperty(config, "mode", {
set(mode: string) {
QWeb.dev = mode === "dev";
if (QWeb.dev) {
const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
const url = `https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode`;
console.warn(
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
);
} else {
console.log(`Owl is now running in 'prod' mode.`);
}
},
});
let env:Env;
Object.defineProperty(config, "env", {
get() {
if (!env) {
env = {} as Env;
}
if (!env.qweb) {
env.qweb = new QWeb();
}
return env;
},
set(newEnv: Env) {
env = newEnv;
},
}
});
+17 -12
View File
@@ -2,7 +2,7 @@ import { Component } from "./component/component";
import { scheduler } from "./component/scheduler";
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,
@@ -45,7 +45,16 @@ export class Context extends EventBus {
constructor(state: Object = {}) {
super();
this.observer = new Observer();
this.observer.notifyCB = this.__notifyComponents.bind(this);
this.observer.notifyCB = () => {
// notify components in the next microtask tick to ensure that subscribers
// are notified only once for all changes that occur in the same micro tick
let rev = this.rev;
return Promise.resolve().then(() => {
if (rev === this.rev) {
this.__notifyComponents();
}
});
};
this.state = this.observer.observe(state);
this.subscriptions.update = [];
}
@@ -72,13 +81,7 @@ export class Context extends EventBus {
const subscriptions = this.subscriptions.update;
const groups = partitionBy(subscriptions, s => (s.owner ? s.owner.__owl__.depth : -1));
for (let group of groups) {
const proms = Promise.all(
group.map(sub => {
if (sub.owner ? sub.owner.__owl__.isMounted : true) {
return sub.callback.call(sub.owner, rev);
}
})
);
const proms = group.map(sub => sub.callback.call(sub.owner, rev));
// at this point, each component in the current group has registered a
// top level fiber in the scheduler. It could happen that rendering these
// components is done (if they have no children). This is why we manually
@@ -87,7 +90,7 @@ export class Context extends EventBus {
// promise to resolve earlier, which means that there is a chance of
// processing the next group in the same frame.
scheduler.flush();
await proms;
await Promise.all(proms);
}
}
}
@@ -135,9 +138,11 @@ export function useContextWithCB(ctx: Context, component: Component<any, any>, m
await method();
}
});
onWillUnmount(() => {
const __destroy = component.__destroy;
component.__destroy = (parent) => {
ctx.off("update", component);
delete mapping[id];
});
__destroy.call(component, parent);
}
return ctx.state;
}
+2 -10
View File
@@ -24,14 +24,6 @@ export class Observer {
weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {}
async notifyChange() {
this.dirty = true;
await Promise.resolve();
if (this.dirty) {
this.dirty = false;
this.notifyCB();
}
}
observe<T>(value: T, parent?: any): T {
if (value === null || typeof value !== "object" || value instanceof Date) {
@@ -65,7 +57,7 @@ export class Observer {
}
self._updateRevNumber(target);
target[key] = newVal;
self.notifyChange();
self.notifyCB();
}
return true;
},
@@ -73,7 +65,7 @@ export class Observer {
if (key in target) {
delete target[key];
self._updateRevNumber(target);
self.notifyChange();
self.notifyCB();
}
return true;
}
+15
View File
@@ -0,0 +1,15 @@
import { Component } from "../component/component";
/**
* We define here OwlEvent, a subclass of CustomEvent, with an additional
* attribute:
* - originalComponent: the component that triggered the event
*/
export class OwlEvent<T> extends CustomEvent<T> {
originalComponent: Component<any, any>;
constructor(component, eventType, options) {
super(eventType, options);
this.originalComponent = component;
}
}
+6 -1
View File
@@ -104,7 +104,12 @@ export function useRef(name: string): Ref {
return {
get el(): HTMLElement | null {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof HTMLElement ? val : null;
if (val instanceof HTMLElement) {
return val;
} else if (val instanceof Component) {
return val.el;
}
return null;
},
get comp(): Component<any, any> | null {
const val = __owl__.refs && __owl__.refs[name];
+18 -14
View File
@@ -1,5 +1,4 @@
import { CompilationContext } from "./compilation_context";
import { QWebExprVar } from "./expression_parser";
import { QWeb } from "./qweb";
import { htmlToVDOM } from "../vdom/html_to_vdom";
@@ -34,7 +33,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
return;
}
if (value.xml instanceof NodeList) {
if (value.xml instanceof NodeList && !value.id) {
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
}
@@ -71,6 +70,12 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
qweb._compileChildren(node, ctx);
}
if (value.xml instanceof NodeList && value.id) {
ctx.addElse();
for (let node of Array.from(value.xml)) {
qweb._compileNode(<ChildNode>node, ctx);
}
}
ctx.closeIf();
}
@@ -104,22 +109,21 @@ QWeb.addDirective({
atNodeEncounter({ node, ctx }): boolean {
const variable = node.getAttribute("t-set")!;
let value = node.getAttribute("t-value")!;
ctx.variables[variable] = ctx.variables[variable] || {};
let qwebvar = ctx.variables[variable];
if (value) {
const formattedValue = ctx.formatExpression(value);
if (ctx.variables.hasOwnProperty(variable)) {
ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`);
if (ctx.variables.hasOwnProperty(variable) && qwebvar.id) {
ctx.addLine(`${qwebvar.id} = ${formattedValue}`);
} else {
const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`);
ctx.variables[variable] = {
id: varName,
expr: formattedValue
};
qwebvar.id = varName;
qwebvar.expr = formattedValue;
}
} else {
ctx.variables[variable] = {
xml: node.childNodes
};
qwebvar.xml = node.childNodes;
}
return true;
}
@@ -133,7 +137,7 @@ QWeb.addDirective({
priority: 20,
atNodeEncounter({ node, ctx }): boolean {
let cond = ctx.getValue(node.getAttribute("t-if")!);
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : cond.id);
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : cond.id!);
return false;
},
finalize({ ctx }) {
@@ -244,8 +248,8 @@ QWeb.addDirective({
// add new variables, if any
for (let key in tempCtx.variables) {
const v = tempCtx.variables[key];
if ((<QWebExprVar>v).expr) {
ctx.addLine(`let ${(<QWebExprVar>v).id} = ${(<QWebExprVar>v).expr};`);
if (v.expr) {
ctx.addLine(`let ${v.id} = ${v.expr};`);
}
// todo: handle XML variables...
}
+26 -2
View File
@@ -1,4 +1,4 @@
import { compileExpr, QWebVar, QWebExprVar } from "./expression_parser";
import { compileExpr, QWebVar } from "./expression_parser";
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------
@@ -47,6 +47,30 @@ export class CompilationContext {
return id;
}
/**
* This method generates a "template key", which is basically a unique key
* which depends on the currently set keys, and on the iteration numbers (if
* we are in a loop).
*
* Such a key is necessary when we need to associate an id to some element
* generated by a template (for example, a component)
*/
generateTemplateKey(): string {
const id = this.generateID();
let locationExpr = `\`__${this.generateID()}__`;
for (let i = 0; i < this.loopNumber - 1; i++) {
locationExpr += `\${i${i + 1}}__`;
}
if (this.lastNodeKey || this.currentKey) {
const k = this.lastNodeKey || this.currentKey;
this.addLine(`let k${id} = ${locationExpr}\` + ${k};`);
} else {
locationExpr += this.loopNumber ? `\${i${this.loopNumber}}__\`` : "`";
this.addLine(`let k${id} = ${locationExpr};`);
}
return `k${id}`;
}
generateCode(): string[] {
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
if (shouldTrackScope) {
@@ -149,7 +173,7 @@ export class CompilationContext {
this.addLine("}");
}
getValue(val: any): QWebExprVar | string {
getValue(val: any): QWebVar | string {
return val in this.variables ? this.getValue(this.variables[val]) : val;
}
+11 -14
View File
@@ -25,7 +25,7 @@
// Misc types, constants and helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
@@ -38,17 +38,12 @@ const WORD_REPLACEMENT = {
lte: "<="
};
export interface QWebExprVar {
id: string;
expr: string;
export interface QWebVar {
id?: string;
expr?: string;
xml?: NodeList;
}
export interface QWebXMLVar {
xml: NodeList;
}
export type QWebVar = QWebExprVar | QWebXMLVar;
//------------------------------------------------------------------------------
// Tokenizer
//------------------------------------------------------------------------------
@@ -82,7 +77,9 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
")": "RIGHT_PAREN"
};
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(",");
// note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ".split(",");
type Tokenizer = (expr: string) => Token | false;
@@ -165,9 +162,9 @@ const tokenizeOperator: Tokenizer = function(expr) {
const TOKENIZERS = [
tokenizeString,
tokenizeNumber,
tokenizeOperator,
tokenizeSymbol,
tokenizeStatic,
tokenizeOperator
tokenizeStatic
];
/**
@@ -252,7 +249,7 @@ export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): str
}
if (isVar) {
if (token.value in vars && "id" in vars[token.value]) {
token.value = (<QWebExprVar>vars[token.value]).id;
token.value = vars[token.value].id!;
} else {
token.value = `context['${token.value}']`;
}
+13 -5
View File
@@ -231,7 +231,17 @@ QWeb.addDirective({
const type = node.getAttribute("type");
let handler;
let event = fullName.includes(".lazy") ? "change" : "input";
const expr = ctx.formatExpression(value);
// we keep here a reference to the "base expression" (if the expression
// is `t-model="some.expr.value", then the base expression is "some.expr").
// This is necessary so we can capture it in the handler closure.
let expr = ctx.formatExpression(value);
const index = expr.lastIndexOf(".");
const baseExpr = expr.slice(0, index);
ctx.addLine(`let expr${nodeID} = ${baseExpr};`);
expr = `expr${nodeID}.${expr.slice(index + 1)}`;
const key = ctx.generateTemplateKey();
if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
addNodeHook("create", `n.elm.value=${expr};`);
@@ -255,10 +265,8 @@ QWeb.addDirective({
}
handler = `(ev) => {${expr} = ${valueCode}}`;
}
ctx.addLine(
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`);
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`);
}
});
+2
View File
@@ -460,6 +460,8 @@ export class QWeb extends EventBus {
// this is a component, we modify in place the xml document to change
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
node.setAttribute("t-component", node.tagName);
} else if (node.tagName !== 't' && node.hasAttribute('t-component')) {
throw new Error(`Directive 't-component' can only be used on <t> nodes (used on a <${node.tagName}>)`);
}
const attributes = (<Element>node).attributes;
+6 -3
View File
@@ -84,6 +84,9 @@ const isStrictEqual = (a, b) => a === b;
export function useStore(selector, options: SelectorOptions = {}): any {
const component: Component<any, any> = Component.current!;
const store = options.store || (component.env.store as Store);
if (!(store instanceof Store)) {
throw new Error(`No store found when connecting '${component.constructor.name}'`);
}
let result = selector(store.state, component.props);
const hashFn = store.observer.revNumber.bind(store.observer);
let revNumber = hashFn(result) || result;
@@ -108,9 +111,9 @@ export function useStore(selector, options: SelectorOptions = {}): any {
useContextWithCB(store, component, function(): Promise<void> | void {
let shouldRender = false;
updateFunctions.forEach(function(updateFn) {
shouldRender = updateFn() || shouldRender;
});
for (let fn of updateFunctions) {
shouldRender = fn() || shouldRender;
}
if (shouldRender) {
return component.render();
}
+2 -4
View File
@@ -20,10 +20,8 @@ function htmlToVNode(node: ChildNode): VNode {
attrs[attr.name] = attr.textContent;
}
const children: VNode[] = [];
if (node.hasChildNodes) {
for (let c of node.childNodes) {
children.push(htmlToVNode(c));
}
for (let c of node.childNodes) {
children.push(htmlToVNode(c));
}
return h((node as Element).tagName, { attrs }, children);
}
+12 -10
View File
@@ -12,8 +12,8 @@ exports[`animations t-transition combined with component 1`] = `
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let templateId3 = \`__4__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let k4 = \`__5__\`;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
@@ -28,17 +28,18 @@ exports[`animations t-transition combined with component 1`] = `
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
let pvnode = h('dummy', {key: k4, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w3.destroy();
};
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; });
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
w3.__owl__.parentLastFiberId = extra.fiber.id;
sibling = w3.__owl__.currentFiber || sibling;
return vn1;
}"
@@ -57,8 +58,8 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
var vn1 = h('div', p1, c1);
if (context['state'].display) {
//COMPONENT
let templateId3 = \`__4__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let k4 = \`__5__\`;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props3 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
@@ -73,17 +74,18 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
let pvnode = h('dummy', {key: k4, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w3.destroy();
};
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; });
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
w3.__owl__.parentLastFiberId = extra.fiber.id;
sibling = w3.__owl__.currentFiber || sibling;
}
return vn1;
+3 -4
View File
@@ -1,5 +1,4 @@
import { Component, Env } from "../src/component/component";
import { config } from "../src/config";
import { QWeb } from "../src/qweb/index";
import { useState, useRef } from "../src/hooks";
import {
@@ -30,7 +29,7 @@ let cssEl: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
config.env = env;
Component.env = env;
qweb = new QWeb();
});
@@ -253,11 +252,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="__5__">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="__5__">blue</span></div>'
);
def.resolve();
});
File diff suppressed because it is too large Load Diff
@@ -12,8 +12,8 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let templateId3 = \`__4__\`;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let k4 = \`__5__\`;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let props3 = {message:1};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
@@ -28,14 +28,15 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId3] = w3.__owl__.id;
parent.__owl__.cmap[k4] = w3.__owl__.id;
let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling);
let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});
let pvnode = h('dummy', {key: k4, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
}
w3.__owl__.parentLastFiberId = extra.fiber.id;
sibling = w3.__owl__.currentFiber || sibling;
return vn1;
}"
+508 -114
View File
@@ -3,7 +3,6 @@ import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks";
import { EventBus } from "../../src/core/event_bus";
import { config } from "../../src/config";
import {
makeDeferred,
makeTestFixture,
@@ -36,7 +35,7 @@ beforeEach(() => {
);
env.qweb.addTemplate("WidgetA", `<div>Hello<t t-component="b"/></div>`);
env.qweb.addTemplate("WidgetB", `<div>world</div>`);
config.env = env;
Component.env = env;
});
afterEach(() => {
@@ -72,9 +71,9 @@ class WidgetA extends Widget {
//------------------------------------------------------------------------------
describe("basic widget properties", () => {
test("props is not defined on root components", async () => {
const widget = new Widget();
expect(widget.props).toBe(undefined);
test("props is set on root components", async () => {
const widget = new Widget(null, {});
expect(widget.props).toEqual({});
});
test("has no el after creation", async () => {
@@ -92,6 +91,38 @@ describe("basic widget properties", () => {
expect(fixture.innerHTML).toBe("<div>content</div>");
});
test("display a nice message if mounted on a non existing node", async () => {
class SomeWidget extends Component<any, any> {
static template = xml`<div>content</div>`;
}
const widget = new SomeWidget();
let error;
try {
await widget.mount(null as any);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(
"Component 'SomeWidget' cannot be mounted: the target is not a valid DOM node.\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)"
);
});
test("display an error message if result of rendering is empty", async () => {
class SomeWidget extends Component<any, any> {
static template = xml`<t/>`;
}
const widget = new SomeWidget();
let error;
try {
await widget.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Rendering 'SomeWidget' did not return anything");
});
test("crashes if it cannot find a template", async () => {
expect.assertions(1);
class SomeWidget extends Component<any, any> {}
@@ -944,6 +975,34 @@ describe("destroy method", () => {
await nextTick();
expect(fixture.innerHTML).toBe("<div><span><span>12</span><button>click</button></span></div>");
});
test("destroying a widget before being mounted (2)", async () => {
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`<span></span>`;
willStart() {
steps.push("willStart");
return makeDeferred();
}
__destroy(parent) {
steps.push("__destroy");
super.__destroy(parent);
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: true });
}
const parent = new Parent();
const prom = parent.mount(fixture);
await nextTick(); // wait for Child to be instantiated
parent.state.flag = false;
await prom;
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["willStart", "__destroy"]);
});
});
describe("composition", () => {
@@ -1062,94 +1121,6 @@ describe("composition", () => {
console.error = consoleError;
});
test("t-refs on widget are components", async () => {
class WidgetC extends Widget {
static template = xml`<div>Hello<WidgetB t-ref="mywidgetb" /></div>`;
static components = { WidgetB };
widget = useRef("mywidgetb");
}
const widget = new WidgetC();
await widget.mount(fixture);
expect(widget.widget.comp).toBeInstanceOf(WidgetB);
});
test("t-refs are bound at proper timing", async () => {
expect.assertions(2);
class ParentWidget extends Widget {
static template = xml`
<div>
<t t-foreach="state.list" t-as="elem" t-ref="child" t-key="elem" t-component="Widget"/>
</div>
`;
static components = { Widget };
state = useState({ list: <any>[] });
child = useRef("child");
willPatch() {
expect(this.child.comp).toBeNull();
}
patched() {
expect(this.child.comp).not.toBeNull();
}
}
const parent = new ParentWidget();
await parent.mount(fixture);
parent.state.list.push(1);
await nextTick();
});
test("t-refs are bound at proper timing (2)", async () => {
expect.assertions(10);
env.qweb.addTemplate(
"ParentWidget",
`
<div>
<t t-if="state.child1" t-ref="child1" t-component="Widget"/>
<t t-if="state.child2" t-ref="child2" t-component="Widget"/>
</div>`
);
class ParentWidget extends Widget {
static components = { Widget };
state = useState({ child1: true, child2: false });
child1 = useRef("child1");
child2 = useRef("child2");
count = 0;
mounted() {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeNull();
}
willPatch() {
if (this.count === 0) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeNull();
}
if (this.count === 1) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeDefined();
}
}
patched() {
if (this.count === 0) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeDefined();
}
if (this.count === 1) {
expect(this.child1.comp).toBeNull();
expect(this.child2.comp).toBeDefined();
}
this.count++;
}
}
const parent = new ParentWidget();
await parent.mount(fixture);
parent.state.child2 = true;
await nextTick();
parent.state.child1 = false;
await nextTick();
});
test("modifying a sub widget", async () => {
env.qweb.addTemplate("ParentWidget", `<div><t t-component="Counter"/></div>`);
class ParentWidget extends Widget {
@@ -1443,6 +1414,25 @@ describe("composition", () => {
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-component not on a <t> node", async () => {
class Child extends Component<any, any> {
static template = xml`<span>1</span>`;
}
class Parent extends Component<any, any> {
static components = { Child };
static template = xml`<div><div t-component="Child"/></div>`;
}
const parent = new Parent();
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Directive 't-component' can only be used on <t> nodes (used on a <div>)`);
});
test("sub components, loops, and shouldUpdate", async () => {
class ChildWidget extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
@@ -1747,6 +1737,27 @@ describe("class and style attributes with t-component", () => {
expect(fixture.innerHTML).toBe(`<div><div style="font-size: 30px;"></div></div>`);
});
test("error in subcomponent with class", async () => {
class Child extends Widget {
static template = xml`<div t-esc="this.will.crash"/>`;
}
class ParentWidget extends Widget {
static template = xml`<div><Child class="a"/></div>`;
static components = { Child };
}
const widget = new ParentWidget();
let error;
try {
await widget.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(fixture.innerHTML).toBe("");
});
});
describe("other directives with t-component", () => {
@@ -2683,12 +2694,19 @@ describe("async rendering", () => {
await nextMicroTick();
expect(steps).toEqual(["render"]);
await nextMicroTick();
// For an unknown reason, this test fails on windows without the next microtick. It works
// in linux and osx, but fails on at least this machine.
// I do not see anything harmful in waiting an extra tick. But it is annoying to not
// know what is different.
await nextMicroTick();
expect(steps).toEqual(["render", "render"]);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
parent.state.valA = 3;
await nextMicroTick();
expect(steps).toEqual(["render", "render"]);
await nextMicroTick();
// same as above
await nextMicroTick();
expect(steps).toEqual(["render", "render", "render"]);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
await nextTick();
@@ -3366,6 +3384,166 @@ describe("async rendering", () => {
await nextTick();
expect(fixture.innerHTML).toBe("<div>a2<b>a2</b><p><span>a2b2</span></p></div>");
});
test("concurrent renderings scenario 10", async () => {
// Here is the global idea of this scenario:
// A
// |
// B <- async willUpdateProps
// ----- <- conditional (initialy false)
// |
// C <- async willStart
// Render A and B normally
// Change the condition on B to trigger a re-rendering with C (async willStart)
// Change the state on A to trigger a global re-rendering, which is blocked
// in B (async willUpdateProps)
// Resolve the willStart of C: the first re-rendering has been cancelled by
// the global re-rendering, but handlers waiting for the rendering promise to
// resolve might execute and we don't want them to crash/do anything
const defB = makeDeferred();
const defC = makeDeferred();
let stateB;
class ComponentC extends Component<any, any> {
static template = xml`<span><t t-esc="props.value"/></span>`;
willStart() {
return defC;
}
}
ComponentC.prototype.__render = jest.fn(ComponentC.prototype.__render);
class ComponentB extends Component<any, any> {
static template = xml`<p><ComponentC t-if="state.hasChild" value="props.value"/></p>`;
state = useState({ hasChild: false });
static components = { ComponentC };
constructor(parent, props) {
super(parent, props);
stateB = this.state;
}
willUpdateProps() {
return defB;
}
}
class ComponentA extends Component<any, any> {
static template = xml`<div><ComponentB value="state.value"/></div>`;
static components = { ComponentB };
state = useState({ value: 1 });
}
const componentA = new ComponentA();
await componentA.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p></p></div>");
stateB.hasChild = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p></p></div>");
componentA.state.value = 2;
defC.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p></p></div>");
defB.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span>2</span></p></div>");
expect(ComponentC.prototype.__render).toHaveBeenCalledTimes(1);
});
test("concurrent renderings scenario 11", async () => {
// This scenario is the following: we have a component being updated (by props),
// and then rendered (render method), but before the willUpdateProps resolves.
// We check that in that case, the return value of the render method is a promise
// that is resolved when the component is completely rendered (so, properly
// remapped to the promise of the ambient rendering)
const def = makeDeferred();
let child;
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/>|<t t-esc="val"/></span>`;
val = 3;
willUpdateProps() {
child = this;
return def;
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child val="state.valA"/></div>`;
static components = { Child };
state = useState({ valA: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>1|3</span></div>");
parent.state.valA = 2;
await nextTick();
setTimeout(() => {
def.resolve();
}, 20);
child.val = 5;
await child.render();
expect(fixture.innerHTML).toBe("<div><span>2|5</span></div>");
});
test("concurrent renderings scenario 12", async () => {
// In this scenario, we have a parent component that will be re-rendered
// several times simultaneously:
// - once in a tick: it will create a new fiber, render it, but will have
// to wait for its child (blocking) to be completed
// - twice in the next tick: it will twice reuse the same fiber (as it is
// rendered but not completed yet)
const def = makeDeferred();
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
willUpdateProps() {
return def;
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child val="state.val"/></div>`;
static components = { Child };
state = useState({ val: 1 });
}
Parent.prototype.__render = jest.fn(Parent.prototype.__render);
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
expect(Parent.prototype.__render).toHaveBeenCalledTimes(1);
parent.state.val = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
expect(Parent.prototype.__render).toHaveBeenCalledTimes(2);
parent.state.val = 3;
parent.state.val = 4;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
expect(Parent.prototype.__render).toHaveBeenCalledTimes(3);
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>4</span></div>");
expect(Parent.prototype.__render).toHaveBeenCalledTimes(3);
});
test("change state and call manually render: no unnecessary rendering", async () => {
class Widget extends Component<any, any> {
static template = xml`<div><t t-esc="state.val"/></div>`;
state = useState({ val: 1 });
}
Widget.prototype.__render = jest.fn(Widget.prototype.__render);
const widget = new Widget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>1</div>");
expect(Widget.prototype.__render).toHaveBeenCalledTimes(1);
widget.state.val = 2;
await widget.render();
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(Widget.prototype.__render).toHaveBeenCalledTimes(2);
});
});
describe("widget and observable state", () => {
@@ -3450,7 +3628,7 @@ describe("can deduce template from name", () => {
await abc.mount(fixture);
expect(fixture.innerHTML).toBe("<span>Rochefort 8</span>");
abc.destroy();
config.env = env2;
Component.env = env2;
const abc2 = new ABC();
await abc2.mount(fixture);
expect(fixture.innerHTML).toBe("<span>Rochefort 10</span>");
@@ -4206,6 +4384,31 @@ describe("t-model directive", () => {
expect(comp.state.number).toBe("invalid");
expect(fixture.innerHTML).toBe("<div><input><span>invalid</span></div>");
});
test("in a t-foreach", async () => {
class SomeComponent extends Component<any, any> {
static template = xml`
<div>
<t t-foreach="state" t-as="thing" t-key="thing.id">
<input type="checkbox" t-model="thing.f"/>
</t>
</div>
`;
state = useState([{ f: false, id: 1 }, { f: false, id: 2 }, { f: false, id: 3 }]);
}
const comp = new SomeComponent();
await comp.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><input type="checkbox"><input type="checkbox"><input type="checkbox"></div>'
);
const input = fixture.querySelectorAll("input")[1]!;
input.click();
expect(comp.state[1].f).toBe(true);
expect(comp.state[0].f).toBe(false);
expect(comp.state[2].f).toBe(false);
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
});
});
describe("environment and plugins", () => {
@@ -4240,6 +4443,31 @@ describe("environment and plugins", () => {
await nextTick();
expect(fixture.innerHTML).toBe("<div>Blue</div>");
});
test("can define specific env for root components", async () => {
class App1 extends Component<any, any> {
static template = xml`<span></span>`;
}
App1.env = { test: 1 };
class App2 extends Component<any, any> {
static template = xml`<span></span>`;
}
App2.env = { test: 2 };
class App1B extends App1 {}
class App2B extends App2 {}
App2B.env = { test: 3 };
const app1 = new App1();
const app2 = new App2();
const app1B = new App1B();
const app2B = new App2B();
expect(app1.env.qweb).toBeDefined();
expect(app1.env.test).toBe(1);
expect(app2.env.test).toBe(2);
expect(app1B.env.test).toBe(1);
expect(app2B.env.test).toBe(3);
});
});
describe("component error handling (catchError)", () => {
@@ -4286,11 +4514,9 @@ describe("component error handling (catchError)", () => {
expect(fixture.innerHTML).toBe("<div><div><div>hey</div></div></div>");
app.state.flag = true;
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(1);
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
@@ -4323,8 +4549,6 @@ describe("component error handling (catchError)", () => {
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
app.state.flag = true;
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
@@ -4362,12 +4586,9 @@ describe("component error handling (catchError)", () => {
}
const app = new App();
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(1);
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
@@ -4406,12 +4627,9 @@ describe("component error handling (catchError)", () => {
}
const app = new App();
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(1);
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
@@ -4445,12 +4663,9 @@ describe("component error handling (catchError)", () => {
}
const app = new App();
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(1);
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
@@ -4556,6 +4771,90 @@ describe("component error handling (catchError)", () => {
console.error = consoleError;
});
test("an error in mounted call will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class App extends Component<any, any> {
static template = xml`<div>abc</div>`;
mounted() {
throw new Error("boom");
}
}
const app = new App();
let error;
try {
await app.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("an error in willPatch call will reject the render promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class App extends Component<any, any> {
static template = xml`<div><t t-esc="val"/></div>`;
val = 3;
willPatch() {
throw new Error("boom");
}
}
const app = new App();
await app.mount(fixture);
app.val = 4;
let error;
try {
await app.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("an error in patched call will reject the render promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class App extends Component<any, any> {
static template = xml`<div><t t-esc="val"/></div>`;
val = 3;
patched() {
throw new Error("boom");
}
}
const app = new App();
await app.mount(fixture);
app.val = 4;
let error;
try {
await app.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("a rendering error in a sub component will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
@@ -4863,11 +5162,106 @@ describe("unmounting and remounting", () => {
expect(fixture.innerHTML).toBe("<div>1</div>");
widget.unmount();
expect(fixture.innerHTML).toBe("");
await nextTick(); // wait for changes to be detected before remounting
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>1</div>");
widget.unmount();
await widget.mount(fixture, true);
expect(fixture.innerHTML).toBe("<div>3</div>");
// we want to make sure that there are no remaining tasks left at this point.
expect(Component.scheduler.tasks.length).toBe(0);
});
test("sub component is still active after being unmounted and remounted", async () => {
class Child extends Component<any, any> {
static template = xml`
<p t-on-click="state.value++">
<t t-esc="state.value"/>
</p>`;
state = useState({ value: 1 });
}
class Parent extends Component<any, any> {
static components = { Child };
static template = xml`<div><Child/></div>`;
}
const w = new Parent();
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
fixture.querySelector("p")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>2</p></div>");
w.unmount();
await nextTick();
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>2</p></div>");
fixture.querySelector("p")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>3</p></div>");
});
test("change state just before mounting component", async () => {
const steps: number[] = [];
class TestWidget extends Component<any, any> {
static template = xml`
<div><t t-esc="state.val"/></div>
`;
state = useState({ val: 1 });
__render(f) {
steps.push(this.state.val);
return super.__render(f);
}
}
TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render);
const widget = new TestWidget();
widget.state.val = 2;
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1);
// unmount and re-mount, as in this case, willStart won't be called, so it's
// slightly different
widget.unmount();
widget.state.val = 3;
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(2);
expect(steps).toEqual([2, 3]);
});
test("change state while mounting component", async () => {
const steps: number[] = [];
class TestWidget extends Component<any, any> {
static template = xml`
<div><t t-esc="state.val"/></div>
`;
state = useState({ val: 1 });
__render(f) {
steps.push(this.state.val);
return super.__render(f);
}
}
TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render);
TestWidget.prototype.__patch = jest.fn(TestWidget.prototype.__patch);
const widget = new TestWidget();
let prom = widget.mount(fixture);
widget.state.val = 2;
await prom;
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1);
// unmount and re-mount, as in this case, willStart won't be called, so it's
// slightly different
widget.unmount();
prom = widget.mount(fixture);
widget.state.val = 3;
await prom;
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(3);
expect(TestWidget.prototype.__patch).toHaveBeenCalledTimes(2);
expect(steps).toEqual([2, 2, 3]);
});
});
+77 -18
View File
@@ -1,7 +1,6 @@
import { Component, Env } from "../../src/component/component";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
import { useState } from "../../src/hooks";
import { config } from "../../src/config";
import { QWeb } from "../../src/qweb";
import { xml } from "../../src/tags";
@@ -16,7 +15,7 @@ let dev: boolean = false;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
config.env = env;
Component.env = env;
dev = QWeb.dev;
QWeb.dev = true;
});
@@ -122,7 +121,7 @@ describe("props validation", () => {
expect(error.message).toBe(`Missing props 'p' (component '_a')`);
error = undefined;
props = {p: test.ok};
props = { p: test.ok };
try {
const p = new Parent();
await p.mount(fixture);
@@ -131,7 +130,7 @@ describe("props validation", () => {
}
expect(error).toBeUndefined();
props = {p: test.ko};
props = { p: test.ko };
try {
const p = new Parent();
await p.mount(fixture);
@@ -179,7 +178,7 @@ describe("props validation", () => {
expect(error.message).toBe(`Missing props 'p' (component '_a')`);
error = undefined;
props = {p: test.ok};
props = { p: test.ok };
try {
const p = new Parent();
await p.mount(fixture);
@@ -188,7 +187,7 @@ describe("props validation", () => {
}
expect(error).toBeUndefined();
props = {p: test.ko};
props = { p: test.ko };
try {
const p = new Parent();
await p.mount(fixture);
@@ -204,14 +203,14 @@ describe("props validation", () => {
class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`;
static props = { p: [String, Boolean] };
};
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
};
}
let error;
let props;
@@ -241,21 +240,21 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`);
expect(error.message).toBe("Props 'p' of invalid type in component 'TestWidget'");
});
test("can validate an optional props", async () => {
class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`;
static props = { p: { type: String, optional: true } };
};
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
};
}
let error;
let props;
@@ -292,14 +291,14 @@ describe("props validation", () => {
class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`;
static props = { p: { type: Array, element: String } };
};
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
return props.p;
}
};
}
let error;
let props;
@@ -344,7 +343,7 @@ describe("props validation", () => {
class TestWidget extends Component<any, any> {
static template = xml`<div>hey</div>`;
static props = { p: { type: Array, element: [String, Boolean] } };
};
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
@@ -399,7 +398,7 @@ describe("props validation", () => {
static props = {
p: { type: Object, shape: { id: Number, url: String } }
};
};
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
@@ -426,7 +425,8 @@ describe("props validation", () => {
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
expect(error).toBeDefined();
expect(error.message).toBe("Invalid prop 'p' in component TestWidget (unknown prop 'extra')");
try {
props = { p: { id: "1", url: "url" } };
@@ -462,7 +462,7 @@ describe("props validation", () => {
}
}
};
};
}
class Parent extends Component<any, any> {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
@@ -502,6 +502,39 @@ describe("props validation", () => {
expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`);
});
test("can validate optional attributes in nested sub props", () => {
class TestComponent extends Component<any, any> {
static props = {
myprop: {
type: Array,
element: {
type: Object,
shape: {
num: { type: Number, optional: true }
}
}
}
};
}
let error;
try {
QWeb.utils.validateProps(TestComponent, { myprop: [{}] });
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
try {
QWeb.utils.validateProps(TestComponent, { myprop: [{ a: 1 }] });
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(
"Invalid prop 'myprop' in component TestComponent (unknown prop 'a')"
);
});
test("props are validated in dev mode (code snapshot)", async () => {
env.qweb.addTemplates(`
<templates>
@@ -548,6 +581,32 @@ describe("props validation", () => {
}).toThrow();
});
test("props with type array, and no element", async () => {
class TestWidget extends Widget {
static props = { myprop: { type: Array } };
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { myprop: [1] });
}).not.toThrow();
expect(() => {
QWeb.utils.validateProps(TestWidget, { myprop: 1 });
}).toThrow(`Props 'myprop' of invalid type in component 'TestWidget'`);
});
test("props with type object, and no shape", async () => {
class TestWidget extends Widget {
static props = { myprop: { type: Object } };
}
expect(() => {
QWeb.utils.validateProps(TestWidget, { myprop: { a: 3 } });
}).not.toThrow();
expect(() => {
QWeb.utils.validateProps(TestWidget, { myprop: false });
}).toThrow(`Props 'myprop' of invalid type in component 'TestWidget'`);
});
test("props: extra props cause an error", async () => {
class TestWidget extends Widget {
static props = ["message"];
@@ -678,7 +737,7 @@ describe("default props", () => {
const w = new Parent();
await w.mount(fixture);
expect(fixture.innerHTML).toBe('<div><div>4</div></div>');
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
});
test("default values are also set whenever component is updated", async () => {
+36 -3
View File
@@ -1,7 +1,6 @@
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick } from "./helpers";
import { Component } from "../src/component/component";
import { Context, useContext } from "../src/context";
import { config } from "../src/config";
import { xml } from "../src/tags";
import { useState } from "../src/hooks";
@@ -18,7 +17,7 @@ let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
config.env = makeTestEnv();
Component.env = makeTestEnv();
});
afterEach(() => {
@@ -107,7 +106,7 @@ describe("Context", () => {
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
});
test("two async components on two levels are updated in parallel", async () => {
test("two async components on two levels are updated (mostly) in parallel", async () => {
const testContext = new Context({ value: 123 });
const def = makeDeferred();
const steps: string[] = [];
@@ -152,6 +151,11 @@ describe("Context", () => {
);
def.resolve();
await nextTick();
// we need to wait for an extra tick because it could happen (even though it
// is rare) that the second batch of renderings is not done yet, because
// the initial promise has been given to the macrotask queue, so a small
// delay happens.
await nextTick();
expect(steps).toEqual(["render", "render", "render"]);
expect(fixture.innerHTML).toBe(
"<div><span><p>321</p></span><div><span><p>321</p></span><span><p>321</p></span></div></div>"
@@ -256,6 +260,35 @@ describe("Context", () => {
expect(testContext.subscriptions.update.length).toBe(0);
});
test("destroyed component before being mounted is inactive", async () => {
const testContext = new Context({ a: 123 });
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
contextObj = useContext(testContext);
willStart() {
return makeDeferred();
}
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: true });
}
const parent = new Parent();
const prom = parent.mount(fixture);
await nextTick(); // wait for Child to be instantiated
expect(testContext.subscriptions.update.length).toBe(1);
parent.state.flag = false;
await prom;
expect(fixture.innerHTML).toBe("<div></div>");
// kind of whitebox...
// we make sure we do not have any pending subscriptions to the 'update'
// event
expect(testContext.subscriptions.update.length).toBe(0);
});
test("concurrent renderings", async () => {
const testContext = new Context({ x: { n: 1 }, key: "x" });
const def = makeDeferred();
+1 -1
View File
@@ -408,7 +408,7 @@ describe("observer", () => {
obj.a = 111;
obj.f = 222;
await nextMicroTick();
expect(observer.notifyCB).toBeCalledTimes(4);
expect(observer.notifyCB).toBeCalledTimes(5);
});
test("throw error when state is mutated in object if allowMutation=false", async () => {
+102 -2
View File
@@ -1,6 +1,5 @@
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
import { Component, Env } from "../src/component/component";
import { config } from "../src/config";
import {
useState,
onMounted,
@@ -29,7 +28,7 @@ let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
config.env = env;
Component.env = env;
});
afterEach(() => {
@@ -229,8 +228,11 @@ describe("hooks", () => {
}
}
const counter = new Counter();
expect(counter.button.el).toBe(null);
await counter.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>0</button></div>");
expect(counter.button.el).not.toBe(null);
expect(counter.button.el).toBe(fixture.querySelector("button"));
counter.increment();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
@@ -257,6 +259,104 @@ describe("hooks", () => {
expect(fixture.innerHTML).toBe("<div></div>");
});
test("t-refs on widget are components", async () => {
class WidgetB extends Component<any, any> {
static template = xml`<div>b</div>`;
}
class WidgetC extends Component<any, any> {
static template = xml`<div class="outer-div">Hello<WidgetB t-ref="mywidgetb" /></div>`;
static components = { WidgetB };
ref = useRef("mywidgetb");
}
const widget = new WidgetC();
expect(widget.ref.comp).toBe(null);
expect(widget.ref.el).toBe(null);
await widget.mount(fixture);
expect(widget.ref.comp).toBeInstanceOf(WidgetB);
expect(widget.ref.el).toEqual(fixture.querySelector(".outer-div > div"));
});
test("t-refs are bound at proper timing", async () => {
expect.assertions(2);
class Widget extends Component<any, any> {
static template = xml`<div>widget</div>`;
}
class ParentWidget extends Component<any, any> {
static template = xml`
<div>
<t t-foreach="state.list" t-as="elem" t-ref="child" t-key="elem" t-component="Widget"/>
</div>
`;
static components = { Widget };
state = useState({ list: <any>[] });
child = useRef("child");
willPatch() {
expect(this.child.comp).toBeNull();
}
patched() {
expect(this.child.comp).not.toBeNull();
}
}
const parent = new ParentWidget();
await parent.mount(fixture);
parent.state.list.push(1);
await nextTick();
});
test("t-refs are bound at proper timing (2)", async () => {
expect.assertions(10);
class Widget extends Component<any, any> {
static template = xml`<div>widget</div>`;
}
class ParentWidget extends Component<any, any> {
static template = xml`
<div>
<t t-if="state.child1" t-ref="child1" t-component="Widget"/>
<t t-if="state.child2" t-ref="child2" t-component="Widget"/>
</div>`;
static components = { Widget };
state = useState({ child1: true, child2: false });
child1 = useRef("child1");
child2 = useRef("child2");
count = 0;
mounted() {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeNull();
}
willPatch() {
if (this.count === 0) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeNull();
}
if (this.count === 1) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeDefined();
}
}
patched() {
if (this.count === 0) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeDefined();
}
if (this.count === 1) {
expect(this.child1.comp).toBeNull();
expect(this.child2.comp).toBeDefined();
}
this.count++;
}
}
const parent = new ParentWidget();
await parent.mount(fixture);
parent.state.child2 = true;
await nextTick();
parent.state.child1 = false;
await nextTick();
});
test("can use onPatched, onWillPatch", async () => {
const steps: string[] = [];
function useMyHook() {
+1 -2
View File
@@ -1,5 +1,4 @@
import { AsyncRoot } from "../../src/misc/async_root";
import { config } from "../../src/config";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeDeferred, makeTestFixture, makeTestEnv, nextTick } from "../helpers";
@@ -18,7 +17,7 @@ let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
config.env = makeTestEnv();
Component.env = makeTestEnv();
});
afterEach(() => {
+98 -14
View File
@@ -699,25 +699,25 @@ exports[`misc global 1`] = `
} else {
c17.push({text: \`foo default\`});
}
var _19 = 'bbb';
let c20 = [], p20 = {key:20};
var vn20 = h('span', p20, c20);
c14.push(vn20);
if (_19 || _19 === 0) {
c20.push({text: _19});
_11 = 'bbb'
let c19 = [], p19 = {key:19};
var vn19 = h('span', p19, c19);
c14.push(vn19);
if (_11 || _11 === 0) {
c19.push({text: _11});
} else {
c20.push({text: \`foo default\`});
c19.push({text: \`foo default\`});
}
}
}
let c21 = [], p21 = {key:21};
var vn21 = h('div', p21, c21);
c1.push(vn21);
var _22 = context['toto'];
if (_22 || _22 === 0) {
c21.push(...utils.htmlToVDOM(_22));
let c20 = [], p20 = {key:20};
var vn20 = h('div', p20, c20);
c1.push(vn20);
var _21 = context['toto'];
if (_21 || _21 === 0) {
c20.push(...utils.htmlToVDOM(_21));
} else {
c21.push({text: \`toto default\`});
c20.push({text: \`toto default\`});
}
return vn1;
}"
@@ -2400,6 +2400,90 @@ exports[`t-set t-set should reuse variable if possible 1`] = `
}"
`;
exports[`t-set t-set, t-if, and mix of expression/body lookup, 1 1`] = `
"function anonymous(context,extra
) {
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['flag']) {
}
else {
var _2 = 0;
}
if (_2 || _2 === 0) {
c1.push({text: _2});
} else {
c1.push({text: \`1\`});
}
return vn1;
}"
`;
exports[`t-set t-set, t-if, and mix of expression/body lookup, 1 2`] = `
"function anonymous(context,extra
) {
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['flag']) {
}
else {
var _2 = 0;
}
if (_2 || _2 === 0) {
c1.push({text: _2});
} else {
c1.push({text: \`1\`});
}
return vn1;
}"
`;
exports[`t-set t-set, t-if, and mix of expression/body lookup, 2 1`] = `
"function anonymous(context,extra
) {
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['flag']) {
var _2 = 1;
}
else {
}
if (_2 || _2 === 0) {
c1.push({text: _2});
} else {
c1.push({text: \`0\`});
}
return vn1;
}"
`;
exports[`t-set t-set, t-if, and mix of expression/body lookup, 2 2`] = `
"function anonymous(context,extra
) {
let sibling = null;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['flag']) {
var _2 = 1;
}
else {
}
if (_2 || _2 === 0) {
c1.push({text: _2});
} else {
c1.push({text: \`0\`});
}
return vn1;
}"
`;
exports[`t-set value priority 1`] = `
"function anonymous(context,extra
) {
+26
View File
@@ -242,6 +242,32 @@ describe("t-set", () => {
);
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe("<div>45</div>");
});
test("t-set, t-if, and mix of expression/body lookup, 1", () => {
qweb.addTemplate(
"test",
`<div>
<t t-if="flag" t-set="ourvar">1</t>
<t t-else="" t-set="ourvar" t-value="0"></t>
<t t-esc="ourvar"/>
</div>`
);
expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>");
expect(renderToString(qweb, "test", { flag: false })).toBe("<div>0</div>");
});
test("t-set, t-if, and mix of expression/body lookup, 2", () => {
qweb.addTemplate(
"test",
`<div>
<t t-if="flag" t-set="ourvar" t-value="1"></t>
<t t-else="" t-set="ourvar">0</t>
<t t-esc="ourvar"/>
</div>`
);
expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>");
expect(renderToString(qweb, "test", { flag: false })).toBe("<div>0</div>");
});
});
describe("t-if", () => {
+5
View File
@@ -43,6 +43,10 @@ describe("tokenizer", () => {
{ type: "OPERATOR", value: "!==" },
{ type: "OPERATOR", value: "!=" }
]);
expect(tokenize("typeof a")).toEqual([
{ type: "OPERATOR", value: "typeof " },
{ type: "SYMBOL", value: "a" }
]);
});
test("strings", () => {
@@ -112,6 +116,7 @@ describe("expression evaluation", () => {
expect(compileExpr("!flag", {})).toBe("!context['flag']");
expect(compileExpr("-3", {})).toBe("-3");
expect(compileExpr("-a", {})).toBe("-context['a']");
expect(compileExpr("typeof a", {})).toBe("typeof context['a']");
});
test("various binary operators", () => {
@@ -13,10 +13,10 @@ exports[`RouteComponent can render simple cases 1`] = `
if (context['routeComponent']) {
const nodeKey1 = context['env'].router.currentRouteName;
//COMPONENT
let templateId3 = \`__4__\` + nodeKey1;
let w3 = templateId3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId3]] : false;
let vn5 = {};
result = vn5;
let k4 = \`__5__\` + nodeKey1;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false;
let vn6 = {};
result = vn6;
let props3 = Object.assign({}, context['env'].router.currentParams);
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
w3.destroy();
@@ -25,20 +25,21 @@ exports[`RouteComponent can render simple cases 1`] = `
if (w3) {
w3.__updateProps(props3, extra.fiber, undefined, undefined, sibling);
let pvnode = w3.__owl__.pvnode;
utils.defineProxy(vn5, pvnode);
utils.defineProxy(vn6, pvnode);
} else {
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[templateId3] = w3.__owl__.id;
parent.__owl__.cmap[k4] = 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();}}});
let pvnode = h('dummy', {key: k4, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});
const fiber = w3.__owl__.currentFiber;
def2.then(function () {if (w3.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
utils.defineProxy(vn5, pvnode);
def2.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
utils.defineProxy(vn6, pvnode);
w3.__owl__.pvnode = pvnode;
}
w3.__owl__.parentLastFiberId = extra.fiber.id;
sibling = w3.__owl__.currentFiber || sibling;
}
return result;
+1 -2
View File
@@ -1,5 +1,4 @@
import { Component } from "../../src/component/component";
import { config } from "../../src/config";
import { Link } from "../../src/router/link";
import { RouterEnv } from "../../src/router/router";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
@@ -13,7 +12,7 @@ describe("Link component", () => {
beforeEach(() => {
fixture = makeTestFixture();
env = <RouterEnv>makeTestEnv();
config.env = env;
Component.env = env;
});
afterEach(() => {
+1 -2
View File
@@ -1,5 +1,4 @@
import { Component } from "../../src/component/component";
import { config } from "../../src/config";
import { RouterEnv } from "../../src/router/router";
import { RouteComponent } from "../../src/router/route_component";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
@@ -13,7 +12,7 @@ describe("RouteComponent", () => {
beforeEach(() => {
fixture = makeTestFixture();
env = <RouterEnv>makeTestEnv();
config.env = env;
Component.env = env;
});
afterEach(() => {
+83 -3
View File
@@ -1,5 +1,4 @@
import { Component, Env } from "../src/component/component";
import { config } from "../src/config";
import { Store, useStore, useDispatch, useGetters } from "../src/store";
import { useState } from "../src/hooks";
import { xml } from "../src/tags";
@@ -13,7 +12,7 @@ describe("connecting a component to store", () => {
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
config.env = env;
Component.env = env;
});
afterEach(() => {
@@ -49,6 +48,22 @@ describe("connecting a component to store", () => {
expect(fixture.innerHTML).toBe("<div><span>hello</span></div>");
});
test("throw error if no store is found", async () => {
class App extends Component<any, any> {
static template = xml`<div></div>`;
todos = useStore(state => state.todos);
}
let error;
try {
new App();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("No store found when connecting 'App'");
});
test("can use useStore twice in a component", async () => {
const state = { a: 1, b: 2 };
const actions = {
@@ -557,6 +572,31 @@ describe("connecting a component to store", () => {
);
});
test("store changes occuring when mounting a component are notified", async () => {
const initialState = { x: { val: 1 } };
const actions = {
setValue({ state }, val) {
state.x.val = val;
}
};
class Parent extends Component<any, any> {
static template = xml`<div><t t-esc="x.val"/></div>`;
x = useStore(state => {
return Object.assign({}, state.x);
});
dispatch = useDispatch();
}
Parent.prototype.__render = jest.fn(Parent.prototype.__render);
Parent.env.store = new Store({ state: initialState, actions });
const parent = new Parent();
const prom = parent.mount(fixture);
parent.dispatch("setValue", 2);
await prom;
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(Parent.prototype.__render).toHaveBeenCalledTimes(1);
});
test("correct update order when parent/children are connected", async () => {
const steps: string[] = [];
@@ -884,6 +924,46 @@ describe("connecting a component to store", () => {
expect(steps).toEqual(["on:update", "off:update"]);
});
test("connected child component destroyed by dispatched action", async () => {
let steps: any = [];
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="store.val"/></div>`;
store = useStore(s => {
steps.push("child selector");
return s;
});
}
class Parent extends Component<any, any> {
static template = xml`<div><Child t-if="store.child" /></div>`;
static components = { Child };
store = useStore(s => {
steps.push("parent selector");
return s;
});
dispatch = useDispatch();
}
const state = { child: true, val: 1 };
const actions = {
toggleChild({ state }) {
state.child = !state.child;
}
};
const store = new Store({ state, actions });
(<any>env).store = store;
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
expect(steps).toEqual(["parent selector", "child selector"]);
parent.dispatch("toggleChild");
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["parent selector", "child selector", "parent selector"]);
});
test("dispatch an action", async () => {
class App extends Component<any, any> {
static template = xml`<div><t t-esc="store.counter"/></div>`;
@@ -922,7 +1002,7 @@ describe("various scenarios", () => {
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
config.env = env;
Component.env = env;
});
afterEach(() => {
+2 -4
View File
@@ -15,8 +15,6 @@ class Counter extends owl.Component {
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
static components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
@@ -26,12 +24,11 @@ class Message extends owl.Component {
});
}
}
Message.components = { Counter };
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
static components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
@@ -137,6 +134,7 @@ class App extends owl.Component {
this.refs.log.innerHTML = "";
}
}
App.components = { Message };
//------------------------------------------------------------------------------
// Application initialization
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>OWL v0.24.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='../../owl.js'></script>
<script src='owl.js'></script>
</head>
<body>
<script src='app.js' type="module"></script>
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -16,8 +16,6 @@ class Counter extends owl.Component {
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
static components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
@@ -27,12 +25,12 @@ class Message extends owl.Component {
});
}
}
Message.components = { Counter };
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
static components = { Message };
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
@@ -139,13 +137,14 @@ class App extends owl.Component {
this.logRef.el.innerHTML = "";
}
}
App.components = { Message };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadFile("templates.xml");
owl.config.env = {
App.env = {
qweb: new owl.QWeb({ templates })
};
const app = new App();
+33 -57
View File
@@ -70,13 +70,9 @@ if __name__ == "__main__":
/**
* Make an iframe, with all the js, css and xml properly injected.
*/
function makeCodeIframe(js, css, xml, errorHandler) {
// check templates
var qweb = new owl.QWeb();
function makeCodeIframe(js, css, xml) {
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "");
// will throw error if there is something wrong with xml
qweb.addTemplates(sanitizedXML);
// create iframe
const iframe = document.createElement("iframe");
@@ -92,21 +88,13 @@ function makeCodeIframe(js, css, xml, errorHandler) {
script.type = "text/javascript";
const content = `
{
owl.__info__.mode = 'dev';
owl.config.mode = 'dev';
let templates = \`${sanitizedXML}\`;
const qweb = new owl.QWeb({ templates });
owl.config.env = { qweb };
owl.Component.env = { qweb };
}
${js}`;
script.innerHTML = content;
iframe.contentWindow.addEventListener("error", errorHandler);
iframe.contentWindow.addEventListener("unhandledrejection", errorHandler);
setTimeout(function() {
if (iframe.contentWindow) {
iframe.contentWindow.removeEventListener("error", errorHandler);
iframe.contentWindow.removeEventListener("unhandledrejection", errorHandler);
}
}, 200);
doc.body.appendChild(script);
});
doc.head.appendChild(owlScript);
@@ -132,20 +120,34 @@ async function makeApp(js, css, xml) {
.join("\n");
const JS = `
async function loadTemplates() {
try {
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'\`);
}
}
function start([TEMPLATES]) {
// Application code
/**
* This is the javascript code defined in the playground.
* In a larger application, this code should probably be moved in different
* sub files.
*/
function app() {
${processedJS}
}
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
/**
* Initialization code
* This code load templates, and make sure everything is properly connected.
*/
async function start() {
let templates;
try {
templates = await owl.utils.loadFile('app.xml');
} catch(e) {
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
return;
}
const env = { qweb: new owl.QWeb({templates})};
owl.Component.env = env;
await owl.utils.whenReady();
app();
}
start();
`;
zip.file("app.js", JS);
@@ -317,7 +319,6 @@ class App extends owl.Component {
js: this.SAMPLES[0].code,
css: this.SAMPLES[0].css || "",
xml: this.SAMPLES[0].xml || DEFAULT_XML,
error: false,
displayWelcome: true,
splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2),
@@ -330,37 +331,12 @@ class App extends owl.Component {
this.content = useRef("content");
}
displayError(error) {
this.state.error = error;
if (error) {
setTimeout(() => {
this.content.el.innerHTML = "";
});
return;
}
}
runCode() {
this.state.displayWelcome = false;
let subiframe;
let error = false;
const errorHandler = e => this.displayError(e.message || e.reason.message);
try {
const { js, css, xml } = this.state;
subiframe = makeCodeIframe(js, css, xml, errorHandler);
} catch (e) {
//probably problem with the templates
error = e;
// we still log the error, always useful to have it available
console.error(e);
}
if (error) {
this.displayError(error.message);
return;
} else {
this.state.error = false;
}
this.content.el.innerHTML = "";
this.state.displayWelcome = false;
const { js, css, xml } = this.state;
const subiframe = makeCodeIframe(js, css, xml);
this.content.el.appendChild(subiframe);
}
@@ -443,7 +419,7 @@ async function start() {
owl.utils.whenReady()
]);
const qweb = new owl.QWeb({ templates });
owl.config.env = { qweb };
owl.Component.env = { qweb };
const app = new App();
app.mount(document.body);
}
-13
View File
@@ -179,16 +179,3 @@ body {
padding: 5%;
}
.right-pane .error {
height: 100%;
width: 90%;
padding-top: 30%;
font-size: 18px;
color: darkred;
margin-left: 5%;
}
.right-pane .error pre {
overflow: auto;
width: 100%;
}
+4 -4
View File
@@ -345,7 +345,7 @@ const themeContext = new Context({
foreground: '#fff',
});
// Add the themeContext the environment to make it available to all components
owl.config.env.themeContext = themeContext;
App.env.themeContext = themeContext;
const app = new App();
app.mount(document.body);
`;
@@ -544,7 +544,7 @@ function makeStore() {
return store;
}
owl.config.env.store = makeStore();
TodoApp.env.store = makeStore();
const app = new TodoApp();
app.mount(document.body);
`;
@@ -1032,7 +1032,7 @@ function setupResponsivePlugin(env) {
//------------------------------------------------------------------------------
// Application Startup
//------------------------------------------------------------------------------
setupResponsivePlugin(owl.config.env);
setupResponsivePlugin(App.env);
const app = new App();
app.mount(document.body);
@@ -1543,7 +1543,7 @@ const windows = [
}
];
owl.config.env.windows = windows;
App.env.windows = windows;
const app = new App();
app.mount(document.body);
`;
-4
View File
@@ -51,10 +51,6 @@
</p>
</div>
</div>
<div t-if="state.error" class="error">
<h3>Error</h3>
<pre t-esc="state.error"/>
</div>
<div class="content" t-ref="content"/>
</div>
</div>