mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
276 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1fc513249 | |||
| 73c37c025c | |||
| b88f7a03e9 | |||
| 055cbda750 | |||
| 45f6a8e97a | |||
| 6091912c54 | |||
| 586f90f6d1 | |||
| e62a0ca2d9 | |||
| 3a4ee160ed | |||
| 5f24caf40d | |||
| 8e36a11b6e | |||
| 9a54afcdf5 | |||
| 3eefa4b153 | |||
| 2ffb94ed98 | |||
| 6a15874d23 | |||
| 07d34248ae | |||
| dfc090bb12 | |||
| 7b3593ff8f | |||
| b06791c950 | |||
| f7843c7c00 | |||
| ca9d4ea762 | |||
| 7a407a762d | |||
| 8847824fb7 | |||
| 55b068faeb | |||
| dbee8f50d1 | |||
| f5fec02d4c | |||
| 651c53ebbf | |||
| 3119497508 | |||
| 057e6944d6 | |||
| 83d38a6f48 | |||
| 9577d70b4b | |||
| 6ff6d59895 | |||
| 7ba0c9f8ba | |||
| 79a51f2317 | |||
| 483335df6c | |||
| 287a6d5ac4 | |||
| 68684a9a45 | |||
| e5d773daa8 | |||
| 00ffab1f64 | |||
| a226f92def | |||
| 1749bce155 | |||
| c00127fa1e | |||
| 0b4b7899f6 | |||
| 5c324fa075 | |||
| 3945c009ed | |||
| 5b1132f01a | |||
| f014b1a33e | |||
| 3f09a953b0 | |||
| b8e7ee5ff7 | |||
| 7fdca35a74 | |||
| 489e20843c | |||
| 34b781aeed | |||
| e8b0f31da6 | |||
| d160c4a628 | |||
| 4542171a31 | |||
| 0b1c4dd4ef | |||
| f4da50d350 | |||
| f2921abda8 | |||
| 107200fd94 | |||
| 0f8c859d5f | |||
| cca8438d38 | |||
| a727347d60 | |||
| f19de73b0f | |||
| a8ffcafd23 | |||
| a2f01ef4ad | |||
| d7de25e867 | |||
| 424bc480e2 | |||
| 421f400766 | |||
| c37f3445f6 | |||
| 4cde06d685 | |||
| 7b95315b87 | |||
| 09761ddd8e | |||
| b3c6ec2b48 | |||
| b20462ada6 | |||
| 25000b36be | |||
| 144d4a253c | |||
| 9d48bda227 | |||
| c8db663869 | |||
| 247200194f | |||
| 60b8817ac0 | |||
| e008966291 | |||
| e2fedc6eff | |||
| b3fb9a35bf | |||
| 240259568e | |||
| 18fb67eaad | |||
| e946967867 | |||
| 777b2aae5e | |||
| 0831bb54e3 | |||
| 03f34a38dc | |||
| dcefd26bee | |||
| 922dab8e8f | |||
| 99740f9993 | |||
| 0b79b1c8fd | |||
| 5b0dce94cf | |||
| a1d435c5a5 | |||
| 13c3178760 | |||
| 2b565ce1b4 | |||
| cfcf2c6714 | |||
| 92174559a1 | |||
| 840348892f | |||
| b988a68b6f | |||
| 7869a1f2c6 | |||
| 9e81d14d50 | |||
| 0e4a55ba09 | |||
| 6e9b68dafa | |||
| ba7c9063c0 | |||
| 3a361876ad | |||
| 58b8572f0a | |||
| c23637e6d8 | |||
| 4a4b1fbba5 | |||
| 536d9e1762 | |||
| 6ed68372a6 | |||
| efd934d2b1 | |||
| dabc24cee3 | |||
| 7f49796a07 | |||
| 757dffefac | |||
| bdfe058279 | |||
| 345c44b952 | |||
| ea74739d46 | |||
| 93b53d8017 | |||
| c2284bc6f5 | |||
| 48650da62e | |||
| 2edd6cb8f3 | |||
| 4b0a37c542 | |||
| 50c0e30936 | |||
| e97335d7ab | |||
| 6ea40a66da | |||
| 2806cda420 | |||
| cb0b525f32 | |||
| 30d28670e8 | |||
| 7b761a2f8b | |||
| d834bb2ac4 | |||
| 6ae92fa4b3 | |||
| 3ceb118ace | |||
| fa426b7fc0 | |||
| d7403871fc | |||
| 6aeff21d9a | |||
| 429e145b68 | |||
| fb6aab8b70 | |||
| 9e285e1e0a | |||
| 63bfdfb8db | |||
| d4c4fe853e | |||
| 9163bb1e08 | |||
| a187a376a0 | |||
| 06ea6d2490 | |||
| 04334e23d7 | |||
| 4efe1ae166 | |||
| ad4673cb36 | |||
| e611c20ab4 | |||
| d60a5a414e | |||
| d47dcf6be2 | |||
| 3e70b17da1 | |||
| c718d8e6c6 | |||
| a122a94180 | |||
| ca139166ab | |||
| ae71db28e4 | |||
| 779fc6b02b | |||
| 84913593b2 | |||
| 8d4eb21536 | |||
| 4bdfaf96f3 | |||
| 233c953243 | |||
| ee5ad354ff | |||
| 4e4b85e2a9 | |||
| 6479631983 | |||
| 2a0c410014 | |||
| bb4aecf638 | |||
| a1adfd5a1b | |||
| dd4848f602 | |||
| b25f476c22 | |||
| 90cdb97b49 | |||
| 7d7568d254 | |||
| 06d207f60e | |||
| ff564421da | |||
| c3695ec8db | |||
| 6e0834ce5e | |||
| d1425c7100 | |||
| e01fe301c0 | |||
| ba2fe3ff55 | |||
| 700d574314 | |||
| ba32772a92 | |||
| e4a0277f68 | |||
| abe3748825 | |||
| ee64de8b3f | |||
| ed831320db | |||
| 5f1e1e189f | |||
| 55ef9ca116 | |||
| 642ecf0ccd | |||
| c9cc789f1d | |||
| b8649f1add | |||
| e2d98e4c26 | |||
| e6bb4ef286 | |||
| 7ac20f4fc2 | |||
| 4e3b7c74da | |||
| 98bb2842d8 | |||
| 6e185f987b | |||
| c0f495661d | |||
| 0f7a8289a6 | |||
| 1c3b04f6a8 | |||
| 64db7777dd | |||
| 37313c47a3 | |||
| 3f563de9c0 | |||
| 97564a7612 | |||
| f1abf7f2ea | |||
| 8464a1b04e | |||
| c0a62dfd05 | |||
| 600f1e35d4 | |||
| b3181f119d | |||
| 1e1e05350a | |||
| b242230e20 | |||
| 9fe2da704e | |||
| 21b1661d39 | |||
| ec05b1f5e3 | |||
| 8083678f03 | |||
| 61c2ec5d83 | |||
| e579a993fd | |||
| ea3c6f7bf0 | |||
| 9cfafc30b5 | |||
| ad42c583c6 | |||
| 6faaa6c361 | |||
| bff539cabf | |||
| 88fd1cf483 | |||
| f0b75b6890 | |||
| a1552117f9 | |||
| 17ae1d06c4 | |||
| 27629cedfa | |||
| 3a93370ab6 | |||
| a6bb4d8ee1 | |||
| caf842c482 | |||
| 1b513a1637 | |||
| 52a200878a | |||
| f4994a20d8 | |||
| 7419c1982a | |||
| a8c8f6affe | |||
| dabe7f9bfa | |||
| d08ea63565 | |||
| 7ebe0da962 | |||
| 3e0e8475a6 | |||
| 2e83c739b9 | |||
| 922eb7cd98 | |||
| d12115554c | |||
| 1a6b26c8c6 | |||
| d67d295eaa | |||
| 9cbcf20b33 | |||
| 3a461e1dd1 | |||
| e8387810e6 | |||
| acac9d1741 | |||
| 968a5460bb | |||
| e646eb697e | |||
| 7dfa901332 | |||
| 0290f63ba3 | |||
| 370fae4e1a | |||
| 76735410f8 | |||
| 19a47a7001 | |||
| 398f9f4e53 | |||
| af426aa902 | |||
| 490cf18079 | |||
| dfc782599b | |||
| 71f545058b | |||
| 1a20cc57de | |||
| 25738a1bf0 | |||
| 4a96eff3c6 | |||
| d043d47754 | |||
| 3a10468f7b | |||
| 144b323d2b | |||
| bb64e87634 | |||
| 9a87b9a4a0 | |||
| 2a53a9592e | |||
| f54b9a4a0c | |||
| abb9d0b364 | |||
| 8483cc805e | |||
| 142f47ac82 | |||
| c8a27aa1a3 | |||
| 323cb61d2c | |||
| ac9cc91701 | |||
| d83cfc12ee | |||
| cb07c99d40 |
@@ -24,4 +24,5 @@ jobs:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm run test
|
||||
- run: npm run prettier
|
||||
- run: npm run check-formatting
|
||||
- run: npm run build
|
||||
|
||||
@@ -30,4 +30,5 @@ release-notes.md
|
||||
|
||||
.rpt2_cache
|
||||
|
||||
# useful in some cases
|
||||
/temp
|
||||
+765
@@ -0,0 +1,765 @@
|
||||
# Changelog
|
||||
|
||||
This document contains an overview of all changes between Owl 1.x and
|
||||
Owl 2.x, with some pointers on how to update the code.
|
||||
|
||||
Note that some of these changes can be magically implemented (for example, by
|
||||
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.
|
||||
|
||||
## 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))
|
||||
- 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))
|
||||
- breaking: components can no longer be instantiated and mounted by hand ([details](#5-components-can-no-longer-be-instantiated-and-mounted-by-hand))
|
||||
- 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: 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))
|
||||
|
||||
|
||||
**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**
|
||||
|
||||
- improved performance
|
||||
- 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) ([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: `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))
|
||||
- breaking: transition system is removed ([details](#24-transition-system-is-removed))
|
||||
- breaking: no more global components or templates ([details](#25-no-more-global-components-or-templates))
|
||||
- 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: `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
|
||||
|
||||
All changes are listed in no particular order.
|
||||
|
||||
### 1. component lifecycle methods are removed
|
||||
|
||||
There was two ways to define hooks: the component methods (`willStart`, `mounted`, ...) and the hooks (`onWillStart`, `onMounted`, ...). In Owl 2, the component methods have been removed.
|
||||
|
||||
Rationale: it makes the implementation simpler and slightly faster. Hooks are more composable
|
||||
than component methods. It enforces a single entry point to check all the useful lifecycle
|
||||
calls (instead of it being scattered in the component definition). It feels more "modern".
|
||||
|
||||
Migration: lifecycle methods should be defined in the `setup`:
|
||||
|
||||
```js
|
||||
class MyComponent extends Component {
|
||||
mounted() {
|
||||
// do something
|
||||
}
|
||||
}
|
||||
```
|
||||
should become:
|
||||
```js
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
// do something
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Documentation: [Component Lifecycle](doc/reference/component.md#lifecycle)
|
||||
|
||||
### 2. components can no longer be mounted in a detached dom element
|
||||
|
||||
Nor document fragment.
|
||||
|
||||
Rationale: it is actually very difficult to do it: this implies that a component
|
||||
can be mounted more than once, that we need to check every time different status,
|
||||
that some elements is in the dom, and was a cause for bugs. Also, we don't use it
|
||||
in practice. Removing this means that we have a much simpler mental model of what
|
||||
happens.
|
||||
|
||||
Migration: well, not really easy. The code needs to be refactored in a different way.
|
||||
|
||||
|
||||
### 3. **`t-set` will no longer work to define a slot**
|
||||
|
||||
The `t-set` directive cannot define a slot anymore. Only the `t-set-slot` directive
|
||||
can do it.
|
||||
|
||||
Rationale: it was left for compatibility reason, but was deprecated anyway.
|
||||
|
||||
Migration: `t-set` should be changed to `t-set-slot` (when defining a slot)
|
||||
|
||||
Example:
|
||||
|
||||
```xml
|
||||
<SideBar><t t-set="content">content</t></SideBar>
|
||||
```
|
||||
should become:
|
||||
```xml
|
||||
<SideBar><t t-set-slot="content">content</t></SideBar>
|
||||
```
|
||||
|
||||
### 4. `mount` method API is simpler
|
||||
|
||||
Before, the `mount` method was used like this:
|
||||
|
||||
```js
|
||||
await mount(Root, { target: document.body });
|
||||
```
|
||||
|
||||
It is now simpler and takes the root component and a target argument:
|
||||
|
||||
```js
|
||||
await mount(Root, document.body);
|
||||
```
|
||||
|
||||
Rationale: the `mount` method is only useful anyway for small toy examples,
|
||||
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:
|
||||
|
||||
```js
|
||||
const root = new Root();
|
||||
await root.mount(document.body);
|
||||
```
|
||||
|
||||
Now, it is no longer possible. All component instantiations should be done by
|
||||
the owl framework itself.
|
||||
|
||||
Rationale: the `mount` method does not make sense for all non root components.
|
||||
Also, the fact that it was possible for a component to be sometimes root,
|
||||
sometimes a child made for a weird constructor signature. This changes makes it
|
||||
simpler.
|
||||
|
||||
Migration: all code doing that should use either the `mount` method (if the use
|
||||
case is simple enough, or the `App` class):
|
||||
|
||||
```js
|
||||
const app = new App(Root);
|
||||
app.configure({ templates: ..., ...});
|
||||
await app.mount(document.body);
|
||||
```
|
||||
|
||||
### 6. components can no longer be unmounted/remounted
|
||||
|
||||
Rationale: this is a very difficult feature to implement (it adds a lot of possible
|
||||
state transitions), compared to its benefit.
|
||||
|
||||
Migration: all code using it should find a way to export and reimport the state
|
||||
|
||||
### 7. template name is no longer inferred from the class name
|
||||
|
||||
Before, it was possible to define a component without specifying its template:
|
||||
|
||||
```js
|
||||
class Blabla extends Component {
|
||||
// no static template here!
|
||||
}
|
||||
```
|
||||
with the `Blabla` template. It also worked with subclasses. But then, this means
|
||||
that the code had to look up all the super classes names to find the correct
|
||||
template.
|
||||
|
||||
Rationale: in practice, it is not really useful, since all templates are usually
|
||||
namespaced: `web.SomeComponent` anyway. All the trouble to do that was just not
|
||||
worth it.
|
||||
|
||||
Migration: simply explicitely defines the template key everytime:
|
||||
|
||||
```js
|
||||
class Blabla extends Component {}
|
||||
Blabla.template = "Blabla";
|
||||
```
|
||||
|
||||
### 8. components no longer have a `shouldUpdate` method
|
||||
|
||||
Rationale: `shouldUpdate` is a dangerous method to use, that may cause a lot of
|
||||
issues. Vue does not have such a mechanism (see https://github.com/vuejs/vue/issues/4255),
|
||||
because the reactivity system in Vue is smart enough to only rerender the minimal
|
||||
subset of components that is subscribed to a piece of state. Now, Owl 2 features
|
||||
a much more powerful reactivity system.
|
||||
|
||||
Migration code: remove the `shouldUpdate` methods. Then, maybe the following
|
||||
ideas may help:
|
||||
|
||||
- try to organize the state/architecture to minimize the number of state updates
|
||||
- take advantage of the finer reactivity system. For example, if we have a list
|
||||
of items, with a component for each item, we can write this:
|
||||
|
||||
```js
|
||||
class Item extends Component {
|
||||
setup() {
|
||||
this.item = useState(this.props.item); // and only use this, not props.item
|
||||
}
|
||||
}
|
||||
```
|
||||
Doing so will make it that each `Item` component will register itself as an
|
||||
observer of its own item, and will be the only component being rerendered when
|
||||
its item object is updated.
|
||||
- use the `Memo` component to wrap some piece of template. `Memo` memoize its
|
||||
content, and only update itself if its props are different (shallow comparison):
|
||||
|
||||
```xml
|
||||
<Memo a="state.a" b="state.b">
|
||||
<t t-esc="state.a"/>
|
||||
<t t-esc="state.b"/>
|
||||
<t t-esc="state.c"/>
|
||||
</Memo>
|
||||
```
|
||||
|
||||
### 9. component.el is removed
|
||||
|
||||
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
|
||||
|
||||
Before, it was possible to do this in a template:
|
||||
|
||||
```xml
|
||||
<Child style="..." class="..."/>
|
||||
```
|
||||
(or with `t-att-style` and `t-att-class`). This does no longer work, as they are
|
||||
now considered normal props.
|
||||
|
||||
Rationale: with the move to fragments, the semantics of where the style/class
|
||||
attribute should be set is unclear. Also, it is actually very hard to implement
|
||||
properly, in particular with higher order components. And another issue is that
|
||||
it (slightly) breaks the encapsulation of behaviour from the `Child` component
|
||||
perspective.
|
||||
|
||||
Migration: each component that wishes to be customized should explicitely add
|
||||
the `class` and `style` attributes in its template. Also, the parent component
|
||||
should be aware that since we are talking about props, it should be a javascript expression:
|
||||
|
||||
In parent:
|
||||
|
||||
```xml
|
||||
<Child class="'o_my_god'"/>
|
||||
```
|
||||
and in child:
|
||||
|
||||
```xml
|
||||
<div t-att-class="props.class">
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
### 11. components can no longer be mounted with position=self
|
||||
|
||||
Rationale: this is due to the implementation of owl 2 virtual dom. The hack
|
||||
necessary to support position=self does not work. This position also is not
|
||||
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`. 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
|
||||
|
||||
In owl 1, it was possible to bind an event listener on a component tag in a
|
||||
template:
|
||||
|
||||
```xml
|
||||
<SomeComponent t-on-some-event="doSomething"/>
|
||||
```
|
||||
|
||||
This does not work any more.
|
||||
|
||||
Rationale: with the support of fragments, there is no longer a canonical html
|
||||
element that we can refer. So, this makes it difficult to implement correctly
|
||||
and efficiently. Also, we noticed in practice that the event system was an issue
|
||||
in some cases, when components need to communicate before they are mounted. In
|
||||
those cases, the better solution is to directly use a callback. Also, another
|
||||
conceptual issue with this is that it kind of breaks the component encapsulation.
|
||||
The child component kind of leak its own implementation to the outside world.
|
||||
|
||||
Migration: a quick fix that may work in some cases is to simply bind the event
|
||||
handler on a parent htmlelement. A better way to do it, if possible, is to change
|
||||
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
|
||||
child, and redispatch them on itself. It no longer works.
|
||||
|
||||
Rationale: Portal now supports an arbitrary content (so, more than one child,
|
||||
and potentially no html element), so it is already unclear what it should listen
|
||||
to. Also, redispatching events was an hack. And this changes allows the portal
|
||||
to render itself as a text node, which is nice. This is also in line with the
|
||||
fact that modern Owl moves toward using callback instead of `t-on` for communication.
|
||||
|
||||
Migration: use callback if possible to communicate. Otherwise, use a sub env.
|
||||
|
||||
### 14. Portal does render as an empty text node instead of `<portal/>`
|
||||
|
||||
That is pretty nice. No real migration needed.
|
||||
|
||||
### 15. Context is removed
|
||||
|
||||
Context was an abstraction in Owl that was used to define some reactive state
|
||||
and to let some components subscribe to it, then only them would be rerendered
|
||||
if the context was updated. This has been removed.
|
||||
|
||||
Rationale: first, the Context api and code was kind of awkward, which is a sign
|
||||
that the abstraction is not well thought. But the good news is that it is actually
|
||||
completely replaced by the new reactivity system, which is even more powerful,
|
||||
since it can tracks changes key by key.
|
||||
|
||||
Migration: replace all uses of Context with the new reactivity system.
|
||||
|
||||
```js
|
||||
// somewhere, maybe in a service, or in the global env
|
||||
const context = observe({some: "state"})
|
||||
|
||||
// in a component that would previously get a reference to the context:
|
||||
|
||||
setup() {
|
||||
this.context = useState(context);
|
||||
// now the component is subscribed to the context and will react to any
|
||||
// change for any key read by the component, and only those changes
|
||||
}
|
||||
```
|
||||
|
||||
### 16. `env` is now totally empty
|
||||
|
||||
In Owl 1, the `env` object had to contain a QWeb instance. This was the way
|
||||
components would get a reference to their template function. It no longer works
|
||||
that way: the `env` object is now totally empty (from the perspective of Owl).
|
||||
It is now a user space concept, useful for the application.
|
||||
|
||||
Rationale: first, there is no longer a QWeb class. Also, this changes simplifies
|
||||
the way components works internally.
|
||||
|
||||
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:
|
||||
|
||||
```xml
|
||||
<t t-component="Coucou"/>
|
||||
```
|
||||
|
||||
This meant that Owl would look for the component class like this: `components["Coucou"]`,
|
||||
so, essentially equivalent to `<Coucou/>`. In Owl 2, the `t-component` directive
|
||||
is assumed to be an expression evaluating to a component class:
|
||||
|
||||
```js
|
||||
class Parent extends Component {
|
||||
static template = xml`<t t-component="Child"/>`;
|
||||
Child = Child;
|
||||
}
|
||||
```
|
||||
|
||||
Rationale: it simply seems more consistent with the way directive works. Also,
|
||||
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`.
|
||||
|
||||
Rationale: this makes it easier to work with, instead of importing stuff from
|
||||
`owl`, then `owl.hooks` and `owl.tags` for example.
|
||||
|
||||
Migration: all import code simply need to be slightly adapted.
|
||||
|
||||
### 19. Properties are no longer set as attributes
|
||||
|
||||
Formerly, html properties `<input type="checkbox" t-att-checked="blah"/>` were
|
||||
set as property and as attribute, so, they would be visible in the DOM:
|
||||
`<input type="checkbox" checked="blah"/>`. Now, they are treated as property only:
|
||||
`<input type="checkbox"/>`.
|
||||
|
||||
Rationale: this is actually simple to do, is faster, and makes more sense to me.
|
||||
|
||||
### 20. `t-foreach` should always have a corresponding `t-key`
|
||||
|
||||
It was possible in Owl 1 to write a `t-foreach` without a `t-key`. In that case,
|
||||
the index was used as key. Since it was clearly a possible bug, Owl 1 had a
|
||||
warning in some cases, when it could detect that there was definitely not a `t-key`.
|
||||
However, this was imperfect, and in some cases no warning was displayed. In Owl 2,
|
||||
the tag with a `t-foreach` has to have a corresponding `t-key`.
|
||||
|
||||
Rationale: this makes it easier to avoid bugs.
|
||||
|
||||
Migration: simply move the `t-key` to the tag with the `t-foreach`. If this is
|
||||
a situation where there is really not a need for a `t-key`, you can still add
|
||||
it with the `_index` suffix:
|
||||
|
||||
```xml
|
||||
<div t-foreach="items" t-as="item" t-key="item_index">
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
### 21. `EventBus` api changed: it is now an `EventTarget`
|
||||
|
||||
In Owl 1, the `EventBus` class was done manually, with a custom API. In Owl 2,
|
||||
it simply extends `EventTarget` (the native Dom class), so its implementation
|
||||
is basically only 5 lines long. This means that it has now the usual DOM interface:
|
||||
|
||||
```js
|
||||
bus.addEventListener('event-name', callback);
|
||||
```
|
||||
|
||||
Rationale: it makes it easier to have just one interface to remember, it makes
|
||||
the code simpler
|
||||
|
||||
Migration: most bus methods need to be adapted. So, `bus.on("event-type", owner, (info) => {...})` has to be
|
||||
rewritten like this: `bus.addEventListener("event-type", (({detail: info}) => {...}).bind(owner))`.
|
||||
|
||||
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.
|
||||
|
||||
Rationale: first, it was complicated to maintain. Second, it was not really
|
||||
used in Odoo. Finally, the new reactivity system seems to be a pretty good basis
|
||||
to write a store, and it should not take much work. Also, this can be done in
|
||||
user space (so, not necessarily at the framework level). Another point is that
|
||||
the store API was invented before the hooks, then was still a little awkward.
|
||||
|
||||
Migration:
|
||||
- rewrite the code not to use a store
|
||||
- probably use the reactivity system instead and build a store class and a few
|
||||
hooks on top of it.
|
||||
|
||||
### 23. `Router` is removed
|
||||
|
||||
Rationale: Router was not used that much, and it felt like it did not fit in Owl 2.
|
||||
Its API needs to be reworked, and we are not confident that it is a good
|
||||
experience to use it. Also, it can be done in userspace (it does not need specific
|
||||
integration at the framework level)
|
||||
|
||||
Migration: reimport all missing piece from the code in Owl 1.
|
||||
|
||||
### 24. transition system is removed
|
||||
|
||||
Rationale: this was a high ratio cost/value, with a lot of potential for bugs.
|
||||
We feel like there should be a way to reimplement in userspace the simple cases.
|
||||
|
||||
Maybe something like: add a `t-ref` in the template, and define a hook `useFadeOut`
|
||||
that takes the ref, and add a fadeout class at initial render, then in mounted,
|
||||
wait for a micro tick and remove it.
|
||||
|
||||
Migration: try to reimplement it manually.
|
||||
|
||||
### 25. no more global components or templates
|
||||
|
||||
It was possible in Owl 1 to register globally a component or a template. This is
|
||||
no longer the case in Owl 2.
|
||||
|
||||
Rationale: first, this was a tradeoff: ease of use was gained, but at the cost
|
||||
of a higher complexity. Users had to know that there was a magic mechanism. Also,
|
||||
it was not used much in practice, and the cost of having to import manually components
|
||||
is low. Finally, this can be mostly done in user space (for example, by subclassing
|
||||
`Component`).
|
||||
|
||||
Migration: import manually all required global components, or find a way to organize
|
||||
the code to do it.
|
||||
|
||||
### 26. `AsyncRoot` utility component is removed
|
||||
|
||||
Rationale: it was difficult to understand, never used, and not really useful.
|
||||
It seems better to control the asynchrony of an application by simply controlling
|
||||
how/when the state is updated, and how each component is loading/updating itself.
|
||||
|
||||
Migration: remove the `AsyncRoot` component, then possibly, reorganize the code
|
||||
to fetch data in a higher order component, and using a `t-if/t-else` to display
|
||||
either a fallback when the data is not ready, or the actual component with data
|
||||
as props. If there is no escape, and `AsyncRoot` is needed, please reach out to
|
||||
us so we can study this usecase.
|
||||
|
||||
### 27. `useSubEnv` only applies to child components
|
||||
|
||||
In Owl 1, a call to `useSubEnv` would define a new environment for the children
|
||||
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
|
||||
|
||||
In Owl 2, the `env` object is frozen. It can no longer be modified (structurally)
|
||||
arbitrarily.
|
||||
|
||||
Rationale: it seems like the `env` object purpose is to have a global channel of
|
||||
communication between components. It is however scary if anyone can add something
|
||||
to it. The usual use case is to add something to the environment for some child
|
||||
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
|
||||
longer works.
|
||||
|
||||
Rationale: the possibility of having a ref to a child component breaks the
|
||||
encapsulation provided by Owl components: a child component now has a private
|
||||
and a public interface. Another issue is that it may be unclear when the ref
|
||||
should be set: is the component active on setup, or on mounted? Also, it is
|
||||
kind of awkward to implement.
|
||||
|
||||
Migration: the `env` and `props` should provide a communication channel wide enough:
|
||||
the sub component can expose its public API by calling a callback at the proper
|
||||
timing, or by triggering an event.
|
||||
|
||||
### 30. `t-on` does not accept expressions, only functions
|
||||
|
||||
In Owl 1, it was possible to define simple expressions inline, in a template:
|
||||
|
||||
```xml
|
||||
<button t-on-click="state.value = state.value + 1">blabla</button>
|
||||
<button t-on-click="someFunction(someVar)">blabla</button>
|
||||
```
|
||||
|
||||
This does not work anymore. Now, the `t-on` directive assumes that what it get is
|
||||
a function.
|
||||
|
||||
Rationale: the fact that owl 1 had to support expressions meant that it was not
|
||||
possible to properly inject the event in general. With this restriction, Owl 2
|
||||
can support more general use cases. Also, the examples above can simply be
|
||||
wrapped in a lambda function.
|
||||
|
||||
Migration: use lambda functions. For example, the two examples above can be
|
||||
adapted like this:
|
||||
|
||||
```xml
|
||||
<button t-on-click="() => state.value = state.value + 1">blabla</button>
|
||||
<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
|
||||
root. Now, the content is arbitrary: it can be empty, or multiple html elements.
|
||||
So, the following template works for components:
|
||||
|
||||
```xml
|
||||
<div>1</div>
|
||||
<div>2</div>
|
||||
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
|
||||
the qweb instance knew all templates. But now, the closest analogy is the `App`
|
||||
class, but it is not as convenient, since the `app` instance is no longer visible
|
||||
to components (while before, `qweb` was in the environment).
|
||||
|
||||
Also, this can easily be done in userspace, by mounting a component in a div. For example:
|
||||
|
||||
```js
|
||||
export async function renderToString(template, context) {
|
||||
class C extends Component {
|
||||
static template = template;
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
const component = await mount(C, div);
|
||||
const result = div.innerHTML;
|
||||
app.destroy();
|
||||
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).
|
||||
@@ -1,4 +1,4 @@
|
||||
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">OWL Framework</a> 🦉</h1>
|
||||
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Owl Framework</a> 🦉</h1>
|
||||
|
||||
[](https://www.gnu.org/licenses/lgpl-3.0)
|
||||
[](https://badge.fury.io/js/@odoo%2Fowl)
|
||||
@@ -6,114 +6,110 @@
|
||||
|
||||
_Class based components with hooks, reactive state and concurrent mode_
|
||||
|
||||
**Try it online!** you can experiment with the Owl framework in an online [playground](https://odoo.github.io/owl/playground).
|
||||
|
||||
## Project Overview
|
||||
|
||||
The Odoo Web Library (OWL) is a smallish (~<20kb gzipped) UI framework intended to
|
||||
be the basis for the [Odoo](https://www.odoo.com/) Web Client. Owl is a modern
|
||||
The Odoo Web Library (Owl) is a smallish (~<20kb gzipped) UI framework built by
|
||||
[Odoo](https://www.odoo.com/) for its products. Owl is a modern
|
||||
framework, written in Typescript, taking the best ideas from React and Vue in a
|
||||
simple and consistent way. Owl's main features are:
|
||||
|
||||
- a declarative component system,
|
||||
- a reactivity system based on hooks,
|
||||
- concurrent mode by default,
|
||||
- a store and a frontend router
|
||||
- a fine grained reactivity system similar to Vue,
|
||||
- hooks
|
||||
- fragments
|
||||
- asynchronous rendering
|
||||
|
||||
Owl components are defined with ES6 classes, they use QWeb templates, an
|
||||
Owl components are defined with ES6 classes and xml templates, uses an
|
||||
underlying virtual DOM, integrates beautifully with hooks, and the rendering is
|
||||
asynchronous.
|
||||
|
||||
**Try it online!** An online playground is available at
|
||||
[https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground)
|
||||
to let you experiment with the Owl framework. There are some code examples to
|
||||
showcase some interesting features.
|
||||
Quick links:
|
||||
|
||||
Owl is currently stable. Possible future changes are explained in the
|
||||
[roadmap](roadmap.md).
|
||||
|
||||
## Why Owl?
|
||||
|
||||
Why did Odoo decide to make Yet Another Framework? This is really a question
|
||||
that deserves [a long answer](doc/miscellaneous/why_owl.md). But in short, we believe that
|
||||
while the current state of the art frameworks are excellent, they are not
|
||||
optimized for our use case, and there is still room for something else.
|
||||
|
||||
If you are interested in a comparison with React or Vue, you will
|
||||
find some more additional information [here](doc/miscellaneous/comparison.md).
|
||||
- [documentation](#documentation),
|
||||
- [changelog](CHANGELOG.md) (from Owl 1.x to 2.x),
|
||||
- [playground](https://odoo.github.io/owl/playground)
|
||||
|
||||
## Example
|
||||
|
||||
Here is a short example to illustrate interactive components:
|
||||
|
||||
```javascript
|
||||
const { Component, useState } = owl;
|
||||
const { xml } = owl.tags;
|
||||
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 };
|
||||
}
|
||||
|
||||
const app = new App();
|
||||
app.mount(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
|
||||
[playground](https://odoo.github.io/owl/playground) application.
|
||||
|
||||
## Design Principles
|
||||
|
||||
OWL is designed to be used in highly dynamic applications where changing
|
||||
requirements are common and code needs to be maintained by large teams.
|
||||
|
||||
- **XML based**: templates are based on the XML format, which allows interesting
|
||||
applications. For example, they could be stored in a database and modified
|
||||
dynamically with `xpaths`.
|
||||
- **templates compilation in the browser**: this may not be a good fit for all
|
||||
applications, but if you need to generate dynamically user interfaces in the
|
||||
browser, this is very powerful. For example, a generic form view component
|
||||
could generate a specific form user interface for each various models, from a XML view.
|
||||
- **no toolchain required**: this is extremely useful for some applications, if,
|
||||
for various reasons (security/deployment/dynamic modules/specific assets tools),
|
||||
it is not ok to use standard web tools based on `npm`.
|
||||
|
||||
Owl is not designed to be fast nor small (even though it is quite good on those
|
||||
two topics). It is a no nonsense framework to build applications. There is only
|
||||
one way to define components (with classes). There is no black magic. It just
|
||||
works.
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
A complete documentation for Owl can be found here:
|
||||
### Learning Owl
|
||||
|
||||
- [Main documentation page](doc/readme.md).
|
||||
Are you new to Owl? This is the place to start!
|
||||
|
||||
Some of the most important pages are:
|
||||
|
||||
- [Tutorial: TodoList application](doc/learning/tutorial_todoapp.md)
|
||||
- [Tutorial: create a TodoList application](doc/learning/tutorial_todoapp.md)
|
||||
- [How to start an Owl project](doc/learning/quick_start.md)
|
||||
- [QWeb templating language](doc/reference/qweb_templating_language.md)
|
||||
- [Component](doc/reference/component.md)
|
||||
- [Hooks](doc/reference/hooks.md)
|
||||
- [How to test Components](doc/learning/how_to_test.md)
|
||||
|
||||
### Reference
|
||||
|
||||
- [Overview](doc/readme.md)
|
||||
- [App](doc/reference/app.md)
|
||||
- [Component](doc/reference/component.md)
|
||||
- [Component Lifecycle](doc/reference/component.md#lifecycle)
|
||||
- [Concurrency Model](doc/reference/concurrency_model.md)
|
||||
- [Dynamic sub components](doc/reference/component.md#dynamic-sub-components)
|
||||
- [Environment](doc/reference/environment.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)
|
||||
- [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.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)
|
||||
- [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
|
||||
|
||||
- [Notes On Owl Architecture](doc/miscellaneous/architecture.md)
|
||||
- [Comparison with React/Vue](doc/miscellaneous/comparison.md)
|
||||
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
|
||||
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
|
||||
|
||||
## Installing Owl
|
||||
|
||||
@@ -125,8 +121,5 @@ npm install @odoo/owl
|
||||
|
||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||
|
||||
- [owl-1.0.13](https://github.com/odoo/owl/releases/tag/v1.0.13)
|
||||
- [owl-1.4.7](https://github.com/odoo/owl/releases/tag/v1.4.7)
|
||||
|
||||
## License
|
||||
|
||||
OWL is [LGPL licensed](./LICENSE).
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# 🦉 How to debug Owl applications 🦉
|
||||
|
||||
Non trivial applications become quickly more difficult to understand. It is then
|
||||
useful to have a solid understanding of what is going on. To help with that,
|
||||
logging useful information is extremely valuable. There is a [javascript file](../../tools/debug.js) which can be evaluated in an application.
|
||||
|
||||
Once it is executed, it will log a lot of information on each component main hooks. The following code is a minified version to make it easier to copy/paste:
|
||||
|
||||
```
|
||||
function debugOwl(t,e){let n,o="[OWL_DEBUG]";function r(t){let e;try{e=JSON.stringify(t||{})}catch(t){e="<JSON error>"}return e.length>200&&(e=e.slice(0,200)+"..."),e}if(Object.defineProperty(t.Component,"current",{get:()=>n,set(s){n=s;const i=s.constructor.name;if(e.componentBlackList&&e.componentBlackList.test(i))return;if(e.componentWhiteList&&!e.componentWhiteList.test(i))return;let l;Object.defineProperty(n,"__owl__",{get:()=>l,set(n){!function(n,s,i){let l=`${s}<id=${i}>`,c=t=>console.log(`${o} ${l} ${t}`),u=t=>(!e.methodBlackList||!e.methodBlackList.includes(t))&&!(e.methodWhiteList&&!e.methodWhiteList.includes(t));u("constructor")&&c(`constructor, props=${r(n.props)}`);u("willStart")&&t.hooks.onWillStart(()=>{c("willStart")});u("mounted")&&t.hooks.onMounted(()=>{c("mounted")});u("willUpdateProps")&&t.hooks.onWillUpdateProps(t=>{c(`willUpdateProps, nextprops=${r(t)}`)});u("willPatch")&&t.hooks.onWillPatch(()=>{c("willPatch")});u("patched")&&t.hooks.onPatched(()=>{c("patched")});u("willUnmount")&&t.hooks.onWillUnmount(()=>{c("willUnmount")});const d=n.__render.bind(n);n.__render=function(...t){c("rendering template"),d(...t)};const h=n.render.bind(n);n.render=function(...t){const e=n.__owl__;let o="render";return e.isMounted||e.currentFiber||(o+=" (warning: component is not mounted, this render has no effect)"),c(o),h(...t)};const p=n.mount.bind(n);n.mount=function(...t){return c("mount"),p(...t)}}(s,i,(l=n).id)}})}}),e.logScheduler){let e=t.Component.scheduler.start,n=t.Component.scheduler.stop;t.Component.scheduler.start=function(){this.isRunning||console.log(`${o} scheduler: start running tasks queue`),e.call(this)},t.Component.scheduler.stop=function(){this.isRunning&&console.log(`${o} scheduler: stop running tasks queue`),n.call(this)}}if(e.logStore){let e=t.Store.prototype.dispatch;t.Store.prototype.dispatch=function(t,...n){return console.log(`${o} store: action '${t}' dispatched. Payload: '${r(n)}'`),e.call(this,t,...n)}}}
|
||||
debugOwl(owl, {
|
||||
// componentBlackList: /App/, // regexp
|
||||
// componentWhiteList: /SomeComponent/, // regexp
|
||||
// methodBlackList: ["mounted"], // list of method names
|
||||
// methodWhiteList: ["willStart"], // list of method names
|
||||
logScheduler: false, // display/mute scheduler logs
|
||||
logStore: true, // display/mute store logs
|
||||
});
|
||||
```
|
||||
|
||||
The above code, once pasted somewhere in the main javascript file of an owl
|
||||
application, will log information looking like this:
|
||||
|
||||
```
|
||||
[OWL_DEBUG] TodoApp<id=1> constructor, props={}
|
||||
[OWL_DEBUG] TodoApp<id=1> mount
|
||||
[OWL_DEBUG] TodoApp<id=1> willStart
|
||||
[OWL_DEBUG] TodoApp<id=1> rendering template
|
||||
[OWL_DEBUG] TodoItem<id=2> constructor, props={"id":2,"completed":false,"title":"hey"}
|
||||
[OWL_DEBUG] TodoItem<id=2> willStart
|
||||
[OWL_DEBUG] TodoItem<id=3> constructor, props={"id":4,"completed":false,"title":"aaa"}
|
||||
[OWL_DEBUG] TodoItem<id=3> willStart
|
||||
[OWL_DEBUG] TodoItem<id=2> rendering template
|
||||
[OWL_DEBUG] TodoItem<id=3> rendering template
|
||||
[OWL_DEBUG] TodoItem<id=3> mounted
|
||||
[OWL_DEBUG] TodoItem<id=2> mounted
|
||||
[OWL_DEBUG] TodoApp<id=1> mounted
|
||||
```
|
||||
|
||||
Each component has an internal `id`, which is very useful when debugging.
|
||||
|
||||
Note that it is certainly useful to run this code at some point in an application,
|
||||
just to get a feel of what each user action implies, for the framework.
|
||||
+11
-47
@@ -30,27 +30,21 @@ To help with this, it is useful to have a `helper.js` file that contains some
|
||||
common utility functions:
|
||||
|
||||
```js
|
||||
let lastFixture = null;
|
||||
|
||||
export function makeTestFixture() {
|
||||
let fixture = document.createElement("div");
|
||||
document.body.appendChild(fixture);
|
||||
if (lastFixture) {
|
||||
lastFixture.remove();
|
||||
}
|
||||
lastFixture = fixture;
|
||||
return fixture;
|
||||
}
|
||||
|
||||
export function nextTick() {
|
||||
let requestAnimationFrame = owl.Component.scheduler.requestAnimationFrame;
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(() => requestAnimationFrame(() => resolve()));
|
||||
});
|
||||
}
|
||||
|
||||
export function makeTestEnv() {
|
||||
// application specific. It needs a way to load actual templates
|
||||
const templates = ...;
|
||||
|
||||
return {
|
||||
qweb: new QWeb(templates),
|
||||
..., // each service can be mocked here
|
||||
};
|
||||
export async function nextTick() {
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
}
|
||||
```
|
||||
|
||||
@@ -59,7 +53,7 @@ With such a file, a typical test suite for Jest will look like this:
|
||||
```js
|
||||
// in SomeComponent.test.js
|
||||
import { SomeComponent } from "../../src/ui/SomeComponent";
|
||||
import { nextTick, makeTestFixture, makeTestEnv} from '../helpers';
|
||||
import { nextTick, makeTestFixture } from '../helpers';
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -70,9 +64,6 @@ let env: Env;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestEnv();
|
||||
// we set here the default environment for each component created in the test
|
||||
Component.env = env;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -85,8 +76,7 @@ afterEach(() => {
|
||||
describe("SomeComponent", () => {
|
||||
test("component behaves as expected", async () => {
|
||||
const props = {...}; // depends on the component
|
||||
const comp = new SomeComponent(null, props);
|
||||
await comp.mount(fixture);
|
||||
const comp = await mount(SomeComponent, fixture, { props });
|
||||
|
||||
// do some assertions
|
||||
expect(...).toBe(...);
|
||||
@@ -103,29 +93,3 @@ describe("SomeComponent", () => {
|
||||
Note that Owl does wait for the next animation frame to actually update the DOM.
|
||||
This is why it is necessary to wait with the `nextTick` (or other methods) to
|
||||
make sure that the DOM is up-to-date.
|
||||
|
||||
It is sometimes useful to wait until Owl is completely done updating components
|
||||
(in particular, if we have a highly concurrent user interface). This next
|
||||
helper simply polls every 20ms the internal Owl task queue and returns a promise
|
||||
which resolves when it is empty:
|
||||
|
||||
```js
|
||||
function afterUpdates() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let timer = setTimeout(poll, 20);
|
||||
let counter = 0;
|
||||
function poll() {
|
||||
counter++;
|
||||
if (owl.Component.scheduler.tasks.length) {
|
||||
if (counter > 10) {
|
||||
reject(new Error("timeout"));
|
||||
} else {
|
||||
timer = setTimeout(poll);
|
||||
}
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
@@ -1,133 +0,0 @@
|
||||
# 🦉 Quick Overview 🦉
|
||||
|
||||
Owl components in an application are used to define a (dynamic) tree of components.
|
||||
|
||||
```
|
||||
Root
|
||||
/ \
|
||||
A B
|
||||
/ \
|
||||
C D
|
||||
```
|
||||
|
||||
**State:** each component can manage its own local state. It is a simple ES6
|
||||
class, there are no special rules:
|
||||
|
||||
```js
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = { value: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The example above shows a component with a local state. Note that since there
|
||||
is nothing magical to the `state` object, we need to manually call the `render`
|
||||
function whenever we update it. This can quickly become annoying (and not
|
||||
efficient if we do it too much). There is a better way: using the `useState`
|
||||
hook, which transforms an object into a reactive version of itself:
|
||||
|
||||
```js
|
||||
const { useState } = owl.hooks;
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that the `t-on-click` handler can even be replaced by an inline statement:
|
||||
|
||||
```xml
|
||||
<button t-on-click="state.value++">
|
||||
```
|
||||
|
||||
**Props:** sub components often needs some information from their parents. This
|
||||
is done by adding the required information to the template. This will then be
|
||||
accessible by the sub component in the `props` object. Note that there is an
|
||||
important rule here: the information contained in the `props` object is not
|
||||
owned by the sub component, and should never be modified.
|
||||
|
||||
```js
|
||||
class Child extends Component {
|
||||
static template = xml`<div>Hello <t t-esc="props.name"/></div>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Child name="'Owl'" />
|
||||
<Child name="'Framework'" />
|
||||
</div>`;
|
||||
static components = { Child };
|
||||
}
|
||||
```
|
||||
|
||||
**Communication:** there are multiple ways to communicate information between
|
||||
components. However, the two most important ways are the following:
|
||||
|
||||
- from parent to children: by using `props`,
|
||||
- from a children to one of its parent: by triggering events.
|
||||
|
||||
The following example illustrate both mechanisms:
|
||||
|
||||
```js
|
||||
class OrderLine extends Component {
|
||||
static template = xml`
|
||||
<div t-on-click="add">
|
||||
<div><t t-esc="props.line.name"/></div>
|
||||
<div>Quantity: <t t-esc="props.line.quantity"/></div>
|
||||
</div>`;
|
||||
|
||||
add() {
|
||||
this.trigger("add-to-order", { line: props.line });
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div t-on-add-to-order="addToOrder">
|
||||
<OrderLine
|
||||
t-foreach="orders"
|
||||
t-as="line"
|
||||
line="line" />
|
||||
</div>`;
|
||||
static components = { OrderLine };
|
||||
orders = useState([
|
||||
{ id: 1, name: "Coffee", quantity: 0 },
|
||||
{ id: 2, name: "Tea", quantity: 0 },
|
||||
]);
|
||||
|
||||
addToOrder(event) {
|
||||
const line = event.detail.line;
|
||||
line.quantity++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the `OrderLine` component trigger a `add-to-order` event. This
|
||||
will generate a DOM event which will bubble along the DOM tree. It will then be
|
||||
intercepted by the parent component, which will then get the line (from the
|
||||
`detail` key) and then increment its quantity. See the page on [event handling](../reference/event_handling.md)
|
||||
for more details on how events work.
|
||||
|
||||
Note that this example would have also worked if the `OrderLine` component
|
||||
directly modifies the `line` object. However, this is not a good practice: this
|
||||
only works because the `props` object received by the child component is reactive,
|
||||
so the child component is then coupled to the parents implementation.
|
||||
+40
-52
@@ -36,6 +36,8 @@ hello_owl/
|
||||
The file `owl.js` can be downloaded from the last release published at
|
||||
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases). It
|
||||
is a single javascript file which export all Owl into the global `owl` object.
|
||||
Note that there are multiple files, and in this case, we need one of the two
|
||||
files suffixed with `.iife`: they are built to be directly used in a browser.
|
||||
|
||||
Now, `index.html` should contain the following:
|
||||
|
||||
@@ -45,31 +47,24 @@ Now, `index.html` should contain the following:
|
||||
<head>
|
||||
<title>Hello Owl</title>
|
||||
<script src="owl.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
<body>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
And `app.js` should look like this:
|
||||
|
||||
```js
|
||||
const { Component } = owl;
|
||||
const { xml } = owl.tags;
|
||||
const { whenReady } = owl.utils;
|
||||
const { Component, mount, xml } = owl;
|
||||
|
||||
// Owl Components
|
||||
class App extends Component {
|
||||
class Root extends Component {
|
||||
static template = xml`<div>Hello Owl</div>`;
|
||||
}
|
||||
|
||||
// Setup code
|
||||
function setup() {
|
||||
const app = new App();
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
whenReady(setup);
|
||||
mount(Root, document.body);
|
||||
```
|
||||
|
||||
Now, simply loading this html file in a browser should display a welcome message.
|
||||
@@ -94,14 +89,16 @@ Let us start a new project with the following file structure:
|
||||
```
|
||||
hello_owl/
|
||||
src/
|
||||
app.js
|
||||
index.html
|
||||
main.js
|
||||
owl.js
|
||||
root.js
|
||||
```
|
||||
|
||||
As previously, the file `owl.js` can be downloaded from the last release published at
|
||||
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases).
|
||||
Note that there are multiple files, and in this case, we need one of the two
|
||||
files suffixed with `.iife`: they are built to be directly used in a browser.
|
||||
|
||||
Now, `index.html` should contain the following:
|
||||
|
||||
@@ -111,38 +108,33 @@ Now, `index.html` should contain the following:
|
||||
<head>
|
||||
<title>Hello Owl</title>
|
||||
<script src="owl.js"></script>
|
||||
<script src="main.js" type="module"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
<body>
|
||||
<script src="main.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Not that the `main.js` script tag has the `type="module"` attribute. This means
|
||||
that the browser will parse the script as a module, and load all its dependencies.
|
||||
|
||||
Here is the content of `app.js` and `main.js`:
|
||||
Here is the content of `root.js` and `main.js`:
|
||||
|
||||
```js
|
||||
// app.js ----------------------------------------------------------------------
|
||||
const { Component } = owl;
|
||||
const { xml } = owl.tags;
|
||||
// root.js ----------------------------------------------------------------------
|
||||
const { Component, mount, xml } = owl;
|
||||
|
||||
export class App extends Component {
|
||||
export class Root extends Component {
|
||||
static template = xml`<div>Hello Owl</div>`;
|
||||
}
|
||||
|
||||
// main.js ---------------------------------------------------------------------
|
||||
import { App } from "./app.js";
|
||||
import { Root } from "./root.js";
|
||||
|
||||
function setup() {
|
||||
const app = new App();
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
owl.utils.whenReady(setup);
|
||||
mount(Root, document.body);
|
||||
```
|
||||
|
||||
The `main.js` file import the `app.js` file. Note that the import statement has
|
||||
The `main.js` file imports the `root.js` file. Note that the import statement has
|
||||
a `.js` suffix, which is important. Most text editor can understand this syntax
|
||||
and will provide autocompletion.
|
||||
|
||||
@@ -195,11 +187,11 @@ hello_owl/
|
||||
index.html
|
||||
src/
|
||||
components/
|
||||
App.js
|
||||
Root.js
|
||||
main.js
|
||||
tests/
|
||||
components/
|
||||
App.test.js
|
||||
Root.test.js
|
||||
helpers.js
|
||||
.gitignore
|
||||
package.json
|
||||
@@ -226,13 +218,15 @@ Note that there are no `<script>` tag here. They will be injected by webpack.
|
||||
Now, let's have a look at the javascript files:
|
||||
|
||||
```js
|
||||
// src/components/App.js -------------------------------------------------------
|
||||
import { Component, tags, useState } from "@odoo/owl";
|
||||
// src/components/Root.js -------------------------------------------------------
|
||||
import { Component, xml, useState } from "@odoo/owl";
|
||||
|
||||
const { xml } = tags;
|
||||
export class Root extends Component {
|
||||
static template = xml`
|
||||
<div t-on-click="update">
|
||||
Hello <t t-esc="state.text"/>
|
||||
</div>`;
|
||||
|
||||
export class App extends Component {
|
||||
static template = xml`<div t-on-click="update">Hello <t t-esc="state.text"/></div>`;
|
||||
state = useState({ text: "Owl" });
|
||||
update() {
|
||||
this.state.text = this.state.text === "Owl" ? "World" : "Owl";
|
||||
@@ -240,19 +234,15 @@ export class App extends Component {
|
||||
}
|
||||
|
||||
// src/main.js -----------------------------------------------------------------
|
||||
import { utils } from "@odoo/owl";
|
||||
import { App } from "./components/App";
|
||||
import { utils, mount } from "@odoo/owl";
|
||||
import { Root } from "./components/Root";
|
||||
|
||||
function setup() {
|
||||
const app = new App();
|
||||
app.mount(document.body);
|
||||
}
|
||||
mount(Root, document.body);
|
||||
|
||||
utils.whenReady(setup);
|
||||
|
||||
// tests/components/App.test.js ------------------------------------------------
|
||||
import { App } from "../../src/components/App";
|
||||
// tests/components/Root.test.js ------------------------------------------------
|
||||
import { Root } from "../../src/components/Root";
|
||||
import { makeTestFixture, nextTick, click } from "../helpers";
|
||||
import { mount } from "@odoo/owl";
|
||||
|
||||
let fixture;
|
||||
|
||||
@@ -264,10 +254,9 @@ afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
describe("App", () => {
|
||||
describe("Root", () => {
|
||||
test("Works as expected...", async () => {
|
||||
const app = new App();
|
||||
await app.mount(fixture);
|
||||
await mount(Root, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
|
||||
|
||||
click(fixture, "div");
|
||||
@@ -281,9 +270,8 @@ import { Component } from "@odoo/owl";
|
||||
import "regenerator-runtime/runtime";
|
||||
|
||||
export async function nextTick() {
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(() => Component.scheduler.requestAnimationFrame(() => resolve()));
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
}
|
||||
|
||||
export function makeTestFixture() {
|
||||
|
||||
+297
-330
@@ -50,10 +50,11 @@ the following content:
|
||||
<meta charset="UTF-8" />
|
||||
<title>OWL Todo App</title>
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<script src="owl.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
@@ -71,45 +72,34 @@ Note that we put everything inside an immediately executed function to avoid lea
|
||||
anything to the global scope.
|
||||
|
||||
Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer). Be aware that you should download the `owl.iife.js` or `owl.iife.min.js`, because these files
|
||||
are built to run directly on the browser (other files such as `owl.cjs.js` are
|
||||
are built to run directly on the browser, and rename it `owl.js` (other files such as `owl.cjs.js` are
|
||||
built to be bundled by other tools).
|
||||
|
||||
Now, the project should be ready. Loading the `index.html` file into a browser
|
||||
should show an empty page, with the title `Owl Todo App`, and it should log a
|
||||
message such as `hello owl 1.0.0` in the console.
|
||||
message such as `hello owl 2.x.y` in the console.
|
||||
|
||||
## 2. Adding a first component
|
||||
|
||||
An Owl application is made out of [components](../reference/component.md), with
|
||||
a single root component. Let us start by defining an `App` component. Replace the
|
||||
a single root component. Let us start by defining a `Root` component. Replace the
|
||||
content of the function in `app.js` by the following code:
|
||||
|
||||
```js
|
||||
const { Component } = owl;
|
||||
const { xml } = owl.tags;
|
||||
const { whenReady } = owl.utils;
|
||||
const { Component, mount, xml } = owl;
|
||||
|
||||
// Owl Components
|
||||
class App extends Component {
|
||||
class Root extends Component {
|
||||
static template = xml`<div>todo app</div>`;
|
||||
}
|
||||
|
||||
// Setup code
|
||||
function setup() {
|
||||
const app = new App();
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
whenReady(setup);
|
||||
mount(Root, document.body);
|
||||
```
|
||||
|
||||
Now, reloading the page in a browser should display a message.
|
||||
|
||||
The code is pretty simple, but let us explain the last line in more detail. The
|
||||
browser tries to execute the javascript code in `app.js` as quickly as possible,
|
||||
and it could happen that the DOM is not ready yet when we try to mount the `App`
|
||||
component. To avoid this situation, we use the [`whenReady`](../reference/utils.md#whenready)
|
||||
helper to delay the execution of the `setup` function until the DOM is ready.
|
||||
The code is pretty simple: we define a component with an inline template, then
|
||||
mount it in the document body.
|
||||
|
||||
Note 1: in a larger project, we would split the code in multiple files, with
|
||||
components in a sub folder, and a main file that would initialize the application.
|
||||
@@ -126,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,
|
||||
@@ -150,20 +140,20 @@ with the following keys:
|
||||
tasks. Since the title is something created/edited by the user, it offers
|
||||
no guarantee that it is unique. So, we will generate a unique `id` number for
|
||||
each task.
|
||||
- `title`: a string, to explain what the task is about.
|
||||
- `text`: a string, to explain what the task is about.
|
||||
- `isCompleted`: a boolean, to keep track of the status of the task
|
||||
|
||||
Now that we decided on the internal format of the state, let us add some demo
|
||||
data and a template to the `App` component:
|
||||
|
||||
```js
|
||||
class App extends Component {
|
||||
class Root extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="task-list">
|
||||
<t t-foreach="tasks" t-as="task" t-key="task.id">
|
||||
<div class="task">
|
||||
<input type="checkbox" t-att-checked="task.isCompleted"/>
|
||||
<span><t t-esc="task.title"/></span>
|
||||
<span><t t-esc="task.text"/></span>
|
||||
</div>
|
||||
</t>
|
||||
</div>`;
|
||||
@@ -171,26 +161,26 @@ class App extends Component {
|
||||
tasks = [
|
||||
{
|
||||
id: 1,
|
||||
title: "buy milk",
|
||||
text: "buy milk",
|
||||
isCompleted: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "clean house",
|
||||
text: "clean house",
|
||||
isCompleted: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -246,29 +236,25 @@ a little bit:
|
||||
// -------------------------------------------------------------------------
|
||||
// Task Component
|
||||
// -------------------------------------------------------------------------
|
||||
const TASK_TEMPLATE = xml /* xml */`
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted"/>
|
||||
<span><t t-esc="props.task.title"/></span>
|
||||
</div>`;
|
||||
|
||||
class Task extends Component {
|
||||
static template = TASK_TEMPLATE;
|
||||
static props = ["task"];
|
||||
static template = xml /* xml */`
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted"/>
|
||||
<span><t t-esc="props.task.title"/></span>
|
||||
</div>`;
|
||||
static props = ["task"];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// App Component
|
||||
// Root Component
|
||||
// -------------------------------------------------------------------------
|
||||
const APP_TEMPLATE = xml /* xml */`
|
||||
class Root extends Component {
|
||||
static template = xml /* xml */`
|
||||
<div class="task-list">
|
||||
<t t-foreach="tasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
<t t-foreach="tasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>`;
|
||||
|
||||
class App extends Component {
|
||||
static template = APP_TEMPLATE;
|
||||
static components = { Task };
|
||||
|
||||
tasks = [
|
||||
@@ -277,15 +263,9 @@ class App extends Component {
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Setup code
|
||||
// Setup
|
||||
// -------------------------------------------------------------------------
|
||||
function setup() {
|
||||
owl.config.mode = "dev";
|
||||
const app = new App();
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
whenReady(setup);
|
||||
mount(Root, document.body, {dev: true});
|
||||
```
|
||||
|
||||
A lot of stuff happened here:
|
||||
@@ -294,24 +274,22 @@ A lot of stuff happened here:
|
||||
- whenever we define a sub component, it needs to be added to the static
|
||||
[`components`](../reference/component.md#static-properties)
|
||||
key of its parent, so Owl can get a reference to it,
|
||||
- the templates have been extracted out of the components, to make it easier to
|
||||
differentiate the "view/template" code from the "script/behavior" code,
|
||||
- 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 `setup`
|
||||
function. Note that this should be removed when an app is used in a real
|
||||
[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.
|
||||
|
||||
## 6. Adding tasks (part 1)
|
||||
|
||||
We still use a list of hardcoded tasks. It's really time to give the user a way
|
||||
to add tasks himself. The first step is to add an input to the `App` component.
|
||||
But this input will be outside of the task list, so we need to adapt `App`
|
||||
to add tasks himself. The first step is to add an input to the `Root` component.
|
||||
But this input will be outside of the task list, so we need to adapt `Root`
|
||||
template, js, and css:
|
||||
|
||||
```xml
|
||||
@@ -329,9 +307,9 @@ template, js, and css:
|
||||
addTask(ev) {
|
||||
// 13 is keycode for ENTER
|
||||
if (ev.keyCode === 13) {
|
||||
const title = ev.target.value.trim();
|
||||
const text = ev.target.value.trim();
|
||||
ev.target.value = "";
|
||||
console.log('adding task', title);
|
||||
console.log('adding task', text);
|
||||
// todo
|
||||
}
|
||||
}
|
||||
@@ -360,10 +338,9 @@ task. Notice that when you load the page, the input is not focused. But adding
|
||||
tasks is a core feature of a task list, so let us make it as fast as possible by
|
||||
focusing the input.
|
||||
|
||||
Since `App` is a component, it has a
|
||||
[`mounted` lifecycle method](../reference/component.md#lifecycle) that we can
|
||||
implement. We will also need to get a reference to the input, by using the
|
||||
`t-ref` directive with the [`useRef`](../reference/hooks.md#useref) hook:
|
||||
We need to execute code when the `Root` component is ready (mounted). Let's do
|
||||
that using the `onMounted` hook. We will also need to get a reference to the
|
||||
input, by using the `t-ref` directive with the [`useRef`](../reference/hooks.md#useref) hook:
|
||||
|
||||
```xml
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
|
||||
@@ -371,22 +348,21 @@ implement. We will also need to get a reference to the input, by using the
|
||||
|
||||
```js
|
||||
// on top of file:
|
||||
const { useRef } = owl.hooks;
|
||||
const { Component, mount, xml, useRef, onMounted } = owl;
|
||||
```
|
||||
|
||||
```js
|
||||
// in App
|
||||
inputRef = useRef("add-input");
|
||||
|
||||
mounted() {
|
||||
this.inputRef.el.focus();
|
||||
setup() {
|
||||
const inputRef = useRef("add-input");
|
||||
onMounted(() => inputRef.el.focus());
|
||||
}
|
||||
```
|
||||
|
||||
The `inputRef` is defined as a class field, so it is equivalent to defining it
|
||||
in the constructor. It simply instructs Owl to keep a reference to anything with
|
||||
the corresponding `t-ref` keyword. We then implement the `mounted` lifecycle
|
||||
method, where we now have an active reference that we can use to focus the input.
|
||||
This is a very common situation: whenever we need to perform some actions depending
|
||||
on the lifecycle of a component, we need to do it in the `setup` method, by using
|
||||
one of the lifecycle hook. Here, we first get a reference to the `inputRef`,
|
||||
then in the `onMounted` hook, we simply focus the html element.
|
||||
|
||||
## 7. Adding tasks (part 2)
|
||||
|
||||
@@ -407,12 +383,12 @@ Now, the `addTask` method can be implemented:
|
||||
addTask(ev) {
|
||||
// 13 is keycode for ENTER
|
||||
if (ev.keyCode === 13) {
|
||||
const title = ev.target.value.trim();
|
||||
const text = ev.target.value.trim();
|
||||
ev.target.value = "";
|
||||
if (title) {
|
||||
if (text) {
|
||||
const newTask = {
|
||||
id: this.nextId++,
|
||||
title: title,
|
||||
text: text,
|
||||
isCompleted: false,
|
||||
};
|
||||
this.tasks.push(newTask);
|
||||
@@ -430,7 +406,7 @@ the user interface. We can fix the issue by making `tasks` reactive, with the
|
||||
|
||||
```js
|
||||
// on top of the file
|
||||
const { useRef, useState } = owl.hooks;
|
||||
const { Component, mount, xml, useRef, onMounted, useState } = owl;
|
||||
|
||||
// replace the task definition in App with the following:
|
||||
tasks = useState([]);
|
||||
@@ -445,12 +421,8 @@ did not change in opacity. This is because there is no code to modify the
|
||||
`isCompleted` flag.
|
||||
|
||||
Now, this is an interesting situation: the task is displayed by the `Task`
|
||||
component, but it is not the owner of its state, so it cannot modify it. Instead,
|
||||
we want to communicate the request to toggle a task to the `App` component.
|
||||
Since `App` is a parent of `Task`, we can
|
||||
[trigger](../reference/event_handling.md) an event in `Task` and listen
|
||||
for it in `App`.
|
||||
|
||||
component, but it is not the owner of its state, so ideally, it should not modify it.
|
||||
However, for now, that's what we will do (this will be improved in a later step).
|
||||
In `Task`, change the `input` to:
|
||||
|
||||
```xml
|
||||
@@ -461,36 +433,23 @@ and add the `toggleTask` method:
|
||||
|
||||
```js
|
||||
toggleTask() {
|
||||
this.trigger('toggle-task', {id: this.props.task.id});
|
||||
}
|
||||
```
|
||||
|
||||
We now need to listen for that event in the `App` template:
|
||||
|
||||
```xml
|
||||
<div class="task-list" t-on-toggle-task="toggleTask">
|
||||
```
|
||||
|
||||
and implement the `toggleTask` code:
|
||||
|
||||
```js
|
||||
toggleTask(ev) {
|
||||
const task = this.tasks.find(t => t.id === ev.detail.id);
|
||||
task.isCompleted = !task.isCompleted;
|
||||
this.props.task.isCompleted = !this.props.task.isCompleted;
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Deleting tasks
|
||||
|
||||
Let us now add the possibility do delete tasks. To do that, we first need to add
|
||||
a trash icon on each task, then we will proceed just like in the previous section.
|
||||
Let us now add the possibility do delete tasks. This is different from the previous
|
||||
feature: deleting task has to be done on the task itself, but the actual operation
|
||||
need to be done on the task list. So, we need to communicate the request to the
|
||||
`Root` component. This is usually done by providing a callback in a prop.
|
||||
|
||||
First, let us update the `Task` template, css and js:
|
||||
|
||||
```xml
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
|
||||
<span><t t-esc="props.task.title"/></span>
|
||||
<span><t t-esc="props.task.text"/></span>
|
||||
<span class="delete" t-on-click="deleteTask">🗑</span>
|
||||
</div>
|
||||
```
|
||||
@@ -519,220 +478,226 @@ First, let us update the `Task` template, css and js:
|
||||
```
|
||||
|
||||
```js
|
||||
static props = ["task", "onDelete"];
|
||||
|
||||
deleteTask() {
|
||||
this.trigger('delete-task', {id: this.props.task.id});
|
||||
this.props.onDelete(this.props.task);
|
||||
}
|
||||
```
|
||||
|
||||
And now, we need to listen to the `delete-task` event in `App`:
|
||||
And now, we need to provide the `onDelete` callback to each tasks in the `Root`
|
||||
component:
|
||||
|
||||
```xml
|
||||
<div class="task-list" t-on-toggle-task="toggleTask" t-on-delete-task="deleteTask">
|
||||
<Task task="task" onDelete.bind="deleteTask"/>
|
||||
```
|
||||
|
||||
```js
|
||||
deleteTask(ev) {
|
||||
const index = this.tasks.findIndex(t => t.id === ev.detail.id);
|
||||
deleteTask(task) {
|
||||
const index = this.tasks.findIndex(t => t.id === task.id);
|
||||
this.tasks.splice(index, 1);
|
||||
}
|
||||
```
|
||||
|
||||
Notice that the `onDelete` prop is defined with a `.bind` suffix: this is a special
|
||||
suffix that makes sure the function callback is bound to the component.
|
||||
|
||||
## 10. Using a store
|
||||
|
||||
Looking at the code, it is apparent that we now have code to handle tasks
|
||||
scattered in more than one place. Also, it mixes UI code and business logic
|
||||
code. Owl has a way to manage state separately from the user interface: a
|
||||
[`Store`](../reference/store.md).
|
||||
Looking at the code, it is apparent that all the code handling tasks is scattered
|
||||
all around the application. Also, it mixes UI code and business logic
|
||||
code. Owl does not provide any high level abstraction to manage business logic,
|
||||
but it is easy to do it with the basic reactivity primitives (`useState` and `reactive`).
|
||||
|
||||
Let us use it in our application. This is a pretty large refactoring (for our
|
||||
application), since it involves extracting all task related code out of the
|
||||
components. Here is the new content of the `app.js` file:
|
||||
Let us use it in our application to implement a central store. This is a pretty
|
||||
large refactoring (for our application), since it involves extracting all task
|
||||
related code out of the components. Here is the new content of the `app.js` file:
|
||||
|
||||
```js
|
||||
const { Component, Store } = owl;
|
||||
const { xml } = owl.tags;
|
||||
const { whenReady } = owl.utils;
|
||||
const { useRef, useDispatch, useStore } = owl.hooks;
|
||||
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Store
|
||||
// -------------------------------------------------------------------------
|
||||
const actions = {
|
||||
addTask({ state }, title) {
|
||||
title = title.trim();
|
||||
if (title) {
|
||||
function useStore() {
|
||||
const env = useEnv();
|
||||
return useState(env.store);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// TaskList
|
||||
// -------------------------------------------------------------------------
|
||||
class TaskList {
|
||||
nextId = 1;
|
||||
tasks = [];
|
||||
|
||||
addTask(text) {
|
||||
text = text.trim();
|
||||
if (text) {
|
||||
const task = {
|
||||
id: state.nextId++,
|
||||
title: title,
|
||||
id: this.nextId++,
|
||||
text: text,
|
||||
isCompleted: false,
|
||||
};
|
||||
state.tasks.push(task);
|
||||
this.tasks.push(task);
|
||||
}
|
||||
},
|
||||
toggleTask({ state }, id) {
|
||||
const task = state.tasks.find((t) => t.id === id);
|
||||
}
|
||||
|
||||
toggleTask(task) {
|
||||
task.isCompleted = !task.isCompleted;
|
||||
},
|
||||
deleteTask({ state }, id) {
|
||||
const index = state.tasks.findIndex((t) => t.id === id);
|
||||
state.tasks.splice(index, 1);
|
||||
},
|
||||
};
|
||||
const initialState = {
|
||||
nextId: 1,
|
||||
tasks: [],
|
||||
};
|
||||
}
|
||||
|
||||
deleteTask(task) {
|
||||
const index = this.tasks.findIndex((t) => t.id === task.id);
|
||||
this.tasks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function createTaskStore() {
|
||||
return reactive(new TaskList());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Task Component
|
||||
// -------------------------------------------------------------------------
|
||||
const TASK_TEMPLATE = xml/* xml */ `
|
||||
class Task extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted"
|
||||
t-on-click="dispatch('toggleTask', props.task.id)"/>
|
||||
<span><t t-esc="props.task.title"/></span>
|
||||
<span class="delete" t-on-click="dispatch('deleteTask', props.task.id)">🗑</span>
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="() => store.toggleTask(props.task)"/>
|
||||
<span><t t-esc="props.task.text"/></span>
|
||||
<span class="delete" t-on-click="() => store.deleteTask(props.task)">🗑</span>
|
||||
</div>`;
|
||||
|
||||
class Task extends Component {
|
||||
static template = TASK_TEMPLATE;
|
||||
static props = ["task"];
|
||||
dispatch = useDispatch();
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// App Component
|
||||
// Root Component
|
||||
// -------------------------------------------------------------------------
|
||||
const APP_TEMPLATE = xml/* xml */ `
|
||||
class Root extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="todo-app">
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
|
||||
<div class="task-list">
|
||||
<t t-foreach="tasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
|
||||
<div class="task-list">
|
||||
<t t-foreach="store.tasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
class App extends Component {
|
||||
static template = APP_TEMPLATE;
|
||||
static components = { Task };
|
||||
|
||||
inputRef = useRef("add-input");
|
||||
tasks = useStore((state) => state.tasks);
|
||||
dispatch = useDispatch();
|
||||
|
||||
mounted() {
|
||||
this.inputRef.el.focus();
|
||||
setup() {
|
||||
const inputRef = useRef("add-input");
|
||||
onMounted(() => inputRef.el.focus());
|
||||
this.store = useStore();
|
||||
}
|
||||
|
||||
addTask(ev) {
|
||||
// 13 is keycode for ENTER
|
||||
if (ev.keyCode === 13) {
|
||||
this.dispatch("addTask", ev.target.value);
|
||||
this.store.addTask(ev.target.value);
|
||||
ev.target.value = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Setup code
|
||||
// Setup
|
||||
// -------------------------------------------------------------------------
|
||||
function setup() {
|
||||
owl.config.mode = "dev";
|
||||
const store = new Store({ actions, state: initialState });
|
||||
App.env.store = store;
|
||||
const app = new App();
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
whenReady(setup);
|
||||
const env = {
|
||||
store: createTaskStore(),
|
||||
};
|
||||
mount(Root, document.body, { dev: true, env });
|
||||
```
|
||||
|
||||
## 11-Saving tasks in local storage
|
||||
## 11. Saving tasks in local storage
|
||||
|
||||
Now, our TodoApp works great, except if the user closes or refresh the browser!
|
||||
It is really inconvenient to only keep the state of the application in memory.
|
||||
To fix this, we will save the tasks in the local storage. With our current
|
||||
codebase, it is a simple change: only the setup code needs to be updated.
|
||||
codebase, it is a simple change: we need to save tasks to local storage and
|
||||
listen to any change.
|
||||
|
||||
```js
|
||||
function makeStore() {
|
||||
const localState = window.localStorage.getItem("todoapp");
|
||||
const state = localState ? JSON.parse(localState) : initialState;
|
||||
const store = new Store({ state, actions });
|
||||
store.on("update", null, () => {
|
||||
localStorage.setItem("todoapp", JSON.stringify(store.state));
|
||||
});
|
||||
return store;
|
||||
class TaskList {
|
||||
constructor(tasks) {
|
||||
this.tasks = tasks || [];
|
||||
const taskIds = this.tasks.map((t) => t.id);
|
||||
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
function setup() {
|
||||
owl.config.mode = "dev";
|
||||
App.env.store = makeStore();
|
||||
const app = new App();
|
||||
app.mount(document.body);
|
||||
function createTaskStore() {
|
||||
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
|
||||
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
|
||||
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
|
||||
saveTasks();
|
||||
return taskStore;
|
||||
}
|
||||
```
|
||||
|
||||
The key point is to use the fact that the store is an
|
||||
[`EventBus`](../reference/event_bus.md) which triggers an `update` event
|
||||
whenever it is updated.
|
||||
The key point is that the `reactive` function takes a callback that will be called
|
||||
every time an observed value is changed. Note that we need to call the `saveTasks`
|
||||
method initially to make sure we observe all current values.
|
||||
|
||||
## 12. Filtering tasks
|
||||
|
||||
We are almost done, we can add/update/delete tasks. The only missing feature is
|
||||
the possibility to display the task according to their completed status. We will
|
||||
need to keep track of the state of the filter in `App`, then filter the visible
|
||||
need to keep track of the state of the filter in `Root`, then filter the visible
|
||||
tasks according to its value.
|
||||
|
||||
```js
|
||||
// on top of file, readd useState:
|
||||
const { useRef, useDispatch, useState, useStore } = owl.hooks;
|
||||
|
||||
// in App:
|
||||
filter = useState({value: "all"})
|
||||
|
||||
get displayedTasks() {
|
||||
switch (this.filter.value) {
|
||||
case "active": return this.tasks.filter(t => !t.isCompleted);
|
||||
case "completed": return this.tasks.filter(t => t.isCompleted);
|
||||
case "all": return this.tasks;
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(filter) {
|
||||
this.filter.value = filter;
|
||||
}
|
||||
```
|
||||
|
||||
Finally, we need to display the visible filters. We can do that, and at the
|
||||
same time, display the number of tasks in a small panel below the main list:
|
||||
|
||||
```xml
|
||||
<div class="todo-app">
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
|
||||
<div class="task-list">
|
||||
class Root extends Component {
|
||||
static template = xml /* xml */`
|
||||
<div class="todo-app">
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
|
||||
<div class="task-list">
|
||||
<t t-foreach="displayedTasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="task-panel" t-if="tasks.length">
|
||||
</div>
|
||||
<div class="task-panel" t-if="store.tasks.length">
|
||||
<div class="task-counter">
|
||||
<t t-esc="displayedTasks.length"/>
|
||||
<t t-if="displayedTasks.length lt tasks.length">
|
||||
/ <t t-esc="tasks.length"/>
|
||||
</t>
|
||||
task(s)
|
||||
<t t-esc="displayedTasks.length"/>
|
||||
<t t-if="displayedTasks.length lt store.tasks.length">
|
||||
/ <t t-esc="store.tasks.length"/>
|
||||
</t>
|
||||
task(s)
|
||||
</div>
|
||||
<div>
|
||||
<span t-foreach="['all', 'active', 'completed']"
|
||||
t-as="f" t-key="f"
|
||||
t-att-class="{active: filter.value===f}"
|
||||
t-on-click="setFilter(f)"
|
||||
t-esc="f"/>
|
||||
<span t-foreach="['all', 'active', 'completed']"
|
||||
t-as="f" t-key="f"
|
||||
t-att-class="{active: filter.value===f}"
|
||||
t-on-click="() => this.setFilter(f)"
|
||||
t-esc="f"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
setup() {
|
||||
...
|
||||
this.filter = useState({ value: "all" });
|
||||
}
|
||||
|
||||
get displayedTasks() {
|
||||
const tasks = this.store.tasks;
|
||||
switch (this.filter.value) {
|
||||
case "active": return tasks.filter(t => !t.isCompleted);
|
||||
case "completed": return tasks.filter(t => t.isCompleted);
|
||||
case "all": return tasks;
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(filter) {
|
||||
this.filter.value = filter;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
@@ -757,8 +722,8 @@ same time, display the number of tasks in a small panel below the main list:
|
||||
}
|
||||
```
|
||||
|
||||
Notice here that we set dynamically the class of the filter with the object
|
||||
syntax: each key is a class that we want to set if its value is truthy.
|
||||
Notice here that we set dynamically the css class of the filter with the object
|
||||
syntax.
|
||||
|
||||
## 13. The Final Touch
|
||||
|
||||
@@ -773,16 +738,16 @@ the user experience.
|
||||
}
|
||||
```
|
||||
|
||||
2. Make the title of a task clickable, to toggle its checkbox:
|
||||
2. Make the text of a task clickable, to toggle its checkbox:
|
||||
|
||||
```xml
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted"
|
||||
t-att-id="props.task.id"
|
||||
t-on-click="dispatch('toggleTask', props.task.id)"/>
|
||||
<label t-att-for="props.task.id"><t t-esc="props.task.title"/></label>
|
||||
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
|
||||
```
|
||||
|
||||
3. Strike the title of completed task:
|
||||
3. Strike the text of completed task:
|
||||
|
||||
```css
|
||||
.task.done label {
|
||||
@@ -814,143 +779,145 @@ For reference, here is the final code:
|
||||
|
||||
```js
|
||||
(function () {
|
||||
const { Component, Store } = owl;
|
||||
const { xml } = owl.tags;
|
||||
const { whenReady } = owl.utils;
|
||||
const { useRef, useDispatch, useState, useStore } = owl.hooks;
|
||||
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Store
|
||||
// -------------------------------------------------------------------------
|
||||
const actions = {
|
||||
addTask({ state }, title) {
|
||||
title = title.trim();
|
||||
if (title) {
|
||||
function useStore() {
|
||||
const env = useEnv();
|
||||
return useState(env.store);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// TaskList
|
||||
// -------------------------------------------------------------------------
|
||||
class TaskList {
|
||||
constructor(tasks) {
|
||||
this.tasks = tasks || [];
|
||||
const taskIds = this.tasks.map((t) => t.id);
|
||||
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
|
||||
}
|
||||
|
||||
addTask(text) {
|
||||
text = text.trim();
|
||||
if (text) {
|
||||
const task = {
|
||||
id: state.nextId++,
|
||||
title: title,
|
||||
id: this.nextId++,
|
||||
text: text,
|
||||
isCompleted: false,
|
||||
};
|
||||
state.tasks.push(task);
|
||||
this.tasks.push(task);
|
||||
}
|
||||
},
|
||||
toggleTask({ state }, id) {
|
||||
const task = state.tasks.find((t) => t.id === id);
|
||||
task.isCompleted = !task.isCompleted;
|
||||
},
|
||||
deleteTask({ state }, id) {
|
||||
const index = state.tasks.findIndex((t) => t.id === id);
|
||||
state.tasks.splice(index, 1);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
nextId: 1,
|
||||
tasks: [],
|
||||
};
|
||||
toggleTask(task) {
|
||||
task.isCompleted = !task.isCompleted;
|
||||
}
|
||||
|
||||
deleteTask(task) {
|
||||
const index = this.tasks.findIndex((t) => t.id === task.id);
|
||||
this.tasks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function createTaskStore() {
|
||||
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
|
||||
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
|
||||
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
|
||||
saveTasks();
|
||||
return taskStore;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Task Component
|
||||
// -------------------------------------------------------------------------
|
||||
const TASK_TEMPLATE = xml/* xml */ `
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted"
|
||||
t-att-id="props.task.id"
|
||||
t-on-click="dispatch('toggleTask', props.task.id)"/>
|
||||
<label t-att-for="props.task.id"><t t-esc="props.task.title"/></label>
|
||||
<span class="delete" t-on-click="dispatch('deleteTask', props.task.id)">🗑</span>
|
||||
</div>`;
|
||||
|
||||
class Task extends Component {
|
||||
static template = TASK_TEMPLATE;
|
||||
static template = xml/* xml */ `
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox"
|
||||
t-att-id="props.task.id"
|
||||
t-att-checked="props.task.isCompleted"
|
||||
t-on-click="() => store.toggleTask(props.task)"/>
|
||||
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
|
||||
<span class="delete" t-on-click="() => store.deleteTask(props.task)">🗑</span>
|
||||
</div>`;
|
||||
|
||||
static props = ["task"];
|
||||
dispatch = useDispatch();
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// App Component
|
||||
// Root Component
|
||||
// -------------------------------------------------------------------------
|
||||
const APP_TEMPLATE = xml/* xml */ `
|
||||
<div class="todo-app">
|
||||
class Root extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="todo-app">
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
|
||||
<div class="task-list">
|
||||
<Task t-foreach="displayedTasks" t-as="task" t-key="task.id" task="task"/>
|
||||
<t t-foreach="displayedTasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="task-panel" t-if="tasks.length">
|
||||
<div class="task-counter">
|
||||
<t t-esc="displayedTasks.length"/>
|
||||
<t t-if="displayedTasks.length lt tasks.length">
|
||||
/ <t t-esc="tasks.length"/>
|
||||
</t>
|
||||
task(s)
|
||||
</div>
|
||||
<div>
|
||||
<span t-foreach="['all', 'active', 'completed']"
|
||||
t-as="f" t-key="f"
|
||||
t-att-class="{active: filter.value===f}"
|
||||
t-on-click="setFilter(f)"
|
||||
t-esc="f"/>
|
||||
</div>
|
||||
<div class="task-panel" t-if="store.tasks.length">
|
||||
<div class="task-counter">
|
||||
<t t-esc="displayedTasks.length"/>
|
||||
<t t-if="displayedTasks.length lt store.tasks.length">
|
||||
/ <t t-esc="store.tasks.length"/>
|
||||
</t>
|
||||
task(s)
|
||||
</div>
|
||||
<div>
|
||||
<span t-foreach="['all', 'active', 'completed']"
|
||||
t-as="f" t-key="f"
|
||||
t-att-class="{active: filter.value===f}"
|
||||
t-on-click="() => this.setFilter(f)"
|
||||
t-esc="f"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
class App extends Component {
|
||||
static template = APP_TEMPLATE;
|
||||
</div>`;
|
||||
static components = { Task };
|
||||
|
||||
inputRef = useRef("add-input");
|
||||
tasks = useStore((state) => state.tasks);
|
||||
filter = useState({ value: "all" });
|
||||
dispatch = useDispatch();
|
||||
|
||||
mounted() {
|
||||
this.inputRef.el.focus();
|
||||
setup() {
|
||||
const inputRef = useRef("add-input");
|
||||
onMounted(() => inputRef.el.focus());
|
||||
this.store = useStore();
|
||||
this.filter = useState({ value: "all" });
|
||||
}
|
||||
|
||||
addTask(ev) {
|
||||
// 13 is keycode for ENTER
|
||||
if (ev.keyCode === 13) {
|
||||
this.dispatch("addTask", ev.target.value);
|
||||
this.store.addTask(ev.target.value);
|
||||
ev.target.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
get displayedTasks() {
|
||||
const tasks = this.store.tasks;
|
||||
switch (this.filter.value) {
|
||||
case "active":
|
||||
return this.tasks.filter((t) => !t.isCompleted);
|
||||
return tasks.filter((t) => !t.isCompleted);
|
||||
case "completed":
|
||||
return this.tasks.filter((t) => t.isCompleted);
|
||||
return tasks.filter((t) => t.isCompleted);
|
||||
case "all":
|
||||
return this.tasks;
|
||||
return tasks;
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(filter) {
|
||||
this.filter.value = filter;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Setup code
|
||||
// Setup
|
||||
// -------------------------------------------------------------------------
|
||||
function makeStore() {
|
||||
const localState = window.localStorage.getItem("todoapp");
|
||||
const state = localState ? JSON.parse(localState) : initialState;
|
||||
const store = new Store({ state, actions });
|
||||
store.on("update", null, () => {
|
||||
localStorage.setItem("todoapp", JSON.stringify(store.state));
|
||||
});
|
||||
return store;
|
||||
}
|
||||
|
||||
function setup() {
|
||||
owl.config.mode = "dev";
|
||||
App.env.store = makeStore();
|
||||
const app = new App();
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
whenReady(setup);
|
||||
const env = { store: createTaskStore() };
|
||||
mount(Root, document.body, { dev: true, env });
|
||||
})();
|
||||
```
|
||||
|
||||
|
||||
@@ -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).
|
||||
@@ -4,7 +4,7 @@ OWL, React and Vue have the same main feature: they allow developers to build
|
||||
declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences.
|
||||
|
||||
In this page, we try to highlight some of these differences. Obviously, a lot of
|
||||
effort was done to be fair. However, if you disagree with some of the points
|
||||
effort was put to be fair. However, if you disagree with some of the points
|
||||
discussed, feel free to open an issue/submit a PR to correct this text.
|
||||
|
||||
## Content
|
||||
@@ -14,8 +14,7 @@ discussed, feel free to open an issue/submit a PR to correct this text.
|
||||
- [Tooling/Build Step](#toolingbuild-step)
|
||||
- [Templating](#templating)
|
||||
- [Asynchronous rendering](#asynchronous-rendering)
|
||||
- [Reactiveness](#reactiveness)
|
||||
- [State Management](#state-management)
|
||||
- [Reactivity](#reactivity)
|
||||
- [Hooks](#hooks)
|
||||
|
||||
## Size
|
||||
@@ -47,7 +46,7 @@ components are fast enough for all our usecases, and making it as simple as
|
||||
possible for developers is more valuable (for us).
|
||||
|
||||
Also, functions or class based components are more than just syntax. Functions
|
||||
comes with a mindset of composition and class are about inheritance. Clearly,
|
||||
come with a mindset of composition and class are about inheritance. Clearly,
|
||||
both of these are important mechanisms for reusing code. Also, one does not
|
||||
exclude the other.
|
||||
|
||||
@@ -79,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.
|
||||
@@ -127,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 {
|
||||
@@ -173,7 +172,7 @@ more convoluted. For example, in Vue, you need to use a dynamic import keyword
|
||||
that needs to be transpiled at build time in order for the component to be loaded
|
||||
asynchronously (see [the documentation](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components)).
|
||||
|
||||
## Reactiveness
|
||||
## Reactivity
|
||||
|
||||
React has a simple model: whenever the state changes, it is
|
||||
replaced with a new state (via the `setState` method). Then, the DOM is patched.
|
||||
@@ -189,95 +188,6 @@ with a `Proxy`, which means that it is totally transparent to the developers.
|
||||
Adding new keys is supported. Once any part of the state has been changed, a
|
||||
rendering is scheduled in the next microtask tick (promise queue).
|
||||
|
||||
## State Management
|
||||
|
||||
Managing the state of an application is a tricky issue. Many solutions have
|
||||
been proposed these last few years. It also depends on the kind of application we
|
||||
are talking about. A small application may not need much more than a simple
|
||||
object to contain its state.
|
||||
|
||||
However, there are some common solutions for React and Vue: redux and vuex.
|
||||
Both of them are a centralized store that own the state, and they dictate how
|
||||
the state can be mutated.
|
||||
|
||||
**Redux**
|
||||
|
||||
In Redux, the state is mutated by reducers. Reducers are functions
|
||||
that modify the state by returning a different object:
|
||||
|
||||
```javascript
|
||||
...
|
||||
switch (action.type) {
|
||||
case ADD_TODO: {
|
||||
const { id, content } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
allIds: [...state.allIds, id],
|
||||
byIds: {
|
||||
...state.byIds,
|
||||
[id]: {
|
||||
content,
|
||||
completed: false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This is a little bit awkward to write, but this allows the component system to
|
||||
check if a part of the state was changed. This is exactly what is done by the
|
||||
`connect` function: it create a _connected_ component, which is subscribed to
|
||||
the state and triggers a rerender if some part of the state was modified.
|
||||
|
||||
**VueX**
|
||||
|
||||
VueX is based on a different principle: the state is mutated through
|
||||
some special functions (the mutations), which modify the state in place:
|
||||
|
||||
```javascript
|
||||
function ({state}, payload) {
|
||||
const { id, content } = payload;
|
||||
const message = {id, content, completed: false};
|
||||
state.messages.push(message)
|
||||
}
|
||||
```
|
||||
|
||||
This is simpler, but there is a little bit more happening behind the scene:
|
||||
each key from the state is silently replaced by getters and setters, and VueX
|
||||
keeps track of who get data, and retrigger a render when it was changed.
|
||||
|
||||
**Owl**
|
||||
|
||||
Owl store is a little bit like a mix of redux and vuex: it has actions (but not
|
||||
mutations), and like VueX, it keeps track of the state changes. However, it does
|
||||
not notify a component when the state changes. Instead, components need to connect
|
||||
to the store like in redux, with the `useStore` hook (see the [store documentation](../reference/store.md#connecting-a-component)).
|
||||
|
||||
```javascript
|
||||
const actions = {
|
||||
increment({ state }, val) {
|
||||
state.counter.value += val;
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
counter: { value: 0 },
|
||||
};
|
||||
const store = new owl.Store({ state, actions });
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-name="Counter" t-on-click="dispatch('increment')">
|
||||
Click Me! [<t t-esc="counter.value"/>]
|
||||
</button>`;
|
||||
counter = useStore((state) => state.counter);
|
||||
dispatch = useDispatch();
|
||||
}
|
||||
|
||||
Counter.env.store = store;
|
||||
const counter = new Counter();
|
||||
```
|
||||
|
||||
## Hooks
|
||||
|
||||
[Hooks](https://reactjs.org/docs/hooks-intro.html#motivation) recently took over
|
||||
@@ -342,4 +252,4 @@ class Example extends Component {
|
||||
|
||||
Since the Owl framework had hooks from early in its life, its main APIs
|
||||
are designed to be interacted with hooks from the start. For example, the
|
||||
`Context` and `Store` abstractions.
|
||||
`Context` abstraction.
|
||||
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -61,14 +61,14 @@ because a lot of the state is hidden in their internals.
|
||||
|
||||
React or Vue have a huge community, and a lot of effort have been made into their
|
||||
tooling. This is wonderful, but at the same time, a pretty big issue for Odoo:
|
||||
since the assets are totally dynamic (and could change whenever the user install
|
||||
or remove an addon), we need to have all that kind of tooling on the production
|
||||
since the assets are totally dynamic (and could change whenever the user installs
|
||||
or removes an addon), we need to have all that kind of tooling on the production
|
||||
servers. This is certainly not ideal.
|
||||
|
||||
Also, this makes it very complicated to setup Vue or React tools: Odoo code is
|
||||
not a simple file that import other files. It changes all the time, assets
|
||||
are bundled differently in different contexts. This is the reason why Odoo has
|
||||
its own module system, which are resolve at runtime, by the browser. The
|
||||
its own module system, which are resolved at runtime, by the browser. The
|
||||
dynamic nature of Odoo means that we often need to delay work as late as possible
|
||||
(in other word, we want a JIT user interface!)
|
||||
|
||||
@@ -78,12 +78,12 @@ deploy. Using React without JSX, or Vue without vue file is not very appealing.
|
||||
At the same time, Owl is designed to solve this issue: it compiles templates
|
||||
by the browser, it doesn't need much code for that, since we use the XML parser
|
||||
built into each browser. Owl works with or without any additional tooling. It
|
||||
can use template strings to write single file component, and is easy to integrate
|
||||
can use template strings to write single file components, and is easy to integrate
|
||||
in any html page, with a simple `<script>` tag.
|
||||
|
||||
## Template based
|
||||
|
||||
Odoo stores template as XML document in a database. This is very powerful, since
|
||||
Odoo stores templates as XML documents in a database. This is very powerful, since
|
||||
this allow the use of xpaths to customize other templates. This is a very
|
||||
important feature of odoo, and one of the key to Odoo modularity.
|
||||
|
||||
@@ -104,12 +104,12 @@ awkward, and very confusing.
|
||||
## Developer Experience
|
||||
|
||||
This brings us to the following point: developer experience. We see this choice
|
||||
as an investment for the future, and we want to make onboarding developer as
|
||||
as an investment for the future, and we want to make onboarding developers as
|
||||
easy as possible.
|
||||
|
||||
While many javascript professionals clearly think that react/vue is not difficult
|
||||
(which is true to some extent), it is alsy true that many non js specialists are
|
||||
overwhelmed with the frontend world: functional component, hooks, and many other
|
||||
overwhelmed with the frontend world: functional components, hooks, and many other
|
||||
fancy words. Also, what is available in the compilation context may be difficult,
|
||||
there is a lot of black magic going on in pretty much every framework. Vue
|
||||
somehow join various namespaces into one, under the hood, and add various internal
|
||||
@@ -135,7 +135,7 @@ needs: Odoo will fetch templates from the database and need to compile them only
|
||||
at the last possible moment, so we can apply all necessary xpaths.
|
||||
|
||||
Even more: Odoo needs to be able to generate (and compile) templates at runtime.
|
||||
Currently, Odoo form views interpret a xml description. But the form view code
|
||||
Currently, Odoo form views interpret an xml description. But the form view code
|
||||
then needs to do a lot of complicated operations. With Owl, we will be able to
|
||||
transform a view description into a QWeb template, then compile that and use it
|
||||
immediately.
|
||||
@@ -147,16 +147,16 @@ For example, the reactivity system. We like the way Vue did it, but it has a
|
||||
flaw: it is not really optional. There is actually a way to opt out of the reactivity
|
||||
system by freezing the state, but then, it is freezed.
|
||||
|
||||
And there certainly are situations where we need a state, which is not readonly,
|
||||
And there certainly are situations where we need a state, which is not read-only,
|
||||
and not observed. For example, imagine a spreadsheet component. It may have a
|
||||
very large internal state, and it knows exactly when it needs to be rendered
|
||||
(basically, whenever the user perform some action). Then, observing its state
|
||||
(basically, whenever the user performs some action). Then, observing its state
|
||||
is a net performance loss, both for the CPU and the memory.
|
||||
|
||||
## Concurrency
|
||||
|
||||
Many applications are happy to simply display a spinner whenever a new asynchronous
|
||||
action is performed, but Odoo want a different user experience: most asynchronous
|
||||
action is performed, but Odoo wants a different user experience: most asynchronous
|
||||
state changes are not displayed until ready. This is sometimes called a concurrent
|
||||
mode: the UI is rendered in memory, and displayed only when it is ready (and
|
||||
only if it has not been cancelled by subsequent user actions).
|
||||
@@ -175,6 +175,6 @@ that current standard frameworks are not tailored to our needs. It is perfectly
|
||||
fine, because they each chose a different set of tradeoffs.
|
||||
|
||||
However, we feel that there is still room in the framework world for something
|
||||
that is different. For a framework that make choices compatible with Odoo.
|
||||
that is different. For a framework that makes choices compatible with Odoo.
|
||||
|
||||
And that is why we built Owl 🦉.
|
||||
|
||||
+33
-46
@@ -1,55 +1,42 @@
|
||||
# 🦉 OWL Documentation 🦉
|
||||
# 🦉 Owl overview 🦉
|
||||
|
||||
## Learning Owl
|
||||
Here is a list of everything exported by the Owl library:
|
||||
|
||||
Are you new to Owl? This is the place to start!
|
||||
Main entities:
|
||||
|
||||
- [Tutorial: create a TodoList application](learning/tutorial_todoapp.md)
|
||||
- [Quick Overview](learning/overview.md)
|
||||
- [How to start an Owl project](learning/quick_start.md)
|
||||
- [How to test Components](learning/how_to_test.md)
|
||||
- [How to write Single File Components](learning/how_to_write_sfc.md)
|
||||
- [How to write debug Owl applications](learning/how_to_debug.md)
|
||||
- [`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
|
||||
|
||||
## Reference
|
||||
Lifecycle hooks:
|
||||
|
||||
You will find here a complete reference of every feature, class or object
|
||||
provided by Owl.
|
||||
- [`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
|
||||
|
||||
- [Animations](reference/animations.md)
|
||||
- [Component](reference/component.md)
|
||||
- [Content](reference/content.md)
|
||||
- [Concurrency Model](reference/concurrency_model.md)
|
||||
- [Configuration](reference/config.md)
|
||||
- [Context](reference/context.md)
|
||||
- [Environment](reference/environment.md)
|
||||
- [Event Bus](reference/event_bus.md)
|
||||
- [Event Handling](reference/event_handling.md)
|
||||
- [Error Handling](reference/error_handling.md)
|
||||
- [Hooks](reference/hooks.md)
|
||||
- [Miscellaneous Components](reference/misc.md)
|
||||
- [Observer](reference/observer.md)
|
||||
- [Props](reference/props.md)
|
||||
- [Props Validation](reference/props_validation.md)
|
||||
- [QWeb Templating Language](reference/qweb_templating_language.md)
|
||||
- [QWeb Engine](reference/qweb_engine.md)
|
||||
- [Router](reference/router.md)
|
||||
- [Store](reference/store.md)
|
||||
- [Slots](reference/slots.md)
|
||||
- [Tags](reference/tags.md)
|
||||
- [Utils](reference/utils.md)
|
||||
Other hooks:
|
||||
|
||||
## Other Topics
|
||||
- [`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
|
||||
|
||||
This section provides miscellaneous document that explains some topics
|
||||
which cannot be considered either a tutorial, or reference documentation.
|
||||
Utility/helpers:
|
||||
|
||||
- [Owl architecture: the Virtual DOM](miscellaneous/vdom.md)
|
||||
- [Owl architecture: the rendering pipeline](miscellaneous/rendering.md)
|
||||
- [Comparison with React/Vue](miscellaneous/comparison.md)
|
||||
- [Why did Odoo built Owl?](miscellaneous/why_owl.md)
|
||||
|
||||
---
|
||||
|
||||
Found an issue in the documentation? A broken link? Some outdated information?
|
||||
Please open an issue or submit a PR!
|
||||
- [`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
|
||||
|
||||
@@ -1,95 +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),
|
||||
- the css class `name-enter-active` will be removed whenever a css transition
|
||||
ends.
|
||||
|
||||
At node destruction:
|
||||
|
||||
- the css classes `name-leave` and `name-leave-active` will be added before the
|
||||
node is removed to the DOM,
|
||||
- the css class `name-leave` will be removed on the next animation frame (so it
|
||||
can be used to trigger css transition effects),
|
||||
- the css class `name-leave-active` will be removed whenever a css transition
|
||||
ends. Only then will the element be removed from the DOM.
|
||||
|
||||
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).
|
||||
@@ -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 });
|
||||
})();
|
||||
```
|
||||
+273
-643
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -158,26 +158,6 @@ There are two different common problems with Owl asynchronous rendering model:
|
||||
Here are a few tips on how to work with asynchronous components:
|
||||
|
||||
1. Minimize the use of asynchronous components!
|
||||
2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
|
||||
synchronous renderings
|
||||
3. Lazy loading external libraries is a good use case for async rendering. This
|
||||
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))
|
||||
4. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When
|
||||
this component is met, a new rendering
|
||||
sub tree is created, such that the rendering of that component (and its
|
||||
children) is not tied to the rendering of the rest of the interface. It can
|
||||
be used on an asynchronous component, to prevent it from delaying the
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
|
||||
```
|
||||
Component misc
|
||||
Context AsyncRoot
|
||||
QWeb Portal
|
||||
Store router
|
||||
useState Link
|
||||
config RouteComponent
|
||||
mode Router
|
||||
core tags
|
||||
EventBus css
|
||||
Observer xml
|
||||
hooks utils
|
||||
onWillStart debounce
|
||||
onMounted escape
|
||||
onWillUpdateProps loadJS
|
||||
onWillPatch loadFile
|
||||
onPatched shallowEqual
|
||||
onWillUnmount whenReady
|
||||
useContext
|
||||
useState
|
||||
useRef
|
||||
useSubEnv
|
||||
useStore
|
||||
useDispatch
|
||||
useGetters
|
||||
```
|
||||
|
||||
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
|
||||
@@ -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,55 +22,30 @@ 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
|
||||
App.env = {
|
||||
const env = {
|
||||
_t: myTranslateFunction,
|
||||
user: {...},
|
||||
services: {
|
||||
...
|
||||
},
|
||||
};
|
||||
const app = new App();
|
||||
app.mount(document.body);
|
||||
|
||||
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,
|
||||
@@ -80,72 +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() {
|
||||
App.env = await myEnv();
|
||||
const app = new App();
|
||||
await app.mount(document.body);
|
||||
}
|
||||
```
|
||||
|
||||
## 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. This is particularly useful when one want
|
||||
to test more advanced components, and be able to mock those methods.
|
||||
|
||||
More specifically, the `browser` object contains the following methods and objects:
|
||||
|
||||
- `setTimeout`
|
||||
- `clearTimeout`
|
||||
- `setInterval`
|
||||
- `clearInterval`
|
||||
- `requestAnimationFrame`
|
||||
- `random`
|
||||
- `Date`
|
||||
- `fetch`
|
||||
- `localStorage`
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -23,5 +23,3 @@ Its API is:
|
||||
| `off(eventType, owner)` | remove all listeners for an owner |
|
||||
| `trigger(eventType, ...args)` | trigger an event |
|
||||
| `clear` | remove all subscriptions |
|
||||
|
||||
Note that the [`Store`](store.md) is an example of an `EventBus`.
|
||||
|
||||
@@ -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>
|
||||
```
|
||||
|
||||
+147
-281
@@ -3,25 +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)
|
||||
- [`useStore`](#usestore)
|
||||
- [`useDispatch`](#usedispatch)
|
||||
- [`useGetters`](#usegetters)
|
||||
- [Making customized hooks](#making-customized-hooks)
|
||||
- [`useComponent`](#usecomponent)
|
||||
- [`useEnv`](#useenv)
|
||||
- [`useEffect`](#useeffect)
|
||||
- [Example: Mouse Position](#example-mouse-position)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -40,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 (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
|
||||
@@ -139,8 +44,7 @@ class SomeComponent extends Component {
|
||||
|
||||
// also ok
|
||||
class SomeComponent extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
setup() {
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
}
|
||||
@@ -153,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`
|
||||
|
||||
@@ -172,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"/>]
|
||||
@@ -191,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
|
||||
@@ -337,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 });
|
||||
}
|
||||
@@ -366,82 +188,126 @@ to be closed:
|
||||
useExternalListener(window, "click", this.closeMenu);
|
||||
```
|
||||
|
||||
### `useStore`
|
||||
### `useComponent`
|
||||
|
||||
The `useStore` hook is the entry point for a component to connect to the store.
|
||||
See the [store documentation](store.md) for more information.
|
||||
The `useComponent` hook is useful as a building block for some customized hooks,
|
||||
that may need a reference to the component calling them.
|
||||
|
||||
### `useDispatch`
|
||||
```js
|
||||
function useSomething() {
|
||||
const component = useComponent();
|
||||
// now, component is bound to the instance of the current component
|
||||
}
|
||||
```
|
||||
|
||||
The `useDispatch` hook is the way for components to get a reference to the store
|
||||
`dispatch` function. See the [store documentation](store.md) for more information.
|
||||
### `useEnv`
|
||||
|
||||
### `useGetters`
|
||||
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.
|
||||
|
||||
The `useGetters` hook is the way for components to get a reference to the store
|
||||
getters. See the [store documentation](store.md) for more information.
|
||||
```js
|
||||
function useSomething() {
|
||||
const env = useEnv();
|
||||
// now, env is bound to the env of the current component
|
||||
}
|
||||
```
|
||||
|
||||
### Making customized hooks
|
||||
### `useEffect`
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
But, like every good things in life, hooks should be used with moderation. They are
|
||||
not the solution to every problem.
|
||||
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.
|
||||
|
||||
- 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:
|
||||
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.
|
||||
|
||||
```js
|
||||
// maybe overkill
|
||||
class A extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
useMySpecificHook();
|
||||
}
|
||||
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() {
|
||||
return Component.current.env.router;
|
||||
}
|
||||
```
|
||||
|
||||
This means that we give control to the application developer to create the
|
||||
router, which is good, so they can set it up, subclass it, ... And then, to
|
||||
test our components, we can just add a mock router in the environment.
|
||||
|
||||
Note: the code above makes use of the `Component.current` property. This is the
|
||||
way hooks are able to get a reference to the component currently being created.
|
||||
Note that we use the prefix `use` for hooks, just like in React. This is just
|
||||
a convention.
|
||||
|
||||
@@ -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.
|
||||
@@ -1,134 +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 } = owl;
|
||||
const { Portal } = owl.misc;
|
||||
|
||||
class TeleportedComponent extends Component {}
|
||||
class App extends Component {
|
||||
static components = { Portal, TeleportedComponent };
|
||||
}
|
||||
|
||||
const app = new App();
|
||||
app.mount(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.
|
||||
@@ -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.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.Observer();
|
||||
const obj = observer.observe({ a: { b: 1 } });
|
||||
|
||||
observer.deepRevNumber(obj.a); // 1
|
||||
obj.a.b = 2;
|
||||
|
||||
observer.deepRevNumber(obj.a); // 2
|
||||
```
|
||||
|
||||
The `deepRevNumber` can also return 0, which indicates that the value is not
|
||||
observed.
|
||||
@@ -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.
|
||||
+187
-23
@@ -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
|
||||
|
||||
@@ -18,7 +21,7 @@ class Child extends Component {
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><ComponentA a="state.a" b="'string'"/></div>`;
|
||||
static template = xml`<div><Child a="state.a" b="'string'"/></div>`;
|
||||
static components = { Child };
|
||||
state = useState({ a: "fromparent" });
|
||||
}
|
||||
@@ -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 } };
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
};
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,183 +0,0 @@
|
||||
# 🦉 Router 🦉
|
||||
|
||||
Remove?
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [Route Definition](#route-definition)
|
||||
- [Router](#router)
|
||||
- [Navigation Guards](#navigation-guards)
|
||||
- [RouteComponent](#routecomponent)
|
||||
- [Link](#link)
|
||||
|
||||
## Overview
|
||||
|
||||
It is often useful to organize an application around urls. If the application is
|
||||
a single page application, then we need a way to manage those urls in the browser.
|
||||
This is why there are many different routers for different frameworks. A generic
|
||||
router can do the job just fine, but a specialized router for Owl can give a
|
||||
better developer experience.
|
||||
|
||||
The Owl router support the following features:
|
||||
|
||||
- `history` or `hash` mode
|
||||
- declarative routes
|
||||
- route redirection
|
||||
- navigation guards
|
||||
- parameterized routes
|
||||
- a `<Link/>` component
|
||||
- a `<RouteComponent/>` component
|
||||
|
||||
Note that it is still in early stage of developments, and there are probably
|
||||
still some issues.
|
||||
|
||||
## Example
|
||||
|
||||
To use the Owl router, there are some steps that needs to be done:
|
||||
|
||||
- declare some routes
|
||||
- create a router
|
||||
- add it to the environment
|
||||
|
||||
```js
|
||||
async function protectRoute({ env, to }) {
|
||||
if (!env.session.authUser) {
|
||||
env.session.setNextRoute(to.name);
|
||||
return { to: "SIGN_IN" };
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export const ROUTES = [
|
||||
{ name: "LANDING", path: "/", component: Landing },
|
||||
{ name: "TASK", path: "/tasks/{{id}}", component: Task },
|
||||
{ name: "SIGN_UP", path: "/signup", component: SignUp },
|
||||
{ name: "SIGN_IN", path: "/signin", component: SignIn },
|
||||
{ name: "ADMIN", path: "/admin", component: Admin, beforeRouteEnter: protectRoute },
|
||||
{ name: "ACCOUNT", path: "/account", component: Account, beforeRouteEnter: protectRoute },
|
||||
{ name: "UNKNOWN", path: "*", redirect: { to: "LANDING" } }
|
||||
];
|
||||
|
||||
function makeEnvironment() {
|
||||
...
|
||||
const env = { qweb };
|
||||
env.session = new Session(env);
|
||||
env.router = new owl.router.Router(env, ROUTES);
|
||||
await env.router.start();
|
||||
return env;
|
||||
}
|
||||
|
||||
App.env = makeEnvironment();
|
||||
// create root component here
|
||||
```
|
||||
|
||||
Notice that the router needs to be started. This is an asynchronous operation
|
||||
because it needs to apply the potential navigation guards on the current route
|
||||
(which may or may not mean that the application is redirected to another route).
|
||||
|
||||
## Reference
|
||||
|
||||
### Route definition
|
||||
|
||||
A route need to be defined as an object with the following keys:
|
||||
|
||||
- `name` (optional): a (unique) string useful to identify the current route. If not
|
||||
given, it will be assigned an automatic name,
|
||||
- `path`: a string describing the url. It can be static: `/admin` or dynamic: `/users/{{id}}`.
|
||||
It also can be `*`, to catch all remaining routes.
|
||||
- `component` (optional): an Owl component that will be used by the `t-routecomponent`
|
||||
directive if the route is active
|
||||
- `redirect` (optional): should be destination object (with optional keys `path`, `to` and `params`) if given, the application will be redirected to the destination whenever we match this route
|
||||
- `beforeRouteEnter`: defines a [navigation guard](#navigation-guards).
|
||||
|
||||
### `Router`
|
||||
|
||||
The `Router` constructor takes three arguments:
|
||||
|
||||
- `env`: a valid environment,
|
||||
- a list of routes,
|
||||
- an optional object (with the only key `mode` which can be `history` (default
|
||||
value) or `hash`).
|
||||
|
||||
`history` will use the browser [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) as the mechanism to manage URL.\
|
||||
Example: `https://yourdomain.tld/my_custom_route`.\
|
||||
For this mechanism to work, you need a way to configure your web server accordingly.
|
||||
|
||||
`hash` will manipulate the hash of the URL.\
|
||||
Example: `https://yourdomain.tld/index.html#/my_custom_route`.
|
||||
|
||||
```js
|
||||
const ROUTES = [...];
|
||||
const router = new owl.router.Router(env, ROUTES, {mode: 'history'});
|
||||
```
|
||||
|
||||
Note that the route are defined in a list, and the order matters: the router
|
||||
tries to find a match by going down the list.
|
||||
|
||||
The router needs to be added to the environment in the `router` sub key.
|
||||
|
||||
Once a router is created, it needs to be started. This is necessary to initialize
|
||||
its current state to the current URL (and also, to potentially apply any
|
||||
navigation guards and/or redirecting).
|
||||
|
||||
```js
|
||||
await router.start();
|
||||
```
|
||||
|
||||
Once started, the router will keep track of the current url and reflect its
|
||||
value in two keys:
|
||||
|
||||
- `router.currentRoute`
|
||||
- `router.currentParams`
|
||||
|
||||
The router also has a `navigate` method, useful to programmatically change the
|
||||
application to another state (and the url):
|
||||
|
||||
```js
|
||||
router.navigate({ to: "USER", params: { id: 51 } });
|
||||
```
|
||||
|
||||
### Navigation Guards
|
||||
|
||||
Navigation guards are very useful to be able to execute some business logic/
|
||||
perform some actions or redirect to other routes whenever the application is
|
||||
entering a new route. For example, the following guard checks if there is an
|
||||
authenticated user, and if it is not the case, redirect to the sign in route.
|
||||
|
||||
```js
|
||||
async function protectRoute({ env, to }) {
|
||||
if (!env.session.authUser) {
|
||||
env.session.setNextRoute(to.name);
|
||||
return { to: "SIGN_IN" };
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
A navigation guard is a function that returns a promise, which either resolves
|
||||
to `true` (the navigation is accepted), or to another destination object.
|
||||
|
||||
### `RouteComponent`
|
||||
|
||||
The `RouteComponent` component directs Owl to render the component associated
|
||||
to the currently active route (if any):
|
||||
|
||||
```xml
|
||||
<div t-name="App">
|
||||
<NavBar />
|
||||
<RouteComponent />
|
||||
</div>
|
||||
```
|
||||
|
||||
### `Link`
|
||||
|
||||
The `Link` component is a Owl component which render as a `<a>` tag with any
|
||||
content. It will compute the proper href from its props, and allow Owl to
|
||||
properly navigate to a given url if clicked on it.
|
||||
|
||||
```xml
|
||||
<Link to="'HOME'">Home</Link>
|
||||
```
|
||||
+189
-47
@@ -3,66 +3,91 @@
|
||||
## 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.
|
||||
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
|
||||
The first element inside the component which is not a named slot will
|
||||
be considered the `default` slot. For example:
|
||||
|
||||
```xml
|
||||
@@ -77,7 +102,21 @@ be considered the `default` slot. For example:
|
||||
</div>
|
||||
```
|
||||
|
||||
Default content: slots can define a default content, in case the parent did not define them:
|
||||
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:
|
||||
|
||||
```xml
|
||||
<div t-name="Parent">
|
||||
@@ -90,7 +129,110 @@ Default content: slots can define a default content, in case the parent did not
|
||||
<!-- 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
|
||||
|
||||
The `t-slot` directive is actually able to use any expressions, using string
|
||||
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>
|
||||
```
|
||||
|
||||
@@ -1,349 +0,0 @@
|
||||
# 🦉 Store 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [Store](#store)
|
||||
- [Actions](#actions)
|
||||
- [Getters](#getters)
|
||||
- [Connecting a Component](#connecting-a-component)
|
||||
- [`useStore`](#usestore)
|
||||
- [`useDispatch`](#usedispatch)
|
||||
- [`useGetters`](#usegetters)
|
||||
- [Semantics](#semantics)
|
||||
- [Good Practices](#good-practices)
|
||||
|
||||
## Overview
|
||||
|
||||
Managing the state in an application is not an easy task. In some cases, the
|
||||
state of an application can be part of the component tree, in a natural way.
|
||||
However, there are situations where some parts of the state need to be displayed
|
||||
in various parts of the user interface, and then, it is not obvious which
|
||||
component should own which part of the state.
|
||||
|
||||
Owl's solution to this issue is a centralized store. It is a class that owns
|
||||
some (or all) state, and lets the developer update it in a structured way, with
|
||||
`actions`. Owl components can then connect to the store to read their relevant
|
||||
state, and they will be rerendered if the state is updated.
|
||||
|
||||
Note: Owl store is inspired by React Redux and VueX.
|
||||
|
||||
## Example
|
||||
|
||||
Here is what a simple store looks like:
|
||||
|
||||
```js
|
||||
const actions = {
|
||||
addTodo({ state }, message) {
|
||||
state.todos.push({
|
||||
id: state.nextId++,
|
||||
message,
|
||||
isCompleted: false,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
todos: [],
|
||||
nextId: 1,
|
||||
};
|
||||
|
||||
const store = new owl.Store({ state, actions });
|
||||
store.on("update", null, () => console.log(store.state));
|
||||
|
||||
// updating the state
|
||||
store.dispatch("addTodo", "fix all bugs");
|
||||
```
|
||||
|
||||
This example shows how a store can be defined and used. Note that in most cases,
|
||||
actions will be dispatched by connected components.
|
||||
|
||||
## Reference
|
||||
|
||||
### `Store`
|
||||
|
||||
The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events
|
||||
whenever its state is changed. Note that these events are triggered only after a
|
||||
microtask tick, so only one event will be triggered for any number of state changes in a
|
||||
call stack.
|
||||
|
||||
Also, it is important to mention that the state is observed (with an `owl.Observer`),
|
||||
which is the reason why it is able to know if it was changed. See the
|
||||
[Observer](observer.md)'s documentation for more details.
|
||||
|
||||
The `Store` class is quite small. It has two public methods:
|
||||
|
||||
- its constructor
|
||||
- `dispatch`
|
||||
|
||||
The constructor takes a configuration object with four (optional) keys:
|
||||
|
||||
- the initial state
|
||||
- the actions
|
||||
- the getters
|
||||
- the environment
|
||||
|
||||
```javascript
|
||||
const config = {
|
||||
state,
|
||||
actions,
|
||||
getters,
|
||||
env,
|
||||
};
|
||||
const store = new Store(config);
|
||||
```
|
||||
|
||||
### Actions
|
||||
|
||||
Actions are used to coordinate state changes. It can be used for both synchronous
|
||||
and asynchronous logic.
|
||||
|
||||
```js
|
||||
const actions = {
|
||||
async login({ state }, info) {
|
||||
state.loginState = "pending";
|
||||
try {
|
||||
const loginInfo = await doSomeRPC("/login/", info);
|
||||
state.loginState = loginInfo;
|
||||
} catch (e) {
|
||||
state.loginState = "error";
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The first argument to an action method is an object with four keys:
|
||||
|
||||
- `state`: the current state of the store content,
|
||||
- `dispatch`: a function that can be used to dispatch other actions,
|
||||
- `getters`: an object containing all getters defined in the store,
|
||||
- `env`: the current environment. This is useful sometimes, in particular if
|
||||
an action needs to apply some side effects (such as performing an rpc), and
|
||||
the `rpc` method is located in the environment.
|
||||
|
||||
Actions are called with the `dispatch` method on the store, and can receive an
|
||||
arbitrary number of arguments.
|
||||
|
||||
```js
|
||||
store.dispatch("login", someInfo);
|
||||
```
|
||||
|
||||
Note that anything returned by an action will also be returned by the `dispatch`
|
||||
call.
|
||||
|
||||
Also, it is important to be aware that we need to be careful with asynchronous
|
||||
logic. Each state change will potentially trigger a rerendering, so we need to
|
||||
make sure that we do not have a partially corrupted state. Here is an example that
|
||||
is likely not a good idea:
|
||||
|
||||
```javascript
|
||||
const actions = {
|
||||
async fetchSomeData({ state }, recordId) {
|
||||
state.recordId = recordId;
|
||||
const data = await doSomeRPC("/read/", recordId);
|
||||
state.recordData = data;
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
In the previous example, there is a period of time in which the state has a
|
||||
`recordId` which does not correspond to the `recordData`. It is more likely that
|
||||
we want an atomic update: updating the `recordId` at the same time as the `recordData`
|
||||
values:
|
||||
|
||||
```javascript
|
||||
const actions = {
|
||||
async fetchSomeData({ state }, recordId) {
|
||||
const data = await doSomeRPC("/read/", recordId);
|
||||
state.recordId = recordId;
|
||||
state.recordData = data;
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Getters
|
||||
|
||||
Usually, data contained in the store will be stored in a normalized way. For
|
||||
example,
|
||||
|
||||
```js
|
||||
{
|
||||
posts: [{id: 11, authorId: 4, content: 'Greetings'}],
|
||||
authors: [{id: 4, name: 'John'}]
|
||||
}
|
||||
```
|
||||
|
||||
However, the user interface will probably need some denormalized data like
|
||||
|
||||
```js
|
||||
{id: 11, author: {id: 4, name: 'John'}, content: 'Greetings'}
|
||||
```
|
||||
|
||||
This is what `getters` are for: they give a centralized way to process and
|
||||
transform the data contained in the store.
|
||||
|
||||
```js
|
||||
const getters = {
|
||||
getPost({ state }, id) {
|
||||
const post = state.posts.find((p) => p.id === id);
|
||||
const author = state.authors.find((a) => a.id === post.id);
|
||||
return {
|
||||
id,
|
||||
author,
|
||||
content: post.content,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// somewhere else
|
||||
const post = store.getters.getPost(id);
|
||||
```
|
||||
|
||||
Getters take _at most_ one argument.
|
||||
|
||||
Note that getters are not cached.
|
||||
|
||||
### Connecting a Component
|
||||
|
||||
At some point, we need a way to interact with the store from a component. This
|
||||
means that the component needs a reference to the store. By default, it looks
|
||||
for it in the `env.store` key. However, this can be configured with the `useStore`
|
||||
hook.
|
||||
|
||||
Every component-store interactions are done with the help of the three store hooks:
|
||||
|
||||
- [`useStore`](#usestore) to subscribe a component to some part of the store state,
|
||||
- [`useDispatch`](#usedispatch) to get a reference to a dispatch function,
|
||||
- [`useGetters`](#usegetters) to get a reference to the getters defined in the store.
|
||||
|
||||
Assume we have this store:
|
||||
|
||||
```javascript
|
||||
const actions = {
|
||||
increment({ state }, val) {
|
||||
state.counter.value += val;
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
counter: { value: 0 },
|
||||
};
|
||||
const store = new owl.Store({ state, actions });
|
||||
```
|
||||
|
||||
To make it accessible to the complete application, we will put it in the
|
||||
environment:
|
||||
|
||||
```js
|
||||
// in this example, the root component is App
|
||||
App.env.store = store;
|
||||
```
|
||||
|
||||
A counter component can then select this value and dispatch an action like this:
|
||||
|
||||
```js
|
||||
class Counter extends Component {
|
||||
counter = useStore((state) => state.counter);
|
||||
dispatch = useDispatch();
|
||||
}
|
||||
|
||||
const counter = new Counter({ store, qweb });
|
||||
```
|
||||
|
||||
```xml
|
||||
<button t-name="Counter" t-on-click="dispatch('increment')">
|
||||
Click Me! [<t t-esc="counter.value"/>]
|
||||
</button>
|
||||
```
|
||||
|
||||
### `useStore`
|
||||
|
||||
The `useStore` hook is used to select some part of the store state. It accepts
|
||||
two arguments:
|
||||
|
||||
- a selector function, which takes the store state as first argument (and the
|
||||
component props as second argument) and which must return the part of the
|
||||
store state that will be made available and observed for changes,
|
||||
- optionally, an object which can have the following optional keys:
|
||||
- a `store` key containing a store object if we want to use another store than
|
||||
the default store,
|
||||
- an `isEqual` key containing an equality function if we want to specialize
|
||||
the comparison (the function must accept two arguments: the previous result
|
||||
and the new result, and must return whether they are equal),
|
||||
- and an `onUpdate` key containing an update function if we want to execute an
|
||||
arbitrary code every time the selected state changes (the function will
|
||||
receive one argument, the new result, and can execute arbitrary code).
|
||||
|
||||
If the `useStore` selector returns a sub part of the store state, the component
|
||||
will only be rerendered whenever this part of the state changes. Otherwise, it
|
||||
will perform a strict equality check (unless the `isEqual` option is defined,
|
||||
then it will call it) and will update the component every time this check fails.
|
||||
|
||||
Note that if the selector function returns a primitive type, the result of
|
||||
`useStore` will be immutable and it will not react to changes. In this case, it
|
||||
is important to define the `onUpdate` option to properly update the value
|
||||
manually when it changes.
|
||||
|
||||
Also, the return value from `useStore` is not supposed to be modified. The store
|
||||
state should only be updated with actions.
|
||||
|
||||
### `useDispatch`
|
||||
|
||||
The `useDispatch` hook is useful when a component needs to be able to dispatch
|
||||
actions. It takes an optional argument, which is a store. If not given, it will
|
||||
use the store in the environment.
|
||||
|
||||
Note that a component does not need to be connected in any other way to the store.
|
||||
For example:
|
||||
|
||||
```js
|
||||
class DoSomethingButton extends Component {
|
||||
static template = xml`<button t-on-click="dispatch('something')">Click</button>`;
|
||||
dispatch = useDispatch();
|
||||
}
|
||||
```
|
||||
|
||||
### `useGetters`
|
||||
|
||||
The `useGetters` hook is useful when a component needs to be able to use the
|
||||
getters defined in a store. It takes an optional argument, which is a store. If
|
||||
not given, it will use the store in the environment.
|
||||
|
||||
Note that a component does not need to be connected in any other way to the store.
|
||||
For example:
|
||||
|
||||
```js
|
||||
class InfoButton extends Component {
|
||||
static template = xml`<span><t t-esc="getters.somevalue()"></span>`;
|
||||
getters = useGetters();
|
||||
}
|
||||
```
|
||||
|
||||
### Semantics
|
||||
|
||||
The `Store` class and the `useStore` hook try to be smart and to optimize as much
|
||||
as possible the rendering and update process. What is important to know is:
|
||||
|
||||
- components are always updated in the order of their creation (so, parent
|
||||
before children),
|
||||
- they are updated only if they are in the DOM,
|
||||
- if a parent is asynchronous, the system will wait for it to complete its
|
||||
update before updating other components,
|
||||
- in general, updates are not coordinated. This is not a problem for synchronous
|
||||
components, but if there are many asynchronous components, this could lead to
|
||||
a situation where some part of the UI is updated and some other part of the UI is
|
||||
not updated.
|
||||
|
||||
### Good Practices
|
||||
|
||||
- avoid asynchronous components as much as possible. Asynchronous components
|
||||
lead to situations where parts of the UI is not updated immediately,
|
||||
- do not be afraid to connect many components, parent or children if needed. For
|
||||
example, a `MessageList` component could get a list of ids in its `useStore`
|
||||
call and a `Message` component could get the data of its own
|
||||
message,
|
||||
- since the `useStore` function is called for each connected component,
|
||||
for each state update, it is important to make sure that these functions are
|
||||
as fast as possible.
|
||||
@@ -1,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 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,30 +1,33 @@
|
||||
# 🦉 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)
|
||||
- [Setting Variables](#setting-variables)
|
||||
- [Conditionals](#conditionals)
|
||||
- [Dynamic Attributes](#dynamic-attributes)
|
||||
- [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>
|
||||
@@ -51,33 +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) |
|
||||
| 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
|
||||
|
||||
@@ -87,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
|
||||
@@ -187,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
|
||||
|
||||
@@ -324,6 +306,40 @@ values) or a pair `[key, value]`. For example:
|
||||
<div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->
|
||||
```
|
||||
|
||||
### Dynamic class attribute
|
||||
|
||||
For convenience, Owl supports a special case for the `t-att-class` case: one can
|
||||
use an object with keys describing the classes, and values boolean value denoting
|
||||
if the class is or is not present:
|
||||
|
||||
```xml
|
||||
<div t-att-class="{'a': true, 'b': true}"/> <!-- result: <div class="a b"></div> -->
|
||||
|
||||
<div t-att-class="{'a b': true, 'c': true}"/> <!-- result: <div class="a b c"></div> -->
|
||||
```
|
||||
|
||||
Note that it can be combined with normal class attribute:
|
||||
|
||||
```xml
|
||||
<div class="a" t-att-class="{'b': true}"/> <!-- result: <div class="a b"></div> -->
|
||||
```
|
||||
|
||||
### Dynamic tag names
|
||||
|
||||
When writing generic components or templates, the specific concrete tag for an
|
||||
HTML element is not known yet. In those situations, the `t-tag` directive is
|
||||
useful. It simply evaluates dynamically an expression to use as a tag name. The
|
||||
template:
|
||||
|
||||
```xml
|
||||
<t t-tag="tag">
|
||||
<span>content</span>
|
||||
</t>
|
||||
```
|
||||
|
||||
will be rendered as `<div><span>content</span></div>` if the `tag` context key
|
||||
is set to `div`.
|
||||
|
||||
### Loops
|
||||
|
||||
QWeb has an iteration directive `t-foreach` which take an expression returning the
|
||||
@@ -331,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>
|
||||
```
|
||||
@@ -347,13 +363,17 @@ will be rendered as:
|
||||
Like conditions, `t-foreach` applies to the element bearing the directive’s 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).
|
||||
|
||||
@@ -379,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 -->
|
||||
@@ -393,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,
|
||||
@@ -464,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
|
||||
@@ -537,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:
|
||||
@@ -574,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.
|
||||
@@ -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
@@ -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 app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
});
|
||||
const { whenReady } = owl;
|
||||
|
||||
await whenReady();
|
||||
// do something
|
||||
```
|
||||
|
||||
or alternatively:
|
||||
|
||||
```js
|
||||
owl.utils.whenReady(function () {
|
||||
const qweb = new owl.QWeb();
|
||||
const app = new App({ qweb });
|
||||
app.mount(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: `"<ok>"`). 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
|
||||
```
|
||||
|
||||
+17
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "1.0.13",
|
||||
"version": "2.0.0-alpha1",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"browser": "dist/owl.iife.js",
|
||||
@@ -10,18 +10,20 @@
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.15.3"
|
||||
"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",
|
||||
"test:watch": "jest --watch",
|
||||
"tools:serve": "python3 tools/server.py || python tools/server.py",
|
||||
"tools": "npm run build && npm run tools:serve",
|
||||
"pretools:watch": "npm run build",
|
||||
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"",
|
||||
"playground:serve": "python3 tools/server.py || python tools/server.py",
|
||||
"playground": "npm run build && npm run playground:serve",
|
||||
"preplayground:watch": "npm run build",
|
||||
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
|
||||
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
|
||||
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check",
|
||||
"publish": "npm run build && npm publish",
|
||||
"release": "node tools/release.js"
|
||||
},
|
||||
@@ -40,20 +42,22 @@
|
||||
"@types/node": "^14.11.8",
|
||||
"chalk": "^3.0.0",
|
||||
"cpx": "^1.5.0",
|
||||
"current-git-branch": "^1.1.0",
|
||||
"git-rev-sync": "^1.12.0",
|
||||
"github-api": "^3.3.0",
|
||||
"jest": "^27.1.0",
|
||||
"jest-diff": "^27.3.1",
|
||||
"jest-environment-jsdom": "^27.1.0",
|
||||
"live-server": "^1.2.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.0.4",
|
||||
"prettier": "2.4.1",
|
||||
"rollup": "^2.56.3",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-typescript2": "^0.30.0",
|
||||
"rollup-plugin-typescript2": "^0.31.1",
|
||||
"sass": "^1.16.1",
|
||||
"source-map-support": "^0.5.10",
|
||||
"ts-jest": "^27.0.5",
|
||||
"typescript": "3.9.6",
|
||||
"typescript": "4.5.2",
|
||||
"uglify-es": "^3.3.9"
|
||||
},
|
||||
"jest": {
|
||||
@@ -62,7 +66,9 @@
|
||||
"<rootDir>/src",
|
||||
"<rootDir>/tests"
|
||||
],
|
||||
"setupFiles": ["./tests/mocks/mockEventTarget.js"],
|
||||
"setupFiles": [
|
||||
"./tests/mocks/mockEventTarget.js"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.ts?$": "ts-jest"
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# 🦉 OWL Roadmap 🦉
|
||||
|
||||
- Current version: 1.0.13
|
||||
- Current version: 1.4.7
|
||||
- Status: stable
|
||||
|
||||
This roadmap is only an attempt at predicting Owl's future. Everything may
|
||||
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import { Component } from "./component/component";
|
||||
import { ComponentNode } from "./component/component_node";
|
||||
import { Scheduler } from "./component/scheduler";
|
||||
import { TemplateSet } from "./qweb/template_helpers";
|
||||
|
||||
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
||||
|
||||
export class App<T extends typeof Component = any> extends TemplateSet {
|
||||
Root: T;
|
||||
props: any;
|
||||
env: any = {};
|
||||
scheduler = new Scheduler(window.requestAnimationFrame.bind(window));
|
||||
root: ComponentNode | null = null;
|
||||
dev: boolean = true;
|
||||
|
||||
constructor(Root: T, props?: any) {
|
||||
super();
|
||||
this.Root = Root;
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
configure(params: any) {
|
||||
if (params.env) {
|
||||
this.env = params.env;
|
||||
}
|
||||
}
|
||||
|
||||
mount(target: HTMLElement): Promise<InstanceType<T>> {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
throw new Error("Cannot mount component: the target is not a valid DOM element");
|
||||
}
|
||||
if (!document.body.contains(target)) {
|
||||
throw new Error("Cannot mount a component on a detached dom node");
|
||||
}
|
||||
const node = new ComponentNode(this.Root, this.props, this);
|
||||
this.root = node;
|
||||
return node.mountComponent(target);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.root) {
|
||||
this.root.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { Component } from "../component/component";
|
||||
import { ComponentNode } from "../component/component_node";
|
||||
import { MountOptions } from "../component/fibers";
|
||||
import { Scheduler } from "../component/scheduler";
|
||||
import { TemplateSet, TemplateSetConfig } from "./template_set";
|
||||
import { nodeErrorHandlers } from "../component/error_handling";
|
||||
|
||||
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
||||
|
||||
export interface Env {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface AppConfig extends TemplateSetConfig {
|
||||
env?: Env;
|
||||
props?: any;
|
||||
}
|
||||
|
||||
export const DEV_MSG = `Owl is running in 'dev' mode.
|
||||
|
||||
This is not suitable for production use.
|
||||
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`;
|
||||
|
||||
export class App<T extends typeof Component = any> extends TemplateSet {
|
||||
Root: T;
|
||||
props: any;
|
||||
env: Env;
|
||||
scheduler = new Scheduler();
|
||||
root: ComponentNode | null = null;
|
||||
|
||||
constructor(Root: T, config: AppConfig = {}) {
|
||||
super(config);
|
||||
this.Root = Root;
|
||||
if (config.dev) {
|
||||
console.info(DEV_MSG);
|
||||
}
|
||||
const descrs = Object.getOwnPropertyDescriptors(config.env || {});
|
||||
this.env = Object.freeze(Object.defineProperties({}, descrs));
|
||||
this.props = config.props || {};
|
||||
}
|
||||
|
||||
mount(target: HTMLElement, options?: MountOptions): Promise<InstanceType<T>> {
|
||||
this.checkTarget(target);
|
||||
const node = this.makeNode(this.Root, this.props);
|
||||
const prom = this.mountNode(node, target, options);
|
||||
this.root = node;
|
||||
return prom;
|
||||
}
|
||||
|
||||
checkTarget(target: HTMLElement) {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
throw new Error("Cannot mount component: the target is not a valid DOM element");
|
||||
}
|
||||
if (!document.body.contains(target)) {
|
||||
throw new Error("Cannot mount a component on a detached dom node");
|
||||
}
|
||||
}
|
||||
|
||||
makeNode(Component: T, props: any): ComponentNode {
|
||||
return new ComponentNode(Component, props, this);
|
||||
}
|
||||
|
||||
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
|
||||
const promise: any = new Promise((resolve, reject) => {
|
||||
let isResolved = false;
|
||||
// manually set a onMounted callback.
|
||||
// that way, we are independant from the current node.
|
||||
node.mounted.push(() => {
|
||||
resolve(node.component);
|
||||
isResolved = true;
|
||||
});
|
||||
|
||||
// Manually add the last resort error handler on the node
|
||||
let handlers = nodeErrorHandlers.get(node);
|
||||
if (!handlers) {
|
||||
handlers = [];
|
||||
nodeErrorHandlers.set(node, handlers);
|
||||
}
|
||||
handlers.unshift((e) => {
|
||||
if (isResolved) {
|
||||
console.error(e);
|
||||
} else {
|
||||
reject(e);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
node.mountComponent(target, options);
|
||||
return promise;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.root) {
|
||||
this.root.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function mount<T extends typeof Component>(
|
||||
C: T,
|
||||
target: HTMLElement,
|
||||
config: AppConfig & MountOptions = {}
|
||||
): Promise<InstanceType<T>> {
|
||||
return new App(C, config).mount(target, config);
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import { BDom, multi, text, toggler } from "../blockdom";
|
||||
import { validateProps } from "../component/props_validation";
|
||||
import { Markup } from "../utils";
|
||||
import { html } from "../blockdom/index";
|
||||
import { TARGET } from "../reactivity";
|
||||
|
||||
/**
|
||||
* This file contains utility functions that will be injected in each template,
|
||||
* to perform various useful tasks in the compiled code.
|
||||
*/
|
||||
|
||||
function withDefault(value: any, defaultValue: any): any {
|
||||
return value === undefined || value === null || value === false ? defaultValue : value;
|
||||
}
|
||||
|
||||
function callSlot(
|
||||
ctx: any,
|
||||
parent: any,
|
||||
key: string,
|
||||
name: string,
|
||||
dynamic: boolean,
|
||||
extra: any,
|
||||
defaultContent?: (ctx: any, node: any, key: string) => BDom
|
||||
): BDom {
|
||||
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) {
|
||||
slotScope[__scope] = extra || {};
|
||||
}
|
||||
const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
|
||||
if (defaultContent) {
|
||||
let child1: BDom | undefined = undefined;
|
||||
let child2: BDom | undefined = undefined;
|
||||
if (slotBDom) {
|
||||
child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
|
||||
} else {
|
||||
child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
|
||||
}
|
||||
return multi([child1, child2]);
|
||||
}
|
||||
return slotBDom || text("");
|
||||
}
|
||||
|
||||
function capture(ctx: any): any {
|
||||
const component = ctx.__owl__.component;
|
||||
const result = Object.create(component);
|
||||
for (let k in ctx) {
|
||||
result[k] = ctx[k];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function withKey(elem: any, k: string) {
|
||||
elem.key = k;
|
||||
return elem;
|
||||
}
|
||||
|
||||
function prepareList(collection: any): [any[], any[], number, any[]] {
|
||||
let keys: any[];
|
||||
let values: any[];
|
||||
|
||||
if (Array.isArray(collection)) {
|
||||
keys = collection;
|
||||
values = collection;
|
||||
} else if (collection) {
|
||||
values = Object.keys(collection);
|
||||
keys = Object.values(collection);
|
||||
} else {
|
||||
throw new Error("Invalid loop expression");
|
||||
}
|
||||
const n = values.length;
|
||||
return [keys, values, n, new Array(n)];
|
||||
}
|
||||
|
||||
const isBoundary = Symbol("isBoundary");
|
||||
|
||||
function setContextValue(ctx: { [key: string]: any }, key: string, value: any): void {
|
||||
const ctx0 = ctx;
|
||||
while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
|
||||
const newCtx = ctx.__proto__;
|
||||
if (!newCtx) {
|
||||
ctx = ctx0;
|
||||
break;
|
||||
}
|
||||
ctx = newCtx;
|
||||
}
|
||||
ctx[key] = value;
|
||||
}
|
||||
|
||||
function toNumber(val: string): number | string {
|
||||
const n = parseFloat(val);
|
||||
return isNaN(n) ? val : n;
|
||||
}
|
||||
|
||||
function shallowEqual(l1: any[], l2: any[]): boolean {
|
||||
for (let i = 0, l = l1.length; i < l; i++) {
|
||||
if (l1[i] !== l2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
class LazyValue {
|
||||
fn: any;
|
||||
ctx: any;
|
||||
node: any;
|
||||
constructor(fn: any, ctx: any, node: any) {
|
||||
this.fn = fn;
|
||||
this.ctx = capture(ctx);
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
evaluate(): any {
|
||||
return this.fn(this.ctx, this.node);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.evaluate().toString();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Safely outputs `value` as a block depending on the nature of `value`
|
||||
*/
|
||||
export function safeOutput(value: any): ReturnType<typeof toggler> {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
let safeKey;
|
||||
let block;
|
||||
if (value instanceof Markup) {
|
||||
safeKey = `string_safe`;
|
||||
block = html(value as string);
|
||||
} else if (value instanceof LazyValue) {
|
||||
safeKey = `lazy_value`;
|
||||
block = value.evaluate();
|
||||
} else if (typeof value === "string") {
|
||||
safeKey = "string_unsafe";
|
||||
block = text(value);
|
||||
} else {
|
||||
// Assuming it is a block
|
||||
safeKey = "block_safe";
|
||||
block = value;
|
||||
}
|
||||
return toggler(safeKey, block);
|
||||
}
|
||||
|
||||
let boundFunctions = new WeakMap();
|
||||
|
||||
function bind(ctx: any, fn: Function): Function {
|
||||
let component = ctx.__owl__.component;
|
||||
let boundFnMap = boundFunctions.get(component);
|
||||
if (!boundFnMap) {
|
||||
boundFnMap = new WeakMap();
|
||||
boundFunctions.set(component, boundFnMap);
|
||||
}
|
||||
let boundFn = boundFnMap.get(fn);
|
||||
if (!boundFn) {
|
||||
boundFn = fn.bind(component);
|
||||
boundFnMap.set(fn, boundFn);
|
||||
}
|
||||
return boundFn;
|
||||
}
|
||||
|
||||
type RefMap = { [key: string]: HTMLElement | null };
|
||||
type RefSetter = (el: HTMLElement | null) => void;
|
||||
|
||||
function multiRefSetter(refs: RefMap, name: string): RefSetter {
|
||||
let count = 0;
|
||||
return (el) => {
|
||||
if (el) {
|
||||
count++;
|
||||
if (count > 1) {
|
||||
throw new Error("Cannot have 2 elements with same ref name at the same time");
|
||||
}
|
||||
}
|
||||
if (count === 0 || el) {
|
||||
refs[name] = el;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const UTILS = {
|
||||
withDefault,
|
||||
zero: Symbol("zero"),
|
||||
isBoundary,
|
||||
callSlot,
|
||||
capture,
|
||||
withKey,
|
||||
prepareList,
|
||||
setContextValue,
|
||||
multiRefSetter,
|
||||
shallowEqual,
|
||||
toNumber,
|
||||
validateProps,
|
||||
LazyValue,
|
||||
safeOutput,
|
||||
bind,
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
import { createBlock, html, list, multi, text, toggler, comment } from "../blockdom";
|
||||
import { compile, Template } from "../compiler";
|
||||
import { component } from "../component/component_node";
|
||||
import { UTILS } from "./template_helpers";
|
||||
|
||||
const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
|
||||
|
||||
export const globalTemplates: { [key: string]: string | Node } = {};
|
||||
|
||||
function parseXML(xml: string): Document {
|
||||
const parser = new DOMParser();
|
||||
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
let msg = "Invalid XML in template.";
|
||||
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
||||
if (parsererrorText) {
|
||||
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
||||
const re = /\d+/g;
|
||||
const firstMatch = re.exec(parsererrorText);
|
||||
if (firstMatch) {
|
||||
const lineNumber = Number(firstMatch[0]);
|
||||
const line = xml.split("\n")[lineNumber - 1];
|
||||
const secondMatch = re.exec(parsererrorText);
|
||||
if (line && secondMatch) {
|
||||
const columnIndex = Number(secondMatch[0]) - 1;
|
||||
if (line[columnIndex]) {
|
||||
msg +=
|
||||
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
||||
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
export interface TemplateSetConfig {
|
||||
dev?: boolean;
|
||||
translatableAttributes?: string[];
|
||||
translateFn?: (s: string) => string;
|
||||
templates?: string | Document;
|
||||
}
|
||||
|
||||
export class TemplateSet {
|
||||
dev: boolean;
|
||||
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
|
||||
templates: { [name: string]: Template } = {};
|
||||
translateFn?: (s: string) => string;
|
||||
translatableAttributes?: string[];
|
||||
utils: typeof UTILS = Object.assign({}, UTILS, {
|
||||
call: (owner: any, subTemplate: string, ctx: any, parent: any, key: any) => {
|
||||
const template = this.getTemplate(subTemplate);
|
||||
return toggler(subTemplate, template.call(owner, ctx, parent, key));
|
||||
},
|
||||
getTemplate: (name: string) => this.getTemplate(name),
|
||||
});
|
||||
|
||||
constructor(config: TemplateSetConfig = {}) {
|
||||
this.dev = config.dev || false;
|
||||
this.translateFn = config.translateFn;
|
||||
this.translatableAttributes = config.translatableAttributes;
|
||||
if (config.templates) {
|
||||
this.addTemplates(config.templates);
|
||||
}
|
||||
}
|
||||
|
||||
addTemplate(name: string, template: string | Node, options: { allowDuplicate?: boolean } = {}) {
|
||||
if (name in this.rawTemplates && !options.allowDuplicate) {
|
||||
throw new Error(`Template ${name} already defined`);
|
||||
}
|
||||
this.rawTemplates[name] = template;
|
||||
}
|
||||
|
||||
addTemplates(xml: string | Document, options: { allowDuplicate?: boolean } = {}) {
|
||||
if (!xml) {
|
||||
// empty string
|
||||
return;
|
||||
}
|
||||
xml = xml instanceof Document ? xml : parseXML(xml);
|
||||
for (const template of xml.querySelectorAll("[t-name]")) {
|
||||
const name = template.getAttribute("t-name")!;
|
||||
template.removeAttribute("t-name");
|
||||
this.addTemplate(name, template, options);
|
||||
}
|
||||
}
|
||||
|
||||
getTemplate(name: string): Template {
|
||||
if (!(name in this.templates)) {
|
||||
const rawTemplate = this.rawTemplates[name];
|
||||
if (rawTemplate === undefined) {
|
||||
throw new Error(`Missing template: "${name}"`);
|
||||
}
|
||||
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
|
||||
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;
|
||||
}
|
||||
return this.templates[name];
|
||||
}
|
||||
|
||||
_compileTemplate(name: string, template: string | Node) {
|
||||
return compile(template, {
|
||||
name,
|
||||
dev: this.dev,
|
||||
translateFn: this.translateFn,
|
||||
translatableAttributes: this.translatableAttributes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// xml tag helper
|
||||
// -----------------------------------------------------------------------------
|
||||
export function xml(...args: Parameters<typeof String.raw>) {
|
||||
const name = `__template__${xml.nextId++}`;
|
||||
const value = String.raw(...args);
|
||||
globalTemplates[name] = value;
|
||||
return name;
|
||||
}
|
||||
|
||||
xml.nextId = 1;
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Setter } from "./block_compiler";
|
||||
|
||||
const { setAttribute, removeAttribute } = Element.prototype;
|
||||
const { setAttribute: elemSetAttribute, removeAttribute } = Element.prototype;
|
||||
const tokenList = DOMTokenList.prototype;
|
||||
const tokenListAdd = tokenList.add;
|
||||
const tokenListRemove = tokenList.remove;
|
||||
@@ -14,11 +14,23 @@ const wordRegexp = /\s+/;
|
||||
* file.
|
||||
*/
|
||||
|
||||
function setAttribute(this: HTMLElement, key: string, value: any) {
|
||||
switch (value) {
|
||||
case false:
|
||||
case undefined:
|
||||
removeAttribute.call(this, key);
|
||||
break;
|
||||
case true:
|
||||
elemSetAttribute.call(this, key, "");
|
||||
break;
|
||||
default:
|
||||
elemSetAttribute.call(this, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
export function createAttrUpdater(attr: string): Setter<HTMLElement> {
|
||||
return function (this: HTMLElement, value: any) {
|
||||
if (value !== false) {
|
||||
setAttribute.call(this, attr, value === true ? "" : value);
|
||||
}
|
||||
setAttribute.call(this, attr, value);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,10 +156,10 @@ export function isProp(tag: string, key: string): boolean {
|
||||
case "option":
|
||||
return key === "selected" || key === "disabled";
|
||||
case "textarea":
|
||||
return key === "readonly" || key === "disabled";
|
||||
break;
|
||||
case "button":
|
||||
return key === "value" || key === "readonly" || key === "disabled";
|
||||
case "select":
|
||||
return key === "value" || key === "disabled";
|
||||
case "button":
|
||||
case "optgroup":
|
||||
return key === "disabled";
|
||||
}
|
||||
|
||||
@@ -113,9 +113,10 @@ interface IntermediateTree {
|
||||
nextSibling: IntermediateTree | null;
|
||||
el: Node;
|
||||
info: DynamicInfo[];
|
||||
forceRef?: boolean;
|
||||
isRef?: boolean;
|
||||
refIdx?: number;
|
||||
refN: number;
|
||||
currentNS: string | null;
|
||||
}
|
||||
|
||||
function buildTree(
|
||||
@@ -124,9 +125,9 @@ function buildTree(
|
||||
domParentTree: IntermediateTree | null = null
|
||||
): IntermediateTree {
|
||||
switch (node.nodeType) {
|
||||
case 1: {
|
||||
case Node.ELEMENT_NODE: {
|
||||
// HTMLElement
|
||||
let isActive = false;
|
||||
let currentNS = domParentTree && domParentTree.currentNS;
|
||||
const tagName = (node as Element).tagName;
|
||||
let el: Node | undefined = undefined;
|
||||
const info: DynamicInfo[] = [];
|
||||
@@ -134,25 +135,31 @@ function buildTree(
|
||||
const index = parseInt(tagName.slice(11), 10);
|
||||
info.push({ type: "text", idx: index });
|
||||
el = document.createTextNode("");
|
||||
isActive = true;
|
||||
}
|
||||
if (tagName.startsWith("block-child-")) {
|
||||
domParentTree!.forceRef = true;
|
||||
if (!domParentTree!.isRef) {
|
||||
addRef(domParentTree!);
|
||||
}
|
||||
const index = parseInt(tagName.slice(12), 10);
|
||||
info.push({ type: "child", idx: index });
|
||||
el = document.createTextNode("");
|
||||
isActive = true;
|
||||
}
|
||||
const attrs = (node as Element).attributes;
|
||||
const ns = attrs.getNamedItem("block-ns");
|
||||
if (ns) {
|
||||
attrs.removeNamedItem("block-ns");
|
||||
currentNS = ns.value;
|
||||
}
|
||||
if (!el) {
|
||||
el = document.createElement(tagName);
|
||||
el = currentNS
|
||||
? document.createElementNS(currentNS, tagName)
|
||||
: document.createElement(tagName);
|
||||
}
|
||||
if (el instanceof HTMLElement) {
|
||||
const attrs = (node as Element).attributes;
|
||||
if (el instanceof Element) {
|
||||
for (let i = 0; i < attrs.length; i++) {
|
||||
const attrName = attrs[i].name;
|
||||
const attrValue = attrs[i].value;
|
||||
if (attrName.startsWith("block-handler-")) {
|
||||
isActive = true;
|
||||
const idx = parseInt(attrName.slice(14), 10);
|
||||
info.push({
|
||||
type: "handler",
|
||||
@@ -160,7 +167,6 @@ function buildTree(
|
||||
event: attrValue,
|
||||
});
|
||||
} else if (attrName.startsWith("block-attribute-")) {
|
||||
isActive = true;
|
||||
const idx = parseInt(attrName.slice(16), 10);
|
||||
info.push({
|
||||
type: "attribute",
|
||||
@@ -169,13 +175,11 @@ function buildTree(
|
||||
tag: tagName,
|
||||
});
|
||||
} else if (attrName === "block-attributes") {
|
||||
isActive = true;
|
||||
info.push({
|
||||
type: "attributes",
|
||||
idx: parseInt(attrValue, 10),
|
||||
});
|
||||
} else if (attrName === "block-ref") {
|
||||
isActive = true;
|
||||
info.push({
|
||||
type: "ref",
|
||||
idx: parseInt(attrValue, 10),
|
||||
@@ -192,21 +196,20 @@ function buildTree(
|
||||
nextSibling: null,
|
||||
el,
|
||||
info,
|
||||
refN: isActive ? 1 : 0,
|
||||
refN: 0,
|
||||
currentNS,
|
||||
};
|
||||
|
||||
if (node.firstChild) {
|
||||
const childNode = node.childNodes[0];
|
||||
if (
|
||||
node.childNodes.length === 1 &&
|
||||
childNode.nodeType === 1 &&
|
||||
childNode.nodeType === Node.ELEMENT_NODE &&
|
||||
(childNode as Element).tagName.startsWith("block-child-")
|
||||
) {
|
||||
const tagName = (childNode as Element).tagName;
|
||||
const index = parseInt(tagName.slice(12), 10);
|
||||
info.push({ idx: index, type: "child", isOnlyChild: true });
|
||||
isActive = true;
|
||||
tree.refN = 1;
|
||||
} else {
|
||||
tree.firstChild = buildTree(node.firstChild, tree, tree);
|
||||
el.appendChild(tree.firstChild.el);
|
||||
@@ -219,19 +222,16 @@ function buildTree(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isActive) {
|
||||
let cur: IntermediateTree | null = tree;
|
||||
while ((cur = cur.parent)) {
|
||||
cur.refN++;
|
||||
}
|
||||
if (tree.info.length) {
|
||||
addRef(tree);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
case 3:
|
||||
case 8: {
|
||||
case Node.TEXT_NODE:
|
||||
case Node.COMMENT_NODE: {
|
||||
// text node or comment node
|
||||
const el =
|
||||
node.nodeType === 3
|
||||
node.nodeType === Node.TEXT_NODE
|
||||
? document.createTextNode(node.textContent!)
|
||||
: document.createComment(node.textContent!);
|
||||
return {
|
||||
@@ -241,12 +241,20 @@ function buildTree(
|
||||
el,
|
||||
info: [],
|
||||
refN: 0,
|
||||
currentNS: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
throw new Error("boom");
|
||||
}
|
||||
|
||||
function addRef(tree: IntermediateTree) {
|
||||
tree.isRef = true;
|
||||
do {
|
||||
tree.refN++;
|
||||
} while ((tree = tree.parent as any));
|
||||
}
|
||||
|
||||
function parentTree(tree: IntermediateTree): IntermediateTree | null {
|
||||
let parent = tree.parent;
|
||||
while (parent && parent.nextSibling === tree) {
|
||||
@@ -293,21 +301,15 @@ interface BlockCtx {
|
||||
cbRefs: number[];
|
||||
}
|
||||
|
||||
function buildContext(
|
||||
tree: IntermediateTree,
|
||||
ctx?: BlockCtx,
|
||||
fromIdx?: number,
|
||||
toIdx?: number
|
||||
): BlockCtx {
|
||||
function buildContext(tree: IntermediateTree, ctx?: BlockCtx, fromIdx?: number): BlockCtx {
|
||||
if (!ctx) {
|
||||
const children = new Array(tree.info.filter((v) => v.type === "child").length);
|
||||
ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN };
|
||||
fromIdx = 0;
|
||||
toIdx = tree.refN - 1;
|
||||
}
|
||||
if (tree.refN) {
|
||||
const initialIdx = fromIdx!;
|
||||
const isRef = tree.forceRef || tree.info.length > 0;
|
||||
const isRef = tree.isRef;
|
||||
const firstChild = tree.firstChild ? tree.firstChild.refN : 0;
|
||||
const nextSibling = tree.nextSibling ? tree.nextSibling.refN : 0;
|
||||
|
||||
@@ -325,13 +327,13 @@ function buildContext(
|
||||
if (nextSibling) {
|
||||
const idx = fromIdx! + firstChild;
|
||||
ctx.collectors.push({ idx, prevIdx: initialIdx, getVal: nodeGetNextSibling });
|
||||
buildContext(tree.nextSibling!, ctx, idx, toIdx);
|
||||
buildContext(tree.nextSibling!, ctx, idx);
|
||||
}
|
||||
|
||||
// left
|
||||
if (firstChild) {
|
||||
ctx.collectors.push({ idx: fromIdx!, prevIdx: initialIdx, getVal: nodeGetFirstChild });
|
||||
buildContext(tree.firstChild!, ctx, fromIdx!, toIdx! - nextSibling);
|
||||
buildContext(tree.firstChild!, ctx, fromIdx!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,12 +398,12 @@ function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
|
||||
});
|
||||
break;
|
||||
case "handler": {
|
||||
const setupHandler = createEventHandler(info.event!);
|
||||
const { setup, update } = createEventHandler(info.event!);
|
||||
ctx.locations.push({
|
||||
idx: info.idx,
|
||||
refIdx: info.refIdx!,
|
||||
setData: setupHandler,
|
||||
updateData: setupHandler,
|
||||
setData: setup,
|
||||
updateData: update,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
+13
-1
@@ -1,3 +1,13 @@
|
||||
export function filterOutModifiersFromData(dataList: any[]): { modifiers: string[]; data: any[] } {
|
||||
dataList = dataList.slice();
|
||||
const modifiers = [];
|
||||
let elm;
|
||||
while ((elm = dataList[0]) && typeof elm === "string") {
|
||||
modifiers.push(dataList.shift());
|
||||
}
|
||||
return { modifiers, data: dataList };
|
||||
}
|
||||
|
||||
export const config = {
|
||||
// whether or not blockdom should normalize DOM whenever a block is created.
|
||||
// Normalizing dom mean removing empty text nodes (or containing only spaces)
|
||||
@@ -6,11 +16,13 @@ export const config = {
|
||||
// this is the main event handler. Every event handler registered with blockdom
|
||||
// will go through this function, giving it the data registered in the block
|
||||
// and the event
|
||||
mainEventHandler: (data: any, ev: Event) => {
|
||||
mainEventHandler: (data: any, ev: Event, currentTarget?: EventTarget | null): boolean => {
|
||||
if (typeof data === "function") {
|
||||
data(ev);
|
||||
} else if (Array.isArray(data)) {
|
||||
data = filterOutModifiersFromData(data).data;
|
||||
data[0](data[1], ev);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
+74
-16
@@ -1,21 +1,77 @@
|
||||
import { config } from "./config";
|
||||
|
||||
export function createEventHandler(event: string) {
|
||||
setupSyntheticEvent(event);
|
||||
const key = `__event__${event}`;
|
||||
return function setupHandler(this: HTMLElement, data: any) {
|
||||
(this as any)[key] = data;
|
||||
};
|
||||
type EventHandlerSetter = (this: HTMLElement, data: any) => void;
|
||||
|
||||
interface EventHandlerCreator {
|
||||
setup: EventHandlerSetter;
|
||||
update: EventHandlerSetter;
|
||||
}
|
||||
|
||||
function nativeToSyntheticEvent(event: Event, name: string) {
|
||||
const eventKey = `__event__${name}`;
|
||||
export function createEventHandler(rawEvent: string): EventHandlerCreator {
|
||||
const eventName = rawEvent.split(".")[0];
|
||||
const capture = rawEvent.includes(".capture");
|
||||
if (rawEvent.includes(".synthetic")) {
|
||||
return createSyntheticHandler(eventName, capture);
|
||||
} else {
|
||||
return createElementHandler(eventName, capture);
|
||||
}
|
||||
}
|
||||
|
||||
// Native listener
|
||||
let nextNativeEventId = 1;
|
||||
function createElementHandler(evName: string, capture: boolean = false): EventHandlerCreator {
|
||||
let eventKey = `__event__${evName}_${nextNativeEventId++}`;
|
||||
if (capture) {
|
||||
eventKey = `${eventKey}_capture`;
|
||||
}
|
||||
|
||||
function listener(ev: Event) {
|
||||
const currentTarget = ev.currentTarget;
|
||||
if (!currentTarget || !document.contains(currentTarget as HTMLElement)) return;
|
||||
const data = (currentTarget as any)[eventKey];
|
||||
if (!data) return;
|
||||
config.mainEventHandler(data, ev, currentTarget);
|
||||
}
|
||||
|
||||
function setup(this: HTMLElement, data: any) {
|
||||
(this as any)[eventKey] = data;
|
||||
this.addEventListener(evName, listener, { capture });
|
||||
}
|
||||
|
||||
function update(this: HTMLElement, data: any) {
|
||||
(this as any)[eventKey] = data;
|
||||
}
|
||||
|
||||
return { setup, update };
|
||||
}
|
||||
|
||||
// Synthetic handler: a form of event delegation that allows placing only one
|
||||
// listener per event type.
|
||||
let nextSyntheticEventId = 1;
|
||||
function createSyntheticHandler(evName: string, capture: boolean = false): EventHandlerCreator {
|
||||
let eventKey = `__event__synthetic_${evName}`;
|
||||
if (capture) {
|
||||
eventKey = `${eventKey}_capture`;
|
||||
}
|
||||
setupSyntheticEvent(evName, eventKey, capture);
|
||||
const currentId = nextSyntheticEventId++;
|
||||
function setup(this: HTMLElement, data: any) {
|
||||
const _data = (this as any)[eventKey] || {};
|
||||
_data[currentId] = data;
|
||||
(this as any)[eventKey] = _data;
|
||||
}
|
||||
return { setup, update: setup };
|
||||
}
|
||||
|
||||
function nativeToSyntheticEvent(eventKey: string, event: Event) {
|
||||
let dom = event.target;
|
||||
while (dom !== null) {
|
||||
const data = (dom as any)[eventKey];
|
||||
if (data) {
|
||||
config.mainEventHandler(data, event);
|
||||
return;
|
||||
const _data = (dom as any)[eventKey];
|
||||
if (_data) {
|
||||
for (const data of Object.values(_data)) {
|
||||
const stopped = config.mainEventHandler(data, event, dom);
|
||||
if (stopped) return;
|
||||
}
|
||||
}
|
||||
dom = (dom as any).parentNode;
|
||||
}
|
||||
@@ -23,10 +79,12 @@ function nativeToSyntheticEvent(event: Event, name: string) {
|
||||
|
||||
const CONFIGURED_SYNTHETIC_EVENTS: { [event: string]: boolean } = {};
|
||||
|
||||
function setupSyntheticEvent(name: string) {
|
||||
if (CONFIGURED_SYNTHETIC_EVENTS[name]) {
|
||||
function setupSyntheticEvent(evName: string, eventKey: string, capture: boolean = false) {
|
||||
if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) {
|
||||
return;
|
||||
}
|
||||
document.addEventListener(name, (event) => nativeToSyntheticEvent(event, name));
|
||||
CONFIGURED_SYNTHETIC_EVENTS[name] = true;
|
||||
document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event), {
|
||||
capture,
|
||||
});
|
||||
CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export { toggler } from "./toggler";
|
||||
export { createBlock } from "./block_compiler";
|
||||
export { list } from "./list";
|
||||
export { multi } from "./multi";
|
||||
export { text } from "./text";
|
||||
export { text, comment } from "./text";
|
||||
export { html } from "./html";
|
||||
|
||||
export interface VNode<T = any> {
|
||||
@@ -23,8 +23,8 @@ export interface VNode<T = any> {
|
||||
|
||||
export type BDom = VNode<any>;
|
||||
|
||||
export function mount(vnode: VNode, fixture: HTMLElement) {
|
||||
vnode.mount(fixture, null);
|
||||
export function mount(vnode: VNode, fixture: HTMLElement, afterNode: Node | null = null) {
|
||||
vnode.mount(fixture, afterNode);
|
||||
}
|
||||
|
||||
export function patch(vnode1: VNode, vnode2: VNode, withBeforeRemove: boolean = false) {
|
||||
|
||||
@@ -98,7 +98,6 @@ class VList {
|
||||
let endVn2 = ch2[endIdx2];
|
||||
|
||||
let mapping: any = undefined;
|
||||
// let noFullRemove = this.hasNoComponent;
|
||||
|
||||
while (startIdx1 <= endIdx1 && startIdx2 <= endIdx2) {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -125,7 +125,7 @@ export class VMulti {
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.children.map((c) => c!.toString()).join("");
|
||||
return this.children.map((c) => (c ? c!.toString() : "")).join("");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+29
-12
@@ -8,18 +8,17 @@ const nodeInsertBefore = nodeProto.insertBefore;
|
||||
const characterDataSetData = getDescriptor(characterDataProto, "data").set!;
|
||||
const nodeRemoveChild = nodeProto.removeChild;
|
||||
|
||||
class VText {
|
||||
abstract class VSimpleNode {
|
||||
text: string;
|
||||
parentEl?: HTMLElement | undefined;
|
||||
el?: Text;
|
||||
el?: any;
|
||||
|
||||
constructor(text: string) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
mountNode(node: Node, parent: HTMLElement, afterNode: Node | null) {
|
||||
this.parentEl = parent;
|
||||
const node = document.createTextNode(toText(this.text));
|
||||
nodeInsertBefore.call(parent, node, afterNode);
|
||||
this.el = node;
|
||||
}
|
||||
@@ -29,14 +28,6 @@ class VText {
|
||||
nodeInsertBefore.call(this.parentEl, this.el!, target);
|
||||
}
|
||||
|
||||
patch(other: VText) {
|
||||
const text2 = other.text;
|
||||
if (this.text !== text2) {
|
||||
characterDataSetData.call(this.el!, toText(text2));
|
||||
this.text = text2;
|
||||
}
|
||||
}
|
||||
|
||||
beforeRemove() {}
|
||||
|
||||
remove() {
|
||||
@@ -52,10 +43,36 @@ class VText {
|
||||
}
|
||||
}
|
||||
|
||||
class VText extends VSimpleNode {
|
||||
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
this.mountNode(document.createTextNode(toText(this.text)), parent, afterNode);
|
||||
}
|
||||
|
||||
patch(other: VText) {
|
||||
const text2 = other.text;
|
||||
if (this.text !== text2) {
|
||||
characterDataSetData.call(this.el!, toText(text2));
|
||||
this.text = text2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class VComment extends VSimpleNode {
|
||||
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
this.mountNode(document.createComment(toText(this.text)), parent, afterNode);
|
||||
}
|
||||
|
||||
patch() {}
|
||||
}
|
||||
|
||||
export function text(str: string): VNode<VText> {
|
||||
return new VText(str);
|
||||
}
|
||||
|
||||
export function comment(str: string): VNode<VComment> {
|
||||
return new VComment(str);
|
||||
}
|
||||
|
||||
export function toText(value: any): string {
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
|
||||
@@ -43,7 +43,9 @@ class VToggler {
|
||||
}
|
||||
}
|
||||
|
||||
beforeRemove() {}
|
||||
beforeRemove() {
|
||||
this.child.beforeRemove();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.child.remove();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
import type { BDom } from "../blockdom";
|
||||
import { CodeGenerator, Config } from "./code_generator";
|
||||
import { parse } from "./parser";
|
||||
|
||||
export type Template = (context: any, vnode: any, key?: string) => BDom;
|
||||
|
||||
export type TemplateFunction = (blocks: any, utils: any) => Template;
|
||||
|
||||
interface CompileOptions extends Config {
|
||||
name?: string;
|
||||
}
|
||||
export function compile(template: string | Node, options: CompileOptions = {}): TemplateFunction {
|
||||
// parsing
|
||||
const ast = parse(template);
|
||||
|
||||
// some work
|
||||
const hasSafeContext =
|
||||
template instanceof Node
|
||||
? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
|
||||
: !template.includes("t-set") && !template.includes("t-call");
|
||||
|
||||
// code generation
|
||||
const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
|
||||
const code = codeGenerator.generateCode();
|
||||
// template function
|
||||
return new Function("bdom, helpers", code) as TemplateFunction;
|
||||
}
|
||||
@@ -70,6 +70,7 @@ interface Token {
|
||||
size?: number;
|
||||
varName?: string;
|
||||
replace?: Function;
|
||||
isLocal?: boolean;
|
||||
}
|
||||
|
||||
const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(null), {
|
||||
@@ -325,6 +326,13 @@ export function compileExprToArray(expr: string): Token[] {
|
||||
}
|
||||
i++;
|
||||
}
|
||||
// Mark all variables that have been used locally.
|
||||
// This assumes the expression has only one scope (incorrect but "good enough for now")
|
||||
for (const token of tokens) {
|
||||
if (token.type === "SYMBOL" && localVars.has(token.value)) {
|
||||
token.isLocal = true;
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export const enum ASTType {
|
||||
TIf,
|
||||
TSet,
|
||||
TCall,
|
||||
TRaw,
|
||||
TOut,
|
||||
TForEach,
|
||||
TKey,
|
||||
TComponent,
|
||||
@@ -19,6 +19,8 @@ export const enum ASTType {
|
||||
TLog,
|
||||
TSlot,
|
||||
TCallBlock,
|
||||
TTranslation,
|
||||
TPortal,
|
||||
}
|
||||
|
||||
export interface ASTText {
|
||||
@@ -34,10 +36,21 @@ export interface ASTComment {
|
||||
export interface ASTDomNode {
|
||||
type: ASTType.DomNode;
|
||||
tag: string;
|
||||
dynamicTag: string | null;
|
||||
attrs: { [key: string]: string };
|
||||
content: AST[];
|
||||
ref: string | null;
|
||||
on: { [key: string]: string };
|
||||
model: {
|
||||
baseExpr: string;
|
||||
expr: string;
|
||||
targetAttr: string;
|
||||
specialInitTargetAttr: string | null;
|
||||
eventType: "change" | "click" | "input";
|
||||
shouldTrim: boolean;
|
||||
shouldNumberize: boolean;
|
||||
} | null;
|
||||
ns: string | null;
|
||||
}
|
||||
|
||||
export interface ASTMulti {
|
||||
@@ -51,8 +64,8 @@ export interface ASTTEsc {
|
||||
defaultValue: string;
|
||||
}
|
||||
|
||||
export interface ASTTRaw {
|
||||
type: ASTType.TRaw;
|
||||
export interface ASTTOut {
|
||||
type: ASTType.TOut;
|
||||
expr: string;
|
||||
body: AST[] | null;
|
||||
}
|
||||
@@ -80,8 +93,6 @@ export interface ASTTForEach {
|
||||
key: string | null;
|
||||
body: AST;
|
||||
memo: string;
|
||||
isOnlyChild: boolean;
|
||||
hasNoComponent: boolean;
|
||||
hasNoFirst: boolean;
|
||||
hasNoLast: boolean;
|
||||
hasNoIndex: boolean;
|
||||
@@ -104,14 +115,15 @@ export interface ASTComponent {
|
||||
type: ASTType.TComponent;
|
||||
name: string;
|
||||
isDynamic: boolean;
|
||||
dynamicProps: string | null;
|
||||
props: { [name: string]: string };
|
||||
handlers: { [event: string]: string };
|
||||
slots: { [name: string]: AST };
|
||||
slots: { [name: string]: { content: AST; attrs?: { [key: string]: string }; scope?: string } };
|
||||
}
|
||||
|
||||
export interface ASTSlot {
|
||||
type: ASTType.TSlot;
|
||||
name: string;
|
||||
attrs: { [key: string]: string };
|
||||
defaultContent: AST | null;
|
||||
}
|
||||
|
||||
@@ -131,6 +143,17 @@ export interface ASTLog {
|
||||
content: AST | null;
|
||||
}
|
||||
|
||||
export interface ASTTranslation {
|
||||
type: ASTType.TTranslation;
|
||||
content: AST | null;
|
||||
}
|
||||
|
||||
export interface ASTTPortal {
|
||||
type: ASTType.TPortal;
|
||||
target: string;
|
||||
content: AST;
|
||||
}
|
||||
|
||||
export type AST =
|
||||
| ASTText
|
||||
| ASTComment
|
||||
@@ -140,27 +163,30 @@ export type AST =
|
||||
| ASTTif
|
||||
| ASTTSet
|
||||
| ASTTCall
|
||||
| ASTTRaw
|
||||
| ASTTOut
|
||||
| ASTTForEach
|
||||
| ASTTKey
|
||||
| ASTComponent
|
||||
| ASTSlot
|
||||
| ASTTCallBlock
|
||||
| ASTLog
|
||||
| ASTDebug;
|
||||
| ASTDebug
|
||||
| ASTTranslation
|
||||
| ASTTPortal;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Parser
|
||||
// -----------------------------------------------------------------------------
|
||||
interface ParsingContext {
|
||||
inPreTag: boolean;
|
||||
inSVG: boolean;
|
||||
}
|
||||
|
||||
export function parse(xml: string): AST {
|
||||
const template = `<t>${xml}</t>`;
|
||||
const doc = parseXML(template);
|
||||
const ctx = { inPreTag: false };
|
||||
const ast = parseNode(doc.firstChild!, ctx);
|
||||
export function parse(xml: string | Node): AST {
|
||||
const node = xml instanceof Element ? xml : (parseXML(`<t>${xml}</t>`).firstChild! as Element);
|
||||
normalizeXML(node);
|
||||
const ctx = { inPreTag: false, inSVG: false };
|
||||
const ast = parseNode(node, ctx);
|
||||
if (!ast) {
|
||||
return { type: ASTType.Text, value: "" };
|
||||
}
|
||||
@@ -175,12 +201,14 @@ function parseNode(node: ChildNode, ctx: ParsingContext): AST | null {
|
||||
parseTDebugLog(node, ctx) ||
|
||||
parseTForEach(node, ctx) ||
|
||||
parseTIf(node, ctx) ||
|
||||
parseTPortal(node, ctx) ||
|
||||
parseTCall(node, ctx) ||
|
||||
parseTCallBlock(node, ctx) ||
|
||||
parseTEscNode(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTSlot(node, ctx) ||
|
||||
parseTRawNode(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseComponent(node, ctx) ||
|
||||
parseDOMNode(node, ctx) ||
|
||||
parseTSetNode(node, ctx) ||
|
||||
@@ -196,24 +224,7 @@ function parseTNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (node.tagName !== "t") {
|
||||
return null;
|
||||
}
|
||||
const children: AST[] = [];
|
||||
for (let child of node.childNodes) {
|
||||
const ast = parseNode(child, ctx);
|
||||
if (ast) {
|
||||
children.push(ast);
|
||||
}
|
||||
}
|
||||
switch (children.length) {
|
||||
case 0:
|
||||
return null;
|
||||
case 1:
|
||||
return children[0];
|
||||
default:
|
||||
return {
|
||||
type: ASTType.Multi,
|
||||
content: children,
|
||||
};
|
||||
}
|
||||
return parseChildNodes(node, ctx);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -223,7 +234,7 @@ const lineBreakRE = /[\r\n]/;
|
||||
const whitespaceRE = /\s+/g;
|
||||
|
||||
function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null {
|
||||
if (node.nodeType === 3) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
let value = node.textContent || "";
|
||||
if (!ctx.inPreTag) {
|
||||
if (lineBreakRE.test(value) && !value.trim()) {
|
||||
@@ -233,7 +244,7 @@ function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null
|
||||
}
|
||||
|
||||
return { type: ASTType.Text, value };
|
||||
} else if (node.nodeType === 8) {
|
||||
} else if (node.nodeType === Node.COMMENT_NODE) {
|
||||
return { type: ASTType.Comment, value: node.textContent || "" };
|
||||
}
|
||||
return null;
|
||||
@@ -267,38 +278,79 @@ function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null {
|
||||
// -----------------------------------------------------------------------------
|
||||
// Regular dom node
|
||||
// -----------------------------------------------------------------------------
|
||||
const hasDotAtTheEnd = /\.[\w_]+\s*$/;
|
||||
const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
|
||||
|
||||
function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (node.tagName === "t") {
|
||||
const { tagName } = node;
|
||||
const dynamicTag = node.getAttribute("t-tag");
|
||||
node.removeAttribute("t-tag");
|
||||
if (tagName === "t" && !dynamicTag) {
|
||||
return null;
|
||||
}
|
||||
const children: AST[] = [];
|
||||
if (node.tagName === "pre") {
|
||||
ctx = { inPreTag: true };
|
||||
}
|
||||
let ref = null;
|
||||
if (node.hasAttribute("t-ref")) {
|
||||
ref = node.getAttribute("t-ref");
|
||||
node.removeAttribute("t-ref");
|
||||
ctx = Object.assign({}, ctx);
|
||||
if (tagName === "pre") {
|
||||
ctx.inPreTag = true;
|
||||
}
|
||||
const shouldAddSVGNS = tagName === "svg" || (tagName === "g" && !ctx.inSVG);
|
||||
ctx.inSVG = ctx.inSVG || shouldAddSVGNS;
|
||||
const ns = shouldAddSVGNS ? "http://www.w3.org/2000/svg" : null;
|
||||
const ref = node.getAttribute("t-ref");
|
||||
node.removeAttribute("t-ref");
|
||||
|
||||
for (let child of node.childNodes) {
|
||||
const ast = parseNode(child, ctx);
|
||||
if (ast) {
|
||||
children.push(ast);
|
||||
}
|
||||
}
|
||||
const children = parseChildren(node, ctx);
|
||||
|
||||
const nodeAttrsNames = node.getAttributeNames();
|
||||
const attrs: ASTDomNode["attrs"] = {};
|
||||
const on: ASTDomNode["on"] = {};
|
||||
let model: ASTDomNode["model"] = null;
|
||||
|
||||
for (let attr of node.getAttributeNames()) {
|
||||
for (let attr of nodeAttrsNames) {
|
||||
const value = node.getAttribute(attr)!;
|
||||
if (attr.startsWith("t-on")) {
|
||||
if (attr === "t-on") {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
on[attr.slice(5)] = value;
|
||||
} else if (attr.startsWith("t-model")) {
|
||||
if (!["input", "select", "textarea"].includes(tagName)) {
|
||||
throw new Error("The t-model directive only works with <input>, <textarea> and <select>");
|
||||
}
|
||||
|
||||
let baseExpr, expr;
|
||||
if (hasDotAtTheEnd.test(value)) {
|
||||
const index = value.lastIndexOf(".");
|
||||
baseExpr = value.slice(0, index);
|
||||
expr = `'${value.slice(index + 1)}'`;
|
||||
} else if (hasBracketsAtTheEnd.test(value)) {
|
||||
const index = value.lastIndexOf("[");
|
||||
baseExpr = value.slice(0, index);
|
||||
expr = value.slice(index + 1, -1);
|
||||
} else {
|
||||
throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
|
||||
}
|
||||
|
||||
const typeAttr = node.getAttribute("type");
|
||||
const isInput = tagName === "input";
|
||||
const isSelect = tagName === "select";
|
||||
const isTextarea = tagName === "textarea";
|
||||
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
||||
const isRadioInput = isInput && typeAttr === "radio";
|
||||
const isOtherInput = isInput && !isCheckboxInput && !isRadioInput;
|
||||
const hasLazyMod = attr.includes(".lazy");
|
||||
const hasNumberMod = attr.includes(".number");
|
||||
const hasTrimMod = attr.includes(".trim");
|
||||
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
|
||||
|
||||
model = {
|
||||
baseExpr,
|
||||
expr,
|
||||
targetAttr: isCheckboxInput ? "checked" : "value",
|
||||
specialInitTargetAttr: isRadioInput ? "checked" : null,
|
||||
eventType,
|
||||
shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
|
||||
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
|
||||
};
|
||||
} else {
|
||||
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||||
throw new Error(`Unknown QWeb directive: '${attr}'`);
|
||||
@@ -306,16 +358,16 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
attrs[attr] = value;
|
||||
}
|
||||
}
|
||||
if (children.length === 1 && children[0].type === ASTType.TForEach) {
|
||||
children[0].isOnlyChild = true;
|
||||
}
|
||||
return {
|
||||
type: ASTType.DomNode,
|
||||
tag: node.tagName,
|
||||
tag: tagName,
|
||||
dynamicTag,
|
||||
attrs,
|
||||
on,
|
||||
ref,
|
||||
content: children,
|
||||
model,
|
||||
ns,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -340,56 +392,53 @@ function parseTEscNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!ast) {
|
||||
return tesc;
|
||||
}
|
||||
if (ast && ast.type === ASTType.DomNode) {
|
||||
if (ast.type === ASTType.DomNode) {
|
||||
return {
|
||||
type: ASTType.DomNode,
|
||||
tag: ast.tag,
|
||||
attrs: ast.attrs,
|
||||
on: ast.on,
|
||||
...ast,
|
||||
ref,
|
||||
content: [tesc],
|
||||
};
|
||||
}
|
||||
if (ast && ast.type === ASTType.TComponent) {
|
||||
return {
|
||||
...ast,
|
||||
slots: { default: tesc },
|
||||
};
|
||||
if (ast.type === ASTType.TComponent) {
|
||||
throw new Error("t-esc is not supported on Component nodes");
|
||||
}
|
||||
return tesc;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-raw
|
||||
// t-out
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTRawNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-raw")) {
|
||||
function parseTOutNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-out") && !node.hasAttribute("t-raw")) {
|
||||
return null;
|
||||
}
|
||||
const expr = node.getAttribute("t-raw")!;
|
||||
if (node.hasAttribute("t-raw")) {
|
||||
console.warn(
|
||||
`t-raw has been deprecated in favor of t-out. If the value to render is not wrapped by the "markup" function, it will be escaped`
|
||||
);
|
||||
}
|
||||
const expr = (node.getAttribute("t-out") || node.getAttribute("t-raw"))!;
|
||||
node.removeAttribute("t-out");
|
||||
node.removeAttribute("t-raw");
|
||||
|
||||
const tRaw: AST = { type: ASTType.TRaw, expr, body: null };
|
||||
const tOut: AST = { type: ASTType.TOut, expr, body: null };
|
||||
const ref = node.getAttribute("t-ref");
|
||||
node.removeAttribute("t-ref");
|
||||
const ast = parseNode(node, ctx);
|
||||
if (!ast) {
|
||||
return tRaw;
|
||||
return tOut;
|
||||
}
|
||||
if (ast && ast.type === ASTType.DomNode) {
|
||||
tRaw.body = ast.content.length ? ast.content : null;
|
||||
if (ast.type === ASTType.DomNode) {
|
||||
tOut.body = ast.content.length ? ast.content : null;
|
||||
return {
|
||||
type: ASTType.DomNode,
|
||||
tag: ast.tag,
|
||||
attrs: ast.attrs,
|
||||
on: ast.on,
|
||||
...ast,
|
||||
ref,
|
||||
content: [tRaw],
|
||||
content: [tOut],
|
||||
};
|
||||
}
|
||||
|
||||
return tRaw;
|
||||
return tOut;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -406,6 +455,11 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
|
||||
const elem = node.getAttribute("t-as") || "";
|
||||
node.removeAttribute("t-as");
|
||||
const key = node.getAttribute("t-key");
|
||||
if (!key) {
|
||||
throw new Error(
|
||||
`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`
|
||||
);
|
||||
}
|
||||
node.removeAttribute("t-key");
|
||||
const memo = node.getAttribute("t-memo") || "";
|
||||
node.removeAttribute("t-memo");
|
||||
@@ -428,8 +482,6 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
|
||||
body,
|
||||
memo,
|
||||
key,
|
||||
isOnlyChild: false,
|
||||
hasNoComponent: hasNoComponent(body),
|
||||
hasNoFirst,
|
||||
hasNoLast,
|
||||
hasNoIndex,
|
||||
@@ -437,57 +489,6 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if we are sure the ast does not contain any component
|
||||
*/
|
||||
function hasNoComponent(ast: AST): boolean {
|
||||
switch (ast.type) {
|
||||
case ASTType.TComponent:
|
||||
case ASTType.TRaw:
|
||||
case ASTType.TCall:
|
||||
case ASTType.TCallBlock:
|
||||
case ASTType.TSlot:
|
||||
return false;
|
||||
case ASTType.TSet:
|
||||
case ASTType.Text:
|
||||
case ASTType.Comment:
|
||||
case ASTType.TEsc:
|
||||
return true;
|
||||
case ASTType.TKey:
|
||||
return hasNoComponent(ast.content);
|
||||
case ASTType.TDebug:
|
||||
case ASTType.TLog:
|
||||
return ast.content ? hasNoComponent(ast.content) : true;
|
||||
case ASTType.TForEach:
|
||||
return ast.hasNoComponent;
|
||||
case ASTType.Multi:
|
||||
case ASTType.DomNode: {
|
||||
for (let elem of ast.content) {
|
||||
if (!hasNoComponent(elem)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case ASTType.TIf: {
|
||||
if (!hasNoComponent(ast.content)) {
|
||||
return false;
|
||||
}
|
||||
if (ast.tElif) {
|
||||
for (let elem of ast.tElif) {
|
||||
if (!hasNoComponent(elem.content)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ast.tElse && !hasNoComponent(ast.tElse)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseTKey(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-key")) {
|
||||
return null;
|
||||
@@ -522,17 +523,11 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (ast && ast.type === ASTType.TComponent) {
|
||||
return {
|
||||
...ast,
|
||||
slots: { default: tcall },
|
||||
slots: { default: { content: tcall } },
|
||||
};
|
||||
}
|
||||
}
|
||||
const body: AST[] = [];
|
||||
for (let child of node.childNodes) {
|
||||
const ast = parseNode(child, ctx);
|
||||
if (ast) {
|
||||
body.push(ast);
|
||||
}
|
||||
}
|
||||
const body = parseChildren(node, ctx);
|
||||
|
||||
return {
|
||||
type: ASTType.TCall,
|
||||
@@ -566,10 +561,7 @@ function parseTIf(node: Element, ctx: ParsingContext): AST | null {
|
||||
}
|
||||
const condition = node.getAttribute("t-if")!;
|
||||
node.removeAttribute("t-if");
|
||||
const content = parseNode(node, ctx);
|
||||
if (!content) {
|
||||
throw new Error("hmmm");
|
||||
}
|
||||
const content = parseNode(node, ctx) || { type: ASTType.Text, value: "" };
|
||||
|
||||
let nextElement = node.nextElementSibling;
|
||||
// t-elifs
|
||||
@@ -616,13 +608,7 @@ function parseTSetNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
const defaultValue = node.innerHTML === node.textContent ? node.textContent || null : null;
|
||||
let body: AST[] | null = null;
|
||||
if (node.textContent !== node.innerHTML) {
|
||||
body = [];
|
||||
for (let child of node.childNodes) {
|
||||
let childAst = parseNode(child, ctx);
|
||||
if (childAst) {
|
||||
body.push(childAst);
|
||||
}
|
||||
}
|
||||
body = parseChildren(node, ctx);
|
||||
}
|
||||
return { type: ASTType.TSet, name, value, defaultValue, body };
|
||||
}
|
||||
@@ -631,11 +617,29 @@ function parseTSetNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
// Components
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Error messages when trying to use an unsupported directive on a component
|
||||
const directiveErrorMap = new Map([
|
||||
["t-on", "t-on is no longer supported on components. Consider passing a callback in props."],
|
||||
[
|
||||
"t-ref",
|
||||
"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
|
||||
],
|
||||
["t-att", "t-att makes no sense on component: props are already treated as expressions"],
|
||||
[
|
||||
"t-attf",
|
||||
"t-attf is not supported on components: use template strings for string interpolation in props",
|
||||
],
|
||||
]);
|
||||
|
||||
function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
let name = node.tagName;
|
||||
const firstLetter = name[0];
|
||||
let isDynamic = node.hasAttribute("t-component");
|
||||
|
||||
if (isDynamic && name !== "t") {
|
||||
throw new Error(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);
|
||||
}
|
||||
|
||||
if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
|
||||
return null;
|
||||
}
|
||||
@@ -644,12 +648,15 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
node.removeAttribute("t-component");
|
||||
}
|
||||
|
||||
const dynamicProps = node.getAttribute("t-props");
|
||||
node.removeAttribute("t-props");
|
||||
|
||||
const props: ASTComponent["props"] = {};
|
||||
const handlers: ASTComponent["handlers"] = {};
|
||||
for (let name of node.getAttributeNames()) {
|
||||
const value = node.getAttribute(name)!;
|
||||
if (name.startsWith("t-on-")) {
|
||||
handlers[name.slice(5)] = value;
|
||||
if (name.startsWith("t-")) {
|
||||
const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
|
||||
throw new Error(message || `unsupported directive on Component: ${name}`);
|
||||
} else {
|
||||
props[name] = value;
|
||||
}
|
||||
@@ -662,6 +669,11 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
// named slots
|
||||
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
|
||||
for (let slotNode of slotNodes) {
|
||||
if (slotNode.tagName !== "t") {
|
||||
throw new Error(
|
||||
`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`
|
||||
);
|
||||
}
|
||||
const name = slotNode.getAttribute("t-set-slot")!;
|
||||
|
||||
// check if this is defined in a sub component (in which case it should
|
||||
@@ -683,17 +695,30 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
slotNode.remove();
|
||||
const slotAst = parseNode(slotNode, ctx);
|
||||
if (slotAst) {
|
||||
slots[name] = slotAst;
|
||||
const slotInfo: any = { content: slotAst };
|
||||
const attrs: { [key: string]: string } = {};
|
||||
for (let attributeName of slotNode.getAttributeNames()) {
|
||||
const value = slotNode.getAttribute(attributeName)!;
|
||||
if (attributeName === "t-slot-scope") {
|
||||
slotInfo.scope = value;
|
||||
continue;
|
||||
}
|
||||
attrs[attributeName] = value;
|
||||
}
|
||||
if (Object.keys(attrs).length) {
|
||||
slotInfo.attrs = attrs;
|
||||
}
|
||||
slots[name] = slotInfo;
|
||||
}
|
||||
}
|
||||
|
||||
// default slot
|
||||
const defaultContent = parseChildNodes(clone, ctx);
|
||||
if (defaultContent) {
|
||||
slots.default = defaultContent;
|
||||
slots.default = { content: defaultContent };
|
||||
}
|
||||
}
|
||||
return { type: ASTType.TComponent, name, isDynamic, props, handlers, slots };
|
||||
return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots };
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -704,25 +729,84 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-slot")) {
|
||||
return null;
|
||||
}
|
||||
const name = node.getAttribute("t-slot")!;
|
||||
node.removeAttribute("t-slot");
|
||||
const attrs: { [key: string]: string } = {};
|
||||
for (let attributeName of node.getAttributeNames()) {
|
||||
const value = node.getAttribute(attributeName)!;
|
||||
attrs[attributeName] = value;
|
||||
}
|
||||
return {
|
||||
type: ASTType.TSlot,
|
||||
name: node.getAttribute("t-slot")!,
|
||||
name,
|
||||
attrs,
|
||||
defaultContent: parseChildNodes(node, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (node.getAttribute("t-translation") !== "off") {
|
||||
return null;
|
||||
}
|
||||
node.removeAttribute("t-translation");
|
||||
return {
|
||||
type: ASTType.TTranslation,
|
||||
content: parseNode(node, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Portal
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTPortal(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-portal")) {
|
||||
return null;
|
||||
}
|
||||
const target = node.getAttribute("t-portal")!;
|
||||
node.removeAttribute("t-portal");
|
||||
const content = parseNode(node, ctx);
|
||||
if (!content) {
|
||||
return {
|
||||
type: ASTType.Text,
|
||||
value: "",
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: ASTType.TPortal,
|
||||
target,
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseChildNodes(node: Element, ctx: ParsingContext): AST | null {
|
||||
/**
|
||||
* Parse all the child nodes of a given node and return a list of ast elements
|
||||
*/
|
||||
function parseChildren(node: Node, ctx: ParsingContext): AST[] {
|
||||
const children: AST[] = [];
|
||||
for (let child of node.childNodes) {
|
||||
const childAst = parseNode(child, ctx);
|
||||
if (childAst) {
|
||||
children.push(childAst);
|
||||
if (childAst.type === ASTType.Multi) {
|
||||
children.push(...childAst.content);
|
||||
} else {
|
||||
children.push(childAst);
|
||||
}
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all the child nodes of a given node and return an ast if possible.
|
||||
* In the case there are multiple children, they are wrapped in a astmulti.
|
||||
*/
|
||||
function parseChildNodes(node: Node, ctx: ParsingContext): AST | null {
|
||||
const children = parseChildren(node, ctx);
|
||||
switch (children.length) {
|
||||
case 0:
|
||||
return null;
|
||||
@@ -732,34 +816,17 @@ function parseChildNodes(node: Element, ctx: ParsingContext): AST | null {
|
||||
return { type: ASTType.Multi, content: children };
|
||||
}
|
||||
}
|
||||
function parseXML(xml: string): Document {
|
||||
const parser = new DOMParser();
|
||||
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
let msg = "Invalid XML in template.";
|
||||
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
||||
if (parsererrorText) {
|
||||
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
||||
const re = /\d+/g;
|
||||
const firstMatch = re.exec(parsererrorText);
|
||||
if (firstMatch) {
|
||||
const lineNumber = Number(firstMatch[0]);
|
||||
const line = xml.split("\n")[lineNumber - 1];
|
||||
const secondMatch = re.exec(parsererrorText);
|
||||
if (line && secondMatch) {
|
||||
const columnIndex = Number(secondMatch[0]) - 1;
|
||||
if (line[columnIndex]) {
|
||||
msg +=
|
||||
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
||||
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
let tbranch = doc.querySelectorAll("[t-elif], [t-else]");
|
||||
/**
|
||||
* Normalizes the content of an Element so that t-if/t-elif/t-else directives
|
||||
* immediately follow one another (by removing empty text nodes or comments).
|
||||
* Throws an error when a conditional branching statement is malformed. This
|
||||
* function modifies the Element in place.
|
||||
*
|
||||
* @param el the element containing the tree that should be normalized
|
||||
*/
|
||||
function normalizeTIf(el: Element) {
|
||||
let tbranch = el.querySelectorAll("[t-elif], [t-else]");
|
||||
for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
|
||||
let node = tbranch[i];
|
||||
let prevElem = node.previousElementSibling!;
|
||||
@@ -793,6 +860,78 @@ function parseXML(xml: string): Document {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the content of an Element so that t-esc directives on components
|
||||
* are removed and instead places a <t t-esc=""> as the default slot of the
|
||||
* component. Also throws if the component already has content. This function
|
||||
* modifies the Element in place.
|
||||
*
|
||||
* @param el the element containing the tree that should be normalized
|
||||
*/
|
||||
function normalizeTEsc(el: Element) {
|
||||
const elements = [...el.querySelectorAll("[t-esc]")].filter(
|
||||
(el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component")
|
||||
);
|
||||
for (const el of elements) {
|
||||
if (el.childNodes.length) {
|
||||
throw new Error("Cannot have t-esc on a component that already has content");
|
||||
}
|
||||
const value = el.getAttribute("t-esc");
|
||||
el.removeAttribute("t-esc");
|
||||
const t = el.ownerDocument.createElement("t");
|
||||
if (value != null) {
|
||||
t.setAttribute("t-esc", value);
|
||||
}
|
||||
el.appendChild(t);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the tree inside a given element and do some preliminary validation
|
||||
* on it. This function modifies the Element in place.
|
||||
*
|
||||
* @param el the element containing the tree that should be normalized
|
||||
*/
|
||||
function normalizeXML(el: Element) {
|
||||
normalizeTIf(el);
|
||||
normalizeTEsc(el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an XML string into an XML document, throwing errors on parser errors
|
||||
* instead of returning an XML document containing the parseerror.
|
||||
*
|
||||
* @param xml the string to parse
|
||||
* @returns an XML document corresponding to the content of the string
|
||||
*/
|
||||
function parseXML(xml: string): XMLDocument {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
let msg = "Invalid XML in template.";
|
||||
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
||||
if (parsererrorText) {
|
||||
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
||||
const re = /\d+/g;
|
||||
const firstMatch = re.exec(parsererrorText);
|
||||
if (firstMatch) {
|
||||
const lineNumber = Number(firstMatch[0]);
|
||||
const line = xml.split("\n")[lineNumber - 1];
|
||||
const secondMatch = re.exec(parsererrorText);
|
||||
if (line && secondMatch) {
|
||||
const columnIndex = Number(secondMatch[0]) - 1;
|
||||
if (line[columnIndex]) {
|
||||
msg +=
|
||||
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
||||
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
@@ -4,26 +4,23 @@ 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;
|
||||
env: any;
|
||||
props: Props;
|
||||
env: Env;
|
||||
__owl__: ComponentNode;
|
||||
|
||||
constructor(props: any, env: any, node: ComponentNode) {
|
||||
constructor(props: Props, env: Env, node: ComponentNode) {
|
||||
this.props = props;
|
||||
this.env = env;
|
||||
this.__owl__ = node;
|
||||
}
|
||||
get el(): HTMLElement | Text | undefined {
|
||||
const node = this.__owl__;
|
||||
return node.bdom ? (node.bdom.firstNode() as any) : undefined;
|
||||
}
|
||||
|
||||
setup() {}
|
||||
|
||||
render(): Promise<void> {
|
||||
return this.__owl__.render();
|
||||
render(force: boolean = false) {
|
||||
this.__owl__.render(force);
|
||||
}
|
||||
}
|
||||
|
||||
+210
-99
@@ -1,16 +1,70 @@
|
||||
import type { App } from "../app";
|
||||
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,
|
||||
makeRootFiber,
|
||||
MountFiber,
|
||||
MountOptions,
|
||||
RootFiber,
|
||||
__internal__destroyed,
|
||||
} from "./fibers";
|
||||
import { applyDefaultProps } from "./props_validation";
|
||||
import { STATUS } from "./status";
|
||||
|
||||
let currentNode: ComponentNode | null = null;
|
||||
|
||||
export function getCurrent(): ComponentNode | null {
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -21,9 +75,13 @@ export function component(
|
||||
let node: any = ctx.children[key];
|
||||
let isDynamic = typeof name !== "string";
|
||||
|
||||
if (node && node.status < STATUS.MOUNTED) {
|
||||
node.destroy();
|
||||
node = undefined;
|
||||
if (node) {
|
||||
if (node.status < STATUS.MOUNTED) {
|
||||
node.destroy();
|
||||
node = undefined;
|
||||
} else if (node.status === STATUS.DESTROYED) {
|
||||
node = undefined;
|
||||
}
|
||||
}
|
||||
if (isDynamic && node && node.component.constructor !== name) {
|
||||
node = undefined;
|
||||
@@ -31,11 +89,22 @@ 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
|
||||
const C = isDynamic ? name : parent.constructor.components[name as any];
|
||||
node = new ComponentNode(C, props, ctx.app);
|
||||
let C;
|
||||
if (isDynamic) {
|
||||
C = name;
|
||||
} else {
|
||||
C = parent.constructor.components[name as any];
|
||||
if (!C) {
|
||||
throw new Error(`Cannot find the definition of component "${name}"`);
|
||||
}
|
||||
}
|
||||
node = new ComponentNode(C, props, ctx.app, ctx);
|
||||
ctx.children[key] = node;
|
||||
|
||||
const fiber = makeChildFiber(node, parentFiber);
|
||||
@@ -45,20 +114,15 @@ export function component(
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Component VNode
|
||||
// Component VNode class
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let currentNode: ComponentNode | null = null;
|
||||
|
||||
export function getCurrent(): ComponentNode | null {
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
type LifecycleHook = Function;
|
||||
|
||||
export class ComponentNode<T extends typeof Component = any> implements VNode<ComponentNode> {
|
||||
export class ComponentNode<T extends typeof Component = typeof Component>
|
||||
implements VNode<ComponentNode>
|
||||
{
|
||||
el?: HTMLElement | Text | undefined;
|
||||
handlers: any = null;
|
||||
app: App;
|
||||
fiber: Fiber | null = null;
|
||||
component: InstanceType<T>;
|
||||
@@ -66,8 +130,10 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
|
||||
status: STATUS = STATUS.NEW;
|
||||
|
||||
renderFn: Function;
|
||||
parent: ComponentNode | null;
|
||||
level: number;
|
||||
childEnv: Env;
|
||||
children: { [key: string]: ComponentNode } = Object.create(null);
|
||||
slots: any = {};
|
||||
refs: any = {};
|
||||
|
||||
willStart: LifecycleHook[] = [];
|
||||
@@ -76,118 +142,165 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
|
||||
mounted: LifecycleHook[] = [];
|
||||
willPatch: LifecycleHook[] = [];
|
||||
patched: LifecycleHook[] = [];
|
||||
destroyed: LifecycleHook[] = [];
|
||||
willDestroy: LifecycleHook[] = [];
|
||||
|
||||
constructor(C: T, props: any, app: App) {
|
||||
constructor(C: T, props: any, app: App, parent?: ComponentNode) {
|
||||
currentNode = this;
|
||||
this.app = app;
|
||||
this.component = new C(props, app.env, this) as any;
|
||||
this.renderFn = app.getTemplate(C.template).bind(null, this.component, this);
|
||||
this.parent = parent || null;
|
||||
this.level = parent ? parent.level + 1 : 0;
|
||||
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();
|
||||
}
|
||||
|
||||
mountComponent(target: any): Promise<InstanceType<T>> {
|
||||
const fiber = new MountFiber(this, target);
|
||||
mountComponent(target: any, options?: MountOptions) {
|
||||
const fiber = new MountFiber(this, target, options);
|
||||
this.app.scheduler.addFiber(fiber);
|
||||
this.initiateRender(fiber);
|
||||
return fiber.promise.then(() => this.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;
|
||||
const prom = Promise.all(this.willStart.map((f) => f.call(component)));
|
||||
await prom;
|
||||
try {
|
||||
await Promise.all(this.willStart.map((f) => f.call(component)));
|
||||
} catch (e) {
|
||||
handleError({ node: this, error: e });
|
||||
return;
|
||||
}
|
||||
if (this.status === STATUS.NEW && this.fiber === fiber) {
|
||||
this._render(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
async render() {
|
||||
if (this.fiber && !this.fiber.bdom) {
|
||||
return this.fiber.root.promise;
|
||||
async render(force: boolean = false) {
|
||||
let current = this.fiber;
|
||||
if (current && current.root!.locked) {
|
||||
await Promise.resolve();
|
||||
// situation may have changed after the microtask tick
|
||||
current = this.fiber;
|
||||
}
|
||||
if (!this.bdom && !this.fiber) {
|
||||
// should find a way to return the future mounting promise
|
||||
if (current && !current.bdom && !fibersInError.has(current)) {
|
||||
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();
|
||||
if (this.status === STATUS.DESTROYED) {
|
||||
return;
|
||||
}
|
||||
if (this.fiber === fiber) {
|
||||
// We only want to actually render the component if the following two
|
||||
// conditions are true:
|
||||
// * this.fiber: it could be null, in which case the render has been cancelled
|
||||
// * (current || !fiber.parent): if current is not null, this means that the
|
||||
// render function was called when a render was already occurring. In this
|
||||
// case, the pending rendering was cancelled, and the fiber needs to be
|
||||
// rendered to complete the work. If current is null, we check that the
|
||||
// fiber has no parent. If that is the case, the fiber was downgraded from
|
||||
// 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 === fiber && (current || !fiber.parent)) {
|
||||
this._render(fiber);
|
||||
}
|
||||
return fiber.root.promise;
|
||||
}
|
||||
|
||||
_render(fiber: Fiber | RootFiber) {
|
||||
try {
|
||||
fiber.bdom = this.renderFn();
|
||||
fiber.root!.counter--;
|
||||
} catch (e) {
|
||||
fiber.root.error = e;
|
||||
this.handleError(fiber);
|
||||
handleError({ node: this, error: e });
|
||||
}
|
||||
fiber.root.counter--;
|
||||
}
|
||||
|
||||
handleError(fiber: Fiber) {
|
||||
fiber.node.app.destroy();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.status === STATUS.MOUNTED) {
|
||||
callWillUnmount(this);
|
||||
let shouldRemove = this.status === STATUS.MOUNTED;
|
||||
this._destroy();
|
||||
if (shouldRemove) {
|
||||
this.bdom!.remove();
|
||||
}
|
||||
callDestroyed(this);
|
||||
}
|
||||
|
||||
function callWillUnmount(node: ComponentNode) {
|
||||
const component = node.component;
|
||||
for (let cb of node.willUnmount) {
|
||||
cb.call(component);
|
||||
}
|
||||
for (let child of Object.values(node.children)) {
|
||||
if (child.status === STATUS.MOUNTED) {
|
||||
callWillUnmount(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function callDestroyed(node: ComponentNode) {
|
||||
const component = node.component;
|
||||
node.status = STATUS.DESTROYED;
|
||||
for (let child of Object.values(node.children)) {
|
||||
callDestroyed(child);
|
||||
}
|
||||
for (let cb of node.destroyed) {
|
||||
_destroy() {
|
||||
const component = this.component;
|
||||
if (this.status === STATUS.MOUNTED) {
|
||||
for (let cb of this.willUnmount) {
|
||||
cb.call(component);
|
||||
}
|
||||
}
|
||||
for (let child of Object.values(this.children)) {
|
||||
child._destroy();
|
||||
}
|
||||
for (let cb of this.willDestroy) {
|
||||
cb.call(component);
|
||||
}
|
||||
this.status = STATUS.DESTROYED;
|
||||
}
|
||||
|
||||
async updateAndRender(props: any, parentFiber: Fiber) {
|
||||
// update
|
||||
const fiber = makeChildFiber(this, parentFiber);
|
||||
if (this.willPatch.length) {
|
||||
parentFiber.root.willPatch.push(fiber);
|
||||
}
|
||||
if (this.patched.length) {
|
||||
parentFiber.root.patched.push(fiber);
|
||||
}
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
this.component.props = props;
|
||||
component.props = props;
|
||||
this._render(fiber);
|
||||
const parentRoot = parentFiber.root!;
|
||||
if (this.willPatch.length) {
|
||||
parentRoot.willPatch.push(fiber);
|
||||
}
|
||||
if (this.patched.length) {
|
||||
parentRoot.patched.push(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a child that has dom that is not yet updated, and update it. This
|
||||
* method is meant to be used only in the context of repatching the dom after
|
||||
* a mounted hook failed and was handled.
|
||||
*/
|
||||
updateDom() {
|
||||
if (!this.fiber) {
|
||||
return;
|
||||
}
|
||||
if (this.bdom === this.fiber!.bdom) {
|
||||
// If the error was handled by some child component, we need to find it to
|
||||
// apply its change
|
||||
for (let k in this.children) {
|
||||
const child = this.children[k];
|
||||
child.updateDom();
|
||||
}
|
||||
} else {
|
||||
// if we get here, this is the component that handled the error and rerendered
|
||||
// itself, so we can simply patch the dom
|
||||
this.bdom!.patch(this.fiber!.bdom, false);
|
||||
this.fiber!.appliedToDom = true;
|
||||
this.fiber = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -206,18 +319,6 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
|
||||
this.status = STATUS.MOUNTED;
|
||||
this.fiber!.appliedToDom = true;
|
||||
this.fiber = null;
|
||||
if (this.handlers) {
|
||||
for (let i = 0; i < this.handlers.length; i++) {
|
||||
const handler = this.handlers[i];
|
||||
const eventType = handler[0];
|
||||
const el = bdom.el!;
|
||||
el.addEventListener(eventType, (ev: Event) => {
|
||||
const info = this.handlers![i];
|
||||
const [, ctx, method] = info;
|
||||
(ctx.__owl__.component as any)[method](ev);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveBefore(other: ComponentNode | null, afterNode: Node | null) {
|
||||
@@ -225,32 +326,42 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
|
||||
}
|
||||
|
||||
patch() {
|
||||
this.bdom!.patch(this!.fiber!.bdom!, false);
|
||||
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() {
|
||||
visitRemovedNodes(this);
|
||||
this._destroy();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.bdom!.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function visitRemovedNodes(node: ComponentNode) {
|
||||
if (node.status === STATUS.MOUNTED) {
|
||||
const component = node.component;
|
||||
for (let cb of node.willUnmount) {
|
||||
cb.call(component);
|
||||
cleanOutdatedChildren() {
|
||||
const children = this.children;
|
||||
for (const key in children) {
|
||||
const node = children[key];
|
||||
const status = node.status;
|
||||
if (status !== STATUS.MOUNTED) {
|
||||
delete children[key];
|
||||
if (status !== STATUS.DESTROYED) {
|
||||
node.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let child of Object.values(node.children)) {
|
||||
visitRemovedNodes(child);
|
||||
}
|
||||
node.status = STATUS.DESTROYED;
|
||||
if (node.destroyed.length) {
|
||||
__internal__destroyed.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { ComponentNode } from "./component_node";
|
||||
import type { Fiber } from "./fibers";
|
||||
|
||||
// Maps fibers to thrown errors
|
||||
export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
|
||||
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
|
||||
|
||||
function _handleError(node: ComponentNode | null, error: any, isFirstRound = false): boolean {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
const fiber = node.fiber;
|
||||
if (fiber) {
|
||||
fibersInError.set(fiber, error);
|
||||
}
|
||||
|
||||
const errorHandlers = nodeErrorHandlers.get(node);
|
||||
if (errorHandlers) {
|
||||
let stopped = false;
|
||||
// execute in the opposite order
|
||||
for (let i = errorHandlers.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
errorHandlers[i](error);
|
||||
stopped = true;
|
||||
break;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
|
||||
if (stopped) {
|
||||
if (isFirstRound && fiber && fiber.node.fiber) {
|
||||
fiber.root!.counter--;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return _handleError(node.parent, error);
|
||||
}
|
||||
|
||||
type ErrorParams = { error: any } & ({ node: ComponentNode } | { fiber: Fiber });
|
||||
export function handleError(params: ErrorParams) {
|
||||
const error = params.error;
|
||||
const node = "node" in params ? params.node : params.fiber.node;
|
||||
const fiber = "fiber" in params ? params.fiber : node.fiber!;
|
||||
|
||||
// resets the fibers on components if possible. This is important so that
|
||||
// new renderings can be properly included in the initial one, if any.
|
||||
let current: Fiber | null = fiber;
|
||||
do {
|
||||
current.node.fiber = current;
|
||||
current = current.parent;
|
||||
} while (current);
|
||||
|
||||
fibersInError.set(fiber.root!, error);
|
||||
|
||||
const handled = _handleError(node, error, true);
|
||||
if (!handled) {
|
||||
console.warn(`[Owl] Unhandled error. Destroying the root component`);
|
||||
try {
|
||||
node.app.destroy();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+99
-85
@@ -1,42 +1,42 @@
|
||||
import type { BDom } from "../blockdom";
|
||||
import { mount } from "../blockdom";
|
||||
import { BDom, mount } from "../blockdom";
|
||||
import type { ComponentNode } from "./component_node";
|
||||
import { fibersInError, handleError } from "./error_handling";
|
||||
import { STATUS } from "./status";
|
||||
// import { mountBlock } from "./bdom/block";
|
||||
|
||||
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
|
||||
let current = node.fiber;
|
||||
if (current) {
|
||||
// current is necessarily a rootfiber here
|
||||
let root = parent.root;
|
||||
cancelFibers(root, current.children);
|
||||
current.children = [];
|
||||
current.parent = parent;
|
||||
root.counter++;
|
||||
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);
|
||||
current.appliedToDom = false;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
const fiber = new RootFiber(node);
|
||||
const fiber = new RootFiber(node, null);
|
||||
if (node.willPatch.length) {
|
||||
fiber.willPatch.push(fiber);
|
||||
}
|
||||
if (node.patched.length) {
|
||||
fiber.patched.push(fiber);
|
||||
}
|
||||
|
||||
fiber.force = force;
|
||||
return fiber;
|
||||
}
|
||||
|
||||
@@ -59,17 +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;
|
||||
node.fiber = this;
|
||||
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);
|
||||
@@ -81,105 +82,118 @@ export class Fiber {
|
||||
|
||||
export class RootFiber extends Fiber {
|
||||
counter: number = 1;
|
||||
error: Error | null = null;
|
||||
resolve: any;
|
||||
promise: Promise<any>;
|
||||
reject: any;
|
||||
|
||||
// only add stuff in this if they have registered some hooks
|
||||
willPatch: Fiber[] = [];
|
||||
patched: Fiber[] = [];
|
||||
mounted: Fiber[] = [];
|
||||
|
||||
constructor(node: ComponentNode) {
|
||||
super(node, null);
|
||||
this.counter = 1;
|
||||
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
}
|
||||
// A fiber is typically locked when it is completing and the patch has not, or is being applied.
|
||||
// i.e.: render triggered in onWillUnmount or in willPatch will be delayed
|
||||
locked: boolean = false;
|
||||
|
||||
complete() {
|
||||
const node = this.node;
|
||||
|
||||
// Step 1: calling all willPatch lifecycle hooks
|
||||
for (let fiber of this.willPatch) {
|
||||
// because of the asynchronous nature of the rendering, some parts of the
|
||||
// UI may have been rendered, then deleted in a followup rendering, and we
|
||||
// do not want to call onWillPatch in that case.
|
||||
let node = fiber.node;
|
||||
if (node.fiber === fiber) {
|
||||
const component = node.component;
|
||||
for (let cb of node.willPatch) {
|
||||
cb.call(component);
|
||||
this.locked = true;
|
||||
let current: Fiber | undefined = undefined;
|
||||
try {
|
||||
// Step 1: calling all willPatch lifecycle hooks
|
||||
for (current of this.willPatch) {
|
||||
// because of the asynchronous nature of the rendering, some parts of the
|
||||
// UI may have been rendered, then deleted in a followup rendering, and we
|
||||
// do not want to call onWillPatch in that case.
|
||||
let node = current.node;
|
||||
if (node.fiber === current) {
|
||||
const component = node.component;
|
||||
for (let cb of node.willPatch) {
|
||||
cb.call(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
current = undefined;
|
||||
|
||||
// Step 2: patching the dom
|
||||
node.bdom!.patch(this.bdom!, Object.keys(node.children).length > 0);
|
||||
this.appliedToDom = true;
|
||||
// Step 2: patching the dom
|
||||
node._patch();
|
||||
this.locked = false;
|
||||
|
||||
// Step 3: calling all destroyed hooks
|
||||
for (let node of __internal__destroyed) {
|
||||
for (let cb of node.destroyed) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
__internal__destroyed.length = 0;
|
||||
|
||||
// Step 4: calling all mounted lifecycle hooks
|
||||
let current;
|
||||
let mountedFibers = this.mounted;
|
||||
while ((current = mountedFibers.pop())) {
|
||||
if (current.appliedToDom) {
|
||||
for (let cb of current.node.mounted) {
|
||||
cb();
|
||||
// Step 4: calling all mounted lifecycle hooks
|
||||
let mountedFibers = this.mounted;
|
||||
while ((current = mountedFibers.pop())) {
|
||||
current = current;
|
||||
if (current.appliedToDom) {
|
||||
for (let cb of current.node.mounted) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: calling all patched hooks
|
||||
let patchedFibers = this.patched;
|
||||
while ((current = patchedFibers.pop())) {
|
||||
if (current.appliedToDom) {
|
||||
for (let cb of current.node.patched) {
|
||||
cb();
|
||||
// Step 5: calling all patched hooks
|
||||
let patchedFibers = this.patched;
|
||||
while ((current = patchedFibers.pop())) {
|
||||
current = current;
|
||||
if (current.appliedToDom) {
|
||||
for (let cb of current.node.patched) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.locked = false;
|
||||
handleError({ fiber: current || this, error: e });
|
||||
}
|
||||
|
||||
// unregistering the fiber
|
||||
node.fiber = null;
|
||||
}
|
||||
}
|
||||
|
||||
export let __internal__destroyed: ComponentNode[] = [];
|
||||
type Position = "first-child" | "last-child";
|
||||
|
||||
export interface MountOptions {
|
||||
position?: Position;
|
||||
}
|
||||
|
||||
export class MountFiber extends RootFiber {
|
||||
target: HTMLElement;
|
||||
position: Position;
|
||||
|
||||
constructor(node: ComponentNode, target: HTMLElement) {
|
||||
super(node);
|
||||
constructor(node: ComponentNode, target: HTMLElement, options: MountOptions = {}) {
|
||||
super(node, null);
|
||||
this.target = target;
|
||||
this.position = options.position || "last-child";
|
||||
}
|
||||
complete() {
|
||||
const node = this.node;
|
||||
node.bdom = this.bdom;
|
||||
mount(node.bdom!, this.target);
|
||||
node.status = STATUS.MOUNTED;
|
||||
this.appliedToDom = true;
|
||||
let current;
|
||||
let mountedFibers = this.mounted;
|
||||
while ((current = mountedFibers.pop())) {
|
||||
if (current.appliedToDom) {
|
||||
for (let cb of current.node.mounted) {
|
||||
cb();
|
||||
let current: Fiber | undefined = this;
|
||||
try {
|
||||
const node = this.node;
|
||||
if (node.bdom) {
|
||||
// this is a complicated situation: if we mount a fiber with an existing
|
||||
// bdom, this means that this same fiber was already completed, mounted,
|
||||
// but a crash occurred in some mounted hook. Then, it was handled and
|
||||
// the new rendering is being applied.
|
||||
node.updateDom();
|
||||
} else {
|
||||
node.bdom = this.bdom;
|
||||
if (this.position === "last-child" || this.target.childNodes.length === 0) {
|
||||
mount(node.bdom!, this.target);
|
||||
} else {
|
||||
const firstChild = this.target.childNodes[0];
|
||||
mount(node.bdom!, this.target, firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// unregistering the fiber before mounted since it can do another render
|
||||
// and that the current rendering is obviously completed
|
||||
node.fiber = null;
|
||||
|
||||
node.status = STATUS.MOUNTED;
|
||||
this.appliedToDom = true;
|
||||
let mountedFibers = this.mounted;
|
||||
while ((current = mountedFibers.pop())) {
|
||||
if (current.appliedToDom) {
|
||||
for (let cb of current.node.mounted) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
handleError({ fiber: current as Fiber, error: e });
|
||||
}
|
||||
node.fiber = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,44 @@
|
||||
export function mainEventHandler(data: any, ev: Event) {
|
||||
if (typeof data === "function") {
|
||||
data(ev);
|
||||
} else {
|
||||
const ctx = data[0];
|
||||
const method = data[1];
|
||||
const args = data[2] || [];
|
||||
ctx.__owl__.component[method](...args, ev);
|
||||
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);
|
||||
data = _data;
|
||||
let stopped = false;
|
||||
if (modifiers.length) {
|
||||
let selfMode = false;
|
||||
const isSelf = ev.target === currentTarget;
|
||||
for (const mod of modifiers) {
|
||||
switch (mod) {
|
||||
case "self":
|
||||
selfMode = true;
|
||||
if (isSelf) {
|
||||
continue;
|
||||
} else {
|
||||
return stopped;
|
||||
}
|
||||
case "prevent":
|
||||
if ((selfMode && isSelf) || !selfMode) ev.preventDefault();
|
||||
continue;
|
||||
case "stop":
|
||||
if ((selfMode && isSelf) || !selfMode) ev.stopPropagation();
|
||||
stopped = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
|
||||
// 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)) {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { getCurrent } from "./component_node";
|
||||
import { nodeErrorHandlers } from "./error_handling";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// hooks
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export function onWillStart(fn: () => Promise<void> | void | any) {
|
||||
const node = getCurrent()!;
|
||||
node.willStart.push(fn.bind(node.component));
|
||||
}
|
||||
|
||||
export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) {
|
||||
const node = getCurrent()!;
|
||||
node.willUpdateProps.push(fn.bind(node.component));
|
||||
}
|
||||
|
||||
export function onMounted(fn: () => void | any) {
|
||||
const node = getCurrent()!;
|
||||
node.mounted.push(fn.bind(node.component));
|
||||
}
|
||||
|
||||
export function onWillPatch(fn: () => Promise<void> | any | void) {
|
||||
const node = getCurrent()!;
|
||||
node.willPatch.unshift(fn.bind(node.component));
|
||||
}
|
||||
|
||||
export function onPatched(fn: () => void | any) {
|
||||
const node = getCurrent()!;
|
||||
node.patched.push(fn.bind(node.component));
|
||||
}
|
||||
|
||||
export function onWillUnmount(fn: () => Promise<void> | void | any) {
|
||||
const node = getCurrent()!;
|
||||
node.willUnmount.unshift(fn.bind(node.component));
|
||||
}
|
||||
|
||||
export function onWillDestroy(fn: () => Promise<void> | void | any) {
|
||||
const node = getCurrent()!;
|
||||
node.willDestroy.push(fn.bind(node.component));
|
||||
}
|
||||
|
||||
export function onWillRender(fn: () => void | any) {
|
||||
const node = getCurrent()!;
|
||||
const renderFn = node.renderFn;
|
||||
node.renderFn = () => {
|
||||
fn.call(node.component);
|
||||
return renderFn();
|
||||
};
|
||||
}
|
||||
|
||||
export function onRendered(fn: () => void | any) {
|
||||
const node = getCurrent()!;
|
||||
const renderFn = node.renderFn;
|
||||
node.renderFn = () => {
|
||||
const result = renderFn();
|
||||
fn.call(node.component);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
type OnErrorCallback = (error: any) => void | any;
|
||||
export function onError(callback: OnErrorCallback) {
|
||||
const node = getCurrent()!;
|
||||
let handlers = nodeErrorHandlers.get(node);
|
||||
if (!handlers) {
|
||||
handlers = [];
|
||||
nodeErrorHandlers.set(node, handlers);
|
||||
}
|
||||
handlers.push(callback.bind(node.component));
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Component } from "./component";
|
||||
|
||||
/**
|
||||
* Apply default props (only top level).
|
||||
*
|
||||
* Note that this method does modify in place the props
|
||||
*/
|
||||
export function applyDefaultProps(props: { [key: string]: any }, ComponentClass: typeof Component) {
|
||||
const defaultProps = (ComponentClass as any).defaultProps;
|
||||
if (defaultProps) {
|
||||
for (let propName in defaultProps) {
|
||||
if (props![propName] === undefined) {
|
||||
props![propName] = defaultProps[propName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Prop validation helper
|
||||
//------------------------------------------------------------------------------
|
||||
function getPropDescription(staticProps: any) {
|
||||
if (staticProps instanceof Array) {
|
||||
return Object.fromEntries(
|
||||
staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true]))
|
||||
);
|
||||
}
|
||||
return staticProps || { "*": true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the component props (or next props) against the (static) props
|
||||
* description. This is potentially an expensive operation: it may needs to
|
||||
* visit recursively the props and all the children to check if they are valid.
|
||||
* This is why it is only done in 'dev' mode.
|
||||
*/
|
||||
export const validateProps = function (name: string | typeof Component, props: any, parent?: any) {
|
||||
const ComponentClass = (
|
||||
typeof name !== "string" ? name : parent.constructor.components[name]
|
||||
) as typeof Component;
|
||||
|
||||
applyDefaultProps(props, ComponentClass);
|
||||
|
||||
let propsDef = getPropDescription(ComponentClass.props);
|
||||
const allowAdditionalProps = "*" in propsDef;
|
||||
|
||||
for (let propName in propsDef) {
|
||||
if (propName === "*") {
|
||||
continue;
|
||||
}
|
||||
if (props[propName] === undefined) {
|
||||
if (propsDef[propName] && !propsDef[propName].optional) {
|
||||
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let isValid;
|
||||
try {
|
||||
isValid = isValidProp(props[propName], propsDef[propName]);
|
||||
} catch (e) {
|
||||
(e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
|
||||
(e as Error).message
|
||||
})`;
|
||||
throw e;
|
||||
}
|
||||
if (!isValid) {
|
||||
throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
|
||||
}
|
||||
}
|
||||
if (!allowAdditionalProps) {
|
||||
for (let propName in props) {
|
||||
if (!(propName in propsDef)) {
|
||||
throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an invidual prop value matches its (static) prop definition
|
||||
*/
|
||||
function isValidProp(prop: any, propDef: any): boolean {
|
||||
if (propDef === true) {
|
||||
return true;
|
||||
}
|
||||
if (typeof propDef === "function") {
|
||||
// Check if a value is constructed by some Constructor. Note that there is a
|
||||
// slight abuse of language: we want to consider primitive values as well.
|
||||
//
|
||||
// So, even though 1 is not an instance of Number, we want to consider that
|
||||
// it is valid.
|
||||
if (typeof prop === "object") {
|
||||
return prop instanceof propDef;
|
||||
}
|
||||
return typeof prop === propDef.name.toLowerCase();
|
||||
} else if (propDef instanceof Array) {
|
||||
// If this code is executed, this means that we want to check if a prop
|
||||
// matches at least one of its descriptor.
|
||||
let result = false;
|
||||
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
|
||||
result = result || isValidProp(prop, propDef[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// propsDef is an object
|
||||
if (propDef.optional && prop === undefined) {
|
||||
return true;
|
||||
}
|
||||
let result = propDef.type ? isValidProp(prop, propDef.type) : true;
|
||||
if (propDef.validate) {
|
||||
result = result && propDef.validate(prop);
|
||||
}
|
||||
if (propDef.type === Array && propDef.element) {
|
||||
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
||||
result = result && isValidProp(prop[i], propDef.element);
|
||||
}
|
||||
}
|
||||
if (propDef.type === Object && propDef.shape) {
|
||||
const shape = propDef.shape;
|
||||
for (let key in shape) {
|
||||
result = result && isValidProp(prop[key], shape[key]);
|
||||
}
|
||||
if (result) {
|
||||
for (let propName in prop) {
|
||||
if (!(propName in shape)) {
|
||||
throw new Error(`unknown prop '${propName}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
+11
-13
@@ -1,3 +1,4 @@
|
||||
import { fibersInError } from "./error_handling";
|
||||
import { Fiber, RootFiber } from "./fibers";
|
||||
import { STATUS } from "./status";
|
||||
|
||||
@@ -6,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() {
|
||||
@@ -24,7 +28,7 @@ export class Scheduler {
|
||||
}
|
||||
|
||||
addFiber(fiber: Fiber) {
|
||||
this.tasks.add(fiber.root);
|
||||
this.tasks.add(fiber.root!);
|
||||
if (!this.isRunning) {
|
||||
this.start();
|
||||
}
|
||||
@@ -37,29 +41,23 @@ export class Scheduler {
|
||||
flush() {
|
||||
this.tasks.forEach((fiber) => {
|
||||
if (fiber.root !== fiber) {
|
||||
// this is wrong! should be something like
|
||||
// if (this.tasks.has(fiber.root)) {
|
||||
// // parent rendering has completed
|
||||
// fiber.resolve();
|
||||
// this.tasks.delete(fiber);
|
||||
// }
|
||||
this.tasks.delete(fiber);
|
||||
return;
|
||||
}
|
||||
if (fiber.error) {
|
||||
const hasError = fibersInError.has(fiber);
|
||||
if (hasError && fiber.counter !== 0) {
|
||||
this.tasks.delete(fiber);
|
||||
fiber.reject(fiber.error);
|
||||
return;
|
||||
}
|
||||
if (fiber.node.status === STATUS.DESTROYED) {
|
||||
this.tasks.delete(fiber);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fiber.counter === 0) {
|
||||
if (!fiber.error) {
|
||||
if (!hasError) {
|
||||
fiber.complete();
|
||||
}
|
||||
fiber.resolve();
|
||||
this.tasks.delete(fiber);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export class EventBus extends EventTarget {
|
||||
trigger(name: string, payload?: any) {
|
||||
this.dispatchEvent(new CustomEvent(name, { detail: payload }));
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import type { Env } from "./app/app";
|
||||
import { getCurrent } from "./component/component_node";
|
||||
import { onMounted, onPatched, onWillUnmount } from "./component/lifecycle_hooks";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useRef
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The purpose of this hook is to allow components to get a reference to a sub
|
||||
* html node or component.
|
||||
*/
|
||||
export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el: T | null } {
|
||||
const node = getCurrent()!;
|
||||
const refs = node.refs;
|
||||
return {
|
||||
get el(): T | null {
|
||||
return refs[name] || null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useEnv and useSubEnv
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This hook is useful as a building block for some customized hooks, that may
|
||||
* need a reference to the env of the component calling them.
|
||||
*/
|
||||
export function useEnv<E extends Env>(): E {
|
||||
return getCurrent()!.component.env as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook is a simple way to let components use a sub environment. Note that
|
||||
* like for all hooks, it is important that this is only called in the
|
||||
* constructor method.
|
||||
*/
|
||||
export function useSubEnv(envExtension: Env) {
|
||||
const node = getCurrent()!;
|
||||
const env = Object.create(node.childEnv);
|
||||
const descrs = Object.getOwnPropertyDescriptors(envExtension);
|
||||
node.childEnv = Object.freeze(Object.defineProperties(env, descrs));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useEffect
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const NO_OP = () => {};
|
||||
/**
|
||||
* @param {...any} dependencies the dependencies computed by computeDependencies
|
||||
* @returns {void|(()=>void)} a cleanup function that reverses the side
|
||||
* effects of the effect callback.
|
||||
*/
|
||||
type Effect = (...dependencies: any[]) => void | (() => void);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {Effect} effect the effect to run on component mount and/or patch
|
||||
* @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
|
||||
* dependencies that will decide if the effect needs to be cleaned up and
|
||||
* run again. If the dependencies did not change, the effect will not run
|
||||
* again. The default value returns an array containing only NaN because
|
||||
* NaN !== NaN, which will cause the effect to rerun on every patch.
|
||||
*/
|
||||
export function useEffect(effect: Effect, computeDependencies: () => any[] = () => [NaN]) {
|
||||
let cleanup: () => void;
|
||||
let dependencies: any[];
|
||||
onMounted(() => {
|
||||
dependencies = computeDependencies();
|
||||
cleanup = effect(...dependencies) || NO_OP;
|
||||
});
|
||||
|
||||
onPatched(() => {
|
||||
const newDeps = computeDependencies();
|
||||
const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
|
||||
if (shouldReapply) {
|
||||
dependencies = newDeps;
|
||||
cleanup();
|
||||
cleanup = effect(...dependencies) || NO_OP;
|
||||
}
|
||||
});
|
||||
|
||||
onWillUnmount(() => cleanup());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useExternalListener
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* When a component needs to listen to DOM Events on element(s) that are not
|
||||
* part of his hierarchy, we can use the `useExternalListener` hook.
|
||||
* It will correctly add and remove the event listener, whenever the
|
||||
* component is mounted and unmounted.
|
||||
*
|
||||
* Example:
|
||||
* a menu needs to listen to the click on window to be closed automatically
|
||||
*
|
||||
* Usage:
|
||||
* in the constructor of the OWL component that needs to be notified,
|
||||
* `useExternalListener(window, 'click', this._doSomething);`
|
||||
* */
|
||||
export function useExternalListener(
|
||||
target: HTMLElement | typeof window,
|
||||
eventName: string,
|
||||
handler: EventListener,
|
||||
eventParams?: AddEventListenerOptions
|
||||
) {
|
||||
const node = getCurrent()!;
|
||||
const boundHandler = handler.bind(node.component);
|
||||
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
|
||||
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
|
||||
}
|
||||
+20
-31
@@ -1,3 +1,4 @@
|
||||
import { UTILS } from "./app/template_helpers";
|
||||
import {
|
||||
config,
|
||||
createBlock,
|
||||
@@ -9,11 +10,15 @@ import {
|
||||
remove,
|
||||
text,
|
||||
toggler,
|
||||
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,
|
||||
@@ -28,37 +33,19 @@ export const blockDom = {
|
||||
toggler,
|
||||
createBlock,
|
||||
html,
|
||||
comment,
|
||||
};
|
||||
|
||||
// import { makeBlockClass } from "./_old_bdom/element";
|
||||
import { App } from "./app";
|
||||
import { Component } from "./component/component";
|
||||
import { getCurrent } from "./component/component_node";
|
||||
// import { getCurrent } from "./b_node";
|
||||
|
||||
export { App, Component };
|
||||
|
||||
export async function mount<T extends typeof Component>(
|
||||
C: T,
|
||||
target: HTMLElement
|
||||
): Promise<InstanceType<T>> {
|
||||
const app = new App(C);
|
||||
return app.mount(target);
|
||||
}
|
||||
|
||||
export function useComponent(): Component {
|
||||
const current = getCurrent();
|
||||
return current!.component;
|
||||
}
|
||||
|
||||
export { App, mount } from "./app/app";
|
||||
export { Component } from "./component/component";
|
||||
export { useComponent } from "./component/component_node";
|
||||
export { status } from "./component/status";
|
||||
export { Portal } from "./misc/portal";
|
||||
export { Memo } from "./misc/memo";
|
||||
export { xml } from "./tags";
|
||||
export { useState } from "./reactivity";
|
||||
export { useRef } from "./refs";
|
||||
export { EventBus } from "./event_bus";
|
||||
|
||||
export { Memo } from "./memo";
|
||||
export { xml } from "./app/template_set";
|
||||
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 {
|
||||
onWillStart,
|
||||
onMounted,
|
||||
@@ -66,8 +53,10 @@ export {
|
||||
onWillUpdateProps,
|
||||
onWillPatch,
|
||||
onPatched,
|
||||
onRender,
|
||||
onDestroyed,
|
||||
} from "./lifecycle_hooks";
|
||||
onWillRender,
|
||||
onRendered,
|
||||
onWillDestroy,
|
||||
onError,
|
||||
} from "./component/lifecycle_hooks";
|
||||
|
||||
export const __info__ = {};
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { getCurrent } from "./component/component_node";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// hooks
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export function onWillStart(fn: () => Promise<void> | void | any) {
|
||||
const node = getCurrent()!;
|
||||
node.willStart.push(fn);
|
||||
}
|
||||
|
||||
export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) {
|
||||
const node = getCurrent()!;
|
||||
node.willUpdateProps.push(fn);
|
||||
}
|
||||
|
||||
export function onMounted(fn: () => void | any) {
|
||||
const node = getCurrent()!;
|
||||
node.mounted.push(fn);
|
||||
}
|
||||
|
||||
export function onWillPatch(fn: () => Promise<void> | any | void) {
|
||||
const node = getCurrent()!;
|
||||
node.willPatch.push(fn);
|
||||
}
|
||||
|
||||
export function onPatched(fn: () => void | any) {
|
||||
const node = getCurrent()!;
|
||||
node.patched.push(fn);
|
||||
}
|
||||
|
||||
export function onWillUnmount(fn: () => Promise<void> | void | any) {
|
||||
const node = getCurrent()!;
|
||||
node.willUnmount.push(fn);
|
||||
}
|
||||
|
||||
export function onDestroyed(fn: () => Promise<void> | void | any) {
|
||||
const node = getCurrent()!;
|
||||
node.destroyed.push(fn);
|
||||
}
|
||||
|
||||
export function onRender(fn: () => void | any) {
|
||||
const node = getCurrent()!;
|
||||
const renderFn = node.renderFn;
|
||||
node.renderFn = () => {
|
||||
fn();
|
||||
return renderFn();
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component } from "../component/component";
|
||||
import type { ComponentNode } from "../component/component_node";
|
||||
import { xml } from "../tags";
|
||||
import { Fiber } from "../component/fibers";
|
||||
import { Component } from "./component/component";
|
||||
import type { ComponentNode } from "./component/component_node";
|
||||
import { xml } from "./app/template_set";
|
||||
import { Fiber } from "./component/fibers";
|
||||
|
||||
export class Memo extends Component {
|
||||
static template = xml`<t t-slot="default"/>`;
|
||||
@@ -39,7 +39,7 @@ export class Memo extends Component {
|
||||
*/
|
||||
function shallowEqual(p1: any, p2: any): boolean {
|
||||
for (let k in p1) {
|
||||
if (p1[k] !== p2[k]) {
|
||||
if (k !== "slots" && p1[k] !== p2[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComponentNode } from "../component/component_node";
|
||||
import { Component } from "../component/component";
|
||||
import { xml } from "../tags";
|
||||
import { BDom, text, VNode } from "../blockdom";
|
||||
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;
|
||||
|
||||
@@ -35,9 +35,11 @@ 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) {
|
||||
@@ -53,13 +55,21 @@ 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,
|
||||
};
|
||||
|
||||
constructor(props: any, env: any, node: ComponentNode) {
|
||||
super(props, env, node);
|
||||
node._render = function (fiber: any) {
|
||||
const bdom = new VPortal(props.target, this.renderFn());
|
||||
fiber.bdom = bdom;
|
||||
fiber.root.counter--;
|
||||
};
|
||||
setup() {
|
||||
const node = this.__owl__;
|
||||
const renderFn = node.renderFn;
|
||||
node.renderFn = () => new VPortal(this.props.target, renderFn());
|
||||
onWillUnmount(() => {
|
||||
if (node.bdom) {
|
||||
node.bdom.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
// import { compileTemplate, Template } from "./qweb/index";
|
||||
import { BDom, createBlock, html, list, multi, text, toggler } from "../blockdom";
|
||||
import { component } from "../component/component_node";
|
||||
import { Template, compileTemplate } from "./compiler";
|
||||
|
||||
const bdom = { text, createBlock, list, multi, html, toggler, component };
|
||||
|
||||
export const globalTemplates: { [key: string]: string } = {};
|
||||
|
||||
function withDefault(value: any, defaultValue: any): any {
|
||||
return value === undefined || value === null || value === false ? defaultValue : value;
|
||||
}
|
||||
|
||||
function callSlot(
|
||||
ctx: any,
|
||||
parent: any,
|
||||
key: string,
|
||||
name: string,
|
||||
defaultSlot?: (ctx: any, key: string) => BDom,
|
||||
dynamic?: boolean
|
||||
): BDom | null {
|
||||
const slots = ctx.__owl__.slots;
|
||||
const slotFn = slots[name];
|
||||
const slotBDom = slotFn ? slotFn(parent, key) : null;
|
||||
if (defaultSlot) {
|
||||
let child1: BDom | undefined = undefined;
|
||||
let child2: BDom | undefined = undefined;
|
||||
// const result = new BMulti(2);
|
||||
if (slotBDom) {
|
||||
child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
|
||||
} else {
|
||||
child2 = defaultSlot(parent, key);
|
||||
}
|
||||
return multi([child1, child2]);
|
||||
}
|
||||
return slotBDom;
|
||||
}
|
||||
|
||||
function capture(ctx: any): any {
|
||||
const component = ctx.__owl__.component;
|
||||
const result = Object.create(component);
|
||||
for (let k in ctx) {
|
||||
result[k] = ctx[k];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function withKey(elem: any, k: string) {
|
||||
elem.key = k;
|
||||
return elem;
|
||||
}
|
||||
|
||||
function prepareList(collection: any): [any[], any[], number, any[]] {
|
||||
let keys: any[];
|
||||
let values: any[];
|
||||
|
||||
if (Array.isArray(collection)) {
|
||||
keys = collection;
|
||||
values = collection;
|
||||
} else if (collection) {
|
||||
values = Object.keys(collection);
|
||||
keys = Object.values(collection);
|
||||
} else {
|
||||
throw new Error("Invalid loop expression");
|
||||
}
|
||||
const n = values.length;
|
||||
return [keys, values, n, new Array(n)];
|
||||
}
|
||||
export const UTILS = {
|
||||
// elem,
|
||||
// setText,
|
||||
withDefault,
|
||||
zero: Symbol("zero"),
|
||||
callSlot,
|
||||
capture,
|
||||
// toClassObj,
|
||||
withKey,
|
||||
prepareList,
|
||||
shallowEqual,
|
||||
};
|
||||
|
||||
export class TemplateSet {
|
||||
rawTemplates: { [name: string]: string } = Object.create(globalTemplates);
|
||||
templates: { [name: string]: Template } = {};
|
||||
utils: typeof UTILS;
|
||||
|
||||
constructor() {
|
||||
const call = (subTemplate: string, ctx: any, parent: any) => {
|
||||
const template = this.getTemplate(subTemplate);
|
||||
return toggler(subTemplate, template(ctx, parent));
|
||||
};
|
||||
|
||||
const getTemplate = (name: string) => this.getTemplate(name);
|
||||
this.utils = Object.assign({}, UTILS, { getTemplate, call });
|
||||
}
|
||||
|
||||
addTemplate(name: string, template: string, options: { allowDuplicate?: boolean } = {}) {
|
||||
if (name in this.rawTemplates && !options.allowDuplicate) {
|
||||
throw new Error(`Template ${name} already defined`);
|
||||
}
|
||||
this.rawTemplates[name] = template;
|
||||
}
|
||||
|
||||
getTemplate(name: string): Template {
|
||||
if (!(name in this.templates)) {
|
||||
const rawTemplate = this.rawTemplates[name];
|
||||
if (rawTemplate === undefined) {
|
||||
throw new Error(`Missing template: "${name}"`);
|
||||
}
|
||||
const templateFn = compileTemplate(rawTemplate, name);
|
||||
|
||||
// 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 template = templateFn(bdom, this.utils);
|
||||
this.templates[name] = template;
|
||||
}
|
||||
return this.templates[name];
|
||||
}
|
||||
}
|
||||
|
||||
function shallowEqual(l1: any[], l2: any[]): boolean {
|
||||
for (let i = 0, l = l1.length; i < l; i++) {
|
||||
if (l1[i] !== l2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
+176
-130
@@ -1,144 +1,190 @@
|
||||
import { getCurrent } from "./component/component_node";
|
||||
import { Callback } from "./utils";
|
||||
|
||||
export function useState<T>(state: T): T {
|
||||
const node = getCurrent()!;
|
||||
return observe(state, () => node.render());
|
||||
}
|
||||
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
|
||||
export const TARGET = Symbol("Target");
|
||||
// Special key to subscribe to, to be notified of key creation/deletion
|
||||
const KEYCHANGES = Symbol("Key changes");
|
||||
|
||||
type CB = () => void;
|
||||
const observers: WeakMap<any, PSet<CB>> = new WeakMap();
|
||||
type ObjectKey = string | number | symbol;
|
||||
type Target = object;
|
||||
export type Reactive<T extends Target = Target> = T & {
|
||||
[TARGET]: any;
|
||||
};
|
||||
|
||||
/**
|
||||
* PSet (for Prototypal Set) are sets that can lookup in their "parent sets", if
|
||||
* any.
|
||||
* Checks whether a given value can be made into a reactive object.
|
||||
*
|
||||
* @param value the value to check
|
||||
* @returns whether the value can be made reactive
|
||||
*/
|
||||
|
||||
class PSet<T> extends Set<T> {
|
||||
parent?: PSet<T>;
|
||||
|
||||
static createChild<T>(parent: PSet<T>): PSet<T> {
|
||||
const pset: PSet<T> = new PSet();
|
||||
pset.parent = parent;
|
||||
return pset;
|
||||
}
|
||||
|
||||
has(key: T): boolean {
|
||||
if (super.has(key)) {
|
||||
return true;
|
||||
}
|
||||
return this.parent ? this.parent.has(key) : false;
|
||||
}
|
||||
|
||||
*[Symbol.iterator](): Generator<T> {
|
||||
let iterator = super[Symbol.iterator]();
|
||||
for (let elem of iterator) {
|
||||
yield elem;
|
||||
}
|
||||
if (this.parent) {
|
||||
for (let elem of this.parent) {
|
||||
yield elem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export function observe<T>(value: T, cb: CB): T {
|
||||
if (isNotObservable(value)) {
|
||||
return value;
|
||||
}
|
||||
if (observers.has(value)) {
|
||||
const callbacks = observers.get(value)!;
|
||||
callbacks.add(cb);
|
||||
return value;
|
||||
}
|
||||
const callbacks: PSet<CB> = new PSet();
|
||||
callbacks.add(cb);
|
||||
return observeValue(value, callbacks);
|
||||
}
|
||||
|
||||
export function unobserve<T>(value: T, cb: () => void) {
|
||||
if (isNotObservable(value)) {
|
||||
return;
|
||||
}
|
||||
if (observers.has(value)) {
|
||||
const callbacks = observers.get(value)!;
|
||||
callbacks.delete(cb);
|
||||
}
|
||||
}
|
||||
|
||||
function isNotObservable(value: any): boolean {
|
||||
function canBeMadeReactive(value: any): boolean {
|
||||
return (
|
||||
value === null || typeof value !== "object" || value instanceof Date || value instanceof Promise
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!(value instanceof Date) &&
|
||||
!(value instanceof Promise) &&
|
||||
!(value instanceof String) &&
|
||||
!(value instanceof Number)
|
||||
);
|
||||
}
|
||||
|
||||
const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>>>();
|
||||
/**
|
||||
* value should
|
||||
* 1. be observable
|
||||
* 2. not yet be observed
|
||||
* Observes a given key on a target with an callback. The callback will be
|
||||
* called when the given key changes on the target.
|
||||
*
|
||||
* @param target the target whose key should be observed
|
||||
* @param key the key to observe (or Symbol(KEYCHANGES) for key creation
|
||||
* or deletion)
|
||||
* @param callback the function to call when the key changes
|
||||
*/
|
||||
function observeValue(value: any, callbacks: PSet<CB>): any {
|
||||
const proxy = new Proxy(value as any, {
|
||||
get(target: any, key: any): any {
|
||||
const current = target[key];
|
||||
if (isNotObservable(current)) {
|
||||
return current;
|
||||
}
|
||||
if (observers.has(current)) {
|
||||
// this is wrong ?
|
||||
observers.get(current)!.parent = callbacks;
|
||||
return current;
|
||||
}
|
||||
const subCallbacks = PSet.createChild(callbacks);
|
||||
const subValue = observeValue(current, subCallbacks);
|
||||
target[key] = subValue;
|
||||
return subValue;
|
||||
},
|
||||
set(target: any, key: any, value: any): boolean {
|
||||
// TODO: check if current !== target or proxy ??
|
||||
const current = target[key];
|
||||
if (current !== value) {
|
||||
if (isNotObservable(value)) {
|
||||
target[key] = value;
|
||||
} else {
|
||||
// TODO: test following scenario:
|
||||
// 1. obj1 = observer({a:1}, somecb);
|
||||
// 2. unobserve(obj1, somecb)
|
||||
// 3. obj1.a = {b: 2};
|
||||
// check that somecb was not called
|
||||
// obj1.a.b = 3;
|
||||
// check again that somecb was not called
|
||||
if (observers.has(value)) {
|
||||
const pset = observers.get(value)!;
|
||||
pset.parent = callbacks;
|
||||
target[key] = value;
|
||||
} else {
|
||||
const subCallbacks = PSet.createChild(callbacks);
|
||||
target[key] = observeValue(value, subCallbacks);
|
||||
}
|
||||
}
|
||||
notify(target);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
deleteProperty(target: any, key: any) {
|
||||
if (key in target) {
|
||||
delete target[key];
|
||||
notify(target);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
observers.set(value, callbacks);
|
||||
observers.set(proxy, callbacks);
|
||||
return proxy;
|
||||
function observeTargetKey(target: Target, key: ObjectKey, callback: Callback): void {
|
||||
if (!targetToKeysToCallbacks.get(target)) {
|
||||
targetToKeysToCallbacks.set(target, new Map());
|
||||
}
|
||||
const keyToCallbacks = targetToKeysToCallbacks.get(target)!;
|
||||
if (!keyToCallbacks.get(key)) {
|
||||
keyToCallbacks.set(key, new Set());
|
||||
}
|
||||
keyToCallbacks.get(key)!.add(callback);
|
||||
if (!callbacksToTargets.has(callback)) {
|
||||
callbacksToTargets.set(callback, new Set());
|
||||
}
|
||||
callbacksToTargets.get(callback)!.add(target);
|
||||
}
|
||||
|
||||
function notify(value: any) {
|
||||
const cbs = observers.get(value)!;
|
||||
for (let cb of cbs) {
|
||||
cb();
|
||||
/**
|
||||
* Notify Reactives that are observing a given target that a key has changed on
|
||||
* the target.
|
||||
*
|
||||
* @param target target whose Reactives should be notified that the target was
|
||||
* changed.
|
||||
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
|
||||
* or deleted)
|
||||
*/
|
||||
function notifyReactives(target: Target, key: ObjectKey): void {
|
||||
const keyToCallbacks = targetToKeysToCallbacks.get(target);
|
||||
if (!keyToCallbacks) {
|
||||
return;
|
||||
}
|
||||
const callbacks = keyToCallbacks.get(key);
|
||||
if (!callbacks) {
|
||||
return;
|
||||
}
|
||||
// Loop on copy because clearReactivesForCallback will modify the set in place
|
||||
for (const callback of [...callbacks]) {
|
||||
clearReactivesForCallback(callback);
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
const callbacksToTargets = new WeakMap<Callback, Set<Target>>();
|
||||
/**
|
||||
* Clears all subscriptions of the Reactives associated with a given callback.
|
||||
*
|
||||
* @param callback the callback for which the reactives need to be cleared
|
||||
*/
|
||||
export function clearReactivesForCallback(callback: Callback): void {
|
||||
const targetsToClear = callbacksToTargets.get(callback);
|
||||
if (!targetsToClear) {
|
||||
return;
|
||||
}
|
||||
for (const target of targetsToClear) {
|
||||
const observedKeys = targetToKeysToCallbacks.get(target);
|
||||
if (!observedKeys) {
|
||||
continue;
|
||||
}
|
||||
for (const callbacks of observedKeys.values()) {
|
||||
callbacks.delete(callback);
|
||||
}
|
||||
}
|
||||
targetsToClear.clear();
|
||||
}
|
||||
|
||||
const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive>>();
|
||||
/**
|
||||
* Creates a reactive proxy for an object. Reading data on the reactive object
|
||||
* subscribes to changes to the data. Writing data on the object will cause the
|
||||
* notify callback to be called if there are suscriptions to that data. Nested
|
||||
* objects and arrays are automatically made reactive as well.
|
||||
*
|
||||
* Whenever you are notified of a change, all subscriptions are cleared, and if
|
||||
* you would like to be notified of any further changes, you should go read
|
||||
* the underlying data again. We assume that if you don't go read it again after
|
||||
* being notified, it means that you are no longer interested in that data.
|
||||
*
|
||||
* Subscriptions:
|
||||
* + Reading a property on an object will subscribe you to changes in the value
|
||||
* of that property.
|
||||
* + Accessing an object keys (eg with Object.keys or with `for..in`) will
|
||||
* subscribe you to the creation/deletion of keys. Checking the presence of a
|
||||
* key on the object with 'in' has the same effect.
|
||||
* - getOwnPropertyDescriptor does not currently subscribe you to the property.
|
||||
* This is a choice that was made because changing a key's value will trigger
|
||||
* this trap and we do not want to subscribe by writes. This also means that
|
||||
* Object.hasOwnProperty doesn't subscribe as it goes through this trap.
|
||||
*
|
||||
* @param target the object for which to create a reactive proxy
|
||||
* @param callback the function to call when an observed property of the
|
||||
* reactive has changed
|
||||
* @returns a proxy that tracks changes to it
|
||||
*/
|
||||
export function reactive<T extends Target>(target: T, callback: Callback = () => {}): Reactive<T> {
|
||||
if (!canBeMadeReactive(target)) {
|
||||
throw new Error(`Cannot make the given value reactive`);
|
||||
}
|
||||
const originalTarget = (target as Reactive)[TARGET];
|
||||
if (originalTarget) {
|
||||
return reactive(originalTarget, callback);
|
||||
}
|
||||
if (!reactiveCache.has(target)) {
|
||||
reactiveCache.set(target, new Map());
|
||||
}
|
||||
const reactivesForTarget = reactiveCache.get(target)!;
|
||||
if (!reactivesForTarget.has(callback)) {
|
||||
const proxy = new Proxy(target, {
|
||||
get(target: any, key: ObjectKey, proxy: Reactive<T>) {
|
||||
if (key === TARGET) {
|
||||
return target;
|
||||
}
|
||||
observeTargetKey(target, key, callback);
|
||||
const value = Reflect.get(target, key, proxy);
|
||||
if (!canBeMadeReactive(value)) {
|
||||
return value;
|
||||
}
|
||||
return reactive(value, callback);
|
||||
},
|
||||
set(target, key, value, proxy) {
|
||||
const isNewKey = !Object.hasOwnProperty.call(target, key);
|
||||
const originalValue = Reflect.get(target, key, proxy);
|
||||
const ret = Reflect.set(target, key, value, proxy);
|
||||
if (isNewKey) {
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
}
|
||||
// While Array length may trigger the set trap, it's not actually set by this
|
||||
// method but is updated behind the scenes, and the trap is not called with the
|
||||
// new value. We disable the "same-value-optimization" for it because of that.
|
||||
if (originalValue !== value || (Array.isArray(target) && key === "length")) {
|
||||
notifyReactives(target, key);
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
deleteProperty(target, key) {
|
||||
const ret = Reflect.deleteProperty(target, key);
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
notifyReactives(target, key);
|
||||
return ret;
|
||||
},
|
||||
ownKeys(target) {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
return Reflect.ownKeys(target);
|
||||
},
|
||||
has(target, key) {
|
||||
// TODO: this observes all key changes instead of only the presence of the argument key
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
return Reflect.has(target, key);
|
||||
},
|
||||
});
|
||||
reactivesForTarget.set(callback, proxy);
|
||||
}
|
||||
return reactivesForTarget.get(callback) as Reactive<T>;
|
||||
}
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// useRef
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
import type { Component } from "./component/component";
|
||||
import { getCurrent } from "./component/component_node";
|
||||
|
||||
/**
|
||||
* The purpose of this hook is to allow components to get a reference to a sub
|
||||
* html node or component.
|
||||
*/
|
||||
interface Ref<C extends Component = Component> {
|
||||
el: HTMLElement | null;
|
||||
comp: C | null;
|
||||
}
|
||||
|
||||
export function useRef<C extends Component = Component>(name: string): Ref<C> {
|
||||
const node = getCurrent()!;
|
||||
return {
|
||||
get el(): HTMLElement | null {
|
||||
const val = node.refs[name];
|
||||
return val!;
|
||||
// if (val instanceof HTMLElement) {
|
||||
// return val;
|
||||
// } else if (val instanceof Component) {
|
||||
// return val.el;
|
||||
// }
|
||||
// return null;
|
||||
},
|
||||
get comp(): C | null {
|
||||
return null;
|
||||
// const val = node.refs && node.refs[name];
|
||||
// return val instanceof Component ? (val as C) : null;
|
||||
},
|
||||
};
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { globalTemplates } from "./qweb/template_helpers";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Global templates
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export function xml(strings: TemplateStringsArray, ...args: any[]) {
|
||||
const name = `__template__${xml.nextId++}`;
|
||||
const value = String.raw(strings, ...args);
|
||||
globalTemplates[name] = value;
|
||||
return name;
|
||||
}
|
||||
|
||||
xml.nextId = 1;
|
||||
@@ -0,0 +1,64 @@
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
|
||||
export function whenReady(fn?: any): Promise<void> {
|
||||
return new Promise(function (resolve) {
|
||||
if (document.readyState !== "loading") {
|
||||
resolve(true);
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", resolve, false);
|
||||
}
|
||||
}).then(fn || function () {});
|
||||
}
|
||||
|
||||
export async function loadFile(url: string): Promise<string> {
|
||||
const result = await fetch(url);
|
||||
if (!result.ok) {
|
||||
throw new Error("Error while fetching xml templates");
|
||||
}
|
||||
return await result.text();
|
||||
}
|
||||
|
||||
/*
|
||||
* This class just transports the fact that a string is safe
|
||||
* to be injected as HTML. Overriding a JS primitive is quite painful though
|
||||
* so we need to redfine toString and valueOf.
|
||||
*/
|
||||
export class Markup extends String {}
|
||||
|
||||
/*
|
||||
* Marks a value as safe, that is, a value that can be injected as HTML directly.
|
||||
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
|
||||
*/
|
||||
export function markup(value: any) {
|
||||
return new Markup(value);
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Reactivity: useState concurrent renderings 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/><block-text-1/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['context'][ctx['props'].key].n;
|
||||
let d2 = ctx['state'].x;
|
||||
return block1([d1, d2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState concurrent renderings 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<p><block-child-0/></p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`ComponentC\`, {key: ctx['props'].key}, key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState concurrent renderings 3`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`ComponentB\`, {key: ctx['context'].key}, key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState destroyed component before being mounted is inactive 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2;
|
||||
if (ctx['state'].flag) {
|
||||
b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||
}
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState destroyed component before being mounted is inactive 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['contextObj'].a;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState destroyed component is inactive 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2;
|
||||
if (ctx['state'].flag) {
|
||||
b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||
}
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState destroyed component is inactive 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['contextObj'].a;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState one components can subscribe twice to same context 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><block-text-0/><block-text-1/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['contextObj1'].a;
|
||||
let txt2 = ctx['contextObj2'].b;
|
||||
return block1([txt1, txt2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState parent and children subscribed to same context 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/><block-text-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||
let txt1 = ctx['contextObj'].b;
|
||||
return block1([txt1], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState parent and children subscribed to same context 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['contextObj'].a;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState several nodes on different level use same context 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-text-0/> <block-text-1/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['contextObj'].a;
|
||||
let d2 = ctx['contextObj'].b;
|
||||
return block1([d1, d2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState several nodes on different level use same context 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['contextObj'].b;
|
||||
return block1([d1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState several nodes on different level use same context 3`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-text-0/><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['contextObj'].a;
|
||||
let b2 = component(\`L3A\`, {}, key + \`__1\`, node, ctx);
|
||||
return block1([d1], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState several nodes on different level use same context 4`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`L2A\`, {}, key + \`__1\`, node, ctx);
|
||||
let b3 = component(\`L2B\`, {}, key + \`__2\`, node, ctx);
|
||||
return block1([], [b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState two components are updated in parallel 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>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||
let b3 = component(\`Child\`, {}, key + \`__2\`, node, ctx);
|
||||
return block1([], [b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState two components are updated in parallel 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['contextObj'].value;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState two components can subscribe to same context 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>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||
let b3 = component(\`Child\`, {}, key + \`__2\`, node, ctx);
|
||||
return block1([], [b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState two components can subscribe to same context 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['contextObj'].value;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState two independent components on different levels are updated in parallel 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>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||
let b3 = component(\`Parent\`, {}, key + \`__2\`, node, ctx);
|
||||
return block1([], [b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState two independent components on different levels are updated in parallel 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['contextObj'].value;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState two independent components on different levels are updated in parallel 3`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState useContext=useState hook is reactive, for one component 1`] = `
|
||||
"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['contextObj'].value;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState useless atoms should be deleted 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { prepareList, withKey } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/> Total: <block-text-0/> Count: <block-text-1/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
const [k_block2, v_block2, l_block2, c_block2] = prepareList(Object.keys(ctx['state']));
|
||||
for (let i1 = 0; i1 < l_block2; i1++) {
|
||||
ctx[\`id\`] = v_block2[i1];
|
||||
let key1 = ctx['id'];
|
||||
c_block2[i1] = withKey(component(\`Quantity\`, {id: ctx['id']}, key + \`__1__\${key1}\`, node, ctx), key1);
|
||||
}
|
||||
ctx = ctx.__proto__;
|
||||
let b2 = list(c_block2);
|
||||
let txt1 = ctx['total'];
|
||||
let txt2 = Object.keys(ctx['state']).length;
|
||||
return block1([txt1, txt2], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState useless atoms should be deleted 2`] = `
|
||||
"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['state'].quantity;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState very simple use, with initial value 1`] = `
|
||||
"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['contextObj'].value;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -0,0 +1,43 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`app App supports env with getters/setters 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><block-text-0/> <block-text-1/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['env'].someVal;
|
||||
let txt2 = Object.keys(ctx['env'].services);
|
||||
return block1([txt1, txt2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`app can configure an app with props 1`] = `
|
||||
"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'].value;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`app destroy remove the widget from the DOM 1`] = `
|
||||
"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();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { App, Component, xml } from "../../src";
|
||||
import { status } from "../../src/component/status";
|
||||
import { makeTestFixture, snapshotEverything, nextTick, elem } from "../helpers";
|
||||
|
||||
let fixture: HTMLElement;
|
||||
|
||||
snapshotEverything();
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
describe("app", () => {
|
||||
test("destroy remove the widget from the DOM", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<div/>`;
|
||||
}
|
||||
|
||||
const app = new App(SomeComponent);
|
||||
const comp = await app.mount(fixture);
|
||||
const el = elem(comp);
|
||||
expect(document.contains(el)).toBe(true);
|
||||
app.destroy();
|
||||
expect(document.contains(el)).toBe(false);
|
||||
expect(status(comp)).toBe("destroyed");
|
||||
});
|
||||
|
||||
test("App supports env with getters/setters", async () => {
|
||||
let someVal = "maggot";
|
||||
|
||||
const services: any = { serv1: "" };
|
||||
const env = {
|
||||
get someVal() {
|
||||
return someVal;
|
||||
},
|
||||
services,
|
||||
};
|
||||
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<div><t t-esc="env.someVal" /> <t t-esc="Object.keys(env.services)" /></div>`;
|
||||
}
|
||||
|
||||
const app = new App(SomeComponent, { env });
|
||||
const comp = await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>maggot serv1</div>");
|
||||
someVal = "brain";
|
||||
services.serv2 = "";
|
||||
comp.render();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>brain serv1,serv2</div>");
|
||||
});
|
||||
|
||||
test("can configure an app with props", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<div t-esc="props.value"/>`;
|
||||
}
|
||||
|
||||
const app = new App(SomeComponent, { props: { value: 333 } });
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>333</div>");
|
||||
});
|
||||
});
|
||||
@@ -227,6 +227,38 @@ describe("misc", () => {
|
||||
expect(fixture.innerHTML).toBe("<p>() => 3tostring</p>");
|
||||
});
|
||||
|
||||
test("block with 2 subblocks: variation", async () => {
|
||||
const block = createBlock("<a><b><c><block-child-0/>2</c></b><block-child-1/></a>");
|
||||
const tree = block([], [text("1"), text("3")]);
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<a><b><c>12</c></b>3</a>");
|
||||
});
|
||||
|
||||
test("block with 2 subblocks: another variation", async () => {
|
||||
const block = createBlock(
|
||||
`<a block-attribute-0="hello"><b><c><block-child-0/>2</c></b><block-child-1/></a>`
|
||||
);
|
||||
const tree = block(["world"], [text("1"), text("3")]);
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe(`<a hello="world"><b><c>12</c></b>3</a>`);
|
||||
});
|
||||
|
||||
test("namespace is not propagated to siblings", () => {
|
||||
const block = createBlock(`<div><svg block-ns="someNameSpace"><g/></svg><div></div></div>`);
|
||||
|
||||
const fixture = makeTestFixture();
|
||||
mount(block(), fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div><svg><g></g></svg><div></div></div>");
|
||||
expect(fixture.querySelector("svg")!.namespaceURI).toBe("someNameSpace");
|
||||
expect(fixture.querySelector("g")!.namespaceURI).toBe("someNameSpace");
|
||||
const allDivs = fixture.querySelectorAll("div");
|
||||
expect(Array.from(allDivs).map((el) => el.namespaceURI)).toEqual([
|
||||
"http://www.w3.org/1999/xhtml",
|
||||
"http://www.w3.org/1999/xhtml",
|
||||
]);
|
||||
});
|
||||
|
||||
// test.skip("reusing a block skips patching process", async () => {
|
||||
// const block = createBlock('<div><block-text-0/></div>');
|
||||
// const foo = block(["foo"]);
|
||||
|
||||
@@ -26,6 +26,26 @@ test("simple attribute", async () => {
|
||||
expect(fixture.innerHTML).toBe(`<div hello="owl"></div>`);
|
||||
});
|
||||
|
||||
test("updating attribute with falsy value", async () => {
|
||||
const block = createBlock('<div block-attribute-0="hello"></div>');
|
||||
const tree = block([false]);
|
||||
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe(`<div></div>`);
|
||||
|
||||
patch(tree, block(["owl"]));
|
||||
expect(fixture.innerHTML).toBe(`<div hello="owl"></div>`);
|
||||
|
||||
patch(tree, block([false]));
|
||||
expect(fixture.innerHTML).toBe(`<div></div>`);
|
||||
|
||||
patch(tree, block(["owl"]));
|
||||
expect(fixture.innerHTML).toBe(`<div hello="owl"></div>`);
|
||||
|
||||
patch(tree, block([undefined]));
|
||||
expect(fixture.innerHTML).toBe(`<div></div>`);
|
||||
});
|
||||
|
||||
test("dynamic attribute (pair)", async () => {
|
||||
const block = createBlock('<div block-attributes="0"></div>');
|
||||
const tree = block([["hello", "world"]]);
|
||||
@@ -37,6 +57,20 @@ test("dynamic attribute (pair)", async () => {
|
||||
expect(fixture.innerHTML).toBe(`<div ola="mundo"></div>`);
|
||||
});
|
||||
|
||||
test("dynamic attribute (pair, with false value)", async () => {
|
||||
const block = createBlock('<div block-attributes="0"></div>');
|
||||
const tree = block([["hello", false]]);
|
||||
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe(`<div></div>`);
|
||||
|
||||
patch(tree, block([["hello", "world"]]));
|
||||
expect(fixture.innerHTML).toBe(`<div hello="world"></div>`);
|
||||
|
||||
patch(tree, block([["hello", false]]));
|
||||
expect(fixture.innerHTML).toBe(`<div></div>`);
|
||||
});
|
||||
|
||||
test("dynamic attribute (object)", async () => {
|
||||
const block = createBlock('<div block-attributes="0"></div>');
|
||||
const tree = block([{ hello: "world" }]);
|
||||
@@ -48,6 +82,23 @@ test("dynamic attribute (object)", async () => {
|
||||
expect(fixture.innerHTML).toBe(`<div ola="mundo"></div>`);
|
||||
});
|
||||
|
||||
test("dynamic attribute (object), with falsy values", async () => {
|
||||
const block = createBlock('<div block-attributes="0"></div>');
|
||||
const tree = block([{ hello: "world", blip: false }]);
|
||||
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe(`<div hello="world"></div>`);
|
||||
|
||||
patch(tree, block([{ ola: "mundo", blip: undefined }]));
|
||||
expect(fixture.innerHTML).toBe(`<div ola="mundo"></div>`);
|
||||
|
||||
patch(tree, block([{ ola: false, blip: 1 }]));
|
||||
expect(fixture.innerHTML).toBe(`<div blip="1"></div>`);
|
||||
|
||||
patch(tree, block([{ ola: undefined, blip: undefined }]));
|
||||
expect(fixture.innerHTML).toBe(`<div></div>`);
|
||||
});
|
||||
|
||||
test("class attribute", async () => {
|
||||
const block = createBlock('<div block-attribute-0="class"></div>');
|
||||
const tree = block(["fire"]);
|
||||
@@ -65,6 +116,14 @@ test("class attribute", async () => {
|
||||
expect(fixture.innerHTML).toBe(`<div class="0"></div>`);
|
||||
});
|
||||
|
||||
test("attribute with undefined value", async () => {
|
||||
const block = createBlock('<div block-attribute-0="abc"></div>');
|
||||
const tree = block([undefined]);
|
||||
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe(`<div></div>`);
|
||||
});
|
||||
|
||||
test("class attribute with undefined value", async () => {
|
||||
const block = createBlock('<div block-attribute-0="class"></div>');
|
||||
const tree = block([undefined]);
|
||||
|
||||
@@ -61,6 +61,7 @@ test("simple event handling ", async () => {
|
||||
const [owner, method] = data;
|
||||
owner[method]();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const block = createBlock('<div block-handler-0="click"></div>');
|
||||
@@ -106,3 +107,167 @@ test("two same block nodes with different handlers", async () => {
|
||||
(fixture.firstChild!.nextSibling as HTMLDivElement).click();
|
||||
expect(steps).toEqual(["1", "2"]);
|
||||
});
|
||||
|
||||
test("two same block nodes with different handlers (synthetic)", async () => {
|
||||
const block = createBlock('<div block-handler-0="click.synthetic"></div>');
|
||||
let steps: string[] = [];
|
||||
let handler1 = () => steps.push("1");
|
||||
let handler2 = () => steps.push("2");
|
||||
const tree = multi([block([handler1]), block([handler2])]);
|
||||
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div></div><div></div>");
|
||||
|
||||
(fixture.firstChild as HTMLDivElement).click();
|
||||
(fixture.firstChild!.nextSibling as HTMLDivElement).click();
|
||||
expect(steps).toEqual(["1", "2"]);
|
||||
});
|
||||
|
||||
test("two event handlers on same event", async () => {
|
||||
const block = createBlock('<div block-handler-0="click" block-handler-1="click"></div>');
|
||||
let n = 0;
|
||||
let m = 0;
|
||||
const tree = block([() => m++, () => n++]);
|
||||
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
|
||||
expect(m).toBe(0);
|
||||
expect(n).toBe(0);
|
||||
(fixture.firstChild as HTMLDivElement).click();
|
||||
expect(m).toBe(1);
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("two synthetic event handlers on same event", async () => {
|
||||
const block = createBlock(
|
||||
'<div block-handler-0="click.synthetic" block-handler-1="click.synthetic"></div>'
|
||||
);
|
||||
let n = 0;
|
||||
let m = 0;
|
||||
const tree = block([() => m++, () => n++]);
|
||||
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
|
||||
expect(m).toBe(0);
|
||||
expect(n).toBe(0);
|
||||
(fixture.firstChild as HTMLDivElement).click();
|
||||
expect(m).toBe(1);
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("synthetic and native handlers can cohabitate", async () => {
|
||||
const block = createBlock(
|
||||
'<div block-handler-0="click.synthetic"><div block-handler-1="click"/></div>'
|
||||
);
|
||||
let steps: string[] = [];
|
||||
let handler1 = () => steps.push("1");
|
||||
let handler2 = () => steps.push("2");
|
||||
const tree = block([handler1, handler2]);
|
||||
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div></div></div>");
|
||||
|
||||
(fixture.firstChild!.firstChild as HTMLDivElement).click();
|
||||
expect(steps).toEqual(["2", "1"]);
|
||||
(fixture.firstChild as HTMLDivElement).click();
|
||||
expect(steps).toEqual(["2", "1", "1"]);
|
||||
});
|
||||
|
||||
test("synthetic and native handlers can cohabitate (2)", async () => {
|
||||
const block = createBlock(
|
||||
'<div block-handler-0="click"><div block-handler-1="click.synthetic"/></div>'
|
||||
);
|
||||
let steps: string[] = [];
|
||||
let handler1 = () => steps.push("1");
|
||||
let handler2 = () => steps.push("2");
|
||||
const tree = block([handler1, handler2]);
|
||||
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div></div></div>");
|
||||
|
||||
(fixture.firstChild!.firstChild as HTMLDivElement).click();
|
||||
expect(steps).toEqual(["1", "2"]);
|
||||
(fixture.firstChild as HTMLDivElement).click();
|
||||
expect(steps).toEqual(["1", "2", "1"]);
|
||||
});
|
||||
|
||||
test("synthetic and native handlers can cohabitate (3)", async () => {
|
||||
const parent = createBlock(`<div block-handler-0="click"><block-child-0/><block-child-1/></div>`);
|
||||
const block = createBlock('<div block-handler-0="click"/>');
|
||||
const blockSynth = createBlock('<div block-handler-0="click.synthetic"/>');
|
||||
let steps: string[] = [];
|
||||
const handler0 = () => steps.push("0");
|
||||
let handler1 = () => steps.push("1");
|
||||
let handler2 = () => steps.push("2");
|
||||
const tree = parent([handler0], [block([handler1]), blockSynth([handler2])]);
|
||||
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div></div><div></div></div>");
|
||||
|
||||
const children = fixture.children[0].children;
|
||||
|
||||
(children[0] as HTMLElement).click();
|
||||
expect(steps).toEqual(["1", "0"]);
|
||||
(children[1] as HTMLElement).click();
|
||||
expect(steps).toEqual(["1", "0", "0", "2"]);
|
||||
});
|
||||
|
||||
test("synthetic and native handlers can cohabitate (4)", async () => {
|
||||
const parent = createBlock(`<div block-handler-0="click"><block-child-0/><block-child-1/></div>`);
|
||||
const block = createBlock('<div block-handler-0="click"/>');
|
||||
const blockSynth = createBlock('<div block-handler-0="click.synthetic"/>');
|
||||
let steps: string[] = [];
|
||||
const handler0 = (ev: Event) => {
|
||||
steps.push("0");
|
||||
ev.stopPropagation();
|
||||
};
|
||||
let handler1 = () => steps.push("1");
|
||||
let handler2 = () => steps.push("2");
|
||||
const tree = parent([handler0], [block([handler1]), blockSynth([handler2])]);
|
||||
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div></div><div></div></div>");
|
||||
|
||||
const children = fixture.children[0].children;
|
||||
|
||||
(children[0] as HTMLElement).click();
|
||||
expect(steps).toEqual(["1", "0"]);
|
||||
(children[1] as HTMLElement).click();
|
||||
expect(steps).toEqual(["1", "0", "0"]);
|
||||
});
|
||||
|
||||
test("synthetic and native handlers can cohabitate (5)", async () => {
|
||||
const block = createBlock('<div block-handler-0="click.synthetic" block-handler-1="click"/>');
|
||||
let steps: string[] = [];
|
||||
let handler1 = () => steps.push("1");
|
||||
let handler2 = () => steps.push("2");
|
||||
const tree = block([handler1, handler2]);
|
||||
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
|
||||
(fixture.firstChild as HTMLDivElement).click();
|
||||
expect(steps).toEqual(["2", "1"]);
|
||||
});
|
||||
|
||||
test("synthetic and native handlers can cohabitate (6)", async () => {
|
||||
const block = createBlock(`<div
|
||||
block-handler-0="click.synthetic"
|
||||
block-handler-1="click"
|
||||
block-handler-2="click.synthetic"
|
||||
block-handler-3="click"
|
||||
/>`);
|
||||
let steps: string[] = [];
|
||||
const handler1 = () => steps.push("1");
|
||||
const handler2 = () => steps.push("2");
|
||||
const handler3 = () => steps.push("3");
|
||||
const handler4 = () => steps.push("4");
|
||||
const tree = block([handler1, handler2, handler3, handler4]);
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
|
||||
(fixture.firstChild as HTMLDivElement).click();
|
||||
expect(steps).toEqual(["2", "4", "1", "3"]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { comment, mount } from "../../src/blockdom";
|
||||
import { makeTestFixture } from "./helpers";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
let fixture: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
test("simple comment node", async () => {
|
||||
const tree = comment("foo");
|
||||
expect(tree.el).toBe(undefined);
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<!--foo-->");
|
||||
expect(tree.el).not.toBe(undefined);
|
||||
tree.remove();
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
});
|
||||
@@ -76,4 +76,16 @@ describe("multi blocks", () => {
|
||||
mount(text(multi([text("a"), text("b")]) as any), fixture);
|
||||
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")])]);
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>foobar</div>");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { createBlock, mount } from "../../src/blockdom";
|
||||
import { makeTestFixture } from "./helpers";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const XHTML_URI = "http://www.w3.org/1999/xhtml";
|
||||
const SVG_URI = "http://www.w3.org/2000/svg";
|
||||
let fixture: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
describe("namespace", () => {
|
||||
test("default namespace is xhtml", () => {
|
||||
const block = createBlock(`<tag/>`);
|
||||
const tree = block();
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<tag></tag>");
|
||||
expect(fixture.firstElementChild!.namespaceURI).toBe(XHTML_URI);
|
||||
});
|
||||
|
||||
test("namespace can be changed with block-ns", () => {
|
||||
const block = createBlock(`<tag block-ns="${SVG_URI}"/>`);
|
||||
const tree = block();
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<tag></tag>");
|
||||
expect(fixture.firstElementChild!.namespaceURI).toBe(SVG_URI);
|
||||
});
|
||||
|
||||
test("namespace is kept for children", () => {
|
||||
const block = createBlock(
|
||||
`<parent block-ns="${SVG_URI}"><child><subchild/></child><child/></parent>`
|
||||
);
|
||||
const tree = block();
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<parent><child><subchild></subchild></child><child></child></parent>"
|
||||
);
|
||||
const parent = fixture.firstElementChild!;
|
||||
const child1 = parent.firstElementChild!;
|
||||
const subchild = child1.firstElementChild!;
|
||||
const child2 = child1.nextElementSibling!;
|
||||
expect(parent.namespaceURI).toBe(SVG_URI);
|
||||
expect(child1.namespaceURI).toBe(SVG_URI);
|
||||
expect(child2.namespaceURI).toBe(SVG_URI);
|
||||
expect(subchild.namespaceURI).toBe(SVG_URI);
|
||||
});
|
||||
|
||||
test("various namespaces in same block", () => {
|
||||
const block = createBlock(`<none><one block-ns="one"/><two block-ns="two"/></none>`);
|
||||
const tree = block();
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<none><one></one><two></two></none>");
|
||||
const none = fixture.firstElementChild!;
|
||||
const one = none.firstElementChild!;
|
||||
const two = one.nextElementSibling!;
|
||||
expect(none.namespaceURI).toBe(XHTML_URI);
|
||||
expect(one.namespaceURI).toBe("one");
|
||||
expect(two.namespaceURI).toBe("two");
|
||||
});
|
||||
});
|
||||
@@ -54,4 +54,61 @@ describe("togglers", () => {
|
||||
expect(fixture.innerHTML).toBe("<p>hey</p>");
|
||||
expect(fixture.firstChild).not.toBe(textnode);
|
||||
});
|
||||
|
||||
test("beforeRemove is called", () => {
|
||||
const block = createBlock("<p>hey</p>");
|
||||
|
||||
function blockOverriden() {
|
||||
const vn = block();
|
||||
vn.beforeRemove = () => {
|
||||
steps.push("beforeRemove");
|
||||
};
|
||||
return vn;
|
||||
}
|
||||
|
||||
const steps: string[] = [];
|
||||
const tree = toggler("key1", blockOverriden());
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe("<p>hey</p>");
|
||||
patch(tree, toggler("key2", text("foo")));
|
||||
expect(fixture.innerHTML).toBe("foo");
|
||||
|
||||
patch(tree, toggler("key3", blockOverriden()), true);
|
||||
expect(fixture.innerHTML).toBe("<p>hey</p>");
|
||||
|
||||
patch(tree, toggler("key2", text("foo")), true);
|
||||
expect(fixture.innerHTML).toBe("foo");
|
||||
|
||||
expect(steps).toEqual(["beforeRemove"]);
|
||||
});
|
||||
|
||||
test("beforeRemove is called when within a block", () => {
|
||||
const block = createBlock("<p><block-child-0/></p>");
|
||||
const conditionalBlock = createBlock("<p>hey</p>");
|
||||
|
||||
const steps: string[] = [];
|
||||
|
||||
function blockOverriden() {
|
||||
const vn = conditionalBlock();
|
||||
vn.beforeRemove = function () {
|
||||
steps.push("beforeRemove");
|
||||
};
|
||||
return vn;
|
||||
}
|
||||
const mainBlock = block([], [toggler("key1", blockOverriden())]);
|
||||
|
||||
mount(mainBlock, fixture);
|
||||
expect(fixture.innerHTML).toBe("<p><p>hey</p></p>");
|
||||
|
||||
patch(mainBlock, block([], []));
|
||||
expect(fixture.innerHTML).toBe("<p></p>");
|
||||
|
||||
patch(mainBlock, block([], [toggler("key1", blockOverriden())]), true);
|
||||
expect(fixture.innerHTML).toBe("<p><p>hey</p></p>");
|
||||
|
||||
patch(mainBlock, block([], []), true);
|
||||
expect(fixture.innerHTML).toBe("<p></p>");
|
||||
|
||||
expect(steps).toEqual(["beforeRemove"]);
|
||||
});
|
||||
});
|
||||
|
||||
+191
-174
@@ -3,14 +3,13 @@
|
||||
exports[`attributes changing a class with t-att-class (preexisting class 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"hoy\\" block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['v'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['v'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -18,14 +17,13 @@ exports[`attributes changing a class with t-att-class (preexisting class 1`] = `
|
||||
exports[`attributes changing a class with t-att-class 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['v'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['v'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -33,14 +31,13 @@ exports[`attributes changing a class with t-att-class 1`] = `
|
||||
exports[`attributes changing an attribute with t-att- 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"value\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['v'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['v'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -48,14 +45,13 @@ exports[`attributes changing an attribute with t-att- 1`] = `
|
||||
exports[`attributes class and t-att-class should combine together 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"class\\" class=\\"hello\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['value'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['value'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -63,14 +59,13 @@ exports[`attributes class and t-att-class should combine together 1`] = `
|
||||
exports[`attributes class and t-attf-class with ternary operation 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"hello\\" block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = (ctx['value']?'world':'');
|
||||
return block1([d1]);
|
||||
let attr1 = (ctx['value']?'world':'');
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -78,14 +73,13 @@ exports[`attributes class and t-attf-class with ternary operation 1`] = `
|
||||
exports[`attributes dynamic attribute evaluating to 0 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"foo\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['value'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['value'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -93,14 +87,13 @@ exports[`attributes dynamic attribute evaluating to 0 1`] = `
|
||||
exports[`attributes dynamic attribute falsy variable 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"foo\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['value'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['value'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -108,14 +101,13 @@ exports[`attributes dynamic attribute falsy variable 1`] = `
|
||||
exports[`attributes dynamic attribute with a dash 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"data-action-id\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['id'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['id'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -123,14 +115,13 @@ exports[`attributes dynamic attribute with a dash 1`] = `
|
||||
exports[`attributes dynamic attributes 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"foo\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = 'bar';
|
||||
return block1([d1]);
|
||||
let attr1 = 'bar';
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -138,14 +129,13 @@ exports[`attributes dynamic attributes 1`] = `
|
||||
exports[`attributes dynamic class attribute 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['c'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['c'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -153,14 +143,13 @@ exports[`attributes dynamic class attribute 1`] = `
|
||||
exports[`attributes dynamic class attribute evaluating to 0 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['value'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['value'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -168,14 +157,13 @@ exports[`attributes dynamic class attribute evaluating to 0 1`] = `
|
||||
exports[`attributes dynamic empty class attribute 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['c'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['c'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -183,14 +171,41 @@ exports[`attributes dynamic empty class attribute 1`] = `
|
||||
exports[`attributes dynamic formatted attributes with a dash 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"aria-label\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = \`Some text \${ctx['id']}\`;
|
||||
return block1([d1]);
|
||||
let attr1 = \`Some text \${ctx['id']}\`;
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`attributes dynamic undefined class attribute 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let attr1 = ctx['c'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`attributes dynamic undefined generic attribute 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"thing\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let attr1 = ctx['c'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -198,14 +213,13 @@ exports[`attributes dynamic formatted attributes with a dash 1`] = `
|
||||
exports[`attributes fixed variable 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"foo\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['value'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['value'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -213,14 +227,13 @@ exports[`attributes fixed variable 1`] = `
|
||||
exports[`attributes format expression 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"foo\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = (ctx['value']+37);
|
||||
return block1([d1]);
|
||||
let attr1 = (ctx['value']+37);
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -228,14 +241,13 @@ exports[`attributes format expression 1`] = `
|
||||
exports[`attributes format literal 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"foo\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = \`bar\`;
|
||||
return block1([d1]);
|
||||
let attr1 = \`bar\`;
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -243,14 +255,13 @@ exports[`attributes format literal 1`] = `
|
||||
exports[`attributes format multiple 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"foo\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = \`a \${ctx['value1']} is \${ctx['value2']} of \${ctx['value3']} ]\`;
|
||||
return block1([d1]);
|
||||
let attr1 = \`a \${ctx['value1']} is \${ctx['value2']} of \${ctx['value3']} ]\`;
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -258,14 +269,13 @@ exports[`attributes format multiple 1`] = `
|
||||
exports[`attributes format value 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"foo\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = \`b\${ctx['value']}r\`;
|
||||
return block1([d1]);
|
||||
let attr1 = \`b\${ctx['value']}r\`;
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -273,16 +283,17 @@ exports[`attributes format value 1`] = `
|
||||
exports[`attributes from object variables set previously 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><span block-attribute-0=\\"class\\"/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
ctx[\`o\`] = {a:'b'};
|
||||
let d1 = ctx['o'].a;
|
||||
return block1([d1]);
|
||||
ctx[isBoundary] = 1
|
||||
setContextValue(ctx, \\"o\\", {a:'b'});
|
||||
let attr1 = ctx['o'].a;
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -290,16 +301,17 @@ exports[`attributes from object variables set previously 1`] = `
|
||||
exports[`attributes from variables set previously (no external node) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<span block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
ctx[\`abc\`] = 'def';
|
||||
let d1 = ctx['abc'];
|
||||
return block1([d1]);
|
||||
ctx[isBoundary] = 1
|
||||
setContextValue(ctx, \\"abc\\", 'def');
|
||||
let attr1 = ctx['abc'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -307,16 +319,17 @@ exports[`attributes from variables set previously (no external node) 1`] = `
|
||||
exports[`attributes from variables set previously 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><span block-attribute-0=\\"class\\"/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
ctx[\`abc\`] = 'def';
|
||||
let d1 = ctx['abc'];
|
||||
return block1([d1]);
|
||||
ctx[isBoundary] = 1
|
||||
setContextValue(ctx, \\"abc\\", 'def');
|
||||
let attr1 = ctx['abc'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -324,14 +337,13 @@ exports[`attributes from variables set previously 1`] = `
|
||||
exports[`attributes object 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attributes=\\"0\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['value'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['value'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -339,8 +351,7 @@ exports[`attributes object 1`] = `
|
||||
exports[`attributes static attributes 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div foo=\\"a\\" bar=\\"b\\" baz=\\"c\\"/>\`);
|
||||
|
||||
@@ -353,8 +364,7 @@ exports[`attributes static attributes 1`] = `
|
||||
exports[`attributes static attributes on void elements 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<img src=\\"/test.skip.jpg\\" alt=\\"Test\\"/>\`);
|
||||
|
||||
@@ -367,8 +377,7 @@ exports[`attributes static attributes on void elements 1`] = `
|
||||
exports[`attributes static attributes with dashes 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div aria-label=\\"Close\\"/>\`);
|
||||
|
||||
@@ -381,14 +390,13 @@ exports[`attributes static attributes with dashes 1`] = `
|
||||
exports[`attributes t-att-class and class should combine together 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"hello\\" block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['value'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['value'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -396,14 +404,13 @@ exports[`attributes t-att-class and class should combine together 1`] = `
|
||||
exports[`attributes t-att-class with multiple classes 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = {'a b c':ctx['value']};
|
||||
return block1([d1]);
|
||||
let attr1 = {'a b c':ctx['value']};
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -411,14 +418,13 @@ exports[`attributes t-att-class with multiple classes 1`] = `
|
||||
exports[`attributes t-att-class with multiple classes 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = {['a b c']:ctx['value']};
|
||||
return block1([d1]);
|
||||
let attr1 = {['a b c']:ctx['value']};
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -426,14 +432,13 @@ exports[`attributes t-att-class with multiple classes 2`] = `
|
||||
exports[`attributes t-att-class with multiple classes, some of which are duplicate 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = {'a b c':ctx['value'],'a b d':!ctx['value']};
|
||||
return block1([d1]);
|
||||
let attr1 = {'a b c':ctx['value'],'a b d':!ctx['value']};
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -441,14 +446,13 @@ exports[`attributes t-att-class with multiple classes, some of which are duplica
|
||||
exports[`attributes t-att-class with object 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"static\\" block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = {a:ctx['b'],c:ctx['d'],e:ctx['f']};
|
||||
return block1([d1]);
|
||||
let attr1 = {a:ctx['b'],c:ctx['d'],e:ctx['f']};
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -456,14 +460,13 @@ exports[`attributes t-att-class with object 1`] = `
|
||||
exports[`attributes t-attf-class 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = \`hello\`;
|
||||
return block1([d1]);
|
||||
let attr1 = \`hello\`;
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -471,14 +474,13 @@ exports[`attributes t-attf-class 1`] = `
|
||||
exports[`attributes t-attf-class should combine with class 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"hello\\" block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = \`world\`;
|
||||
return block1([d1]);
|
||||
let attr1 = \`world\`;
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -486,14 +488,13 @@ exports[`attributes t-attf-class should combine with class 1`] = `
|
||||
exports[`attributes t-attf-class with multiple classes 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = \`hello \${ctx['word']}\`;
|
||||
return block1([d1]);
|
||||
let attr1 = \`hello \${ctx['word']}\`;
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -501,14 +502,13 @@ exports[`attributes t-attf-class with multiple classes 1`] = `
|
||||
exports[`attributes t-attf-class with multiple classes separated by multiple spaces 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = \`hello \${ctx['word']}\`;
|
||||
return block1([d1]);
|
||||
let attr1 = \`hello \${ctx['word']}\`;
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -516,14 +516,13 @@ exports[`attributes t-attf-class with multiple classes separated by multiple spa
|
||||
exports[`attributes tuple literal 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attributes=\\"0\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ['foo','bar'];
|
||||
return block1([d1]);
|
||||
let attr1 = ['foo','bar'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -531,14 +530,13 @@ exports[`attributes tuple literal 1`] = `
|
||||
exports[`attributes tuple variable 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attributes=\\"0\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['value'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['value'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -546,8 +544,7 @@ exports[`attributes tuple variable 1`] = `
|
||||
exports[`attributes two classes 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"a b\\"/>\`);
|
||||
|
||||
@@ -560,15 +557,14 @@ exports[`attributes two classes 1`] = `
|
||||
exports[`attributes two dynamic attributes 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"foo\\" block-attribute-1=\\"bar\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = 'bar';
|
||||
let d2 = 'foo';
|
||||
return block1([d1, d2]);
|
||||
let attr1 = 'bar';
|
||||
let attr2 = 'foo';
|
||||
return block1([attr1, attr2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -576,14 +572,13 @@ exports[`attributes two dynamic attributes 1`] = `
|
||||
exports[`attributes updating classes (with obj notation) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"hoy\\" block-attribute-0=\\"class\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = {'a b':ctx['condition']};
|
||||
return block1([d1]);
|
||||
let attr1 = {'a b':ctx['condition']};
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -591,16 +586,15 @@ exports[`attributes updating classes (with obj notation) 1`] = `
|
||||
exports[`attributes various escapes 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div foo=\\"<foo\\" block-attribute-0=\\"bar\\" block-attribute-1=\\"baz\\" block-attributes=\\"2\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['bar'];
|
||||
let d2 = \`<\${ctx['baz']}>\`;
|
||||
let d3 = ctx['qux'];
|
||||
return block1([d1, d2, d3]);
|
||||
let attr1 = ctx['bar'];
|
||||
let attr2 = \`<\${ctx['baz']}>\`;
|
||||
let attr3 = ctx['qux'];
|
||||
return block1([attr1, attr2, attr3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -608,8 +602,7 @@ exports[`attributes various escapes 1`] = `
|
||||
exports[`attributes various escapes 2 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div> < </div>\`);
|
||||
|
||||
@@ -622,14 +615,13 @@ exports[`attributes various escapes 2 1`] = `
|
||||
exports[`special cases for some specific html attributes/properties input of type checkbox with t-att-indeterminate 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<input type=\\"checkbox\\" block-attribute-0=\\"indeterminate\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['v'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['v'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -637,14 +629,13 @@ exports[`special cases for some specific html attributes/properties input of typ
|
||||
exports[`special cases for some specific html attributes/properties input type= checkbox, with t-att-checked 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<input type=\\"checkbox\\" block-attribute-0=\\"checked\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['flag'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['flag'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -652,14 +643,41 @@ exports[`special cases for some specific html attributes/properties input type=
|
||||
exports[`special cases for some specific html attributes/properties input with t-att-value 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<input block-attribute-0=\\"value\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['v'];
|
||||
return block1([d1]);
|
||||
let attr1 = ctx['v'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`special cases for some specific html attributes/properties select with t-att-value 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<select block-attribute-0=\\"value\\"><option value=\\"potato\\">Potato</option><option value=\\"tomato\\">Tomato</option><option value=\\"onion\\">Onion</option></select>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let attr1 = ctx['value'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`special cases for some specific html attributes/properties textarea with t-att-value 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<textarea block-attribute-0=\\"value\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let attr1 = ctx['v'];
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -667,8 +685,7 @@ exports[`special cases for some specific html attributes/properties input with t
|
||||
exports[`special cases for some specific html attributes/properties various boolean html attributes 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><input type=\\"checkbox\\" checked=\\"checked\\"/><input checked=\\"checked\\"/><div checked=\\"checked\\"/><div selected=\\"selected\\"/><option selected=\\"selected\\" other=\\"1\\"/><input readonly=\\"readonly\\"/><button disabled=\\"disabled\\"/></div>\`);
|
||||
|
||||
+4
-9
@@ -3,13 +3,10 @@
|
||||
exports[`comments only a comment 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<!-- comment-->\`);
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
return comment(\` comment\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -17,8 +14,7 @@ exports[`comments only a comment 1`] = `
|
||||
exports[`comments properly handle comments 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>hello <!-- comment-->owl</div>\`);
|
||||
|
||||
@@ -31,8 +27,7 @@ exports[`comments properly handle comments 1`] = `
|
||||
exports[`comments properly handle comments between t-if/t-else 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = 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>true</span>\`);
|
||||
@@ -0,0 +1,530 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`t-on can bind event handler 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['add'], ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on can bind handlers with arguments 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 = \\"\\") {
|
||||
const v1 = ctx['add'];
|
||||
let hdlr1 = [()=>v1(5), ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on can bind handlers with empty object 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 = \\"\\") {
|
||||
const v1 = ctx['doSomething'];
|
||||
let hdlr1 = [()=>v1({}), ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on can bind handlers with empty object (with non empty inner string) 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 = \\"\\") {
|
||||
const v1 = ctx['doSomething'];
|
||||
let hdlr1 = [()=>v1({}), ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on can bind handlers with empty object (with non empty inner string) 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { prepareList, withKey } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<ul><block-child-0/></ul>\`);
|
||||
let block3 = createBlock(\`<li><a block-handler-0=\\"click\\">link</a></li>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
const [k_block2, v_block2, l_block2, c_block2] = prepareList(['someval']);
|
||||
for (let i1 = 0; i1 < l_block2; i1++) {
|
||||
ctx[\`action\`] = v_block2[i1];
|
||||
ctx[\`action_index\`] = i1;
|
||||
let key1 = ctx['action_index'];
|
||||
const v1 = ctx['activate'];
|
||||
const v2 = ctx['action'];
|
||||
let hdlr1 = [()=>v1(v2), ctx];
|
||||
c_block2[i1] = withKey(block3([hdlr1]), key1);
|
||||
}
|
||||
let b2 = list(c_block2);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on can bind handlers with object arguments 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 = \\"\\") {
|
||||
const v1 = ctx['add'];
|
||||
let hdlr1 = [()=>v1({val:5}), ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on can bind two event handlers 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<button block-handler-0=\\"click\\" block-handler-1=\\"dblclick\\">Click</button>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [ctx['handleClick'], ctx];
|
||||
let hdlr2 = [ctx['handleDblClick'], ctx];
|
||||
return block1([hdlr1, hdlr2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on handler is bound to proper owner 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['add'], ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on handler is bound to proper owner, part 2 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { prepareList, withKey } = helpers;
|
||||
|
||||
let block2 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
const [k_block1, v_block1, l_block1, c_block1] = prepareList([1]);
|
||||
for (let i1 = 0; i1 < l_block1; i1++) {
|
||||
ctx[\`value\`] = v_block1[i1];
|
||||
let key1 = ctx['value'];
|
||||
let hdlr1 = [ctx['add'], ctx];
|
||||
c_block1[i1] = withKey(block2([hdlr1]), key1);
|
||||
}
|
||||
return list(c_block1);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on handler is bound to proper owner, part 3 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { getTemplate } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`sub\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on handler is bound to proper owner, part 3 2`] = `
|
||||
"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['add'], ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on handler is bound to proper owner, part 4 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { prepareList, getTemplate, withKey } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`sub\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
const [k_block1, v_block1, l_block1, c_block1] = prepareList([1]);
|
||||
for (let i1 = 0; i1 < l_block1; i1++) {
|
||||
ctx[\`value\`] = v_block1[i1];
|
||||
ctx[\`value_first\`] = i1 === 0;
|
||||
ctx[\`value_last\`] = i1 === v_block1.length - 1;
|
||||
ctx[\`value_index\`] = i1;
|
||||
ctx[\`value_value\`] = k_block1[i1];
|
||||
let key1 = ctx['value'];
|
||||
c_block1[i1] = withKey(callTemplate_1.call(this, ctx, node, key + \`__1__\${key1}\`), key1);
|
||||
}
|
||||
return list(c_block1);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on handler is bound to proper owner, part 4 2`] = `
|
||||
"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['add'], ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on receive event in first argument 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['add'], ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on modifiers (native listener) basic support for native listener 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"myClass\\" block-handler-0=\\"click\\"><button block-handler-1=\\"click\\">Button</button></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [ctx['divClicked'], ctx];
|
||||
let hdlr2 = [ctx['btnClicked'], ctx];
|
||||
return block1([hdlr1, hdlr2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on modifiers (native listener) t-on combined with t-esc 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><button block-handler-0=\\"click\\"><block-text-1/></button></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [ctx['onClick'], ctx];
|
||||
let txt1 = ctx['text'];
|
||||
return block1([hdlr1, txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on modifiers (native listener) t-on combined with t-out 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { safeOutput } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><button block-handler-0=\\"click\\"><block-child-0/></button></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [ctx['onClick'], ctx];
|
||||
let b2 = safeOutput(ctx['html']);
|
||||
return block1([hdlr1], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on modifiers (native listener) t-on with .capture modifier 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-handler-0=\\"click.capture\\"><button block-handler-1=\\"click\\">Button</button></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [\\"capture\\", ctx['onCapture'], ctx];
|
||||
let hdlr2 = [ctx['doSomething'], ctx];
|
||||
return block1([hdlr1, hdlr2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on modifiers (native listener) t-on with empty handler (only modifiers) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><button block-handler-0=\\"click.prevent\\">Button</button></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [\\"prevent\\", , ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on modifiers (native listener) t-on with prevent and self modifiers (order matters) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><button block-handler-0=\\"click.prevent.self\\"><span>Button</span></button></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [\\"prevent\\",\\"self\\", ctx['onClick'], ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on modifiers (native listener) t-on with prevent and/or stop modifiers 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><button block-handler-0=\\"click.prevent\\">Button 1</button><button block-handler-1=\\"click.stop\\">Button 2</button><button block-handler-2=\\"click.prevent.stop\\">Button 3</button></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [\\"prevent\\", ctx['onClickPrevented'], ctx];
|
||||
let hdlr2 = [\\"stop\\", ctx['onClickStopped'], ctx];
|
||||
let hdlr3 = [\\"prevent\\",\\"stop\\", ctx['onClickPreventedAndStopped'], ctx];
|
||||
return block1([hdlr1, hdlr2, hdlr3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on modifiers (native listener) t-on with prevent modifier in t-foreach 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { prepareList, withKey } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
let block3 = createBlock(\`<a href=\\"#\\" block-handler-0=\\"click.prevent\\"> Edit <block-text-1/></a>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['projects']);
|
||||
for (let i1 = 0; i1 < l_block2; i1++) {
|
||||
ctx[\`project\`] = v_block2[i1];
|
||||
let key1 = ctx['project'];
|
||||
const v1 = ctx['onEdit'];
|
||||
const v2 = ctx['project'];
|
||||
let hdlr1 = [\\"prevent\\", ev=>v1(v2.id,ev), ctx];
|
||||
let txt1 = ctx['project'].name;
|
||||
c_block2[i1] = withKey(block3([hdlr1, txt1]), key1);
|
||||
}
|
||||
let b2 = list(c_block2);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on modifiers (native listener) t-on with self and prevent modifiers (order matters) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><button block-handler-0=\\"click.self.prevent\\"><span>Button</span></button></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [\\"self\\",\\"prevent\\", ctx['onClick'], ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on modifiers (native listener) t-on with self modifier 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><button block-handler-0=\\"click\\"><span>Button</span></button><button block-handler-1=\\"click.self\\"><span>Button</span></button></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [ctx['onClick'], ctx];
|
||||
let hdlr2 = [\\"self\\", ctx['onClickSelf'], ctx];
|
||||
return block1([hdlr1, hdlr2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on modifiers (synthetic listener) basic support for synthetic 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-handler-0=\\"click.synthetic\\"><button block-handler-1=\\"click.synthetic\\">Button</button></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [\\"synthetic\\", ctx['divClicked'], ctx];
|
||||
let hdlr2 = [\\"synthetic\\", ctx['btnClicked'], ctx];
|
||||
return block1([hdlr1, hdlr2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on with inline statement (function call) 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 = \\"\\") {
|
||||
const v1 = ctx['state'];
|
||||
let hdlr1 = [()=>v1.incrementCounter(2), ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on with inline statement 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 = \\"\\") {
|
||||
const v1 = ctx['state'];
|
||||
let hdlr1 = [()=>v1.counter++, ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on with inline statement, part 2 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Toggle</button>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const v1 = ctx['state'];
|
||||
let hdlr1 = [()=>v1.flag=!v1.flag, ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on with inline statement, part 3 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Toggle</button>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const v1 = ctx['state'];
|
||||
const v2 = ctx['someFunction'];
|
||||
let hdlr1 = [()=>v1.n=v2(3), ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on with t-call 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { getTemplate } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`sub\`);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on with t-call 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [ctx['update'], ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on, with arguments and t-call 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { getTemplate } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`sub\`);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on, with arguments and t-call 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const v1 = ctx['value'];
|
||||
let hdlr1 = [()=>this.update(v1), ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
+147
-145
@@ -3,8 +3,8 @@
|
||||
exports[`misc complex template 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { prepareList, withKey } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div block-attribute-0=\\"class\\"><div block-attribute-1=\\"class\\"><div class=\\"batch_header\\"><a block-attribute-2=\\"href\\" block-attribute-3=\\"class\\" title=\\"View Batch\\"><block-text-4/><block-child-0/><i class=\\"arrow fa fa-window-maximize\\"/></a></div><block-child-1/><div class=\\"batch_slots\\"><block-child-2/><block-child-3/></div><div class=\\"batch_commits\\"><block-child-4/></div></div></div>\`);
|
||||
let block2 = createBlock(\`<i class=\\"fa fa-exclamation-triangle\\"/>\`);
|
||||
@@ -18,11 +18,11 @@ exports[`misc complex template 1`] = `
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2,b3,b4,b6,b8;
|
||||
let d1 = \`batch_tile \${ctx['options'].more?'more':'nomore'}\`;
|
||||
let d2 = \`card bg-\${ctx['klass']}-light\`;
|
||||
let d3 = \`/runbot/batch/\${ctx['batch'].id}\`;
|
||||
let d4 = \`badge badge-\${ctx['batch'].has_warning?'warning':'light'}\`;
|
||||
let d5 = ctx['batch'].formated_age;
|
||||
let attr1 = \`batch_tile \${ctx['options'].more?'more':'nomore'}\`;
|
||||
let attr2 = \`card bg-\${ctx['klass']}-light\`;
|
||||
let attr3 = \`/runbot/batch/\${ctx['batch'].id}\`;
|
||||
let attr4 = \`badge badge-\${ctx['batch'].has_warning?'warning':'light'}\`;
|
||||
let txt1 = ctx['batch'].formated_age;
|
||||
if (ctx['batch'].has_warning) {
|
||||
b2 = block2();
|
||||
}
|
||||
@@ -30,31 +30,31 @@ exports[`misc complex template 1`] = `
|
||||
b3 = block3();
|
||||
}
|
||||
ctx = Object.create(ctx);
|
||||
const [k4, v4, l4, c4] = prepareList(ctx['batch'].slot_ids.filter(slot=>slot.build_id.id&&!slot.trigger_id.manual&&(ctx['options'].trigger_display[slot.trigger_id.id])));
|
||||
for (let i1 = 0; i1 < l4; i1++) {
|
||||
ctx[\`slot\`] = v4[i1];
|
||||
const [k_block4, v_block4, l_block4, c_block4] = prepareList(ctx['batch'].slot_ids.filter(slot=>slot.build_id.id&&!slot.trigger_id.manual&&(ctx['options'].trigger_display[slot.trigger_id.id])));
|
||||
for (let i1 = 0; i1 < l_block4; i1++) {
|
||||
ctx[\`slot\`] = v_block4[i1];
|
||||
let key1 = ctx['slot'].id;
|
||||
c4[i1] = withKey(component(\`SlotButton\`, {class: ctx['slot_container'],slot: ctx['slot']}, key + \`__1__\${key1}\`, node, ctx), key1);
|
||||
c_block4[i1] = withKey(component(\`SlotButton\`, {class: ctx['slot_container'],slot: ctx['slot']}, key + \`__1__\${key1}\`, node, ctx), key1);
|
||||
}
|
||||
ctx = ctx.__proto__;
|
||||
b4 = list(c4);
|
||||
b4 = list(c_block4);
|
||||
ctx = Object.create(ctx);
|
||||
const [k6, v6, l6, c6] = prepareList([1,2,3,4]);
|
||||
for (let i1 = 0; i1 < l6; i1++) {
|
||||
ctx[\`x\`] = v6[i1];
|
||||
const [k_block6, v_block6, l_block6, c_block6] = prepareList([1,2,3,4]);
|
||||
for (let i1 = 0; i1 < l_block6; i1++) {
|
||||
ctx[\`x\`] = v_block6[i1];
|
||||
let key1 = ctx['x'];
|
||||
c6[i1] = withKey(block7(), key1);
|
||||
c_block6[i1] = withKey(block7(), key1);
|
||||
}
|
||||
ctx = ctx.__proto__;
|
||||
b6 = list(c6);
|
||||
b6 = list(c_block6);
|
||||
ctx = Object.create(ctx);
|
||||
const [k8, v8, l8, c8] = prepareList(ctx['commit_links']);
|
||||
for (let i1 = 0; i1 < l8; i1++) {
|
||||
ctx[\`commit_link\`] = v8[i1];
|
||||
const [k_block8, v_block8, l_block8, c_block8] = prepareList(ctx['commit_links']);
|
||||
for (let i1 = 0; i1 < l_block8; i1++) {
|
||||
ctx[\`commit_link\`] = v_block8[i1];
|
||||
let key1 = ctx['commit_link'].id;
|
||||
let b10,b11,b12,b13;
|
||||
let d6 = \`/runbot/commit/\${ctx['commit_link'].commit_id}\`;
|
||||
let d7 = \`badge badge-light batch_commit match_type_\${ctx['commit_link'].match_type}\`;
|
||||
let attr5 = \`/runbot/commit/\${ctx['commit_link'].commit_id}\`;
|
||||
let attr6 = \`badge badge-light batch_commit match_type_\${ctx['commit_link'].match_type}\`;
|
||||
if (ctx['commit_link'].match_type=='new') {
|
||||
b10 = block10();
|
||||
}
|
||||
@@ -67,13 +67,13 @@ exports[`misc complex template 1`] = `
|
||||
if (ctx['commit_link'].match_type=='base_head') {
|
||||
b13 = block13();
|
||||
}
|
||||
let d8 = ctx['commit_link'].commit_dname;
|
||||
let d9 = 'https://%s/commit/%s'%(ctx['commit_link'].commit_remote_url,ctx['commit_link'].commit_name);
|
||||
let d10 = ctx['commit_link'].commit_subject;
|
||||
c8[i1] = withKey(block9([d6, d7, d8, d9, d10], [b10, b11, b12, b13]), key1);
|
||||
let txt2 = ctx['commit_link'].commit_dname;
|
||||
let attr7 = 'https://%s/commit/%s'%(ctx['commit_link'].commit_remote_url,ctx['commit_link'].commit_name);
|
||||
let txt3 = ctx['commit_link'].commit_subject;
|
||||
c_block8[i1] = withKey(block9([attr5, attr6, txt2, attr7, txt3], [b10, b11, b12, b13]), key1);
|
||||
}
|
||||
b8 = list(c8);
|
||||
return block1([d1, d2, d3, d4, d5], [b2, b3, b4, b6, b8]);
|
||||
b8 = list(c_block8);
|
||||
return block1([attr1, attr2, attr3, attr4, txt1], [b2, b3, b4, b6, b8]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -81,15 +81,51 @@ exports[`misc complex template 1`] = `
|
||||
exports[`misc global 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { prepareList, isBoundary, withDefault, setContextValue, getTemplate, zero, withKey } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`_callee-uses-foo\`);
|
||||
const callTemplate_2 = getTemplate(\`_callee-uses-foo\`);
|
||||
const callTemplate_3 = getTemplate(\`_callee-uses-foo\`);
|
||||
const callTemplate_4 = getTemplate(\`_callee-asc\`);
|
||||
const callTemplate_5 = getTemplate(\`_callee-asc-toto\`);
|
||||
|
||||
let block1 = createBlock(\`<año block-attribute-0=\\"falló\\"><block-child-0/></año>\`);
|
||||
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
||||
let block4 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = 'agüero';
|
||||
let b2 = html(ctx[zero]);
|
||||
return block1([d1], [b2]);
|
||||
ctx = Object.create(ctx);
|
||||
ctx[isBoundary] = 1
|
||||
ctx = Object.create(ctx);
|
||||
const [k_block2, v_block2, l_block2, c_block2] = prepareList([4,5,6]);
|
||||
for (let i1 = 0; i1 < l_block2; i1++) {
|
||||
ctx[\`value\`] = v_block2[i1];
|
||||
ctx[\`value_first\`] = i1 === 0;
|
||||
ctx[\`value_last\`] = i1 === v_block2.length - 1;
|
||||
ctx[\`value_index\`] = i1;
|
||||
ctx[\`value_value\`] = k_block2[i1];
|
||||
let key1 = ctx['value'];
|
||||
let txt1 = ctx['value'];
|
||||
let b4 = block4([txt1]);
|
||||
ctx = Object.create(ctx);
|
||||
ctx[isBoundary] = 1;
|
||||
ctx = Object.create(ctx);
|
||||
ctx[isBoundary] = 1;
|
||||
setContextValue(ctx, \\"foo\\", 'aaa');
|
||||
let b6 = callTemplate_1.call(this, ctx, node, key + \`__1__\${key1}\`);
|
||||
ctx = ctx.__proto__;
|
||||
let b7 = callTemplate_2.call(this, ctx, node, key + \`__2__\${key1}\`);
|
||||
setContextValue(ctx, \\"foo\\", 'bbb');
|
||||
let b8 = callTemplate_3.call(this, ctx, node, key + \`__3__\${key1}\`);
|
||||
let b5 = multi([b6, b7, b8]);
|
||||
ctx[zero] = b5;
|
||||
let b9 = callTemplate_4.call(this, ctx, node, key + \`__4__\${key1}\`);
|
||||
ctx = ctx.__proto__;
|
||||
c_block2[i1] = withKey(multi([b4, b9]), key1);
|
||||
}
|
||||
ctx = ctx.__proto__;
|
||||
let b2 = list(c_block2);
|
||||
let b10 = callTemplate_5.call(this, ctx, node, key + \`__5\`);
|
||||
return block1([], [b2, b10]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -97,14 +133,14 @@ exports[`misc global 1`] = `
|
||||
exports[`misc global 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { withDefault } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = withDefault(ctx['foo'], \`foo default\`);
|
||||
return block1([d1]);
|
||||
let txt1 = withDefault(ctx['foo'], \`foo default\`);
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -112,15 +148,15 @@ exports[`misc global 2`] = `
|
||||
exports[`misc global 3`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { zero } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
let block1 = createBlock(\`<año block-attribute-0=\\"falló\\"><block-child-0/></año>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b3 = text(\`toto default\`);
|
||||
let b2 = html(withDefault(ctx['toto'], b3));
|
||||
return block1([], [b2]);
|
||||
let attr1 = 'agüero';
|
||||
let b2 = ctx[zero];
|
||||
return block1([attr1], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -128,48 +164,15 @@ exports[`misc global 3`] = `
|
||||
exports[`misc global 4`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
const callTemplate_2 = getTemplate(\`_callee-uses-foo\`);
|
||||
const callTemplate_4 = getTemplate(\`_callee-uses-foo\`);
|
||||
const callTemplate_6 = getTemplate(\`_callee-uses-foo\`);
|
||||
const callTemplate_8 = getTemplate(\`_callee-asc\`);
|
||||
const callTemplate_10 = getTemplate(\`_callee-asc-toto\`);
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { safeOutput, withDefault } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
||||
let block4 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
ctx = Object.create(ctx);
|
||||
const [k2, v2, l2, c2] = prepareList([4,5,6]);
|
||||
for (let i1 = 0; i1 < l2; i1++) {
|
||||
ctx[\`value\`] = v2[i1];
|
||||
ctx[\`value_first\`] = i1 === 0;
|
||||
ctx[\`value_last\`] = i1 === v2.length - 1;
|
||||
ctx[\`value_index\`] = i1;
|
||||
ctx[\`value_value\`] = k2[i1];
|
||||
let key1 = ctx['value'];
|
||||
let d1 = ctx['value'];
|
||||
let b4 = block4([d1]);
|
||||
ctx = Object.create(ctx);
|
||||
ctx = Object.create(ctx);
|
||||
ctx[\`foo\`] = 'aaa';
|
||||
let b6 = callTemplate_2(ctx, node, key + \`__1__\${key1}\`);
|
||||
ctx = ctx.__proto__;
|
||||
let b7 = callTemplate_4(ctx, node, key + \`__3__\${key1}\`);
|
||||
ctx[\`foo\`] = 'bbb';
|
||||
let b8 = callTemplate_6(ctx, node, key + \`__5__\${key1}\`);
|
||||
let b5 = multi([b6, b7, b8]);
|
||||
ctx[zero] = b5;
|
||||
let b9 = callTemplate_8(ctx, node, key + \`__7__\${key1}\`);
|
||||
ctx = ctx.__proto__;
|
||||
c2[i1] = withKey(multi([b4, b9]), key1);
|
||||
}
|
||||
ctx = ctx.__proto__;
|
||||
let b2 = list(c2);
|
||||
let b10 = callTemplate_10(ctx, node, key + \`__9\`);
|
||||
return block1([], [b2, b10]);
|
||||
let b3 = text(\`toto default\`);
|
||||
let b2 = withDefault(safeOutput(ctx['toto']), b3);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -177,9 +180,9 @@ exports[`misc global 4`] = `
|
||||
exports[`misc other complex template 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
const callTemplate_3 = getTemplate(\`LOAD_INFOS_TEMPLATE\`);
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { prepareList, withKey, getTemplate } = helpers;
|
||||
const callTemplate_1 = getTemplate(\`LOAD_INFOS_TEMPLATE\`);
|
||||
|
||||
let block1 = createBlock(\`<div><header><nav class=\\"navbar navbar-expand-md navbar-light bg-light\\"><a block-attribute-0=\\"href\\"><b style=\\"color:#777;\\"><block-text-1/></b></a><button type=\\"button\\" class=\\"navbar-toggler\\" data-toggle=\\"collapse\\" data-target=\\"#top_menu_collapse\\"><span class=\\"navbar-toggler-icon\\"/></button><div class=\\"collapse navbar-collapse\\" id=\\"top_menu_collapse\\" aria-expanded=\\"false\\"><ul class=\\"nav navbar-nav ml-auto text-right\\" id=\\"top_menu\\"><block-child-0/><li class=\\"nav-item divider\\"/><block-child-1/></ul><div><div class=\\"input-group input-group-sm\\"><div class=\\"input-group-prepend input-group-sm\\"><button class=\\"btn btn-default fa fa-cog\\" title=\\"Settings\\" block-handler-2=\\"click\\"/><button class=\\"btn btn-default\\" block-handler-3=\\"click\\"> More </button><block-child-2/></div><input class=\\"form-control\\" type=\\"text\\" placeholder=\\"Search\\" aria-label=\\"Search\\" name=\\"search\\" block-attribute-4=\\"value\\" block-handler-5=\\"keyup\\" block-handler-6=\\"change\\" block-ref=\\"7\\"/><div class=\\"input-group-append\\"><button class=\\"btn btn-default fa fa-eraser\\" block-handler-8=\\"click\\"/></div></div></div></div></nav></header><div class=\\"container-fluid\\" block-ref=\\"9\\"><div class=\\"row\\"><!--div class=\\"form-group col-md-6\\">
|
||||
<h5>Search options</h5>
|
||||
@@ -212,32 +215,33 @@ exports[`misc other complex template 1`] = `
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const refs = ctx.__owl__.refs;
|
||||
const ref1 = (el) => refs[\`search_input\`] = el;
|
||||
const ref2 = (el) => refs[\`settings_menu\`] = el;
|
||||
let b2,b4,b14,b17,b22,b23,b24,b25;
|
||||
let d1 = \`/runbot/\${ctx['project'].slug}\`;
|
||||
let d2 = ctx['project'].name;
|
||||
let attr1 = \`/runbot/\${ctx['project'].slug}\`;
|
||||
let txt1 = ctx['project'].name;
|
||||
ctx = Object.create(ctx);
|
||||
const [k2, v2, l2, c2] = prepareList(ctx['projects']);
|
||||
for (let i1 = 0; i1 < l2; i1++) {
|
||||
ctx[\`project\`] = v2[i1];
|
||||
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['projects']);
|
||||
for (let i1 = 0; i1 < l_block2; i1++) {
|
||||
ctx[\`project\`] = v_block2[i1];
|
||||
let key1 = ctx['project'].id;
|
||||
const arg1 = [ctx['project']];
|
||||
let d3 = [ctx, 'selectProject', arg1];
|
||||
let d4 = ctx['project'].name;
|
||||
c2[i1] = withKey(block3([d3, d4]), key1);
|
||||
let hdlr1 = [ctx['selectProject'](ctx['project']), ctx];
|
||||
let txt2 = ctx['project'].name;
|
||||
c_block2[i1] = withKey(block3([hdlr1, txt2]), key1);
|
||||
}
|
||||
ctx = ctx.__proto__;
|
||||
b2 = list(c2);
|
||||
b2 = list(c_block2);
|
||||
if (ctx['user']) {
|
||||
let b5,b6;
|
||||
if (ctx['user'].public) {
|
||||
let d5 = \`/web/login?redirect=/\`;
|
||||
b5 = block5([d5]);
|
||||
let attr2 = \`/web/login?redirect=/\`;
|
||||
b5 = block5([attr2]);
|
||||
} else {
|
||||
let b7,b10,b13;
|
||||
if (ctx['nb_assigned_errors']&&ctx['nb_assigned_errors']>0) {
|
||||
let d6 = \`You have \${ctx['nb_assigned_errors']} random bug assigned\`;
|
||||
let d7 = ctx['nb_assigned_errors'];
|
||||
let b8 = block8([d6, d7]);
|
||||
let attr3 = \`You have \${ctx['nb_assigned_errors']} random bug assigned\`;
|
||||
let txt3 = ctx['nb_assigned_errors'];
|
||||
let b8 = block8([attr3, txt3]);
|
||||
let b9 = block9();
|
||||
b7 = multi([b8, b9]);
|
||||
} else if (ctx['nb_build_errors']&&ctx['nb_build_errors']>0) {
|
||||
@@ -245,80 +249,78 @@ exports[`misc other complex template 1`] = `
|
||||
let b12 = block12();
|
||||
b10 = multi([b11, b12]);
|
||||
}
|
||||
let d8 = ctx['user'].name.length>25?ctx['user'].namesubstring(0,23)+'...':ctx['user'].name;
|
||||
let d9 = \`/web/session/logout?redirect=/\`;
|
||||
let d10 = \`/web\`;
|
||||
b13 = block13([d8, d9, d10]);
|
||||
let txt4 = ctx['user'].name.length>25?ctx['user'].namesubstring(0,23)+'...':ctx['user'].name;
|
||||
let attr4 = \`/web/session/logout?redirect=/\`;
|
||||
let attr5 = \`/web\`;
|
||||
b13 = block13([txt4, attr4, attr5]);
|
||||
b6 = multi([b7, b10, b13]);
|
||||
}
|
||||
b4 = multi([b5, b6]);
|
||||
}
|
||||
let d11 = [ctx, 'toggleSettingsMenu'];
|
||||
let d12 = [ctx, 'toggleMore'];
|
||||
let hdlr2 = [ctx['toggleSettingsMenu'], ctx];
|
||||
let hdlr3 = [ctx['toggleMore'], ctx];
|
||||
if (ctx['categories']&&ctx['categories'].length>1) {
|
||||
ctx = Object.create(ctx);
|
||||
const [k15, v15, l15, c15] = prepareList(ctx['categories']);
|
||||
for (let i1 = 0; i1 < l15; i1++) {
|
||||
ctx[\`category\`] = v15[i1];
|
||||
const [k_block15, v_block15, l_block15, c_block15] = prepareList(ctx['categories']);
|
||||
for (let i1 = 0; i1 < l_block15; i1++) {
|
||||
ctx[\`category\`] = v_block15[i1];
|
||||
let key1 = ctx['category'].id;
|
||||
let d13 = ctx['category'].id;
|
||||
let d14 = ctx['category'].id==ctx['options'].active_category_id;
|
||||
let d15 = ctx['category'].name;
|
||||
c15[i1] = withKey(block16([d13, d14, d15]), key1);
|
||||
let attr6 = ctx['category'].id;
|
||||
let attr7 = ctx['category'].id==ctx['options'].active_category_id;
|
||||
let txt5 = ctx['category'].name;
|
||||
c_block15[i1] = withKey(block16([attr6, attr7, txt5]), key1);
|
||||
}
|
||||
ctx = ctx.__proto__;
|
||||
let b15 = list(c15);
|
||||
let b15 = list(c_block15);
|
||||
b14 = block14([], [b15]);
|
||||
}
|
||||
let d16 = ctx['search'].value;
|
||||
let d17 = [ctx, 'updateFilter'];
|
||||
let d18 = [ctx, 'updateFilter'];
|
||||
let d19 = (el) => refs[\`search_input\`] = el;
|
||||
let d20 = [ctx, 'clearSearch'];
|
||||
let d21 = (el) => refs[\`settings_menu\`] = el;
|
||||
let attr8 = ctx['search'].value;
|
||||
let hdlr4 = [ctx['updateFilter'], ctx];
|
||||
let hdlr5 = [ctx['updateFilter'], ctx];
|
||||
let hdlr6 = [ctx['clearSearch'], ctx];
|
||||
if (ctx['triggers']) {
|
||||
ctx = Object.create(ctx);
|
||||
const [k18, v18, l18, c18] = prepareList(ctx['triggers']);
|
||||
for (let i1 = 0; i1 < l18; i1++) {
|
||||
ctx[\`trigger\`] = v18[i1];
|
||||
const [k_block18, v_block18, l_block18, c_block18] = prepareList(ctx['triggers']);
|
||||
for (let i1 = 0; i1 < l_block18; i1++) {
|
||||
ctx[\`trigger\`] = v_block18[i1];
|
||||
let key1 = ctx['trigger'].id;
|
||||
let b20;
|
||||
if (!ctx['trigger'].manual&&ctx['trigger'].project_id===ctx['project'].id&&ctx['trigger'].category_id===ctx['options'].active_category_id) {
|
||||
let d22 = \`trigger_\${ctx['trigger'].id}\`;
|
||||
let d23 = \`trigger_\${ctx['trigger'].id}\`;
|
||||
let d24 = ctx['options'].trigger_display[ctx['trigger'].id];
|
||||
let d25 = ctx['trigger'].id;
|
||||
let d26 = [ctx, 'updateTriggerDisplay'];
|
||||
let d27 = \`trigger_\${ctx['trigger'].id}\`;
|
||||
let d28 = ctx['trigger'].name;
|
||||
b20 = block20([d22, d23, d24, d25, d26, d27, d28]);
|
||||
let attr9 = \`trigger_\${ctx['trigger'].id}\`;
|
||||
let attr10 = \`trigger_\${ctx['trigger'].id}\`;
|
||||
let attr11 = ctx['options'].trigger_display[ctx['trigger'].id];
|
||||
let attr12 = ctx['trigger'].id;
|
||||
let hdlr7 = [ctx['updateTriggerDisplay'], ctx];
|
||||
let attr13 = \`trigger_\${ctx['trigger'].id}\`;
|
||||
let txt6 = ctx['trigger'].name;
|
||||
b20 = block20([attr9, attr10, attr11, attr12, hdlr7, attr13, txt6]);
|
||||
}
|
||||
c18[i1] = withKey(multi([b20]), key1);
|
||||
c_block18[i1] = withKey(multi([b20]), key1);
|
||||
}
|
||||
ctx = ctx.__proto__;
|
||||
let b18 = list(c18);
|
||||
let d29 = [ctx, 'triggerAll'];
|
||||
let d30 = [ctx, 'triggerNone'];
|
||||
let d31 = [ctx, 'triggerDefault'];
|
||||
let d32 = [ctx, 'toggleSettingsMenu'];
|
||||
let b21 = block21([d29, d30, d31, d32]);
|
||||
let b18 = list(c_block18);
|
||||
let hdlr8 = [ctx['triggerAll'], ctx];
|
||||
let hdlr9 = [ctx['triggerNone'], ctx];
|
||||
let hdlr10 = [ctx['triggerDefault'], ctx];
|
||||
let hdlr11 = [ctx['toggleSettingsMenu'], ctx];
|
||||
let b21 = block21([hdlr8, hdlr9, hdlr10, hdlr11]);
|
||||
b17 = multi([b18, b21]);
|
||||
}
|
||||
if (ctx['load_infos']) {
|
||||
b22 = callTemplate_3(ctx, node, key + \`__2\`);
|
||||
b22 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
}
|
||||
if (ctx['message']) {
|
||||
let d33 = ctx['message'];
|
||||
b23 = block23([d33]);
|
||||
let txt7 = ctx['message'];
|
||||
b23 = block23([txt7]);
|
||||
}
|
||||
if (!ctx['project']) {
|
||||
b24 = block24();
|
||||
} else {
|
||||
let b26 = component(\`BundlesList\`, {bundles: ctx['bundles'].sticky,category_custom_views: ctx['category_custom_views'],search: ctx['search']}, key + \`__4\`, node, ctx);
|
||||
let b27 = component(\`BundlesList\`, {bundles: ctx['bundles'].dev,search: ctx['search']}, key + \`__5\`, node, ctx);
|
||||
let b26 = component(\`BundlesList\`, {bundles: ctx['bundles'].sticky,category_custom_views: ctx['category_custom_views'],search: ctx['search']}, key + \`__2\`, node, ctx);
|
||||
let b27 = component(\`BundlesList\`, {bundles: ctx['bundles'].dev,search: ctx['search']}, key + \`__3\`, node, ctx);
|
||||
b25 = block25([], [b26, b27]);
|
||||
}
|
||||
return block1([d1, d2, d11, d12, d16, d17, d18, d19, d20, d21], [b2, b4, b14, b17, b22, b23, b24, b25]);
|
||||
return block1([attr1, txt1, hdlr2, hdlr3, attr8, hdlr4, hdlr5, ref1, hdlr6, ref2], [b2, b4, b14, b17, b22, b23, b24, b25]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`qweb parser simple t-foreach expression, t-key mandatory 1`] = `"\\"Directive t-foreach should always be used with a t-key!\\" (expression: t-foreach=\\"list\\" t-as=\\"item\\")"`;
|
||||
@@ -0,0 +1,24 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`memory t-foreach does not leak stuff in global scope 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { prepareList, withKey } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<p><block-child-0/></p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
const [k_block2, v_block2, l_block2, c_block2] = prepareList([3,2,1]);
|
||||
for (let i1 = 0; i1 < l_block2; i1++) {
|
||||
ctx[\`item\`] = v_block2[i1];
|
||||
ctx[\`item_index\`] = i1;
|
||||
let key1 = ctx['item_index'];
|
||||
c_block2[i1] = withKey(text(ctx['item']), key1);
|
||||
}
|
||||
let b2 = list(c_block2);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
+107
-61
@@ -3,8 +3,7 @@
|
||||
exports[`simple templates, mostly static can render a table row 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<tr><td>cell</td></tr>\`);
|
||||
|
||||
@@ -17,8 +16,7 @@ exports[`simple templates, mostly static can render a table row 1`] = `
|
||||
exports[`simple templates, mostly static div with a class attribute 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"abc\\">foo</div>\`);
|
||||
|
||||
@@ -31,8 +29,7 @@ exports[`simple templates, mostly static div with a class attribute 1`] = `
|
||||
exports[`simple templates, mostly static div with a class attribute with a quote 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"a'bc\\">word</div>\`);
|
||||
|
||||
@@ -45,8 +42,7 @@ exports[`simple templates, mostly static div with a class attribute with a quote
|
||||
exports[`simple templates, mostly static div with a span child node 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><span>word</span></div>\`);
|
||||
|
||||
@@ -59,8 +55,7 @@ exports[`simple templates, mostly static div with a span child node 1`] = `
|
||||
exports[`simple templates, mostly static div with an arbitrary attribute with a quote 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div abc=\\"a'bc\\">word</div>\`);
|
||||
|
||||
@@ -73,8 +68,7 @@ exports[`simple templates, mostly static div with an arbitrary attribute with a
|
||||
exports[`simple templates, mostly static div with an empty class attribute 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>word</div>\`);
|
||||
|
||||
@@ -87,8 +81,7 @@ exports[`simple templates, mostly static div with an empty class attribute 1`] =
|
||||
exports[`simple templates, mostly static div with content 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>foo</div>\`);
|
||||
|
||||
@@ -101,14 +94,13 @@ exports[`simple templates, mostly static div with content 1`] = `
|
||||
exports[`simple templates, mostly static dom node with t-esc 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = 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 d1 = ctx['text'];
|
||||
return block1([d1]);
|
||||
let txt1 = ctx['text'];
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -116,14 +108,13 @@ exports[`simple templates, mostly static dom node with t-esc 1`] = `
|
||||
exports[`simple templates, mostly static dom node with t-esc 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = 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 d1 = ctx['text'];
|
||||
return block1([d1]);
|
||||
let txt1 = ctx['text'];
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -131,8 +122,7 @@ exports[`simple templates, mostly static dom node with t-esc 2`] = `
|
||||
exports[`simple templates, mostly static dynamic text value 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(ctx['text']);
|
||||
@@ -143,8 +133,7 @@ exports[`simple templates, mostly static dynamic text value 1`] = `
|
||||
exports[`simple templates, mostly static empty div 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div/>\`);
|
||||
|
||||
@@ -157,8 +146,7 @@ exports[`simple templates, mostly static empty div 1`] = `
|
||||
exports[`simple templates, mostly static empty string 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`\`);
|
||||
@@ -169,8 +157,7 @@ exports[`simple templates, mostly static empty string 1`] = `
|
||||
exports[`simple templates, mostly static empty string in a template set 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`\`);
|
||||
@@ -178,11 +165,47 @@ exports[`simple templates, mostly static empty string in a template set 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`simple templates, mostly static inline template string in t-esc 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`text\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`simple templates, mostly static inline template string with content in t-esc 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
ctx[isBoundary] = 1
|
||||
setContextValue(ctx, \\"v\\", 1);
|
||||
return text(\`text\${ctx['v']}\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`simple templates, mostly static inline template string with variable in context 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`text \${ctx['v']}\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`simple templates, mostly static multiple root nodes 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block2 = createBlock(\`<div>foo</div>\`);
|
||||
let block3 = createBlock(\`<span>hey</span>\`);
|
||||
@@ -198,8 +221,7 @@ exports[`simple templates, mostly static multiple root nodes 1`] = `
|
||||
exports[`simple templates, mostly static simple string 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`hello vdom\`);
|
||||
@@ -210,8 +232,7 @@ exports[`simple templates, mostly static simple string 1`] = `
|
||||
exports[`simple templates, mostly static simple string in t tag 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`hello vdom\`);
|
||||
@@ -222,8 +243,7 @@ exports[`simple templates, mostly static simple string in t tag 1`] = `
|
||||
exports[`simple templates, mostly static static text and dynamic text (no t tag) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = text(\`hello \`);
|
||||
@@ -236,8 +256,7 @@ exports[`simple templates, mostly static static text and dynamic text (no t tag)
|
||||
exports[`simple templates, mostly static static text and dynamic text 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = text(\`hello \`);
|
||||
@@ -250,14 +269,13 @@ exports[`simple templates, mostly static static text and dynamic text 1`] = `
|
||||
exports[`simple templates, mostly static t-esc in dom node 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = 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 d1 = ctx['text'];
|
||||
return block1([d1]);
|
||||
let txt1 = ctx['text'];
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -265,14 +283,13 @@ exports[`simple templates, mostly static t-esc in dom node 1`] = `
|
||||
exports[`simple templates, mostly static t-esc in dom node, variations 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>hello <block-text-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['text'];
|
||||
return block1([d1]);
|
||||
let txt1 = ctx['text'];
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -280,14 +297,46 @@ exports[`simple templates, mostly static t-esc in dom node, variations 1`] = `
|
||||
exports[`simple templates, mostly static t-esc in dom node, variations 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>hello <block-text-0/> world</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['text'];
|
||||
return block1([d1]);
|
||||
let txt1 = ctx['text'];
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`simple templates, mostly static template with multiple t tag with multiple content 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><block-text-0/><block-text-1/>Loading<block-text-2/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['a'];
|
||||
let txt2 = ctx['b'];
|
||||
let txt3 = ctx['c'];
|
||||
return block1([txt1, txt2, txt3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`simple templates, mostly static template with t tag with multiple content 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>Loading<block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2;
|
||||
if (false) {
|
||||
b2 = text(\`\`);
|
||||
}
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -295,8 +344,7 @@ exports[`simple templates, mostly static t-esc in dom node, variations 2`] = `
|
||||
exports[`simple templates, mostly static two t-escs next to each other 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = text(ctx['text1']);
|
||||
@@ -309,8 +357,7 @@ exports[`simple templates, mostly static two t-escs next to each other 1`] = `
|
||||
exports[`simple templates, mostly static two t-escs next to each other 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = text(ctx['text1']);
|
||||
@@ -323,15 +370,14 @@ exports[`simple templates, mostly static two t-escs next to each other 2`] = `
|
||||
exports[`simple templates, mostly static two t-escs next to each other, in a div 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><block-text-0/><block-text-1/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let d1 = ctx['text1'];
|
||||
let d2 = ctx['text2'];
|
||||
return block1([d1, d2]);
|
||||
let txt1 = ctx['text1'];
|
||||
let txt2 = ctx['text2'];
|
||||
return block1([txt1, txt2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -0,0 +1,71 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`properly support svg add proper namespace to g tags 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<g block-ns=\\"http://www.w3.org/2000/svg\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/> </g>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`properly support svg add proper namespace to svg 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\\" width=\\"100px\\" height=\\"90px\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/> </svg>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`properly support svg namespace to g tags not added if already in svg namespace 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\\"><g/></svg>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`properly support svg namespace to svg tags added even if already in svg namespace 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\\"><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]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
+466
-443
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user