Compare commits

..

19 Commits

Author SHA1 Message Date
Géry Debongnie 0290f63ba3 [FIX] component: various issues while mounting manually components
The initial problem solved by this commit is that it was possible to get
into a situation where a mounting/rendering was started, then the component was
updated, but then another mounting operation begins, and it tries to
reuse the previous rendering operation, which is no longer uptodate.

The underlying issue is that Owl did not track properly the various
internal state change of a component.  These issue should be solved by
the introduction of the status enum, which currently tracks 6 possible
states:

- CREATED
- WILLSTARTED
- RENDERED
- MOUNTED
- UNMOUNTED
- DESTROYED

This status number replaces the isMounted and isDestroyed boolean flags.
It has the advantage of making sure that the component is in a
consistent state (it is no longer possible to be destroyed and mounted,
for example)

Another advantage is that it gives us an easy way to track the fact that
a component has been rendered, but is not in the DOM.  This is a subtle
situation where some various events can happen, and we need to be able
to react to that case.

Note that there is a change of behaviour: if a component is mounted in a
specific target, then before the mounting is complete, the component is
mounted in another target, we no longer reject the first mounting
operation.
2021-02-08 10:59:28 +01:00
Géry Debongnie 370fae4e1a [FIX] package.json: rollback live-server from 1.2.3 to 1.2.1
No idea why, but it looks like the npm package 1.2.3 disappeared.
2021-02-04 09:07:26 +01:00
Géry Debongnie 76735410f8 [FIX] qweb: prevent issue with builtin object properties
The QWeb expression parser use an object as a mapping between some
strings and the desired output in the compiled template.  However, as we
should all know, objects are not Maps, they have some additional
properties, such as "constructor" or "hasOwnProperty".

The simple solution is to make sure the mapping object does not have
anything in its prototype chain to pollute its purpose.

closes #835
2021-02-04 09:07:26 +01:00
Géry Debongnie 19a47a7001 [IMP] component: add setup lifecycle hook 2021-02-03 15:32:38 +01:00
Géry Debongnie 398f9f4e53 [IMP] component: can trigger event handlers even if unmounted
From the beginning, Owl was designed to only call event handler on
components that are mounted in the DOM.  The main benefit is that if a
component is destroyed, we are guaranteed to not execute any useless (or
potentially dangerous code).

However, there is one downside: if a component tree is being mounted,
and a child component trigger an event in its mounted hook, then it
cannot be caught by the parent, since the parent is technically not yet
mounted.

This may not be a good situation, but the point is that Owl
unnecessarily prevent the handler to be called.

We can fix this issue by simply checking if the component is not destroyed
instead of checking that it is mounted.

closes #809
2021-02-03 14:46:18 +01:00
Géry Debongnie af426aa902 [DOC] misc small fixes
closes #827, #825, #822, #821
2021-02-03 13:44:04 +01:00
Géry Debongnie 490cf18079 [REL] v1.2.3
# v1.2.3

This is just another small bug fix release, because we need it in Odoo.

Fix: prevent crash when rendering a component before mounting it
2021-01-19 15:39:26 +01:00
Géry Debongnie dfc782599b [FIX] crash when rendering component before mounting
This commit makes sure that Owl does not crash when a component is
created, then updated (for example, with a (observed) state change), and
then, some moment later, mounted.

The initial render is not useful, because it is not linked to a mounting
action anyway.  And it caused issues such as a crash when Owl tried to
patch it to a non existing target
2021-01-19 15:26:15 +01:00
Géry Debongnie 71f545058b [REL] v1.2.2
# v1.2.2

This is just a small bug fix release, because we need it in Odoo.

Bug fix: allow renderings for detached components. Before this release, Owl
ignored renderings in some cases if a component is detached.  We now still render
it, because it is important in some cases.
2021-01-14 09:10:35 +01:00
Géry Debongnie 1a20cc57de [FIX] component can be updated while detached from the main DOM
This commit tries to improve the interactions involving unmounted
components, or components mounted in an htmelement which is detached
from the main DOM, and rendering actions.

The main example is mounting a component in detached div, to prepare all
children.  If we just mount the component, it will work as expected: the
full component tree is rendered in memory, and ready to be really
mounted at the desired target.

However, if before doing that, we update the component and call render
on it (for example, with a change in an observed state), then this
rendering will be ignored, and therefore, the full subcomponent tree is
not uptodate.

This commit will also solve another issue in the compatibility layer in
odoo: in the form renderer, we mount components with the adapter in a
div, which is not yet attached to the DOM. We then manually call the
mounted hook when on_attach_callback is called.  This means that before
this commit, any change to the components between the initial rendering
and the call to mounted will be ignored.

As a bonus, this commit has the effect of bringing closer the semantics
of render and mount operations, which is certainly good.

closes #823
2021-01-14 09:05:05 +01:00
Géry Debongnie 25738a1bf0 [REL] v1.2.1
# Owl v1.2.1

## Changes

- fix: issue with components with shouldUpdate and remounting not rendering
- fix: issue with connected components using onUpdate callback not rendering
- fix: error in todoapp example code
2021-01-08 15:26:28 +01:00
Géry Debongnie 4a96eff3c6 [FIX] store: properly call onUpdate functions in some cases
Before this commit, the following scenario could happen:

Suppose that we have a parent component A, connected to a store,
 and a child component B, also connected to the store and using
the onUpdate feature.

Then, we remount the A component in some other places and a
rendering is initiated in A.  We immediately update the store state.
What happens next is:

- rendering A is done, A internal revid is updated
- store update A (but nothing is done because the state change here
  does not modify A)
- rendering B is done (from parent), B internal revid is updated
- store update B, notice internal revid is updated, does not call the
  onUpdate function

We then have the B component which has not its internal state updated,
because we did not call its onUpdate function.

The solution is to move the onUpdate call in a "preupdate" event, to be
sure that it is called everytime the store is updated.

closes #816
2021-01-08 15:05:07 +01:00
Géry Debongnie d043d47754 [FIX] component: propagate correct info when reusing fibers
In some cases, a rendering initiated in some component is then remapped
into a larger rendering initiated by some parent.

If we have some components which implement shouldUpdate to return false,
then the following scenario can happen:

- some parent component is mounted (which triggers a rendering with
force: true => bypass the shouldUpdate)
- some sub component is updated and rerendered, AFTER the previous
rendering goes through it
- the sub component notices that there is an ongoing rendering, and
  remaps itself in the parent rendering

Before this commit, the new fiber in the subcomponent does not have
force flag set to true, so the new rendering for the subcomponent does
not go through its own children (if they have shouldUpdate=false)

Another more complex kind of scenaria can happen when a remapped
rendering happen with sub components with dynamic shouldUpdate. The
problem is the same at the end: the new rendering should ignore the
shouldUpdate, to make sure we have the last correct information.

With this commit, we make sure that the flag of the new fiber is set to
true.

closes #818
2021-01-07 10:08:29 +01:00
Vishnu Vanneri 3a10468f7b Update todoapp mount issue (#817)
APP gets error while running "ReferenceError: mount is not defined"
because of mount not defined with "Component"
2021-01-06 08:43:33 +01:00
Géry Debongnie 144b323d2b [REL] v1.2.0
# Owl v1.2.0

## Changes

- translation fix for terms surrounded by spaces
- fix: properly remount components with shouldUpdate=false
- add: add two new generic hooks:  and
- fix: do not skip rendering in components using store and local state (in some cases)

Note that the last change is a pretty significant change: component connected to
a store should be very careful if the data that they represent is deleted, because
they will always be rendered with the current state of the store.
2020-12-14 13:23:46 +01:00
Géry Debongnie bb64e87634 [FIX] store: properly render, even if shouldupdate is implemented
Before this commit, an unwanted behaviour happened when using components
with shouldUpdate implemented, and store/state.

The actual problem is the following: the sub component is using a store,
and shouldupdate.  Whenever the component is rendered, it checks if there is an incoming
rendering from the context.  If that is the case, it skips the
rendering, because we actually only want to be rendered by the store
rendering (otherwise, we may run into issue with inconsistent data (more
recent data from the context, older data in the component).

However, if shouldUpdate is implemented, then the rendering coming from
the context simply does not arrive.

To fix this, we tried to just force these renderings to go through all children,
regardless of their shouldUpdate status. This actually works, but then
shouldUpdate is ignored, which is an issue in Odoo discuss.

Then, after discussing this situation, we noticed that the context/store
system is actually unsafe: the protection given by the check mentioned
above is in fact fundamentally insufficient: there are other perfectly
valid situations where a rendering can be triggered on the component,
which will bypass the check (for example, an explicit call to
this.render() or a rendering initiated by some parent component) and
cause a crash if the component is not properly defensively written.

Therefore, it is currently mandatory for all components using
context/store to be aware of that, and to protect themselves against
such situations. So, in that regard, the check is not really a
protection, it just helps hiding an unsafe situation anyway and we
decided to remove it.

Note that this is a potentially breaking change: components using a
store and some local state will now be rendered twice in some cases...

closes #799
2020-12-14 13:19:36 +01:00
Géry Debongnie 9a87b9a4a0 [IMP] hooks: add some building blocks for hooks
This commit introduces two new hooks: useComponent, and
useEnv.
2020-12-14 11:53:01 +01:00
Géry Debongnie 2a53a9592e [FIX] component: force mounting subcomponents with shouldUpdate
Components can implement shouldUpdate to return false.  In that case,
renderings coming from above should be ignored.

However, if the component was unmounted and is remounted, we actually
need to force a rerendering in that case, so it is mounted, otherwise
the subcomponent is left in unmounted state, which means that rendering
are ignored.

closes #800
2020-12-11 15:19:37 +01:00
Ivan Yelizariev f54b9a4a0c [FIX] properly translate terms surrounded by spaces
This reimplements logic from Odoo v13 [1], i.e. if we have a text that follows
by space e.g. "This database will expire in " [2], then we want get translation
for the term without that space and add the space manually. This way we can
export trimmed terms for translations and don't miss the space, when atranslator
forget to add it at the end of translation

[1] https://github.com/odoo/odoo/blob/26927417e2957acba6fb79446c2520107daa7eea/addons/web/static/src/js/core/qweb.js#L46
[2] https://github.com/odoo/enterprise/blob/ab0e893493f909ec3033e8a1cf6c141ea9375587/web_enterprise/static/src/xml/base.xml#L21

---

opw-2410708
2020-12-03 13:36:17 +01:00
36 changed files with 971 additions and 285 deletions
+1 -1
View File
@@ -120,7 +120,7 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.1.1](https://github.com/odoo/owl/releases/tag/v1.1.1)
- [owl-1.2.3](https://github.com/odoo/owl/releases/tag/v1.2.3)
## License
+4 -4
View File
@@ -545,7 +545,7 @@ application), since it involves extracting all task related code out of the
components. Here is the new content of the `app.js` file:
```js
const { Component, Store } = owl;
const { Component, Store, mount } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
const { useRef, useDispatch, useStore } = owl.hooks;
@@ -663,7 +663,7 @@ function makeStore() {
function setup() {
owl.config.mode = "dev";
const env = {store = makeStore()};
const env = { store: makeStore() };
mount(App, { target: document.body, env });
}
```
@@ -808,7 +808,7 @@ For reference, here is the final code:
```js
(function () {
const { Component, Store } = owl;
const { Component, Store, mount } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
const { useRef, useDispatch, useState, useStore } = owl.hooks;
@@ -939,7 +939,7 @@ For reference, here is the final code:
function setup() {
owl.config.mode = "dev";
const env = {store = makeStore()};
const env = { store: makeStore() };
mount(App, { target: document.body, env });
}
+36 -5
View File
@@ -10,13 +10,22 @@
- [Static Properties](#static-properties)
- [Methods](#methods)
- [Lifecycle](#lifecycle)
- [`constructor(parent, props)`](#constructorparent-props)
- [`setup()`](#setup)
- [`willStart()`](#willstart)
- [`mounted()`](#mounted)
- [`willUpdateProps(nextProps)`](#willupdatepropsnextprops)
- [`willPatch()`](#willpatch)
- [`patched(snapshot)`](#patchedsnapshot)
- [`willUnmount()`](#willunmount)
- [`catchError(error)`](#catcherrorerror)
- [Root Component](#root-component)
- [Composition](#composition)
- [Form Input Bindings](#form-input-bindings)
- [References](#references)
- [Dynamic sub components](#dynamic-sub-components)
- [Functional Components](#functional-components)
- [SVG components](#svg-components)
- [SVG Components](#svg-components)
## Overview
@@ -289,8 +298,13 @@ We explain here all the public methods of the `Component` class.
are updated. It returns a boolean, which indicates if the component should
ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
can be useful if we are handling large number of components.
always return true. Note that this is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
can be useful if we are handling large number of components. Since this is an
optimization, Owl has the freedom to ignore the result of `shouldUpdate` in
some cases (for example, if a component is remounted, or if we want to force
a full rerender of the UI). However, if `shouldUpdate` returns true, then Owl
provides the guarantee that the component will be rendered at some point in
the future (except if the component is destroyed or if some part of the UI crashes).
* **`destroy()`**. As its name suggests, this method will remove the component,
and perform all necessary cleanup, such as unmounting the component, its children,
@@ -311,7 +325,7 @@ a owl component:
| Method | Description |
| ------------------------------------------------ | ----------------------------------------------------------- |
| **[constructor](#constructorparent-props)** | constructor |
| **[setup](#setup)** | setup |
| **[willStart](#willstart)** | async, before first rendering |
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
@@ -356,6 +370,23 @@ class ClickCounter extends owl.Component {
}
```
Hook functions can be called in the constructor.
#### `setup()`
_setup_ is run just after the component is constructed. It is a lifecycle method,
very similar to the _constructor_, except that it does not receive any argument.
It is a valid method to call hook functions. Note that one of the main reason to
have the `setup` hook in the component lifecycle is to make it possible to
monkey patch it. It is a common need in the Odoo ecosystem.
```javascript
setup() {
useSetupAutofocus();
}
```
#### `willStart()`
willStart is an asynchronous hook that can be implemented to
@@ -758,7 +789,7 @@ template rendered with `props`. In Owl, this can be done by
simply defining a template, that will access the `props` object:
```js
const Welcome = xml`<h1>Hello, {props.name}</h1>`;
const Welcome = xml`<h1>Hello, <t t-esc="props.name"/></h1>`;
class MyComponent extends Component {
static template = xml`
+2
View File
@@ -29,6 +29,8 @@ hooks utils
useContext
useState
useRef
useComponent
useEnv
useSubEnv
useStore
useDispatch
+22 -5
View File
@@ -21,6 +21,8 @@
- [`useStore`](#usestore)
- [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters)
- [`useComponent`](#usecomponent)
- [`useEnv`](#useenv)
- [Making customized hooks](#making-customized-hooks)
## Overview
@@ -129,7 +131,7 @@ class SomeComponent extends Component {
### One rule
There is only one rule: every hook for a component has to be called in the
constructor (or in class fields):
constructor, in the _setup_ method, or in class fields:
```js
// ok
@@ -145,6 +147,13 @@ class SomeComponent extends Component {
}
}
// also ok
class SomeComponent extends Component {
setup() {
this.state = useState({ value: 0 });
}
}
// not ok: this is executed after the constructor is called
class SomeComponent extends Component {
async willStart() {
@@ -381,6 +390,16 @@ The `useDispatch` hook is the way for components to get a reference to the store
The `useGetters` hook is the way for components to get a reference to the store
getters. See the [store documentation](store.md) for more information.
### `useComponent`
The `useComponent` hook is useful as a building block for some customized hooks,
that may need a reference to the component calling them.
### `useEnv`
The `useEnv` hook is useful as a building block for some customized hooks,
that may need a reference to the env of the component calling them.
### Making customized hooks
Hooks are a wonderful way to organize the code of a complex component by feature
@@ -435,13 +454,11 @@ not the solution to every problem.
```js
function useRouter() {
return Component.current.env.router;
const env = useEnv();
return env.router;
}
```
This means that we give control to the application developer to create the
router, which is good, so they can set it up, subclass it, ... And then, to
test our components, we can just add a mock router in the environment.
Note: the code above makes use of the `Component.current` property. This is the
way hooks are able to get a reference to the component currently being created.
+5 -5
View File
@@ -15,7 +15,7 @@ use cases, there is no need to directly instantiate an observer.
For example, this code will display `update` in the console:
```javascript
const observer = new owl.Observer();
const observer = new owl.core.Observer();
observer.notifyCB = () => console.log("update");
const obj = observer.observe({ a: { b: 1 } });
@@ -39,14 +39,14 @@ is incremented every time the value is observed. Sometimes, it can be useful
to obtain that number:
```js
const observer = new owl.Observer();
const observer = new owl.core.Observer();
const obj = observer.observe({ a: { b: 1 } });
observer.deepRevNumber(obj.a); // 1
observer.revNumber(obj.a); // 1
obj.a.b = 2;
observer.deepRevNumber(obj.a); // 2
observer.revNumber(obj.a); // 2
```
The `deepRevNumber` can also return 0, which indicates that the value is not
The `revNumber` can also return 0, which indicates that the value is not
observed.
+1 -1
View File
@@ -44,7 +44,7 @@ Slots are defined by the caller, with the `t-set-slot` directive:
```xml
<div t-name="SomeComponent">
<div>some component</div>
<Dialog title="Some Dialog">
<Dialog title="'Some Dialog'">
<t t-set-slot="content">
<div>hey</div>
</t>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "1.1.1",
"version": "1.2.3",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js",
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.1.1
- Current version: 1.2.3
- Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may
+96 -78
View File
@@ -44,6 +44,15 @@ interface MountOptions {
position?: MountPosition;
}
export const enum STATUS {
CREATED,
WILLSTARTED, // willstart has been called
RENDERED, // first render is completed (so, vnode is now defined)
MOUNTED, // is ready, and in DOM. It has a valid el
UNMOUNTED, // has a valid el, but is not in DOM
DESTROYED,
}
/**
* This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the
@@ -56,8 +65,7 @@ interface Internal<T extends Env> {
depth: number;
vnode: VNode | null;
pvnode: VNode | null;
isMounted: boolean;
isDestroyed: boolean;
status: STATUS;
// parent and children keys are obviously useful to setup the parent-children
// relationship.
@@ -164,16 +172,18 @@ export class Component<Props extends {} = any, T extends Env = Env> {
this.env.browser = browser;
}
this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) {
this.render(true);
}
if (this.__owl__.isDestroyed) {
// this is unlikely to happen, but if a root widget is destroyed,
// we want to remove our subscription. The usual way to do that
// would be to perform some check in the destroy method, but since
// it is very performance sensitive, and since this is a rare event,
// we simply do it lazily
this.env.qweb.off("update", this);
switch (this.__owl__.status) {
case STATUS.MOUNTED:
this.render(true);
break;
case STATUS.DESTROYED:
// this is unlikely to happen, but if a root widget is destroyed,
// we want to remove our subscription. The usual way to do that
// would be to perform some check in the destroy method, but since
// it is very performance sensitive, and since this is a rare event,
// we simply do it lazily
this.env.qweb.off("update", this);
break;
}
});
depth = 0;
@@ -186,8 +196,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
depth: depth,
vnode: null,
pvnode: null,
isMounted: false,
isDestroyed: false,
status: STATUS.CREATED,
parent: parent || null,
children: {},
cmap: {},
@@ -209,8 +218,20 @@ export class Component<Props extends {} = any, T extends Env = Env> {
if (constr.style) {
this.__applyStyles(constr);
}
this.setup();
}
/**
* setup is run just after the component is constructed. This is the standard
* location where the component can setup its hooks. It has some advantages
* over the constructor:
* - it can be patched (useful in odoo ecosystem)
* - it does not need to propagate the arguments to the super call
*
* Note: this method should not be called manually.
*/
setup() {}
/**
* willStart is an asynchronous hook that can be implemented to perform some
* action before the initial rendering of a component.
@@ -305,42 +326,49 @@ export class Component<Props extends {} = any, T extends Env = Env> {
* Note that a component can be mounted an unmounted several times
*/
async mount(target: HTMLElement | DocumentFragment, options: MountOptions = {}): Promise<void> {
const position = options.position || "last-child";
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
if (position !== "self" && this.el!.parentNode !== target) {
// in this situation, we are trying to mount a component on a different
// target. In this case, we need to unmount first, otherwise it will
// not work.
this.unmount();
} else {
return Promise.resolve();
}
}
if (__owl__.isDestroyed) {
throw new Error("Cannot mount a destroyed component");
}
if (__owl__.currentFiber) {
const currentFiber = __owl__.currentFiber;
if (currentFiber.target === target && currentFiber.position === position) {
return scheduler.addFiber(currentFiber);
} else {
scheduler.rejectFiber(currentFiber, "Mounting operation cancelled");
}
}
if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) {
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);
}
const fiber = new Fiber(null, this, false, target, position);
fiber.shouldPatch = false;
if (!__owl__.vnode) {
this.__prepareAndRender(fiber, () => {});
} else {
this.__render(fiber);
const position = options.position || "last-child";
const __owl__ = this.__owl__;
const currentFiber = __owl__.currentFiber;
switch (__owl__.status) {
case STATUS.CREATED: {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__prepareAndRender(fiber, () => {});
return scheduler.addFiber(fiber);
}
case STATUS.WILLSTARTED:
case STATUS.RENDERED:
currentFiber.target = target;
currentFiber.position = position;
return scheduler.addFiber(currentFiber);
case STATUS.UNMOUNTED: {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__render(fiber);
return scheduler.addFiber(fiber);
}
case STATUS.MOUNTED: {
if (position !== "self" && this.el!.parentNode !== target) {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__render(fiber);
return scheduler.addFiber(fiber);
} else {
return Promise.resolve();
}
}
case STATUS.DESTROYED:
throw new Error("Cannot mount a destroyed component");
}
return scheduler.addFiber(fiber);
}
/**
@@ -348,7 +376,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
* to call willUnmount calls and remove the component from the DOM.
*/
unmount() {
if (this.__owl__.isMounted) {
if (this.__owl__.status === STATUS.MOUNTED) {
this.__callWillUnmount();
this.el!.remove();
}
@@ -366,10 +394,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
async render(force: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
const currentFiber = __owl__.currentFiber;
if (!__owl__.isMounted && !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.
if (!__owl__.vnode && !currentFiber) {
return;
}
if (currentFiber && !currentFiber.isRendered && !currentFiber.isCompleted) {
@@ -378,15 +403,13 @@ export class Component<Props extends {} = any, T extends Env = Env> {
// 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 status = __owl__.status;
const fiber = new Fiber(null, this, force, null, null);
Promise.resolve().then(() => {
if (__owl__.isMounted || !isMounted) {
if (fiber.isCompleted) {
if (__owl__.status === STATUS.MOUNTED || status !== STATUS.MOUNTED) {
if (fiber.isCompleted || fiber.isRendered) {
return;
}
// 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
@@ -410,7 +433,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
*/
destroy() {
const __owl__ = this.__owl__;
if (!__owl__.isDestroyed) {
if (__owl__.status !== STATUS.DESTROYED) {
const el = this.el;
this.__destroy(__owl__.parent);
if (el) {
@@ -455,13 +478,12 @@ export class Component<Props extends {} = any, T extends Env = Env> {
*/
__destroy(parent: Component | null) {
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
if (__owl__.status === STATUS.MOUNTED) {
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.isMounted = false;
__owl__.status = STATUS.UNMOUNTED;
}
const children = __owl__.children;
for (let key in children) {
@@ -472,7 +494,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
delete parent.__owl__.children[id];
__owl__.parent = null;
}
__owl__.isDestroyed = true;
__owl__.status = STATUS.DESTROYED;
delete __owl__.vnode;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true;
@@ -482,7 +504,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
__callMounted() {
const __owl__ = this.__owl__;
__owl__.isMounted = true;
__owl__.status = STATUS.MOUNTED;
__owl__.currentFiber = null;
this.mounted();
if (__owl__.mountedCB) {
@@ -496,7 +518,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.isMounted = false;
__owl__.status = STATUS.UNMOUNTED;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true;
__owl__.currentFiber.root.counter = 0;
@@ -504,7 +526,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
if (comp.__owl__.status === STATUS.MOUNTED) {
comp.__callWillUnmount();
}
}
@@ -625,18 +647,25 @@ export class Component<Props extends {} = any, T extends Env = Env> {
}
return p._template;
}
async __prepareAndRender(fiber: Fiber, cb: CallableFunction) {
try {
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
const proms = Promise.all([
this.willStart(),
this.__owl__.willStartCB && this.__owl__.willStartCB(),
]);
this.__owl__.status = STATUS.WILLSTARTED;
await proms;
if (this.__owl__.status === <any>STATUS.DESTROYED) {
return Promise.resolve();
}
} catch (e) {
fiber.handleError(e);
return Promise.resolve();
}
if (this.__owl__.isDestroyed) {
return Promise.resolve();
}
if (!fiber.isCompleted) {
this.__render(fiber);
this.__owl__.status = STATUS.RENDERED;
cb();
}
}
@@ -659,7 +688,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
for (let childKey in __owl__.children) {
const child = __owl__.children[childKey];
const childOwl = child.__owl__;
if (!childOwl.isMounted && childOwl.parentLastFiberId < fiber.id) {
if (childOwl.status !== STATUS.MOUNTED && childOwl.parentLastFiberId < fiber.id) {
// we only do here a "soft" destroy, meaning that we leave the child
// dom node alone, without removing it. Most of the time, it does not
// matter, because the child component is already unmounted. However,
@@ -706,17 +735,6 @@ export class Component<Props extends {} = any, T extends Env = Env> {
}
}
/**
* Only called by qweb t-component directive (when t-keepalive is set)
*/
__remount() {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
__owl__.isMounted = true;
this.mounted();
}
}
/**
* Apply default props (only top level).
*
+2 -1
View File
@@ -1,6 +1,7 @@
import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/compilation_context";
import { makeHandlerCode, MODS_CODE } from "../qweb/extensions";
import { STATUS } from "./component";
//------------------------------------------------------------------------------
// t-component
@@ -361,7 +362,7 @@ QWeb.addDirective({
// need to update component
let styleCode = "";
if (tattStyle) {
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
styleCode = `.then(()=>{if (w${componentID}.__owl__.status === ${STATUS.DESTROYED}) {return};w${componentID}.el.style=${tattStyle};});`;
}
ctx.addLine(
`w${componentID}.__updateProps(props${componentID}, extra.fiber, ${scope})${styleCode};`
+35 -20
View File
@@ -1,5 +1,5 @@
import { h, VNode } from "../vdom/index";
import { Component, MountPosition } from "./component";
import { Component, MountPosition, STATUS } from "./component";
import { scheduler } from "./scheduler";
/**
@@ -82,6 +82,7 @@ export class Fiber {
let oldFiber = __owl__.currentFiber;
if (oldFiber && !oldFiber.isCompleted) {
this.force = true;
if (oldFiber.root === oldFiber && !parent) {
// both oldFiber and this fiber are root fibers
this._reuseFiber(oldFiber);
@@ -106,6 +107,8 @@ export class Fiber {
*/
_reuseFiber(oldFiber: Fiber) {
oldFiber.cancel(); // cancel children fibers
oldFiber.target = this.target || oldFiber.target;
oldFiber.position = this.position || oldFiber.position;
oldFiber.isCompleted = false; // keep the root fiber alive
oldFiber.isRendered = false; // the fiber has to be re-rendered
if (oldFiber.child) {
@@ -187,7 +190,8 @@ export class Fiber {
complete() {
let component = this.component;
this.isCompleted = true;
if (!this.target && !component.__owl__.isMounted) {
const status = component.__owl__.status;
if (status === STATUS.DESTROYED) {
return;
}
@@ -201,14 +205,16 @@ export class Fiber {
const patchLen = patchQueue.length;
// call willPatch hook on each fiber of patchQueue
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
if (fiber.shouldPatch) {
component = fiber.component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
if (status === STATUS.MOUNTED) {
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
if (fiber.shouldPatch) {
component = fiber.component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
}
component.willPatch();
}
}
@@ -249,8 +255,9 @@ export class Fiber {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
}
}
if (fiber === component.__owl__.currentFiber) {
component.__owl__.currentFiber = null;
const compOwl = component.__owl__;
if (fiber === compOwl.currentFiber) {
compOwl.currentFiber = null;
}
}
@@ -270,16 +277,24 @@ export class Fiber {
}
// call patched/mounted hook on each fiber of (reversed) patchQueue
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
if (fiber.shouldPatch && !this.target) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
if (status === STATUS.MOUNTED || inDOM) {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
if (fiber.shouldPatch && !this.target) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
} else {
component.__callMounted();
}
} else if (this.target ? inDOM : true) {
component.__callMounted();
}
} else {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
component.__owl__.status = STATUS.UNMOUNTED;
}
}
}
-10
View File
@@ -115,16 +115,6 @@ export function useContextWithCB(ctx: Context, component: Component, method): an
__owl__.observer = new Observer();
__owl__.observer.notifyCB = component.render.bind(component);
}
const currentCB = __owl__.observer.notifyCB;
__owl__.observer.notifyCB = function () {
if (ctx.rev > mapping[id]) {
// in this case, the context has been updated since we were rendering
// last, and we do not need to render here with the observer. A
// rendering is coming anyway, with the correct props.
return;
}
currentCB();
};
mapping[id] = 0;
const renderFn = __owl__.renderFn;
+21 -1
View File
@@ -1,4 +1,4 @@
import { Component } from "./component/component";
import { Component, Env } from "./component/component";
import { Observer } from "./core/observer";
/**
@@ -118,6 +118,26 @@ export function useRef<C extends Component = Component>(name: string): Ref<C> {
};
}
// -----------------------------------------------------------------------------
// "Builder" hooks
// -----------------------------------------------------------------------------
/**
* This hook is useful as a building block for some customized hooks, that may
* need a reference to the component calling them.
*/
export function useComponent<P, E extends Env>(): Component<P, E> {
return Component.current as any;
}
/**
* This hook is useful as a building block for some customized hooks, that may
* need a reference to the env of the component calling them.
*/
export function useEnv<E extends Env>(): E {
return Component.current.env as any;
}
// -----------------------------------------------------------------------------
// useSubEnv
// -----------------------------------------------------------------------------
+4 -4
View File
@@ -29,14 +29,14 @@ const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in
","
);
const WORD_REPLACEMENT = {
const WORD_REPLACEMENT = Object.assign(Object.create(null), {
and: "&&",
or: "||",
gt: ">",
gte: ">=",
lt: "<",
lte: "<=",
};
});
export interface QWebVar {
id: string; // foo
@@ -69,7 +69,7 @@ interface Token {
varName?: string;
}
const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(null), {
"{": "LEFT_BRACE",
"}": "RIGHT_BRACE",
"[": "LEFT_BRACKET",
@@ -78,7 +78,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
",": "COMMA",
"(": "LEFT_PAREN",
")": "RIGHT_PAREN",
};
});
// note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof
+2 -1
View File
@@ -1,3 +1,4 @@
import { STATUS } from "../component/component";
import { VNode } from "../vdom/index";
import { INTERP_REGEXP } from "./compilation_context";
import { QWeb } from "./qweb";
@@ -76,7 +77,7 @@ export function makeHandlerCode(
code = ctx.captureExpression(value);
}
const modCode = mods.map((mod) => modcodes[mod]).join("");
let handler = `function (e) {if (!context.__owl__.isMounted){return}${modCode}${code}}`;
let handler = `function (e) {if (context.__owl__.status === ${STATUS.DESTROYED}){return}${modCode}${code}}`;
if (putInCache) {
const key = ctx.generateTemplateKey(event);
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || ${handler};`);
+3 -1
View File
@@ -70,6 +70,7 @@ const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
const NODE_HOOKS_PARAMS = {
create: "(_, n)",
@@ -496,7 +497,8 @@ export class QWeb extends EventBus {
}
if (this.translateFn) {
if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
text = this.translateFn(text);
const match = translationRE.exec(text);
text = match[1] + this.translateFn(match[2]) + match[3];
}
}
if (ctx.parentNode) {
+15 -5
View File
@@ -1,5 +1,4 @@
import { Component } from "./component/component";
import { Env } from "./component/component";
import { Component, Env } from "./component/component";
import { Context, useContextWithCB } from "./context";
import { onWillUpdateProps } from "./hooks";
@@ -76,6 +75,11 @@ export class Store extends Context {
);
return result;
}
__notifyComponents(): Promise<void> {
this.trigger("before-update");
return super.__notifyComponents();
}
}
interface SelectorOptions {
@@ -106,13 +110,16 @@ export function useStore(selector, options: SelectorOptions = {}): any {
const newRevNumber = hashFn(result);
if ((newRevNumber > 0 && revNumber !== newRevNumber) || !isEqual(oldResult, result)) {
revNumber = newRevNumber;
if (options.onUpdate) {
options.onUpdate(result);
}
return true;
}
return false;
}
if (options.onUpdate) {
store.on("before-update", component, () => {
const newValue = selector(store!.state, component.props!);
options.onUpdate(newValue);
});
}
store.updateFunctions[componentId].push(function (): boolean {
return selectCompareUpdate(store!.state, component.props);
});
@@ -133,6 +140,9 @@ export function useStore(selector, options: SelectorOptions = {}): any {
const __destroy = component.__destroy;
component.__destroy = (parent) => {
delete store.updateFunctions[componentId];
if (options.onUpdate) {
store.off("before-update", component);
}
__destroy.call(component, parent);
};
@@ -20,7 +20,7 @@ exports[`class and style attributes with t-component dynamic t-att-style is prop
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined).then(()=>{if (w2.__owl__.isDestroyed) {return};w2.el.style=_4;});;
w2.__updateProps(props2, extra.fiber, undefined).then(()=>{if (w2.__owl__.status === 5) {return};w2.el.style=_4;});;
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
@@ -444,7 +444,7 @@ exports[`composition t-ref on a node, and t-on-click 2`] = `
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('click', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);});}});});
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('click', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -579,7 +579,7 @@ exports[`other directives with t-component t-on expression captured in t-foreach
c6.push(vn7);
const otherState_8 = scope['otherState'];
const iter_8 = scope.iter;
p7.on['click'] = function (e) {if (!context.__owl__.isMounted){return}otherState_8.vals.push(iter_8+'_'+iter_8)};
p7.on['click'] = function (e) {if (context.__owl__.status === 5){return}otherState_8.vals.push(iter_8+'_'+iter_8)};
c7.push({text: \`expr\`});
utils.getScope(scope, 'iter').iter = scope.iter+1;
}
@@ -630,7 +630,7 @@ exports[`other directives with t-component t-on expression in t-foreach 1`] = `
c6.push(vn9);
const otherState_10 = scope['otherState'];
const val_10 = scope['val'];
p9.on['click'] = function (e) {if (!context.__owl__.isMounted){return}otherState_10.vals.push(val_10)};
p9.on['click'] = function (e) {if (context.__owl__.status === 5){return}otherState_10.vals.push(val_10)};
c9.push({text: \`Expr\`});
}
scope = _origScope5;
@@ -684,7 +684,7 @@ exports[`other directives with t-component t-on expression in t-foreach with t-s
const otherState_10 = scope['otherState'];
const val_10 = scope['val'];
const bossa_10 = scope.bossa;
p9.on['click'] = function (e) {if (!context.__owl__.isMounted){return}otherState_10.vals.push(val_10+'_'+bossa_10)};
p9.on['click'] = function (e) {if (context.__owl__.status === 5){return}otherState_10.vals.push(val_10+'_'+bossa_10)};
c9.push({text: \`Expr\`});
}
scope = _origScope5;
@@ -734,7 +734,7 @@ exports[`other directives with t-component t-on method call in t-foreach 1`] = `
let vn9 = h('button', p9, c9);
c6.push(vn9);
let args10 = [scope['val']];
p9.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['addVal'](...args10, e);};
p9.on['click'] = function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['addVal'](...args10, e);};
c9.push({text: \`meth call\`});
}
scope = _origScope5;
@@ -770,7 +770,7 @@ exports[`other directives with t-component t-on with .capture modifier 1`] = `
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('click', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['capture'](e);}, true);}});});
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('click', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['capture'](e);}, true);}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -812,7 +812,7 @@ exports[`other directives with t-component t-on with getter as handler 1`] = `
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap['__4__'] = w3.__owl__.id;
let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['handler'](e);});}});});
let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['handler'](e);});}});});
let pvnode = h('dummy', {key: '__4__', hook: {remove() {},destroy(vn) {w3.destroy();}}});
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
@@ -851,7 +851,7 @@ exports[`other directives with t-component t-on with handler bound to argument 1
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onEv'](...args4, e);});}});});
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onEv'](...args4, e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -890,7 +890,7 @@ exports[`other directives with t-component t-on with handler bound to empty obje
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onEv'](...args4, e);});}});});
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onEv'](...args4, e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -929,7 +929,7 @@ exports[`other directives with t-component t-on with handler bound to empty obje
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onEv'](...args4, e);});}});});
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onEv'](...args4, e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -968,7 +968,7 @@ exports[`other directives with t-component t-on with handler bound to object 1`]
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onEv'](...args4, e);});}});});
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onEv'](...args4, e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -1011,7 +1011,7 @@ exports[`other directives with t-component t-on with inline statement 1`] = `
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap['__4__'] = w3.__owl__.id;
let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}state_5.counter++});}});});
let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}state_5.counter++});}});});
let pvnode = h('dummy', {key: '__4__', hook: {remove() {},destroy(vn) {w3.destroy();}}});
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
@@ -1049,7 +1049,7 @@ exports[`other directives with t-component t-on with no handler (only modifiers)
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onEv'](e);});}});});
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onEv'](e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -1087,7 +1087,7 @@ exports[`other directives with t-component t-on with prevent and self modifiers
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}e.preventDefault();if (e.target !== vn.elm) {return}utils.getComponent(context)['onEv'](e);});}});});
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}e.preventDefault();if (e.target !== vn.elm) {return}utils.getComponent(context)['onEv'](e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -1125,7 +1125,7 @@ exports[`other directives with t-component t-on with self and prevent modifiers
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}if (e.target !== vn.elm) {return}e.preventDefault();utils.getComponent(context)['onEv'](e);});}});});
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}if (e.target !== vn.elm) {return}e.preventDefault();utils.getComponent(context)['onEv'](e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -1163,7 +1163,7 @@ exports[`other directives with t-component t-on with self modifier 1`] = `
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onEv1'](e);});vn.elm.addEventListener('ev-2', function (e) {if (!context.__owl__.isMounted){return}if (e.target !== vn.elm) {return}utils.getComponent(context)['onEv2'](e);});}});});
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onEv1'](e);});vn.elm.addEventListener('ev-2', function (e) {if (context.__owl__.status === 5){return}if (e.target !== vn.elm) {return}utils.getComponent(context)['onEv2'](e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -1201,7 +1201,7 @@ exports[`other directives with t-component t-on with stop and/or prevent modifie
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {if (!context.__owl__.isMounted){return}e.stopPropagation();utils.getComponent(context)['onEv1'](e);});vn.elm.addEventListener('ev-2', function (e) {if (!context.__owl__.isMounted){return}e.preventDefault();utils.getComponent(context)['onEv2'](e);});vn.elm.addEventListener('ev-3', function (e) {if (!context.__owl__.isMounted){return}e.stopPropagation();e.preventDefault();utils.getComponent(context)['onEv3'](e);});}});});
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {if (context.__owl__.status === 5){return}e.stopPropagation();utils.getComponent(context)['onEv1'](e);});vn.elm.addEventListener('ev-2', function (e) {if (context.__owl__.status === 5){return}e.preventDefault();utils.getComponent(context)['onEv2'](e);});vn.elm.addEventListener('ev-3', function (e) {if (context.__owl__.status === 5){return}e.stopPropagation();e.preventDefault();utils.getComponent(context)['onEv3'](e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -1555,7 +1555,7 @@ exports[`random stuff/miscellaneous t-on with handler bound to dynamic argument
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
w6 = new W6(parent, props6);
parent.__owl__.cmap[k7] = w6.__owl__.id;
let fiber = w6.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onEv'](...args8, e);});}});});
let fiber = w6.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onEv'](...args8, e);});}});});
let pvnode = h('dummy', {key: k7, hook: {remove() {},destroy(vn) {w6.destroy();}}});
c1.push(pvnode);
w6.__owl__.pvnode = pvnode;
@@ -1580,7 +1580,7 @@ exports[`t-call handlers are properly bound through a t-call 1`] = `
let vn3 = h('p', p3, c3);
c2.push(vn3);
let k4 = \`click__4__\${key0}__\`;
extra.handlers[k4] = extra.handlers[k4] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['update'](e);};
extra.handlers[k4] = extra.handlers[k4] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['update'](e);};
p3.on['click'] = extra.handlers[k4];
c3.push({text: \`lucas\`});
}"
@@ -1599,7 +1599,7 @@ exports[`t-call handlers with arguments are properly bound through a t-call 1`]
let vn3 = h('p', p3, c3);
c2.push(vn3);
let args4 = [scope['a']];
p3.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['update'](...args4, e);};
p3.on['click'] = function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['update'](...args4, e);};
c3.push({text: \`lucas\`});
}"
`;
@@ -229,7 +229,7 @@ exports[`t-slot directive dynamic t-slot call 1`] = `
let h = this.h;
let c10 = [], p10 = {key:10,on:{}};
let vn10 = h('button', p10, c10);
extra.handlers['click__11__'] = extra.handlers['click__11__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['toggle'](e);};
extra.handlers['click__11__'] = extra.handlers['click__11__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['toggle'](e);};
p10.on['click'] = extra.handlers['click__11__'];
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + (scope['current'].slot)];
if (slot12) {
@@ -301,7 +301,7 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
let c9 = [], p9 = {key:9,on:{}};
let vn9 = h('button', p9, c9);
c8.push(vn9);
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](e);};
p9.on['click'] = extra.handlers['click__10__'];
const ref11 = \`myButton\`;
p9.hook = {
@@ -326,7 +326,7 @@ exports[`t-slot directive slots are rendered with proper context 1`] = `
let c9 = [], p9 = {key:9,on:{}};
let vn9 = h('button', p9, c9);
c8.push(vn9);
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](e);};
p9.on['click'] = extra.handlers['click__10__'];
c9.push({text: \`do something\`});
}"
+161 -5
View File
@@ -1,4 +1,4 @@
import { Component, Env } from "../../src/component/component";
import { Component, Env, STATUS } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeDeferred, makeTestEnv, makeTestFixture, nextMicroTick, nextTick } from "../helpers";
@@ -40,14 +40,14 @@ describe("async rendering", () => {
}
}
const w = new W();
expect(w.__owl__.status).toBe(STATUS.CREATED);
w.mount(fixture);
expect(w.__owl__.isDestroyed).toBe(false);
expect(w.__owl__.isMounted).toBe(false);
expect(w.__owl__.status).toBe(STATUS.WILLSTARTED);
w.destroy();
expect(w.__owl__.status).toBe(STATUS.DESTROYED);
def.resolve();
await nextTick();
expect(w.__owl__.isDestroyed).toBe(true);
expect(w.__owl__.isMounted).toBe(false);
expect(w.__owl__.status).toBe(STATUS.DESTROYED);
});
test("destroying/recreating a subwidget with different props (if start is not over)", async () => {
@@ -1405,4 +1405,160 @@ describe("async rendering", () => {
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(Widget.prototype.__render).toHaveBeenCalledTimes(2);
});
test("components with shouldUpdate=false", async () => {
const state = { p: 1, cc: 10 };
class ChildChild extends Component {
static template = xml`
<div>
child child: <t t-esc="state.cc"/>
</div>`;
state = state;
shouldUpdate() {
return false;
}
}
class Child extends Component {
static components = { ChildChild };
static template = xml`
<div>
child
<ChildChild/>
</div>`;
shouldUpdate() {
return false;
}
}
let parent: any;
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
parent: <t t-esc="state.p"/>
<Child/>
</div>`;
state = state;
constructor(a, b) {
super(a, b);
parent = this;
}
shouldUpdate() {
return false;
}
}
class App extends Component {
static components = { Parent };
static template = xml`
<div>
<Parent/>
</div>`;
}
var div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div></div><div><div> parent: 1<div> child <div> child child: 10</div></div></div></div>"
);
app.mount(div);
// wait for rendering from second mount to go through parent
await Promise.resolve();
await Promise.resolve();
state.cc++;
state.p++;
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><div> parent: 2<div> child <div> child child: 11</div></div></div></div></div>"
);
});
test("components with shouldUpdate=false, part 2", async () => {
const state = { p: 1, cc: 10 };
let shouldUpdate = true;
class ChildChild extends Component {
static template = xml`
<div>
child child: <t t-esc="state.cc"/>
</div>`;
state = state;
shouldUpdate() {
return shouldUpdate;
}
}
class Child extends Component {
static components = { ChildChild };
static template = xml`
<div>
child
<ChildChild/>
</div>`;
shouldUpdate() {
return shouldUpdate;
}
}
let parent: any;
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
parent: <t t-esc="state.p"/>
<Child/>
</div>`;
state = state;
constructor(a, b) {
super(a, b);
parent = this;
}
shouldUpdate() {
return shouldUpdate;
}
}
class App extends Component {
static components = { Parent };
static template = xml`
<div>
<Parent/>
</div>`;
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div> parent: 1<div> child <div> child child: 10</div></div></div></div>"
);
state.cc++;
state.p++;
app.render();
// wait for rendering to go through child
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
shouldUpdate = false;
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div> parent: 2<div> child <div> child child: 11</div></div></div></div>"
);
});
});
+105 -28
View File
@@ -1,4 +1,4 @@
import { Component, Env, mount } from "../../src/component/component";
import { Component, Env, mount, STATUS } from "../../src/component/component";
import { EventBus } from "../../src/core/event_bus";
import { useRef, useState } from "../../src/hooks";
import { QWeb } from "../../src/qweb/qweb";
@@ -163,7 +163,7 @@ describe("basic widget properties", () => {
expect(fixture.innerHTML).toBe("<div><span></span></div>");
});
test("cannot be clicked on and updated if not in DOM", async () => {
test("can be clicked on and updated if not in DOM", async () => {
class Counter extends Component {
static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
@@ -178,8 +178,8 @@ describe("basic widget properties", () => {
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
button.click();
await nextTick();
expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>");
expect(counter.state.counter).toBe(0);
expect(target.innerHTML).toBe("<div>1<button>Inc</button></div>");
expect(counter.state.counter).toBe(1);
});
test("widget style and classname", async () => {
@@ -661,9 +661,8 @@ describe("lifecycle hooks", () => {
class ChildWidget extends Component {
static template = xml`<div/>`;
constructor(parent) {
super(parent);
steps.push("init");
setup() {
steps.push("setup");
}
async willStart() {
steps.push("willstart");
@@ -687,10 +686,10 @@ describe("lifecycle hooks", () => {
const widget = new ParentWidget();
await widget.mount(fixture);
expect(steps).toEqual(["init", "willstart", "mounted"]);
expect(steps).toEqual(["setup", "willstart", "mounted"]);
widget.state.ok = false;
await nextTick();
expect(steps).toEqual(["init", "willstart", "mounted", "willunmount"]);
expect(steps).toEqual(["setup", "willstart", "mounted", "willunmount"]);
});
test("components are unmounted and destroyed if no longer in DOM, even after updateprops", async () => {
@@ -738,8 +737,7 @@ describe("lifecycle hooks", () => {
class ChildWidget extends Component {
static template = xml`<div/>`;
constructor(parent) {
super(parent);
setup() {
steps.push("c init");
}
async willStart() {
@@ -755,8 +753,7 @@ describe("lifecycle hooks", () => {
class ParentWidget extends Component {
static template = xml`<div><t t-component="child"/></div>`;
static components = { child: ChildWidget };
constructor(parent?) {
super(parent);
setup() {
steps.push("p init");
}
async willStart() {
@@ -962,6 +959,69 @@ describe("lifecycle hooks", () => {
"parent:patched",
]);
});
test("willPatch/patched hook is not called if not mounted in DOM", async () => {
const steps: string[] = [];
class ChildWidget extends Component {
static template = xml`<div/>`;
constructor(parent, props) {
super(parent, props);
steps.push("child:constructor");
}
mounted() {
steps.push("child:mounted");
}
willPatch() {
steps.push("child:willPatch");
}
patched() {
steps.push("child:patched");
}
}
class ParentWidget extends Component {
static template = xml`
<div>
<t t-component="child" v="state.n"/>
</div>
`;
static components = { child: ChildWidget };
state = useState({ n: 1 });
constructor() {
super();
steps.push("parent:constructor");
}
mounted() {
steps.push("parent:mounted");
}
willPatch() {
steps.push("parent:willPatch");
}
patched() {
steps.push("parent:patched");
}
}
const div = document.createElement("div");
const widget = new ParentWidget();
await widget.mount(div);
expect(steps).toEqual(["parent:constructor", "child:constructor"]);
widget.state.n = 2;
await nextTick();
expect(steps).toEqual(["parent:constructor", "child:constructor"]);
// then we remount the component in the dom
await widget.mount(fixture);
expect(steps).toEqual([
"parent:constructor",
"child:constructor",
"child:mounted",
"parent:mounted",
]);
});
});
describe("destroy method", () => {
@@ -975,8 +1035,7 @@ describe("destroy method", () => {
expect(document.contains(widget.el)).toBe(true);
widget.destroy();
expect(document.contains(widget.el)).toBe(false);
expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(true);
expect(widget.__owl__.status).toBe(STATUS.DESTROYED);
});
test("destroying a parent also destroys its children", async () => {
@@ -985,9 +1044,9 @@ describe("destroy method", () => {
const child = children(parent)[0];
expect(child.__owl__.isDestroyed).toBe(false);
expect(child.__owl__.status).toBe(STATUS.MOUNTED);
parent.destroy();
expect(child.__owl__.isDestroyed).toBe(true);
expect(child.__owl__.status).toBe(STATUS.DESTROYED);
});
test("destroy remove the parent/children link", async () => {
@@ -1013,17 +1072,15 @@ describe("destroy method", () => {
}
expect(fixture.innerHTML).toBe("");
const widget = new DelayedWidget();
expect(widget.__owl__.status).toBe(STATUS.CREATED);
widget.mount(fixture);
expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(false);
expect(widget.__owl__.status).toBe(STATUS.WILLSTARTED);
widget.destroy();
expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(true);
expect(widget.__owl__.status).toBe(STATUS.DESTROYED);
def.resolve();
await nextTick();
expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(true);
expect(widget.__owl__.status).toBe(STATUS.DESTROYED);
expect(widget.__owl__.vnode).toBe(undefined);
expect(fixture.innerHTML).toBe("");
expect(isRendered).toBe(false);
@@ -1479,7 +1536,7 @@ describe("composition", () => {
parent.state.flag = true;
await nextTick();
expect(children(parent)[0]).toBe(child);
expect(child.__owl__.isDestroyed).toBe(false);
expect(child.__owl__.status).toBe(STATUS.MOUNTED);
expect(normalize(fixture.innerHTML)).toBe(
normalize(`
<div>
@@ -2227,9 +2284,29 @@ describe("other directives with t-component", () => {
el.click();
expect(steps).toEqual(["click"]);
parent.unmount();
expect(child.__owl__.isMounted).toBe(false);
expect(child.__owl__.status).toBe(STATUS.UNMOUNTED);
el.click();
expect(steps).toEqual(["click"]);
expect(steps).toEqual(["click", "click"]);
});
test("triggering custom event on mounted components", async () => {
let value = false;
class Child extends Component {
static template = xml`<div/>`;
mounted() {
this.trigger("coucou");
}
}
class Parent extends Component {
static template = xml`<Child t-on-coucou="doSomething"/>`;
static components = { Child };
doSomething() {
value = true;
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(value).toBe(true);
});
test("t-on with .capture modifier", async () => {
@@ -2281,7 +2358,7 @@ describe("other directives with t-component", () => {
expect(steps).toEqual(["click"]);
parent.state.flag = false;
await nextTick();
expect(child.__owl__.isDestroyed).toBe(true);
expect(child.__owl__.status).toBe(STATUS.DESTROYED);
el.click();
expect(steps).toEqual(["click"]);
});
@@ -2316,7 +2393,7 @@ describe("other directives with t-component", () => {
expect(steps).toEqual(["click"]);
parent.state.flag = false;
await nextTick();
expect(child.__owl__.isDestroyed).toBe(true);
expect(child.__owl__.status).toBe(STATUS.DESTROYED);
el.click();
expect(steps).toEqual(["click"]);
});
+2 -2
View File
@@ -1,4 +1,4 @@
import { Component, Env } from "../../src/component/component";
import { Component, Env, STATUS } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
@@ -108,7 +108,7 @@ describe("component error handling (catchError)", () => {
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(app.__owl__.isDestroyed).toBe(true);
expect(app.__owl__.status).toBe(STATUS.DESTROYED);
expect(handler).toBeCalledTimes(1);
});
+133 -4
View File
@@ -323,6 +323,54 @@ describe("unmounting and remounting", () => {
expect(steps).toEqual([2, 2, 3]);
});
test("change state and render while mounted in detached dom", async () => {
class App extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
state = useState({ val: 1 });
}
const detachedDiv = document.createElement("div");
const app = await mount(App, { target: detachedDiv });
expect(detachedDiv.innerHTML).toBe("<div>1</div>");
app.state.val = 2;
await nextTick();
expect(detachedDiv.innerHTML).toBe("<div>2</div>");
});
test("change state and render while not mounted ", async () => {
class App extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
state = useState({ val: 1 });
}
const app = new App(null);
app.state.val = 2; // will call the render method (before being mounted)
await nextTick();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>2</div>");
});
test("destroy and change state after mounted in detached dom", async () => {
class App extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
state = useState({ val: 1 });
}
const detachedDiv = document.createElement("div");
const app = await mount(App, { target: detachedDiv });
expect(detachedDiv.innerHTML).toBe("<div>1</div>");
app.destroy();
app.state.val = 2;
await nextTick();
expect(detachedDiv.innerHTML).toBe("");
});
test("change state while component is unmounted", async () => {
let child;
class Child extends Component {
@@ -356,6 +404,40 @@ describe("unmounting and remounting", () => {
expect(fixture.innerHTML).toBe("<div>P2<span>C2</span></div>");
});
test("change state while component is mounted in a fragment", async () => {
class Child1 extends Component {
static template = xml`<span>C1</span>`;
}
class Child2 extends Component {
static template = xml`<span>C2</span>`;
}
class Parent extends Component {
static components = { Child1, Child2 };
static template = xml`
<div>
<Child1 t-if="child == 'c1'"/>
<Child2 t-if="child == 'c2'"/>
</div>`;
child: string | false = false;
}
const fragment = document.createDocumentFragment();
const parent = new Parent();
await parent.mount(fragment);
expect(parent.el.outerHTML).toBe("<div></div>");
parent.child = "c1";
parent.render();
await Promise.resolve();
parent.child = "c2";
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>C2</span></div>");
});
test("unmount component during a re-rendering", async () => {
const def = makeDeferred();
class Child extends Component {
@@ -442,17 +524,17 @@ describe("unmounting and remounting", () => {
// one full tick.
await nextMicroTick();
await nextMicroTick();
expect(steps).toEqual(["1 catch"]);
expect(steps).toEqual([]);
await nextTick();
expect(fixture.innerHTML).toBe("<div></div><span></span>");
def.resolve();
await nextTick();
expect(steps).toEqual(["1 catch", "2 resolved"]);
expect(steps).toEqual(["2 resolved"]);
expect(fixture.innerHTML).toBe("<div></div><span><div>Hey</div></span>");
});
test("widget can be mounted on same target, another situation", async () => {
test("component can be mounted on same target, another situation", async () => {
const def = makeDeferred();
const steps: string[] = [];
@@ -480,8 +562,8 @@ describe("unmounting and remounting", () => {
def.resolve();
await nextTick();
expect(steps).toEqual(["1 resolved", "2 resolved"]);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["1 resolved", "2 resolved"]);
});
test("mounting a destroyed widget", async () => {
@@ -600,4 +682,51 @@ describe("unmounting and remounting", () => {
await parent.render();
expect(fixture.textContent).toBe("fixedsome text");
});
test("remounting component tree where a component implement shouldupdate", async () => {
let state: any;
const steps = [];
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/></div>`;
state = useState({ word: "hello" });
constructor(parent, props) {
super(parent, props);
state = this.state;
}
patched() {
steps.push("patched");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willUnmount");
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
}
const parent = await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
parent.unmount();
expect(fixture.innerHTML).toBe("");
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
state.word = "test";
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld</div></div>");
expect(steps).toEqual(["mounted", "willUnmount", "mounted", "patched"]);
});
});
+20 -1
View File
@@ -289,7 +289,26 @@ describe("Context", () => {
expect(testContext.subscriptions.update.length).toBe(0);
});
test("concurrent renderings", async () => {
test.skip("concurrent renderings", async () => {
/**
* Note: this test is interesting, but sadly just an incomplete attempt at
* protecting users against themselves. With the context API, it is not
* possible for the framework to protect completely against crashes. Maybe
* like in this case, when a component is in a simple hierarchy where all
* renderings come from the context changes, but in a real case, where some
* code can trigger a rendering independently, it is insufficient.
*
* The main problem is that the sub component depends on some external state,
* which may be modified, and then incompatible with the component actual
* state (for example, if the sub component has an id key related to some
* object that has been removed from the context).
*
* For now, sadly, the only solution is that components that depends on external
* state should guarantee their own integrity themselves. Then maybe this
* could be solved at the level of a state management solution that has a
* more advanced API, to let components determine if they should be updated
* or not (so, something slightly more advanced that the useStore hook).
*/
const testContext = new Context({ x: { n: 1 }, key: "x" });
const def = makeDeferred();
let stateC;
+2 -2
View File
@@ -1,4 +1,4 @@
import { Env, Component } from "../src/component/component";
import { Env, Component, STATUS } from "../src/component/component";
import { scheduler } from "../src/component/scheduler";
import { EvalContext, QWeb } from "../src/qweb/qweb";
import { CompilationContext } from "../src/qweb/compilation_context";
@@ -92,7 +92,7 @@ export function renderToDOM(
if (!context.__owl__) {
// we add `__owl__` to better simulate a component as context. This is
// particularly important for event handlers added with the `t-on` directive.
context.__owl__ = { isMounted: true };
context.__owl__ = { status: STATUS.MOUNTED };
}
const vnode = qweb.render(template, context, extra);
+28
View File
@@ -9,8 +9,10 @@ import {
onWillPatch,
onWillStart,
onWillUpdateProps,
useEnv,
useSubEnv,
useExternalListener,
useComponent,
} from "../src/hooks";
import { xml } from "../src/tags";
@@ -520,6 +522,19 @@ describe("hooks", () => {
});
});
test("can use useEnv", async () => {
expect.assertions(1);
class TestComponent extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor() {
super();
expect(useEnv()).toBe(env);
}
}
const component = new TestComponent();
await component.mount(fixture);
});
test("can use sub env", async () => {
class TestComponent extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
@@ -535,6 +550,19 @@ describe("hooks", () => {
expect(component.env).toHaveProperty("val");
});
test("can use useComponent", async () => {
expect.assertions(1);
class TestComponent extends Component {
static template = xml`<div></div>`;
constructor() {
super();
expect(useComponent()).toBe(this);
}
}
const component = new TestComponent();
await component.mount(fixture);
});
test("parent and child env", async () => {
class Child extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
+38 -26
View File
@@ -2727,7 +2727,7 @@ exports[`t-on can bind event handler 1`] = `
let h = this.h;
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1);
extra.handlers['click__2__'] = extra.handlers['click__2__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['add'](e);};
extra.handlers['click__2__'] = extra.handlers['click__2__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['add'](e);};
p1.on['click'] = extra.handlers['click__2__'];
c1.push({text: \`Click\`});
return vn1;
@@ -2744,7 +2744,7 @@ exports[`t-on can bind handlers with arguments 1`] = `
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1);
let args2 = [5];
p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['add'](...args2, e);};
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['add'](...args2, e);};
c1.push({text: \`Click\`});
return vn1;
}"
@@ -2760,7 +2760,7 @@ exports[`t-on can bind handlers with empty object (with non empty inner string)
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1);
let args2 = [{}];
p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](...args2, e);};
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](...args2, e);};
c1.push({text: \`Click\`});
return vn1;
}"
@@ -2776,7 +2776,7 @@ exports[`t-on can bind handlers with empty object 1`] = `
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1);
let args2 = [{}];
p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](...args2, e);};
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](...args2, e);};
c1.push({text: \`Click\`});
return vn1;
}"
@@ -2815,7 +2815,7 @@ exports[`t-on can bind handlers with loop variable as argument 1`] = `
let vn7 = h('a', p7, c7);
c6.push(vn7);
let args8 = [scope['action']];
p7.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['activate'](...args8, e);};
p7.on['click'] = function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['activate'](...args8, e);};
c7.push({text: \`link\`});
}
scope = _origScope5;
@@ -2833,7 +2833,7 @@ exports[`t-on can bind handlers with object arguments 1`] = `
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1);
let args2 = [{val:5}];
p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['add'](...args2, e);};
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['add'](...args2, e);};
c1.push({text: \`Click\`});
return vn1;
}"
@@ -2847,9 +2847,9 @@ exports[`t-on can bind two event handlers 1`] = `
let h = this.h;
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1);
extra.handlers['click__2__'] = extra.handlers['click__2__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['handleClick'](e);};
extra.handlers['click__2__'] = extra.handlers['click__2__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['handleClick'](e);};
p1.on['click'] = extra.handlers['click__2__'];
extra.handlers['dblclick__3__'] = extra.handlers['dblclick__3__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['handleDblClick'](e);};
extra.handlers['dblclick__3__'] = extra.handlers['dblclick__3__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['handleDblClick'](e);};
p1.on['dblclick'] = extra.handlers['dblclick__3__'];
c1.push({text: \`Click\`});
return vn1;
@@ -2864,7 +2864,7 @@ exports[`t-on handler is bound to proper owner 1`] = `
let h = this.h;
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1);
extra.handlers['click__2__'] = extra.handlers['click__2__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['add'](e);};
extra.handlers['click__2__'] = extra.handlers['click__2__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['add'](e);};
p1.on['click'] = extra.handlers['click__2__'];
c1.push({text: \`Click\`});
return vn1;
@@ -2883,7 +2883,7 @@ exports[`t-on t-on combined with t-esc 1`] = `
let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2);
c1.push(vn2);
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onClick'](e);};
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onClick'](e);};
p2.on['click'] = extra.handlers['click__3__'];
let _4 = scope['text'];
if (_4 != null) {
@@ -2905,7 +2905,7 @@ exports[`t-on t-on combined with t-raw 1`] = `
let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2);
c1.push(vn2);
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onClick'](e);};
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onClick'](e);};
p2.on['click'] = extra.handlers['click__3__'];
let _4 = scope['html'];
if (_4 != null) {
@@ -2923,12 +2923,12 @@ exports[`t-on t-on with .capture modifier 1`] = `
let h = this.h;
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('div', p1, c1);
extra.handlers['!click__2__'] = extra.handlers['!click__2__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onCapture'](e);};
extra.handlers['!click__2__'] = extra.handlers['!click__2__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onCapture'](e);};
p1.on['!click'] = extra.handlers['!click__2__'];
let c3 = [], p3 = {key:3,on:{}};
let vn3 = h('button', p3, c3);
c1.push(vn3);
extra.handlers['click__4__'] = extra.handlers['click__4__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
extra.handlers['click__4__'] = extra.handlers['click__4__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](e);};
p3.on['click'] = extra.handlers['click__4__'];
c3.push({text: \`Button\`});
return vn1;
@@ -2946,7 +2946,7 @@ exports[`t-on t-on with empty handler (only modifiers) 1`] = `
let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2);
c1.push(vn2);
p2.on['click'] = function (e) {if (!context.__owl__.isMounted){return}e.preventDefault();};
p2.on['click'] = function (e) {if (context.__owl__.status === 5){return}e.preventDefault();};
c2.push({text: \`Button\`});
return vn1;
}"
@@ -2961,7 +2961,7 @@ exports[`t-on t-on with inline statement (function call) 1`] = `
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1);
const state_2 = scope['state'];
p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}state_2.incrementCounter(2)};
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}state_2.incrementCounter(2)};
c1.push({text: \`Click\`});
return vn1;
}"
@@ -2976,7 +2976,7 @@ exports[`t-on t-on with inline statement 1`] = `
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1);
const state_2 = scope['state'];
p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}state_2.counter++};
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}state_2.counter++};
c1.push({text: \`Click\`});
return vn1;
}"
@@ -2991,7 +2991,7 @@ exports[`t-on t-on with inline statement, part 2 1`] = `
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1);
const state_2 = scope['state'];
p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}state_2.flag=!state_2.flag};
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}state_2.flag=!state_2.flag};
c1.push({text: \`Toggle\`});
return vn1;
}"
@@ -3007,7 +3007,7 @@ exports[`t-on t-on with inline statement, part 3 1`] = `
let vn1 = h('button', p1, c1);
const state_2 = scope['state'];
const someFunction_2 = scope['someFunction'];
p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}state_2.n=someFunction_2(3)};
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}state_2.n=someFunction_2(3)};
c1.push({text: \`Toggle\`});
return vn1;
}"
@@ -3024,7 +3024,7 @@ exports[`t-on t-on with prevent and self modifiers (order matters) 1`] = `
let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2);
c1.push(vn2);
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (!context.__owl__.isMounted){return}e.preventDefault();if (e.target !== this.elm) {return}utils.getComponent(context)['onClick'](e);};
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (context.__owl__.status === 5){return}e.preventDefault();if (e.target !== this.elm) {return}utils.getComponent(context)['onClick'](e);};
p2.on['click'] = extra.handlers['click__3__'];
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
@@ -3045,19 +3045,19 @@ exports[`t-on t-on with prevent and/or stop modifiers 1`] = `
let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2);
c1.push(vn2);
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (!context.__owl__.isMounted){return}e.preventDefault();utils.getComponent(context)['onClickPrevented'](e);};
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (context.__owl__.status === 5){return}e.preventDefault();utils.getComponent(context)['onClickPrevented'](e);};
p2.on['click'] = extra.handlers['click__3__'];
c2.push({text: \`Button 1\`});
let c4 = [], p4 = {key:4,on:{}};
let vn4 = h('button', p4, c4);
c1.push(vn4);
extra.handlers['click__5__'] = extra.handlers['click__5__'] || function (e) {if (!context.__owl__.isMounted){return}e.stopPropagation();utils.getComponent(context)['onClickStopped'](e);};
extra.handlers['click__5__'] = extra.handlers['click__5__'] || function (e) {if (context.__owl__.status === 5){return}e.stopPropagation();utils.getComponent(context)['onClickStopped'](e);};
p4.on['click'] = extra.handlers['click__5__'];
c4.push({text: \`Button 2\`});
let c6 = [], p6 = {key:6,on:{}};
let vn6 = h('button', p6, c6);
c1.push(vn6);
extra.handlers['click__7__'] = extra.handlers['click__7__'] || function (e) {if (!context.__owl__.isMounted){return}e.preventDefault();e.stopPropagation();utils.getComponent(context)['onClickPreventedAndStopped'](e);};
extra.handlers['click__7__'] = extra.handlers['click__7__'] || function (e) {if (context.__owl__.status === 5){return}e.preventDefault();e.stopPropagation();utils.getComponent(context)['onClickPreventedAndStopped'](e);};
p6.on['click'] = extra.handlers['click__7__'];
c6.push({text: \`Button 3\`});
return vn1;
@@ -3097,7 +3097,7 @@ exports[`t-on t-on with prevent modifier in t-foreach 1`] = `
let vn7 = h('a', p7, c7);
c1.push(vn7);
let args8 = [scope['project'].id];
p7.on['click'] = function (e) {if (!context.__owl__.isMounted){return}e.preventDefault();utils.getComponent(context)['onEdit'](...args8, e);};
p7.on['click'] = function (e) {if (context.__owl__.status === 5){return}e.preventDefault();utils.getComponent(context)['onEdit'](...args8, e);};
c7.push({text: \` Edit \`});
let _9 = scope['project'].name;
if (_9 != null) {
@@ -3121,7 +3121,7 @@ exports[`t-on t-on with self and prevent modifiers (order matters) 1`] = `
let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2);
c1.push(vn2);
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (!context.__owl__.isMounted){return}if (e.target !== this.elm) {return}e.preventDefault();utils.getComponent(context)['onClick'](e);};
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (context.__owl__.status === 5){return}if (e.target !== this.elm) {return}e.preventDefault();utils.getComponent(context)['onClick'](e);};
p2.on['click'] = extra.handlers['click__3__'];
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
@@ -3142,7 +3142,7 @@ exports[`t-on t-on with self modifier 1`] = `
let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2);
c1.push(vn2);
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onClick'](e);};
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onClick'](e);};
p2.on['click'] = extra.handlers['click__3__'];
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
@@ -3151,7 +3151,7 @@ exports[`t-on t-on with self modifier 1`] = `
let c5 = [], p5 = {key:5,on:{}};
let vn5 = h('button', p5, c5);
c1.push(vn5);
extra.handlers['click__6__'] = extra.handlers['click__6__'] || function (e) {if (!context.__owl__.isMounted){return}if (e.target !== this.elm) {return}utils.getComponent(context)['onClickSelf'](e);};
extra.handlers['click__6__'] = extra.handlers['click__6__'] || function (e) {if (context.__owl__.status === 5){return}if (e.target !== this.elm) {return}utils.getComponent(context)['onClickSelf'](e);};
p5.on['click'] = extra.handlers['click__6__'];
let c7 = [], p7 = {key:7};
let vn7 = h('span', p7, c7);
@@ -3924,6 +3924,18 @@ exports[`translation support some attributes are translated 1`] = `
}"
`;
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
c1.push({text: \` mot \`});
return vn1;
}"
`;
exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = `
"function anonymous(context, extra
) {
+11
View File
@@ -2154,6 +2154,17 @@ describe("translation support", () => {
'<div><p label="mot">mot</p><p title="mot">mot</p><p placeholder="mot">mot</p><p alt="mot">mot</p><p something="word">mot</p></div>'
);
});
test("translation is done on the trimmed text, with extra spaces readded after", () => {
const translations = {
word: "mot",
};
const translateFn = jest.fn((expr) => translations[expr] || expr);
const qweb = new QWeb({ translateFn });
qweb.addTemplate("test", "<div> word </div>");
expect(renderToString(qweb, "test")).toBe("<div> mot </div>");
expect(translateFn).toHaveBeenCalledWith("word");
});
});
describe("t-key tests", () => {
+4
View File
@@ -196,4 +196,8 @@ describe("expression evaluation", () => {
expect(compileExpr("f(...state.list)", {})).toBe("scope['f'](...scope['state'].list)");
expect(compileExpr("f([...list])", {})).toBe("scope['f']([...scope['list']])");
});
test("works with builtin properties", () => {
expect(compileExpr("state.constructor.name", {})).toBe("scope['state'].constructor.name");
});
});
+1 -1
View File
@@ -11,7 +11,7 @@ exports[`Link component can render simple cases 1`] = `
let _6 = scope['href'];
let c7 = [], p7 = {key:7,attrs:{href: _6},class:_5,on:{}};
let vn7 = h('a', p7, c7);
extra.handlers['click__8__'] = extra.handlers['click__8__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['navigate'](e);};
extra.handlers['click__8__'] = extra.handlers['click__8__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['navigate'](e);};
p7.on['click'] = extra.handlers['click__8__'];
const slot9 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot9) {
+161 -3
View File
@@ -1,4 +1,4 @@
import { Component, Env } from "../src/component/component";
import { Component, Env, mount } from "../src/component/component";
import { Store, useStore, useDispatch, useGetters, EnvWithStore } from "../src/store";
import { useState } from "../src/hooks";
import { xml } from "../src/tags";
@@ -571,12 +571,12 @@ describe("connecting a component to store", () => {
app.state.beerId = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>kwak</span></div>");
expect(counter).toBe(1);
expect(counter).toBe(0);
store.dispatch("renameBeer", { id: 2, name: "orval" });
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>orval</span></div>");
expect(counter).toBe(2);
expect(counter).toBe(1);
});
test("connected component is properly cleaned up on destroy", async () => {
@@ -1241,4 +1241,162 @@ describe("various scenarios", () => {
await nextTick();
expect(fixture.innerHTML).toMatchSnapshot();
});
test("component with store, useState and shouldUpdate=false", async () => {
let state: any;
const store = new Store({ state: { rev: 0 } });
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/></div>`;
state = useState({ word: "hello" });
constructor(parent, props) {
super(parent, props);
state = this.state;
useStore((props) => {
return 1;
});
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
constructor(parent, props) {
super(parent, props);
useStore((props) => store.state.rev);
}
}
(env as any).store = store;
await mount(Parent, { target: fixture, env });
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
store.state.rev++;
// this is the key to the bug, it makes Parent be in "render" state but not
// yet rendered while the change of state happens
await Promise.resolve();
state.word = "test";
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld</div></div>");
});
test("component with store, useState, shouldUpdate=false and child with shouldupdate false", async () => {
let state: any;
const store = new Store({ state: { rev: 0 } });
class ChildChild extends Component {
static template = xml`<div><t t-esc="props.value"/></div>`;
shouldUpdate() {
return false;
}
}
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/><ChildChild value="state.value"/></div>`;
static components = { ChildChild };
state = useState({ word: "hello", value: 3 });
constructor(parent, props) {
super(parent, props);
state = this.state;
useStore((props) => {
return 1;
});
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
constructor(parent, props) {
super(parent, props);
useStore((props) => store.state.rev);
}
}
(env as any).store = store;
await mount(Parent, { target: fixture, env });
expect(fixture.innerHTML).toBe("<div><div>helloWorld<div>3</div></div></div>");
store.state.rev++;
// this is the key to the bug, it makes Parent be in "render" state but not
// yet rendered while the change of state happens
await Promise.resolve();
state.word = "test";
state.value = 44;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld<div>3</div></div></div>");
});
test("parent/children with store, parent is remounted", async () => {
const store = new Store({ state: { a: 1, b: 1 } });
class Child extends Component {
static template = xml`<div><t t-esc="a"/></div>`;
a: any;
constructor(parent, props) {
super(parent, props);
this.a = useStore(
(state, props) => {
return state.a;
},
{
onUpdate: (a) => {
this.a = a;
},
}
);
}
}
class Parent extends Component {
static template = xml`
<div>
parent: <t t-esc="b"/>
<Child/>
</div>`;
static components = { Child };
b: any;
constructor(parent, props) {
super(parent, props);
this.b = useStore((state, props) => {
return state.b;
});
}
}
(env as any).store = store;
const div = document.createElement("div");
fixture.appendChild(div);
// initial mounting
const parent = await mount(Parent, { target: fixture, env });
expect(fixture.innerHTML).toBe("<div></div><div> parent: 1<div>1</div></div>");
// remounting component, then immediately update store.state
parent.mount(div);
store.state.a++;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div> parent: 1<div>2</div></div></div>");
});
});
+6 -2
View File
@@ -8,7 +8,7 @@ import * as owl from "../../src/index";
import { Component, Env } from "../../src/component/component";
import { xml } from "../../src/tags";
import { makeTestFixture, makeTestEnv } from "../helpers";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
let fixture: HTMLElement = makeTestFixture();
let env: Env = makeTestEnv();
@@ -31,6 +31,7 @@ test("log a specific message for render method calls if component is not mounted
parent.unmount();
parent.state.value = 2;
await nextTick();
expect(steps).toEqual([
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
"[OWL_DEBUG] Parent<id=1> mount",
@@ -40,7 +41,10 @@ test("log a specific message for render method calls if component is not mounted
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Parent<id=1> render (warning: component is not mounted, this render has no effect)",
"[OWL_DEBUG] Parent<id=1> render (warning: component is not mounted)",
"[OWL_DEBUG] scheduler: start running tasks queue",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] scheduler: stop running tasks queue",
]);
console.log = log;
});
+2 -2
View File
@@ -101,8 +101,8 @@
component.render = function(...args) {
const __owl__ = component.__owl__;
let msg = `render`;
if (!__owl__.isMounted && !__owl__.currentFiber) {
msg += ` (warning: component is not mounted, this render has no effect)`;
if (__owl__.status !== 3 /* mounted */ && !__owl__.currentFiber) {
msg += ` (warning: component is not mounted)`;
}
log(msg);
return render(...args);
+22 -41
View File
@@ -2,8 +2,7 @@ const COMPONENTS = `// In this example, we show how components can be defined an
const { Component, useState, mount } = owl;
class Greeter extends Component {
constructor() {
super(...arguments);
setup() {
this.state = useState({ word: 'Hello' });
}
@@ -14,10 +13,9 @@ class Greeter extends Component {
// Main root component
class App extends Component {
constructor() {
super(...arguments);
this.state = useState({ name: 'World'});
}
setup() {
this.state = useState({ name: 'World'});
}
}
App.components = { Greeter };
@@ -52,8 +50,7 @@ const ANIMATION = `// The goal of this component is to see how the t-transition
const { Component, useState, mount } = owl;
class Counter extends Component {
constructor() {
super(...arguments);
setup() {
this.state = useState({ value: 0 });
}
@@ -63,8 +60,7 @@ class Counter extends Component {
}
class App extends Component {
constructor() {
super(...arguments);
setup() {
this.state = useState({ flag: false, componentFlag: false, numbers: [] });
}
@@ -194,10 +190,9 @@ const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
const { Component, useState, mount } = owl;
class DemoComponent extends Component {
constructor() {
super(...arguments);
setup() {
this.state = useState({ n: 0 });
console.log("constructor");
console.log("setup");
}
async willStart() {
console.log("willstart");
@@ -223,8 +218,7 @@ class DemoComponent extends Component {
}
class App extends Component {
constructor() {
super(...arguments);
setup() {
this.state = useState({ n: 0, flag: true });
}
@@ -295,8 +289,7 @@ function useMouse() {
// Main root component
class App extends owl.Component {
constructor() {
super(...arguments);
setup() {
// simple state hook (reactive object)
this.counter = useState({ value: 0 });
@@ -333,8 +326,7 @@ const { Component, Context, mount } = owl;
const { useContext } = owl.hooks;
class ToolbarButton extends Component {
constructor() {
super(...arguments);
setup() {
this.theme = useContext(this.env.themeContext);
}
@@ -468,8 +460,7 @@ const actions = {
// TodoItem
//------------------------------------------------------------------------------
class TodoItem extends Component {
constructor() {
super(...arguments);
setup() {
useAutofocus("input");
this.state = useState({ isEditing: false });
this.dispatch = useDispatch();
@@ -499,8 +490,7 @@ class TodoItem extends Component {
// TodoApp
//------------------------------------------------------------------------------
class TodoApp extends Component {
constructor() {
super(...arguments);
setup() {
this.state = useState({ filter: "all" });
this.todos = useStore(state => state.todos);
this.dispatch = useDispatch();
@@ -1026,8 +1016,7 @@ class FormView extends owl.Component {}
FormView.components = { AdvancedComponent };
class Chatter extends owl.Component {
constructor() {
super(...arguments);
setup() {
this.messages = Array.from(Array(100).keys());
}
}
@@ -1170,8 +1159,7 @@ const SLOTS = `// We show here how slots can be used to create generic component
const { Component, useState, mount } = owl;
class Card extends Component {
constructor() {
super(...arguments);
setup() {
this.state = useState({ showContent: true });
}
@@ -1181,8 +1169,7 @@ class Card extends Component {
}
class Counter extends Component {
constructor() {
super(...arguments);
setup() {
this.state = useState({val: 1});
}
@@ -1193,8 +1180,7 @@ class Counter extends Component {
// Main root component
class App extends Component {
constructor() {
super(...arguments);
setup() {
this.state = useState({a: 1, b: 3});
}
@@ -1306,8 +1292,7 @@ class SlowComponent extends Component {
class NotificationList extends Component {}
class App extends Component {
constructor() {
super(...arguments);
setup() {
this.state = useState({ value: 0, notifs: [] });
}
@@ -1381,8 +1366,7 @@ const FORM = `// This example illustrate how the t-model directive can be used t
const { Component, useState, mount } = owl;
class Form extends Component {
constructor() {
super(...arguments);
setup() {
this.state = useState({
text: "",
othertext: "",
@@ -1556,8 +1540,7 @@ const { useRef } = owl.hooks;
class HelloWorld extends Component {}
class Counter extends Component {
constructor() {
super(...arguments);
setup() {
this.state = useState({ value: 0 });
}
@@ -1610,8 +1593,7 @@ class Window extends Component {
}
class WindowManager extends Component {
constructor() {
super(...arguments);
setup() {
this.windows = [];
this.nextId = 1;
this.currentZindex = 1;
@@ -1661,8 +1643,7 @@ class WindowManager extends Component {
WindowManager.components = { Window };
class App extends Component {
constructor() {
super(...arguments);
setup() {
this.wmRef = useRef("wm");
}