Compare commits

..

38 Commits

Author SHA1 Message Date
Géry Debongnie e1fc513249 [IMP] big change: shallow render
With this commit, component only render child
components if they have different props (shallow
equality). Otherwise, we trust the reactivity
system to make sure that all impacted components
are updated
2022-01-21 09:48:53 +01:00
Géry Debongnie 73c37c025c [REF] move useState into component_node.ts 2022-01-21 09:48:53 +01:00
Lucas Perais (lpe) b88f7a03e9 [FIX] compiler: svg in new block takes the right namespace 2022-01-21 09:32:22 +01:00
Lucas Perais (lpe) 055cbda750 [FIX] compiler: t-key on a sub-domnode pushes an anchor in parent block 2022-01-20 18:13:01 +01:00
Géry Debongnie 45f6a8e97a [REF] utils: move batched from reactivity to utils 2022-01-20 15:48:50 +01:00
Géry Debongnie 6091912c54 [IMP] component: improve error message when invalid handler 2022-01-20 15:48:50 +01:00
Géry Debongnie 586f90f6d1 [FIX] doc: fix broken link, and fix doc link checker test 2022-01-20 12:41:31 +01:00
Géry Debongnie e62a0ca2d9 [DOC] update changelog (add browser removal to changes) 2022-01-20 12:29:51 +01:00
Géry Debongnie 3a4ee160ed [FIX] playground: update window management example 2022-01-20 12:29:51 +01:00
Géry Debongnie 5f24caf40d [FIX] scheduler: make sure raf is bound to window 2022-01-20 11:06:59 +01:00
Géry Debongnie 8e36a11b6e [REF] scheduler: capture requestAnimationFrame asap
This is useful to prevent interactions with other testing code.
2022-01-20 10:19:55 +01:00
Géry Debongnie 9a54afcdf5 [FIX] playground: update example to owl 2 2022-01-19 16:50:08 +01:00
Géry Debongnie 3eefa4b153 [DOC] improve useEffect doc 2022-01-19 16:50:08 +01:00
Géry Debongnie 2ffb94ed98 [FIX] reactivity: export Reactive type 2022-01-19 16:50:08 +01:00
Géry Debongnie 6a15874d23 [FIX] typing: Component class should be generic on Props and Env
Otherwise, it prevents proper typing with typescript
2022-01-19 14:46:26 +01:00
Géry Debongnie 07d34248ae [DOC] update changelog content 2022-01-19 14:45:53 +01:00
Géry Debongnie dfc090bb12 [IMP] portal: ensure that destroy is synchronous 2022-01-19 14:11:59 +01:00
Géry Debongnie 7b3593ff8f [DOC] document synthetic events 2022-01-19 14:00:24 +01:00
Géry Debongnie b06791c950 [IMP] components: crash when using unknown suffix/modifiers 2022-01-19 14:00:24 +01:00
Lucas Perais (lpe) f7843c7c00 [FIX] component, error_handling: do not cancel the error fiber twice 2022-01-19 12:28:28 +01:00
Géry Debongnie ca9d4ea762 [IMP] doc: add changelog to doc test, update changelog 2022-01-19 11:31:53 +01:00
Géry Debongnie 7a407a762d [IMP] ci: fails if circular dependencies are found in build 2022-01-19 10:29:41 +01:00
Géry Debongnie 8847824fb7 [FIX] remove circular dependency 2022-01-19 10:03:30 +01:00
Géry Debongnie 55b068faeb [DOC] remove reference to catchError, fix mistake in changelog 2022-01-19 10:03:30 +01:00
Lucas Perais (lpe) dbee8f50d1 [FIX] components: cascading render after microtaskTick (makeChildFiber)
Co-authored-by: Samuel Degueldre <sad@odoo.com>
Co-authored-by: François Georis <fge@odoo.com>
2022-01-19 09:23:17 +01:00
Géry Debongnie f5fec02d4c [DOC] update changelog 2022-01-19 09:12:26 +01:00
Géry Debongnie 651c53ebbf [DOC] update translations page 2022-01-19 09:12:26 +01:00
Géry Debongnie 3119497508 [DOC] add more information to the slots page 2022-01-19 09:12:26 +01:00
Michael (mcm) 057e6944d6 [FIX] bind lifecycle callbacks to component
Before this commit, some of the callbacks were bound to the component
and some were not.
This commit makes all the callbacks bind to the component.
2022-01-18 15:02:07 +01:00
Géry Debongnie 83d38a6f48 [DOC] reorganize and update documentation to owl 2 2022-01-18 11:07:35 +01:00
Jorge Pinna Puissant 9577d70b4b [IMP] portal: compile t-portal in an internal Component Portal
This commit also clean-up the deepRemove for the Portal that is not
needed any more.
2022-01-17 16:04:01 +01:00
Géry Debongnie 6ff6d59895 [FIX] components: only call handlers if component is mounted 2022-01-17 15:46:18 +01:00
Géry Debongnie 7ba0c9f8ba [FIX] blockdom: toString method in multi could crash 2022-01-17 13:55:39 +01:00
Lucas Perais (lpe) 79a51f2317 [FIX] app: t_call recursive template is bound to the correct this 2022-01-17 10:46:08 +01:00
Lucas Perais (lpe) 483335df6c [FIX] compiler: slot are called with a specific and different key 2022-01-14 15:46:02 +01:00
Samuel Degueldre 287a6d5ac4 [FIX] code_generator: stop matching other variables prefix
Previously, we would replace block declarations in some contexts with
the variable alone, and decalres the variable higher in the generated
code. Issues arise because whe sometimes try to replace "let b2" with
"b2" but end up matching "let b20" which is incorrect.

This commit fixes that by adding a space to the text that we are trying
to match ("let b2 " won't match "let b20")
2022-01-14 09:58:28 +01:00
Lucas Perais (lpe) 68684a9a45 [FIX] component, fiber: subchildren should also patch and destroy their children
Have a GrandParent which controls whether one of its GrandChildren is displayed or not.
First, the GrandChild is displayed. Then, change the state of the GrandParent in order to kill
the GrandChild.

Before this commit the GrandChild is only removed from the DOM, as bdom correctly works.
But it is not destroyed.

After this commit, the GrandChild is correctly destroyed.
2022-01-14 09:51:55 +01:00
Lucas Perais (lpe) e5d773daa8 [FIX] components tests: place tests in right submodule 2022-01-14 09:51:55 +01:00
93 changed files with 4448 additions and 3705 deletions
+1
View File
@@ -25,3 +25,4 @@ jobs:
- run: npm install
- run: npm run test
- run: npm run check-formatting
- run: npm run build
+172 -19
View File
@@ -8,13 +8,16 @@ patching the `setup` method of `Component` to auto register all the lifecycle
methods as hooks). This will be done for the transition period, but will be
removed after.
## Changes
## From Owl 1.x to Owl 2.0
All changes are documented here in no particular order.
**Components**
- components can now have empty content or multiple root nodes (htmlelement or text) ([details](#31-components-can-now-have-arbitrary-content))
- new `useEffect` hook
- new `onDestroyed`, `onWillRender` and `onRendered` hooks
- breaking: component.el is removed ([details](#9-componentel-is-removed))
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
- new `onWillDestroy`, `onWillRender` and `onRendered` hooks ([doc](doc/reference/component.md#lifecycle))
- breaking: lifecycle methods are removed ([details](#1-component-lifecycle-methods-are-removed))
- breaking: can no longer be mounted on detached DOM ([details](#2-components-can-no-longer-be-mounted-in-a-detached-dom-element))
- breaking: standalone `mount` method API is simpler ([details](#4-mount-method-api-is-simpler))
@@ -22,21 +25,42 @@ removed after.
- breaking: components can no longer be unmounted/remounted ([details](#6-components-can-no-longer-be-unmountedremounted))
- breaking: template name is no longer inferred from the class name ([details](#7-template-name-is-no-longer-inferred-from-the-class-name))
- breaking: components no longer have a `shouldUpdate` method ([details](#8-components-no-longer-have-a-shouldupdate-method))
- breaking: component.el may be a text node, and is no longer `null` ([details](#9-componentel-may-be-a-text-node-and-is-no-longer-null))
- breaking: style/class on components are now regular props ([details](#10-styleclass-on-components-are-now-regular-props))
- breaking: components can no longer be mounted with position=self ([details](#11-components-can-no-longer-be-mounted-with-positionself))
- breaking: `render` method does not return a promise anymore ([details](#35-render-method-does-not-return-a-promise-anymore))
- breaking: `catchError` method is replaced by `onError` hook ([details](#36-catcherror-method-is-replaced-by-onerror-hook))
- breaking: Support for inline css (`css` tag and static `style`) has been removed ([details](#37-support-for-inline-css-css-tag-and-static-style-has-been-removed))
- new: prop validation system can now describe that additional props are allowed (with `*`) ([doc](doc/reference/props.md#props-validation))
**Templates**
- breaking: `t-foreach` should always have a corresponding `t-key` ([details](#20-t-foreach-should-always-have-a-corresponding-t-key))
- breaking: `t-ref` does not work on components ([details](#29-t-ref-does-not-work-on-component))
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
- breaking: `t-raw` directive has been removed (replaced by `t-out`) ([details](#38-t-raw-directive-has-been-removed-replaced-by-t-out))
- new: add support for synthetic events ([doc](doc/reference/event_handling.md#synthetic-events))
- breaking: style/class on components are now regular props ([details](#10-styleclass-on-components-are-now-regular-props))
- new: components can use the `.bind` suffix to bind function props ([doc](doc/reference/props.md#binding-function-props))
- breaking: `t-on` does not work on components any more ([details](#12-t-on-does-not-work-on-components-any-more))
- breaking: `t-component` no longer accepts strings ([details](#17-t-component-no-longer-accepts-strings))
**Portal**
- portals can now have arbitrary content (no longer restricted to one single child)
- breaking: does no longer transfer dom events ([details](#13-portal-does-no-longer-transfer-dom-events))
- breaking: does render as an empty text node instead of `<portal/>` ([details](#14-portal-does-render-as-an-empty-text-node-instead-of-portal))
**Slots**
- breaking: `t-set` does not define a slot any more ([details](#3-t-set-will-no-longer-work-to-define-a-slot))
- slots capabilities have been improved ([doc](doc/reference/slots.md))
- params can be give to slot content (to pass information from slot owner to slot user)
- slots are given as a `prop` (and can be manipulated/propagated to sub components )
- slots can define scopes (to pass information from slot user to slot owner)
**Portal**
- Portal are now defined with `t-portal` ([details](#33-portal-are-now-defined-with-t-portal))
- portals can now have arbitrary content (no longer restricted to one single child)
- breaking: does no longer transfer dom events ([details](#13-portal-does-no-longer-transfer-dom-events))
- breaking: does render as an empty text node instead of `<portal/>` ([details](#14-portal-does-render-as-an-empty-text-node-instead-of-portal))
**Miscellaneous**
@@ -44,13 +68,13 @@ removed after.
- much simpler code
- finer grained reactivity: owl 2 tracks change per key/component
- finer grained reactivity: sub components can reobserve state
- new App class to encapsulate a root Owl component (with the config for that application)
- new App class to encapsulate a root Owl component (with the config for that application) ([doc](doc/reference/app.md))
- new `Memo` component
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
- breaking: `Context` is removed ([details](#15-context-is-removed))
- breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty))
- breaking: most exports are exported at top level ([details](#18-most-exports-are-exported-at-top-level))
- breaking: properties are no longer set as attributes ([details](#19-properties-are-no-longer-set-as-attributes))
- breaking: `t-foreach` should always have a corresponding `t-key` ([details](#20-t-foreach-should-always-have-a-corresponding-t-key))
- breaking: `EventBus` api changed: it is now an `EventTarget` ([details](#21-eventbus-api-changed-it-is-now-an-eventtarget))
- breaking: `Store` is removed ([details](#22-store-is-removed))
- breaking: `Router` is removed ([details](#23-router-is-removed))
@@ -59,10 +83,9 @@ removed after.
- breaking: `AsyncRoot` utility component is removed ([details](#26-asyncroot-utility-component-is-removed))
- breaking: `useSubEnv` only applies to child components ([details](#27-usesubenv-only-applies-to-child-components))
- breaking: `env` is now frozen ([details](#28-env-is-now-frozen))
- breaking: `t-ref` does not work on components ([details](#29-t-ref-does-not-work-on-component))
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
- breaking: `renderToString` function on qweb has been removed ([details](#32-rendertostring-on-qweb-has-been-removed))
- breaking: `debounce` utility function has been removed ([details](#34-debounce-utility-function-has-been-removed))
- breaking: `browser` object has been removed ([details](#39-browser-object-has-been-removed))
## Details/Rationale/Migration
@@ -96,6 +119,8 @@ class MyComponent extends Component {
}
```
Documentation: [Component Lifecycle](doc/reference/component.md#lifecycle)
### 2. components can no longer be mounted in a detached dom element
Nor document fragment.
@@ -147,6 +172,8 @@ because real applications will need to configure the templates, the translations
and other stuff. All complex usecases need to go through the new `App` class,
that encapsulates the root of an owl application.
Documentation: [Mounting a component](doc/reference/app.md#mount-helper)
### 5. components can no longer be instantiated and mounted by hand
In Owl 1, it was possible to instantiate a component by hand:
@@ -240,10 +267,14 @@ ideas may help:
</Memo>
```
### 9. component.el may be a text node, and is no longer `null`
### 9. component.el is removed
This comes from the fact that Owl 2 supports fragments (arbitrary content). When
it is not defined, it was `null` in Owl 1 and is `undefined` in owl 2.
This comes from the fact that Owl 2 supports fragments (arbitrary content).
Migration: if one need a reference to the root htmlelement of a template, it is
suggested to simply add a `ref` on it, and access the reference as needed.
Documentation: [Refs](doc/reference/refs.md)
### 10. style/class on components are now regular props
@@ -286,7 +317,13 @@ compatible with the fact that a component can have a root `<div>` then later,
change it to something else, or even a text node.
Migration: no real way to do the same. Owl application needs to be appended or
prepended in something, maybe a `div`.
prepended in something, maybe a `div`. Remember that you the root component
can have multiple roots
Documentation:
- [Fragments](doc/reference/templates.md#fragments)
- [Mounting a component](doc/reference/app.md#mount-helper)
### 12. `t-on` does not work on components any more
@@ -313,8 +350,16 @@ the component API to accept explicitely a callback as props.
```xml
<SomeComponent onSomeEvent="doSomething"/>
<!-- or alternatively: -->
<SomeComponent onSomeEvent.bind="doSomething"/>
```
Note that one of the example above uses the `.bind` suffix, to bind the function
prop to the component. Most of the time, binding the function is necessary, and
using the `.bind` suffix is very helpful in that case.
Documentation: [Binding function props](doc/reference/props.md#binding-function-props)
### 13. Portal does no longer transfer DOM events
In Owl 1, a Portal component would listen to events emitted on its portalled
@@ -372,6 +417,8 @@ Migration: there is no proper way to get an equivalent. The closest is to get
a reference to the root App using `this.__owl__.app`. If you need to do this,
let us know. If this is a legitimate usecase, we may add a `useApp` hook.
Documentation: [Environment](doc/reference/environment.md)
### 17. `t-component` no longer accepts strings
In owl 1, we could write this:
@@ -397,6 +444,8 @@ the implementation is slightly simpler.
Migration: simply using `constructor.components.Coucou` instead of `Coucou` will
do the trick.
Documentation: [Component](doc/reference/component.md#dynamic-sub-components)
### 18. most exports are exported at top level
Most exports are flattened: for ex, `onMounted` is in owl, not in `owl.hooks`.
@@ -453,6 +502,8 @@ rewritten like this: `bus.addEventListener("event-type", (({detail: info}) => {.
Do not forget to similarly replace `bus.off(...)` by `bus.removeEventListener(...)`
Documentation: [EventBus](doc/reference/utils.md#eventbus)
### 22. `Store` is removed
The Store system had been abandoned in owl 2.
@@ -522,6 +573,7 @@ AND the component. It now only defines an environment for the children.
Rationale: This was a subtle cause for bugs: some code had to be rrun
before the call to `useSubEnv`, otherwise it could interfere with the sub environment.
Documentation: [Hooks](doc/reference/hooks.md#usesubenv)
### 28. `env` is now frozen
@@ -536,6 +588,8 @@ components. This use case still works with `useSubEnv`.
Migration: use `useSubEnv` instead of writing directly to the env. Also, note
that the environment given to the App can initially contain anything.
Documentation: [Environment](doc/reference/environment.md)
### 29. `t-ref` does not work on component
Before, `t-ref` could be used to get a reference to a child component. It no
@@ -576,6 +630,8 @@ adapted like this:
<button t-on-click="() => this.someFunction(someVar)">blabla</button>
```
Documentation: [Event Handling](doc/reference/event_handling.md)
### 31. components can now have arbitrary content
Before Owl 2, components had to limit themselves to one single htmlelement as
@@ -588,6 +644,8 @@ So, the following template works for components:
hello
```
Documentation: [Fragments](doc/reference/templates.md#fragments)
### 32. `renderToString` on QWeb has been removed
Rationale: the `renderToString` function was a qweb method, which made sense because
@@ -610,3 +668,98 @@ export async function renderToString(template, context) {
div.remove();
return result;
}
```
### 33. Portal are now defined with `t-portal`
Before Owl 2, one could use the `Portal` component by importing it and using it.
Now, it is no longer available. Instead, we can simply use the `t-portal` directive:
```xml
<div>
some content
<span t-portal="'body'">
portalled content
</span>
<div>
```
Rationale: it makes it slightly simpler to use (just need the directive, instead
of having to import and use a sub component), it makes the implementation slightly
simpler as well. Also, it prevents subclassing the Portal component, which could
be dangerous, since it is really doing weird stuff under the hood, and could
easily be broken inadvertendly.
### 34. `debounce` utility function has been removed
Rationale: it did not really help that much, is available as utility function
elsewhere, so, we decided to have a smaller footprint by focusing Owl on what
it does best.
### 35. `render` method does not return a promise anymore
Rationale: using the `render` method directly and waiting for it to complete
was slightly un-declarative. Also, it can be done using the lifecycle hooks
any way.
Migration: if necessary, one can use the lifecycle hooks to execute code after
the next mounted/patched operation.
### 36. `catchError` method is replaced by `onError` hook
The `catchError` method was used to provide a way to components to handle errors
occurring during the component lifecycle. This has been replaced by a `onError`
hook, with a similar API.
Rationale: `catchError` felt a little big awkward, when most of the way we
interact with componentss is via hooks. Using hooks felt more natural and
consistent.
Migration: mostly replace all `catchError` methods by `onError` hooks in the
`setup` method.
Documentation: [Error Handling](doc/reference/error_handling.md)
## 37. Support for inline css (`css` tag and static `style`) has been removed
Rationale: Owl tries to focus on what it does best, and supporting inline css
was not a priority. It used to support some simplified scss language, but it
was feared that it would cause more trouble than it was worth. Also, it seems
like it can be done in userspace.
Migration: it seems possible to implement an equivalent solution using hooks. A
simple implementation could look like this:
```js
let cache = {};
function useStyle(css) {
if (!css in cache) {
const sheet = document.createElement("style");
sheet.innerHTML = css;
cache[css] = sheet;
document.head.appendChild(sheet);
}
}
```
## 38. `t-raw` directive has been removed (replaced by `t-out`)
To match the Odoo qweb server implementation, Owl does no longer implement `t-raw`.
It is replaced by the `t-out` directive, which is safer: it requires the data
to be marked explicitely as markup if it is to be inserted without escaping.
Otherwise, it will be escaped (just like `t-esc`).
Migration: replace all `t-raw` uses by `t-out`, and uses the `markup` function
to mark all the js values.
Documentation: [Outputting data](doc/reference/templates.md#outputting-data)
## 39. `browser` object has been removed
Rationale: the `browser` object caused more trouble than it was worth. Also, it
seems like this should be done in user space, not at the framework level.
Migration: code should just be adapted to either use another browser object,
or to use native browser function (and then, just mock them directly).
+31 -36
View File
@@ -16,8 +16,10 @@ framework, written in Typescript, taking the best ideas from React and Vue in a
simple and consistent way. Owl's main features are:
- a declarative component system,
- a reactivity system based on hooks,
- concurrent mode by default,
- a fine grained reactivity system similar to Vue,
- hooks
- fragments
- asynchronous rendering
Owl components are defined with ES6 classes and xml templates, uses an
underlying virtual DOM, integrates beautifully with hooks, and the rendering is
@@ -38,28 +40,26 @@ const { Component, useState, mount, xml } = owl;
class Counter extends Component {
static template = xml`
<button t-on-click="() => state.value++">
<button t-on-click="() => state.value = state.value + props.increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
}
class App extends Component {
class Root extends Component {
static template = xml`
<div>
<span>Hello Owl</span>
<Counter />
</div>`;
<span>Hello Owl</span>
<Counter increment="2"/>`;
static components = { Counter };
}
mount(App, document.body);
mount(Root, document.body);
```
Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
Also, all examples here uses the [`xml` helper](doc/reference/tags.md#xml-tag) to define inline templates.
Also, all examples here uses the [`xml` helper](doc/reference/templates.md#inline-templates) to define inline templates.
But this is not mandatory, many applications will load templates separately.
More interesting examples can be found on the
@@ -74,47 +74,42 @@ Are you new to Owl? This is the place to start!
- [Tutorial: create a TodoList application](doc/learning/tutorial_todoapp.md)
- [How to start an Owl project](doc/learning/quick_start.md)
- [How to test Components](doc/learning/how_to_test.md)
- [How to write Single File Components](doc/learning/how_to_write_sfc.md)
### Reference
You will find here a complete reference of every feature, class or object
provided by Owl.
- [Animations](doc/reference/animations.md)
- [Browser](doc/reference/browser.md)
- [Overview](doc/readme.md)
- [App](doc/reference/app.md)
- [Component](doc/reference/component.md)
- [Content](doc/reference/content.md)
- [Component Lifecycle](doc/reference/component.md#lifecycle)
- [Concurrency Model](doc/reference/concurrency_model.md)
- [Configuration](doc/reference/config.md)
- [Context](doc/reference/context.md)
- [Dynamic sub components](doc/reference/component.md#dynamic-sub-components)
- [Environment](doc/reference/environment.md)
- [Event Bus](doc/reference/event_bus.md)
- [Event Handling](doc/reference/event_handling.md)
- [Error Handling](doc/reference/error_handling.md)
- [Event Handling](doc/reference/event_handling.md)
- [Form Input Bindings](doc/reference/input_bindings.md)
- [Fragments](doc/reference/templates.md#fragments)
- [Hooks](doc/reference/hooks.md)
- [Mounting a component](doc/reference/mounting.md)
- [Miscellaneous Components](doc/reference/misc.md)
- [Observer](doc/reference/observer.md)
- [Loading Templates](doc/reference/app.md#loading-templates)
- [Mounting a component](doc/reference/app.md#mount-helper)
- [Portal](doc/reference/portal.md)
- [Props](doc/reference/props.md)
- [Props Validation](doc/reference/props_validation.md)
- [QWeb Templating Language](doc/reference/qweb_templating_language.md)
- [QWeb Engine](doc/reference/qweb_engine.md)
- [Props Validation](doc/reference/props.md#props-validation)
- [Reactivity](doc/reference/reactivity.md)
- [Rendering SVG](doc/reference/templates.md#rendering-svg)
- [Refs](doc/reference/refs.md)
- [Slots](doc/reference/slots.md)
- [Tags](doc/reference/tags.md)
- [Sub components](doc/reference/component.md#sub-components)
- [Sub templates](doc/reference/templates.md#sub-templates)
- [Templates (Qweb)](doc/reference/templates.md)
- [Translations](doc/reference/translations.md)
- [Utils](doc/reference/utils.md)
### Other Topics
This section provides miscellaneous document that explains some topics
which cannot be considered either a tutorial, or reference documentation.
- [Owl architecture: the Virtual DOM](doc/miscellaneous/vdom.md)
- [Owl architecture: the rendering pipeline](doc/miscellaneous/rendering.md)
- [Notes On Owl Architecture](doc/miscellaneous/architecture.md)
- [Comparison with React/Vue](doc/miscellaneous/comparison.md)
- [Why did Odoo built Owl?](doc/miscellaneous/why_owl.md)
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
## Installing Owl
-52
View File
@@ -1,52 +0,0 @@
# 🦉 How to write Single File Components 🦉
It is very useful to group code by feature instead of by type of file. It makes
it easier to scale application to larger size.
To do so, Owl has two small helpers that make it easy to define a
template or a stylesheet inside a javascript (or typescript) file: the
[`xml`](../reference/tags.md#xml-tag) and [`css`](../reference/tags.md#css-tag)
helper.
This means that the template, the style and the javascript code can be defined in
the same file. For example:
```js
const { Component } = owl;
const { xml, css } = owl.tags;
// -----------------------------------------------------------------------------
// TEMPLATE
// -----------------------------------------------------------------------------
const TEMPLATE = xml/* xml */ `
<div class="main">
<Sidebar/>
<Content />
</div>`;
// -----------------------------------------------------------------------------
// STYLE
// -----------------------------------------------------------------------------
const STYLE = css/* css */ `
.main {
display: grid;
grid-template-columns: 200px auto;
}
`;
// -----------------------------------------------------------------------------
// CODE
// -----------------------------------------------------------------------------
class Main extends Component {
static template = TEMPLATE;
static style = STYLE;
static components = { Sidebar, Content };
// rest of component...
}
```
Note that the above example has an inline xml comment, just after the `xml` call.
This is useful for some editor plugins, such as the VS Code addon
`Comment tagged template`, which, if installed, add syntax highlighting to the
content of the template string.
+5 -5
View File
@@ -116,7 +116,7 @@ class App extends Component {}
App.template = xml`<div>todo app</div>`;
```
Note 3: writing inline templates with the [`xml` helper](../reference/tags.md#xml-tag)
Note 3: writing inline templates with the [`xml` helper](../reference/templates.md#inline-templates)
is nice, but there is no syntax highlighting, and this makes it very easy to
have malformed xml. Some editors support syntax highlighting for this situation.
For example, VS Code has an addon `Comment tagged template`, which, if installed,
@@ -173,14 +173,14 @@ class Root extends Component {
}
```
The template contains a [`t-foreach`](../reference/qweb_templating_language.md#loops) loop to iterate
The template contains a [`t-foreach`](../reference/templates.md#loops) loop to iterate
through the tasks. It can find the `tasks` list from the component, since the
component is the rendering context. Note that we use the `id` of each task as a
`t-key`, which is very common. There are two css classes: `task-list` and `task`,
that we will use in the next section.
Finally, notice the use of the `t-att-checked` attribute:
prefixing an attribute by [`t-att`](../reference/qweb_templating_language.md#dynamic-attributes) makes
prefixing an attribute by [`t-att`](../reference/templates.md#dynamic-attributes) makes
it dynamic. Owl will evaluate the expression and set it as the value of the
attribute.
@@ -277,10 +277,10 @@ A lot of stuff happened here:
- the `Task` component has a `props` key: this is only useful for validation
purpose. It says that each `Task` should be given exactly one prop, named
`task`. If this is not the case, Owl will throw an
[error](../reference/props_validation.md). This is extremely
[error](../reference/props.md#props-validation). This is extremely
useful when refactoring components
- finally, to activate the props validation, we need to set Owl's
[mode](../reference/config.md#mode) to `dev`. This is done in the last argument
[mode](../reference/app.md#configuration) to `dev`. This is done in the last argument
of the `mount` function. Note that this should be removed when an app is used in a real
production environment, since `dev` mode is slightly slower, due to extra
checks and validations.
+68
View File
@@ -0,0 +1,68 @@
# 🦉 Notes On Owl Architecture 🦉
We explain here how Owl is designed
Warning: these notes are technical by nature, and intended for people working
on Owl (or interested in understanding its design).
## Overview
Roughly speaking, Owl has 5 main parts:
- a virtual dom system (in `src/blockdom`)
- a component system (in `src/component`)
- a template compiler (located in the `src/compiler` folder)
- a small runtime code to tie them together (in `src/app`)
- a reactivity system (in `src/reactivity.ts`)
There are some other files, but the core of Owl can be understood with these
five main parts.
The virtual dom is an optimized virtual dom based on blocks, which supports
multi blocks (for fragments). Everything that owl renders is internally
represented by a virtual node. The job of the virtual dom is to efficiently
represent the current state of the application, and to build an actual DOM
representation when needed, or update the DOM whenever it is needed.
- some other helpers/smaller scale stuff
A rendering occurs in two phases:
- virtual rendering: this generates the virtual dom in memory, asynchronously
- patch: applies a virtual tree to the screen (synchronously)
There are several classes involved in a rendering:
- components
- a scheduler
- fibers: small objects containing some metadata, associated with a rendering of
a specific component
Components are organized in a dynamic component tree, visible in the user
interface. Whenever a rendering is initiated in a component `C`:
- a fiber is created on `C` with the rendering props information
- the virtual rendering phase starts on C (will asynchronously render all the
child components)
- the fiber is added to the scheduler, which will poll continuously, every
animation frame, if the fiber is done
- once it is done, the scheduler will call the task callback, which will apply
the patch (if it was not cancelled in the meantime).
# 🦉 VDom 🦉
Owl is a declarative component system: we declare the structure of the component
tree, and Owl will translate that to a list of imperative operations. This
translation is done by a virtual dom. This is the low level layer of Owl, most
developer will not need to call directly the virtual dom functions.
The main idea behind a virtual dom is to keep a in-memory representation of the
DOM (called a virtual node), and whenever some change is needed, to regenerate
a new representation, compute the difference between the old and the new, then
apply the changes.
`vdom` exports two functions:
- `h`: create a new virtual node
- `patch`: compare two virtual nodes, and apply the difference.
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
+2 -2
View File
@@ -78,7 +78,7 @@ additional tools, we made a lot of effort to make the most of the web platform.
For example, Owl uses the standard `xml` parser that comes with every browser.
Because of that, Owl did not have to write its own template parser. Another
example is the [`xml`](../reference/tags.md#xml-tag) tag helper function, which makes use of
example is the [`xml`](../reference/templates.md#inline-templates) tag helper function, which makes use of
native template literals to allow in a natural way to write `xml` templates
directly in the javascript code. This can be easily integrated with editor
plugins to have autocompletion inside the template.
@@ -126,7 +126,7 @@ structured than a template language. Note that the tooling is quite impressive:
there is a syntax highlighter for jsx here on github!
By comparison, here is the equivalent Owl component, written with the
[`xml`](../reference/tags.md#xml-tag) tag helper:
[`xml`](../reference/templates.md#inline-templates) tag helper:
```js
class Clock extends Component {
-32
View File
@@ -1,32 +0,0 @@
# 🦉 Rendering Pipeline 🦉
We explain here how Owl is designed, from the perspective of its rendering
pipeline.
Warning: these notes are technical by nature, and intended for people working
on Owl (or interested in understanding its design).
## Overview
A rendering occurs in two phases:
- virtual rendering: this generates the virtual dom in memory, asynchronously
- patch: applies a virtual tree to the screen (synchronously)
There are several classes involved in a rendering:
- components
- a scheduler
- fibers: small objects containing some metadata, associated with a rendering of
a specific component
Components are organized in a dynamic component tree, visible in the user
interface. Whenever a rendering is initiated in a component `C`:
- a fiber is created on `C` with the rendering props information
- the virtual rendering phase starts on C (will asynchronously render all the
child components)
- the fiber is added to the scheduler, which will poll continuously, every
animation frame, if the fiber is done
- once it is done, the scheduler will call the task callback, which will apply
the patch (if it was not cancelled in the meantime).
-18
View File
@@ -1,18 +0,0 @@
# 🦉 VDom 🦉
Owl is a declarative component system: we declare the structure of the component
tree, and Owl will translate that to a list of imperative operations. This
translation is done by a virtual dom. This is the low level layer of Owl, most
developer will not need to call directly the virtual dom functions.
The main idea behind a virtual dom is to keep a in-memory representation of the
DOM (called a virtual node), and whenever some change is needed, to regenerate
a new representation, compute the difference between the old and the new, then
apply the changes.
`vdom` exports two functions:
- `h`: create a new virtual node
- `patch`: compare two virtual nodes, and apply the difference.
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
+42
View File
@@ -0,0 +1,42 @@
# 🦉 Owl overview 🦉
Here is a list of everything exported by the Owl library:
Main entities:
- [`App`](reference/app.md): represent an Owl application (mainly a root component,a set of templates, and a config)
- [`Component`](reference/component.md): the main class to define a concrete Owl component
- [`mount`](reference/app.md#mount-helper): main entry point for most application: mount a component to a target
- [`useState`](reference/reactivity.md#usestate): create a reactive object (hook, linked to a specific component)
- [`reactive`](reference/reactivity.md#reactive): create a reactive object (not linked to any component)
- [`xml`](reference/templates.md#inline-templates): helper to define an inline template
Lifecycle hooks:
- [`onWillStart`](reference/component.md#willstart): hook to define asynchronous code that should be executed before component is rendered
- [`onMounted`](reference/component.md#mounted): hook to define code that should be executed when component is mounted
- [`onWillPatch`](reference/component.md#willpatch): hook to define code that should be executed before component is patched
- [`onWillUpdateProps`](reference/component.md#willupdateprops): hook to define code that should be executed before component is updated
- [`onPatched`](reference/component.md#patched): hook to define code that should be executed when component is patched
- [`onWillRender`](reference/component.md#willrender): hook to define code that should be executed before component is rendered
- [`onRendered`](reference/component.md#rendered): hook to define code that should be executed after component is rendered
- [`onWillUnmount`](reference/component.md#willunmount): hook to define code that should be executed before component is unmounted
- [`onWillDestroy`](reference/component.md#willdestroy): hook to define code that should be executed before component is destroyed
- [`onError`](reference/component.md#onerror): hook to define a Owl error handler
Other hooks:
- [`useComponent`](reference/hooks.md#usecomponent): return a reference to the current component (useful to create derived hooks)
- [`useEffect`](reference/hooks.md#useeffect): define an effect with its dependencies
- [`useEnv`](reference/hooks.md#useenv): return a reference to the current env
- [`useExternalListener`](reference/hooks.md#useexternallistener): add a listener outside of a component DOM
- [`useRef`](reference/hooks.md#useref): get an object representing a reference (`t-ref`)
- [`useSubEnv`](reference/hooks.md#usesubenv): extend the current env with additional information for child components
Utility/helpers:
- [`EventBus`](reference/utils.md#eventbus): a simple event bus
- [`loadFile`](reference/utils.md#loadfile): an helper to load a file from the server
- [`markup`](reference/templates.md#outputting-data): utility function to define strings that represent html (should not be escaped)
- [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed)
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
-127
View File
@@ -1,127 +0,0 @@
# 🦉 Animations 🦉
Animation is a complex topic. There are many different use cases, and many
solutions and technologies. Owl only supports some basic use cases.
## Simple CSS effects
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
necessary: it just needs to render a DOM element with a specific class. For
example:
```xml
<a class="btn flash" t-on-click="doSomething">Click</a>
```
with the following CSS:
```css
btn {
background-color: gray;
}
.flash {
transition: background 0.5s;
}
.flash:active {
background-color: #41454a;
transition: background 0s;
}
```
will produce a nice flash effect whenever the user clicks (or activates with the
keyboard) the button.
## CSS Transitions
A more complex situation occurs when we want to transition an element in or out
of the page. For example, we may want a fade-in and fade-out effect.
The `t-transition` directive is here to help us. It works on html elements and
on components, by adding and removing some css classes.
To perform useful transition effects, whenever an element appears or disappears,
it is necessary to add/remove some css style or class at some precise moment in
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
directive is there to help.
Whenever a node has a `t-transition` directive, with a `name` value, the following
sequence of events will happen:
At node insertion:
- the css classes `name-enter` and `name-enter-active` will be added directly
when the node is inserted into the DOM.
- on the next animation frame: the css class `name-enter` will be removed and the
class `name-enter-to` will be added (so they can be used to trigger css
transition effects).
- at the end of the transition, `name-enter-to` and `name-enter-active` will be removed.
At node destruction:
- the css classes `name-leave` and `name-leave-active` will be added before the
node is removed to the DOM.
- on the next animation frame: the css class `name-leave` will be removed and the
class `name-leave-to` will be added (so they can be used to trigger css
transition effects).
- at the end of the transition, `name-leave-to` and `name-leave-active` will be removed.
For example, a simple fade in/out effect can be done with this:
```xml
<div>
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
</div>
```
```css
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
```
The `t-transition` directive can be applied on a node element or on a component.
Notes:
Owl does not support more than one transition on a single node, so the
`t-transition` expression must be a single value (i.e. no space allowed).
## SCSS Mixins
If you use SCSS, you can use mixins to make generic animations. Here is an exemple with a fade in / fade out animation:
```scss
@mixin animation-fade($time, $name) {
.#{$name}_fade-enter-active,
.#{$name}_fade-active {
transition: all $time;
}
.#{$name}_fade-enter {
opacity: 0;
}
.#{$name}_fade-leave-to {
opacity: 0;
}
}
```
Usage:
```scss
@include animation-fade(0.5s, "o_notification");
```
You can now have in your template:
```xml
<SomeTag t-transition="o_notification_fade"/>
```
+107
View File
@@ -0,0 +1,107 @@
# 🦉 App 🦉
## Content
- [Overview](#overview)
- [Configuration](#configuration)
- [`mount` helper](#mount-helper)
- [Loading templates](#loading-templates)
## Overview
Every Owl application has a root element, a set of templates, an environment and
possibly a few other settings. The `App` class is a simple class that represents
all of these elements. Here is an example:
```js
const {Component, App } = owl;
class MyComponent extends Component { ... }
const app = new App(MyComponent, { props: {...}, templates: "..."});
app.mount(document.body);
```
The basic workflow is: create an `App` instance configured with the root
component, the templates, and possibly other settings. Then, we mount that
instance somewhere in the DOM.
## Configuration
- **`constructor(Root[, config])`**: first argument should be a component class (not
an instance), and the optional second argument is a configuration object.
The `config` object is an object with some of the following keys:
- **`env (object)`**: if given, this will be the shared `env` given to each component
- **`props (object)`**: the props given to the root component
- **`dev (boolean, default=false)`**: if `true`, the application is rendered in `dev`
mode, which activates some additional checks (in particular, the props validation
code is only performed in dev mode)
- **`translatableAttributes (string[])`**: a list of additional attributes that should
be translated (see [translations](translations.md))
- **`translateFn (function)`**: a function that will be called by owl to translate
templates (see [translations](translations.md))
- **`templates (string | xml document)`**: all the templates that will be used by
the components created by the application.
- **`mount(target, options)`**: first argument is an html element, and the optional
second argument is an object with mounting options (see below). Mount the app
to a target in the DOM. Note that this is an asynchronous operation: the `mount`
method returns a promise that resolves to the component instance whenever it
is complete.
The `option` object is an object with the following keys:
- **`position (string)`**: either `first-child` or `last-child`. This option determines
the position of the application in the target: either first or last child.
- **`destroy()`**: destroys the application
## `mount` helper
Note that there is a `mount` helper to do that in just a line:
```js
const { mount, Component } = owl;
class MyComponent extends Component {
...
}
mount(MyComponent, document.body, { props: {...}, templates: "..."});
```
Here is the `mount` function signature:
**`mount(Component, target, config)`** with the following arguments:
- **`Component`**: a component class (Root component of the app)
- **`target`**: an html element, where the component will be mounted as last child
- **`config (optional)`**: a config object (the same as the App config object)
Most of the time, the `mount` helper is more convenient, but whenever one needs
a reference to the actual Owl App, then using the `App` class directly is
possible.
## Loading templates
Most applications will need to load templates whenever they start. Here is
what it could look like in practice:
```js
// in the main js file:
const { loadFile, mount } = owl;
// async, so we can use async/await
(async function setup() {
const templates = await loadFile(`/some/endpoint/that/return/templates`);
const env = {
_t: someTranslateFn,
templates,
// possibly other stuff
};
mount(Root, document.body, { env });
})();
```
-33
View File
@@ -1,33 +0,0 @@
# 🦉 Browser 🦉
## Content
- [Overview](#overview)
- [Browser Content](#browser-content)
## Overview
The browser object contains some browser native APIs, such as `setTimeout`, that
are used by Owl and its utility functions. They are exposed with the intent of
making them mockable if necessary.
```js
owl.browser.setTimeout === window.setTimeout; // return true
```
For now, this object contains some functions that are not used by Owl. They
will eventually be removed in Owl 2.0.
## Browser Content
More specifically, the `browser` object contains the following methods and objects:
- `setTimeout`
- `clearTimeout`
- `setInterval`
- `clearInterval`
- `requestAnimationFrame`
- `random`
- `Date`
- `fetch`
- `localStorage`
+267 -672
View File
File diff suppressed because it is too large Load Diff
+6 -24
View File
@@ -11,7 +11,7 @@
Owl was designed from the very beginning with asynchronous components. This comes
from the `willStart` and the `willUpdateProps` lifecycle hooks. With these
methods, it is possible to build complex highly concurrent applications.
asynchronous hooks, it is possible to build complex highly concurrent applications.
Owl concurrent mode has several benefits: it makes it possible to delay the
rendering until some asynchronous operation is complete, it makes it possible
@@ -35,7 +35,8 @@ two phases: _virtual rendering_ and _patching_.
### Virtual rendering
This phase represent the process of rendering a template, in memory, which create a virtual representation of the desired component html). The output of this phase is a
This phase represent the process of rendering a template, in memory, which creates
a virtual representation of the desired component html). The output of this phase is a
virtual DOM.
It is asynchronous: each subcomponents needs to either be created (so, `willStart`
@@ -94,7 +95,7 @@ component (with some code like `app.mount(document.body)`).
5. The method `mounted` is called recursively on all components in the following
order: `E`, `D`, `C`, `B`, `A`.
**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some
**Scenario 2: updating a component**. Now, let's assume that the user clicked on some
button in `C`, and this results in a state update, which is supposed to:
- update `D`,
@@ -136,8 +137,7 @@ Here is what Owl will do:
6. `mounted` hook is called on `F`, `patched` hooks are called on `D`, `C`
Tags are very small helpers to make it easy to write inline templates. There is
only one currently available tag: `xml`, but we plan to add other tags later,
such as a `css` tag, which will be used to write [single file components](../learning/how_to_write_sfc.md).
only one currently available tag: `xml`.
### Asynchronous Rendering
@@ -160,22 +160,4 @@ Here are a few tips on how to work with asynchronous components:
1. Minimize the use of asynchronous components!
2. Lazy loading external libraries is a good use case for async rendering. This
is mostly fine, because we can assume that it will only takes a fraction of a
second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs))
3. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When
this component is met, a new rendering
sub tree is created, such that the rendering of that component (and its
children) is not tied to the rendering of the rest of the interface. It can
be used on an asynchronous component, to prevent it from delaying the
rendering of the whole interface, or on a synchronous one, such that its
rendering isn't delayed by other (asynchronous) components. Note that this
directive has no effect on the first rendering, but only on subsequent ones
(triggered by state or props changes).
```xml
<div t-name="ParentComponent">
<SyncChild />
<AsyncRoot>
<AsyncChild/>
</AsyncRoot>
</div>
```
second, and only once.
-44
View File
@@ -1,44 +0,0 @@
# 🦉 Config 🦉
The Owl framework is designed to work in many situations. However, it is
sometimes necessary to customize some behaviour. This is done by using the
global `config` object. It provides two settings:
- [`mode`](#mode) (default value: `prod`),
- [`enableTransitions`](#enabletransitions) (default value: `true`).
## Mode
By default, Owl is in _production_ mode, this means that it will try to do its
job fast, and skip some expensive operations. However, it is sometimes necessary
to have better information on what is going on, this is the purpose
of the `dev` mode.
Owl has a mode flag, in `owl.config.mode`. Its default value is `prod`, but
it can be set to `dev`:
```js
owl.config.mode = "dev";
```
Note that templates compiled with the `prod` settings will not be recompiled.
So, changing this setting is best done at startup.
An important job done by the `dev` mode is to validate props for each component
creation and update. Also, extra props will cause an error.
## `enableTransitions`
Transitions are usually nice, but they can cause issues in some specific cases,
such as automated tests. It is uncomfortable having to wait for a transition
to end before moving to the next step.
To solve this issue, Owl can be configured to ignore the `t-transition` directive.
To do that, one only needs to set the `enableTransitions` flag to false:
```js
owl.config.enableTransitions = false;
```
Note that it suffers from the same drawback as the "dev" mode: all compiled
templates, if any, will keep their current behaviours.
-36
View File
@@ -1,36 +0,0 @@
# 🦉 Owl Content 🦉
Here is a complete visual representation of everything exported by the `owl`
global object.
For example, `Component` is available at `owl.Component` and `EventBus` is
exported as `owl.core.EventBus`.
```
browser
Component misc
Context AsyncRoot
QWeb Portal
mount
useState tags
config css
mode xml
core utils
EventBus debounce
Observer escape
hooks loadJS
onWillStart loadFile
onMounted shallowEqual
onWillUpdateProps whenReady
onWillPatch
onPatched
onWillUnmount
useContext
useState
useRef
useComponent
useEnv
useSubEnv
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
+22 -82
View File
@@ -6,15 +6,15 @@
- [Setting an Environment](#setting-an-environment)
- [Using a sub environment](#using-a-sub-environment)
- [Content of an Environment](#content-of-an-environment)
- [Special keys](#special-keys)
## Overview
An environment is an object which contains a [`QWeb` instance](qweb_engine.md). Whenever
a root component is created, it is assigned an environment (see
[below](#setting-an-environment) for more info on this). This environment is
then automatically given to each sub component (and accessible in the `this.env`
property).
An environment is a shared object given to all components in a tree. It is not
used by Owl itself, but it is useful for application developers to provide a
simple communication channel between components (in addition to the props).
The `env` given to the [`App`](app.md) is assigned to the `env` component
property.
```
Root
@@ -22,31 +22,14 @@ property).
A B
```
This way, all components share the same `QWeb` instance. Owl internally requires
that the environment has a `qweb` key which maps to a
[`QWeb`](qweb_engine.md) instance. This is the QWeb instance that will be used to
render each templates in this specific component tree. Note that if no `QWeb`
instance is provided, Owl will simply generate it on the fly.
The environment is mostly static. Each application is free to add anything to
the environment, which is very useful, since this can be accessed by each sub
component.
Also, the `env` object is frozen when the application is started. This is done
to ensure a simpler mental model of what's happening in runtime. Note that it
is only shallowly frozen, so sub objects can be modified.
## Setting an environment
An Owl application needs an [environment](environment.md) to be executed. The
environment has an important key: the [QWeb](qweb_engine.md) instance, which will render
all templates.
Whenever a root component `App` is mounted, Owl will setup a valid environment by
following the next steps:
- take the `env` object defined on `App.env` (if no `env` was explicitly setup,
this will return the empty `env` object defined on `Component`)
- if `env.qweb` is not set, then Owl will create a `QWeb` instance.
The correct way to customize an environment is to simply set it up on the root
component class, before the first component is created:
The correct way to customize an environment is to simply give it to the `App`,
whenever it is created.
```js
const env = {
@@ -56,20 +39,13 @@ const env = {
...
},
};
mount(App, { target: document.body, env });
new App(Root, { env }).mount(document.body);
// or alternatively
mount(App, document.body, { env });
```
It is also possible to simply share an environment between all root components,
by simply doing this:
```js
Component.env = myEnv; // will be the default env for all components
```
Note that this environment is the global owl environment for an application. The
next section explains how to extend an environment for a specific sub component
and its children.
## Using a sub environment
It is sometimes useful to add one (or more) specific keys to the environment,
@@ -79,58 +55,22 @@ solution presented above will not work, since it sets the global environment.
There is a hook for this situation: [`useSubEnv`](hooks.md#usesubenv).
```js
class FormComponent extends Component {
constructor(parent, props) {
super(parent, props);
useSubEnv({ myKey: someValue });
class SomeComponent extends Component {
setup() {
useSubEnv({ myKey: someValue }); // myKey is now available for all child components
}
}
```
## Content of an Environment
Some good use cases for additional keys in the environment are:
The `env` object content is totally up to the application developer. However,
some good use cases for additional keys in the environment are:
- some configuration keys,
- session information,
- generic services (such as doing rpcs).
- other utility functions that one want to inject, such as a translation function.
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
For example:
```js
async function myEnv() {
const templates = await loadTemplates();
const qweb = new QWeb({ templates });
const session = getSession();
return {
_t: myTranslateFunction,
session: session,
qweb: qweb,
services: {
localStorage: localStorage,
rpc: rpc,
},
debug: false,
inMobileMode: true,
};
}
async function start() {
const env = await myEnv();
mount(App, { target: document.body, env });
}
```
## Special Keys
There are two special key/value added by Owl if not provided in the environment:
the `QWeb` instance and a `browser` object:
- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
for Owl to be able to render anything
- `browser`: this is an object that contains some common access points to the
browser methods with a side effect. See [browser](browser.md) for more information. Note that the browser object will be removed from the environment in Owl 2.0.
+44 -50
View File
@@ -3,60 +3,30 @@
## Content
- [Overview](#overview)
- [Managing Errors](#managing-errors)
- [Example](#example)
- [Reference](#reference)
## Overview
By default, whenever an error occurs in the rendering of an Owl application, we
destroy the whole application. Otherwise, we cannot offer any guarantee on the
state of the resulting component tree. It might be hopelessly corrupted, but
without any user-visible state.
without any user-visible feedback.
Clearly, it sometimes is a little bit extreme to destroy the application. This
is why we have a builtin mechanism to handle rendering errors (and errors coming
from lifecycle hooks): the `catchError` hook.
Clearly, it is usually a little bit extreme to destroy the application. This
is why we need a mechanism to handle rendering errors (and errors coming
from lifecycle hooks): the `onError` hook.
## Example
The main idea is that the `onError` hook register a function that will be called
with the error. This function need to handle the situation, most of the time by
updating some state and rerendering itself, so the application can return to a
normal state.
For example, here is how we could implement an `ErrorBoundary` component:
## Managing Errors
```xml
<div t-name="ErrorBoundary">
<t t-if="state.error">
Error handled
</t>
<t t-else="">
<t t-slot="default" />
</t>
</div>
```
```js
class ErrorBoundary extends Component {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
```
Using the `ErrorBoundary` is then extremely simple:
```xml
<ErrorBoundary><SomeOtherComponent/></ErrorBoundary>
```
Note that we need to be careful here: the fallback UI should not throw any
error, otherwise we risk going into an infinite loop (also, see the page on
[slots](slots.md) for more information on the `t-slot` directive).
## Reference
Whenever the `catchError` lifecycle hook is implemented, all errors coming from
Whenever the `onError` lifecycle hook is used, all errors coming from
sub components rendering and/or lifecycle method calls will be caught and given
to the `catchError` method. This allows us to properly handle the error, and to
to the `onError` method. This allows us to properly handle the error, and to
not break the application.
There are important things to know:
@@ -65,17 +35,41 @@ There are important things to know:
Owl will destroy the full application. This is done on purpose, because Owl
cannot guarantee that the state is not corrupted from this point on.
- errors coming from event handlers are NOT managed by `catchError` or any other
- errors coming from event handlers are NOT managed by `onError` or any other
owl mechanism. This is up to the application developer to properly recover
from an error
Also, it may be useful to know that whenever an error is caught, it is then
broadcasted to the application by an event on the `qweb` instance. It may be
useful, for example, to log the error somewhere.
- if an error handler is unable to properly handle an error, it can just rethrow
an error, and Owl will try looking for another error handler up the component
tree.
## Example
For example, here is how we could implement a generic component `ErrorBoundary`
that render its content, and a fallback if an error happened.
```js
env.qweb.on("error", null, function (error) {
// do something
// react to the error
});
class ErrorBoundary extends Component {
static template = xml`
<t t-if="error" t-slot="fallback">An error occurred</t>
<t t-else="" t-slot="content"`;
setup() {
this.state = useState({ error: false });
onError(() => (this.state.error = true));
}
}
```
Using the `ErrorBoundary` is then simple simple:
```xml
<ErrorBoundary>
<SomeOtherComponent/>
<t t-set-slot="fallback">Some specific error message</t>
</ErrorBoundary>
```
Note that we need to be careful here: the fallback UI should not throw any
error, otherwise we risk going into an infinite loop (also, see the page on
[slots](slots.md) for more information on the `t-slot` directive).
+46 -95
View File
@@ -3,22 +3,14 @@
## Content
- [Event Handling](#event-handling)
- [Business DOM Events](#business-dom-events)
- [Inline Event Handlers](#inline-event-handlers)
- [Modifiers](#modifiers)
- [Synthetic Events](#synthetic-events)
## Event Handling
In a component's template, it is useful to be able to register handlers on DOM
elements to some specific events. This is what makes a template _alive_. There
are four different use cases.
1. Register an event handler on a DOM node (_pure_ DOM event)
2. Register an event handler on a component (_pure_ DOM event)
3. Register an event handler on a DOM node (_business_ DOM event)
4. Register an event handler on a component (_business_ DOM event)
A _pure_ DOM event is directly triggered by a user interaction (e.g. a `click`).
elements to some specific events. This is what makes a template _alive_. This
is done with the `t-on` directive. For example:
```xml
<button t-on-click="someMethod">Do something</button>
@@ -31,93 +23,28 @@ button.addEventListener("click", component.someMethod.bind(component));
```
The suffix (`click` in this example) is simply the name of the actual DOM
event.
## Business DOM Events
A _business_ DOM event is triggered by a call to `trigger` on a component.
event. The value of the `t-on` expression should be a valid javascript expression
that evaluates to a function in the context of the current component. So, one
can get a reference to the event, or pass some additional arguments. For example,
all the following expressions are valid:
```xml
<MyComponent t-on-menu-loaded="someMethod" />
<button t-on-click="someMethod">Do something</button>
<button t-on-click="() => this.increment(3)">Add 3</button>
<button t-on-click="ev => this.doStuff(ev, 'value')">Do something</button>
```
```js
class MyComponent {
someWhere() {
const payload = ...;
this.trigger('menu-loaded', payload);
}
}
```
Notice the use of the `this` keyword in the lambda function: this is the
correct way to call a method on the component in a lambda function.
The call to `trigger` generates an `OwlEvent`, a subclass of [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
with an additional attribute `originalComponent` (the component that triggered
the event). The generated event is of type `menu-loaded` and dispatches it on
the component's DOM element (`this.el`). The event bubbles and is cancelable.
The parent component listening to event `menu-loaded` will receive the payload
in its `someMethod` handler (in the `detail` property of the event), whenever
the event is triggered.
```js
class ParentComponent {
someMethod(ev) {
const payload = ev.detail;
...
}
}
```
By convention, we use KebabCase for the name of _business_ events.
The `t-on` directive allows to prebind its arguments. For example,
One could use the following expression:
```xml
<button t-on-click="someMethod(expr)">Do something</button>
<button t-on-click="() => increment(3)">Add 3</button>
```
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context.
### Type Hinting
Note that if you work with Typescript, the `trigger` method is generic on the type of the payload.
You can then describe the type of the event, so you will see typing errors...
```typescript
this.trigger<MyCustomPayload>("my-custom-event", payload);
```
```typescript
myCustomEventHandler(ev: OwlEvent<MyCustomPayload>) { ... }
```
## Inline Event Handlers
One can also directly specify inline statements. For example,
```xml
<button t-on-click="state.counter++">Increment counter</button>
```
Here, `state` must be defined in the rendering context (typically the component)
as it will be translated to:
```js
button.addEventListener("click", () => {
context.state.counter++;
});
```
Warning: inline expressions are evaluated in the context of the template. This
means that they can access the component methods and properties. But if they set
a key, the inline statement will actually not modify the component, but a key in
a sub scope.
```xml
<button t-on-click="value = 1">Set value to 1 (does not work!!!)</button>
<button t-on-click="state.value = 1">Set state.value to 1 (work as expected)</button>
```
But then, the increment function may be unbound (unless the component binds it
in its setup function, for example).
## Modifiers
@@ -125,12 +52,13 @@ In order to remove the DOM event details from the event handlers (like calls to
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
specified as additional suffixes of the `t-on` directive.
| Modifier | Description |
| ---------- | ------------------------------------------------------------------------------------------------------------------------ |
| `.stop` | calls `event.stopPropagation()` before calling the method |
| `.prevent` | calls `event.preventDefault()` before calling the method |
| `.self` | calls the method only if the `event.target` is the element itself |
| `.capture` | bind the event handler in [capture](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) mode. |
| Modifier | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------------------ |
| `.stop` | calls `event.stopPropagation()` before calling the method |
| `.prevent` | calls `event.preventDefault()` before calling the method |
| `.self` | calls the method only if the `event.target` is the element itself |
| `.capture` | bind the event handler in [capture](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) mode. |
| `.synthetic` | define a synthetic event handler (see below) |
```xml
<button t-on-click.stop="someMethod">Do something</button>
@@ -149,3 +77,26 @@ modifiers. For example,
```
This will simply stop the propagation of the event.
## Synthetic Events
In some cases, attaching an event handler for each element of large lists has
a non trivial cost. Owl provides a way to efficiently improve the performance:
with synthetic event, it actually adds only one handler on the document body,
and will properly call the handler, just as expected.
The only difference with regular events is that the event is caught at the document
body, so it cannot be stopped before it actually gets there. Since it may be
surprising in some cases, it is not enabled by default.
To enable it, one can just use the `.synthetic` suffix:
```xml
<div>
<t t-foreach="largeList" t-as="elem" t-key="elem.id">
<button t-on-click.synthetic="doSomething" ...>
<!-- some content -->
</button>
</t>
</div>
```
+140 -273
View File
@@ -3,24 +3,17 @@
## Content
- [Overview](#overview)
- [Example: Mouse Position](#example-mouse-position)
- [Example: Autofocus](#example-autofocus)
- [Reference](#reference)
- [One Rule](#one-rule)
- [The Hook Rule](#the-hook-rule)
- [Lifecycle hooks](#lifecycle-hooks)
- [Other hooks](#other-hooks)
- [`useState`](#usestate)
- [`onMounted`](#onmounted)
- [`onWillUnmount`](#onwillunmount)
- [`onWillPatch`](#onwillpatch)
- [`onPatched`](#onpatched)
- [`onWillStart`](#onwillstart)
- [`onWillUpdateProps`](#onwillupdateprops)
- [`useContext`](#usecontext)
- [`useRef`](#useref)
- [`useSubEnv`](#usesubenv)
- [`useExternalListener`](#useexternallistener)
- [`useComponent`](#usecomponent)
- [`useEnv`](#useenv)
- [Making customized hooks](#making-customized-hooks)
- [`useEffect`](#useeffect)
- [Example: Mouse Position](#example-mouse-position)
## Overview
@@ -39,96 +32,9 @@ Hooks work beautifully with Owl components: they solve the problems mentioned
above, and in particular, they are the perfect way to make your component
reactive.
## Example: mouse position
## The Hook Rule
Here is the classical example of a non trivial hook to track the mouse position.
```js
const { useState, onMounted, onWillUnmount } = owl.hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({ x: 0, y: 0 });
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
onMounted(() => {
window.addEventListener("mousemove", update);
});
onWillUnmount(() => {
window.removeEventListener("mousemove", update);
});
return position;
}
// Main root component
class App extends owl.Component {
static template = xml`
<div t-name="App">
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>`;
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
}
```
Note that we use the prefix `use` for hooks, just like in React. This is just
a convention.
## Example: autofocus
Hooks can be combined to create the desired effect. For example, the following
hook combines the `useRef` hook with the `onPatched` and `onMounted` functions
to create an easy way to focus an input whenever it appears in the DOM:
```js
function useAutofocus(name) {
let ref = useRef(name);
let isInDom = false;
function updateFocus() {
if (!isInDom && ref.el) {
isInDom = true;
ref.el.focus();
} else if (isInDom && !ref.el) {
isInDom = false;
}
}
onPatched(updateFocus);
onMounted(updateFocus);
}
```
This hook takes the name of a valid `t-ref` directive, which should be present
in the template. It then checks whenever the component is mounted or patched if
the reference is not valid, and in this case, it will focus the node element.
This hook can be used like this:
```js
class SomeComponent extends Component {
static template = xml`
<div>
<input />
<input t-ref="myinput"/>
</div>`;
constructor(...args) {
super(...args);
useAutofocus("myinput");
}
}
```
## Reference
### One rule
There is only one rule: every hook for a component has to be called in the
constructor, in the _setup_ method, or in class fields:
There is only one rule: every hook for a component has to be called in the _setup_ method, or in class fields:
```js
// ok
@@ -136,14 +42,6 @@ class SomeComponent extends Component {
state = useState({ value: 0 });
}
// also ok
class SomeComponent extends Component {
constructor(...args) {
super(...args);
this.state = useState({ value: 0 });
}
}
// also ok
class SomeComponent extends Component {
setup() {
@@ -159,15 +57,24 @@ class SomeComponent extends Component {
}
```
As you can see, the `useState` hook does not need to be given a reference to
the component. This is possible because there is a way to get a reference to the
current component: the `Component.current` static property is the reference to the
component instance that is currently being created.
## Lifecycle Hooks
Hooks need to be called in the constructor to ensure that this reference is
properly set. This is also a good thing for performance reasons (Owl can use
this to optimize its implementation), and for a clean architecture (this makes
it easier for developers to understand what is really happening in a component).
All lifecycle hooks are documented in detail in their specific [section](component.md#lifecycle).
| Hook | Description |
| ----------------------------------------------------- | ---------------------------------------------------------------------- |
| **[onWillStart](component.md#willstart)** | async, before first rendering |
| **[onWillRender](component.md#willrender)** | just before component is rendered |
| **[onRendered](component.md#rendered)** | just after component is rendered |
| **[onMounted](component.md#mounted)** | just after component is rendered and added to the DOM |
| **[onWillUpdateProps](component.md#willupdateprops)** | async, before props update |
| **[onWillPatch](component.md#willpatch)** | just before the DOM is patched |
| **[onPatched](component.md#patched)** | just after the DOM is patched |
| **[onWillUnmount](component.md#willunmount)** | just before removing component from DOM |
| **[onWillDestroy](component.md#willdestroy)** | just before component is destroyed |
| **[onError](component.md#onerror)** | catch and handle errors (see [error handling page](error_handling.md)) |
## Other Hooks
### `useState`
@@ -178,9 +85,9 @@ The `useState` hook has to be given an object or an array, and will return
an observed version of it (using a `Proxy`).
```javascript
const { useState } = owl.hooks;
const { useState, Component } = owl;
class Counter extends owl.Component {
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
@@ -197,128 +104,38 @@ class Counter extends owl.Component {
It is important to remember that `useState` only works with objects or arrays. It
is necessary, since Owl needs to react to a change in state.
### `onMounted`
`onMounted` is not a user hook, but is a building block designed to help make useful
abstractions. `onMounted` registers a callback, which will be called when the component
is mounted (see example on top of this page).
### `onWillUnmount`
`onWillUnmount` is not a user hook, but is a building block designed to help make useful
abstractions. `onWillUnmount` registers a callback, which will be called when the component
is unmounted (see example on top of this page).
### `onWillPatch`
`onWillPatch` is not a user hook, but is a building block designed to help make useful
abstractions. `onWillPatch` registers a callback, which will be called just
before the component patched.
### `onPatched`
`onPatched` is not a user hook, but is a building block designed to help make useful
abstractions. `onPatched` registers a callback, which will be called just
after the component patched.
### `onWillStart`
`onWillStart` is an asynchronous hook. This means that the function registered
in the hook will be run just before the component is first rendered and can return a
promise, to express the fact that it is an asynchronous operation.
Note that if there are more than one `onWillStart` registered callback, then they
will all be run in parallel.
It can be used to load some initial data. For example, the following hook will
automatically load some data from the server, and return an object that will
be ready whenever the component is rendered:
```js
function useLoader() {
const component = Component.current;
const record = useState({});
onWillStart(async () => {
const recordId = component.props.id;
Object.assign(record, await fetchSomeRecord(recordId));
});
return record;
}
```
Note that this example does not update the record value whenever props are
updated. For that situation, we need to use the `onWillUpdateProps` hook.
### `onWillUpdateProps`
Just like `onWillStart`, `onWillUpdateProps` is an asynchronous hook. It is
designed to be run whenever the component props are updated. This could be
useful to perform some asynchronous task such as fetching updated data.
```js
function useLoader() {
const component = Component.current;
const record = useState({});
async function updateRecord(id) {
Object.assign(record, await fetchSomeRecord(id));
}
onWillStart(() => updateRecord(component.props.id));
onWillUpdateProps((nextProps) => updateRecord(nextProps.id));
return record;
}
```
Note that if there are more than one `onWillUpdateProps` registered callback,
then they will all be run in parallel.
### `useContext`
See [`useContext`](context.md#usecontext) for reference documentation.
### `useRef`
The `useRef` hook is useful when we need a way to interact with some inside part
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
tagged by the `t-ref` directive:
of a component, rendered by Owl. It only work on a html element tagged by the
`t-ref` directive:
```xml
<div>
<div t-ref="someDiv"/>
<SubComponent t-ref="someComponent"/>
<input t-ref="someDiv"/>
<span>hello</span>
</div>
```
In this example, the component will be able to access the `div` and the component
`SubComponent` using the `useRef` hook:
`SubComponent` with the `useRef` hook:
```js
class Parent extends Component {
subRef = useRef("someComponent");
divRef = useRef("someDiv");
inputRef = useRef("someComponent");
someMethod() {
// here, if component is mounted, refs are active:
// - this.divRef.el is the div HTMLElement
// - this.subRef.comp is the instance of the sub component
// - this.subRef.el is the root HTML node of the sub component (i.e. this.subRef.comp.el)
// - this.inputRef.el is the input HTMLElement
}
}
```
As shown by the example above, html elements are accessed by using the `el`
key, and components references are accessed with `comp`.
Notes:
- if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`,
- on a component, accessing `ref.el` will get the root node of the component.
As shown by the example above, the actual HTMLElement instance is accessed with
the `el` key.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb_templating_language.md#dynamic-attributes) and
(like the [`t-attf-`](templates.md#dynamic-attributes) and
`t-component` directives). For example,
```xml
@@ -343,13 +160,12 @@ all components. But sometimes, we want to _scope_ that knowledge to a subtree.
For example, if we have a form view component, maybe we would like to make some
`model` object available to all sub components, but not to the whole application.
This is where the `useSubEnv` hook may be useful: it lets a component add some
information to the environment in a way that only the component and its children
information to the environment in a way that only its children
can access it:
```js
class FormComponent extends Component {
constructor(...args) {
super(...args);
setup() {
const model = makeModel();
useSubEnv({ model });
}
@@ -377,70 +193,121 @@ useExternalListener(window, "click", this.closeMenu);
The `useComponent` hook is useful as a building block for some customized hooks,
that may need a reference to the component calling them.
```js
function useSomething() {
const component = useComponent();
// now, component is bound to the instance of the current component
}
```
### `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
```js
function useSomething() {
const env = useEnv();
// now, env is bound to the env of the current component
}
```
Hooks are a wonderful way to organize the code of a complex component by feature
instead of by lifecycle methods. They are like mixins, except that they can be
easily composed together.
### `useEffect`
But, like every good things in life, hooks should be used with moderation. They are
not the solution to every problem.
This hook will run a callback when a component is mounted and patched, and
will run a cleanup function before patching and before unmounting the
the component (only if some dependencies have changed).
- they may be overkill: if your component needs to perform some action specific
to itself (so, the specific code does not need to be shared), there is nothing
wrong with a simple class method:
It has almost the same API as the React `useEffect` hook, except that the dependencies
are defined by a function instead of just the dependencies.
```js
// maybe overkill
class A extends Component {
constructor(...args) {
super(...args);
useMySpecificHook();
}
The `useEffect` hook takes two function: the effect function and the dependency
function. The effect function perform some task and return (optionally) a cleanup
function. The dependency function returns a list of dependencies. If any of these
dependencies changes, then the current effect will be cleaned up and reexecuted.
Here is an example without any dependencies:
```js
useEffect(
() => {
window.addEventListener("mousemove", someHandler);
return () => window.removeEventListener("mousemove", someHandler);
},
() => []
);
```
In the example above, the dependency list is empty, so the effect is only cleaned
up when the component is unmounted.
If the dependency function is skipped, then the effect will be cleaned up and
rerun at every patch.
Here is another example, of how one could implement a `useAutofocus` hook with
the `useEffect` hook:
```js
function useAutofocus(name) {
let ref = useRef(name);
useEffect(
(el) => el && el.focus(),
() => [ref.el]
);
}
```
This hook takes the name of a valid `t-ref` directive, which should be present
in the template. It then checks whenever the component is mounted or patched if
the reference is not valid, and in this case, it will focus the node element.
This hook can be used like this:
```js
class SomeComponent extends Component {
static template = xml`
<div>
<input />
<input t-ref="myinput"/>
</div>`;
setup() {
useAutofocus("myinput");
}
}
```
// ok
class B extends Component {
constructor(...args) {
super(...args);
this.performSpecificTask();
}
## Example: mouse position
Here is the classical example of a non trivial hook to track the mouse position.
```js
const { useState, onWillDestroy, Component } = owl;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({ x: 0, y: 0 });
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
```
window.addEventListener("mousemove", update);
onWillDestroy(() => {
window.removeEventListener("mousemove", update);
});
Note that the second solution is easier to extend in sub components.
return position;
}
- they may be harder to test: if a customized hook injects some external side
effect dependency, then it is harder to test without doing some non obvious
manipulation. For example, assume that we want to give a reference to a
router in a `useRouter` hook. We could do this:
// Main root component
class Root extends Component {
static template = xml`<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>`;
```js
const router = new Router(...);
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
}
```
function useRouter() {
return router;
}
```
As you can see, this does not _hook_ into the internal of the component. It
simply returns a global object, which is difficult to mock.
A better way would be to do something like this: get the reference from the
environment.
```js
function useRouter() {
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 that we use the prefix `use` for hooks, just like in React. This is just
a convention.
+92
View File
@@ -0,0 +1,92 @@
# 🦉 Form Input Bindings 🦉
It is very common to need to be able to read the value out of an html `input` (or
`textarea`, or `select`) in order to use it (note: it does not need to be in a
form!). A possible way to do this is to do it by hand:
```js
class Form extends owl.Component {
state = useState({ text: "" });
_updateInputValue(event) {
this.state.text = event.target.value;
}
}
```
```xml
<div>
<input t-on-input="_updateInputValue" />
<span t-esc="state.text" />
</div>
```
This works. However, this requires a little bit of _plumbing_ code. Also, the
plumbing code is slightly different if you need to interact with a checkbox,
or with radio buttons, or with select tags.
To help with this situation, Owl has a builtin directive `t-model`: its value
should be an observed value in the component (usually `state.someValue`). With
the `t-model` directive, we can write a shorter code, equivalent to the previous
example:
```js
class Form extends owl.Component {
state = { text: "" };
}
```
```xml
<div>
<input t-model="state.text" />
<span t-esc="state.text" />
</div>
```
The `t-model` directive works with `<input>`, `<input type="checkbox">`,
`<input type="radio">`, `<textarea>` and `<select>`:
```xml
<div>
<div>Text in an input: <input t-model="state.someVal"/></div>
<div>Textarea: <textarea t-model="state.otherVal"/></div>
<div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
<div>Selection:
<select t-model="state.color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
<div>
Selection with radio buttons:
<span>
<input type="radio" name="color" id="red" value="red" t-model="state.color"/>
<label for="red">Red</label>
</span>
<span>
<input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
<label for="blue">Blue</label>
</span>
</div>
</div>
```
Like event handling, the `t-model` directive accepts the following modifiers:
| Modifier | Description |
| --------- | -------------------------------------------------------------------- |
| `.lazy` | update the value on the `change` event (default is on `input` event) |
| `.number` | try to parse the value to a number (using `parseFloat`) |
| `.trim` | trim the resulting value |
For example:
```xml
<input t-model.lazy="state.someVal" />
```
These modifiers can be combined. For instance, `t-model.lazy.number` will only
update a number whenever the change is done.
Note: the online playground has an example to show how it works.
-133
View File
@@ -1,133 +0,0 @@
# 🦉 Miscellaneous 🦉
## Content
- [Portal](#portal)
- [AsyncRoot](#asyncroot)
## `Portal`
### Overview
The component `Portal` is meant to be used as a transparent way to 'teleport' a piece
of DOM to the node represented by its sole `target` props.
This component aims at helping the implementation of the needed infrastructure
for modals (as in `bootstrap-modal`).
### Usage
The content it will teleport is defined within the `<Portal>` node and
internally uses the `default` [Slot](slots.md).
This slot must contain only **one** node, which in turn can have as many children as necessary.
The element under which the content will be teleported is represented as a selector
by the `target` props which only accepts a string as value.
The `target` props only supports static selector, and is not meant to be passed to `Portal`
as a variable. Namely, `<Portal target="'body'" />` is the intended use.
By contrast, `<Portal target="state.target" />` is not supported.
The component `Portal` has no particular state, rather it is meant to be a slave to its parent,
and ultimately just a way for the parent to teleport a piece of its own DOM elsewhere.
The `Portal`'s root node is always `<portal/>` and is placed where the teleported content
_would have_ been. It is this element that the [teleported events](#expected-behaviors) are re-directed on.
### Example
The canonic use-case is to implement a Dialog, where a Component may choose to break the natural
workflow to help the user put in some data, which it could use later on.
JavaScript:
```js
const { Component, mount } = owl;
const { Portal } = owl.misc;
class TeleportedComponent extends Component {}
class App extends Component {
static components = { Portal, TeleportedComponent };
}
mount(App, { target: document.body });
```
XML:
```xml
<templates>
<div t-name="TeleportedComponent">
<span>I will move soon enough</span>
</div>
<div t-name="App">
<span>I am like the rest of us</span>
<Portal target="'body'">
<TeleportedComponent />
</Portal>
</div>
</templates>
```
In this example, the `Portal` component will teleport the `TeleportedComponent`'s `div` as a child of the `body`.
`TeleportedComponent` is acting as a Dialog here.
The resulting DOM will look like:
```xml
<body>
<div>
<span>I am like the rest of us</span>
<portal></portal>
</div>
<div>
<span>I will move soon enough</span>
</div>
</body>
```
### Expected Behaviors
The teleported piece is updated as any other `Component`'s DOM and in the same sequence.
Namely the teleported piece will be updated in function of its parents components, and patched as
a normal child.
The [_business_ events](event_handling.md#business-dom-events) triggered by a child component will be stopped
to not bubble outside of the `target`. They will, on the other hand, be re-directed onto the
`Portal`'s root node and bubble up the DOM as if it were triggered by a regular child component.
Beware that those re-directed events are copies of the original event.
They have:
- The same payload.
- The same `originalComponent` than their original counterpart,
that is the actual Component that triggered it.
- A **different** `target` property than their original counterpart.
The `target` of a re-directed event is necessarily the `Portal`'s root node.
Pure DOM events do not follow this pattern and are free to bubble their natural, unaltered way
up to the `body`.
## `AsyncRoot`
When this component is used, a new rendering sub tree is created, such that the
rendering of that component (and its children) is not tied to the rendering of
the rest of the interface. It can be used on an asynchronous component, to
prevent it from delaying the rendering of the whole interface, or on a
synchronous one, such that its rendering isn't delayed by other (asynchronous)
components. Note that this directive has no effect on the first rendering, but
only on subsequent ones (triggered by state or props changes).
```xml
<div t-name="ParentComponent">
<SyncChild />
<AsyncRoot>
<AsyncChild/>
</AsyncRoot>
</div>
```
The `AsyncRoot` assumes that there is exactly one root node inside it. It can
be a dom node or a component.
-60
View File
@@ -1,60 +0,0 @@
# 🦉 Mounting an application 🦉
## Content
- [Overview](#overview)
- [API](#api)
## Overview
Mounting an Owl application is done by using the `mount` method (available in
`owl.mount` if you are using the iife build, or it can be directly imported
from `owl` if you are using a module system):
```js
const mount = { owl }; // if owl is available as an object
const env = { ... };
const app = await mount(MyComponent, { target: document.body, env });
```
Another example:
```js
const config = {
env: ...,
props: ...,
target: document.body,
position: "self",
};
const app = await mount(App, config);
```
A common way to initialize an application is to first setup an environment,
then to call the `mount` method.
## API
Mount takes two parameters:
- `C`, which should be a component class (NOT instance),
- `params`, which is an object with the following keys:
- `target (HTMLElement | DocumentFragment)`: the target of the mount operation
- `env (optional, Env)` an environment
- `position (optional, "first-child" | "last-child" | "self")` the position
where it should be mounted (see below for more informations)
- `props (optional, any)`: some initial values that are given as props. Useful
when the root component is configurable, or when testing sub components
Here are the various positions supported by Owl:
- `first-child`: with this option, the component will be prepended inside the target,
- `last-child` (default value): with this option, the component will be
appended in the target element,
- `self`: the target will be used as the root element for the component. This
means that the target has to be an HTMLElement (and not a document fragment).
In this situation, it is possible that the component cannot be unmounted. For
example, if its target is `document.body`.
The `mount` method returns a promise that resolves to the instance of the created
component.
-52
View File
@@ -1,52 +0,0 @@
# 🦉 Observer 🦉
Owl needs to be able to react to state changes. For example, whenever the state
of a component is changed, Owl needs to rerender it. To help with that, there is
an Observer class. Its job is to observe the state of an object (or array), and
to react to any change. The observer is implemented with the native `Proxy`
object. Note that this means that it will not work on older browsers.
Note that the `Observer` is used by the `useState` and `useContext` hooks. This
is the way most Owl applications will create observers. For the majority of
use cases, there is no need to directly instantiate an observer.
## Example
For example, this code will display `update` in the console:
```javascript
const observer = new owl.core.Observer();
observer.notifyCB = () => console.log("update");
const obj = observer.observe({ a: { b: 1 } });
obj.a.b = 2;
```
This example shows that an observer can observe nested properties.
## Reference
**observe** An observer can observe multiple values with the `observe` method.
This method takes an object or an array as its argument and will return a proxy
(which is mapped to the initial object/array). With this proxy, the observer
can detect whenever any internal value is changed.
**Registering a callback** Whenever an observer sees a state change, it will
call its `notifyCB` method. No additional information is given to the callback.
**deepRevNumber** Each observed value has an internal revision number, which
is incremented every time the value is observed. Sometimes, it can be useful
to obtain that number:
```js
const observer = new owl.core.Observer();
const obj = observer.observe({ a: { b: 1 } });
observer.revNumber(obj.a); // 1
obj.a.b = 2;
observer.revNumber(obj.a); // 2
```
The `revNumber` can also return 0, which indicates that the value is not
observed.
+17
View File
@@ -0,0 +1,17 @@
# 🦉 Portal 🦉
It is sometimes useful to be able to render some content outside the boundaries
of a component. To do that, Owl provides a special directive: `t-portal`:
```js
class SomeComponent extends Component {
static template = xml`
<div>this is inside the component</div>
<div t-portal="'body'">and this is outside</div>
`;
}
```
The `t-portal` directive takes a valid css selector as argument. The content of
the portalled template will be mounted at the corresponding location. Note that
Owl need to insert an empty text node at the location of the portalled content.
+186 -22
View File
@@ -4,8 +4,11 @@
- [Overview](#overview)
- [Definition](#definition)
- [Good Practices](#good-practices)
- [Binding function props](#binding-function-props)
- [Dynamic Props](#dynamic-props)
- [Default Props](#default-props)
- [Props validation](#props-validation)
- [Good Practices](#good-practices)
## Overview
@@ -38,8 +41,6 @@ The `props` object is made of every attributes defined on the template, with the
following exceptions:
- every attribute starting with `t-` are not props (they are QWeb directives),
- `style` and `class` attributes are excluded as well (they are applied by Owl on
the root element of the component).
In the following example:
@@ -47,7 +48,6 @@ In the following example:
<div>
<ComponentA a="state.a" b="'string'"/>
<ComponentB t-if="state.flag" model="model"/>
<ComponentC style="color:red;" class="left-pane" />
</div>
```
@@ -55,7 +55,188 @@ the `props` object contains the following keys:
- for `ComponentA`: `a` and `b`,
- for `ComponentB`: `model`,
- for `ComponentC`: empty object
## Binding function props
It is common to have the need to pass a callback as a prop. Since Owl components
are class based, the callback frequently needs to be bound to its owner component.
So, one can do this:
```js
class SomeComponent extends Component {
static template = xml`
<div>
<Child callback="doSomething"/>
</div>`;
setup() {
this.doSomething = this.doSomething.bind(this);
}
doSomething() {
// ...
}
}
```
However, this is such a common use case that Owl provides a special suffix to do
just that: `.bind`. This looks like this:
```js
class SomeComponent extends Component {
static template = xml`
<div>
<Child callback.bind="doSomething"/>
</div>`;
doSomething() {
// ...
}
}
```
## Dynamic Props
The `t-props` directive can be used to specify totally dynamic props:
```xml
<div t-name="ParentComponent">
<Child t-props="some.obj"/>
</div>
```
```js
class ParentComponent {
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
```
## Default Props
If the static `defaultProps` property is defined, it will be used to complete
props received by the parent, if missing.
```js
class Counter extends owl.Component {
static defaultProps = {
initialValue: 0,
};
...
}
```
In the example above, the `initialValue` props is now by default set to 0.
## Props Validation
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
- hard to tell how a component should be used, by looking at its code.
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
A props type system solves both issues, by describing the types and shapes
of the props. Here is how it works in Owl:
- `props` key is a static key (so, different from `this.props` in a component instance)
- it is optional: it is ok for a component to not define a `props` key.
- props are validated whenever a component is created/updated
- props are only validated in `dev` mode (see [how to configure an app](app.md#configuration))
- if a key does not match the description, an error is thrown
- it validates keys defined in (static) `props`. Additional keys given by the
parent will cause an error (unless the special prop `*` is present).
- it is an object or a list of strings
- a list of strings is a simplified props definition, which only lists the name
of the props. Also, if the name ends with `?`, it is considered optional.
- all props are by default required, unless they are defined with `optional: true`
(in that case, it is only done if there is a value)
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
constructor functions (so, if you have a `Person` class, it can be used as a type)
- arrays are homogeneous (all elements have the same type/shape)
For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
- a boolean: indicate that the props exists, and is mandatory.
- a constructor: this should describe the type, for example: `id: Number` describe
the props `id` as a number
- a list of constructors. In that case, this means that we allow more than one
type. For example, `id: [Number, String]` means that `id` can be either a string
or a number.
- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory):
- `type`: the main type of the prop being validated
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
- `validate`: this is a function which should return a boolean to determine if
the value is valid or not. Useful for custom validation logic.
There is a special `*` prop that means that additional prop are allowed. This is
sometimes useful for generic components that will propagate some or all their
props to their child components.
Examples:
```js
class ComponentA extends owl.Component {
static props = ['id', 'url'];
...
}
class ComponentB extends owl.Component {
static props = {
count: {type: Number},
messages: {
type: Array,
element: {type: Object, shape: {id: Boolean, text: String }
},
date: Date,
combinedVal: [Number, Boolean]
};
...
}
```
```js
// only the existence of those 3 keys is documented
static props = ['message', 'id', 'date'];
```
```js
// only the existence of those 3 keys is documented. any other key is allowed.
static props = ['message', 'id', 'date', '*'];
```
```js
// size is optional
static props = ['message', 'size?'];
```
```js
static props = {
messageIds: {type: Array, element: Number}, // list of number
otherArr: {type: Array}, // just array. no validation is made on sub elements
otherArr2: Array, // same as otherArr
someObj: {type: Object}, // just an object, no internal validation
someObj2: {
type: Object,
shape: {
id: Number,
name: {type: String, optional: true},
url: String
]}, // object, with keys id (number), name (string, optional) and url (string)
someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop
kindofsmallnumber: {
type: Number,
validate: n => (0 <= n && n <= 10)
},
size: {
validate: e => ["small", "medium", "large"].includes(e)
},
};
```
## Good Practices
@@ -78,20 +259,3 @@ sent to the parent (for example, with an event).
Any value can go in a props. Strings, objects, classes, or even callbacks could
be given to a child component (but then, in the case of callbacks, communicating
with events seems more appropriate).
## Dynamic Props
The `t-props` directive can be used to specify totally dynamic props:
```xml
<div t-name="ParentComponent">
<Child t-props="some.obj"/>
</div>
```
```js
class ParentComponent {
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
```
-103
View File
@@ -1,103 +0,0 @@
# 🦉 Props Validation 🦉
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
- hard to tell how a component should be used, by looking at its code.
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
A props type system solves both issues, by describing the types and shapes
of the props. Here is how it works in Owl:
- `props` key is a static key (so, different from `this.props` in a component instance)
- it is optional: it is ok for a component to not define a `props` key.
- props are validated whenever a component is created/updated
- props are only validated in `dev` mode (see [config page](config.md#mode))
- if a key does not match the description, an error is thrown
- it validates keys defined in (static) `props`. Additional keys given by the
parent will cause an error.
For example:
```js
class ComponentA extends owl.Component {
static props = ['id', 'url'];
...
}
class ComponentB extends owl.Component {
static props = {
count: {type: Number},
messages: {
type: Array,
element: {type: Object, shape: {id: Boolean, text: String }
},
date: Date,
combinedVal: [Number, Boolean]
};
...
}
```
- it is an object or a list of strings
- a list of strings is a simplified props definition, which only lists the name
of the props. Also, if the name ends with `?`, it is considered optional.
- all props are by default required, unless they are defined with `optional: true`
(in that case, validation is only done if there is a value)
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
constructor functions (so, if you have a `Person` class, it can be used as a type)
- arrays are homogeneous (all elements have the same type/shape)
For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
- a boolean: indicate that the props exists, and is mandatory.
- a constructor: this should describe the type, for example: `id: Number` describe
the props `id` as a number
- a list of constructors. In that case, this means that we allow more than one
type. For example, `id: [Number, String]` means that `id` can be either a string
or a number.
- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory):
- `type`: the main type of the prop being validated
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
- `validate`: this is a function which should return a boolean to determine if
the value is valid or not. Useful for custom validation logic.
Examples:
```js
// only the existence of those 3 keys is documented
static props = ['message', 'id', 'date'];
```
```js
// size is optional
static props = ['message', 'size?'];
```
```js
static props = {
messageIds: {type: Array, element: Number}, // list of number
otherArr: {type: Array}, // just array. no validation is made on sub elements
otherArr2: Array, // same as otherArr
someObj: {type: Object}, // just an object, no internal validation
someObj2: {
type: Object,
shape: {
id: Number,
name: {type: String, optional: true},
url: String
]}, // object, with keys id (number), name (string, optional) and url (string)
someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop
kindofsmallnumber: {
type: Number,
validate: n => (0 <= n && n <= 10)
},
size: {
validate: e => ["small", "medium", "large"].includes(e)
},
};
```
-153
View File
@@ -1,153 +0,0 @@
# 🦉 QWeb Engine 🦉
## Content
- [Overview](#overview)
- [Reference](#reference)
## Overview
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
templating engine used by Odoo. The QWeb class in the OWL project is an
implementation of that specification with a few interesting points:
- it compiles templates into functions that output a virtual DOM instead of a
string. This is necessary for the component system.
- it has a few extra directives: `t-component`, `t-on`, ...
We present in this section the engine, not the templating language.
## Reference
This section is about the javascript code that implements the `QWeb` specification.
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
instantiated:
```js
const qweb = new owl.QWeb();
```
Its API is quite simple:
- **`constructor(config)`**: constructor. Takes an optional configuration object
with an optional `templates` string to add initial
templates (see `addTemplates` for more information on format of the string)
and an optional `translateFn` translate function (see the section on
[translations](#translations)).
```js
const qweb = new owl.QWeb({ templates: TEMPLATES, translateFn: _t });
```
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
```js
qweb.addTemplate("mytemplate", "<div>hello</div>");
```
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply
ignore templates added for a second time. Otherwise, `QWeb` will crash.
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute).
```js
const TEMPLATES = `
<templates>
<div t-name="App" class="main">main</div>
<div t-name="OtherComponent">other component</div>
</templates>`;
qweb.addTemplates(TEMPLATES);
```
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
which is a virtual representation of the DOM (see [vdom doc](../miscellaneous/vdom.md)).
```js
const vnode = qweb.render("App", component);
```
- **`renderToString(name, context)`**: renders a template, but returns an html
string.
```js
const str = qweb.renderToString("someTemplate", somecontext);
```
- **`registerTemplate(name, template)`**: static function to register a global
QWeb template. This is useful for commonly used components accross the
application, and for making a template available to an application without
having a reference to the actual QWeb instance.
```js
QWeb.registerTemplate("mytemplate", `<div>some template</div>`);
```
- **`registerComponent(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-component` directive). This is useful for commonly used
components accross the application.
```js
class Dialog extends owl.Component { ... }
QWeb.registerComponent("Dialog", Dialog);
...
class ParentComponent extends owl.Component { ... }
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
```
In some way, a `QWeb` instance is the core of an Owl application. It is the only
mandatory element of an [environment](environment.md). As such, it
has an extra responsibility: it can act as an event bus for internal communication
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
### Translations
take care of this and "cherry-pick" 8464a1b04e7469434f9dcb3d68a543f58cb61b8e
If properly setup, Owl QWeb engine can translate all rendered templates. To do
so, it needs a translate function, which takes a string and returns a string.
For example:
```js
const translations = {
hello: "bonjour",
yes: "oui",
no: "non",
};
const translateFn = (str) => translations[str] || str;
const qweb = new QWeb({ translateFn });
```
Once setup, all rendered templates will be translated using `translateFn`:
- each text node will be replaced with its translation,
- each of the following attribute values will be translated as well: `title`,
`placeholder`, `label` and `alt`,
- translating text nodes can be disabled with the special attribute `t-translation`,
if its value is `off`.
So, with the above `translateFn`, the following templates:
```xml
<div>hello</div>
<div t-translation="off">hello</div>
<div>Are you sure?</div>
<input placeholder="hello" other="yes"/>
```
will be rendered as:
```xml
<div>bonjour</div>
<div>hello</div>
<div>Are you sure?</div>
<input placeholder="bonjour" other="yes"/>
```
Note that the translation is done during the compilation of the template, not
when it is rendered.
+80
View File
@@ -0,0 +1,80 @@
# 🦉 Reactivity 🦉
## Content
- [Overview](#overview)
- [`useState`](#usestate)
- [`reactive`](#reactive)
## Overview
Reactivity is a big topic in javascript frameworks. The goal is to provide a
simple way to manipulate state, in such a way that the interface automatically
update accordingly to state changes. Also, we obviously want this to happen in
a performant way.
To solve this issue, Owl provides two reactivity primitives:
- `reactive`, which returns a proxy to its first argument, and tracks all read/update
operation going through it,
- `useState`: a hook, that internally uses `reactive`, and is linked to its
owner component: any read operation will be tracked (key by key), and any
updates to these tracked values will cause the component to be rerendered.
Most of the time, the `useState` hook is the best solution.
## `useState`
Let us start by an example of how `useState` could be used:
```js
class Counter extends Component {
static template = xml`
<div t-on-click="increment">
<t t-esc="state.value"/>
</div>`;
setup() {
this.state = useState({ value: 0 });
}
increment() {
this.state.value++;
}
}
```
If one were to use a simple state object, Owl would not be aware that the value
was changed and that the component should be rerendered. With the `useState`
hook, `this.state` is now a reactive object, so this component works as expected.
## `reactive`
The `reactive` function is the basic reactivity primitive. It takes an object
or an array as first argument, and optionally, a function as the second argument.
The function will be called whenever any tracked value is updated.
```js
const obj = reactive({ a: 1 }, () => console.log("changed"));
obj.a = 2; // does not log anything: the 'a' key was not read
console.log(obj.a); // log 2, and reads the 'a' key => it is now tracked
obj.a = 3; // log 'changed' because we updated a tracked value
```
An important property of reactive objects is that they can be reobserved: this
will create an independant proxy that tracks another set of keys:
```js
const obj1 = reactive({ a: 1, b: 2 }, () => console.log("observer 1"));
const obj2 = reactive(obj1, () => console.log("observer 2"));
console.log(obj1.a); // log 1, and reads the 'a' key => it is now tracked by observer 1
console.log(obj1.b); // log 2, and 'b' is now tracked by observer 1
console.log(obj2.b); // log 2, and 'b' is now tracked by observer 1
obj2.a = 3; // log 'observer1', because observer2 does not track a
obj2.b = 3; // log 'observer1' and 'observer2'
```
Obviously, one can use `reactive` on the result of a `useState` if wanted, this
is the proper way to watch for some state changes.
+37
View File
@@ -0,0 +1,37 @@
# 🦉 References 🦉
The `useRef` hook is useful when we need a way to interact with some inside part
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
targeted by the `t-ref` directive. See the [hooks section](hooks.md#useref) for
more detail.
As a short example, here is how we could set the focus on a given input:
```xml
<div>
<input t-ref="input"/>
<button t-on-click="focusInput">Click</button>
</div>
```
```js
import { useRef } from "owl/hooks";
class SomeComponent extends Component {
inputRef = useRef("input");
focusInput() {
this.inputRef.el.focus();
}
}
```
Be aware that the `el` property will only be set when the target of the `t-ref`
directive is mounted in the DOM. Otherwise, it will be set to `null`.
The `useRef` hook cannot be used to get a reference to an instance of a sub
component.
Note that this example uses the suffix `ref` to name the reference. This
is not mandatory, but it is a useful convention, so we do not forget that it is
a reference object.
+179 -52
View File
@@ -3,68 +3,89 @@
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Named slots](#named-slots)
- [Rendering Context](#rendering-context)
- [Default Slot](#default-slot)
- [Default Content](#default-content)
- [Dynamic slots](#dynamic-slots)
- [Slots and props](#slots-and-props)
- [Slot params](#slot-params)
- [Slot scopes](#slot-scopes)
## Overview
Owl is a template based component system. There is therefore a need to be able
to make generic components. For example, imagine a generic `Dialog`
component, which is able to display some arbitrary content.
Obviously, we want to use this component everywhere in our application, to
display various different content. The `Dialog` component is technically the
owner of its content, but is only a container. The user of the `Dialog` is
the component that want to _inject_ something inside the `Dialog`. This is
exactly what slots are for.
## Example
To make generic components, it is useful to be able for a parent component to _inject_
some sub template, but still be the owner. For example, a generic dialog component
will need to render some content, some footer, but with the parent as the
rendering context.
Slots are inserted with the `t-slot` directive:
to make generic components. For example, imagine a generic `Navbar`
component, which displays a navbar, but with some customizable content. Since
the specific content is only known to the user of the `Navbar`, it would be nice
to specify it in the template where `Navbar` is used:
```xml
<div t-name="Dialog" class="modal">
<div class="modal-title"><t t-esc="props.title"/></div>
<div class="modal-content">
<div>
<Navbar>
<span>Hello Owl</span>
</Navbar>
</div>
```
This is exactly the way slots work! In the example above, the user of the `Navbar`
component specify some content (here, in the default slot). The `Navbar`
component can insert that content in its own template at the appropriate location.
An important information to notice is that the content of the slot is rendered in
the parent context, not in the navbar. As such, it can access values and methods
from the parent component.
Here is how the `Navbar` component could be defined, with the `t-slot` directive:
```xml
<div class="navbar">
<t t-slot="default"/>
<ul>
<!-- rest of the navbar here -->
</ul>
</div>
```
## Named slots
Default slots are very useful, but sometimes, we may need more than one slot.
This is what named slots are for! For example, suppose we implement a component
`InfoBox` that display a title and some specific content. Its template could look
like this:
```xml
<div class="info-box">
<div class="info-box-title">
<t t-slot="title"/>
<span class="info-box-close-button" t-on-click="close">X</span>
</div>
<div class="info-box-content">
<t t-slot="content"/>
</div>
<div class="modal-footer">
<t t-slot="footer"/>
</div>
</div>
```
Slots are defined by the caller, with the `t-set-slot` directive:
And one could use it with the `t-set-slot` directive:
```xml
<div t-name="SomeComponent">
<div>some component</div>
<Dialog title="'Some Dialog'">
<t t-set-slot="content">
<div>hey</div>
</t>
<t t-set-slot="footer">
<button t-on-click="doSomething">ok</button>
</t>
</Dialog>
</div>
<InfoBox>
<t t-set-slot="title">
Specific Title. It could be html also.
</t>
<t t-set-slot="content">
<!-- some template here, with html, events, whatever -->
</t>
</InfoBox>
```
In this example, the component `Dialog` will render the slots `content` and `footer`
with its parent as rendering context. This means that clicking on the button
will execute the `doSomething` method on the parent, not on the dialog.
## Rendering context
Note: Owl previously used the `t-set` directive to define the content of a slot.
This is deprecated and should no longer be used in new code.
The content of the slots is actually rendered with the rendering context corresponding
to where it was defined, not where it is positioned. This allows the user to define
event handlers that will be bound to the correct component (usually, the
grandparent of the slot content).
## Reference
### Default Slot
## Default Slot
The first element inside the component which is not a named slot will
be considered the `default` slot. For example:
@@ -81,7 +102,19 @@ be considered the `default` slot. For example:
</div>
```
### Default content
One can mix default slot and named slots:
```xml
<div>
<Child>
default content
<t t-set-slot="footer">
content for footer slot here
</Child>
</div>
```
## Default content
Slots can define a default content, in case the parent did not define them:
@@ -96,12 +129,7 @@ Slots can define a default content, in case the parent did not define them:
<!-- will be rendered as: <div><span>default content</span></div> -->
```
Rendering context: the content of the slots is actually rendered with the
rendering context corresponding to where it was defined, not where it is
positioned. This allows the user to define event handlers that will be bound
to the correct component (usually, the grandparent of the slot content).
### Dynamic Slots
## Dynamic Slots
The `t-slot` directive is actually able to use any expressions, using string
interplolation:
@@ -109,3 +137,102 @@ interplolation:
```xml
<t t-slot="{{current}}" />
```
This will evaluate the `current` expression, and insert the corresponding slot
at the place of the `t-slot` directive.
## Slots and props
In a sense, slots are almost the same as a prop: they define some information
to pass to the child component. To make it possible to use it, and to pass it
down to sub component, Owl actually define a special prop `slots` that contains
all slot information given to the component. It looks like this:
```js
{ slotName_1: slotInfo_1, ..., slotName_m: slotInfo_m }
```
So, a component can pass its slots to a subcomponent like this:
```xml
<Child slots="props.slots"/>
```
## Slot params
For advanced usecases, it may be necessary to pass additional information to a
slot. This can be done by providing extra key/value pairs to the `t-set-slot`
directive. Then, the generic component can read them in its prop `slots`.
For example, here is how a Notebook component could be implemented (a component
with multiple page, and a tab bar, which only render the current active page,
and each page has a title).
```js
class Notebook extends Component {
static template = xml`
<div class="notebook">
<div class="tabs">
<t t-foreach="tabNames" t-as="tab" t-key="tab_index">
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab">
<t t-esc="props.slots[tab].title"/>
</span>
</t>
</div>
<div class="page">
<t t-slot="{{currentSlot}}"/>
</div>
</div>`;
setup() {
this.state = useState({ activeTab: 0 });
this.tabNames = Object.keys(this.props.slots);
}
get currentSlot() {
return this.tabNames[this.state.activeTab];
}
}
```
Notice how one can read the `title` value for each slots. Here is how one could
use this `Notebook` component:
```xml
<Notebook>
<t t-set-slot="page1" title="'Page 1'">
<div>this is in the page 1</div>
</t>
<t t-set-slot="page2" title="'Page 2'" hidden="somevalue">
<div>this is in the page 2</div>
</t>
</Notebook>
```
## Slot scopes
For other kind of advanced use cases, the content of a slot may depends on some
specific information specific to the generic component. This is the opposite
of the slot params.
To solve this kind of problems, one can use the `t-set-scope` directive along
with the `t-set-slot`. This defines the name of a variable that can access
everything given by the child component:
```xml
<div>
<t t-set-slot="foo" t-set-scope="scope">
content
<t t-esc="scope.bool"/>
<t t-esc="scope.num"/>
</t>
</div>
```
And the child component that includes the slot can provide values like this:
```xml
<div>
<t t-slot="foo" bool="other_var" num="5">
</div>
```
-184
View File
@@ -1,184 +0,0 @@
# 🦉 Tags 🦉
## Content
- [Overview](#overview)
- [`xml` tag](#xml-tag)
- [`css` tag](#css-tag)
## Overview
Tags are very small helpers intended to make it easy to write inline templates
or styles. There are currently two tags: `css` and `xml`. With these functions,
it is possible to write [single file components](../learning/how_to_write_sfc.md).
## XML tag
The `xml` tag is certainly the most useful tag. It is used to define an inline
QWeb template for a component. Without tags, creating a standalone component
would look like this:
```js
import { Component } from 'owl'
const name = 'some-unique-name';
const template = `
<div>
<span t-if="somecondition">text</span>
<button t-on-click="someMethod">Click</button>
</div>
`;
QWeb.registerTemplate(name, template);
class MyComponent extends Component {
static template = name;
...
}
```
With tags, this process is slightly simplified. The name is uniquely generated,
and the template is automatically registered:
```js
const { Component } = owl;
const { xml } = owl.tags;
class MyComponent extends Component {
static template = xml`
<div>
<span t-if="somecondition">text</span>
<button t-on-click="someMethod">Click</button>
</div>
`;
...
}
```
## CSS tag
The CSS tag is useful to define a css stylesheet in the javascript file:
```js
class MyComponent extends Component {
static template = xml`
<div class="my-component">some template</div>
`;
static style = css`
.my-component {
color: red;
}
`;
}
```
The `css` tag registers internally the css information. Then, whenever the first
instance of the component is created, will add a `<style>` tag to the document
`<head>`.
Note that to make it more useful, like other css preprocessors, the `css` tag
accepts a small extension of the css specification: css scopes can be nested,
and the rules will then be expanded by the `css` helper:
```scss
.my-component {
display: block;
.sub-component h {
color: red;
}
}
```
will be formatted as:
```css
.my-component {
display: block;
}
.my-component .sub-component h {
color: red;
}
```
This extension brings another useful feature: the `&` selector which refers to
the parent selector. For example, we want our component to be red when hovered.
We would like to write something like:
```scss
.my-component {
display: block;
:hover {
color: red;
}
}
```
but it will be formatted as:
```css
.my-component {
display: block;
}
.my-component :hover {
color: red;
}
```
The `&` selector can be used to solve this problem:
```scss
.my-component {
display: block;
&:hover {
color: red;
}
}
```
will be formatted as:
```css
.my-component {
display: block;
}
.my-component:hover {
color: red;
}
```
Now, there is no additional processing done by the `css` tag. However, since it
is done in javascript at runtime, we actually have more power. For example:
1. sharing values between javascript and css:
```js
import { theme } from "./theme";
class MyComponent extends Component {
static template = xml`<div class="my-component">...</div>`;
static style = css`
.my-component {
color: ${theme.MAIN_COLOR};
background-color: ${theme.SECONDARY_color};
}
`;
}
```
2. scoping rules to the current component:
```js
import { generateUUID } from "./utils";
const uuid = generateUUID();
class MyComponent extends Component {
static template = xml`<div data-o-${uuid}="">...</div>`;
static style = css`
[data-o-${uuid}] {
color: red;
}
`;
}
```
@@ -1,12 +1,11 @@
# 🦉 QWeb Templating Language🦉
# 🦉 Templates 🦉
## Content
- [Overview](#overview)
- [Directives](#directives)
- [Reference](#reference)
- [QWeb Template reference](#qweb-template-reference)
- [White Spaces](#white-spaces)
- [Root Nodes](#root-nodes)
- [Expression Evaluation](#expression-evaluation)
- [Static html Nodes](#static-html-nodes)
- [Outputting Data](#outputting-data)
@@ -16,17 +15,19 @@
- [Dynamic Class Attribute](#dynamic-class-attribute)
- [Dynamic Tag Names](#dynamic-tag-names)
- [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates)
- [Sub Templates](#sub-templates)
- [Dynamic Sub Templates](#dynamic-sub-templates)
- [Translations](#translations)
- [Debugging](#debugging)
- [Fragments](#fragments)
- [Inline templates](#inline-templates)
- [Rendering svg](#rendering-svg)
## Overview
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
templating engine used by Odoo. It is based on the XML format, and used
Owl templates are describe using the [QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) specification. It is based on the XML format, and used
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the HTML.
generate a virtual dom representation of the HTML. Also, since Owl is a live
component system, there are additional directives specific to Owl (such as `t-on`).
```xml
<div>
@@ -53,34 +54,33 @@ extensions.
For reference, here is a list of all standard QWeb directives:
| Name | Description |
| ------------------------------ | -------------------------------------------------------------- |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-translation` | [Disabling the translation of a node](#translations) |
| `t-name` | [Defining a template (not really a directive)](qweb_engine.md) |
| Name | Description |
| ------------------------------ | --------------------------------------------------------------- |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-translation` | [Disabling the translation of a node](translations.md) |
The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
| Name | Description |
| ------------------------ | ------------------------------------------------------------------------------- |
| `t-component`, `t-props` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
| `t-on-*` | [Event handling](event_handling.md) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-slot` | [Rendering a slot](slots.md) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
| Name | Description |
| -------------------------------------- | --------------------------------------------------------------- |
| `t-component`, `t-props` | [Defining a sub component](component.md#sub-components) |
| `t-ref` | [Setting a reference to a dom node or a sub component](refs.md) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
| `t-on-*` | [Event handling](event_handling.md) |
| `t-portal` | [Portal](portal.md) |
| `t-slot`, `t-set-slot`, `t-slot-scope` | [Rendering a slot](slots.md) |
| `t-model` | [Form input bindings](input_bindings.md) |
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
## Reference
## QWeb Template Reference
### White Spaces
@@ -90,32 +90,6 @@ White spaces in a template are handled in a special way:
- if a whitespace-only text node contains a linebreak, it is ignored
- the previous rules do not apply if we are in a `<pre>` tag
### Root Nodes
For many reasons, Owl QWeb templates should have a single root node. More
precisely, the result of a template rendering should have a single root node:
```xml
<!–– not ok: two root nodes ––>
<t>
<div>foo</div>
<div>bar</div>
</t>
<!–– ok: result has one single root node ––>
<t>
<div t-if="someCondition">foo</div>
<span t-else="">bar</span>
</t>
```
Extra root nodes will actually be ignored (even though they will be rendered
in memory).
Note: this does not apply to subtemplates (see the `t-call` directive). In that
case, they will be inlined in the main template, and can actually have many
root nodes.
### Expression Evaluation
QWeb expressions are strings that will be processed at compile time. Each variable in
@@ -190,24 +164,29 @@ rendered with the value `value` set to `42` in the rendering context yields:
<p>42</p>
```
The `t-raw` directive is almost the same as `t-esc`, but without the escaping.
This is mostly useful to inject a raw html string somewhere. Obviously, this
is unsafe to do in general, and should only be used for strings known to be safe.
The `t-out` directive is almost the same as `t-esc`, but possibly without the
escaping. The difference is that the value received by the `t-out` directive
will only be not-escaped if it has been marked as such, using the `markup`
utility function:
```xml
<p><t t-raw="value"/></p>
For example, in the following component:
```js
const { markup, Component, xml } = owl;
class SomeComponent extends Component {
static template = xml`
<t t-out="value1"/>
<t t-out="value2"/>`;
value1 = "<div>some text 1</div>";
value2 = markup("<div>some text 2</div>");
}
```
rendered with the value `value` set to `<span>foo</span>` in the rendering context yields:
```html
<p><span>foo</span></p>
```
Note that since the content of the expression is not known beforehand, the `t-raw`
directive has to parse the html (and convert it to a virtual dom structure) for
each rendering. So, it will be much slower than a regular template. It is
therefore advised to limit the use of `t-raw` whenever possible.
The first `t-out` will act as a `t-esc` directive, which means that the content
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
this will be injected as html.
### Setting Variables
@@ -368,7 +347,7 @@ collection to iterate on, and a second parameter `t-as` providing the name to us
for the current item of the iteration:
```xml
<t t-foreach="[1, 2, 3]" t-as="i">
<t t-foreach="[1, 2, 3]" t-as="i" t-key="i">
<p><t t-esc="i"/></p>
</t>
```
@@ -384,13 +363,17 @@ will be rendered as:
Like conditions, `t-foreach` applies to the element bearing the directives attribute, and
```xml
<p t-foreach="[1, 2, 3]" t-as="i">
<p t-foreach="[1, 2, 3]" t-as="i" t-key="i">
<t t-esc="i"/>
</p>
```
is equivalent to the previous example.
An important difference should be made with the usual `QWeb` behaviour: Owl
requires the presence of a `t-key` directive, to be able to properly reconcile
renderings.
`t-foreach` can iterate on an array (the current item will be the current value)
or an object (the current item will be the current key).
@@ -416,7 +399,7 @@ into the global context.
<t t-set="existing_variable" t-value="false"/>
<!-- existing_variable now False -->
<p t-foreach="Array(3)" t-as="i">
<p t-foreach="Array(3)" t-as="i" t-key="i">
<t t-set="existing_variable" t-value="true"/>
<t t-set="new_variable" t-value="true"/>
<!-- existing_variable and new_variable now true -->
@@ -430,17 +413,14 @@ Even though Owl tries to be as declarative as possible, the DOM does not fully
expose its state declaratively in the DOM tree. For example, the scrolling state,
the current user selection, the focused element or the state of an input are not
set as attribute in the DOM tree. This is why we use a virtual dom
algorithm to keep the actual DOM node as much as possible.
However, in some situations, this is not enough, and we need to help Owl decide
if an element is actually the same, or is a different element with the same
properties.
algorithm to make sure we keep the actual DOM node instead of replacing it with
a new one.
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
and we render them in this template:
```xml
<p t-foreach="items" t-as="item"><t t-esc="item.text"/></p>
<p t-foreach="items" t-as="item" t-key="item_index"><t t-esc="item.text"/></p>
```
The result will be two `<p>` tags with text `a` and `b`. Now, if we swap them,
@@ -501,7 +481,7 @@ using the `...` javascript operator. For example:
The `...` operator will convert the `Set` (or any other iterables) into a list,
which will work with Owl QWeb.
### Rendering Sub Templates
### Sub Templates
QWeb templates can be used for top level rendering, but they can also be used
from within another template (to avoid duplication or give names to parts of
@@ -574,21 +554,6 @@ using string interpolation. For example:
Here, the name of the template is obtained from the `template` value in the
template rendering context.
### Translations
By default, QWeb specify that templates should be translated. If this behaviour
is not wanted, there is a `t-translation` directive which can turn off
translations (if it is set to the `off` value), with the following rules:
- each text node will be replaced with its translation,
- each of the following attribute values will be translated as well: `title`,
`placeholder`, `label` and `alt`,
- translating text nodes can be disabled with the special attribute `t-translation`,
if its value is `off`.
See [here](qweb_engine.md#translations) for more information on how to setup a
translate function in Owl QWeb.
### Debugging
The javascript QWeb implementation provides two useful debugging directives:
@@ -611,3 +576,105 @@ will stop execution if the browser dev tools are open.
```
will print 42 to the console.
## Fragments
Owl 2 supports templates with an arbitrary number of root elements, or even just
a text node. So, the following templates are all valid:
```xml
hello owl. This is just a text node!
```
```xml
<div>hello</div>
```
```xml
<div>hello</div>
<div>ola</div>
```
```xml
<div t-if="someCondition"><SomeChildComponent/></div>
```
```xml
<t t-if="someCondition"><SomeChildComponent/></t>
```
## Inline templates
Most real applications will define their templates in a XML file, to benefit
from the XML ecosystem, and to do some additional processing, such as translating
them. However, in some cases, it is convenient to be able to define a template
inline. To do so, one can use the `xml` helper function:
```js
const { Component, xml } = owl;
class MyComponent extends Component {
static template = xml`
<div>
<span t-if="somecondition">text</span>
<button t-on-click="someMethod">Click</button>
</div>
`;
...
}
mount(MyComponent, document.body);
```
This function simply generates an unique string id, and register the template
under that id in the internals of Owl, then return the id.
## Rendering svg
Owl components can be used to generate dynamic SVG graphs:
```js
class Node extends Component {
static template = xml`
<g>
<circle t-att-cx="props.x" t-att-cy="props.y" r="4" fill="black"/>
<text t-att-x="props.x - 5" t-att-y="props.y + 18"><t t-esc="props.node.label"/></text>
<t t-set="childx" t-value="props.x + 100"/>
<t t-set="height" t-value="props.height/(props.node.children || []).length"/>
<t t-foreach="props.node.children || []" t-as="child">
<t t-set="childy" t-value="props.y + child_index*height"/>
<line t-att-x1="props.x" t-att-y1="props.y" t-att-x2="childx" t-att-y2="childy" stroke="black" />
<Node x="childx" y="childy" node="child" height="height"/>
</t>
</g>
`;
static components = { Node };
}
class RootNode extends Component {
static template = xml`
<svg height="180">
<Node node="graph" x="10" y="20" height="180"/>
</svg>
`;
static components = { Node };
graph = {
label: "a",
children: [
{ label: "b" },
{ label: "c", children: [{ label: "d" }, { label: "e" }] },
{ label: "f", children: [{ label: "g" }] },
],
};
}
```
This `RootNode` component will then display a live SVG representation of the
graph described by the `graph` property. Note that there is a recursive structure
here: the `Node` component uses itself as a subcomponent.
Note that since SVG needs to be handled in a specific way (its namespace needs
to be properly set), there is a small constraint for Owl components: if an owl
component is supposed to be a part of an svg graph, then its root node needs to
be a `g` tag, so Owl can properly set the namespace.
+59
View File
@@ -0,0 +1,59 @@
# 🦉 Translations 🦉
If properly setup, Owl can translate all rendered templates. To do
so, it needs a translate function, which takes a string and returns a string.
For example:
```js
const translations = {
hello: "bonjour",
yes: "oui",
no: "non",
};
const translateFn = (str) => translations[str] || str;
const app = new App(Root, { templates, tranaslateFn });
// ...
```
See the [app configuration page](app.md#configuration) for more info on how to
configure an Owl application.
Once setup, all rendered templates will be translated using `translateFn`:
- each text node will be replaced with its translation,
- each of the following attribute values will be translated as well: `title`,
`placeholder`, `label` and `alt`,
- translating text nodes can be disabled with the special attribute `t-translation`,
if its value is `off`.
So, with the above `translateFn`, the following templates:
```xml
<div>hello</div>
<div t-translation="off">hello</div>
<div>Are you sure?</div>
<input placeholder="hello" other="yes"/>
```
will be rendered as:
```xml
<div>bonjour</div>
<div>hello</div>
<div>Are you sure?</div>
<input placeholder="bonjour" other="yes"/>
```
Note that the translation is done during the compilation of the template, not
when it is rendered.
In some case, it is useful to be able to extend the list of translatable attributes.
For example, one may want to also translate `data-title` attributes. To do that,
we can define additional attributes with the `translatableAttributes` option:
```js
const app = new App(Root, { templates, tranaslateFn, translatalbeAttributes: ["data-title"] });
// ...
```
+17 -107
View File
@@ -6,11 +6,8 @@ functions are all available in the `owl.utils` namespace.
## Content
- [`whenReady`](#whenready): executing code when DOM is ready
- [`loadJS`](#loadjs): loading script files
- [`loadFile`](#loadfile): loading a file (useful for templates)
- [`escape`](#escape): sanitizing strings
- [`debounce`](#debounce): limiting rate of function calls
- [`shallowEqual`](#shallowequal): shallow object comparison
- [`EventBus`](#eventbus): a simple EventBus
## `whenReady`
@@ -19,40 +16,20 @@ not ready yet, resolved directly otherwise). If called with a callback as
argument, it executes it as soon as the DOM ready (or directly).
```js
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function ([templates]) {
const qweb = new owl.QWeb({ templates });
const env = { qweb };
await mount(App, { env, target: document.body });
});
const { whenReady } = owl;
await whenReady();
// do something
```
or alternatively:
```js
owl.utils.whenReady(function () {
const qweb = new owl.QWeb();
const env = { qweb };
await mount(App, { env, target: document.body });
whenReady(function () {
// do something
});
```
## `loadJS`
`loadJS` takes a url (string) for a javascript resource, and loads it (by adding
a script tag in the document head). It returns a promise, so the caller can
properly reacts when it is ready. Also, it is smart: it maintains a list of urls
previously loaded (or currently being loaded), and prevent doing twice the work.
For example, it is useful for lazy loading external libraries:
```js
class MyComponent extends owl.Component {
willStart() {
return owl.utils.loadJS("/static/libs/someLib.js");
}
}
```
## `loadFile`
`loadFile` is a helper function to fetch a file. It simply
@@ -60,89 +37,22 @@ performs a `GET` request and returns the resulting string in a promise. The
initial usecase for this function is to load a template file. For example:
```js
const { loadFile } = owl;
async function makeEnv() {
const templates = await owl.utils.loadFile("templates.xml");
const qweb = new owl.QWeb({ templates });
return { qweb };
const templates = await loadFile("templates.xml");
// do something
}
```
Note that unlike `loadJS`, this function returns the content of the file as a
string. It does not add a `script` tag or any other side effect.
## `EventBus`
## `escape`
Sometimes, we need to display dynamic data (for example user-generated data) in
the user interface. If this is done by a `QWeb` template, it is not an issue:
```xml
<div><t t-esc="user.data"/></div>
```
The `QWeb` engine will create a `div` node and add the content of the `user.data`
string as a text node, so the web browser will not parse it as html. However,
it may be a problem if this is done with some javascript code like this:
It is a simple `EventBus`, with the same API as usual DOM elements, and an
additional `trigger` method to dispatch events:
```js
class BadComponent extends Component {
// some template with a ref to a div
// some code ...
const bus = new EventBus();
bus.addEventListener("event", () => console.log("something happened"));
mounted() {
this.divRef.el.innerHTML = this.state.value;
}
}
```
In this case, the content of the `div` will be parsed as html, which may inject
unwanted behaviour. To fix this, the `escape` function will simply transform a
string into an escaped version of the same string, which will be properly displayed
by the browser, but which will not be parsed as html (for example, `"<ok>"` is
escaped to the string: `"&lt;ok&gt;"`). So, the bad example above can be fixed
with the following change:
```js
this.divRef.el.innerHTML = owl.utils.escape(this.state.value);
```
## `debounce`
The `debounce` function is useful when we want to limit the number of times some
function/action is perfomed. For example, this may be useful to prevent issue
with people double clicking on a button.
It takes three arguments:
- `func` (function): this is the function that will be rate limited
- `wait` (number): this is the number of milliseconds that we want to use to
rate limit the function `func`
- `immediate` (optional, boolean, default=false): if `immediate` is true, the
function will be triggered immediately (leading edge of the interval). If false,
the function will be triggered at the end (trailing edge).
It returns a function. For example:
```js
const debounce = owl.utils.debounce;
window.addEventListener("mousemove", debounce(doSomething, 100));
```
As this example shows, it is usualy useful for event handlers which are triggered
very quickly, such as `scroll` or `mousemove` events.
## `shallowEqual`
This function checks if two objects have the same values assigned to each keys:
```js
shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); // true
shallowEqual({ a: 1, b: 2 }, { a: 1, b: 3 }); // false
```
However, for performance reasons, it assumes that the two objects have the same
keys. If we are in a situation where this is not guaranteed, the following code
will work:
```js
const completeShallowEqual = (a, b) => shallowEqual(a, b) && shallowEqual(b, a);
bus.trigger("event"); // 'something happened' is logged
```
+1 -1
View File
@@ -13,7 +13,7 @@
"node": ">=12.18.3"
},
"scripts": {
"build:bundle": "rollup -c",
"build:bundle": "rollup -c --failAfterWarnings",
"build": "npm run build:bundle",
"test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
+1 -1
View File
@@ -25,7 +25,7 @@ export class App<T extends typeof Component = any> extends TemplateSet {
Root: T;
props: any;
env: Env;
scheduler = new Scheduler(window.requestAnimationFrame.bind(window));
scheduler = new Scheduler();
root: ComponentNode | null = null;
constructor(Root: T, config: AppConfig = {}) {
+4 -14
View File
@@ -2,7 +2,7 @@ import { BDom, multi, text, toggler } from "../blockdom";
import { validateProps } from "../component/props_validation";
import { Markup } from "../utils";
import { html } from "../blockdom/index";
import { VPortal } from "../portal";
import { TARGET } from "../reactivity";
/**
* This file contains utility functions that will be injected in each template,
@@ -13,17 +13,6 @@ function withDefault(value: any, defaultValue: any): any {
return value === undefined || value === null || value === false ? defaultValue : value;
}
function callPortal(
ctx: any,
parent: any,
key: string,
target: string,
content: (ctx: any, node: any, key: string) => BDom
): BDom {
const portal = new VPortal(target, content(ctx, parent, key), ctx.__owl__) as any;
return portal;
}
function callSlot(
ctx: any,
parent: any,
@@ -33,7 +22,9 @@ function callSlot(
extra: any,
defaultContent?: (ctx: any, node: any, key: string) => BDom
): BDom {
const slots = (ctx.props && ctx.props.slots) || {};
key = key + "__slot_" + name;
const nonReactiveProps = ctx.props && ctx.props[TARGET];
const slots = nonReactiveProps ? nonReactiveProps.slots || {} : {};
const { __render, __ctx, __scope } = slots[name] || {};
const slotScope = Object.create(__ctx || {});
if (__scope) {
@@ -198,7 +189,6 @@ export const UTILS = {
zero: Symbol("zero"),
isBoundary,
callSlot,
callPortal,
capture,
withKey,
prepareList,
+4 -1
View File
@@ -96,7 +96,10 @@ export class TemplateSet {
const templateFn = this._compileTemplate(name, rawTemplate);
// first add a function to lazily get the template, in case there is a
// recursive call to the template name
this.templates[name] = (context, parent) => this.templates[name](context, parent);
const templates = this.templates;
this.templates[name] = function (context, parent) {
return templates[name].call(this, context, parent);
};
const template = templateFn(bdom, this.utils);
this.templates[name] = template;
}
+3 -12
View File
@@ -47,7 +47,7 @@ const cache: { [key: string]: BlockType } = {};
* @param str
* @returns a new block type, that can build concrete blocks
*/
export function createBlock(str: string, deepRemove: boolean = false): BlockType {
export function createBlock(str: string): BlockType {
if (str in cache) {
return cache[str];
}
@@ -67,7 +67,7 @@ export function createBlock(str: string, deepRemove: boolean = false): BlockType
// step 3: build the final block class
const template = tree.el as HTMLElement;
const Block = buildBlock(template, context, deepRemove);
const Block = buildBlock(template, context);
cache[str] = Block;
return Block;
}
@@ -422,7 +422,7 @@ function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
// building the concrete block class
// -----------------------------------------------------------------------------
function buildBlock(template: HTMLElement, ctx: BlockCtx, deepRemove: boolean): BlockType {
function buildBlock(template: HTMLElement, ctx: BlockCtx): BlockType {
let B = createBlockClass(template, ctx);
if (ctx.cbRefs.length) {
@@ -447,14 +447,6 @@ function buildBlock(template: HTMLElement, ctx: BlockCtx, deepRemove: boolean):
}
};
B.prototype.beforeRemove = VMulti.prototype.beforeRemove;
if (deepRemove) {
const blockRemove = B.prototype.remove;
const vMultiRemove = VMulti.prototype.remove;
B.prototype.remove = function () {
blockRemove.call(this);
vMultiRemove.call(this);
};
}
return (data?: any[], children: (VNode | undefined)[] = []) => new B(data, children);
}
@@ -498,7 +490,6 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
beforeRemove() {}
remove() {
console.log('ddd')
elementRemove.call(this.el);
}
+5 -7
View File
@@ -17,11 +17,9 @@ class VList {
anchor: Node | undefined;
parentEl?: HTMLElement | undefined;
isOnlyChild?: boolean | undefined;
deepRemove: boolean;
constructor(children: VNode[], deepRemove: boolean) {
constructor(children: VNode[]) {
this.children = children;
this.deepRemove = deepRemove;
}
mount(parent: HTMLElement, afterNode: Node | null) {
@@ -77,7 +75,7 @@ class VList {
const parent = this.parentEl!;
// fast path: no new child => only remove
if (ch2.length === 0 && isOnlyChild && !this.deepRemove) {
if (ch2.length === 0 && isOnlyChild) {
if (withBeforeRemove) {
for (let i = 0, l = ch1.length; i < l; i++) {
beforeRemove.call(ch1[i]);
@@ -203,7 +201,7 @@ class VList {
remove() {
const { parentEl, anchor } = this;
if (this.isOnlyChild && !this.deepRemove) {
if (this.isOnlyChild) {
nodeSetTextContent.call(parentEl, "");
} else {
const children = this.children;
@@ -228,8 +226,8 @@ class VList {
}
}
export function list(children: VNode[], deepRemove = false): VNode<VList> {
return new VList(children, deepRemove);
export function list(children: VNode[]): VNode<VList> {
return new VList(children);
}
function createMapping(ch1: any[], startIdx1: number, endIdx2: number): { [key: string]: any } {
+5 -7
View File
@@ -15,11 +15,9 @@ export class VMulti {
anchors?: Node[] | undefined;
parentEl?: HTMLElement | undefined;
isOnlyChild?: boolean | undefined;
deepRemove: boolean;
constructor(children: (VNode | undefined)[], deepRemove: boolean) {
constructor(children: (VNode | undefined)[]) {
this.children = children;
this.deepRemove = deepRemove;
}
mount(parent: HTMLElement, afterNode: Node | null) {
@@ -105,7 +103,7 @@ export class VMulti {
remove() {
const parentEl = this.parentEl;
if (this.isOnlyChild && !this.deepRemove) {
if (this.isOnlyChild) {
nodeSetTextContent.call(parentEl, "");
} else {
const children = this.children;
@@ -127,10 +125,10 @@ export class VMulti {
}
toString(): string {
return this.children.map((c) => c!.toString()).join("");
return this.children.map((c) => (c ? c!.toString() : "")).join("");
}
}
export function multi(children: (VNode | undefined)[], deepRemove = false): VNode<VMulti> {
return new VMulti(children, deepRemove);
export function multi(children: (VNode | undefined)[]): VNode<VMulti> {
return new VMulti(children);
}
+45 -38
View File
@@ -39,6 +39,7 @@ export interface CodeGenOptions extends Config {
// of HTML (as we will parse it as xml later)
const xmlDoc = document.implementation.createDocument(null, null, null);
const MODS = new Set(["stop", "capture", "prevent", "self", "synthetic"]);
// -----------------------------------------------------------------------------
// BlockDescription
// -----------------------------------------------------------------------------
@@ -65,15 +66,13 @@ class BlockDescription {
type: BlockType;
parentVar: string = "";
id: number;
deepRemove: boolean;
constructor(target: CodeTarget, type: BlockType, deepRemove: boolean = false) {
constructor(target: CodeTarget, type: BlockType) {
this.id = BlockDescription.nextBlockId++;
this.varName = "b" + this.id;
this.blockName = "block" + this.id;
this.target = target;
this.type = type;
this.deepRemove = deepRemove;
}
insertData(str: string, prefix: string = "d"): number {
@@ -102,7 +101,7 @@ class BlockDescription {
}
return `${this.blockName}(${params})`;
} else if (this.type === "list") {
return `list(c_block${this.id}${this.deepRemove ? ", true" : ""})`;
return `list(c_block${this.id})`;
}
return expr;
}
@@ -128,6 +127,7 @@ interface Context {
isLast?: boolean;
translate: boolean;
tKeyExpr: string | null;
nameSpace?: string;
}
function createContext(parentCtx: Context, params?: Partial<Context>) {
@@ -138,6 +138,7 @@ function createContext(parentCtx: Context, params?: Partial<Context>) {
forceNewBlock: true,
translate: parentCtx.translate,
tKeyExpr: null,
nameSpace: parentCtx.nameSpace,
},
params
);
@@ -253,8 +254,6 @@ export class CodeGenerator {
mainCode.push(`const ${id} = getTemplate(${template});`);
}
const deepRemove = "deepRemove" in this.ast ? this.ast.deepRemove : false;
// define all blocks
if (this.blocks.length) {
mainCode.push(``);
@@ -264,15 +263,9 @@ export class CodeGenerator {
if (block.dynamicTagName) {
xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
mainCode.push(
`let ${block.blockName} = tag => createBlock(\`${xmlString}\`${
deepRemove ? ", true" : ""
});`
);
mainCode.push(`let ${block.blockName} = tag => createBlock(\`${xmlString}\`);`);
} else {
mainCode.push(
`let ${block.blockName} = createBlock(\`${xmlString}\`${deepRemove ? ", true" : ""});`
);
mainCode.push(`let ${block.blockName} = createBlock(\`${xmlString}\`);`);
}
}
}
@@ -328,11 +321,10 @@ export class CodeGenerator {
createBlock(
parentBlock: BlockDescription | null,
type: BlockType,
ctx: Context,
deepRemove: boolean = false
ctx: Context
): BlockDescription {
const hasRoot = this.target.hasRoot;
const block = new BlockDescription(this.target, type, deepRemove);
const block = new BlockDescription(this.target, type);
if (!hasRoot && !ctx.preventRoot) {
this.target.hasRoot = true;
block.isRoot = true;
@@ -515,7 +507,12 @@ export class CodeGenerator {
const modifiers = rawEvent
.split(".")
.slice(1)
.map((m) => `"${m}"`);
.map((m) => {
if (!MODS.has(m)) {
throw new Error(`Unknown event modifier: '${m}'`);
}
return `"${m}"`;
});
let modifiersCode = "";
if (modifiers.length) {
modifiersCode = `${modifiers.join(",")}, `;
@@ -528,7 +525,7 @@ export class CodeGenerator {
const isNewBlock = !block || forceNewBlock || ast.dynamicTag !== null;
let codeIdx = this.target.code.length;
if (isNewBlock) {
if (ast.dynamicTag && ctx.block) {
if ((ast.dynamicTag || ctx.tKeyExpr) && ctx.block) {
this.insertAnchor(ctx.block!);
}
block = this.createBlock(block, "block", ctx);
@@ -541,9 +538,10 @@ export class CodeGenerator {
}
// attributes
const attrs: { [key: string]: string } = {};
if (ast.ns) {
const nameSpace = ast.ns || ctx.nameSpace;
if (nameSpace && isNewBlock) {
// specific namespace uri
attrs["block-ns"] = ast.ns;
attrs["block-ns"] = nameSpace;
}
for (let key in ast.attrs) {
if (key.startsWith("t-attf")) {
@@ -659,6 +657,7 @@ export class CodeGenerator {
forceNewBlock: false,
isLast: ctx.isLast && i === children.length - 1,
tKeyExpr: ctx.tKeyExpr,
nameSpace,
});
this.compileAST(child, subCtx);
}
@@ -673,7 +672,7 @@ export class CodeGenerator {
const children = block!.children.slice();
let current = children.shift();
for (let i = codeIdx; i < code.length; i++) {
if (code[i].trimStart().startsWith(`let ${current!.varName}`)) {
if (code[i].trimStart().startsWith(`let ${current!.varName} `)) {
code[i] = code[i].replace(`let ${current!.varName}`, current!.varName);
current = children.shift();
if (!current) break;
@@ -718,7 +717,7 @@ export class CodeGenerator {
if (ast.body) {
const nextId = BlockDescription.nextBlockId;
const subCtx: Context = createContext(ctx);
this.compileAST({ type: ASTType.Multi, content: ast.body, deepRemove: false }, subCtx);
this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx);
this.helpers.add("withDefault");
expr = `withDefault(${expr}, b${nextId})`;
}
@@ -768,7 +767,7 @@ export class CodeGenerator {
const children = block!.children.slice();
let current = children.shift();
for (let i = codeIdx; i < code.length; i++) {
if (code[i].trimStart().startsWith(`let ${current!.varName}`)) {
if (code[i].trimStart().startsWith(`let ${current!.varName} `)) {
code[i] = code[i].replace(`let ${current!.varName}`, current!.varName);
current = children.shift();
if (!current) break;
@@ -779,7 +778,7 @@ export class CodeGenerator {
// note: this part is duplicated from end of compilemulti:
const args = block!.children.map((c) => c.varName).join(", ");
this.insertBlock(`multi([${args}]${ast.deepRemove ? ", true" : ""})`, block!, ctx)!;
this.insertBlock(`multi([${args}])`, block!, ctx)!;
}
}
@@ -788,7 +787,7 @@ export class CodeGenerator {
if (block) {
this.insertAnchor(block);
}
block = this.createBlock(block, "list", ctx, ast.deepRemove);
block = this.createBlock(block, "list", ctx);
this.target.loopLevel++;
const loopVar = `i${this.target.loopLevel}`;
this.addLine(`ctx = Object.create(ctx);`);
@@ -912,7 +911,7 @@ export class CodeGenerator {
const children = block!.children.slice();
let current = children.shift();
for (let i = codeIdx; i < code.length; i++) {
if (code[i].trimStart().startsWith(`let ${current!.varName}`)) {
if (code[i].trimStart().startsWith(`let ${current!.varName} `)) {
code[i] = code[i].replace(`let ${current!.varName}`, current!.varName);
current = children.shift();
if (!current) break;
@@ -923,7 +922,7 @@ export class CodeGenerator {
}
const args = block!.children.map((c) => c.varName).join(", ");
this.insertBlock(`multi([${args}]${ast.deepRemove ? ", true" : ""})`, block!, ctx)!;
this.insertBlock(`multi([${args}])`, block!, ctx)!;
}
}
@@ -935,7 +934,7 @@ export class CodeGenerator {
this.helpers.add("isBoundary");
const nextId = BlockDescription.nextBlockId;
const subCtx: Context = createContext(ctx, { preventRoot: true });
this.compileAST({ type: ASTType.Multi, content: ast.body, deepRemove: false }, subCtx);
this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx);
if (nextId !== BlockDescription.nextBlockId) {
this.helpers.add("zero");
this.addLine(`ctx[zero] = b${nextId};`);
@@ -990,7 +989,7 @@ export class CodeGenerator {
const expr = ast.value ? compileExpr(ast.value || "") : "null";
if (ast.body) {
this.helpers.add("LazyValue");
const bodyAst: AST = { type: ASTType.Multi, content: ast.body, deepRemove: false };
const bodyAst: AST = { type: ASTType.Multi, content: ast.body };
const name = this.compileInNewTarget("value", bodyAst, ctx);
let value = `new LazyValue(${name}, ctx, node)`;
value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value;
@@ -1033,6 +1032,8 @@ export class CodeGenerator {
this.helpers.add("bind");
propName = name;
propValue = `bind(ctx, ${propValue})`;
} else {
throw new Error("Invalid prop suffix");
}
}
propName = /^[a-z_]+$/i.test(propName) ? propName : `'${propName}'`;
@@ -1080,11 +1081,9 @@ export class CodeGenerator {
let propString = propStr;
if (ast.dynamicProps) {
if (!props.length) {
propString = `${compileExpr(ast.dynamicProps)}`;
} else {
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}, ${propStr})`;
}
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}${
props.length ? ", " + propStr : ""
})`;
}
let propVar: string;
@@ -1177,10 +1176,18 @@ export class CodeGenerator {
}
}
compileTPortal(ast: ASTTPortal, ctx: Context) {
this.helpers.add("callPortal");
this.helpers.add("Portal");
let { block } = ctx;
const name = this.compileInNewTarget("portalContent", ast.content, ctx);
const blockString = `callPortal(ctx, node, key, ${ast.target}, ${name})`;
const name = this.compileInNewTarget("slot", ast.content, ctx);
const key = this.generateComponentKey();
let ctxStr = "ctx";
if (this.target.loopLevel || !this.hasSafeContext) {
ctxStr = this.generateId("ctx");
this.helpers.add("capture");
this.addLine(`const ${ctxStr} = capture(ctx);`);
}
const blockString = `component(Portal, {target: ${ast.target},slots: {'default': {__render: ${name}, __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx)`;
if (block) {
this.insertAnchor(block);
}
+1 -61
View File
@@ -56,7 +56,6 @@ export interface ASTDomNode {
export interface ASTMulti {
type: ASTType.Multi;
content: AST[];
deepRemove: boolean;
}
export interface ASTTEsc {
@@ -77,7 +76,6 @@ export interface ASTTif {
content: AST;
tElif: { condition: string; content: AST }[] | null;
tElse: AST | null;
deepRemove: boolean;
}
export interface ASTTSet {
@@ -95,7 +93,6 @@ export interface ASTTForEach {
key: string | null;
body: AST;
memo: string;
deepRemove: boolean;
hasNoFirst: boolean;
hasNoLast: boolean;
hasNoIndex: boolean;
@@ -484,7 +481,6 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
elem,
body,
memo,
deepRemove: needDeepRemove(body),
key,
hasNoFirst,
hasNoLast,
@@ -493,44 +489,6 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
};
}
/**
* @returns true if we are sure that a deep remove (without optimisation) is needed, for exemple
* if there is a portal.
*/
function needDeepRemove(ast: AST): boolean {
switch (ast.type) {
case ASTType.Multi:
case ASTType.TForEach:
case ASTType.TIf:
return ast.deepRemove;
case ASTType.TPortal:
return true;
case ASTType.TComponent:
case ASTType.TOut:
case ASTType.TCall:
case ASTType.TCallBlock:
case ASTType.TSlot:
case ASTType.Text:
case ASTType.Comment:
case ASTType.TEsc:
return false;
case ASTType.TKey:
return needDeepRemove(ast.content);
case ASTType.TDebug:
case ASTType.TLog:
case ASTType.TTranslation:
return ast.content ? needDeepRemove(ast.content) : false;
case ASTType.TSet:
return ast.body ? ast.body.some((ast) => needDeepRemove(ast)) : false;
case ASTType.DomNode:
return ast.content.some((ast) => needDeepRemove(ast));
}
}
function parseTKey(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-key")) {
return null;
@@ -628,21 +586,12 @@ function parseTIf(node: Element, ctx: ParsingContext): AST | null {
nextElement.remove();
}
let deepRemove = needDeepRemove(content);
if (tElifs) {
deepRemove = deepRemove || tElifs.some((ast) => needDeepRemove(ast));
}
if (tElse) {
deepRemove = deepRemove || needDeepRemove(tElse);
}
return {
type: ASTType.TIf,
condition,
content,
tElif: tElifs.length ? tElifs : null,
tElse,
deepRemove,
};
}
@@ -814,11 +763,6 @@ function parseTPortal(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-portal")) {
return null;
}
if (node.tagName !== "t") {
throw new Error(
`Directive 't-portal' can only be used on <t> nodes (used on a <${node.tagName}>)`
);
}
const target = node.getAttribute("t-portal")!;
node.removeAttribute("t-portal");
const content = parseNode(node, ctx);
@@ -869,11 +813,7 @@ function parseChildNodes(node: Node, ctx: ParsingContext): AST | null {
case 1:
return children[0];
default:
return {
type: ASTType.Multi,
content: children,
deepRemove: children.some((ast) => needDeepRemove(ast)),
};
return { type: ASTType.Multi, content: children };
}
}
+5 -6
View File
@@ -1,19 +1,18 @@
import type { Env } from "../app/app";
import type { ComponentNode } from "./component_node";
// -----------------------------------------------------------------------------
// Component Class
// -----------------------------------------------------------------------------
export class Component {
export class Component<Props = any, Env = any> {
static template: string = "";
static props?: any;
props: any;
props: Props;
env: Env;
__owl__: ComponentNode;
constructor(props: any, env: Env, node: ComponentNode) {
constructor(props: Props, env: Env, node: ComponentNode) {
this.props = props;
this.env = env;
this.__owl__ = node;
@@ -21,7 +20,7 @@ export class Component {
setup() {}
render() {
this.__owl__.render();
render(force: boolean = false) {
this.__owl__.render(force);
}
}
+77 -20
View File
@@ -1,6 +1,9 @@
import type { App, Env } from "../app/app";
import { BDom, VNode } from "../blockdom";
import { clearReactivesForCallback, Reactive, reactive, TARGET } from "../reactivity";
import { batched, Callback } from "../utils";
import { Component } from "./component";
import { fibersInError, handleError } from "./error_handling";
import {
Fiber,
makeChildFiber,
@@ -9,7 +12,6 @@ import {
MountOptions,
RootFiber,
} from "./fibers";
import { handleError, fibersInError } from "./error_handling";
import { applyDefaultProps } from "./props_validation";
import { STATUS } from "./status";
@@ -23,6 +25,46 @@ export function useComponent(): Component {
return currentNode!.component;
}
// -----------------------------------------------------------------------------
// Integration with reactivity system (useState)
// -----------------------------------------------------------------------------
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
/**
* Creates a reactive object that will be observed by the current component.
* Reading data from the returned object (eg during rendering) will cause the
* component to subscribe to that data and be rerendered when it changes.
*
* @param state the state to observe
* @returns a reactive object that will cause the component to re-render on
* relevant changes
* @see reactive
*/
export function useState<T extends object>(state: T): Reactive<T> {
const node = currentNode!;
let render = batchedRenderFunctions.get(node)!;
if (!render) {
render = batched(node.render.bind(node));
batchedRenderFunctions.set(node, render);
// manual implementation of onWillUnmount to break cyclic dependency
node.willUnmount.unshift(clearReactivesForCallback.bind(null, render));
}
return reactive(state, render);
}
// -----------------------------------------------------------------------------
// component function (used in compiled template code)
// -----------------------------------------------------------------------------
type Props = { [key: string]: any };
function arePropsDifferent(props1: Props, props2: Props): boolean {
for (let k in props1) {
if (props1[k] !== props2[k]) {
return true;
}
}
return false;
}
export function component(
name: string | typeof Component,
props: any,
@@ -47,7 +89,10 @@ export function component(
const parentFiber = ctx.fiber!;
if (node) {
node.updateAndRender(props, parentFiber);
const currentProps = node.component.props[TARGET];
if (parentFiber.force || arePropsDifferent(currentProps, props)) {
node.updateAndRender(props, parentFiber);
}
} else {
// new component
let C;
@@ -69,7 +114,7 @@ export function component(
}
// -----------------------------------------------------------------------------
// Component VNode
// Component VNode class
// -----------------------------------------------------------------------------
type LifecycleHook = Function;
@@ -107,6 +152,7 @@ export class ComponentNode<T extends typeof Component = typeof Component>
applyDefaultProps(props, C);
const env = (parent && parent.childEnv) || app.env;
this.childEnv = env;
props = useState(props);
this.component = new C(props, env, this) as any;
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
this.component.setup();
@@ -121,7 +167,7 @@ export class ComponentNode<T extends typeof Component = typeof Component>
async initiateRender(fiber: Fiber | MountFiber) {
this.fiber = fiber;
if (this.mounted.length) {
fiber.root.mounted.push(fiber);
fiber.root!.mounted.push(fiber);
}
const component = this.component;
try {
@@ -135,21 +181,23 @@ export class ComponentNode<T extends typeof Component = typeof Component>
}
}
async render() {
async render(force: boolean = false) {
let current = this.fiber;
if (current && current.root.locked) {
if (current && current.root!.locked) {
await Promise.resolve();
// situation may have changed after the microtask tick
current = this.fiber;
}
if (current && !current.bdom && !fibersInError.has(current)) {
return;
if (current.force || force === false) {
return;
}
}
if (!this.bdom && !current) {
return;
}
const fiber = makeRootFiber(this);
const fiber = makeRootFiber(this, force);
this.fiber = fiber;
this.app.scheduler.addFiber(fiber);
await Promise.resolve();
@@ -167,7 +215,7 @@ export class ComponentNode<T extends typeof Component = typeof Component>
// a root fiber to a child fiber in the previous microtick, because it was
// embedded in a rendering coming from above, so the fiber will be rendered
// in the next microtick anyway, so we should not render it again.
if (this.fiber && (current || !fiber.parent)) {
if (this.fiber === fiber && (current || !fiber.parent)) {
this._render(fiber);
}
}
@@ -175,7 +223,7 @@ export class ComponentNode<T extends typeof Component = typeof Component>
_render(fiber: Fiber | RootFiber) {
try {
fiber.bdom = this.renderFn();
fiber.root.counter--;
fiber.root!.counter--;
} catch (e) {
handleError({ node: this, error: e });
}
@@ -211,6 +259,9 @@ export class ComponentNode<T extends typeof Component = typeof Component>
this.fiber = fiber;
const component = this.component;
applyDefaultProps(props, component.constructor as any);
currentNode = this;
props = useState(props);
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
await prom;
if (fiber !== this.fiber) {
@@ -218,7 +269,7 @@ export class ComponentNode<T extends typeof Component = typeof Component>
}
component.props = props;
this._render(fiber);
const parentRoot = parentFiber.root;
const parentRoot = parentFiber.root!;
if (this.willPatch.length) {
parentRoot.willPatch.push(fiber);
}
@@ -275,29 +326,35 @@ export class ComponentNode<T extends typeof Component = typeof Component>
}
patch() {
this.bdom!.patch(this!.fiber!.bdom!, false);
this.cleanOutdatedChildren();
if (this.fiber && this.fiber.parent) {
// we only patch here renderings coming from above. renderings initiated
// by the component will be patched independently in the appropriate
// fiber.complete
this._patch();
}
}
_patch() {
const hasChildren = Object.keys(this.children).length > 0;
this.bdom!.patch(this!.fiber!.bdom!, hasChildren);
if (hasChildren) {
this.cleanOutdatedChildren();
}
this.fiber!.appliedToDom = true;
this.fiber = null;
}
beforeRemove() {
console.log('ddddddd')
this._destroy();
}
remove() {
console.log('coucou')
this.bdom!.remove();
}
cleanOutdatedChildren() {
const childrenEntries = Object.entries(this.children);
if (!childrenEntries.length) {
return;
}
const children = this.children;
for (const [key, node] of childrenEntries) {
for (const key in children) {
const node = children[key];
const status = node.status;
if (status !== STATUS.MOUNTED) {
delete children[key];
+3 -3
View File
@@ -29,8 +29,8 @@ function _handleError(node: ComponentNode | null, error: any, isFirstRound = fal
}
if (stopped) {
if (isFirstRound && fiber) {
fiber.root.counter--;
if (isFirstRound && fiber && fiber.node.fiber) {
fiber.root!.counter--;
}
return true;
}
@@ -52,7 +52,7 @@ export function handleError(params: ErrorParams) {
current = current.parent;
} while (current);
fibersInError.set(fiber.root, error);
fibersInError.set(fiber.root!, error);
const handled = _handleError(node, error, true);
if (!handled) {
+10 -47
View File
@@ -3,58 +3,25 @@ import type { ComponentNode } from "./component_node";
import { fibersInError, handleError } from "./error_handling";
import { STATUS } from "./status";
/**
* Cleans on the root fiber the patch and willPatch fiber lists
* It is typically needed when the same root fiber needs to recycle on
* of its children or grandchildren's fiber.
*/
function cleanPatchableFiber(child: Fiber, root: RootFiber) {
const { willPatch, patched } = root;
let i = willPatch.indexOf(child);
if (i > -1) {
willPatch.splice(i, 1);
}
i = patched.indexOf(child);
if (i > -1) {
patched.splice(i, 1);
}
}
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
let current = node.fiber;
if (current) {
// current is necessarily a rootfiber here
let root = parent.root;
const isSameRoot = current.root === root;
cancelFibers(root, current.children);
current.children = [];
current.parent = parent;
// only increment our rendering if we were not
// already accounted for, or that we have been rendered
// already (in which case our fiber was removed from the root rendering)
if (!isSameRoot || current.bdom) {
root.counter++;
}
if (isSameRoot) {
cleanPatchableFiber(current, root);
}
current.bdom = null;
current.root = root;
return current;
current.root = null;
}
return new Fiber(node, parent);
}
export function makeRootFiber(node: ComponentNode): Fiber {
export function makeRootFiber(node: ComponentNode, force: boolean): Fiber {
let current = node.fiber;
if (current) {
let root = current.root;
let root = current.root!;
root.counter -= cancelFibers(root, current.children);
current.children = [];
root.counter++;
current.bdom = null;
current.force = force;
if (fibersInError.has(current)) {
fibersInError.delete(current);
fibersInError.delete(root);
@@ -69,7 +36,7 @@ export function makeRootFiber(node: ComponentNode): Fiber {
if (node.patched.length) {
fiber.patched.push(fiber);
}
fiber.force = force;
return fiber;
}
@@ -92,16 +59,18 @@ function cancelFibers(root: any, fibers: Fiber[]): number {
export class Fiber {
node: ComponentNode;
bdom: BDom | null = null;
root: RootFiber;
root: RootFiber | null; // A Fiber that has been replaced by another has no root
parent: Fiber | null;
children: Fiber[] = [];
appliedToDom = false;
force: boolean = false;
constructor(node: ComponentNode, parent: Fiber | null) {
this.node = node;
this.parent = parent;
if (parent) {
const root = parent.root;
this.force = parent.force;
const root = parent.root!;
root.counter++;
this.root = root;
parent.children.push(this);
@@ -143,14 +112,8 @@ export class RootFiber extends Fiber {
current = undefined;
// Step 2: patching the dom
node.bdom!.patch(this.bdom!, Object.keys(node.children).length > 0);
node.cleanOutdatedChildren();
this.appliedToDom = true;
node._patch();
this.locked = false;
// unregistering the fiber before mounted since it can do another render
// and that the current rendering is obviously completed
node.fiber = null;
// Step 4: calling all mounted lifecycle hooks
let mountedFibers = this.mounted;
+9 -1
View File
@@ -1,4 +1,5 @@
import { filterOutModifiersFromData } from "../blockdom/config";
import { STATUS } from "./status";
export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarget | null) => {
const { data: _data, modifiers } = filterOutModifiersFromData(data);
@@ -30,7 +31,14 @@ export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarg
// We check this rather than data[0] being truthy (or typeof function) so that it crashes
// as expected when there is a handler expression that evaluates to a falsy value
if (Object.hasOwnProperty.call(data, 0)) {
data[0].call(data[1] ? data[1].__owl__.component : null, ev);
const handler = data[0];
if (typeof handler !== "function") {
throw new Error(`Invalid handler (expected a function, received: '${handler}')`);
}
let node = data[1] ? data[1].__owl__ : null;
if (node ? node.status === STATUS.MOUNTED : true) {
handler.call(node ? node.component : null, ev);
}
}
return stopped;
};
+10 -10
View File
@@ -7,44 +7,44 @@ import { nodeErrorHandlers } from "./error_handling";
export function onWillStart(fn: () => Promise<void> | void | any) {
const node = getCurrent()!;
node.willStart.push(fn);
node.willStart.push(fn.bind(node.component));
}
export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) {
const node = getCurrent()!;
node.willUpdateProps.push(fn);
node.willUpdateProps.push(fn.bind(node.component));
}
export function onMounted(fn: () => void | any) {
const node = getCurrent()!;
node.mounted.push(fn);
node.mounted.push(fn.bind(node.component));
}
export function onWillPatch(fn: () => Promise<void> | any | void) {
const node = getCurrent()!;
node.willPatch.unshift(fn);
node.willPatch.unshift(fn.bind(node.component));
}
export function onPatched(fn: () => void | any) {
const node = getCurrent()!;
node.patched.push(fn);
node.patched.push(fn.bind(node.component));
}
export function onWillUnmount(fn: () => Promise<void> | void | any) {
const node = getCurrent()!;
node.willUnmount.unshift(fn);
node.willUnmount.unshift(fn.bind(node.component));
}
export function onWillDestroy(fn: () => Promise<void> | void | any) {
const node = getCurrent()!;
node.willDestroy.push(fn);
node.willDestroy.push(fn.bind(node.component));
}
export function onWillRender(fn: () => void | any) {
const node = getCurrent()!;
const renderFn = node.renderFn;
node.renderFn = () => {
fn();
fn.call(node.component);
return renderFn();
};
}
@@ -54,7 +54,7 @@ export function onRendered(fn: () => void | any) {
const renderFn = node.renderFn;
node.renderFn = () => {
const result = renderFn();
fn();
fn.call(node.component);
return result;
};
}
@@ -67,5 +67,5 @@ export function onError(callback: OnErrorCallback) {
handlers = [];
nodeErrorHandlers.set(node, handlers);
}
handlers.push(callback);
handlers.push(callback.bind(node.component));
}
+6 -3
View File
@@ -7,12 +7,15 @@ import { STATUS } from "./status";
// -----------------------------------------------------------------------------
export class Scheduler {
// capture the value of requestAnimationFrame as soon as possible, to avoid
// interactions with other code, such as test frameworks that override them
static requestAnimationFrame = window.requestAnimationFrame.bind(window);
tasks: Set<RootFiber> = new Set();
isRunning: boolean = false;
requestAnimationFrame: Window["requestAnimationFrame"];
constructor(requestAnimationFrame: Window["requestAnimationFrame"]) {
this.requestAnimationFrame = requestAnimationFrame;
constructor() {
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
}
start() {
@@ -25,7 +28,7 @@ export class Scheduler {
}
addFiber(fiber: Fiber) {
this.tasks.add(fiber.root);
this.tasks.add(fiber.root!);
if (!this.isRunning) {
this.start();
}
+6 -1
View File
@@ -1,3 +1,4 @@
import { UTILS } from "./app/template_helpers";
import {
config,
createBlock,
@@ -12,9 +13,12 @@ import {
comment,
} from "./blockdom";
import { mainEventHandler } from "./component/handler";
import { Portal } from "./portal";
export type { Reactive } from "./reactivity";
config.shouldNormalizeDom = false;
config.mainEventHandler = mainEventHandler;
(UTILS as any).Portal = Portal;
export const blockDom = {
config,
@@ -38,7 +42,8 @@ export { useComponent } from "./component/component_node";
export { status } from "./component/status";
export { Memo } from "./memo";
export { xml } from "./app/template_set";
export { useState, reactive } from "./reactivity";
export { reactive } from "./reactivity";
export { useState } from "./component/component_node";
export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks";
export { EventBus, whenReady, loadFile, markup } from "./utils";
export {
+31 -7
View File
@@ -1,21 +1,22 @@
import { onWillUnmount } from "./component/lifecycle_hooks";
import { xml } from "./app/template_set";
import { BDom, text, VNode } from "./blockdom";
import { Component } from "./component/component";
const VText: any = text("").constructor;
export class VPortal extends VText implements Partial<VNode<VPortal>> {
class VPortal extends VText implements Partial<VNode<VPortal>> {
// selector: string;
realBDom: BDom | null;
target: HTMLElement | null = null;
constructor(selector: string, realBDom: BDom, ownerComponent: ComponentNode) {
constructor(selector: string, realBDom: BDom) {
super("");
this.ownerComponent = ownerComponent;
this.selector = selector;
this.realBDom = realBDom;
}
mount(parent: HTMLElement, anchor: ChildNode) {
super.mount(parent, anchor);
this.ownerComponent.willDestroy.push(() => this.cleanup());
this.target = document.querySelector(this.selector) as any;
if (!this.target) {
let el: any = this.el;
@@ -34,9 +35,11 @@ export class VPortal extends VText implements Partial<VNode<VPortal>> {
this.realBDom!.beforeRemove();
}
remove() {
super.remove();
this.realBDom!.remove();
this.realBDom = null;
if (this.realBDom) {
super.remove();
this.realBDom!.remove();
this.realBDom = null;
}
}
patch(other: VPortal) {
@@ -49,3 +52,24 @@ export class VPortal extends VText implements Partial<VNode<VPortal>> {
}
}
}
export class Portal extends Component {
static template = xml`<t t-slot="default"/>`;
static props = {
target: {
type: String,
},
slots: true,
};
setup() {
const node = this.__owl__;
const renderFn = node.renderFn;
node.renderFn = () => new VPortal(this.props.target, renderFn());
onWillUnmount(() => {
if (node.bdom) {
node.bdom.remove();
}
});
}
}
+4 -55
View File
@@ -1,15 +1,13 @@
import { onWillUnmount } from "./component/lifecycle_hooks";
import { ComponentNode, getCurrent } from "./component/component_node";
import { Callback } from "./utils";
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
const TARGET = Symbol("Target");
export const TARGET = Symbol("Target");
// Special key to subscribe to, to be notified of key creation/deletion
const KEYCHANGES = Symbol("Key changes");
type ObjectKey = string | number | symbol;
type Target = object;
type Callback = () => void;
type Reactive<T extends Target = Target> = T & {
export type Reactive<T extends Target = Target> = T & {
[TARGET]: any;
};
@@ -85,7 +83,7 @@ const callbacksToTargets = new WeakMap<Callback, Set<Target>>();
*
* @param callback the callback for which the reactives need to be cleared
*/
function clearReactivesForCallback(callback: Callback): void {
export function clearReactivesForCallback(callback: Callback): void {
const targetsToClear = callbacksToTargets.get(callback);
if (!targetsToClear) {
return;
@@ -190,52 +188,3 @@ export function reactive<T extends Target>(target: T, callback: Callback = () =>
}
return reactivesForTarget.get(callback) as Reactive<T>;
}
/**
* Creates a batched version of a callback so that all calls to it in the same
* microtick will only call the original callback once.
*
* @param callback the callback to batch
* @returns a batched version of the original callback
*/
export function batched(callback: Callback): Callback {
let called = false;
return async () => {
// This await blocks all calls to the callback here, then releases them sequentially
// in the next microtick. This line decides the granularity of the batch.
await Promise.resolve();
if (!called) {
called = true;
callback();
// wait for all calls in this microtick to fall through before resetting "called"
// so that only the first call to the batched function calls the original callback
await Promise.resolve();
called = false;
}
};
}
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
/**
* Creates a reactive object that will be observed by the current component.
* Reading data from the returned object (eg during rendering) will cause the
* component to subscribe to that data and be rerendered when it changes.
*
* @param state the state to observe
* @returns a reactive object that will cause the component to re-render on
* relevant changes
* @see reactive
*/
export function useState<T extends object>(state: T): Reactive<T> {
const node = getCurrent()!;
if (!batchedRenderFunctions.has(node)) {
batchedRenderFunctions.set(
node,
batched(() => node.render())
);
onWillUnmount(() => clearReactivesForCallback(render));
}
const render = batchedRenderFunctions.get(node)!;
const reactiveState = reactive(state, render);
return reactiveState;
}
+26
View File
@@ -1,3 +1,29 @@
export type Callback = () => void;
/**
* Creates a batched version of a callback so that all calls to it in the same
* microtick will only call the original callback once.
*
* @param callback the callback to batch
* @returns a batched version of the original callback
*/
export function batched(callback: Callback): Callback {
let called = false;
return async () => {
// This await blocks all calls to the callback here, then releases them sequentially
// in the next microtick. This line decides the granularity of the batch.
await Promise.resolve();
if (!called) {
called = true;
callback();
// wait for all calls in this microtick to fall through before resetting "called"
// so that only the first call to the batched function calls the original callback
await Promise.resolve();
called = false;
}
};
}
export class EventBus extends EventTarget {
trigger(name: string, payload?: any) {
this.dispatchEvent(new CustomEvent(name, { detail: payload }));
+5
View File
@@ -77,6 +77,11 @@ describe("multi blocks", () => {
expect(fixture.innerHTML).toBe("ab");
});
test("multi vnode can be used as text", () => {
mount(text(multi([text("a"), undefined]) as any), fixture);
expect(fixture.innerHTML).toBe("a");
});
test("multi inside a block", async () => {
const block = createBlock("<div><block-child-0/></div>");
const tree = block([], [multi([text("foo"), text("bar")])]);
+19 -1
View File
@@ -44,10 +44,28 @@ exports[`properly support svg namespace to svg tags added even if already in svg
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><svg block-ns=\\"http://www.w3.org/2000/svg\\"/></svg>\`);
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><svg/></svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg svg namespace added to sub-blocks 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><block-child-0/></svg>\`);
let block2 = createBlock(\`<path block-ns=\\"http://www.w3.org/2000/svg\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let b2;
if (ctx['path']) {
b2 = block2();
}
return block1([], [b2]);
}
}"
`;
@@ -68,3 +68,38 @@ exports[`t-key t-key directive in a list 1`] = `
}
}"
`;
exports[`t-key t-key on sub dom node pushes a child block in its parent 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
let block2 = createBlock(\`<span/>\`);
let block3 = createBlock(\`<div><h1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2,b3;
if (ctx['hasSpan']) {
b2 = block2();
}
const tKey_1 = ctx['key'];
b3 = toggler(tKey_1, block3());
return block1([], [b2, b3]);
}
}"
`;
exports[`t-key t-key on sub dom node pushes a child block in its parent 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><h1/></div>\`);
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['key'];
return toggler(tKey_1, block1());
}
}"
`;
@@ -87,6 +87,31 @@ exports[`t-set set from attribute lookup 1`] = `
}"
`;
exports[`t-set set from body literal (with t-if/t-else 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, LazyValue } = helpers;
function value1(ctx, node, key = \\"\\") {
let b2,b3;
if (ctx['condition']) {
b2 = text(\`true\`);
} else {
b3 = text(\`false\`);
}
return multi([b2, b3]);
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
ctx[\`value\`] = new LazyValue(value1, ctx, node);
return text(ctx['value']);
}
}"
`;
exports[`t-set set from body literal 1`] = `
"function anonymous(bdom, helpers
) {
+14 -5
View File
@@ -2,6 +2,7 @@ import { TemplateSet } from "../../src/app/template_set";
import { mount } from "../../src/blockdom";
import { makeTestFixture, renderToBdom, renderToString, snapshotEverything } from "../helpers";
import { markup } from "../../src/utils";
import { STATUS } from "../../src/component/status";
snapshotEverything();
// -----------------------------------------------------------------------------
@@ -11,7 +12,7 @@ snapshotEverything();
describe("t-on", () => {
function mountToFixture(template: string, ctx: any = {}, node?: any): HTMLDivElement {
if (!node) {
node = { component: ctx };
node = { component: ctx, status: STATUS.MOUNTED };
ctx.__owl__ = node;
}
const block = renderToBdom(template, ctx, node);
@@ -156,7 +157,7 @@ describe("t-on", () => {
expect(this).toBe(owner);
},
};
const node = { component: owner };
const node = { component: owner, status: STATUS.MOUNTED };
owner.__owl__ = node;
const fixture = makeTestFixture();
const render = context.getTemplate("main");
@@ -181,7 +182,7 @@ describe("t-on", () => {
expect(this).toBe(owner);
},
};
const node = { component: owner };
const node = { component: owner, status: STATUS.MOUNTED };
owner.__owl__ = node;
const fixture = makeTestFixture();
const render = context.getTemplate("main");
@@ -260,7 +261,7 @@ describe("t-on", () => {
expect(this).toBe(owner);
},
};
const node = { component: owner };
const node = { component: owner, status: STATUS.MOUNTED };
owner.__owl__ = node;
const fixture = makeTestFixture();
@@ -286,7 +287,7 @@ describe("t-on", () => {
value: 444,
};
const node = { component: owner };
const node = { component: owner, status: STATUS.MOUNTED };
owner.__owl__ = node;
const fixture = makeTestFixture();
@@ -479,6 +480,14 @@ describe("t-on", () => {
button.click();
});
test("t-on crashes when used with unknown modifier", async () => {
const template = `<div t-on-click.somemodifier="onClick" />`;
let owner = { onClick(e: Event) {} };
expect(() => mountToFixture(template, owner)).toThrowError("Unknown event modifier");
});
test("t-on combined with t-esc", async () => {
expect.assertions(3);
const template = `<div><button t-on-click="onClick" t-esc="text"/></div>`;
-32
View File
@@ -146,7 +146,6 @@ describe("qweb parser", () => {
content: [],
},
],
deepRemove: false,
});
});
@@ -254,7 +253,6 @@ describe("qweb parser", () => {
content: [],
},
],
deepRemove: false,
});
});
@@ -505,7 +503,6 @@ describe("qweb parser", () => {
},
tElif: null,
tElse: null,
deepRemove: false,
},
],
});
@@ -521,7 +518,6 @@ describe("qweb parser", () => {
},
tElif: null,
tElse: null,
deepRemove: false,
});
});
@@ -542,7 +538,6 @@ describe("qweb parser", () => {
},
tElif: null,
tElse: null,
deepRemove: false,
});
});
@@ -559,7 +554,6 @@ describe("qweb parser", () => {
type: ASTType.Text,
value: "else",
},
deepRemove: false,
});
});
@@ -578,7 +572,6 @@ describe("qweb parser", () => {
},
],
tElse: null,
deepRemove: false,
});
});
@@ -600,7 +593,6 @@ describe("qweb parser", () => {
type: ASTType.Text,
value: "else",
},
deepRemove: false,
});
});
@@ -658,7 +650,6 @@ describe("qweb parser", () => {
},
],
},
deepRemove: false,
});
});
@@ -740,7 +731,6 @@ describe("qweb parser", () => {
},
tElif: null,
tElse: null,
deepRemove: false,
});
});
@@ -763,7 +753,6 @@ describe("qweb parser", () => {
content: { type: ASTType.Text, value: "1" },
tElif: null,
tElse: { type: ASTType.TSet, name: "ourvar", value: "0", defaultValue: null, body: null },
deepRemove: false,
},
],
});
@@ -793,7 +782,6 @@ describe("qweb parser", () => {
hasNoIndex: false,
hasNoLast: true,
hasNoValue: true,
deepRemove: false,
});
});
@@ -809,7 +797,6 @@ describe("qweb parser", () => {
hasNoIndex: false,
hasNoLast: true,
hasNoValue: true,
deepRemove: false,
});
});
@@ -835,7 +822,6 @@ describe("qweb parser", () => {
hasNoIndex: false,
hasNoLast: true,
hasNoValue: true,
deepRemove: false,
});
});
@@ -851,7 +837,6 @@ describe("qweb parser", () => {
hasNoIndex: true,
hasNoLast: true,
hasNoValue: true,
deepRemove: false,
});
});
@@ -879,7 +864,6 @@ describe("qweb parser", () => {
hasNoIndex: false,
hasNoLast: true,
hasNoValue: true,
deepRemove: false,
});
});
@@ -898,7 +882,6 @@ describe("qweb parser", () => {
condition: "condition",
tElif: null,
tElse: null,
deepRemove: false,
content: {
type: ASTType.DomNode,
tag: "span",
@@ -916,7 +899,6 @@ describe("qweb parser", () => {
hasNoIndex: false,
hasNoLast: true,
hasNoValue: true,
deepRemove: false,
});
});
@@ -949,7 +931,6 @@ describe("qweb parser", () => {
hasNoIndex: false,
hasNoLast: true,
hasNoValue: true,
deepRemove: false,
});
});
@@ -977,7 +958,6 @@ describe("qweb parser", () => {
hasNoIndex: false,
hasNoLast: true,
hasNoValue: true,
deepRemove: false,
},
],
});
@@ -1015,7 +995,6 @@ describe("qweb parser", () => {
hasNoIndex: false,
hasNoLast: true,
hasNoValue: true,
deepRemove: false,
});
});
@@ -1038,7 +1017,6 @@ describe("qweb parser", () => {
hasNoIndex: false,
hasNoLast: true,
hasNoValue: true,
deepRemove: false,
});
});
@@ -1060,7 +1038,6 @@ describe("qweb parser", () => {
hasNoIndex: false,
hasNoLast: false,
hasNoValue: false,
deepRemove: false,
});
});
@@ -1080,7 +1057,6 @@ describe("qweb parser", () => {
hasNoIndex: false,
hasNoLast: true,
hasNoValue: true,
deepRemove: false,
});
});
@@ -1130,7 +1106,6 @@ describe("qweb parser", () => {
condition: "condition",
tElif: null,
tElse: null,
deepRemove: false,
content: {
type: ASTType.TCall,
name: "blabla",
@@ -1285,7 +1260,6 @@ describe("qweb parser", () => {
ns: null,
},
],
deepRemove: false,
},
},
},
@@ -1628,7 +1602,6 @@ describe("qweb parser", () => {
hasNoValue: true,
key: "item_index",
memo: "",
deepRemove: false,
type: ASTType.TForEach,
});
});
@@ -1800,10 +1773,6 @@ describe("qweb parser", () => {
});
});
test("t-portal must be in a <t> node", async () => {
expect(() => parse(`<div t-portal="target">Content</div>`)).toThrowError();
});
test("t-portal with t-if", async () => {
expect(parse(`<t t-portal="target" t-if="condition">Content</t>`)).toEqual({
condition: "condition",
@@ -1815,7 +1784,6 @@ describe("qweb parser", () => {
tElif: null,
tElse: null,
type: ASTType.TIf,
deepRemove: true,
});
});
});
+28 -3
View File
@@ -1,5 +1,5 @@
import { renderToString, snapshotEverything } from "../helpers";
import { renderToString, renderToBdom, snapshotEverything, makeTestFixture } from "../helpers";
import { mount } from "../../src/blockdom";
// NB: check the snapshots to see where the SVG namespaces are added
snapshotEverything();
@@ -25,6 +25,31 @@ describe("properly support svg", () => {
test("namespace to svg tags added even if already in svg namespace", () => {
const template = `<svg><svg/></svg>`;
expect(renderToString(template)).toBe(`<svg><svg></svg></svg>`);
const bdom = renderToBdom(template);
const fixture = makeTestFixture();
mount(bdom, fixture);
const elems = fixture.querySelectorAll("svg");
expect(elems.length).toEqual(2);
for (const el of elems) {
expect(el.namespaceURI).toBe("http://www.w3.org/2000/svg");
}
});
test("svg namespace added to sub-blocks", () => {
const template = `<svg><path t-if="path"/></svg>`;
expect(renderToString(template, { path: false })).toBe(`<svg></svg>`);
expect(renderToString(template, { path: true })).toBe(`<svg><path></path></svg>`);
const bdom = renderToBdom(template, { path: true });
const fixture = makeTestFixture();
mount(bdom, fixture);
const elems = fixture.querySelectorAll("svg, path");
expect(elems.length).toEqual(2);
for (const el of elems) {
expect(el.namespaceURI).toBe("http://www.w3.org/2000/svg");
}
});
});
+20
View File
@@ -43,4 +43,24 @@ describe("t-key", () => {
})
).toBe("<ul><li>Chimay Rouge</li></ul>");
});
test("t-key on sub dom node pushes a child block in its parent", async () => {
const template = `
<div>
<t t-if="hasSpan"><span /></t>
<div t-key="key"><h1 /></div>
</div>
`;
expect(renderToString(template, { key: "1" })).toBe("<div><div><h1></h1></div></div>");
expect(renderToString(template, { hasSpan: true, key: "1" })).toBe(
"<div><span></span><div><h1></h1></div></div>"
);
const template2 = `
<div t-key="key"><h1 /></div>
`;
expect(renderToString(template2, { key: "1" })).toBe("<div><h1></h1></div>");
});
});
+13
View File
@@ -38,6 +38,19 @@ describe("t-set", () => {
expect(renderToString(template)).toBe("ok");
});
test("set from body literal (with t-if/t-else", () => {
const template = `
<t>
<t t-set="value">
<t t-if="condition">true</t>
<t t-else="">false</t>
</t>
<t t-esc="value"/>
</t>`;
expect(renderToString(template, { condition: true })).toBe("true");
expect(renderToString(template, { condition: false })).toBe("false");
});
test("set from attribute lookup", () => {
const template = `<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`;
expect(renderToString(template, { value: "ok" })).toBe("<div>ok</div>");
@@ -1,5 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`basics GrandChild display is controlled by its GrandParent 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let Comp1 = ctx['myComp'];
return toggler(Comp1, component(Comp1, {displayGrandChild: ctx['displayGrandChild']}, key + \`__1\`, node, ctx));
}
}"
`;
exports[`basics GrandChild display is controlled by its GrandParent 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2;
if (ctx['props'].displayGrandChild) {
b2 = component(\`GrandChild\`, {}, key + \`__1\`, node, ctx);
}
return multi([b2]);
}
}"
`;
exports[`basics GrandChild display is controlled by its GrandParent 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`basics Multi root component 1`] = `
"function anonymous(bdom, helpers
) {
@@ -265,6 +305,59 @@ exports[`basics class parent, class child component with props 2`] = `
}"
`;
exports[`basics component children doesn't leak (if case) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2;
if (ctx['ifVar']) {
b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
return multi([b2]);
}
}"
`;
exports[`basics component children doesn't leak (if case) 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`basics component children doesn't leak (t-key case) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['keyVar'];
return toggler(tKey_1, component(\`Child\`, {}, tKey_1 + key + \`__1\`, node, ctx));
}
}"
`;
exports[`basics component children doesn't leak (t-key case) 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`basics component with dynamic content can be updated 1`] = `
"function anonymous(bdom, helpers
) {
@@ -875,7 +968,7 @@ exports[`basics update props of component without concrete own node 1`] = `
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['childProps'].key;
let b2 = toggler(tKey_1, component(\`Child\`, ctx['childProps'], tKey_1 + key + \`__1\`, node, ctx));
let b2 = toggler(tKey_1, component(\`Child\`, Object.assign({}, ctx['childProps']), tKey_1 + key + \`__1\`, node, ctx));
return block1([], [b2]);
}
}"
@@ -1124,59 +1217,6 @@ exports[`t-out in components can render list of t-out 1`] = `
}"
`;
exports[`t-out in components component children doesn't leak (if case) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2;
if (ctx['ifVar']) {
b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
return multi([b2]);
}
}"
`;
exports[`t-out in components component children doesn't leak (if case) 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`t-out in components component children doesn't leak (t-key case) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['keyVar'];
return toggler(tKey_1, component(\`Child\`, {}, tKey_1 + key + \`__1\`, node, ctx));
}
}"
`;
exports[`t-out in components component children doesn't leak (t-key case) 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`t-out in components update properly on state changes 1`] = `
"function anonymous(bdom, helpers
) {
@@ -1,5 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Cascading renders after microtaskTick 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, withKey } = helpers;
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
let b3 = text(\` _ \`);
ctx = Object.create(ctx);
const [k_block4, v_block4, l_block4, c_block4] = prepareList(ctx['state']);
for (let i1 = 0; i1 < l_block4; i1++) {
ctx[\`elem\`] = v_block4[i1];
let key1 = ctx['elem'].id;
c_block4[i1] = withKey(text(ctx['elem'].id), key1);
}
let b4 = list(c_block4);
return multi([b2, b3, b4]);
}
}"
`;
exports[`Cascading renders after microtaskTick 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, withKey } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['state']);
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`elem\`] = v_block1[i1];
let key1 = ctx['elem'].id;
c_block1[i1] = withKey(component(\`Element\`, {id: ctx['elem'].id}, key + \`__1__\${key1}\`, node, ctx), key1);
}
return list(c_block1);
}
}"
`;
exports[`Cascading renders after microtaskTick 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].id);
}
}"
`;
exports[`async rendering destroying a widget before start is over 1`] = `
"function anonymous(bdom, helpers
) {
@@ -1093,7 +1145,7 @@ exports[`properly behave when destroyed/unmounted while rendering 2`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`SubChild\`, {}, key + \`__1\`, node, ctx);
let b2 = component(\`SubChild\`, {val: ctx['props'].val}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -780,6 +780,71 @@ exports[`can catch errors catching error, rethrow, render parent -- a main comp
}"
`;
exports[`can catch errors catching in child makes parent render 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, capture, withKey } = helpers;
function slot1(ctx, node, key = \\"\\") {
let Comp1 = ctx['elem'][1];
return toggler(Comp1, component(Comp1, {id: ctx['elem'][0]}, key + \`__1\`, node, ctx));
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(Object.entries(this.elements));
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`elem\`] = v_block1[i1];
let key1 = ctx['elem'][0];
const v1 = ctx['elem'];
const ctx1 = capture(ctx);
c_block1[i1] = withKey(component(\`Catch\`, {onError: (error)=>this.onError(v1[0],error),slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2__\${key1}\`, node, ctx), key1);
}
return list(c_block1);
}
}"
`;
exports[`can catch errors catching in child makes parent render 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callSlot } = helpers;
return function template(ctx, node, key = \\"\\") {
return callSlot(ctx, node, key, 'default', false, {});
}
}"
`;
exports[`can catch errors catching in child makes parent render 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`can catch errors catching in child makes parent render 4`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = 'Child '+ctx['props'].id;
return block1([txt1]);
}
}"
`;
exports[`can catch errors error in mounted on a component with a sibling (properly mounted) 1`] = `
"function anonymous(bdom, helpers
) {
@@ -1,5 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`event handling Invalid handler throws an error 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\">click</button>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['dosomething'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`event handling handler is not called if component is destroyed 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<span block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['click'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`event handling handler receive the event as argument 1`] = `
"function anonymous(bdom, helpers
) {
@@ -29,6 +57,37 @@ exports[`event handling handler receive the event as argument 2`] = `
}"
`;
exports[`event handling input blur event is not called if component is destroyed 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/><textarea/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2;
if (ctx['state'].cond) {
b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
return block1([], [b2]);
}
}"
`;
exports[`event handling input blur event is not called if component is destroyed 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<input block-handler-0=\\"blur\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['blur'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`event handling objects from scope are properly captured by t-on 1`] = `
"function anonymous(bdom, helpers
) {
@@ -187,6 +187,28 @@ exports[`lifecycle hooks hooks are called in proper order in widget creation/des
}"
`;
exports[`lifecycle hooks lifecycle callbacks are bound to component 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`Test\`, {rev: ctx['rev']}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`lifecycle hooks lifecycle callbacks are bound to component 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].rev);
}
}"
`;
exports[`lifecycle hooks lifecycle semantics 1`] = `
"function anonymous(bdom, helpers
) {
@@ -477,7 +499,7 @@ exports[`lifecycle hooks onWillRender 1`] = `
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {}, key + \`__1\`, node, ctx);
return component(\`Child\`, {someValue: ctx['state'].value}, key + \`__1\`, node, ctx);
}
}"
`;
@@ -122,7 +122,7 @@ exports[`basics support prop names that aren't valid bare object property names
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {'some-dashed-prop': 5,'a.b': 'keyword prop'}, key + \`__1\`, node, ctx);
return component(\`Child\`, {'some-dashed-prop': 5}, key + \`__1\`, node, ctx);
}
}"
`;
@@ -0,0 +1,163 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`force render in case of existing render 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`B\`, {val: ctx['state'].val}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`force render in case of existing render 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`C\`, {}, key + \`__1\`, node, ctx);
let b3 = text(ctx['props'].val);
return multi([b2, b3]);
}
}"
`;
exports[`force render in case of existing render 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`C\`);
}
}"
`;
exports[`rendering semantics can force a render to update sub tree 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2 = text(ctx['state'].value);
let b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`rendering semantics can force a render to update sub tree 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`child\`);
}
}"
`;
exports[`rendering semantics can render a parent without rendering child 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2 = text(ctx['state'].value);
let b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`rendering semantics can render a parent without rendering child 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`child\`);
}
}"
`;
exports[`rendering semantics props are reactive (nested prop) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {a: ctx['state']}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`rendering semantics props are reactive (nested prop) 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].a.b.c);
}
}"
`;
exports[`rendering semantics props are reactive 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {a: ctx['state']}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`rendering semantics props are reactive 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].a.b);
}
}"
`;
exports[`rendering semantics rendering is atomic (for one subtree) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2 = text(ctx['state'].obj.val);
let b3 = component(\`B\`, {obj: ctx['state'].obj}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`rendering semantics rendering is atomic (for one subtree) 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`C\`, {obj: ctx['props'].obj}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`rendering semantics rendering is atomic (for one subtree) 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].obj.val);
}
}"
`;
@@ -1416,6 +1416,93 @@ exports[`slots slot are properly rendered if inner props are changed 3`] = `
}"
`;
exports[`slots slot content has different key from other content -- dynamic slot 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
function slot1(ctx, node, key = \\"\\") {
return component(\`Child\`, {parent: 'Parent'}, key + \`__1\`, node, ctx);
}
return function template(ctx, node, key = \\"\\") {
return component(\`SlotDisplay\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
}
}"
`;
exports[`slots slot content has different key from other content -- dynamic slot 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callSlot } = helpers;
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {parent: 'SlotDisplay'}, key + \`__1\`, node, ctx);
const slot1 = (ctx['slotName']);
let b3 = toggler(slot1, callSlot(ctx, node, key, slot1), true, {});
return multi([b2, b3]);
}
}"
`;
exports[`slots slot content has different key from other content -- dynamic slot 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['props'].parent;
return block1([txt1]);
}
}"
`;
exports[`slots slot content has different key from other content -- static slot 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
function slot1(ctx, node, key = \\"\\") {
return component(\`Child\`, {parent: 'Parent'}, key + \`__1\`, node, ctx);
}
return function template(ctx, node, key = \\"\\") {
return component(\`SlotDisplay\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
}
}"
`;
exports[`slots slot content has different key from other content -- static slot 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callSlot } = helpers;
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {parent: 'SlotDisplay'}, key + \`__1\`, node, ctx);
let b3 = callSlot(ctx, node, key, 'default', false, {});
return multi([b2, b3]);
}
}"
`;
exports[`slots slot content has different key from other content -- static slot 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['props'].parent;
return block1([txt1]);
}
}"
`;
exports[`slots slot content is bound to caller (variation) 1`] = `
"function anonymous(bdom, helpers
) {
@@ -251,6 +251,56 @@ exports[`t-call parent is set within t-call with no parentNode 3`] = `
}"
`;
exports[`t-call recursive t-call binding this -- static t-call 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue, getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`recursive\`);
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
ctx = Object.create(ctx);
ctx[isBoundary] = 1;
setContextValue(ctx, \\"level\\", 0);
let b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
`;
exports[`t-call recursive t-call binding this -- static t-call 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue, getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`recursive\`);
let block3 = createBlock(\`<div block-handler-0=\\"click.stop\\"><block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
let b2;
if (ctx['level']<2) {
let hdlr1 = [\\"stop\\", ctx['onClicked'].bind(this), ctx];
let txt1 = ctx['level'];
let b3 = block3([hdlr1, txt1]);
ctx = Object.create(ctx);
ctx[isBoundary] = 1;
setContextValue(ctx, \\"level\\", ctx['level']+1);
let b4 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
ctx = ctx.__proto__;
b2 = multi([b3, b4]);
}
return multi([b2]);
}
}"
`;
exports[`t-call sub components in two t-calls 1`] = `
"function anonymous(bdom, helpers
) {
@@ -8,7 +8,7 @@ exports[`t-props basic use 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, ctx['some'].obj, key + \`__1\`, node, ctx);
let b2 = component(\`Child\`, Object.assign({}, ctx['some'].obj), key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -65,7 +65,7 @@ exports[`t-props t-props only 1`] = `
let block1 = createBlock(\`<div><div><block-child-0/></div></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Comp\`, ctx['state'], key + \`__1\`, node, ctx);
let b2 = component(\`Comp\`, Object.assign({}, ctx['state']), key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -93,7 +93,7 @@ exports[`t-props t-props with props 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, Object.assign({}, ctx['props'], {a: 1,b: 2}), key + \`__1\`, node, ctx);
let b2 = component(\`Child\`, Object.assign({}, ctx['childProps'], {a: 1,b: 2}), key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
+107 -70
View File
@@ -121,7 +121,7 @@ describe("basics", () => {
class Test extends Component {
static template = xml`<span>simple vnode</span>`;
setup() {
expect(this.props).toBe(p);
expect(this.props).not.toBe(p);
}
}
@@ -794,6 +794,112 @@ describe("basics", () => {
await nextTick();
expect(fixture.textContent!.trim()).toBe("2__3");
});
test("component children doesn't leak (if case)", async () => {
class Child extends Component {
static template = xml`<div />`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static components = { Child };
static template = xml`<Child t-if="ifVar" />`;
ifVar = true;
}
const parent = await mount(Parent, fixture);
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:mounted",
]).toBeLogged();
parent.ifVar = false;
parent.render();
await nextTick();
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(0);
expect(["Child:willUnmount", "Child:willDestroy"]).toBeLogged();
});
test("component children doesn't leak (t-key case)", async () => {
// This test should encompass the t-foreach and t-call cases too (because they also use a flavor of some key)
class Child extends Component {
static template = xml`<div />`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static components = { Child };
static template = xml`<Child t-key="keyVar" />`;
keyVar = 1;
}
const parent = await mount(Parent, fixture);
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:mounted",
]).toBeLogged();
parent.keyVar = 2;
parent.render();
await nextTick();
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:willUnmount",
"Child:willDestroy",
"Child:mounted",
]).toBeLogged();
});
test("GrandChild display is controlled by its GrandParent", async () => {
class GrandChild extends Component {
static template = xml`<div />`;
setup() {
useLogLifecycle();
}
}
class Child extends Component {
static components = { GrandChild };
static template = xml`<GrandChild t-if="props.displayGrandChild" />`;
}
class Parent extends Component {
static template = xml`<t t-component="myComp" displayGrandChild="displayGrandChild"/>`;
myComp = Child;
displayGrandChild = true;
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
expect([
"GrandChild:setup",
"GrandChild:willStart",
"GrandChild:willRender",
"GrandChild:rendered",
"GrandChild:mounted",
]).toBeLogged();
parent.displayGrandChild = false;
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(["GrandChild:willUnmount", "GrandChild:willDestroy"]).toBeLogged();
});
});
describe("mount targets", () => {
@@ -900,73 +1006,4 @@ describe("t-out in components", () => {
"<div>&lt;b&gt;one&lt;/b&gt;<b>one</b>&lt;b&gt;two&lt;/b&gt;<b>two</b>&lt;b&gt;tree&lt;/b&gt;<b>tree</b></div>"
);
});
test("component children doesn't leak (if case)", async () => {
class Child extends Component {
static template = xml`<div />`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static components = { Child };
static template = xml`<Child t-if="ifVar" />`;
ifVar = true;
}
const parent = await mount(Parent, fixture);
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:mounted",
]).toBeLogged();
parent.ifVar = false;
parent.render();
await nextTick();
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(0);
expect(["Child:willUnmount", "Child:willDestroy"]).toBeLogged();
});
test("component children doesn't leak (t-key case)", async () => {
// This test should encompass the t-foreach and t-call cases too (because they also use a flavor of some key)
class Child extends Component {
static template = xml`<div />`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static components = { Child };
static template = xml`<Child t-key="keyVar" />`;
keyVar = 1;
}
const parent = await mount(Parent, fixture);
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:mounted",
]).toBeLogged();
parent.keyVar = 2;
parent.render();
await nextTick();
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:willUnmount",
"Child:willDestroy",
"Child:mounted",
]).toBeLogged();
});
});
+73 -18
View File
@@ -39,6 +39,8 @@ Scheduler.prototype.addFiber = function (fiber: Fiber) {
afterEach(() => {
if (lastScheduler && lastScheduler.tasks.size > 0) {
// we still clear the scheduler to prevent additional noise
lastScheduler.tasks.clear();
throw new Error("we got a memory leak...");
}
});
@@ -521,7 +523,7 @@ test("properly behave when destroyed/unmounted while rendering ", async () => {
}
class Child extends Component {
static template = xml`<div><SubChild /></div>`;
static template = xml`<div><SubChild val="props.val"/></div>`;
static components = { SubChild };
setup() {
useLogLifecycle();
@@ -1907,18 +1909,13 @@ test("concurrent renderings scenario 13", async () => {
await nextTick(); // wait for this change to be applied
expect([
"Parent:willRender",
"Child:willUpdateProps",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:mounted",
"Child:patched",
"Parent:patched",
"Child:willRender",
"Child:rendered",
@@ -2472,9 +2469,9 @@ test("two renderings initiated between willPatch and patched", async () => {
useLogLifecycle();
onMounted(() => {
this.mounted = "Mounted";
parent.render();
parent.render(true);
});
onWillUnmount(() => parent.render());
onWillUnmount(() => parent.render(true));
}
}
@@ -2507,15 +2504,11 @@ test("two renderings initiated between willPatch and patched", async () => {
"Parent:rendered",
]).toBeLogged();
await nextMicroTick();
expect(["Panel:willRender", "Panel:rendered"]).toBeLogged();
await nextTick();
expect([
"Panel:willRender",
"Panel:rendered",
"Parent:willPatch",
"Panel:willPatch",
"Panel:patched",
"Parent:patched",
]).toBeLogged();
expect(["Parent:willPatch", "Panel:willPatch", "Panel:patched", "Parent:patched"]).toBeLogged();
expect(fixture.innerHTML).toBe("<div><abc>Panel1Mounted</abc></div>");
parent.state.panel = "Panel2";
@@ -2753,12 +2746,20 @@ test("delay willUpdateProps with rendering grandchild", async () => {
static template = xml`<Parent state="state"/>`;
static components = { Parent };
state = { value: 0 };
setup() {
useLogLifecycle();
}
}
const parent = await mount(GrandParent, fixture);
expect(fixture.innerHTML).toBe("0_0<div></div>");
expect([
"GrandParent:setup",
"GrandParent:willStart",
"GrandParent:willRender",
"Parent:setup",
"Parent:willStart",
"GrandParent:rendered",
"Parent:willRender",
"DelayedChild:setup",
"DelayedChild:willStart",
@@ -2772,20 +2773,23 @@ test("delay willUpdateProps with rendering grandchild", async () => {
"ReactiveChild:mounted",
"DelayedChild:mounted",
"Parent:mounted",
"GrandParent:mounted",
]).toBeLogged();
promise = makeDeferred();
const prom1 = promise;
parent.state.value = 1;
child.render(); // trigger a root rendering first
parent.render();
parent.render(true);
reactiveChild.render();
await nextTick();
expect(fixture.innerHTML).toBe("0_0<div></div>");
expect([
"DelayedChild:willRender",
"DelayedChild:rendered",
"GrandParent:willRender",
"Parent:willUpdateProps",
"GrandParent:rendered",
"ReactiveChild:willRender",
"ReactiveChild:rendered",
"Parent:willRender",
@@ -2800,12 +2804,14 @@ test("delay willUpdateProps with rendering grandchild", async () => {
const prom2 = promise;
child.render(); // trigger a root rendering first
parent.state.value = 2;
parent.render();
parent.render(true);
reactiveChild.render();
await nextTick();
expect(fixture.innerHTML).toBe("0_0<div></div>");
expect([
"GrandParent:willRender",
"Parent:willUpdateProps",
"GrandParent:rendered",
"ReactiveChild:willRender",
"ReactiveChild:rendered",
"Parent:willRender",
@@ -2822,12 +2828,14 @@ test("delay willUpdateProps with rendering grandchild", async () => {
expect([
"DelayedChild:willRender",
"DelayedChild:rendered",
"GrandParent:willPatch",
"Parent:willPatch",
"ReactiveChild:willPatch",
"DelayedChild:willPatch",
"DelayedChild:patched",
"ReactiveChild:patched",
"Parent:patched",
"GrandParent:patched",
]).toBeLogged();
prom1.resolve();
@@ -3082,6 +3090,53 @@ test("t-foreach with dynamic async component", async () => {
"Child (3):mounted",
]).toBeLogged();
});
test("Cascading renders after microtaskTick", async () => {
const state = [{ id: 0 }, { id: 1 }];
let child: any;
let parent: any;
class Element extends Component {
static template = xml`<t t-esc="props.id" />`;
}
class Child extends Component {
static components = { Element };
static template = xml`
<t t-foreach="state" t-as="elem" t-key="elem.id">
<Element id="elem.id"/>
</t>`;
state = state;
setup() {
child = this;
}
}
class Parent extends Component {
static components = { Child };
static template = xml`<Child /> _ <t t-foreach="state" t-as="elem" t-key="elem.id" t-esc="elem.id"/>`;
state = state;
setup() {
parent = this;
}
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("01 _ 01");
state.push({ id: 2 });
parent.render();
child.render();
await Promise.resolve();
expect(fixture.innerHTML).toBe("01 _ 01");
state.push({ id: 3 });
parent.render();
child.render();
await nextTick();
expect(fixture.innerHTML).toBe("0123 _ 0123");
});
// test.skip("components with shouldUpdate=false", async () => {
// const state = { p: 1, cc: 10 };
+52
View File
@@ -1033,4 +1033,56 @@ describe("can catch errors", () => {
await nextTick();
expect(fixture.innerHTML).toBe("<div>Sibling</div>");
});
test("catching in child makes parent render", async () => {
class Child extends Component {
static template = xml`<div t-esc="'Child ' + props.id" />`;
}
class ErrorComp extends Component {
static template = xml`<div />`;
setup() {
throw new Error("Error Component");
}
}
class Catch extends Component {
static template = xml`<t t-slot="default" />`;
setup() {
onError((error) => {
this.props.onError(error);
});
}
}
const steps: any[] = [];
class Parent extends Component {
static components = { Catch };
static template = xml`
<t t-foreach="Object.entries(this.elements)" t-as="elem" t-key="elem[0]">
<Catch onError="(error) => this.onError(elem[0], error)">
<t t-component="elem[1]" id="elem[0]" />
</Catch>
</t>
`;
elements: any = {};
onError(id: any, error: Error) {
steps.push(error.message);
delete this.elements[id];
this.elements[2] = Child;
this.render();
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("");
parent.elements[1] = ErrorComp;
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe("<div>Child 2</div>");
expect(steps).toEqual(["Error Component"]);
});
});
+80 -2
View File
@@ -1,5 +1,5 @@
import { makeTestFixture, snapshotEverything, nextTick } from "../helpers";
import { mount, Component, useState, xml } from "../../src/index";
import { makeTestFixture, snapshotEverything, nextTick, logStep, nextMicroTick } from "../helpers";
import { mount, Component, useState, xml, App } from "../../src/index";
snapshotEverything();
@@ -33,6 +33,28 @@ describe("event handling", () => {
expect(fixture.innerHTML).toBe("<span><div>simple vnode</div>2</span>");
});
test("Invalid handler throws an error", async () => {
window.addEventListener(
"error",
(ev) => {
logStep(ev.error.message);
ev.preventDefault();
},
{ once: true }
);
class Parent extends Component {
static template = xml`<button t-on-click="dosomething">click</button>`;
doSomething() {}
}
await mount(Parent, fixture);
expect([]).toBeLogged();
fixture.querySelector("button")!.click();
expect(["Invalid handler (expected a function, received: 'undefined')"]).toBeLogged();
});
test("support for callable expression in event handler", async () => {
class Counter extends Component {
static template = xml`
@@ -93,4 +115,60 @@ describe("event handling", () => {
expect(onClickArgs![0]).toBe(1);
expect(onClickArgs![1]).toBeInstanceOf(MouseEvent);
});
test("handler is not called if component is destroyed", async () => {
class Parent extends Component {
static template = xml`<span t-on-click="click"/>`;
click() {
logStep("click");
}
}
const app = new App(Parent);
await app.mount(fixture);
const span = fixture.querySelector("span")!;
span.click();
expect(["click"]).toBeLogged();
app.destroy();
expect([]).toBeLogged();
span.click();
expect([]).toBeLogged();
});
test("input blur event is not called if component is destroyed", async () => {
class Child extends Component {
static template = xml`<input t-on-blur="blur"/>`;
blur() {
logStep("blur");
}
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="state.cond"><Child/></t>
<textarea/>
</div>`;
static components = { Child };
state = useState({ cond: true });
}
const parent = await mount(Parent, fixture);
fixture.querySelector("input")!.focus();
await nextMicroTick();
// to unfocus input
fixture.querySelector("textarea")!.focus();
expect(["blur"]).toBeLogged();
fixture.querySelector("input")!.focus();
parent.state.cond = false;
await nextTick();
// input is removed when component is destroyed => nothing should happen
expect([]).toBeLogged();
});
});
+1 -1
View File
@@ -223,7 +223,7 @@ describe("hooks", () => {
expect(fixture.innerHTML).toBe("<div>maggot brain</div>");
someVal = "brain";
someVal2 = "maggot";
component.render();
component.render(true);
await nextTick();
expect(fixture.innerHTML).toBe("<div>brain maggot</div>");
});
+60 -7
View File
@@ -5,10 +5,13 @@ import {
onPatched,
onWillUpdateProps,
onWillRender,
onWillDestroy,
onRendered,
} from "../../src/component/lifecycle_hooks";
import { status } from "../../src/component/status";
import {
elem,
logStep,
makeDeferred,
makeTestFixture,
nextTick,
@@ -846,8 +849,9 @@ describe("lifecycle hooks", () => {
class Parent extends Component {
static template = xml`
<Child />`;
<Child someValue="state.value" />`;
static components = { Child };
state = useState({ value: 1 });
setup() {
useLogLifecycle();
}
@@ -868,7 +872,7 @@ describe("lifecycle hooks", () => {
"Parent:mounted",
]).toBeLogged();
parent.render(); // to block child render
parent.state.value++; // to block child render
await nextTick();
expect(["Parent:willRender", "Child:willUpdateProps", "Parent:rendered"]).toBeLogged();
@@ -1005,20 +1009,15 @@ describe("lifecycle hooks", () => {
await nextTick();
expect([
"C:willRender",
"D:willUpdateProps",
"F:setup",
"F:willStart",
"C:rendered",
"D:willRender",
"D:rendered",
"F:willRender",
"F:rendered",
"C:willPatch",
"D:willPatch",
"E:willUnmount",
"E:willDestroy",
"F:mounted",
"D:patched",
"C:patched",
]).toBeLogged();
});
@@ -1196,4 +1195,58 @@ describe("lifecycle hooks", () => {
expect(["Parent:willPatch", "Parent:patched"]).toBeLogged();
expect(fixture.innerHTML).toBe("<span>Patched</span>");
});
test("lifecycle callbacks are bound to component", async () => {
expect.assertions(14);
let instance: any;
class Test extends Component {
static template = xml`<t t-esc="props.rev" />`;
setup() {
instance = this;
onWillStart(this.logger("onWillStart"));
onMounted(this.logger("onMounted"));
onWillUpdateProps(this.logger("onWillUpdateProps"));
onWillPatch(this.logger("onWillPatch"));
onPatched(this.logger("onPatched"));
onWillUnmount(this.logger("onWillUnmount"));
onWillDestroy(this.logger("onWillDestroy"));
onWillRender(this.logger("onWillRender"));
onRendered(this.logger("onRendered"));
}
logger(hookName: string) {
return function (this: Test) {
logStep(hookName);
expect(this === instance).toBe(true);
};
}
}
class Parent extends Component {
static template = xml`<Test rev="rev" />`;
static components = { Test };
rev = 0;
}
const app = new App(Parent);
const comp = await app.mount(fixture);
comp.rev++;
comp.render();
await nextTick();
app.destroy();
expect([
"onWillStart",
"onWillRender",
"onRendered",
"onMounted",
"onWillUpdateProps",
"onWillRender",
"onRendered",
"onWillPatch",
"onPatched",
"onWillUnmount",
"onWillDestroy",
]).toBeLogged();
});
});
+17 -3
View File
@@ -142,17 +142,16 @@ describe("basics", () => {
});
test("support prop names that aren't valid bare object property names", async () => {
expect.assertions(4);
expect.assertions(3);
class Child extends Component {
static template = xml`<button t-on-click="props.onClick"/>`;
setup() {
expect(this.props["some-dashed-prop"]).toBe(5);
expect(this.props["a.b"]).toBe("keyword prop");
}
}
class Parent extends Component {
static template = xml`<Child some-dashed-prop="5" a.b="'keyword prop'"/>`;
static template = xml`<Child some-dashed-prop="5"/>`;
static components = { Child };
}
await mount(Parent, fixture);
@@ -225,3 +224,18 @@ test("bound functions is referentially equal after update", async () => {
expect(fixture.innerHTML).toBe("3");
expect(isEqual).toBe(true);
});
test("throw if prop uses an unknown suffix", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.val"/>`;
}
class Parent extends Component {
static template = xml`<Child val.somesuffix="state.val"/>`;
static components = { Child };
}
await expect(async () => {
await mount(Parent, fixture);
}).rejects.toThrowError("Invalid prop suffix");
});
+346
View File
@@ -0,0 +1,346 @@
import { Component, mount, onRendered, onWillUpdateProps, useState, xml } from "../../src";
import {
makeTestFixture,
snapshotEverything,
nextTick,
useLogLifecycle,
makeDeferred,
} from "../helpers";
let fixture: HTMLElement;
snapshotEverything();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("rendering semantics", () => {
test("can render a parent without rendering child", async () => {
class Child extends Component {
static template = xml`child`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`
<t t-esc="state.value"/>
<Child/>
`;
static components = { Child };
state = useState({ value: "A" });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("Achild");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
parent.state.value = "B";
await nextTick();
expect(fixture.innerHTML).toBe("Bchild");
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
});
test("can force a render to update sub tree", async () => {
let childN = 0;
let parentN = 0;
class Child extends Component {
static template = xml`child`;
setup() {
onRendered(() => childN++);
}
}
class Parent extends Component {
static template = xml`
<t t-esc="state.value"/>
<Child/>
`;
static components = { Child };
state = { value: "A" };
setup() {
onRendered(() => parentN++);
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("Achild");
expect(parentN).toBe(1);
expect(childN).toBe(1);
parent.state.value = "B";
parent.render(true);
await nextTick();
expect(fixture.innerHTML).toBe("Bchild");
expect(parentN).toBe(2);
expect(childN).toBe(2);
});
test("props are reactive", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.a.b"/>`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`
<Child a="state"/>
`;
static components = { Child };
state = useState({ b: 1 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
parent.state.b = 3;
await nextTick();
expect(fixture.innerHTML).toBe("3");
expect(["Child:willRender", "Child:rendered", "Child:willPatch", "Child:patched"]).toBeLogged();
});
test("props are reactive (nested prop)", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.a.b.c"/>`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`
<Child a="state"/>
`;
static components = { Child };
state = useState({ b: { c: 1 } });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
parent.state.b.c = 3; // parent is now subscribed to 'b' key
await nextTick();
expect(fixture.innerHTML).toBe("3");
expect(["Child:willRender", "Child:rendered", "Child:willPatch", "Child:patched"]).toBeLogged();
parent.state.b = { c: 444 }; // triggers a parent and a child render
await nextTick();
expect(fixture.innerHTML).toBe("444");
expect([
"Parent:willRender",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Parent:patched",
"Child:willPatch",
"Child:patched",
]).toBeLogged();
});
test("rendering is atomic (for one subtree)", async () => {
const def = makeDeferred();
class C extends Component {
static template = xml`<t t-esc="props.obj.val"/>`;
setup() {
useLogLifecycle();
}
}
class B extends Component {
static template = xml`<C obj="props.obj"/>`;
static components = { C };
setup() {
useLogLifecycle();
onWillUpdateProps(() => def);
}
}
class A extends Component {
static template = xml`<t t-esc="state.obj.val"/><B obj="state.obj"/>`;
static components = { B };
state = useState({ obj: { val: 1 } });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("11");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
parent.state.obj.val = 3;
await nextTick();
expect(fixture.innerHTML).toBe("33");
expect([
"A:willRender",
"A:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"A:patched",
"C:willPatch",
"C:patched",
]).toBeLogged();
def.resolve();
await nextTick();
expect([]).toBeLogged();
});
});
test("force render in case of existing render", async () => {
const def = makeDeferred();
class C extends Component {
static template = xml`C`;
setup() {
useLogLifecycle();
}
}
class B extends Component {
static template = xml`<C/><t t-esc="props.val"/>`;
static components = { C };
setup() {
useLogLifecycle();
onWillUpdateProps(() => def);
}
}
class A extends Component {
static template = xml`<B val="state.val"/>`;
static components = { B };
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("C1");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// trigger a new rendering, blocked in B
parent.state.val = 2;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
// initiate a new render with force=true. it should cancel the current render
// and also be blocked in B
parent.render(true);
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
def.resolve();
await nextTick();
// we check here that the render reaches C (so, that it was properly forced)
expect([
"B:willRender",
"C:willUpdateProps",
"B:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"B:willPatch",
"C:willPatch",
"C:patched",
"B:patched",
"A:patched",
]).toBeLogged();
});
+45
View File
@@ -1544,4 +1544,49 @@ describe("slots", () => {
}
await mount(Parent, fixture);
});
test("slot content has different key from other content -- static slot", async () => {
class Child extends Component {
static template = xml`<div t-esc="props.parent" />`;
}
class SlotDisplay extends Component {
static components = { Child };
static template = xml`<Child parent="'SlotDisplay'" /><t t-slot="default" />`;
}
class Parent extends Component {
static components = { Child, SlotDisplay };
static template = xml`
<SlotDisplay>
<Child parent="'Parent'" />
</SlotDisplay>`;
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div>SlotDisplay</div><div>Parent</div>");
});
test("slot content has different key from other content -- dynamic slot", async () => {
class Child extends Component {
static template = xml`<div t-esc="props.parent" />`;
}
class SlotDisplay extends Component {
static components = { Child };
slotName = "default";
static template = xml`<Child parent="'SlotDisplay'" /><t t-slot="{{ slotName }}" />`;
}
class Parent extends Component {
static components = { Child, SlotDisplay };
static template = xml`
<SlotDisplay>
<Child parent="'Parent'" />
</SlotDisplay>`;
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div>SlotDisplay</div><div>Parent</div>");
});
});
+38
View File
@@ -206,4 +206,42 @@ describe("t-call", () => {
expect(fixture.innerHTML).toBe(`<div id="0"></div><div id="1"></div>`);
});
test("recursive t-call binding this -- static t-call", async () => {
let clickCount = 0;
class Parent extends Component {
onClicked?: Function;
setup() {
const instance = this;
this.onClicked = function () {
clickCount++;
expect(this).toBe(instance);
};
}
static template = xml`
<div><t t-call="recursive"><t t-set="level" t-value="0" /></t></div>
`;
}
const app = new App(Parent);
app.addTemplate(
"recursive",
`
<t t-if="level &lt; 2" >
<div t-on-click.stop="onClicked.bind(this)" t-esc="level" />
<t t-call="recursive">
<t t-set="level" t-value="level + 1" />
</t>
</t>
`
);
await app.mount(fixture);
for (const div of fixture.querySelectorAll("div")) {
div.click();
}
expect(clickCount).toBe(2);
});
});
+3 -3
View File
@@ -65,7 +65,7 @@ describe("t-props", () => {
`;
setup() {
expect(this.props).toEqual({ a: 1, b: 2 });
expect(this.props).toBe(props);
expect(this.props).not.toBe(props);
}
}
class Parent extends Component {
@@ -95,12 +95,12 @@ describe("t-props", () => {
class Parent extends Component {
static template = xml`
<div>
<Child t-props="props" a="1" b="2" />
<Child t-props="childProps" a="1" b="2" />
</div>
`;
static components = { Child };
props = { a: "a", c: "c" };
childProps = { a: "a", c: "c" };
}
await mount(Parent, fixture);
+207 -94
View File
@@ -4,9 +4,9 @@ exports[`Portal Add and remove portals 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, callPortal, withKey } = helpers;
let { prepareList, Portal, capture, withKey } = helpers;
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
let b3 = text(\` Portal\`);
let b4 = text(ctx['portalId']);
return multi([b3, b4]);
@@ -18,9 +18,37 @@ exports[`Portal Add and remove portals 1`] = `
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`portalId\`] = v_block1[i1];
let key1 = ctx['portalId'];
c_block1[i1] = withKey(callPortal(ctx, node, key, '#outside', portalContent1), key1);
const ctx1 = capture(ctx);
c_block1[i1] = withKey(component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__1__\${key1}\`, node, ctx), key1);
}
return list(c_block1, true);
return list(c_block1);
}
}"
`;
exports[`Portal Add and remove portals on div 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, Portal, capture, withKey } = helpers;
let block2 = createBlock(\`<div> Portal<block-text-0/></div>\`);
function slot1(ctx, node, key = \\"\\") {
let txt1 = ctx['portalId'];
return block2([txt1]);
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['portalIds']);
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`portalId\`] = v_block1[i1];
let key1 = ctx['portalId'];
const ctx1 = capture(ctx);
c_block1[i1] = withKey(component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__1__\${key1}\`, node, ctx), key1);
}
return list(c_block1);
}
}"
`;
@@ -29,11 +57,11 @@ exports[`Portal Add and remove portals with t-foreach 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, callPortal, withKey } = helpers;
let { prepareList, Portal, capture, withKey } = helpers;
let block2 = createBlock(\`<div><block-text-0/><block-child-0/></div>\`, true);
let block2 = createBlock(\`<div><block-text-0/><block-child-0/></div>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
let b4 = text(\` Portal\`);
let b5 = text(ctx['portalId']);
return multi([b4, b5]);
@@ -46,10 +74,73 @@ exports[`Portal Add and remove portals with t-foreach 1`] = `
ctx[\`portalId\`] = v_block1[i1];
let key1 = ctx['portalId'];
let txt1 = ctx['portalId'];
let b6 = callPortal(ctx, node, key, '#outside', portalContent1);
const ctx1 = capture(ctx);
let b6 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__1__\${key1}\`, node, ctx);
c_block1[i1] = withKey(block2([txt1], [b6]), key1);
}
return list(c_block1, true);
return list(c_block1);
}
}"
`;
exports[`Portal Add and remove portals with t-foreach and destroy 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, Portal, capture, withKey } = helpers;
let block2 = createBlock(\`<div><block-text-0/><block-child-0/></div>\`);
function slot1(ctx, node, key = \\"\\") {
let b4 = text(\` Portal\`);
let b5 = text(ctx['portalId']);
return multi([b4, b5]);
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['portalIds']);
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`portalId\`] = v_block1[i1];
let key1 = ctx['portalId'];
let txt1 = ctx['portalId'];
const ctx1 = capture(ctx);
let b6 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__1__\${key1}\`, node, ctx);
c_block1[i1] = withKey(block2([txt1], [b6]), key1);
}
return list(c_block1);
}
}"
`;
exports[`Portal Add and remove portals with t-foreach inside div 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, Portal, capture, withKey } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block3 = createBlock(\`<div><block-text-0/><block-child-0/></div>\`);
function slot1(ctx, node, key = \\"\\") {
let b5 = text(\` Portal\`);
let b6 = text(ctx['portalId']);
return multi([b5, b6]);
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['portalIds']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`portalId\`] = v_block2[i1];
let key1 = ctx['portalId'];
let txt1 = ctx['portalId'];
const ctx1 = capture(ctx);
let b7 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__1__\${key1}\`, node, ctx);
c_block2[i1] = withKey(block3([txt1], [b7]), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
}
}"
`;
@@ -76,14 +167,14 @@ exports[`Portal Portal composed with t-slot 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal, callSlot } = helpers;
let { Portal, callSlot } = helpers;
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return callSlot(ctx, node, key, 'default', false, {});
}
return function template(ctx, node, key = \\"\\") {
return callPortal(ctx, node, key, '#outside', portalContent1);
return component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
}
}"
`;
@@ -106,17 +197,17 @@ exports[`Portal basic use of portal 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><span>1</span><block-child-0/></div>\`);
let block2 = createBlock(\`<p>2</p>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return block2();
}
return function template(ctx, node, key = \\"\\") {
let b3 = callPortal(ctx, node, key, '#outside', portalContent1);
let b3 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return block1([], [b3]);
}
}"
@@ -126,17 +217,37 @@ exports[`Portal basic use of portal in dev mode 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><span>1</span><block-child-0/></div>\`);
let block2 = createBlock(\`<p>2</p>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return block2();
}
return function template(ctx, node, key = \\"\\") {
let b3 = callPortal(ctx, node, key, '#outside', portalContent1);
let b3 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return block1([], [b3]);
}
}"
`;
exports[`Portal basic use of portal on div 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { Portal } = helpers;
let block1 = createBlock(\`<div><span>1</span><block-child-0/></div>\`);
let block2 = createBlock(\`<div><p>2</p></div>\`);
function slot1(ctx, node, key = \\"\\") {
return block2();
}
return function template(ctx, node, key = \\"\\") {
let b3 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return block1([], [b3]);
}
}"
@@ -146,11 +257,11 @@ exports[`Portal conditional use of Portal (with sub Component) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block2 = createBlock(\`<span>1</span>\`, true);
let block2 = createBlock(\`<span>1</span>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return component(\`Child\`, {val: ctx['state'].val}, key + \`__1\`, node, ctx);
}
@@ -158,9 +269,9 @@ exports[`Portal conditional use of Portal (with sub Component) 1`] = `
let b2,b4;
b2 = block2();
if (ctx['state'].hasPortal) {
b4 = callPortal(ctx, node, key, '#outside', portalContent1);
b4 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
}
return multi([b2, b4], true);
return multi([b2, b4]);
}
}"
`;
@@ -183,12 +294,12 @@ exports[`Portal conditional use of Portal 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block2 = createBlock(\`<span>1</span>\`, true);
let block3 = createBlock(\`<p>2</p>\`, true);
let block2 = createBlock(\`<span>1</span>\`);
let block3 = createBlock(\`<p>2</p>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return block3();
}
@@ -196,9 +307,9 @@ exports[`Portal conditional use of Portal 1`] = `
let b2,b4;
b2 = block2();
if (ctx['state'].hasPortal) {
b4 = callPortal(ctx, node, key, '#outside', portalContent1);
b4 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
}
return multi([b2, b4], true);
return multi([b2, b4]);
}
}"
`;
@@ -222,12 +333,12 @@ exports[`Portal conditional use of Portal with child and div 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, callPortal, withKey } = helpers;
let { prepareList, Portal, capture, withKey } = helpers;
let block1 = createBlock(\`<div><span>hasPortal</span><block-child-0/></div>\`);
let block3 = createBlock(\`<p>thePortal</p>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return block3();
}
@@ -237,9 +348,10 @@ exports[`Portal conditional use of Portal with child and div 2`] = `
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`elem\`] = v_block2[i1];
let key1 = ctx['elem'];
c_block2[i1] = withKey(callPortal(ctx, node, key, '#outside', portalContent1), key1);
const ctx1 = capture(ctx);
c_block2[i1] = withKey(component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__1__\${key1}\`, node, ctx), key1);
}
let b2 = list(c_block2, true);
let b2 = list(c_block2);
return block1([], [b2]);
}
}"
@@ -267,12 +379,12 @@ exports[`Portal conditional use of Portal with child and div, variation 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, callPortal, withKey } = helpers;
let { prepareList, Portal, capture, withKey } = helpers;
let block2 = createBlock(\`<span>hasPortal</span>\`, true);
let block4 = createBlock(\`<p>thePortal</p>\`, true);
let block2 = createBlock(\`<span>hasPortal</span>\`);
let block4 = createBlock(\`<p>thePortal</p>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return block4();
}
@@ -283,10 +395,11 @@ exports[`Portal conditional use of Portal with child and div, variation 2`] = `
for (let i1 = 0; i1 < l_block3; i1++) {
ctx[\`elem\`] = v_block3[i1];
let key1 = ctx['elem'];
c_block3[i1] = withKey(callPortal(ctx, node, key, '#outside', portalContent1), key1);
const ctx1 = capture(ctx);
c_block3[i1] = withKey(component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__1__\${key1}\`, node, ctx), key1);
}
let b3 = list(c_block3, true);
return multi([b2, b3], true);
let b3 = list(c_block3);
return multi([b2, b3]);
}
}"
`;
@@ -295,22 +408,22 @@ exports[`Portal conditional use of Portal with div 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block2 = createBlock(\`<div><span>hasPortal</span><block-child-0/></div>\`, true);
let block3 = createBlock(\`<p>thePortal</p>\`, true);
let block2 = createBlock(\`<div><span>hasPortal</span><block-child-0/></div>\`);
let block3 = createBlock(\`<p>thePortal</p>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return block3();
}
return function template(ctx, node, key = \\"\\") {
let b2;
if (ctx['state'].hasPortal) {
let b4 = callPortal(ctx, node, key, '#outside', portalContent1);
let b4 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
b2 = block2([], [b4]);
}
return multi([b2], true);
return multi([b2]);
}
}"
`;
@@ -319,18 +432,18 @@ exports[`Portal lifecycle hooks of portal sub component are properly called 1`]
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><block-child-0/><block-child-0/></div>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return component(\`Child\`, {val: ctx['state'].val}, key + \`__1\`, node, ctx);
}
return function template(ctx, node, key = \\"\\") {
let b3;
if (ctx['state'].hasChild) {
b3 = callPortal(ctx, node, key, '#outside', portalContent1);
b3 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
}
return block1([], [b3]);
}
@@ -355,12 +468,12 @@ exports[`Portal portal could have dynamically no content 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block3 = createBlock(\`<span><block-text-0/></span>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
let b3;
if (ctx['state'].val) {
let txt1 = ctx['state'].val;
@@ -370,7 +483,7 @@ exports[`Portal portal could have dynamically no content 1`] = `
}
return function template(ctx, node, key = \\"\\") {
let b4 = callPortal(ctx, node, key, '#outside', portalContent1);
let b4 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return block1([], [b4]);
}
}"
@@ -380,16 +493,16 @@ exports[`Portal portal destroys on crash 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return component(\`Child\`, {error: ctx['state'].error}, key + \`__1\`, node, ctx);
}
return function template(ctx, node, key = \\"\\") {
let b3 = callPortal(ctx, node, key, '#outside', portalContent1);
let b3 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b3]);
}
}"
@@ -413,16 +526,16 @@ exports[`Portal portal with child and props 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return component(\`Child\`, {val: ctx['state'].val}, key + \`__1\`, node, ctx);
}
return function template(ctx, node, key = \\"\\") {
let b3 = callPortal(ctx, node, key, '#outside', portalContent1);
let b3 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b3]);
}
}"
@@ -446,13 +559,13 @@ exports[`Portal portal with dynamic body 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block3 = createBlock(\`<span><block-text-0/></span>\`);
let block4 = createBlock(\`<div/>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
let b3,b4;
if (ctx['state'].val) {
let txt1 = ctx['state'].val;
@@ -464,7 +577,7 @@ exports[`Portal portal with dynamic body 1`] = `
}
return function template(ctx, node, key = \\"\\") {
let b5 = callPortal(ctx, node, key, '#outside', portalContent1);
let b5 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return block1([], [b5]);
}
}"
@@ -474,20 +587,20 @@ exports[`Portal portal with many children 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block3 = createBlock(\`<div>1</div>\`);
let block4 = createBlock(\`<p>2</p>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
let b3 = block3();
let b4 = block4();
return multi([b3, b4]);
}
return function template(ctx, node, key = \\"\\") {
let b5 = callPortal(ctx, node, key, '#outside', portalContent1);
let b5 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return block1([], [b5]);
}
}"
@@ -497,11 +610,11 @@ exports[`Portal portal with no content 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
let b3;
if (false) {
b3 = text('ABC');
@@ -510,7 +623,7 @@ exports[`Portal portal with no content 1`] = `
}
return function template(ctx, node, key = \\"\\") {
let b4 = callPortal(ctx, node, key, '#outside', portalContent1);
let b4 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return block1([], [b4]);
}
}"
@@ -520,16 +633,16 @@ exports[`Portal portal with only text as content 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return text('only text');
}
return function template(ctx, node, key = \\"\\") {
let b3 = callPortal(ctx, node, key, '#outside', portalContent1);
let b3 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return block1([], [b3]);
}
}"
@@ -539,17 +652,17 @@ exports[`Portal portal with target not in dom 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<div>2</div>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return block2();
}
return function template(ctx, node, key = \\"\\") {
let b3 = callPortal(ctx, node, key, '#does-not-exist', portalContent1);
let b3 = component(Portal, {target: '#does-not-exist',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return block1([], [b3]);
}
}"
@@ -559,16 +672,16 @@ exports[`Portal portal's parent's env is not polluted 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
return function template(ctx, node, key = \\"\\") {
let b3 = callPortal(ctx, node, key, '#outside', portalContent1);
let b3 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b3]);
}
}"
@@ -610,18 +723,18 @@ exports[`Portal simple catchError with portal 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><span>1</span><block-child-0/></div>\`);
let block2 = createBlock(\`<p><block-text-0/></p>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
let txt1 = ctx['a'].b.c;
return block2([txt1]);
}
return function template(ctx, node, key = \\"\\") {
let b3 = callPortal(ctx, node, key, '#outside', portalContent1);
let b3 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return block1([], [b3]);
}
}"
@@ -631,17 +744,17 @@ exports[`Portal with target in template (after portal) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><span>1</span><block-child-0/><div id=\\"local-target\\"/></div>\`);
let block2 = createBlock(\`<p>2</p>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return block2();
}
return function template(ctx, node, key = \\"\\") {
let b3 = callPortal(ctx, node, key, '#local-target', portalContent1);
let b3 = component(Portal, {target: '#local-target',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return block1([], [b3]);
}
}"
@@ -651,17 +764,17 @@ exports[`Portal with target in template (before portal) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><div id=\\"local-target\\"/><span>1</span><block-child-0/></div>\`);
let block2 = createBlock(\`<p>2</p>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return block2();
}
return function template(ctx, node, key = \\"\\") {
let b3 = callPortal(ctx, node, key, '#local-target', portalContent1);
let b3 = component(Portal, {target: '#local-target',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return block1([], [b3]);
}
}"
@@ -671,17 +784,17 @@ exports[`Portal: Props validation target must be a valid selector 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<div>2</div>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return block2();
}
return function template(ctx, node, key = \\"\\") {
let b3 = callPortal(ctx, node, key, ' ', portalContent1);
let b3 = component(Portal, {target: ' ',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return block1([], [b3]);
}
}"
@@ -691,17 +804,17 @@ exports[`Portal: Props validation target must be a valid selector 2 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<div>2</div>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return block2();
}
return function template(ctx, node, key = \\"\\") {
let b3 = callPortal(ctx, node, key, 'aa', portalContent1);
let b3 = component(Portal, {target: 'aa',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return block1([], [b3]);
}
}"
@@ -711,16 +824,16 @@ exports[`Portal: UI/UX focus is kept across re-renders 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callPortal } = helpers;
let { Portal } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
function portalContent1(ctx, node, key = \\"\\") {
function slot1(ctx, node, key = \\"\\") {
return component(\`Child\`, {val: ctx['state'].val}, key + \`__1\`, node, ctx);
}
return function template(ctx, node, key = \\"\\") {
let b3 = callPortal(ctx, node, key, '#outside', portalContent1);
let b3 = component(Portal, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b3]);
}
}"
+125 -3
View File
@@ -1,4 +1,5 @@
import {
App,
Component,
mount,
onError,
@@ -66,7 +67,24 @@ describe("Portal", () => {
expect(fixture.innerHTML).toBe('<div id="outside"><p>2</p></div><div><span>1</span></div>');
});
test("basic use of portal on div", async () => {
class Parent extends Component {
static template = xml`
<div>
<span>1</span>
<div t-portal="'#outside'">
<p>2</p>
</div>
</div>`;
}
addOutsideDiv(fixture);
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(
'<div id="outside"><div><p>2</p></div></div><div><span>1</span></div>'
);
});
test("simple catchError with portal", async () => {
class Boom extends Component {
static template = xml`
@@ -565,6 +583,39 @@ describe("Portal", () => {
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
});
test("Add and remove portals on div", async () => {
class Parent extends Component {
static template = xml`
<div t-portal="'#outside'" t-foreach="portalIds" t-as="portalId" t-key="portalId">
Portal<t t-esc="portalId"/>
</div>`;
portalIds = useState([] as any);
}
addOutsideDiv(fixture);
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
parent.portalIds.push(1);
await nextTick();
expect(fixture.innerHTML).toBe('<div id="outside"><div> Portal1</div></div>');
parent.portalIds.push(2);
await nextTick();
expect(fixture.innerHTML).toBe(
'<div id="outside"><div> Portal1</div><div> Portal2</div></div>'
);
parent.portalIds.pop();
await nextTick();
expect(fixture.innerHTML).toBe('<div id="outside"><div> Portal1</div></div>');
parent.portalIds.pop();
await nextTick();
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
});
test("Add and remove portals with t-foreach", async () => {
class Parent extends Component {
static template = xml`
@@ -603,6 +654,41 @@ describe("Portal", () => {
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
});
test("Add and remove portals with t-foreach and destroy", async () => {
class Parent extends Component {
static template = xml`
<t t-foreach="portalIds" t-as="portalId" t-key="portalId">
<div>
<t t-esc="portalId"/>
<t t-portal="'#outside'">
Portal<t t-esc="portalId"/>
</t>
</div>
</t>`;
portalIds = useState([] as any);
}
addOutsideDiv(fixture);
const app = new App(Parent);
const parent = await app.mount(fixture);
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
parent.portalIds.push(1);
await nextTick();
expect(fixture.innerHTML).toBe('<div id="outside"> Portal1</div><div>1</div>');
parent.portalIds.push(2);
await nextTick();
expect(fixture.innerHTML).toBe(
'<div id="outside"> Portal1 Portal2</div><div>1</div><div>2</div>'
);
app.destroy();
//This will test explicitly that we don't use an await nextTick(); after the destroy.
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
});
test("conditional use of Portal with div", async () => {
class Parent extends Component {
static template = xml`
@@ -650,7 +736,6 @@ describe("Portal", () => {
</t>
</t>
</div>`;
}
class Parent extends Component {
static template = xml`
@@ -684,7 +769,7 @@ describe("Portal", () => {
);
});
test.only("conditional use of Portal with child and div, variation", async () => {
test("conditional use of Portal with child and div, variation", async () => {
class Child extends Component {
static template = xml`
<span>hasPortal</span>
@@ -693,7 +778,6 @@ describe("Portal", () => {
<p>thePortal</p>
</t>
</t>`;
}
class Parent extends Component {
static template = xml`
@@ -728,7 +812,45 @@ describe("Portal", () => {
'<div id="outside"><p>thePortal</p></div><div><span>hasPortal</span></div>'
);
});
test("Add and remove portals with t-foreach inside div", async () => {
class Parent extends Component {
static template = xml`
<div>
<t t-foreach="portalIds" t-as="portalId" t-key="portalId">
<div>
<t t-esc="portalId"/>
<t t-portal="'#outside'">
Portal<t t-esc="portalId"/>
</t>
</div>
</t>
</div>`;
portalIds = useState([] as any);
}
addOutsideDiv(fixture);
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('<div id="outside"></div><div></div>');
parent.portalIds.push(1);
await nextTick();
expect(fixture.innerHTML).toBe('<div id="outside"> Portal1</div><div><div>1</div></div>');
parent.portalIds.push(2);
await nextTick();
expect(fixture.innerHTML).toBe(
'<div id="outside"> Portal1 Portal2</div><div><div>1</div><div>2</div></div>'
);
parent.portalIds.pop();
await nextTick();
expect(fixture.innerHTML).toBe('<div id="outside"> Portal1</div><div><div>1</div></div>');
parent.portalIds.pop();
await nextTick();
expect(fixture.innerHTML).toBe('<div id="outside"></div><div></div>');
});
});
describe("Portal: UI/UX", () => {
+3 -2
View File
@@ -7,7 +7,8 @@ import {
useState,
xml,
} from "../src";
import { batched, reactive } from "../src/reactivity";
import { reactive } from "../src/reactivity";
import { batched } from "../src/utils";
import {
makeDeferred,
makeTestFixture,
@@ -1611,7 +1612,7 @@ describe("Reactivity: useState", () => {
expect([...steps]).toEqual(["list"]);
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>3</div> Total: 3 Count: 1</div>");
expect([...steps]).toEqual(["list", "quantity1"]);
expect([...steps]).toEqual(["list"]);
steps.clear();
secondQuantity.quantity = 2;
+2 -1
View File
@@ -60,6 +60,7 @@ function getFiles(path: string[] = []): FileData[] {
if (path.length === 0) {
const baseFiles: FileData[] = [
{ name: "README.md", path: [], links: [], sections: [], fullName: "README.md" },
{ name: "CHANGELOG.md", path: [], links: [], sections: [], fullName: "CHANGELOG.md" },
{ name: "roadmap.md", path: [], links: [], sections: [], fullName: "roadmap.md" },
];
const rest = getFiles(["doc"]);
@@ -85,7 +86,7 @@ function getFiles(path: string[] = []): FileData[] {
return Array.prototype.concat(...files);
}
const LOCAL_FILES = ["LICENSE", "CHANGELOG.md"];
const LOCAL_FILES = ["LICENSE"];
export function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean {
if (link.link.startsWith("http")) {
// no check on external links
+19 -1
View File
@@ -1,4 +1,5 @@
import { EventBus } from "../src/utils";
import { batched, EventBus } from "../src/utils";
import { nextMicroTick } from "./helpers";
describe("event bus behaviour", () => {
test("can subscribe and be notified", () => {
@@ -33,3 +34,20 @@ describe("event bus behaviour", () => {
bus.trigger("event", "hello world");
});
});
describe("batched", () => {
test("callback is called only once after operations", async () => {
let n = 0;
let fn = batched(() => n++);
expect(n).toBe(0);
fn();
fn();
expect(n).toBe(0);
await nextMicroTick();
expect(n).toBe(1);
await nextMicroTick();
expect(n).toBe(1);
});
});
+2 -11
View File
@@ -39,18 +39,9 @@ function makeCodeIframe(js, css, xml) {
const script = doc.createElement("script");
script.type = "module";
const content = `
import * as utils from "./utils.js";
(function (owl) {
const _configure = owl.App.prototype.configure;
owl.App.prototype.configure = function configureOverriden(config) {
config = Object.assign({ dev: true }, config);
this.addTemplates(\`${sanitizedXML}\`);
return _configure.call(this, config);
}
})(owl);
(async function() {
(async function(TEMPLATES) {
${js}
})()`;
})(\`${sanitizedXML}\`)`;
script.innerHTML = content;
doc.body.appendChild(script);
});
File diff suppressed because it is too large Load Diff