Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56041bc133 | |||
| e788e361c7 | |||
| 9d378b0e7b | |||
| fd3c194525 | |||
| ac9ccb81ca | |||
| 2b5cea944b | |||
| cf8039f643 | |||
| aec2373e6d | |||
| a9be149e1e | |||
| b365ea5c9c | |||
| b31fa81083 | |||
| 9b656fd9e4 | |||
| 17f4823b13 | |||
| d3bc101177 | |||
| 4287beae19 | |||
| a073c68685 | |||
| 7078d64049 | |||
| ccd31f12d9 | |||
| aff4019cde | |||
| 5ecb4809ff | |||
| c2daecc07b | |||
| fcda17c8e9 | |||
| aeed79c7e5 | |||
| 1c5b6f2573 | |||
| cd9b72158b | |||
| 7fc552e2f8 | |||
| e6768501cd | |||
| 6b2486473f | |||
| 7e687234bf | |||
| 968e96ad08 | |||
| 26c7856d5d | |||
| b8d09e523d | |||
| 04c2808701 | |||
| 15c2604df1 | |||
| 3e11fe6b12 | |||
| 20c6cacb4e | |||
| eb2b32ab60 | |||
| 2a223288d4 | |||
| 1272278225 | |||
| f502dd732e | |||
| 9c2d957525 | |||
| f8bb86820e | |||
| 0cde4b8737 | |||
| 66a801393f | |||
| e7f405cc97 | |||
| 55c48b2b12 | |||
| 2eb151c92d | |||
| 7952f31e63 | |||
| 11e4e67599 | |||
| 7b7a6de373 | |||
| b63d1e28b2 | |||
| c0667a11c6 | |||
| fddb1ec924 | |||
| e6c3b62ef0 | |||
| 97b69f164f | |||
| 33dfeb1b41 | |||
| dd292472b9 | |||
| 9b18b57fdf | |||
| 68f491cd32 | |||
| 7b3e39ba27 | |||
| 61fc3f4fdc | |||
| 7b454dae66 | |||
| 70101e4c66 | |||
| 5ef405293a | |||
| 9dcbbe54eb | |||
| e94428a186 | |||
| 941190dfa8 | |||
| a53e42518f | |||
| b7c37ca69a |
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x, 14.x, 16.x]
|
node-version: [20.x, 22.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|||||||
@@ -47,3 +47,4 @@ Utility/helpers:
|
|||||||
- [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed)
|
- [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed)
|
||||||
- [`validate`](reference/utils.md#validate): validates if an object satisfies a specified schema
|
- [`validate`](reference/utils.md#validate): validates if an object satisfies a specified schema
|
||||||
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
|
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
|
||||||
|
- [`batched`](reference/utils.md#batched): utility function to batch function calls
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
- [API](#api)
|
- [API](#api)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [`mount` helper](#mount-helper)
|
- [`mount` helper](#mount-helper)
|
||||||
|
- [Roots](#roots)
|
||||||
- [Loading templates](#loading-templates)
|
- [Loading templates](#loading-templates)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
@@ -61,8 +62,13 @@ The `config` object is an object with some of the following keys:
|
|||||||
templates (see [translations](translations.md))
|
templates (see [translations](translations.md))
|
||||||
- **`templates (string | xml document)`**: all the templates that will be used by
|
- **`templates (string | xml document)`**: all the templates that will be used by
|
||||||
the components created by the application.
|
the components created by the application.
|
||||||
|
- **`getTemplate ((s: string) => Element | Function | string | void)`**: a function that will be called by owl when it
|
||||||
|
needs a template. If undefined is returned, owl looks into the app templates.
|
||||||
- **`warnIfNoStaticProps (boolean, default=false)`**: if true, Owl will log a warning
|
- **`warnIfNoStaticProps (boolean, default=false)`**: if true, Owl will log a warning
|
||||||
whenever it encounters a component that does not provide a [static props description](props.md#props-validation).
|
whenever it encounters a component that does not provide a [static props description](props.md#props-validation).
|
||||||
|
- **`customDirectives (object)`**: if given, the corresponding function on the object will be called
|
||||||
|
on the template custom directives: `t-custom-*` (see [Custom Directives](templates.md#custom-directives)).
|
||||||
|
- **`globalValues (object)`**: Global object of elements available at compilations.
|
||||||
|
|
||||||
## `mount` helper
|
## `mount` helper
|
||||||
|
|
||||||
@@ -90,6 +96,33 @@ 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
|
a reference to the actual Owl App, then using the `App` class directly is
|
||||||
possible.
|
possible.
|
||||||
|
|
||||||
|
## Roots
|
||||||
|
|
||||||
|
An application can have multiple roots. It is sometimes useful to instantiate
|
||||||
|
sub components in places that are not managed by Owl, such as an html editor
|
||||||
|
with dynamic content (the Knowledge application in Odoo).
|
||||||
|
|
||||||
|
To create a root, one can use the `createRoot` method, which takes two arguments:
|
||||||
|
|
||||||
|
- **`Component`**: a component class (Root component of the app)
|
||||||
|
- **`config (optional)`**: a config object that may contain a `props` object or a
|
||||||
|
`env` object.
|
||||||
|
|
||||||
|
The `createRoot` method returns an object with a `mount` method (same API as
|
||||||
|
the `App.mount` method), and a `destroy` method.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const root = app.createRoot(MyComponent, { props: { someProps: true } });
|
||||||
|
await root.mount(targetElement);
|
||||||
|
|
||||||
|
// later
|
||||||
|
root.destroy();
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that, like with owl `App`, it is the responsibility of the code that created
|
||||||
|
the root to properly destroy it (before it has been removed from the DOM!). Owl
|
||||||
|
has no way of doing it itself.
|
||||||
|
|
||||||
## Loading templates
|
## Loading templates
|
||||||
|
|
||||||
Most applications will need to load templates whenever they start. Here is
|
Most applications will need to load templates whenever they start. Here is
|
||||||
|
|||||||
@@ -140,6 +140,28 @@ class SomeComponent extends Component {
|
|||||||
The `.bind` suffix also implies `.alike`, so these props will not cause additional
|
The `.bind` suffix also implies `.alike`, so these props will not cause additional
|
||||||
renderings.
|
renderings.
|
||||||
|
|
||||||
|
## Translatable props
|
||||||
|
|
||||||
|
When you need to pass a user-facing string to a subcomponent, you likely want it
|
||||||
|
to be translated. Unfortunately, because props are arbitrary expressions, it wouldn't
|
||||||
|
be practical for Owl to find out which parts of the expression are strings and translate
|
||||||
|
them, and it also makes it difficult for tooling to extract these strings to generate
|
||||||
|
terms to translate. While you can work around this issue by doing the translation in
|
||||||
|
JavaScript, or by using `t-set` with a body (the body of `t-set` is translated),
|
||||||
|
and passing the variable as a prop, this is a sufficiently common use case that Owl
|
||||||
|
provides a suffix for this purpose: `.translate`.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<t t-name="ParentComponent">
|
||||||
|
<Child someProp.translate="some message"/>
|
||||||
|
</t>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the content of this attribute is _NOT_ treated as a JavaScript expression:
|
||||||
|
it is treated as a string, as if it was an attribute on an HTML element, and translated
|
||||||
|
before being passed to the component. If you need to interpolate some data into the
|
||||||
|
string, you will still have to do this in JavaScript.
|
||||||
|
|
||||||
## Dynamic Props
|
## Dynamic Props
|
||||||
|
|
||||||
The `t-props` directive can be used to specify totally dynamic props:
|
The `t-props` directive can be used to specify totally dynamic props:
|
||||||
@@ -238,7 +260,7 @@ class ComponentB extends owl.Component {
|
|||||||
count: {type: Number},
|
count: {type: Number},
|
||||||
messages: {
|
messages: {
|
||||||
type: Array,
|
type: Array,
|
||||||
element: {type: Object, shape: {id: Boolean, text: String }
|
element: {type: Object, shape: {id: Boolean, text: String }}
|
||||||
},
|
},
|
||||||
date: Date,
|
date: Date,
|
||||||
combinedVal: [Number, Boolean],
|
combinedVal: [Number, Boolean],
|
||||||
@@ -276,7 +298,8 @@ class ComponentB extends owl.Component {
|
|||||||
id: Number,
|
id: Number,
|
||||||
name: {type: String, optional: true},
|
name: {type: String, optional: true},
|
||||||
url: String
|
url: String
|
||||||
]}, // object, with keys id (number), name (string, optional) and url (string)
|
}
|
||||||
|
}, // object, with keys id (number), name (string, optional) and url (string)
|
||||||
someObj3: {
|
someObj3: {
|
||||||
type: Object,
|
type: Object,
|
||||||
values: { type: Array, element: String },
|
values: { type: Array, element: String },
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ This may seem counter-intuitive, but it makes perfect sense in the context of co
|
|||||||
```js
|
```js
|
||||||
class DoubleCounter extends Component {
|
class DoubleCounter extends Component {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
<t t-esc="state.selected + ': ' + state[state.selected].value"/>
|
<t t-esc="'selected: ' + state.selected + ', value: ' + state[state.selected]"/>
|
||||||
<button t-on-click="() => this.state.count1++">increment count 1</button>
|
<button t-on-click="() => this.state.count1++">increment count 1</button>
|
||||||
<button t-on-click="() => this.state.count2++">increment count 2</button>
|
<button t-on-click="() => this.state.count2++">increment count 2</button>
|
||||||
<button t-on-click="changeCounter">Switch counter</button>
|
<button t-on-click="changeCounter">Switch counter</button>
|
||||||
@@ -193,7 +193,7 @@ to be able to opt out of creating them in the first place. This is the purpose o
|
|||||||
### `markRaw`
|
### `markRaw`
|
||||||
|
|
||||||
Marks an object so that it is ignored by the reactivity system, meaning that if this object is ever
|
Marks an object so that it is ignored by the reactivity system, meaning that if this object is ever
|
||||||
part of a of a reactive object, it will be returned as is, and no keys in that object will be
|
part of a reactive object, it will be returned as is, and no keys in that object will be
|
||||||
observed.
|
observed.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ Slots can define a default content, in case the parent did not define them:
|
|||||||
## Dynamic Slots
|
## Dynamic Slots
|
||||||
|
|
||||||
The `t-slot` directive is actually able to use any expressions, using string
|
The `t-slot` directive is actually able to use any expressions, using string
|
||||||
interplolation:
|
interpolation:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<t t-slot="{{current}}" />
|
<t t-slot="{{current}}" />
|
||||||
@@ -201,16 +201,17 @@ use this `Notebook` component:
|
|||||||
|
|
||||||
```xml
|
```xml
|
||||||
<Notebook>
|
<Notebook>
|
||||||
<t t-set-slot="page1" title="'Page 1'">
|
<t t-set-slot="page1" title.translate="Page 1">
|
||||||
<div>this is in the page 1</div>
|
<div>this is in the page 1</div>
|
||||||
</t>
|
</t>
|
||||||
<t t-set-slot="page2" title="'Page 2'" hidden="somevalue">
|
<t t-set-slot="page2" title.translate="Page 2" hidden="somevalue">
|
||||||
<div>this is in the page 2</div>
|
<div>this is in the page 2</div>
|
||||||
</t>
|
</t>
|
||||||
</Notebook>
|
</Notebook>
|
||||||
```
|
```
|
||||||
|
|
||||||
Slot params works like normal props, so one can use the `.bind` suffix to
|
Slot params works like normal props, so one can use suffixes like `.translate`
|
||||||
|
when a prop is a user facing string and should be translated, or `.bind` to
|
||||||
bind a function if needed.
|
bind a function if needed.
|
||||||
|
|
||||||
## Slot scopes
|
## Slot scopes
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
- [Sub Templates](#sub-templates)
|
- [Sub Templates](#sub-templates)
|
||||||
- [Dynamic Sub Templates](#dynamic-sub-templates)
|
- [Dynamic Sub Templates](#dynamic-sub-templates)
|
||||||
- [Debugging](#debugging)
|
- [Debugging](#debugging)
|
||||||
|
- [Custom Directives](#custom-directives)
|
||||||
- [Fragments](#fragments)
|
- [Fragments](#fragments)
|
||||||
- [Inline templates](#inline-templates)
|
- [Inline templates](#inline-templates)
|
||||||
- [Rendering svg](#rendering-svg)
|
- [Rendering svg](#rendering-svg)
|
||||||
@@ -56,7 +57,7 @@ extensions.
|
|||||||
For reference, here is a list of all standard QWeb directives:
|
For reference, here is a list of all standard QWeb directives:
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| ------------------------------ | --------------------------------------------------------------- |
|
| ------------------------------ | ----------------------------------------------------------------------- |
|
||||||
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
||||||
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
|
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
|
||||||
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
||||||
@@ -66,6 +67,8 @@ For reference, here is a list of all standard QWeb directives:
|
|||||||
| `t-call` | [Rendering sub templates](#sub-templates) |
|
| `t-call` | [Rendering sub templates](#sub-templates) |
|
||||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||||
| `t-translation` | [Disabling the translation of a node](translations.md) |
|
| `t-translation` | [Disabling the translation of a node](translations.md) |
|
||||||
|
| `t-translation-context` | [Context of translations within a node](translations.md) |
|
||||||
|
| `t-translation-context-*` | [Context of translation for a specific node attribute](translations.md) |
|
||||||
|
|
||||||
The component system in Owl requires additional directives, to express various
|
The component system in Owl requires additional directives, to express various
|
||||||
needs. Here is a list of all Owl specific directives:
|
needs. Here is a list of all Owl specific directives:
|
||||||
@@ -80,6 +83,7 @@ needs. Here is a list of all Owl specific directives:
|
|||||||
| `t-slot`, `t-set-slot`, `t-slot-scope` | [Rendering a slot](slots.md) |
|
| `t-slot`, `t-set-slot`, `t-slot-scope` | [Rendering a slot](slots.md) |
|
||||||
| `t-model` | [Form input bindings](input_bindings.md) |
|
| `t-model` | [Form input bindings](input_bindings.md) |
|
||||||
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
|
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
|
||||||
|
| `t-custom-*` | [Rendering nodes with custom directives](#custom-directives) |
|
||||||
|
|
||||||
## QWeb Template Reference
|
## QWeb Template Reference
|
||||||
|
|
||||||
@@ -189,6 +193,15 @@ 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,
|
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
|
||||||
this will be injected as html.
|
this will be injected as html.
|
||||||
|
|
||||||
|
`markup` can also be used as a tag function, allowing the interpolated values to
|
||||||
|
be safely escaped:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const maliciousInput = "<script>alert('💥💥')</script>";
|
||||||
|
// <b><script>alert('💥💥')</script></b>
|
||||||
|
const value = markup`<b>${maliciousInput}</b>`;
|
||||||
|
```
|
||||||
|
|
||||||
### Setting Variables
|
### Setting Variables
|
||||||
|
|
||||||
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
||||||
@@ -588,6 +601,35 @@ will stop execution if the browser dev tools are open.
|
|||||||
|
|
||||||
will print 42 to the console.
|
will print 42 to the console.
|
||||||
|
|
||||||
|
### Custom Directives
|
||||||
|
|
||||||
|
Owl 2 supports the declaration of custom directives. To use them, an Object of functions needs to be configured on the owl APP:
|
||||||
|
|
||||||
|
```js
|
||||||
|
new App(..., {
|
||||||
|
customDirectives: {
|
||||||
|
test_directive: function (el, value) {
|
||||||
|
el.setAttribute("t-on-click", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The functions will be called when a custom directive with the name of the
|
||||||
|
function is found. The original element will be replaced with the one
|
||||||
|
modified by the function.
|
||||||
|
This :
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-custom-test_directive="click" />
|
||||||
|
```
|
||||||
|
|
||||||
|
will be replaced by :
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-on-click="value"/>
|
||||||
|
```
|
||||||
|
|
||||||
## Fragments
|
## Fragments
|
||||||
|
|
||||||
Owl 2 supports templates with an arbitrary number of root elements, or even just
|
Owl 2 supports templates with an arbitrary number of root elements, or even just
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
# 🦉 Translations 🦉
|
# 🦉 Translations 🦉
|
||||||
|
|
||||||
If properly setup, Owl can translate all rendered templates. To do
|
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.
|
so, it needs a translate function, which takes
|
||||||
|
|
||||||
|
- a string (the term to translate)
|
||||||
|
- a string (the translation context of the term)
|
||||||
|
and returns a string.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const translations = {
|
const translations = {
|
||||||
|
fr: {
|
||||||
hello: "bonjour",
|
hello: "bonjour",
|
||||||
yes: "oui",
|
yes: "oui",
|
||||||
no: "non",
|
no: "non",
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
hello: "bom dia",
|
||||||
|
yes: "sim",
|
||||||
|
no: "não",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const translateFn = (str) => translations[str] || str;
|
const translateFn = (str, ctx) => translations[ctx]?.[str] || str;
|
||||||
|
|
||||||
const app = new App(Root, { templates, tranaslateFn });
|
const app = new App(Root, { templates, tranaslateFn });
|
||||||
// ...
|
// ...
|
||||||
@@ -27,6 +38,11 @@ Once setup, all rendered templates will be translated using `translateFn`:
|
|||||||
`placeholder`, `label` and `alt`,
|
`placeholder`, `label` and `alt`,
|
||||||
- translating text nodes can be disabled with the special attribute `t-translation`,
|
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||||
if its value is `off`.
|
if its value is `off`.
|
||||||
|
- the translate function receives as second parameter a context that can be used
|
||||||
|
to contextualized the translation. That context can be set globally on a node
|
||||||
|
and its children by using `t-translation-context`. If a specific node
|
||||||
|
attribute `x` needs another context, that context can be specified with a
|
||||||
|
special directive `t-translation-context-x`.
|
||||||
|
|
||||||
So, with the above `translateFn`, the following templates:
|
So, with the above `translateFn`, the following templates:
|
||||||
|
|
||||||
@@ -46,6 +62,22 @@ will be rendered as:
|
|||||||
<input placeholder="bonjour" other="yes"/>
|
<input placeholder="bonjour" other="yes"/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
and the following template:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-translation-context="fr" title="hello">hello</div>
|
||||||
|
<div>Are you sure?</div>
|
||||||
|
<input t-translation-context-placeholder="pt" placeholder="hello" other="yes"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
will be rendered as:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div title="bonjour">bonjour</div>
|
||||||
|
<div>Are you sure?</div>
|
||||||
|
<input placeholder="bom dia" other="yes"/>
|
||||||
|
```
|
||||||
|
|
||||||
Note that the translation is done during the compilation of the template, not
|
Note that the translation is done during the compilation of the template, not
|
||||||
when it is rendered.
|
when it is rendered.
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ functions are all available in the `owl.utils` namespace.
|
|||||||
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
||||||
- [`EventBus`](#eventbus): a simple EventBus
|
- [`EventBus`](#eventbus): a simple EventBus
|
||||||
- [`validate`](#validate): a validation function
|
- [`validate`](#validate): a validation function
|
||||||
|
- [`batched`](#batched): batch function calls
|
||||||
|
|
||||||
## `whenReady`
|
## `whenReady`
|
||||||
|
|
||||||
@@ -78,3 +79,22 @@ validate(
|
|||||||
// - 'id' is missing (should be a number),
|
// - 'id' is missing (should be a number),
|
||||||
// - 'url' is missing (should be a boolean or list of numbers),
|
// - 'url' is missing (should be a boolean or list of numbers),
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `batched`
|
||||||
|
|
||||||
|
The `batched` function creates a batched version of a callback so that multiple calls to it within the same microtick will only result in a single invocation of the original callback.
|
||||||
|
|
||||||
|
```js
|
||||||
|
function hello() {
|
||||||
|
console.log("hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchedHello = batched(hello);
|
||||||
|
batchedHello();
|
||||||
|
// Nothing is logged
|
||||||
|
batchedHello();
|
||||||
|
// Still not logged
|
||||||
|
|
||||||
|
await Promise.resolve(); // Await the next microtick
|
||||||
|
// "hello" is logged only once
|
||||||
|
```
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ The components tab is separated into two sub windows: the components tree in the
|
|||||||
the component details in the right. The components tree will display all the different
|
the component details in the right. The components tree will display all the different
|
||||||
components that are present in the tab in the form of a tree. The root of this tree is
|
components that are present in the tab in the form of a tree. The root of this tree is
|
||||||
actually the app which is not a component but can still be inspected by the devtools like
|
actually the app which is not a component but can still be inspected by the devtools like
|
||||||
one. There can also be multiple apps loaded in the page like in the following:
|
one. There can also be multiple apps loaded in the page like in website:
|
||||||
|
|
||||||
<img src="screenshots/multi_apps.png"/>
|
<img src="screenshots/multi_apps.png"/>
|
||||||
|
|
||||||
There is a convenient search bar at the top of the components tree which will help finding
|
There is a convenient search bar at the top of the components tree which will help finding
|
||||||
the components tou want in the tree and also, an element picker can be used to directly select
|
the components tou want in the tree and also, an element picker can be used to directly select
|
||||||
the component you want to focus on in the page which is especially useful when trying to find
|
the component you want to focus on in the page which is especially useful when 1trying to find
|
||||||
what you want. Just click on the elements picker icon and click on the element you want to focus
|
what you want. Just click on the elements picker icon and click on the element you want to focus
|
||||||
on in the page and it will be selected in the devtools accordingly. Hovering any element in the
|
on in the page and it will be selected in the devtools accordingly. Hovering any element in the
|
||||||
page in this mode will highlight it and the same happens anytime in the components tree.
|
page in this mode will highlight it and the same happens anytime in the components tree.
|
||||||
@@ -64,31 +64,40 @@ as its env, props, observed states and all the other variables that are present
|
|||||||
While the props and the env are already present on the actual instance of the component and are
|
While the props and the env are already present on the actual instance of the component and are
|
||||||
pretty explicit by themselves, the observed state value is a bit more complicated to grasp.
|
pretty explicit by themselves, the observed state value is a bit more complicated to grasp.
|
||||||
|
|
||||||
The observed state is actually information about which variables are observed by the component
|
The observed state is actually information about which variables are being observed by the component:
|
||||||
which will trigger a rerender of the component when it is modified. The keys represent which part of the
|
when any property of a reactive object is being read by the component, the component will subscribe
|
||||||
variable is actually observed and the target is the actual variable. For simplicity, the properties
|
to this property which means it will listen to any change that can occur on the property and render
|
||||||
that are not observed by the component are greyed out while the others are in bold. This means that
|
when such a change occurs. This can be visualized easily within the devtools inside of the observed
|
||||||
editing bold ones will trigger a rerender while the greyed out ones will not.
|
state section: observed properties of the reactive object(s) are displayed in bold while the others
|
||||||
|
are greyed out. Do keep in mind that a greyed out property in the observed state of one component
|
||||||
|
may be observed by another and the other way around is also possible. Here is an example for some
|
||||||
|
user Field component:
|
||||||
|
|
||||||
<img src="screenshots/states.png"/>
|
<img src="screenshots/states.png"/>
|
||||||
|
|
||||||
In the given example, we have two keys/target pairs for two different variables. The first one indicates
|
Navigation inside the properties is also similar to the one in console variables: properties have
|
||||||
that adding or removing an element to the array will trigger a rerender since the length will have changed.
|
|
||||||
Replacing the element at index 0, 1 or 2 will also have the same effect as implied by the keys. It doesn't
|
|
||||||
mean that editing the properties of element at index 0, 1 or 2 will rerender the component though. It may
|
|
||||||
be the case for some but this will be described in another keys/target pair. The second keys/target pair
|
|
||||||
is actually the element at index 0 of the first pair. It only has id in the keys meaning that only the
|
|
||||||
id property will actually trigger a rerender the component when modified. Be aware however that the other
|
|
||||||
properties may be in the observed state of another component like a child one in this case. A greyed out
|
|
||||||
property only implies it is not reactive for the selected component and not for the others.
|
|
||||||
|
|
||||||
The navigation inside the properties is also similar to the one in console variables: properties have
|
|
||||||
their prototype displayed and getters will get their value when clicked on (...). It is also possible to
|
their prototype displayed and getters will get their value when clicked on (...). It is also possible to
|
||||||
send any property to the console using the right-click context menu on it and functions can be inspected
|
send any property to the console using the right-click context menu on it and functions can be inspected
|
||||||
in the sources tab as well.
|
in the sources tab as well.
|
||||||
|
|
||||||
<img src="screenshots/function_menu.png"/>
|
<img src="screenshots/function_menu.png"/>
|
||||||
|
|
||||||
|
Using the right-click context menu on a property also allows to observe variables. Observed variables will
|
||||||
|
be sent to a dedicated section of the details window and their value will be refreshed every 200ms. These
|
||||||
|
variables are only shown when they are found and their access path will be kept in memory inside the
|
||||||
|
browser so that it will always persist until the user decides to stop observing the variable. As in the
|
||||||
|
browser's devtools, observed objects are displayed in reduced form and cannot be interacted with. It is
|
||||||
|
still possible to send them to the console or remove them from the list using right-click.
|
||||||
|
|
||||||
|
<img src="screenshots/observe_variables.png"/>
|
||||||
|
|
||||||
|
The last section of the details window is filled with the component's lifecycle hooks. Using right click on
|
||||||
|
them allows to place breakpoints inside the hook (either on its instance or class, hooks like mounted and
|
||||||
|
willStart cannot have instance-based breakpoints because they will never trigger). Conditions in conditional
|
||||||
|
breakpoints will be evaluated in the context of the component's definition.
|
||||||
|
|
||||||
|
<img src="screenshots/hooks.png"/>
|
||||||
|
|
||||||
There are several icons available to perform several of the actions described before in the components
|
There are several icons available to perform several of the actions described before in the components
|
||||||
tree context menu and all these actions are also available by opening the menu by right-clicking on the
|
tree context menu and all these actions are also available by opening the menu by right-clicking on the
|
||||||
component's name. Using the left click on the component's name will focus it in the components tree.
|
component's name. Using the left click on the component's name will focus it in the components tree.
|
||||||
@@ -96,8 +105,10 @@ component's name. Using the left click on the component's name will focus it in
|
|||||||
It is also possible to edit any of the leaf node properties. To do so, you must double click on the
|
It is also possible to edit any of the leaf node properties. To do so, you must double click on the
|
||||||
property's value and modify it using the freshly created input then press enter to apply the changes.
|
property's value and modify it using the freshly created input then press enter to apply the changes.
|
||||||
Do note that the modified values should be written in JSON format in order to be valid (examples:
|
Do note that the modified values should be written in JSON format in order to be valid (examples:
|
||||||
89, "yes", undefined, null, \["hello", 15\], {"a": 1}, true, ...). Whether it has an impact on the
|
89, "yes", undefined, null, \["hello", 15\], {"a": 1}, true, ...). Editing any value will produce a
|
||||||
component or not and whether it produces an error is the responsability of the user.
|
manual render of the component (or the root component of the application in the case of env values).
|
||||||
|
Whether the edition has an impact on the component or not and whether it produces an error is the
|
||||||
|
responsability of the user.
|
||||||
|
|
||||||
<img src="screenshots/edit.png"/>
|
<img src="screenshots/edit.png"/>
|
||||||
|
|
||||||
@@ -117,7 +128,8 @@ are intercepted by the devtools using the record button.
|
|||||||
The second button is used to clear all the events that have been recorded. The select can be used to
|
The second button is used to clear all the events that have been recorded. The select can be used to
|
||||||
switch between the tree view (which shows the causality between renders) and the events log view which
|
switch between the tree view (which shows the causality between renders) and the events log view which
|
||||||
simply displays the events in the exact order they were triggered. In this view, you can expand the create,
|
simply displays the events in the exact order they were triggered. In this view, you can expand the create,
|
||||||
update and destroy events which reveals the component that initiated the event.
|
update and destroy events which reveals the component that initiated the event. Also, a transition line will
|
||||||
|
appear each time a new animation frame has been loaded between events.
|
||||||
|
|
||||||
<img src="screenshots/events_log.png"/>
|
<img src="screenshots/events_log.png"/>
|
||||||
|
|
||||||
@@ -131,20 +143,29 @@ There is also the Trace Renderings and Trace Subscriptions features. These featu
|
|||||||
recording of events and have no effect on the profiler tab. The Trace Renderings option is used to log in
|
recording of events and have no effect on the profiler tab. The Trace Renderings option is used to log in
|
||||||
the console all the render events and allows to show their traceback information. Similarly, the Trace
|
the console all the render events and allows to show their traceback information. Similarly, the Trace
|
||||||
Subscriptions option logs all the properties that caused a render event and also allows to see the traceback
|
Subscriptions option logs all the properties that caused a render event and also allows to see the traceback
|
||||||
of the modification
|
of the modification.
|
||||||
|
|
||||||
<img src="screenshots/trace_rendering.png"/>
|
<img src="screenshots/trace_rendering.png"/>
|
||||||
<img src="screenshots/trace_subscriptions.png"/>
|
<img src="screenshots/trace_subscriptions.png"/>
|
||||||
|
|
||||||
|
The Owl Devtools also allow to inspect iframes coded in Owl: when an Owl iframe is detected in the page,
|
||||||
|
the iframe selector will appear next to the tabs. This allows to switch from an iframe to another easily.
|
||||||
|
Be aware that switching iframes will clear all record events from the profiler tab. Iframes detection is
|
||||||
|
currently not working in the firefox version, we are aware of this issue and will try to address it in the
|
||||||
|
future.
|
||||||
|
|
||||||
|
<img src="screenshots/iframes.png"/>
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
The owl devtools extension has a dark mode feature which defaults to your general devtools settings and can
|
The owl devtools extension has a dark mode feature which defaults to your general devtools settings and can
|
||||||
be toggled using the sun/moon icon at the top-right corner of the tab. All the examples above were created
|
be toggled using the sun/moon icon at the top-right corner of the tab. There is also a refresh button to
|
||||||
with the dark mode enabled. There is also a refresh button to completely reset the owl devtools.
|
completely reset the owl devtools.
|
||||||
|
|
||||||
<img src="screenshots/darkmode.png"/>
|
<img src="screenshots/darkmode.png"/>
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
If the feedback from the page to the devtools seems to be cut, just close the devtools and refresh the page.
|
If the feedback from the page to the devtools seems to be cut, you can first try to use the refresh
|
||||||
|
button mentioned above but if it still doesn't seem to work, just close the devtools and refresh the page.
|
||||||
This will eventually happen any time a tab stays opened for too long without being refreshed.
|
This will eventually happen any time a tab stays opened for too long without being refreshed.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 333 KiB After Width: | Height: | Size: 314 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 488 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 182 KiB |
@@ -41,9 +41,6 @@ const loadFile = (path) => {
|
|||||||
* Make an iframe, with all the js, css and xml properly injected.
|
* Make an iframe, with all the js, css and xml properly injected.
|
||||||
*/
|
*/
|
||||||
function makeCodeIframe(js, css, xml) {
|
function makeCodeIframe(js, css, xml) {
|
||||||
// escape backticks in the xml so they don't close the template string
|
|
||||||
const escapedXml = xml.replace(/`/g, '\\\`');
|
|
||||||
|
|
||||||
const iframe = document.createElement("iframe");
|
const iframe = document.createElement("iframe");
|
||||||
iframe.onload = () => {
|
iframe.onload = () => {
|
||||||
const doc = iframe.contentDocument;
|
const doc = iframe.contentDocument;
|
||||||
@@ -55,6 +52,8 @@ function makeCodeIframe(js, css, xml) {
|
|||||||
|
|
||||||
const script = doc.createElement("script");
|
const script = doc.createElement("script");
|
||||||
script.type = "module";
|
script.type = "module";
|
||||||
|
// escape characters with special meaning in template literals
|
||||||
|
const escapedXml = xml.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${");
|
||||||
script.textContent = `const TEMPLATES = \`${escapedXml}\`\n${js}`;
|
script.textContent = `const TEMPLATES = \`${escapedXml}\`\n${js}`;
|
||||||
doc.body.appendChild(script);
|
doc.body.appendChild(script);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "2.2.6",
|
"version": "2.7.0",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "dist/owl.cjs.js",
|
"main": "dist/owl.cjs.js",
|
||||||
"module": "dist/owl.es.js",
|
"module": "dist/owl.es.js",
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.18.3"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:bundle": "rollup -c --failAfterWarnings",
|
"build:bundle": "rollup -c --failAfterWarnings",
|
||||||
@@ -32,7 +32,10 @@
|
|||||||
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
|
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
|
||||||
"lint": "eslint src/**/*.ts tests/**/*.ts",
|
"lint": "eslint src/**/*.ts tests/**/*.ts",
|
||||||
"release": "node tools/release.js",
|
"release": "node tools/release.js",
|
||||||
"compile_templates": "node tools/compile_xml.js"
|
"compile_templates": "node tools/compile_owl_templates.mjs"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"compile_owl_templates": "tools/compile_owl_templates.mjs"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -46,6 +49,7 @@
|
|||||||
"homepage": "https://github.com/odoo/owl#readme",
|
"homepage": "https://github.com/odoo/owl#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.0.1",
|
"@types/jest": "^27.0.1",
|
||||||
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^14.11.8",
|
"@types/node": "^14.11.8",
|
||||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
"@typescript-eslint/eslint-plugin": "5.48.1",
|
||||||
"@typescript-eslint/parser": "5.48.1",
|
"@typescript-eslint/parser": "5.48.1",
|
||||||
@@ -97,5 +101,8 @@
|
|||||||
"prettier": {
|
"prettier": {
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"endOfLine": "auto"
|
"endOfLine": "auto"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"jsdom": "^25.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pkg from "./package.json";
|
import pkg from "./package.json";
|
||||||
import git from "git-rev-sync";
|
import git from "git-rev-sync";
|
||||||
import typescript from 'rollup-plugin-typescript2';
|
import typescript from "rollup-plugin-typescript2";
|
||||||
import { terser } from "rollup-plugin-terser";
|
import { terser } from "rollup-plugin-terser";
|
||||||
import dts from "rollup-plugin-dts";
|
import dts from "rollup-plugin-dts";
|
||||||
|
|
||||||
@@ -22,38 +22,36 @@ __info__.url = 'https://github.com/odoo/owl';
|
|||||||
|
|
||||||
switch (process.argv[4]) {
|
switch (process.argv[4]) {
|
||||||
case "compiler":
|
case "compiler":
|
||||||
input = "src/compiler/index.ts",
|
(input = "src/compiler/index.ts"),
|
||||||
output = [
|
(output = [getConfigForFormat("cjs", "dist/compiler.js", "")]);
|
||||||
getConfigForFormat('cjs', 'dist/compiler.js', ''),
|
|
||||||
]
|
|
||||||
break;
|
break;
|
||||||
case "runtime":
|
case "runtime":
|
||||||
input = "src/runtime/index.ts";
|
input = "src/runtime/index.ts";
|
||||||
output = [
|
output = [
|
||||||
getConfigForFormat('esm', addSuffix(ES_FILENAME, 'runtime'), outro),
|
getConfigForFormat("esm", addSuffix(ES_FILENAME, "runtime"), outro),
|
||||||
getConfigForFormat('cjs', addSuffix(CJS_FILENAME, 'runtime'), outro),
|
getConfigForFormat("cjs", addSuffix(CJS_FILENAME, "runtime"), outro),
|
||||||
getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro),
|
getConfigForFormat("iife", addSuffix(IIFE_FILENAME, "runtime"), outro),
|
||||||
getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro, true),
|
getConfigForFormat("iife", addSuffix(IIFE_FILENAME, "runtime"), outro, true),
|
||||||
]
|
];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
input = "src/index.ts",
|
(input = "src/index.ts"),
|
||||||
output = [
|
(output = [
|
||||||
getConfigForFormat('esm', ES_FILENAME, outro),
|
getConfigForFormat("esm", ES_FILENAME, outro),
|
||||||
getConfigForFormat('cjs', CJS_FILENAME, outro),
|
getConfigForFormat("cjs", CJS_FILENAME, outro),
|
||||||
getConfigForFormat('iife', IIFE_FILENAME, outro),
|
getConfigForFormat("iife", IIFE_FILENAME, outro),
|
||||||
getConfigForFormat('iife', IIFE_FILENAME, outro, true),
|
getConfigForFormat("iife", IIFE_FILENAME, outro, true),
|
||||||
]
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate from a string depicting a path a new path for the minified version.
|
* Generate from a string depicting a path a new path for the minified version.
|
||||||
* @param {string} pkgFileName file name
|
* @param {string} pkgFileName file name
|
||||||
*/
|
*/
|
||||||
function addSuffix(pkgFileName, suffix) {
|
function addSuffix(pkgFileName, suffix) {
|
||||||
const parts = pkgFileName.split('.');
|
const parts = pkgFileName.split(".");
|
||||||
parts.splice(parts.length - 1, 0, suffix);
|
parts.splice(parts.length - 1, 0, suffix);
|
||||||
return parts.join('.');
|
return parts.join(".");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,7 +69,7 @@ function getConfigForFormat(format, generatedFileName, outro, minified = false)
|
|||||||
outro: outro,
|
outro: outro,
|
||||||
freeze: false,
|
freeze: false,
|
||||||
plugins: minified ? [terser()] : [],
|
plugins: minified ? [terser()] : [],
|
||||||
indent: ' ', // indent with 4 spaces
|
indent: " ", // indent with 4 spaces
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,9 +79,19 @@ export default [
|
|||||||
output,
|
output,
|
||||||
plugins: [
|
plugins: [
|
||||||
typescript({
|
typescript({
|
||||||
useTsconfigDeclarationDir: true
|
useTsconfigDeclarationDir: true,
|
||||||
}),
|
}),
|
||||||
]
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "src/compiler/standalone/index.ts",
|
||||||
|
output: [{ file: "dist/compile_templates.mjs", format: "es" }],
|
||||||
|
external: ["fs", "fs/promises", "path", "jsdom"],
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
useTsconfigDeclarationDir: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "dist/types/index.d.ts",
|
input: "dist/types/index.d.ts",
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export type customDirectives = Record<
|
||||||
|
string,
|
||||||
|
(node: Element, value: string, modifier: string[]) => void
|
||||||
|
>;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { OwlError } from "./owl_error";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
export 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 OwlError(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
ASTTOut,
|
ASTTOut,
|
||||||
ASTTPortal,
|
ASTTPortal,
|
||||||
ASTTranslation,
|
ASTTranslation,
|
||||||
|
ASTTranslationContext,
|
||||||
ASTTSet,
|
ASTTSet,
|
||||||
ASTType,
|
ASTType,
|
||||||
Attrs,
|
Attrs,
|
||||||
@@ -35,7 +36,7 @@ type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment";
|
|||||||
const whitespaceRE = /\s+/g;
|
const whitespaceRE = /\s+/g;
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
translateFn?: (s: string) => string;
|
translateFn?: (s: string, translationCtx: string) => string;
|
||||||
translatableAttributes?: string[];
|
translatableAttributes?: string[];
|
||||||
dev?: boolean;
|
dev?: boolean;
|
||||||
}
|
}
|
||||||
@@ -43,6 +44,7 @@ export interface Config {
|
|||||||
export interface CodeGenOptions extends Config {
|
export interface CodeGenOptions extends Config {
|
||||||
hasSafeContext?: boolean;
|
hasSafeContext?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
hasGlobalValues: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// using a non-html document so that <inner/outer>HTML serializes as XML instead
|
// using a non-html document so that <inner/outer>HTML serializes as XML instead
|
||||||
@@ -82,6 +84,14 @@ function isProp(tag: string, key: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a template literal that evaluates to str. You can add interpolation
|
||||||
|
* sigils into the string if required
|
||||||
|
*/
|
||||||
|
function toStringExpression(str: string) {
|
||||||
|
return `\`${str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${")}\``;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// BlockDescription
|
// BlockDescription
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -162,6 +172,7 @@ interface Context {
|
|||||||
forceNewBlock: boolean;
|
forceNewBlock: boolean;
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
translate: boolean;
|
translate: boolean;
|
||||||
|
translationCtx: string;
|
||||||
tKeyExpr: string | null;
|
tKeyExpr: string | null;
|
||||||
nameSpace?: string;
|
nameSpace?: string;
|
||||||
tModelSelectedExpr?: string;
|
tModelSelectedExpr?: string;
|
||||||
@@ -176,6 +187,7 @@ function createContext(parentCtx: Context, params?: Partial<Context>): Context {
|
|||||||
index: 0,
|
index: 0,
|
||||||
forceNewBlock: true,
|
forceNewBlock: true,
|
||||||
translate: parentCtx.translate,
|
translate: parentCtx.translate,
|
||||||
|
translationCtx: parentCtx.translationCtx,
|
||||||
tKeyExpr: null,
|
tKeyExpr: null,
|
||||||
nameSpace: parentCtx.nameSpace,
|
nameSpace: parentCtx.nameSpace,
|
||||||
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
|
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
|
||||||
@@ -254,7 +266,7 @@ export class CodeGenerator {
|
|||||||
target = new CodeTarget("template");
|
target = new CodeTarget("template");
|
||||||
templateName?: string;
|
templateName?: string;
|
||||||
dev: boolean;
|
dev: boolean;
|
||||||
translateFn: (s: string) => string;
|
translateFn: (s: string, translationCtx: string) => string;
|
||||||
translatableAttributes: string[] = TRANSLATABLE_ATTRS;
|
translatableAttributes: string[] = TRANSLATABLE_ATTRS;
|
||||||
ast: AST;
|
ast: AST;
|
||||||
staticDefs: { id: string; expr: string }[] = [];
|
staticDefs: { id: string; expr: string }[] = [];
|
||||||
@@ -278,6 +290,9 @@ export class CodeGenerator {
|
|||||||
this.dev = options.dev || false;
|
this.dev = options.dev || false;
|
||||||
this.ast = ast;
|
this.ast = ast;
|
||||||
this.templateName = options.name;
|
this.templateName = options.name;
|
||||||
|
if (options.hasGlobalValues) {
|
||||||
|
this.helpers.add("__globals__");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
generateCode(): string {
|
generateCode(): string {
|
||||||
@@ -291,6 +306,7 @@ export class CodeGenerator {
|
|||||||
forceNewBlock: false,
|
forceNewBlock: false,
|
||||||
isLast: true,
|
isLast: true,
|
||||||
translate: true,
|
translate: true,
|
||||||
|
translationCtx: "",
|
||||||
tKeyExpr: null,
|
tKeyExpr: null,
|
||||||
});
|
});
|
||||||
// define blocks and utility functions
|
// define blocks and utility functions
|
||||||
@@ -311,14 +327,13 @@ export class CodeGenerator {
|
|||||||
mainCode.push(``);
|
mainCode.push(``);
|
||||||
for (let block of this.blocks) {
|
for (let block of this.blocks) {
|
||||||
if (block.dom) {
|
if (block.dom) {
|
||||||
let xmlString = block.asXmlString();
|
let xmlString = toStringExpression(block.asXmlString());
|
||||||
xmlString = xmlString.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
||||||
if (block.dynamicTagName) {
|
if (block.dynamicTagName) {
|
||||||
xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
|
xmlString = xmlString.replace(/^`<\w+/, `\`<\${tag || '${block.dom.nodeName}'}`);
|
||||||
xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
|
xmlString = xmlString.replace(/\w+>`$/, `\${tag || '${block.dom.nodeName}'}>\``);
|
||||||
mainCode.push(`let ${block.blockName} = tag => createBlock(\`${xmlString}\`);`);
|
mainCode.push(`let ${block.blockName} = tag => createBlock(${xmlString});`);
|
||||||
} else {
|
} else {
|
||||||
mainCode.push(`let ${block.blockName} = createBlock(\`${xmlString}\`);`);
|
mainCode.push(`let ${block.blockName} = createBlock(${xmlString});`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -446,9 +461,9 @@ export class CodeGenerator {
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
translate(str: string): string {
|
translate(str: string, translationCtx: string): string {
|
||||||
const match = translationRE.exec(str) as any;
|
const match = translationRE.exec(str) as any;
|
||||||
return match[1] + this.translateFn(match[2]) + match[3];
|
return match[1] + this.translateFn(match[2], translationCtx) + match[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -490,6 +505,8 @@ export class CodeGenerator {
|
|||||||
return this.compileTSlot(ast, ctx);
|
return this.compileTSlot(ast, ctx);
|
||||||
case ASTType.TTranslation:
|
case ASTType.TTranslation:
|
||||||
return this.compileTTranslation(ast, ctx);
|
return this.compileTTranslation(ast, ctx);
|
||||||
|
case ASTType.TTranslationContext:
|
||||||
|
return this.compileTTranslationContext(ast, ctx);
|
||||||
case ASTType.TPortal:
|
case ASTType.TPortal:
|
||||||
return this.compileTPortal(ast, ctx);
|
return this.compileTPortal(ast, ctx);
|
||||||
}
|
}
|
||||||
@@ -515,7 +532,7 @@ export class CodeGenerator {
|
|||||||
const isNewBlock = !block || forceNewBlock;
|
const isNewBlock = !block || forceNewBlock;
|
||||||
if (isNewBlock) {
|
if (isNewBlock) {
|
||||||
block = this.createBlock(block, "comment", ctx);
|
block = this.createBlock(block, "comment", ctx);
|
||||||
this.insertBlock(`comment(\`${ast.value}\`)`, block, {
|
this.insertBlock(`comment(${toStringExpression(ast.value)})`, block, {
|
||||||
...ctx,
|
...ctx,
|
||||||
forceNewBlock: forceNewBlock && !block,
|
forceNewBlock: forceNewBlock && !block,
|
||||||
});
|
});
|
||||||
@@ -531,7 +548,7 @@ export class CodeGenerator {
|
|||||||
|
|
||||||
let value = ast.value;
|
let value = ast.value;
|
||||||
if (value && ctx.translate !== false) {
|
if (value && ctx.translate !== false) {
|
||||||
value = this.translate(value);
|
value = this.translate(value, ctx.translationCtx);
|
||||||
}
|
}
|
||||||
if (!ctx.inPreTag) {
|
if (!ctx.inPreTag) {
|
||||||
value = value.replace(whitespaceRE, " ");
|
value = value.replace(whitespaceRE, " ");
|
||||||
@@ -539,7 +556,7 @@ export class CodeGenerator {
|
|||||||
|
|
||||||
if (!block || forceNewBlock) {
|
if (!block || forceNewBlock) {
|
||||||
block = this.createBlock(block, "text", ctx);
|
block = this.createBlock(block, "text", ctx);
|
||||||
this.insertBlock(`text(\`${value}\`)`, block, {
|
this.insertBlock(`text(${toStringExpression(value)})`, block, {
|
||||||
...ctx,
|
...ctx,
|
||||||
forceNewBlock: forceNewBlock && !block,
|
forceNewBlock: forceNewBlock && !block,
|
||||||
});
|
});
|
||||||
@@ -620,7 +637,8 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.translatableAttributes.includes(key)) {
|
} else if (this.translatableAttributes.includes(key)) {
|
||||||
attrs[key] = this.translateFn(ast.attrs[key]);
|
const attrTranslationCtx = ast.attrsTranslationCtx?.[key] || ctx.translationCtx;
|
||||||
|
attrs[key] = this.translateFn(ast.attrs[key], attrTranslationCtx);
|
||||||
} else {
|
} else {
|
||||||
expr = `"${ast.attrs[key]}"`;
|
expr = `"${ast.attrs[key]}"`;
|
||||||
attrName = key;
|
attrName = key;
|
||||||
@@ -774,7 +792,8 @@ export class CodeGenerator {
|
|||||||
expr = compileExpr(ast.expr);
|
expr = compileExpr(ast.expr);
|
||||||
if (ast.defaultValue) {
|
if (ast.defaultValue) {
|
||||||
this.helpers.add("withDefault");
|
this.helpers.add("withDefault");
|
||||||
expr = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
// FIXME: defaultValue is not translated
|
||||||
|
expr = `withDefault(${expr}, ${toStringExpression(ast.defaultValue)})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!block || forceNewBlock) {
|
if (!block || forceNewBlock) {
|
||||||
@@ -1039,7 +1058,7 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = `key + \`${this.generateComponentKey()}\``;
|
const key = this.generateComponentKey();
|
||||||
if (isDynamic) {
|
if (isDynamic) {
|
||||||
const templateVar = generateId("template");
|
const templateVar = generateId("template");
|
||||||
if (!this.staticDefs.find((d) => d.id === "call")) {
|
if (!this.staticDefs.find((d) => d.id === "call")) {
|
||||||
@@ -1091,11 +1110,13 @@ export class CodeGenerator {
|
|||||||
} else {
|
} else {
|
||||||
let value: string;
|
let value: string;
|
||||||
if (ast.defaultValue) {
|
if (ast.defaultValue) {
|
||||||
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
|
const defaultValue = toStringExpression(
|
||||||
|
ctx.translate ? this.translate(ast.defaultValue, ctx.translationCtx) : ast.defaultValue
|
||||||
|
);
|
||||||
if (ast.value) {
|
if (ast.value) {
|
||||||
value = `withDefault(${expr}, \`${defaultValue}\`)`;
|
value = `withDefault(${expr}, ${defaultValue})`;
|
||||||
} else {
|
} else {
|
||||||
value = `\`${defaultValue}\``;
|
value = defaultValue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = expr;
|
value = expr;
|
||||||
@@ -1106,12 +1127,12 @@ export class CodeGenerator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateComponentKey() {
|
generateComponentKey(currentKey: string = "key") {
|
||||||
const parts = [generateId("__")];
|
const parts = [generateId("__")];
|
||||||
for (let i = 0; i < this.target.loopLevel; i++) {
|
for (let i = 0; i < this.target.loopLevel; i++) {
|
||||||
parts.push(`\${key${i + 1}}`);
|
parts.push(`\${key${i + 1}}`);
|
||||||
}
|
}
|
||||||
return parts.join("__");
|
return `${currentKey} + \`${parts.join("__")}\``;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1125,8 +1146,18 @@ export class CodeGenerator {
|
|||||||
* "some-prop" "state" "'some-prop': ctx['state']"
|
* "some-prop" "state" "'some-prop': ctx['state']"
|
||||||
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
||||||
*/
|
*/
|
||||||
formatProp(name: string, value: string): string {
|
formatProp(
|
||||||
|
name: string,
|
||||||
|
value: string,
|
||||||
|
attrsTranslationCtx: { [name: string]: string } | null,
|
||||||
|
translationCtx: string
|
||||||
|
): string {
|
||||||
|
if (name.endsWith(".translate")) {
|
||||||
|
const attrTranslationCtx = attrsTranslationCtx?.[name] || translationCtx;
|
||||||
|
value = toStringExpression(this.translateFn(value, attrTranslationCtx));
|
||||||
|
} else {
|
||||||
value = this.captureExpression(value);
|
value = this.captureExpression(value);
|
||||||
|
}
|
||||||
if (name.includes(".")) {
|
if (name.includes(".")) {
|
||||||
let [_name, suffix] = name.split(".");
|
let [_name, suffix] = name.split(".");
|
||||||
name = _name;
|
name = _name;
|
||||||
@@ -1135,17 +1166,24 @@ export class CodeGenerator {
|
|||||||
value = `(${value}).bind(this)`;
|
value = `(${value}).bind(this)`;
|
||||||
break;
|
break;
|
||||||
case "alike":
|
case "alike":
|
||||||
|
case "translate":
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new OwlError("Invalid prop suffix");
|
throw new OwlError(`Invalid prop suffix: ${suffix}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
|
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
|
||||||
return `${name}: ${value || undefined}`;
|
return `${name}: ${value || undefined}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
formatPropObject(obj: { [prop: string]: any }): string[] {
|
formatPropObject(
|
||||||
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));
|
obj: { [prop: string]: any },
|
||||||
|
attrsTranslationCtx: { [name: string]: string } | null,
|
||||||
|
translationCtx: string
|
||||||
|
): string[] {
|
||||||
|
return Object.entries(obj).map(([k, v]) =>
|
||||||
|
this.formatProp(k, v, attrsTranslationCtx, translationCtx)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPropString(props: string[], dynProps: string | null): string {
|
getPropString(props: string[], dynProps: string | null): string {
|
||||||
@@ -1162,7 +1200,9 @@ export class CodeGenerator {
|
|||||||
let { block } = ctx;
|
let { block } = ctx;
|
||||||
// props
|
// props
|
||||||
const hasSlotsProp = "slots" in (ast.props || {});
|
const hasSlotsProp = "slots" in (ast.props || {});
|
||||||
const props: string[] = ast.props ? this.formatPropObject(ast.props) : [];
|
const props: string[] = ast.props
|
||||||
|
? this.formatPropObject(ast.props, ast.propsTranslationCtx, ctx.translationCtx)
|
||||||
|
: [];
|
||||||
|
|
||||||
// slots
|
// slots
|
||||||
let slotDef: string = "";
|
let slotDef: string = "";
|
||||||
@@ -1186,7 +1226,13 @@ export class CodeGenerator {
|
|||||||
params.push(`__scope: "${scope}"`);
|
params.push(`__scope: "${scope}"`);
|
||||||
}
|
}
|
||||||
if (ast.slots[slotName].attrs) {
|
if (ast.slots[slotName].attrs) {
|
||||||
params.push(...this.formatPropObject(ast.slots[slotName].attrs!));
|
params.push(
|
||||||
|
...this.formatPropObject(
|
||||||
|
ast.slots[slotName].attrs!,
|
||||||
|
ast.slots[slotName].attrsTranslationCtx,
|
||||||
|
ctx.translationCtx
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const slotInfo = `{${params.join(", ")}}`;
|
const slotInfo = `{${params.join(", ")}}`;
|
||||||
slotStr.push(`'${slotName}': ${slotInfo}`);
|
slotStr.push(`'${slotName}': ${slotInfo}`);
|
||||||
@@ -1214,7 +1260,6 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cmap key
|
// cmap key
|
||||||
const key = this.generateComponentKey();
|
|
||||||
let expr: string;
|
let expr: string;
|
||||||
if (ast.isDynamic) {
|
if (ast.isDynamic) {
|
||||||
expr = generateId("Comp");
|
expr = generateId("Comp");
|
||||||
@@ -1232,7 +1277,7 @@ export class CodeGenerator {
|
|||||||
this.insertAnchor(block);
|
this.insertAnchor(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
let keyArg = `key + \`${key}\``;
|
let keyArg = this.generateComponentKey();
|
||||||
if (ctx.tKeyExpr) {
|
if (ctx.tKeyExpr) {
|
||||||
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
||||||
}
|
}
|
||||||
@@ -1305,16 +1350,17 @@ export class CodeGenerator {
|
|||||||
isMultiple = isMultiple || this.slotNames.has(ast.name);
|
isMultiple = isMultiple || this.slotNames.has(ast.name);
|
||||||
this.slotNames.add(ast.name);
|
this.slotNames.add(ast.name);
|
||||||
}
|
}
|
||||||
const dynProps = ast.attrs ? ast.attrs["t-props"] : null;
|
const attrs = { ...ast.attrs };
|
||||||
if (ast.attrs) {
|
const dynProps = attrs["t-props"];
|
||||||
delete ast.attrs["t-props"];
|
delete attrs["t-props"];
|
||||||
}
|
|
||||||
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
||||||
if (isMultiple) {
|
if (isMultiple) {
|
||||||
key = `${key} + \`${this.generateComponentKey()}\``;
|
key = this.generateComponentKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
const props = ast.attrs
|
||||||
|
? this.formatPropObject(attrs, ast.attrsTranslationCtx, ctx.translationCtx)
|
||||||
|
: [];
|
||||||
const scope = this.getPropString(props, dynProps);
|
const scope = this.getPropString(props, dynProps);
|
||||||
if (ast.defaultContent) {
|
if (ast.defaultContent) {
|
||||||
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
||||||
@@ -1347,6 +1393,15 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
compileTTranslationContext(ast: ASTTranslationContext, ctx: Context): string | null {
|
||||||
|
if (ast.content) {
|
||||||
|
return this.compileAST(
|
||||||
|
ast.content,
|
||||||
|
Object.assign({}, ctx, { translationCtx: ast.translationCtx })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
compileTPortal(ast: ASTTPortal, ctx: Context): string {
|
compileTPortal(ast: ASTTPortal, ctx: Context): string {
|
||||||
if (!this.staticDefs.find((d) => d.id === "Portal")) {
|
if (!this.staticDefs.find((d) => d.id === "Portal")) {
|
||||||
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
|
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
|
||||||
@@ -1354,7 +1409,6 @@ export class CodeGenerator {
|
|||||||
|
|
||||||
let { block } = ctx;
|
let { block } = ctx;
|
||||||
const name = this.compileInNewTarget("slot", ast.content, ctx);
|
const name = this.compileInNewTarget("slot", ast.content, ctx);
|
||||||
const key = this.generateComponentKey();
|
|
||||||
let ctxStr = "ctx";
|
let ctxStr = "ctx";
|
||||||
if (this.target.loopLevel || !this.hasSafeContext) {
|
if (this.target.loopLevel || !this.hasSafeContext) {
|
||||||
ctxStr = generateId("ctx");
|
ctxStr = generateId("ctx");
|
||||||
@@ -1368,7 +1422,8 @@ export class CodeGenerator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const target = compileExpr(ast.target);
|
const target = compileExpr(ast.target);
|
||||||
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`;
|
const key = this.generateComponentKey();
|
||||||
|
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, ${key}, node, ctx, Portal)`;
|
||||||
if (block) {
|
if (block) {
|
||||||
this.insertAnchor(block);
|
this.insertAnchor(block);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { customDirectives } from "../common/types";
|
||||||
import type { TemplateSet } from "../runtime/template_set";
|
import type { TemplateSet } from "../runtime/template_set";
|
||||||
import type { BDom } from "../runtime/blockdom";
|
import type { BDom } from "../runtime/blockdom";
|
||||||
import { CodeGenerator, Config } from "./code_generator";
|
import { CodeGenerator, Config } from "./code_generator";
|
||||||
@@ -10,13 +11,17 @@ export type TemplateFunction = (app: TemplateSet, bdom: any, helpers: any) => Te
|
|||||||
|
|
||||||
interface CompileOptions extends Config {
|
interface CompileOptions extends Config {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
customDirectives?: customDirectives;
|
||||||
|
hasGlobalValues: boolean;
|
||||||
}
|
}
|
||||||
export function compile(
|
export function compile(
|
||||||
template: string | Element,
|
template: string | Element,
|
||||||
options: CompileOptions = {}
|
options: CompileOptions = {
|
||||||
|
hasGlobalValues: false,
|
||||||
|
}
|
||||||
): TemplateFunction {
|
): TemplateFunction {
|
||||||
// parsing
|
// parsing
|
||||||
const ast = parse(template);
|
const ast = parse(template, options.customDirectives);
|
||||||
|
|
||||||
// some work
|
// some work
|
||||||
const hasSafeContext =
|
const hasSafeContext =
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { OwlError } from "../common/owl_error";
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
const RESERVED_WORDS =
|
const RESERVED_WORDS =
|
||||||
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date".split(
|
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date,__globals__".split(
|
||||||
","
|
","
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ export function compileExprToArray(expr: string): Token[] {
|
|||||||
const localVars = new Set<string>();
|
const localVars = new Set<string>();
|
||||||
const tokens = tokenize(expr);
|
const tokens = tokenize(expr);
|
||||||
let i = 0;
|
let i = 0;
|
||||||
let stack = []; // to track last opening [ or {
|
let stack = []; // to track last opening (, [ or {
|
||||||
|
|
||||||
while (i < tokens.length) {
|
while (i < tokens.length) {
|
||||||
let token = tokens[i];
|
let token = tokens[i];
|
||||||
@@ -279,10 +279,12 @@ export function compileExprToArray(expr: string): Token[] {
|
|||||||
switch (token.type) {
|
switch (token.type) {
|
||||||
case "LEFT_BRACE":
|
case "LEFT_BRACE":
|
||||||
case "LEFT_BRACKET":
|
case "LEFT_BRACKET":
|
||||||
|
case "LEFT_PAREN":
|
||||||
stack.push(token.type);
|
stack.push(token.type);
|
||||||
break;
|
break;
|
||||||
case "RIGHT_BRACE":
|
case "RIGHT_BRACE":
|
||||||
case "RIGHT_BRACKET":
|
case "RIGHT_BRACKET":
|
||||||
|
case "RIGHT_PAREN":
|
||||||
stack.pop();
|
stack.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { OwlError } from "../common/owl_error";
|
import { OwlError } from "../common/owl_error";
|
||||||
|
import type { customDirectives } from "../common/types";
|
||||||
|
import { parseXML } from "../common/utils";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// AST Type definition
|
// AST Type definition
|
||||||
@@ -25,6 +27,7 @@ export const enum ASTType {
|
|||||||
TSlot,
|
TSlot,
|
||||||
TCallBlock,
|
TCallBlock,
|
||||||
TTranslation,
|
TTranslation,
|
||||||
|
TTranslationContext,
|
||||||
TPortal,
|
TPortal,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +57,7 @@ export interface ASTDomNode {
|
|||||||
tag: string;
|
tag: string;
|
||||||
content: AST[];
|
content: AST[];
|
||||||
attrs: Attrs | null;
|
attrs: Attrs | null;
|
||||||
|
attrsTranslationCtx: Attrs | null;
|
||||||
ref: string | null;
|
ref: string | null;
|
||||||
on: EventHandlers | null;
|
on: EventHandlers | null;
|
||||||
model: TModelInfo | null;
|
model: TModelInfo | null;
|
||||||
@@ -125,6 +129,7 @@ interface SlotDefinition {
|
|||||||
scope: string | null;
|
scope: string | null;
|
||||||
on: EventHandlers | null;
|
on: EventHandlers | null;
|
||||||
attrs: Attrs | null;
|
attrs: Attrs | null;
|
||||||
|
attrsTranslationCtx: Attrs | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTComponent {
|
export interface ASTComponent {
|
||||||
@@ -134,6 +139,7 @@ export interface ASTComponent {
|
|||||||
dynamicProps: string | null;
|
dynamicProps: string | null;
|
||||||
on: EventHandlers | null;
|
on: EventHandlers | null;
|
||||||
props: { [name: string]: string } | null;
|
props: { [name: string]: string } | null;
|
||||||
|
propsTranslationCtx: { [name: string]: string } | null;
|
||||||
slots: { [name: string]: SlotDefinition } | null;
|
slots: { [name: string]: SlotDefinition } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +147,7 @@ export interface ASTSlot {
|
|||||||
type: ASTType.TSlot;
|
type: ASTType.TSlot;
|
||||||
name: string;
|
name: string;
|
||||||
attrs: Attrs | null;
|
attrs: Attrs | null;
|
||||||
|
attrsTranslationCtx: Attrs | null;
|
||||||
on: EventHandlers | null;
|
on: EventHandlers | null;
|
||||||
defaultContent: AST | null;
|
defaultContent: AST | null;
|
||||||
}
|
}
|
||||||
@@ -166,6 +173,12 @@ export interface ASTTranslation {
|
|||||||
content: AST | null;
|
content: AST | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ASTTranslationContext {
|
||||||
|
type: ASTType.TTranslationContext;
|
||||||
|
content: AST | null;
|
||||||
|
translationCtx: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ASTTPortal {
|
export interface ASTTPortal {
|
||||||
type: ASTType.TPortal;
|
type: ASTType.TPortal;
|
||||||
target: string;
|
target: string;
|
||||||
@@ -190,6 +203,7 @@ export type AST =
|
|||||||
| ASTLog
|
| ASTLog
|
||||||
| ASTDebug
|
| ASTDebug
|
||||||
| ASTTranslation
|
| ASTTranslation
|
||||||
|
| ASTTranslationContext
|
||||||
| ASTTPortal;
|
| ASTTPortal;
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -197,23 +211,26 @@ export type AST =
|
|||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
const cache: WeakMap<Element, AST> = new WeakMap();
|
const cache: WeakMap<Element, AST> = new WeakMap();
|
||||||
|
|
||||||
export function parse(xml: string | Element): AST {
|
export function parse(xml: string | Element, customDir?: customDirectives): AST {
|
||||||
|
const ctx = {
|
||||||
|
inPreTag: false,
|
||||||
|
customDirectives: customDir,
|
||||||
|
};
|
||||||
if (typeof xml === "string") {
|
if (typeof xml === "string") {
|
||||||
const elem = parseXML(`<t>${xml}</t>`).firstChild as Element;
|
const elem = parseXML(`<t>${xml}</t>`).firstChild as Element;
|
||||||
return _parse(elem);
|
return _parse(elem, ctx);
|
||||||
}
|
}
|
||||||
let ast = cache.get(xml);
|
let ast = cache.get(xml);
|
||||||
if (!ast) {
|
if (!ast) {
|
||||||
// we clone here the xml to prevent modifying it in place
|
// we clone here the xml to prevent modifying it in place
|
||||||
ast = _parse(xml.cloneNode(true) as Element);
|
ast = _parse(xml.cloneNode(true) as Element, ctx);
|
||||||
cache.set(xml, ast);
|
cache.set(xml, ast);
|
||||||
}
|
}
|
||||||
return ast;
|
return ast;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _parse(xml: Element): AST {
|
function _parse(xml: Element, ctx: ParsingContext): AST {
|
||||||
normalizeXML(xml);
|
normalizeXML(xml);
|
||||||
const ctx = { inPreTag: false };
|
|
||||||
return parseNode(xml, ctx) || { type: ASTType.Text, value: "" };
|
return parseNode(xml, ctx) || { type: ASTType.Text, value: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +238,7 @@ interface ParsingContext {
|
|||||||
tModelInfo?: TModelInfo | null;
|
tModelInfo?: TModelInfo | null;
|
||||||
nameSpace?: string;
|
nameSpace?: string;
|
||||||
inPreTag: boolean;
|
inPreTag: boolean;
|
||||||
|
customDirectives?: customDirectives;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
||||||
@@ -228,6 +246,7 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
|||||||
return parseTextCommentNode(node, ctx);
|
return parseTextCommentNode(node, ctx);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
parseTCustom(node, ctx) ||
|
||||||
parseTDebugLog(node, ctx) ||
|
parseTDebugLog(node, ctx) ||
|
||||||
parseTForEach(node, ctx) ||
|
parseTForEach(node, ctx) ||
|
||||||
parseTIf(node, ctx) ||
|
parseTIf(node, ctx) ||
|
||||||
@@ -238,6 +257,7 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
|||||||
parseTOutNode(node, ctx) ||
|
parseTOutNode(node, ctx) ||
|
||||||
parseTKey(node, ctx) ||
|
parseTKey(node, ctx) ||
|
||||||
parseTTranslation(node, ctx) ||
|
parseTTranslation(node, ctx) ||
|
||||||
|
parseTTranslationContext(node, ctx) ||
|
||||||
parseTSlot(node, ctx) ||
|
parseTSlot(node, ctx) ||
|
||||||
parseComponent(node, ctx) ||
|
parseComponent(node, ctx) ||
|
||||||
parseDOMNode(node, ctx) ||
|
parseDOMNode(node, ctx) ||
|
||||||
@@ -276,6 +296,37 @@ function parseTextCommentNode(node: Node, ctx: ParsingContext): AST | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseTCustom(node: Element, ctx: ParsingContext): AST | null {
|
||||||
|
if (!ctx.customDirectives) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const nodeAttrsNames = node.getAttributeNames();
|
||||||
|
for (let attr of nodeAttrsNames) {
|
||||||
|
if (attr === "t-custom" || attr === "t-custom-") {
|
||||||
|
throw new OwlError("Missing custom directive name with t-custom directive");
|
||||||
|
}
|
||||||
|
if (attr.startsWith("t-custom-")) {
|
||||||
|
const directiveName = attr.split(".")[0].slice(9);
|
||||||
|
const customDirective = ctx.customDirectives[directiveName];
|
||||||
|
if (!customDirective) {
|
||||||
|
throw new OwlError(`Custom directive "${directiveName}" is not defined`);
|
||||||
|
}
|
||||||
|
const value = node.getAttribute(attr)!;
|
||||||
|
const modifiers = attr.split(".").slice(1);
|
||||||
|
node.removeAttribute(attr);
|
||||||
|
try {
|
||||||
|
customDirective(node, value, modifiers);
|
||||||
|
} catch (error) {
|
||||||
|
throw new OwlError(
|
||||||
|
`Custom directive "${directiveName}" throw the following error: ${error}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return parseNode(node, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// debugging
|
// debugging
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -330,6 +381,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
|
|
||||||
const nodeAttrsNames = node.getAttributeNames();
|
const nodeAttrsNames = node.getAttributeNames();
|
||||||
let attrs: ASTDomNode["attrs"] = null;
|
let attrs: ASTDomNode["attrs"] = null;
|
||||||
|
let attrsTranslationCtx: ASTDomNode["attrsTranslationCtx"] = null;
|
||||||
let on: EventHandlers | null = null;
|
let on: EventHandlers | null = null;
|
||||||
let model: TModelInfo | null = null;
|
let model: TModelInfo | null = null;
|
||||||
|
|
||||||
@@ -366,9 +418,9 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
const isSelect = tagName === "select";
|
const isSelect = tagName === "select";
|
||||||
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
||||||
const isRadioInput = isInput && typeAttr === "radio";
|
const isRadioInput = isInput && typeAttr === "radio";
|
||||||
const hasLazyMod = attr.includes(".lazy");
|
|
||||||
const hasNumberMod = attr.includes(".number");
|
|
||||||
const hasTrimMod = attr.includes(".trim");
|
const hasTrimMod = attr.includes(".trim");
|
||||||
|
const hasLazyMod = hasTrimMod || attr.includes(".lazy");
|
||||||
|
const hasNumberMod = attr.includes(".number");
|
||||||
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
|
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
|
||||||
|
|
||||||
model = {
|
model = {
|
||||||
@@ -390,6 +442,10 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
throw new OwlError(`Invalid attribute: '${attr}'`);
|
throw new OwlError(`Invalid attribute: '${attr}'`);
|
||||||
} else if (attr === "xmlns") {
|
} else if (attr === "xmlns") {
|
||||||
ns = value;
|
ns = value;
|
||||||
|
} else if (attr.startsWith("t-translation-context-")) {
|
||||||
|
const attrName = attr.slice(22);
|
||||||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||||
|
attrsTranslationCtx[attrName] = value;
|
||||||
} else if (attr !== "t-name") {
|
} else if (attr !== "t-name") {
|
||||||
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||||||
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
|
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
|
||||||
@@ -412,6 +468,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
tag: tagName,
|
tag: tagName,
|
||||||
dynamicTag,
|
dynamicTag,
|
||||||
attrs,
|
attrs,
|
||||||
|
attrsTranslationCtx,
|
||||||
on,
|
on,
|
||||||
ref,
|
ref,
|
||||||
content: children,
|
content: children,
|
||||||
@@ -571,7 +628,15 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
if (ast && ast.type === ASTType.TComponent) {
|
if (ast && ast.type === ASTType.TComponent) {
|
||||||
return {
|
return {
|
||||||
...ast,
|
...ast,
|
||||||
slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
|
slots: {
|
||||||
|
default: {
|
||||||
|
content: tcall,
|
||||||
|
scope: null,
|
||||||
|
on: null,
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,9 +771,14 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
let on: ASTComponent["on"] = null;
|
let on: ASTComponent["on"] = null;
|
||||||
|
|
||||||
let props: ASTComponent["props"] = null;
|
let props: ASTComponent["props"] = null;
|
||||||
|
let propsTranslationCtx: ASTComponent["propsTranslationCtx"] = null;
|
||||||
for (let name of node.getAttributeNames()) {
|
for (let name of node.getAttributeNames()) {
|
||||||
const value = node.getAttribute(name)!;
|
const value = node.getAttribute(name)!;
|
||||||
if (name.startsWith("t-")) {
|
if (name.startsWith("t-translation-context-")) {
|
||||||
|
const attrName = name.slice(22);
|
||||||
|
propsTranslationCtx = propsTranslationCtx || {};
|
||||||
|
propsTranslationCtx[attrName] = value;
|
||||||
|
} else if (name.startsWith("t-")) {
|
||||||
if (name.startsWith("t-on-")) {
|
if (name.startsWith("t-on-")) {
|
||||||
on = on || {};
|
on = on || {};
|
||||||
on[name.slice(5)] = value;
|
on[name.slice(5)] = value;
|
||||||
@@ -740,14 +810,14 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
// be ignored)
|
// be ignored)
|
||||||
let el = slotNode.parentElement!;
|
let el = slotNode.parentElement!;
|
||||||
let isInSubComponent = false;
|
let isInSubComponent = false;
|
||||||
while (el !== clone) {
|
while (el && el !== clone) {
|
||||||
if (el!.hasAttribute("t-component") || el!.tagName[0] === el!.tagName[0].toUpperCase()) {
|
if (el!.hasAttribute("t-component") || el!.tagName[0] === el!.tagName[0].toUpperCase()) {
|
||||||
isInSubComponent = true;
|
isInSubComponent = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
el = el.parentElement!;
|
el = el.parentElement!;
|
||||||
}
|
}
|
||||||
if (isInSubComponent) {
|
if (isInSubComponent || !el) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -756,12 +826,17 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
const slotAst = parseNode(slotNode, ctx);
|
const slotAst = parseNode(slotNode, ctx);
|
||||||
let on: SlotDefinition["on"] = null;
|
let on: SlotDefinition["on"] = null;
|
||||||
let attrs: Attrs | null = null;
|
let attrs: Attrs | null = null;
|
||||||
|
let attrsTranslationCtx: Attrs | null = null;
|
||||||
let scope: string | null = null;
|
let scope: string | null = null;
|
||||||
for (let attributeName of slotNode.getAttributeNames()) {
|
for (let attributeName of slotNode.getAttributeNames()) {
|
||||||
const value = slotNode.getAttribute(attributeName)!;
|
const value = slotNode.getAttribute(attributeName)!;
|
||||||
if (attributeName === "t-slot-scope") {
|
if (attributeName === "t-slot-scope") {
|
||||||
scope = value;
|
scope = value;
|
||||||
continue;
|
continue;
|
||||||
|
} else if (attributeName.startsWith("t-translation-context-")) {
|
||||||
|
const attrName = attributeName.slice(22);
|
||||||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||||
|
attrsTranslationCtx[attrName] = value;
|
||||||
} else if (attributeName.startsWith("t-on-")) {
|
} else if (attributeName.startsWith("t-on-")) {
|
||||||
on = on || {};
|
on = on || {};
|
||||||
on[attributeName.slice(5)] = value;
|
on[attributeName.slice(5)] = value;
|
||||||
@@ -771,7 +846,7 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
slots = slots || {};
|
slots = slots || {};
|
||||||
slots[name] = { content: slotAst, on, attrs, scope };
|
slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope };
|
||||||
}
|
}
|
||||||
|
|
||||||
// default slot
|
// default slot
|
||||||
@@ -779,10 +854,25 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
slots = slots || {};
|
slots = slots || {};
|
||||||
// t-set-slot="default" has priority over content
|
// t-set-slot="default" has priority over content
|
||||||
if (defaultContent && !slots.default) {
|
if (defaultContent && !slots.default) {
|
||||||
slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
|
slots.default = {
|
||||||
|
content: defaultContent,
|
||||||
|
on,
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
scope: defaultSlotScope,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots, on };
|
return {
|
||||||
|
type: ASTType.TComponent,
|
||||||
|
name,
|
||||||
|
isDynamic,
|
||||||
|
dynamicProps,
|
||||||
|
props,
|
||||||
|
propsTranslationCtx,
|
||||||
|
slots,
|
||||||
|
on,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -796,12 +886,17 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
const name = node.getAttribute("t-slot")!;
|
const name = node.getAttribute("t-slot")!;
|
||||||
node.removeAttribute("t-slot");
|
node.removeAttribute("t-slot");
|
||||||
let attrs: Attrs | null = null;
|
let attrs: Attrs | null = null;
|
||||||
|
let attrsTranslationCtx: Attrs | null = null;
|
||||||
let on: ASTComponent["on"] = null;
|
let on: ASTComponent["on"] = null;
|
||||||
for (let attributeName of node.getAttributeNames()) {
|
for (let attributeName of node.getAttributeNames()) {
|
||||||
const value = node.getAttribute(attributeName)!;
|
const value = node.getAttribute(attributeName)!;
|
||||||
if (attributeName.startsWith("t-on-")) {
|
if (attributeName.startsWith("t-on-")) {
|
||||||
on = on || {};
|
on = on || {};
|
||||||
on[attributeName.slice(5)] = value;
|
on[attributeName.slice(5)] = value;
|
||||||
|
} else if (attributeName.startsWith("t-translation-context-")) {
|
||||||
|
const attrName = attributeName.slice(22);
|
||||||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||||
|
attrsTranslationCtx[attrName] = value;
|
||||||
} else {
|
} else {
|
||||||
attrs = attrs || {};
|
attrs = attrs || {};
|
||||||
attrs[attributeName] = value;
|
attrs[attributeName] = value;
|
||||||
@@ -811,11 +906,16 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
type: ASTType.TSlot,
|
type: ASTType.TSlot,
|
||||||
name,
|
name,
|
||||||
attrs,
|
attrs,
|
||||||
|
attrsTranslationCtx,
|
||||||
on,
|
on,
|
||||||
defaultContent: parseChildNodes(node, ctx),
|
defaultContent: parseChildNodes(node, ctx),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Translation
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
|
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
|
||||||
if (node.getAttribute("t-translation") !== "off") {
|
if (node.getAttribute("t-translation") !== "off") {
|
||||||
return null;
|
return null;
|
||||||
@@ -827,6 +927,23 @@ function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Translation Context
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function parseTTranslationContext(node: Element, ctx: ParsingContext): AST | null {
|
||||||
|
const translationCtx = node.getAttribute("t-translation-context");
|
||||||
|
if (!translationCtx) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
node.removeAttribute("t-translation-context");
|
||||||
|
return {
|
||||||
|
type: ASTType.TTranslationContext,
|
||||||
|
content: parseNode(node, ctx),
|
||||||
|
translationCtx,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Portal
|
// Portal
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -972,40 +1089,3 @@ function normalizeXML(el: Element) {
|
|||||||
normalizeTIf(el);
|
normalizeTIf(el);
|
||||||
normalizeTEscTOut(el);
|
normalizeTEscTOut(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 OwlError(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// This file exports a function that allows compiling templates ahead of time.
|
||||||
|
// It is used by the "compile_owl_template" command registered in the "bin"
|
||||||
|
// section of owl's package.json
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import { readdir, readFile, stat } from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import "./setup_jsdom";
|
||||||
|
// Owl imports must be made after setting up jsdom in the global namespace
|
||||||
|
import { compile } from "..";
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// helpers
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
async function getXmlFiles(paths: string[]): Promise<string[]> {
|
||||||
|
return (
|
||||||
|
await Promise.all(
|
||||||
|
paths.map(async (file) => {
|
||||||
|
const stats = await stat(path.join(file));
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
return await getXmlFiles(
|
||||||
|
(await readdir(file)).map((fileName) => path.join(file, fileName))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (file.endsWith(".xml")) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
|
||||||
|
const a = "·-_,:;";
|
||||||
|
const p = new RegExp(a.split("").join("|"), "g");
|
||||||
|
|
||||||
|
function slugify(str: string) {
|
||||||
|
return str
|
||||||
|
.replace(/\//g, "") // remove /
|
||||||
|
.replace(/\./g, "_") // Replace . with _
|
||||||
|
.replace(p, (c) => "_") // Replace special characters
|
||||||
|
.replace(/&/g, "_and_") // Replace & with ‘and’
|
||||||
|
.replace(/[^\w\-]+/g, ""); // Remove all non-word characters
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// main
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function compileTemplates(paths: string[]) {
|
||||||
|
const files = await getXmlFiles(paths);
|
||||||
|
process.stdout.write(`Processing ${files.length} files`);
|
||||||
|
let xmlStrings = await Promise.all(files.map((file) => readFile(file, "utf8")));
|
||||||
|
|
||||||
|
const templates = [];
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const fileName = files[i];
|
||||||
|
const fileContent = xmlStrings[i];
|
||||||
|
process.stdout.write(`.`);
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(fileContent, "text/xml");
|
||||||
|
for (const template of doc.querySelectorAll("[t-name]")) {
|
||||||
|
const name = template.getAttribute("t-name");
|
||||||
|
if (template.hasAttribute("owl")) {
|
||||||
|
template.removeAttribute("owl");
|
||||||
|
}
|
||||||
|
const fnName = slugify(name!);
|
||||||
|
try {
|
||||||
|
const fn = compile(template).toString().replace("anonymous", fnName);
|
||||||
|
templates.push(`"${name}": ${fn},\n`);
|
||||||
|
} catch (e) {
|
||||||
|
errors.push({ name, fileName, e });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.stdout.write(`\n`);
|
||||||
|
|
||||||
|
for (let { name, fileName, e } of errors) {
|
||||||
|
console.warn(`Error while compiling '${name}' (in file ${fileName})`);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
console.log(`${templates.length} templates compiled`);
|
||||||
|
|
||||||
|
return `export const templates = {\n ${templates.join("\n")} \n}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import jsdom from "jsdom";
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// add global DOM stuff for compiler. Needs to be in a separate file so rollup
|
||||||
|
// doesn't hoist the owl imports above this block of code.
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
var document = new jsdom.JSDOM("", {});
|
||||||
|
var window = document.window;
|
||||||
|
global.document = window.document;
|
||||||
|
global.window = window as unknown as Window & typeof globalThis;
|
||||||
|
global.DOMParser = window.DOMParser;
|
||||||
|
global.Element = window.Element;
|
||||||
|
global.Node = window.Node;
|
||||||
@@ -12,5 +12,7 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(
|
|||||||
dev: this.dev,
|
dev: this.dev,
|
||||||
translateFn: this.translateFn,
|
translateFn: this.translateFn,
|
||||||
translatableAttributes: this.translatableAttributes,
|
translatableAttributes: this.translatableAttributes,
|
||||||
|
customDirectives: this.customDirectives,
|
||||||
|
hasGlobalValues: this.hasGlobalValues,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { version } from "../version";
|
import { version } from "../version";
|
||||||
import { Component, ComponentConstructor, Props } from "./component";
|
import { Component, ComponentConstructor, Props } from "./component";
|
||||||
import { ComponentNode } from "./component_node";
|
import { ComponentNode, saveCurrent } from "./component_node";
|
||||||
import { nodeErrorHandlers, handleError } from "./error_handling";
|
import { nodeErrorHandlers, handleError } from "./error_handling";
|
||||||
import { OwlError } from "../common/owl_error";
|
import { OwlError } from "../common/owl_error";
|
||||||
import { Fiber, RootFiber, MountOptions } from "./fibers";
|
import { Fiber, RootFiber, MountOptions } from "./fibers";
|
||||||
@@ -16,25 +16,19 @@ export interface Env {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfig<P, E> extends TemplateSetConfig {
|
export interface RootConfig<P, E> {
|
||||||
name?: string;
|
|
||||||
props?: P;
|
props?: P;
|
||||||
env?: E;
|
env?: E;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppConfig<P, E> extends TemplateSetConfig, RootConfig<P, E> {
|
||||||
|
name?: string;
|
||||||
test?: boolean;
|
test?: boolean;
|
||||||
warnIfNoStaticProps?: boolean;
|
warnIfNoStaticProps?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasBeenLogged = false;
|
let hasBeenLogged = false;
|
||||||
|
|
||||||
export const DEV_MSG = () => {
|
|
||||||
const hash = (window as any).owl ? (window as any).owl.__info__.hash : "master";
|
|
||||||
|
|
||||||
return `Owl is running in 'dev' mode.
|
|
||||||
|
|
||||||
This is not suitable for production use.
|
|
||||||
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const apps = new Set<App>();
|
const apps = new Set<App>();
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -49,6 +43,12 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Root<P extends Props, E> {
|
||||||
|
node: ComponentNode<P, E>;
|
||||||
|
mount(target: HTMLElement | ShadowRoot, options?: MountOptions): Promise<Component<P, E>>;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
|
||||||
window.__OWL_DEVTOOLS__ ||= { apps, Fiber, RootFiber, toRaw, reactive };
|
window.__OWL_DEVTOOLS__ ||= { apps, Fiber, RootFiber, toRaw, reactive };
|
||||||
|
|
||||||
export class App<
|
export class App<
|
||||||
@@ -65,6 +65,7 @@ export class App<
|
|||||||
props: P;
|
props: P;
|
||||||
env: E;
|
env: E;
|
||||||
scheduler = new Scheduler();
|
scheduler = new Scheduler();
|
||||||
|
subRoots: Set<ComponentNode> = new Set();
|
||||||
root: ComponentNode<P, E> | null = null;
|
root: ComponentNode<P, E> | null = null;
|
||||||
warnIfNoStaticProps: boolean;
|
warnIfNoStaticProps: boolean;
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ export class App<
|
|||||||
}
|
}
|
||||||
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
||||||
if (this.dev && !config.test && !hasBeenLogged) {
|
if (this.dev && !config.test && !hasBeenLogged) {
|
||||||
console.info(DEV_MSG());
|
console.info(`Owl is running in 'dev' mode.`);
|
||||||
hasBeenLogged = true;
|
hasBeenLogged = true;
|
||||||
}
|
}
|
||||||
const env = config.env || {};
|
const env = config.env || {};
|
||||||
@@ -91,14 +92,49 @@ export class App<
|
|||||||
target: HTMLElement | ShadowRoot,
|
target: HTMLElement | ShadowRoot,
|
||||||
options?: MountOptions
|
options?: MountOptions
|
||||||
): Promise<Component<P, E> & InstanceType<T>> {
|
): Promise<Component<P, E> & InstanceType<T>> {
|
||||||
|
const root = this.createRoot(this.Root, { props: this.props });
|
||||||
|
this.root = root.node;
|
||||||
|
this.subRoots.delete(root.node);
|
||||||
|
return root.mount(target, options) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot<Props extends object, SubEnv = any>(
|
||||||
|
Root: ComponentConstructor<Props, E>,
|
||||||
|
config: RootConfig<Props, SubEnv> = {}
|
||||||
|
): Root<Props, SubEnv> {
|
||||||
|
const props = config.props || ({} as Props);
|
||||||
|
// hack to make sure the sub root get the sub env if necessary. for owl 3,
|
||||||
|
// would be nice to rethink the initialization process to make sure that
|
||||||
|
// we can create a ComponentNode and give it explicitely the env, instead
|
||||||
|
// of looking it up in the app
|
||||||
|
const env = this.env;
|
||||||
|
if (config.env) {
|
||||||
|
this.env = config.env as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const restore = saveCurrent();
|
||||||
|
const node = this.makeNode(Root, props);
|
||||||
|
restore();
|
||||||
|
if (config.env) {
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
this.subRoots.add(node);
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
mount: (target: HTMLElement | ShadowRoot, options?: MountOptions) => {
|
||||||
App.validateTarget(target);
|
App.validateTarget(target);
|
||||||
if (this.dev) {
|
if (this.dev) {
|
||||||
validateProps(this.Root, this.props, { __owl__: { app: this } });
|
validateProps(Root, props, { __owl__: { app: this } });
|
||||||
}
|
}
|
||||||
const node = this.makeNode(this.Root, this.props);
|
|
||||||
const prom = this.mountNode(node, target, options);
|
const prom = this.mountNode(node, target, options);
|
||||||
this.root = node;
|
|
||||||
return prom;
|
return prom;
|
||||||
|
},
|
||||||
|
destroy: () => {
|
||||||
|
this.subRoots.delete(node);
|
||||||
|
node.destroy();
|
||||||
|
this.scheduler.processTasks();
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
|
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
|
||||||
@@ -134,6 +170,9 @@ export class App<
|
|||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this.root) {
|
if (this.root) {
|
||||||
|
for (let subroot of this.subRoots) {
|
||||||
|
subroot.destroy();
|
||||||
|
}
|
||||||
this.root.destroy();
|
this.root.destroy();
|
||||||
this.scheduler.processTasks();
|
this.scheduler.processTasks();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export type ComponentConstructor<P extends Props = any, E = any> = (new (
|
|||||||
|
|
||||||
export class Component<Props = any, Env = any> {
|
export class Component<Props = any, Env = any> {
|
||||||
static template: string = "";
|
static template: string = "";
|
||||||
static props?: any;
|
static props?: Schema;
|
||||||
static defaultProps?: any;
|
static defaultProps?: any;
|
||||||
|
|
||||||
props: Props;
|
props: Props;
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ import { batched, Callback } from "./utils";
|
|||||||
|
|
||||||
let currentNode: ComponentNode | null = null;
|
let currentNode: ComponentNode | null = null;
|
||||||
|
|
||||||
|
export function saveCurrent() {
|
||||||
|
let n = currentNode;
|
||||||
|
return () => {
|
||||||
|
currentNode = n;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getCurrent(): ComponentNode {
|
export function getCurrent(): ComponentNode {
|
||||||
if (!currentNode) {
|
if (!currentNode) {
|
||||||
throw new OwlError("No active component (a hook function should only be called in 'setup')");
|
throw new OwlError("No active component (a hook function should only be called in 'setup')");
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ export function makeRootFiber(node: ComponentNode): Fiber {
|
|||||||
fibersInError.delete(current);
|
fibersInError.delete(current);
|
||||||
fibersInError.delete(root);
|
fibersInError.delete(root);
|
||||||
current.appliedToDom = false;
|
current.appliedToDom = false;
|
||||||
|
if (current instanceof RootFiber) {
|
||||||
|
// it is possible that this fiber is a fiber that crashed while being
|
||||||
|
// mounted, so the mounted list is possibly corrupted. We restore it to
|
||||||
|
// its normal initial state (which is empty list or a list with a mount
|
||||||
|
// fiber.
|
||||||
|
current.mounted = current instanceof MountFiber ? [current] : [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
@@ -152,6 +159,7 @@ export class RootFiber extends Fiber {
|
|||||||
const node = this.node;
|
const node = this.node;
|
||||||
this.locked = true;
|
this.locked = true;
|
||||||
let current: Fiber | undefined = undefined;
|
let current: Fiber | undefined = undefined;
|
||||||
|
let mountedFibers = this.mounted;
|
||||||
try {
|
try {
|
||||||
// Step 1: calling all willPatch lifecycle hooks
|
// Step 1: calling all willPatch lifecycle hooks
|
||||||
for (current of this.willPatch) {
|
for (current of this.willPatch) {
|
||||||
@@ -173,7 +181,6 @@ export class RootFiber extends Fiber {
|
|||||||
this.locked = false;
|
this.locked = false;
|
||||||
|
|
||||||
// Step 4: calling all mounted lifecycle hooks
|
// Step 4: calling all mounted lifecycle hooks
|
||||||
let mountedFibers = this.mounted;
|
|
||||||
while ((current = mountedFibers.pop())) {
|
while ((current = mountedFibers.pop())) {
|
||||||
current = current;
|
current = current;
|
||||||
if (current.appliedToDom) {
|
if (current.appliedToDom) {
|
||||||
@@ -194,6 +201,15 @@ export class RootFiber extends Fiber {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// if mountedFibers is not empty, this means that a crash occured while
|
||||||
|
// calling the mounted hooks of some component. So, there may still be
|
||||||
|
// some component that have been mounted, but for which the mounted hooks
|
||||||
|
// have not been called. Here, we remove the willUnmount hooks for these
|
||||||
|
// specific component to prevent a worse situation (willUnmount being
|
||||||
|
// called even though mounted has not been called)
|
||||||
|
for (let fiber of mountedFibers) {
|
||||||
|
fiber.node.willUnmount = [];
|
||||||
|
}
|
||||||
this.locked = false;
|
this.locked = false;
|
||||||
node.app.handleError({ fiber: current || this, error: e });
|
node.app.handleError({ fiber: current || this, error: e });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export { useComponent, useState } from "./component_node";
|
|||||||
export { status } from "./status";
|
export { status } from "./status";
|
||||||
export { reactive, markRaw, toRaw } from "./reactivity";
|
export { reactive, markRaw, toRaw } from "./reactivity";
|
||||||
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
||||||
export { EventBus, whenReady, loadFile, markup } from "./utils";
|
export { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils";
|
||||||
export {
|
export {
|
||||||
onWillStart,
|
onWillStart,
|
||||||
onMounted,
|
onMounted,
|
||||||
|
|||||||
@@ -3,42 +3,50 @@ import { nodeErrorHandlers } from "./error_handling";
|
|||||||
import { OwlError } from "../common/owl_error";
|
import { OwlError } from "../common/owl_error";
|
||||||
|
|
||||||
const TIMEOUT = Symbol("timeout");
|
const TIMEOUT = Symbol("timeout");
|
||||||
|
const HOOK_TIMEOUT: { [key: string]: number } = {
|
||||||
|
onWillStart: 3000,
|
||||||
|
onWillUpdateProps: 3000,
|
||||||
|
};
|
||||||
function wrapError(fn: (...args: any[]) => any, hookName: string) {
|
function wrapError(fn: (...args: any[]) => any, hookName: string) {
|
||||||
const error = new OwlError(`The following error occurred in ${hookName}: `) as Error & {
|
const error = new OwlError() as Error & {
|
||||||
cause: any;
|
cause: any;
|
||||||
};
|
};
|
||||||
const timeoutError = new OwlError(`${hookName}'s promise hasn't resolved after 3 seconds`);
|
const timeoutError = new OwlError();
|
||||||
const node = getCurrent();
|
const node = getCurrent();
|
||||||
return (...args: any[]) => {
|
return (...args: any[]) => {
|
||||||
const onError = (cause: any) => {
|
const onError = (cause: any) => {
|
||||||
error.cause = cause;
|
error.cause = cause;
|
||||||
if (cause instanceof Error) {
|
error.message =
|
||||||
error.message += `"${cause.message}"`;
|
cause instanceof Error
|
||||||
} else {
|
? `The following error occurred in ${hookName}: "${cause.message}"`
|
||||||
error.message = `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
|
: `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
};
|
};
|
||||||
|
let result;
|
||||||
try {
|
try {
|
||||||
const result = fn(...args);
|
result = fn(...args);
|
||||||
if (result instanceof Promise) {
|
} catch (cause) {
|
||||||
if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
|
onError(cause);
|
||||||
|
}
|
||||||
|
if (!(result instanceof Promise)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const timeout = HOOK_TIMEOUT[hookName];
|
||||||
|
if (timeout) {
|
||||||
const fiber = node.fiber;
|
const fiber = node.fiber;
|
||||||
Promise.race([
|
Promise.race([
|
||||||
result.catch(() => {}),
|
result.catch(() => {}),
|
||||||
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
|
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), timeout)),
|
||||||
]).then((res) => {
|
]).then((res) => {
|
||||||
if (res === TIMEOUT && node.fiber === fiber) {
|
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
|
||||||
console.warn(timeoutError);
|
timeoutError.message = `${hookName}'s promise hasn't resolved after ${
|
||||||
|
timeout / 1000
|
||||||
|
} seconds`;
|
||||||
|
console.log(timeoutError);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return result.catch(onError);
|
return result.catch(onError);
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} catch (cause) {
|
|
||||||
onError(cause);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export class Portal extends Component {
|
|||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
slots: true,
|
slots: true,
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const node: any = this.__owl__;
|
const node: any = this.__owl__;
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ type CollectionRawType = "Set" | "Map" | "WeakMap";
|
|||||||
const objectToString = Object.prototype.toString;
|
const objectToString = Object.prototype.toString;
|
||||||
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
|
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
|
||||||
|
|
||||||
const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
|
// Use arrays because Array.includes is faster than Set.has for small arrays
|
||||||
const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
|
const SUPPORTED_RAW_TYPES = ["Object", "Array", "Set", "Map", "WeakMap"];
|
||||||
|
const COLLECTION_RAW_TYPES = ["Set", "Map", "WeakMap"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* extract "RawType" from strings like "[object RawType]" => this lets us ignore
|
* extract "RawType" from strings like "[object RawType]" => this lets us ignore
|
||||||
@@ -45,7 +46,7 @@ function canBeMadeReactive(value: any): boolean {
|
|||||||
if (typeof value !== "object") {
|
if (typeof value !== "object") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return SUPPORTED_RAW_TYPES.has(rawType(value));
|
return SUPPORTED_RAW_TYPES.includes(rawType(value));
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Creates a reactive from the given object/callback if possible and returns it,
|
* Creates a reactive from the given object/callback if possible and returns it,
|
||||||
@@ -220,7 +221,7 @@ export function reactive<T extends Target>(target: T, callback: Callback = NO_CA
|
|||||||
const reactivesForTarget = reactiveCache.get(target)!;
|
const reactivesForTarget = reactiveCache.get(target)!;
|
||||||
if (!reactivesForTarget.has(callback)) {
|
if (!reactivesForTarget.has(callback)) {
|
||||||
const targetRawType = rawType(target);
|
const targetRawType = rawType(target);
|
||||||
const handler = COLLECTION_RAWTYPES.has(targetRawType)
|
const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
|
||||||
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
|
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
|
||||||
: basicProxyHandler<T>(callback);
|
: basicProxyHandler<T>(callback);
|
||||||
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export class Scheduler {
|
|||||||
frame: number = 0;
|
frame: number = 0;
|
||||||
delayedRenders: Fiber[] = [];
|
delayedRenders: Fiber[] = [];
|
||||||
cancelledNodes: Set<ComponentNode> = new Set();
|
cancelledNodes: Set<ComponentNode> = new Set();
|
||||||
|
processing = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
|
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
|
||||||
@@ -53,6 +54,10 @@ export class Scheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
processTasks() {
|
processTasks() {
|
||||||
|
if (this.processing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.processing = true;
|
||||||
this.frame = 0;
|
this.frame = 0;
|
||||||
for (let node of this.cancelledNodes) {
|
for (let node of this.cancelledNodes) {
|
||||||
node._destroy();
|
node._destroy();
|
||||||
@@ -66,6 +71,7 @@ export class Scheduler {
|
|||||||
this.tasks.delete(task);
|
this.tasks.delete(task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.processing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
processFiber(fiber: RootFiber) {
|
processFiber(fiber: RootFiber) {
|
||||||
@@ -87,7 +93,14 @@ export class Scheduler {
|
|||||||
if (!hasError) {
|
if (!hasError) {
|
||||||
fiber.complete();
|
fiber.complete();
|
||||||
}
|
}
|
||||||
|
// at this point, the fiber should have been applied to the DOM, so we can
|
||||||
|
// remove it from the task list. If it is not the case, it means that there
|
||||||
|
// was an error and an error handler triggered a new rendering that recycled
|
||||||
|
// the fiber, so in that case, we actually want to keep the fiber around,
|
||||||
|
// otherwise it will just be ignored.
|
||||||
|
if (fiber.appliedToDom) {
|
||||||
this.tasks.delete(fiber);
|
this.tasks.delete(fiber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,44 +4,19 @@ import { getCurrent } from "./component_node";
|
|||||||
import { Portal, portalTemplate } from "./portal";
|
import { Portal, portalTemplate } from "./portal";
|
||||||
import { helpers } from "./template_helpers";
|
import { helpers } from "./template_helpers";
|
||||||
import { OwlError } from "../common/owl_error";
|
import { OwlError } from "../common/owl_error";
|
||||||
|
import { parseXML } from "../common/utils";
|
||||||
|
import type { customDirectives } from "../common/types";
|
||||||
|
|
||||||
const bdom = { text, createBlock, list, multi, html, toggler, comment };
|
const bdom = { text, createBlock, list, multi, html, toggler, comment };
|
||||||
|
|
||||||
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 OwlError(msg);
|
|
||||||
}
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TemplateSetConfig {
|
export interface TemplateSetConfig {
|
||||||
dev?: boolean;
|
dev?: boolean;
|
||||||
translatableAttributes?: string[];
|
translatableAttributes?: string[];
|
||||||
translateFn?: (s: string) => string;
|
translateFn?: (s: string, translationCtx: string) => string;
|
||||||
templates?: string | Document;
|
templates?: string | Document | Record<string, string>;
|
||||||
|
getTemplate?: (s: string) => Element | Function | string | void;
|
||||||
|
customDirectives?: customDirectives;
|
||||||
|
globalValues?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TemplateSet {
|
export class TemplateSet {
|
||||||
@@ -51,18 +26,32 @@ export class TemplateSet {
|
|||||||
dev: boolean;
|
dev: boolean;
|
||||||
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
|
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
|
||||||
templates: { [name: string]: Template } = {};
|
templates: { [name: string]: Template } = {};
|
||||||
translateFn?: (s: string) => string;
|
getRawTemplate?: (s: string) => Element | Function | string | void;
|
||||||
|
translateFn?: (s: string, translationCtx: string) => string;
|
||||||
translatableAttributes?: string[];
|
translatableAttributes?: string[];
|
||||||
Portal = Portal;
|
Portal = Portal;
|
||||||
|
customDirectives: customDirectives;
|
||||||
|
runtimeUtils: object;
|
||||||
|
hasGlobalValues: boolean;
|
||||||
|
|
||||||
constructor(config: TemplateSetConfig = {}) {
|
constructor(config: TemplateSetConfig = {}) {
|
||||||
this.dev = config.dev || false;
|
this.dev = config.dev || false;
|
||||||
this.translateFn = config.translateFn;
|
this.translateFn = config.translateFn;
|
||||||
this.translatableAttributes = config.translatableAttributes;
|
this.translatableAttributes = config.translatableAttributes;
|
||||||
if (config.templates) {
|
if (config.templates) {
|
||||||
|
if (config.templates instanceof Document || typeof config.templates === "string") {
|
||||||
this.addTemplates(config.templates);
|
this.addTemplates(config.templates);
|
||||||
|
} else {
|
||||||
|
for (const name in config.templates) {
|
||||||
|
this.addTemplate(name, config.templates[name]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
this.getRawTemplate = config.getTemplate;
|
||||||
|
this.customDirectives = config.customDirectives || {};
|
||||||
|
this.runtimeUtils = { ...helpers, __globals__: config.globalValues || {} };
|
||||||
|
this.hasGlobalValues = Boolean(config.globalValues && Object.keys(config.globalValues).length);
|
||||||
|
}
|
||||||
|
|
||||||
addTemplate(name: string, template: string | Element) {
|
addTemplate(name: string, template: string | Element) {
|
||||||
if (name in this.rawTemplates) {
|
if (name in this.rawTemplates) {
|
||||||
@@ -100,7 +89,7 @@ export class TemplateSet {
|
|||||||
|
|
||||||
getTemplate(name: string): Template {
|
getTemplate(name: string): Template {
|
||||||
if (!(name in this.templates)) {
|
if (!(name in this.templates)) {
|
||||||
const rawTemplate = this.rawTemplates[name];
|
const rawTemplate = this.getRawTemplate?.(name) || this.rawTemplates[name];
|
||||||
if (rawTemplate === undefined) {
|
if (rawTemplate === undefined) {
|
||||||
let extraInfo = "";
|
let extraInfo = "";
|
||||||
try {
|
try {
|
||||||
@@ -117,7 +106,7 @@ export class TemplateSet {
|
|||||||
this.templates[name] = function (context, parent) {
|
this.templates[name] = function (context, parent) {
|
||||||
return templates[name].call(this, context, parent);
|
return templates[name].call(this, context, parent);
|
||||||
};
|
};
|
||||||
const template = templateFn(this, bdom, helpers);
|
const template = templateFn(this, bdom, this.runtimeUtils);
|
||||||
this.templates[name] = template;
|
this.templates[name] = template;
|
||||||
}
|
}
|
||||||
return this.templates[name];
|
return this.templates[name];
|
||||||
|
|||||||
@@ -35,13 +35,43 @@ export function inOwnerDocument(el?: HTMLElement) {
|
|||||||
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the given element is contained in a specific root documnet:
|
||||||
|
* either directly or with a shadow root in between or in an iframe.
|
||||||
|
*/
|
||||||
|
function isAttachedToDocument(
|
||||||
|
element: HTMLElement | ShadowRoot,
|
||||||
|
documentElement: Document
|
||||||
|
): boolean {
|
||||||
|
let current: Node = element;
|
||||||
|
const shadowRoot = documentElement.defaultView!.ShadowRoot;
|
||||||
|
while (current) {
|
||||||
|
if (current === documentElement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (current.parentNode) {
|
||||||
|
current = current.parentNode;
|
||||||
|
} else if (current instanceof shadowRoot && current.host) {
|
||||||
|
current = current.host;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function validateTarget(target: HTMLElement | ShadowRoot) {
|
export function validateTarget(target: HTMLElement | ShadowRoot) {
|
||||||
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
||||||
const document = target && target.ownerDocument;
|
const document = target && target.ownerDocument;
|
||||||
if (document) {
|
if (document) {
|
||||||
const HTMLElement = document.defaultView!.HTMLElement;
|
if (!document.defaultView) {
|
||||||
|
throw new OwlError(
|
||||||
|
"Cannot mount a component: the target document is not attached to a window (defaultView is missing)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const HTMLElement = document.defaultView.HTMLElement;
|
||||||
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
||||||
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
|
if (!isAttachedToDocument(target, document)) {
|
||||||
throw new OwlError("Cannot mount a component on a detached dom node");
|
throw new OwlError("Cannot mount a component on a detached dom node");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -81,10 +111,50 @@ export async function loadFile(url: string): Promise<string> {
|
|||||||
*/
|
*/
|
||||||
export class Markup extends String {}
|
export class Markup extends String {}
|
||||||
|
|
||||||
|
export function htmlEscape(str: any): Markup {
|
||||||
|
if (str instanceof Markup) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
if (str === undefined) {
|
||||||
|
return markup("");
|
||||||
|
}
|
||||||
|
if (typeof str === "number") {
|
||||||
|
return markup(String(str));
|
||||||
|
}
|
||||||
|
[
|
||||||
|
["&", "&"],
|
||||||
|
["<", "<"],
|
||||||
|
[">", ">"],
|
||||||
|
["'", "'"],
|
||||||
|
['"', """],
|
||||||
|
["`", "`"],
|
||||||
|
].forEach((pairs) => {
|
||||||
|
str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]);
|
||||||
|
});
|
||||||
|
return markup(str);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Marks a value as safe, that is, a value that can be injected as HTML directly.
|
* 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.
|
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
|
||||||
|
*
|
||||||
|
* If called as a tag function, the interpolated strings are escaped.
|
||||||
*/
|
*/
|
||||||
export function markup(value: any) {
|
export function markup(strings: TemplateStringsArray, ...placeholders: unknown[]): Markup;
|
||||||
return new Markup(value);
|
export function markup(value: string): Markup;
|
||||||
|
export function markup(
|
||||||
|
valueOrStrings: string | TemplateStringsArray,
|
||||||
|
...placeholders: unknown[]
|
||||||
|
): Markup {
|
||||||
|
if (!Array.isArray(valueOrStrings)) {
|
||||||
|
return new Markup(valueOrStrings);
|
||||||
|
}
|
||||||
|
const strings = valueOrStrings;
|
||||||
|
let acc = "";
|
||||||
|
let i = 0;
|
||||||
|
for (; i < placeholders.length; ++i) {
|
||||||
|
acc += strings[i] + htmlEscape(placeholders[i]);
|
||||||
|
}
|
||||||
|
acc += strings[i];
|
||||||
|
return new Markup(acc);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
import { OwlError } from "../common/owl_error";
|
import { OwlError } from "../common/owl_error";
|
||||||
import { toRaw } from "./reactivity";
|
import { toRaw } from "./reactivity";
|
||||||
|
|
||||||
type BaseType =
|
type BaseType = { new (...args: any[]): any } | true | "*";
|
||||||
| typeof String
|
|
||||||
| typeof Boolean
|
|
||||||
| typeof Number
|
|
||||||
| typeof Date
|
|
||||||
| typeof Object
|
|
||||||
| typeof Array
|
|
||||||
| true
|
|
||||||
| "*";
|
|
||||||
|
|
||||||
interface TypeInfo {
|
interface TypeInfo {
|
||||||
type?: TypeDescription;
|
type?: TypeDescription;
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// do not modify manually. This file is generated by the release script.
|
// do not modify manually. This file is generated by the release script.
|
||||||
export const version = "2.2.6";
|
export const version = "2.7.0";
|
||||||
|
|||||||
@@ -43,6 +43,48 @@ exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`app can add functions to the bdom 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { __globals__ } = helpers;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\" block-handler-0=\\"click\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let hdlr1 = [()=>__globals__.plop('click'), ctx];
|
||||||
|
return block1([hdlr1]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`app can call processTask twice in a row without crashing 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const b2 = text(\`parent\`);
|
||||||
|
const b3 = comp1({}, key + \`__1\`, node, this, null);
|
||||||
|
return multi([b2, b3]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`app can call processTask twice in a row without crashing 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`app can configure an app with props 1`] = `
|
exports[`app can configure an app with props 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -57,6 +99,19 @@ exports[`app can configure an app with props 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`app can load templates from an object name-string 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"hello\\">hello</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`app can mount app in an iframe 1`] = `
|
exports[`app can mount app in an iframe 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`destroy a subroot while another component is mounted in main app 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`ChildB\`, true, false, false, []);
|
||||||
|
const comp2 = app.createComponent(\`ChildA\`, true, false, false, []);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let b2, b3;
|
||||||
|
if (ctx['state'].flag) {
|
||||||
|
b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||||
|
} else {
|
||||||
|
b3 = comp2({}, key + \`__2\`, node, this, null);
|
||||||
|
}
|
||||||
|
return multi([b2, b3]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`destroy a subroot while another component is mounted in main app 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block3 = createBlock(\`<div block-ref=\\"0\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const b2 = text(\`a\`);
|
||||||
|
let ref1 = (el) => this.__owl__.setRef((\`elem\`), el);
|
||||||
|
const b3 = block3([ref1]);
|
||||||
|
return multi([b2, b3]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`destroy a subroot while another component is mounted in main app 3`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`c\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`destroy a subroot while another component is mounted in main app 4`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`b\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`subroot by default, env is the same in sub root 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>main app</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`subroot by default, env is the same in sub root 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>sub root</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`subroot can create a root in a setup function, then use a hook 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`a\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`subroot can create a root in a setup function, then use a hook 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`c\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`subroot can mount subroot 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>main app</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`subroot can mount subroot 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>sub root</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`subroot can mount subroot inside own dom 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>main app</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`subroot can mount subroot inside own dom 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>sub root</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`subroot env can be specified for sub roots 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>main app</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`subroot env can be specified for sub roots 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>sub root</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`subroot subcomponents can be destroyed, and it properly cleanup the subroots 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>main app</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`subroot subcomponents can be destroyed, and it properly cleanup the subroots 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>sub root</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { App, Component, mount, onWillStart, useState, xml } from "../../src";
|
import { App, Component, mount, onWillPatch, onWillStart, useState, xml } from "../../src";
|
||||||
import { status } from "../../src/runtime/status";
|
import { status } from "../../src/runtime/status";
|
||||||
import {
|
import {
|
||||||
makeTestFixture,
|
makeTestFixture,
|
||||||
@@ -167,4 +167,56 @@ describe("app", () => {
|
|||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can load templates from an object name-string", async () => {
|
||||||
|
const templates = {
|
||||||
|
hello: `<div class="hello">hello</div>`,
|
||||||
|
world: `<div>world</div>`,
|
||||||
|
};
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = "hello";
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App(SomeComponent, { templates });
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.querySelector(".hello")).toBeDefined();
|
||||||
|
// Only the "hello" template is used, so the "world" template is not yet loaded
|
||||||
|
expect(Object.keys(app.templates)).toEqual(["hello"]);
|
||||||
|
expect(Object.keys(app.rawTemplates)).toEqual(["hello", "world"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can call processTask twice in a row without crashing", async () => {
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`<div/>`;
|
||||||
|
setup() {
|
||||||
|
onWillPatch(() => app.scheduler.processTasks());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`parent<Child/>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("parent<div></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can add functions to the bdom", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div t-on-click="() => __globals__.plop('click')" class="my-div"/>`;
|
||||||
|
}
|
||||||
|
const app = new App(SomeComponent, {
|
||||||
|
globalValues: {
|
||||||
|
plop: (string: any) => {
|
||||||
|
steps.push(string);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe(`<div class="my-div"></div>`);
|
||||||
|
fixture.querySelector("div")!.click();
|
||||||
|
expect(steps).toEqual(["click"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import { App, Component, onMounted, onWillDestroy, useRef, useState, xml } from "../../src";
|
||||||
|
import { status } from "../../src/runtime/status";
|
||||||
|
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
|
||||||
|
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
|
||||||
|
snapshotEverything();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
});
|
||||||
|
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div>main app</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubComponent extends Component {
|
||||||
|
static template = xml`<div>sub root</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("subroot", () => {
|
||||||
|
test("can mount subroot", async () => {
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const comp = await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>main app</div>");
|
||||||
|
const subRoot = app.createRoot(SubComponent);
|
||||||
|
const subcomp = await subRoot.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>main app</div><div>sub root</div>");
|
||||||
|
|
||||||
|
app.destroy();
|
||||||
|
expect(fixture.innerHTML).toBe("");
|
||||||
|
expect(status(comp)).toBe("destroyed");
|
||||||
|
expect(status(subcomp)).toBe("destroyed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can mount subroot inside own dom", async () => {
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const comp = await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>main app</div>");
|
||||||
|
const subRoot = app.createRoot(SubComponent);
|
||||||
|
const subcomp = await subRoot.mount(fixture.querySelector("div")!);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>main app<div>sub root</div></div>");
|
||||||
|
|
||||||
|
app.destroy();
|
||||||
|
expect(fixture.innerHTML).toBe("");
|
||||||
|
expect(status(comp)).toBe("destroyed");
|
||||||
|
expect(status(subcomp)).toBe("destroyed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("by default, env is the same in sub root", async () => {
|
||||||
|
let env, subenv;
|
||||||
|
class SC extends SomeComponent {
|
||||||
|
setup() {
|
||||||
|
env = this.env;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Sub extends SubComponent {
|
||||||
|
setup() {
|
||||||
|
subenv = this.env;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App(SC);
|
||||||
|
await app.mount(fixture);
|
||||||
|
const subRoot = app.createRoot(Sub);
|
||||||
|
await subRoot.mount(fixture);
|
||||||
|
|
||||||
|
expect(env).toBeDefined();
|
||||||
|
expect(subenv).toBeDefined();
|
||||||
|
expect(env).toBe(subenv);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("env can be specified for sub roots", async () => {
|
||||||
|
const env1 = { env1: true };
|
||||||
|
const env2 = {};
|
||||||
|
let someComponentEnv: any, subComponentEnv: any;
|
||||||
|
class SC extends SomeComponent {
|
||||||
|
setup() {
|
||||||
|
someComponentEnv = this.env;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Sub extends SubComponent {
|
||||||
|
setup() {
|
||||||
|
subComponentEnv = this.env;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App(SC, { env: env1 });
|
||||||
|
await app.mount(fixture);
|
||||||
|
const subRoot = app.createRoot(Sub, { env: env2 });
|
||||||
|
await subRoot.mount(fixture);
|
||||||
|
|
||||||
|
// because env is different in app => it is given a sub object, frozen and all
|
||||||
|
// not sure it is a good idea, but it's the way owl 2 works. maybe we should
|
||||||
|
// avoid doing anything with the main env and let user code do it if they
|
||||||
|
// want. in that case, we can change the test here to assert that they are equal
|
||||||
|
expect(someComponentEnv).not.toBe(env1);
|
||||||
|
expect(someComponentEnv!.env1).toBe(true);
|
||||||
|
expect(subComponentEnv).toBe(env2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("subcomponents can be destroyed, and it properly cleanup the subroots", async () => {
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const comp = await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>main app</div>");
|
||||||
|
const root = app.createRoot(SubComponent);
|
||||||
|
const subcomp = await root.mount(fixture.querySelector("div")!);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>main app<div>sub root</div></div>");
|
||||||
|
|
||||||
|
root.destroy();
|
||||||
|
expect(fixture.innerHTML).toBe("<div>main app</div>");
|
||||||
|
expect(status(comp)).not.toBe("destroyed");
|
||||||
|
expect(status(subcomp)).toBe("destroyed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can create a root in a setup function, then use a hook", async () => {
|
||||||
|
class C extends Component {
|
||||||
|
static template = xml`c`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class A extends Component {
|
||||||
|
static template = xml`a`;
|
||||||
|
state: any;
|
||||||
|
setup() {
|
||||||
|
app.createRoot(C);
|
||||||
|
this.state = useState({ value: 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App(A);
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("a");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("destroy a subroot while another component is mounted in main app", async () => {
|
||||||
|
class C extends Component {
|
||||||
|
static template = xml`c`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChildA extends Component {
|
||||||
|
static template = xml`a<div t-ref="elem"></div>`;
|
||||||
|
ref: any;
|
||||||
|
setup() {
|
||||||
|
this.ref = useRef("elem");
|
||||||
|
let root = app.createRoot(C);
|
||||||
|
onMounted(() => {
|
||||||
|
root.mount(this.ref.el);
|
||||||
|
});
|
||||||
|
onWillDestroy(() => {
|
||||||
|
root.destroy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ChildB extends Component {
|
||||||
|
static template = xml`b`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<t t-if="state.flag"><ChildB/></t>
|
||||||
|
<t t-else=""><ChildA/></t>
|
||||||
|
`;
|
||||||
|
static components = { ChildA, ChildB };
|
||||||
|
state = useState({ flag: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const comp = await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("a<div></div>");
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("a<div>c</div>");
|
||||||
|
comp.state.flag = true;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("b");
|
||||||
|
});
|
||||||
@@ -1,5 +1,38 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`comments comment node with backslash at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return comment(\` \\\\\\\\ \`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`comments comment node with backtick at top-level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return comment(\` \\\\\` \`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`comments comment node with interpolation sigil at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return comment(\` \\\\\${very cool} \`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`comments only a comment 1`] = `
|
exports[`comments only a comment 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -341,6 +341,39 @@ exports[`simple templates, mostly static template with t tag with multiple conte
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`simple templates, mostly static text node with backslash at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`\\\\\\\\\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`simple templates, mostly static text node with backtick at top-level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`\\\\\`\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`simple templates, mostly static text node with interpolation sigil at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`\\\\\${very cool}\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
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 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`t-custom can use t-custom directive on a node 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\" block-handler-0=\\"click\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let hdlr1 = [ctx['click'], ctx];
|
||||||
|
return block1([hdlr1]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`t-custom can use t-custom directive with modifiers on a node 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\" block-handler-0=\\"click\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let hdlr1 = [ctx['click'], ctx];
|
||||||
|
return block1([hdlr1]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
@@ -1,5 +1,41 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`t-esc default with backslash at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { withDefault } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(withDefault(undefined, \`\\\\\\\\\`));
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`t-esc default with backtick at top-level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { withDefault } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(withDefault(undefined, \`\\\\\`\`));
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`t-esc default with interpolation sigil at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { withDefault } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(withDefault(undefined, \`\\\\\${very cool}\`));
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`t-esc div with falsy values 1`] = `
|
exports[`t-esc div with falsy values 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,5 +1,50 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`t-set body with backslash at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
ctx = Object.create(ctx);
|
||||||
|
ctx[isBoundary] = 1
|
||||||
|
setContextValue(ctx, \\"value\\", \`\\\\\\\\\`);
|
||||||
|
return text(ctx['value']);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`t-set body with backtick at top-level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
ctx = Object.create(ctx);
|
||||||
|
ctx[isBoundary] = 1
|
||||||
|
setContextValue(ctx, \\"value\\", \`\\\\\`\`);
|
||||||
|
return text(ctx['value']);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`t-set body with interpolation sigil at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
ctx = Object.create(ctx);
|
||||||
|
ctx[isBoundary] = 1
|
||||||
|
setContextValue(ctx, \\"value\\", \`\\\\\${very cool}\`);
|
||||||
|
return text(ctx['value']);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`t-set evaluate value expression 1`] = `
|
exports[`t-set evaluate value expression 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`t-slot compile t-props correctly multiple time 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { callSlot } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return callSlot(ctx, node, key, 'default', false, Object.assign({}, {a:1}));
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
@@ -101,3 +101,55 @@ exports[`loading templates can load a few templates from an XMLDocument 2`] = `
|
|||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`loading templates getTemplate: element returned (2) 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>Hello World!</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`loading templates getTemplate: element returned 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>Hello World!</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`loading templates getTemplate: template string returned 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>Hello World!</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`loading templates getTemplate: undefined returned 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>Hello World!</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|||||||
@@ -1,5 +1,128 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`translation context body of t-sets are translated in context 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
ctx = Object.create(ctx);
|
||||||
|
ctx[isBoundary] = 1
|
||||||
|
setContextValue(ctx, \\"label\\", \`traduit\`);
|
||||||
|
const b2 = text(ctx['label']);
|
||||||
|
return multi([b2]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`translation context default slot params and content translated in context 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { callSlot } = helpers;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
|
function defaultContent1(ctx, node, key = \\"\\") {
|
||||||
|
return text(\` foo \`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const b3 = callSlot(ctx, node, key, 'default', false, {param: \`param\`,title: \`título\`}, defaultContent1.bind(this));
|
||||||
|
return block1([], [b3]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`translation context props with modifier .translate are translated in context 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`ChildComponent\`, true, false, false, []);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return comp1({text: \`jeu\`}, key + \`__1\`, node, this, null);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`translation context props with modifier .translate are translated in context 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let txt1 = ctx['props'].text;
|
||||||
|
return block1([txt1]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`translation context slot attrs and text contents are translated in context 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { capture, markRaw } = helpers;
|
||||||
|
const comp1 = app.createComponent(\`ChildComponent\`, true, true, false, []);
|
||||||
|
|
||||||
|
function slot1(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`jeu\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const ctx1 = capture(ctx);
|
||||||
|
return comp1({slots: markRaw({'a': {__render: slot1.bind(this), __ctx: ctx1, title: \`título\`}})}, key + \`__1\`, node, this, null);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`translation context slot attrs and text contents are translated in context 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { callSlot } = helpers;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const b2 = callSlot(ctx, node, key, 'a', false, {});
|
||||||
|
return block1([], [b2]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`translation context translation of attributes in context 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div title=\\"titre\\" label=\\"game\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`translation context translation of text in context 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block2 = createBlock(\`<div>word</div>\`);
|
||||||
|
let block3 = createBlock(\`<div>mot</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const b2 = block2();
|
||||||
|
const b3 = block3();
|
||||||
|
return multi([b2, b3]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`translation support body of t-sets are translated 1`] = `
|
exports[`translation support body of t-sets are translated 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -26,4 +26,19 @@ describe("comments", () => {
|
|||||||
</div>`;
|
</div>`;
|
||||||
expect(renderToString(template)).toBe("<div><span>true</span></div>");
|
expect(renderToString(template)).toBe("<div><span>true</span></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("comment node with backslash at top level", () => {
|
||||||
|
const template = "<!-- \\ -->";
|
||||||
|
expect(renderToString(template)).toBe("<!-- \\ -->");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("comment node with backtick at top-level", () => {
|
||||||
|
const template = "<!-- ` -->";
|
||||||
|
expect(renderToString(template)).toBe("<!-- ` -->");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("comment node with interpolation sigil at top level", () => {
|
||||||
|
const template = "<!-- ${very cool} -->";
|
||||||
|
expect(renderToString(template)).toBe("<!-- ${very cool} -->");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -174,6 +174,9 @@ describe("expression evaluation", () => {
|
|||||||
expect(compileExpr("list.data.map((data) => data)")).toBe(
|
expect(compileExpr("list.data.map((data) => data)")).toBe(
|
||||||
"ctx['list'].data.map((_data)=>_data)"
|
"ctx['list'].data.map((_data)=>_data)"
|
||||||
);
|
);
|
||||||
|
expect(compileExpr("(ev) => { myFunc(v1, v2, ev.target.value); }")).toBe(
|
||||||
|
"(_ev)=>{ctx['myFunc'](ctx['v1'],ctx['v2'],_ev.target.value);}"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
test.skip("arrow functions: not yet supported", () => {
|
test.skip("arrow functions: not yet supported", () => {
|
||||||
// e is added to localvars in inline_expression but not removed after the arrow func body
|
// e is added to localvars in inline_expression but not removed after the arrow func body
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ describe("qweb parser", () => {
|
|||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
content: [],
|
content: [],
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -70,6 +71,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -84,6 +86,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -98,6 +101,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -109,6 +113,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "span",
|
tag: "span",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -128,6 +133,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -139,6 +145,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "span",
|
tag: "span",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -156,6 +163,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -181,6 +189,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -201,6 +210,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -223,6 +233,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -246,6 +257,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "span",
|
tag: "span",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -262,6 +274,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: { class: "abc" },
|
attrs: { class: "abc" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -280,6 +293,7 @@ describe("qweb parser", () => {
|
|||||||
height: "90px",
|
height: "90px",
|
||||||
width: "100px",
|
width: "100px",
|
||||||
},
|
},
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -290,6 +304,7 @@ describe("qweb parser", () => {
|
|||||||
stroke: "green",
|
stroke: "green",
|
||||||
"stroke-width": "1",
|
"stroke-width": "1",
|
||||||
},
|
},
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -312,6 +327,7 @@ describe("qweb parser", () => {
|
|||||||
parse(`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/></g>`)
|
parse(`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/></g>`)
|
||||||
).toEqual({
|
).toEqual({
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -322,6 +338,7 @@ describe("qweb parser", () => {
|
|||||||
stroke: "green",
|
stroke: "green",
|
||||||
"stroke-width": "1",
|
"stroke-width": "1",
|
||||||
},
|
},
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -348,6 +365,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
content: [
|
content: [
|
||||||
@@ -356,6 +374,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "pre",
|
tag: "pre",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
content: [],
|
content: [],
|
||||||
@@ -391,6 +410,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "span",
|
tag: "span",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -413,6 +433,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -455,6 +476,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -469,6 +491,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -489,6 +512,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -530,6 +554,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -607,6 +632,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -626,6 +652,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "h1",
|
tag: "h1",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -639,6 +666,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "h2",
|
tag: "h2",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -685,6 +713,7 @@ describe("qweb parser", () => {
|
|||||||
{
|
{
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
@@ -705,6 +734,7 @@ describe("qweb parser", () => {
|
|||||||
{
|
{
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
@@ -742,6 +772,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -811,6 +842,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -853,6 +885,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "span",
|
tag: "span",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -887,6 +920,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "span",
|
tag: "span",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -920,6 +954,7 @@ describe("qweb parser", () => {
|
|||||||
"t-att-selected": "category.id==options.active_category_id",
|
"t-att-selected": "category.id==options.active_category_id",
|
||||||
"t-att-value": "category.id",
|
"t-att-value": "category.id",
|
||||||
},
|
},
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -940,6 +975,7 @@ describe("qweb parser", () => {
|
|||||||
).toEqual({
|
).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -987,6 +1023,7 @@ describe("qweb parser", () => {
|
|||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
ns: null,
|
ns: null,
|
||||||
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
|
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
|
||||||
},
|
},
|
||||||
@@ -1010,6 +1047,7 @@ describe("qweb parser", () => {
|
|||||||
name: "Comp",
|
name: "Comp",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
slots: null,
|
slots: null,
|
||||||
on: null,
|
on: null,
|
||||||
},
|
},
|
||||||
@@ -1099,6 +1137,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1139,6 +1178,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "button",
|
tag: "button",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: { click: "add" },
|
on: { click: "add" },
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1175,6 +1215,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "select",
|
tag: "select",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
content: [
|
content: [
|
||||||
@@ -1183,6 +1224,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "option",
|
tag: "option",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: { value: "1" },
|
attrs: { value: "1" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
content: [],
|
content: [],
|
||||||
@@ -1212,6 +1254,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "select",
|
tag: "select",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
content: [
|
content: [
|
||||||
@@ -1220,6 +1263,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "option",
|
tag: "option",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: { "t-att-value": "valueVar" },
|
attrs: { "t-att-value": "valueVar" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
content: [],
|
content: [],
|
||||||
@@ -1251,6 +1295,7 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
slots: null,
|
slots: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
@@ -1263,6 +1308,7 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: { a: "1", b: "'b'" },
|
props: { a: "1", b: "'b'" },
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: null,
|
slots: null,
|
||||||
@@ -1275,6 +1321,7 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: "state",
|
dynamicProps: "state",
|
||||||
props: { a: "1" },
|
props: { a: "1" },
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: null,
|
slots: null,
|
||||||
@@ -1287,6 +1334,7 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: { click: "someMethod" },
|
on: { click: "someMethod" },
|
||||||
slots: null,
|
slots: null,
|
||||||
@@ -1329,12 +1377,14 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
default: {
|
default: {
|
||||||
content: { type: ASTType.Text, value: "foo" },
|
content: { type: ASTType.Text, value: "foo" },
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
},
|
},
|
||||||
@@ -1350,12 +1400,14 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
default: {
|
default: {
|
||||||
content: { type: ASTType.Text, value: "foo" },
|
content: { type: ASTType.Text, value: "foo" },
|
||||||
attrs: { param: "param" },
|
attrs: { param: "param" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
},
|
},
|
||||||
@@ -1370,6 +1422,7 @@ describe("qweb parser", () => {
|
|||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
default: {
|
default: {
|
||||||
@@ -1381,6 +1434,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "span",
|
tag: "span",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1392,6 +1446,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1401,6 +1456,7 @@ describe("qweb parser", () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
},
|
},
|
||||||
@@ -1415,9 +1471,11 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
on: null,
|
on: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
slots: {
|
slots: {
|
||||||
mySlot: {
|
mySlot: {
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: null,
|
content: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
@@ -1434,9 +1492,16 @@ describe("qweb parser", () => {
|
|||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
name: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
|
name: {
|
||||||
|
content: { type: ASTType.Text, value: "foo" },
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
on: null,
|
||||||
|
scope: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1448,11 +1513,13 @@ describe("qweb parser", () => {
|
|||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
name: {
|
name: {
|
||||||
content: { type: ASTType.Text, value: "foo" },
|
content: { type: ASTType.Text, value: "foo" },
|
||||||
attrs: { param: "param" },
|
attrs: { param: "param" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
},
|
},
|
||||||
@@ -1469,12 +1536,14 @@ describe("qweb parser", () => {
|
|||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
name: {
|
name: {
|
||||||
content: { type: ASTType.Text, value: "foo" },
|
content: { type: ASTType.Text, value: "foo" },
|
||||||
on: { click: "doStuff" },
|
on: { click: "doStuff" },
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1493,16 +1562,24 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
default: {
|
default: {
|
||||||
content: { type: ASTType.Text, value: " " },
|
content: { type: ASTType.Text, value: " " },
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
on: null,
|
||||||
|
scope: null,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
content: { type: ASTType.Text, value: "foo" },
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
},
|
},
|
||||||
name: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1518,11 +1595,24 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
a: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
|
a: {
|
||||||
b: { content: { type: ASTType.Text, value: "bar" }, attrs: null, on: null, scope: null },
|
content: { type: ASTType.Text, value: "foo" },
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
on: null,
|
||||||
|
scope: null,
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
content: { type: ASTType.Text, value: "bar" },
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
on: null,
|
||||||
|
scope: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1533,6 +1623,7 @@ describe("qweb parser", () => {
|
|||||||
name: "myComponent",
|
name: "myComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: true,
|
isDynamic: true,
|
||||||
on: null,
|
on: null,
|
||||||
slots: null,
|
slots: null,
|
||||||
@@ -1545,6 +1636,7 @@ describe("qweb parser", () => {
|
|||||||
name: "mycomponent",
|
name: "mycomponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: { a: "1", b: "'b'" },
|
props: { a: "1", b: "'b'" },
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: true,
|
isDynamic: true,
|
||||||
on: null,
|
on: null,
|
||||||
slots: null,
|
slots: null,
|
||||||
@@ -1557,6 +1649,7 @@ describe("qweb parser", () => {
|
|||||||
name: "mycomponent",
|
name: "mycomponent",
|
||||||
dynamicProps: "state",
|
dynamicProps: "state",
|
||||||
props: { a: "1" },
|
props: { a: "1" },
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: true,
|
isDynamic: true,
|
||||||
on: null,
|
on: null,
|
||||||
slots: null,
|
slots: null,
|
||||||
@@ -1587,12 +1680,14 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
default: {
|
default: {
|
||||||
content: { body: null, name: "subTemplate", type: ASTType.TCall, context: null },
|
content: { body: null, name: "subTemplate", type: ASTType.TCall, context: null },
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
on: null,
|
on: null,
|
||||||
},
|
},
|
||||||
@@ -1613,11 +1708,13 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
default: {
|
default: {
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
content: {
|
content: {
|
||||||
@@ -1626,11 +1723,13 @@ describe("qweb parser", () => {
|
|||||||
name: "Child",
|
name: "Child",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
brol: {
|
brol: {
|
||||||
content: { type: ASTType.Text, value: "coucou" },
|
content: { type: ASTType.Text, value: "coucou" },
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
on: null,
|
on: null,
|
||||||
},
|
},
|
||||||
@@ -1654,11 +1753,13 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
default: {
|
default: {
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
content: {
|
content: {
|
||||||
@@ -1667,11 +1768,13 @@ describe("qweb parser", () => {
|
|||||||
name: "Child",
|
name: "Child",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
brol: {
|
brol: {
|
||||||
content: { type: ASTType.Text, value: "coucou" },
|
content: { type: ASTType.Text, value: "coucou" },
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
},
|
},
|
||||||
@@ -1691,6 +1794,7 @@ describe("qweb parser", () => {
|
|||||||
type: ASTType.TSlot,
|
type: ASTType.TSlot,
|
||||||
name: "default",
|
name: "default",
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
defaultContent: null,
|
defaultContent: null,
|
||||||
});
|
});
|
||||||
@@ -1701,6 +1805,7 @@ describe("qweb parser", () => {
|
|||||||
type: ASTType.TSlot,
|
type: ASTType.TSlot,
|
||||||
name: "header",
|
name: "header",
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
defaultContent: { type: ASTType.Text, value: "default content" },
|
defaultContent: { type: ASTType.Text, value: "default content" },
|
||||||
});
|
});
|
||||||
@@ -1711,6 +1816,7 @@ describe("qweb parser", () => {
|
|||||||
type: ASTType.TSlot,
|
type: ASTType.TSlot,
|
||||||
name: "default",
|
name: "default",
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: { "click.prevent": "doSomething" },
|
on: { "click.prevent": "doSomething" },
|
||||||
defaultContent: null,
|
defaultContent: null,
|
||||||
});
|
});
|
||||||
@@ -1728,6 +1834,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1746,6 +1853,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1765,6 +1873,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: "name",
|
ref: "name",
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1779,6 +1888,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: "name",
|
ref: "name",
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1795,6 +1905,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: "name",
|
ref: "name",
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1831,6 +1942,7 @@ describe("qweb parser", () => {
|
|||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: ASTType.Text,
|
type: ASTType.Text,
|
||||||
@@ -1859,6 +1971,92 @@ describe("qweb parser", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// t-translation-context
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('t-translation-context="fr"', async () => {
|
||||||
|
expect(parse(`<t t-translation-context="fr">word</t>`)).toEqual({
|
||||||
|
type: ASTType.TTranslationContext,
|
||||||
|
content: {
|
||||||
|
type: ASTType.Text,
|
||||||
|
value: "word",
|
||||||
|
},
|
||||||
|
translationCtx: "fr",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parse(`<div t-translation-context="fr">word</div>`)).toEqual({
|
||||||
|
content: {
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 0,
|
||||||
|
value: "word",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dynamicTag: null,
|
||||||
|
model: null,
|
||||||
|
ns: null,
|
||||||
|
on: null,
|
||||||
|
ref: null,
|
||||||
|
tag: "div",
|
||||||
|
type: ASTType.DomNode,
|
||||||
|
},
|
||||||
|
translationCtx: "fr",
|
||||||
|
type: ASTType.TTranslationContext,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// t-translation-context-attr
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('t-translation-context="fr" and t-translation-context-title="pt" for a div attr title', async () => {
|
||||||
|
expect(
|
||||||
|
parse(
|
||||||
|
`<div t-translation-context="fr" title="hello" t-translation-context-title="pt">word</div>`
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
content: {
|
||||||
|
attrs: { title: "hello" },
|
||||||
|
attrsTranslationCtx: { title: "pt" },
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 0,
|
||||||
|
value: "word",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dynamicTag: null,
|
||||||
|
model: null,
|
||||||
|
ns: null,
|
||||||
|
on: null,
|
||||||
|
ref: null,
|
||||||
|
tag: "div",
|
||||||
|
type: ASTType.DomNode,
|
||||||
|
},
|
||||||
|
translationCtx: "fr",
|
||||||
|
type: ASTType.TTranslationContext,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('t-translation-context-title="fr" for component prop title', async () => {
|
||||||
|
expect(parse(`<Comp title="hello" t-translation-context-title="fr" />`)).toEqual({
|
||||||
|
dynamicProps: null,
|
||||||
|
isDynamic: false,
|
||||||
|
name: "Comp",
|
||||||
|
on: null,
|
||||||
|
props: {
|
||||||
|
title: "hello",
|
||||||
|
},
|
||||||
|
propsTranslationCtx: {
|
||||||
|
title: "fr",
|
||||||
|
},
|
||||||
|
slots: null,
|
||||||
|
type: ASTType.TComponent,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// t-model
|
// t-model
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1866,6 +2064,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<input t-model="state.stuff" />`)).toEqual({
|
expect(parse(`<input t-model="state.stuff" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
@@ -1886,6 +2085,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<input t-model="state['stuff']" />`)).toEqual({
|
expect(parse(`<input t-model="state['stuff']" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
@@ -1906,6 +2106,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<input t-model.lazy.trim.number="state.stuff" />`)).toEqual({
|
expect(parse(`<input t-model.lazy.trim.number="state.stuff" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
@@ -1927,6 +2128,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<textarea t-model="state.stuff" />`)).toEqual({
|
expect(parse(`<textarea t-model="state.stuff" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
@@ -1947,6 +2149,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<input type="checkbox" t-model="state.stuff" />`)).toEqual({
|
expect(parse(`<input type="checkbox" t-model="state.stuff" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: { type: "checkbox" },
|
attrs: { type: "checkbox" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
@@ -1967,6 +2170,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<input type="radio" t-model="state.stuff" />`)).toEqual({
|
expect(parse(`<input type="radio" t-model="state.stuff" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: { type: "radio" },
|
attrs: { type: "radio" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
@@ -1987,6 +2191,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<input type="radio" t-model.lazy.trim.number="state.stuff" />`)).toEqual({
|
expect(parse(`<input type="radio" t-model.lazy.trim.number="state.stuff" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: { type: "radio" },
|
attrs: { type: "radio" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
@@ -2012,6 +2217,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<div t-tag="theTag" />`)).toEqual({
|
expect(parse(`<div t-tag="theTag" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
|
|||||||
@@ -154,4 +154,19 @@ describe("simple templates, mostly static", () => {
|
|||||||
</div>`;
|
</div>`;
|
||||||
expect(renderToString(template, { a: "a", b: "b", c: "c" })).toBe("<div>abLoadingc</div>");
|
expect(renderToString(template, { a: "a", b: "b", c: "c" })).toBe("<div>abLoadingc</div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("text node with backslash at top level", () => {
|
||||||
|
const template = "\\";
|
||||||
|
expect(renderToString(template)).toBe("\\");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("text node with backtick at top-level", () => {
|
||||||
|
const template = "`";
|
||||||
|
expect(renderToString(template)).toBe("`");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("text node with interpolation sigil at top level", () => {
|
||||||
|
const template = "${very cool}";
|
||||||
|
expect(renderToString(template)).toBe("${very cool}");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { App, Component, xml } from "../../src";
|
||||||
|
import { makeTestFixture, snapshotEverything } from "../helpers";
|
||||||
|
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
|
||||||
|
snapshotEverything();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("t-custom", () => {
|
||||||
|
test("can use t-custom directive on a node", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div t-custom-plop="click" class="my-div"/>`;
|
||||||
|
click() {
|
||||||
|
steps.push("clicked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const app = new App(SomeComponent, {
|
||||||
|
customDirectives: {
|
||||||
|
plop: (node, value) => {
|
||||||
|
node.setAttribute("t-on-click", value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe(`<div class="my-div"></div>`);
|
||||||
|
fixture.querySelector("div")!.click();
|
||||||
|
expect(steps).toEqual(["clicked"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can use t-custom directive with modifiers on a node", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div t-custom-plop.mouse.stop="click" class="my-div"/>`;
|
||||||
|
click() {
|
||||||
|
steps.push("clicked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const app = new App(SomeComponent, {
|
||||||
|
customDirectives: {
|
||||||
|
plop: (node, value, modifiers) => {
|
||||||
|
node.setAttribute("t-on-click", value);
|
||||||
|
for (let mod of modifiers) {
|
||||||
|
steps.push(mod);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe(`<div class="my-div"></div>`);
|
||||||
|
fixture.querySelector("div")!.click();
|
||||||
|
expect(steps).toEqual(["mouse", "stop", "clicked"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -121,4 +121,19 @@ describe("t-esc", () => {
|
|||||||
mount(bdom, fixture);
|
mount(bdom, fixture);
|
||||||
expect(fixture.querySelector("span")!.textContent).toBe("<p>escaped</p>");
|
expect(fixture.querySelector("span")!.textContent).toBe("<p>escaped</p>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("default with backslash at top level", () => {
|
||||||
|
const template = '<t t-esc="undefined">\\</t>';
|
||||||
|
expect(renderToString(template)).toBe("\\");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("default with backtick at top-level", () => {
|
||||||
|
const template = '<t t-esc="undefined">`</t>';
|
||||||
|
expect(renderToString(template)).toBe("`");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("default with interpolation sigil at top level", () => {
|
||||||
|
const template = '<t t-esc="undefined">${very cool}</t>';
|
||||||
|
expect(renderToString(template)).toBe("${very cool}");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,6 +54,21 @@ describe("t-set", () => {
|
|||||||
expect(renderToString(template)).toBe("ok");
|
expect(renderToString(template)).toBe("ok");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("body with backslash at top level", () => {
|
||||||
|
const template = '<t t-set="value">\\</t><t t-esc="value"/>';
|
||||||
|
expect(renderToString(template)).toBe("\\");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("body with backtick at top-level", () => {
|
||||||
|
const template = '<t t-set="value">`</t><t t-esc="value"/>';
|
||||||
|
expect(renderToString(template)).toBe("`");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("body with interpolation sigil at top level", () => {
|
||||||
|
const template = '<t t-set="value">${very cool}</t><t t-esc="value"/>';
|
||||||
|
expect(renderToString(template)).toBe("${very cool}");
|
||||||
|
});
|
||||||
|
|
||||||
test("set from body literal (with t-if/t-else", () => {
|
test("set from body literal (with t-if/t-else", () => {
|
||||||
const template = `
|
const template = `
|
||||||
<t>
|
<t>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { parseXML } from "../../src/common/utils";
|
||||||
|
import { compile } from "../../src/compiler";
|
||||||
|
|
||||||
|
describe("t-slot", () => {
|
||||||
|
test("compile t-props correctly multiple time", () => {
|
||||||
|
const template = `<t t-slot="default" t-props="{ a: 1 }"/>`;
|
||||||
|
const parsedTemplate = parseXML(template).firstChild as Element;
|
||||||
|
|
||||||
|
const fn1 = compile(parsedTemplate);
|
||||||
|
expect(fn1.toString()).toMatchSnapshot();
|
||||||
|
|
||||||
|
const fn2 = compile(parsedTemplate);
|
||||||
|
expect(fn2.toString()).toBe(fn1.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -78,4 +78,62 @@ describe("loading templates", () => {
|
|||||||
context.addTemplates(xml);
|
context.addTemplates(xml);
|
||||||
expect(Object.keys(context.rawTemplates)).toEqual([]);
|
expect(Object.keys(context.rawTemplates)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("getTemplate: element returned", () => {
|
||||||
|
const context = new TestContext({
|
||||||
|
getTemplate: (name) => {
|
||||||
|
if (name === "main") {
|
||||||
|
const data = `<div>Hello World!</div>`;
|
||||||
|
const xml = new DOMParser().parseFromString(data, "text/xml");
|
||||||
|
return xml.firstChild as Element;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = context.renderToString("main");
|
||||||
|
expect(result).toBe("<div>Hello World!</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getTemplate: element returned (2)", () => {
|
||||||
|
const context = new TestContext({
|
||||||
|
getTemplate: (name) => {
|
||||||
|
if (name === "main") {
|
||||||
|
const doc = new Document();
|
||||||
|
const div = doc.createElement("div");
|
||||||
|
div.append(doc.createTextNode("Hello World!"));
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = context.renderToString("main");
|
||||||
|
expect(result).toBe("<div>Hello World!</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getTemplate: template string returned", () => {
|
||||||
|
const context = new TestContext({
|
||||||
|
getTemplate: (name) => {
|
||||||
|
if (name === "main") {
|
||||||
|
return `<div>Hello World!</div>`;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = context.renderToString("main");
|
||||||
|
expect(result).toBe("<div>Hello World!</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getTemplate: undefined returned", () => {
|
||||||
|
const context = new TestContext({
|
||||||
|
getTemplate: () => {},
|
||||||
|
});
|
||||||
|
const data = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
<div t-name="main">Hello World!</div>
|
||||||
|
</templates>`;
|
||||||
|
const xml = new DOMParser().parseFromString(data, "text/xml");
|
||||||
|
context.addTemplates(xml);
|
||||||
|
const result = context.renderToString("main");
|
||||||
|
expect(result).toBe("<div>Hello World!</div>");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ describe("translation support", () => {
|
|||||||
|
|
||||||
await mount(SomeComponent, fixture, { translateFn });
|
await mount(SomeComponent, fixture, { translateFn });
|
||||||
expect(fixture.innerHTML).toBe("<div> mot </div>");
|
expect(fixture.innerHTML).toBe("<div> mot </div>");
|
||||||
expect(translateFn).toHaveBeenCalledWith("word");
|
expect(translateFn).toHaveBeenCalledWith("word", "");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("translation works, even if initial string has inner consecutive white space", async () => {
|
test("translation works, even if initial string has inner consecutive white space", async () => {
|
||||||
@@ -97,7 +97,7 @@ describe("translation support", () => {
|
|||||||
const translateFn = jest.fn((expr: string) => (expr === "some word" ? "un mot" : expr));
|
const translateFn = jest.fn((expr: string) => (expr === "some word" ? "un mot" : expr));
|
||||||
|
|
||||||
await mount(SomeComponent, fixture, { translateFn });
|
await mount(SomeComponent, fixture, { translateFn });
|
||||||
expect(translateFn).toHaveBeenCalledWith("some word");
|
expect(translateFn).toHaveBeenCalledWith("some word", "");
|
||||||
expect(fixture.innerHTML).toBe("<div>un mot</div>");
|
expect(fixture.innerHTML).toBe("<div>un mot</div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -171,3 +171,126 @@ describe("translation support", () => {
|
|||||||
expect(fixture.innerHTML).toBe("translated");
|
expect(fixture.innerHTML).toBe("translated");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("translation context", () => {
|
||||||
|
test("translation of text in context", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>word</div>
|
||||||
|
<div t-translation-context="fr">word</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translateFn = jest.fn((expr: string, translationCtx: string) =>
|
||||||
|
translationCtx === "fr" ? (expr === "word" ? "mot" : expr) : expr
|
||||||
|
);
|
||||||
|
|
||||||
|
await mount(SomeComponent, fixture, { translateFn });
|
||||||
|
expect(fixture.innerHTML).toBe("<div>word</div><div>mot</div>");
|
||||||
|
expect(translateFn).toHaveBeenCalledWith("word", "");
|
||||||
|
expect(translateFn).toHaveBeenCalledWith("word", "fr");
|
||||||
|
});
|
||||||
|
test("translation of attributes in context", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div t-translation-context="en" t-translation-context-title="fr" title="title" label="game"/>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translateFn = jest.fn((expr: string, translationCtx: string) =>
|
||||||
|
translationCtx === "fr" ? (expr === "title" ? "titre" : expr) : expr
|
||||||
|
);
|
||||||
|
|
||||||
|
await mount(SomeComponent, fixture, { translateFn });
|
||||||
|
expect(fixture.innerHTML).toBe(`<div title="titre" label="game"></div>`);
|
||||||
|
expect(translateFn).toHaveBeenCalledWith("title", "fr");
|
||||||
|
expect(translateFn).toHaveBeenCalledWith("game", "en");
|
||||||
|
});
|
||||||
|
test("body of t-sets are translated in context", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<t t-set="label" t-translation-context="fr">untranslated</t>
|
||||||
|
<t t-esc="label"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translateFn = jest.fn((expr: string, translationCtx: string) =>
|
||||||
|
translationCtx === "fr" ? "traduit" : expr
|
||||||
|
);
|
||||||
|
|
||||||
|
await mount(SomeComponent, fixture, { translateFn });
|
||||||
|
expect(fixture.innerHTML).toBe("traduit");
|
||||||
|
expect(translateFn).toHaveBeenCalledWith("untranslated", "fr");
|
||||||
|
});
|
||||||
|
test("props with modifier .translate are translated in context", async () => {
|
||||||
|
class ChildComponent extends Component {
|
||||||
|
static props = ["text"];
|
||||||
|
static template = xml`<span t-esc="props.text"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static components = { ChildComponent };
|
||||||
|
static template = xml`
|
||||||
|
<ChildComponent text.translate="game" t-translation-context-text.translate="fr" />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translateFn = jest.fn((expr: string, translationCtx: string) =>
|
||||||
|
translationCtx === "fr" ? "jeu" : expr
|
||||||
|
);
|
||||||
|
|
||||||
|
await mount(SomeComponent, fixture, { translateFn });
|
||||||
|
expect(fixture.innerHTML).toBe("<span>jeu</span>");
|
||||||
|
expect(translateFn).toHaveBeenCalledWith("game", "fr");
|
||||||
|
});
|
||||||
|
test("slot attrs and text contents are translated in context", async () => {
|
||||||
|
class ChildComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div t-translation-context="ja">
|
||||||
|
<t t-slot="a"/>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static components = { ChildComponent };
|
||||||
|
static template = xml`
|
||||||
|
<ChildComponent t-translation-context="fr">
|
||||||
|
<t t-set-slot="a" title.translate="title" t-translation-context-title.translate="pt">game</t>
|
||||||
|
</ChildComponent>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translateFn = jest.fn((expr: string, translationCtx: string) =>
|
||||||
|
translationCtx === "fr" ? "jeu" : translationCtx === "pt" ? "título" : expr
|
||||||
|
);
|
||||||
|
|
||||||
|
await mount(SomeComponent, fixture, { translateFn });
|
||||||
|
expect(fixture.innerHTML).toBe("<div>jeu</div>");
|
||||||
|
expect(translateFn).toHaveBeenCalledWith("game", "fr");
|
||||||
|
expect(translateFn).toHaveBeenCalledWith("title", "pt");
|
||||||
|
});
|
||||||
|
test("default slot params and content translated in context", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<t
|
||||||
|
t-slot="default"
|
||||||
|
t-translation-context="fr"
|
||||||
|
param.translate="param"
|
||||||
|
title.translate="title"
|
||||||
|
t-translation-context-title.translate="pt"
|
||||||
|
>
|
||||||
|
foo
|
||||||
|
</t>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translateFn = jest.fn((expr: string, translationCtx: string) =>
|
||||||
|
translationCtx === "pt" ? "título" : expr
|
||||||
|
);
|
||||||
|
|
||||||
|
await mount(SomeComponent, fixture, { translateFn });
|
||||||
|
expect(fixture.innerHTML).toBe("<div> foo </div>");
|
||||||
|
expect(translateFn).toHaveBeenCalledWith("foo", "fr");
|
||||||
|
expect(translateFn).toHaveBeenCalledWith("param", "fr");
|
||||||
|
expect(translateFn).toHaveBeenCalledWith("title", "pt");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ describe("basic validation", () => {
|
|||||||
test("compilation error", () => {
|
test("compilation error", () => {
|
||||||
const template = `<div t-att-class="a b">test</div>`;
|
const template = `<div t-att-class="a b">test</div>`;
|
||||||
expect(() => renderToString(template))
|
expect(() => renderToString(template))
|
||||||
.toThrow(`Failed to compile anonymous template: Unexpected identifier
|
.toThrow(`Failed to compile anonymous template: Unexpected identifier 'ctx'
|
||||||
|
|
||||||
generated code:
|
generated code:
|
||||||
function(app, bdom, helpers) {
|
function(app, bdom, helpers) {
|
||||||
|
|||||||
@@ -97,6 +97,19 @@ exports[`basics a component cannot be mounted in a detached node (even if node i
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`basics a component cannot be mounted in a detached node 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`basics a component inside a component 1`] = `
|
exports[`basics a component inside a component 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -261,6 +274,19 @@ exports[`basics can mount a simple component with props 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`basics cannot mount on a documentFragment 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>content</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`basics child can be updated 1`] = `
|
exports[`basics child can be updated 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -1002,6 +1028,19 @@ exports[`basics three level of components with collapsing root nodes 3`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`basics throws if mounting on target=null 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<span>simple vnode</span>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`basics two child components 1`] = `
|
exports[`basics two child components 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -320,6 +320,50 @@ exports[`can catch errors can catch an error in a component render function 3`]
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors can catch an error in onmounted 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(null, false, false, false, []);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let b2, b3;
|
||||||
|
b2 = text(\`Main\`);
|
||||||
|
if (ctx['state'].ok) {
|
||||||
|
const Comp1 = ctx['component'];
|
||||||
|
b3 = toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
|
||||||
|
}
|
||||||
|
return multi([b2, b3]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors can catch an error in onmounted 3`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>Error!!!</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors can catch an error in onmounted 4`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div>perfect</div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`can catch errors can catch an error in the constructor call of a component render function 1`] = `
|
exports[`can catch errors can catch an error in the constructor call of a component render function 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -1147,6 +1191,135 @@ exports[`can catch errors error in mounted on a component with a sibling (proper
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors error in onMounted, graceful recovery 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(null, false, false, false, []);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const Comp1 = ctx['component'];
|
||||||
|
return toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors error in onMounted, graceful recovery 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||||
|
const comp2 = app.createComponent(\`Boom\`, true, false, false, []);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const b2 = text(\`parent\`);
|
||||||
|
const b3 = comp1({}, key + \`__1\`, node, this, null);
|
||||||
|
const b4 = comp2({}, key + \`__2\`, node, this, null);
|
||||||
|
return multi([b2, b3, b4]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors error in onMounted, graceful recovery 3`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`abc\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors error in onMounted, graceful recovery 4`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`boom\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors error in onMounted, graceful recovery 5`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`def\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors error in onMounted, graceful recovery, variation 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(null, false, false, false, []);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let b2, b3;
|
||||||
|
b2 = text(\`R\`);
|
||||||
|
if (ctx['state'].gogogo) {
|
||||||
|
const Comp1 = ctx['component'];
|
||||||
|
b3 = toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
|
||||||
|
}
|
||||||
|
return multi([b2, b3]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors error in onMounted, graceful recovery, variation 3`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||||
|
const comp2 = app.createComponent(\`Boom\`, true, false, false, []);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const b2 = text(\`parent\`);
|
||||||
|
const b3 = comp1({}, key + \`__1\`, node, this, null);
|
||||||
|
const b4 = comp2({}, key + \`__2\`, node, this, null);
|
||||||
|
return multi([b2, b3, b4]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors error in onMounted, graceful recovery, variation 4`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`abc\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors error in onMounted, graceful recovery, variation 5`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`boom\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors error in onMounted, graceful recovery, variation 6`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`def\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`can catch errors onError in class inheritance is called if rethrown 1`] = `
|
exports[`can catch errors onError in class inheritance is called if rethrown 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -683,7 +683,7 @@ exports[`lifecycle hooks sub widget (inside sub node): hooks are correctly calle
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`lifecycle hooks timeout in onWillStart emits a warning 1`] = `
|
exports[`lifecycle hooks timeout in onWillStart doesn't emit a console log if app is destroyed 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
@@ -696,7 +696,20 @@ exports[`lifecycle hooks timeout in onWillStart emits a warning 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 1`] = `
|
exports[`lifecycle hooks timeout in onWillStart emits a console log 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<span/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`lifecycle hooks timeout in onWillUpdateProps emits a console log 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
@@ -710,7 +723,7 @@ exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 2`] = `
|
exports[`lifecycle hooks timeout in onWillUpdateProps emits a console log 2`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|||||||
@@ -66,6 +66,29 @@ exports[`.alike suffix in a simple case 2`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`.translate props are translated 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return comp1({message: \`translated message\`}, key + \`__1\`, node, this, null);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`.translate props are translated 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(ctx['props'].message);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`basics accept ES6-like syntax for props (with getters) 1`] = `
|
exports[`basics accept ES6-like syntax for props (with getters) 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -412,6 +435,29 @@ exports[`can bind function prop with bind suffix 2`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`can use .translate suffix 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return comp1({message: \`some message\`}, key + \`__1\`, node, this, null);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`can use .translate suffix 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(ctx['props'].message);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`do not crash when binding anonymous function prop with bind suffix 1`] = `
|
exports[`do not crash when binding anonymous function prop with bind suffix 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -167,6 +167,45 @@ exports[`props validation can specify that additional props are allowed (object)
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`props validation can use custom class as type 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"customObj\\"]);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const props1 = {customObj: ctx['customObj']};
|
||||||
|
helpers.validateProps(\`Child\`, props1, this);
|
||||||
|
return comp1(props1, key + \`__1\`, node, this, null);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`props validation can use custom class as type 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(ctx['props'].customObj.val);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`props validation can use custom class as type: validation failure 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"customObj\\"]);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const props1 = {customObj: ctx['customObj']};
|
||||||
|
helpers.validateProps(\`Child\`, props1, this);
|
||||||
|
return comp1(props1, key + \`__1\`, node, this, null);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`props validation can validate a prop with multiple types 1`] = `
|
exports[`props validation can validate a prop with multiple types 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -885,6 +924,20 @@ exports[`props validation props: list of strings 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`props validation validate props for root component 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let txt1 = ctx['message'];
|
||||||
|
return block1([txt1]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`props validation validate simple types 1`] = `
|
exports[`props validation validate simple types 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,5 +1,30 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`slots .translate slot props are translated 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { capture, markRaw } = helpers;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, true, false, []);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const ctx1 = capture(ctx);
|
||||||
|
return comp1({slots: markRaw({'default': {message: \`translated message\`}})}, key + \`__1\`, node, this, null);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`slots .translate slot props are translated 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(ctx['props'].slots.default.message);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`slots can define a default content 1`] = `
|
exports[`slots can define a default content 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -201,6 +226,31 @@ exports[`slots can render only empty slot 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`slots can use .translate suffix on slot props 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { capture, markRaw } = helpers;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, true, false, []);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const ctx1 = capture(ctx);
|
||||||
|
return comp1({slots: markRaw({'default': {message: \`some message\`}})}, key + \`__1\`, node, this, null);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`slots can use .translate suffix on slot props 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(ctx['props'].slots.default.message);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`slots can use component in default-content of t-slot 1`] = `
|
exports[`slots can use component in default-content of t-slot 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -1066,6 +1116,45 @@ exports[`slots multiple slots containing components 3`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`slots named slot inside named slot in t-component 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { capture, markRaw } = helpers;
|
||||||
|
const comp1 = app.createComponent(null, false, true, false, []);
|
||||||
|
const comp2 = app.createComponent(\`Child\`, true, true, false, []);
|
||||||
|
|
||||||
|
function slot1(ctx, node, key = \\"\\") {
|
||||||
|
const b2 = text(\` outer \`);
|
||||||
|
const ctx2 = capture(ctx);
|
||||||
|
const Comp1 = ctx['Child'];
|
||||||
|
const b4 = toggler(Comp1, comp1({slots: markRaw({'brol': {__render: slot2.bind(this), __ctx: ctx2}})}, (Comp1).name + key + \`__1\`, node, this, Comp1));
|
||||||
|
return multi([b2, b4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function slot2(ctx, node, key = \\"\\") {
|
||||||
|
return text(ctx['value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const ctx1 = capture(ctx);
|
||||||
|
return comp2({slots: markRaw({'brol': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__2\`, node, this, null);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`slots named slot inside named slot in t-component 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { callSlot } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return callSlot(ctx, node, key, 'brol', false, {});
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`slots named slot inside slot 1`] = `
|
exports[`slots named slot inside slot 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -44,7 +44,26 @@ exports[`t-model directive .trim modifier 1`] = `
|
|||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
let { toNumber } = helpers;
|
let { toNumber } = helpers;
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><input block-property-0=\\"value\\" block-handler-1=\\"input\\"/><span><block-text-2/></span></div>\`);
|
let block1 = createBlock(\`<div><input block-property-0=\\"value\\" block-handler-1=\\"change\\"/><span><block-text-2/></span></div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const bExpr1 = ctx['state'];
|
||||||
|
const expr1 = 'text';
|
||||||
|
let prop1 = bExpr1[expr1];
|
||||||
|
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value.trim(); }];
|
||||||
|
let txt1 = ctx['state'].text;
|
||||||
|
return block1([prop1, hdlr1, txt1]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`t-model directive .trim modifier implies .lazy modifier 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { toNumber } = helpers;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div><input block-property-0=\\"value\\" block-handler-1=\\"change\\"/><span><block-text-2/></span></div>\`);
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
return function template(ctx, node, key = \\"\\") {
|
||||||
const bExpr1 = ctx['state'];
|
const bExpr1 = ctx['state'];
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ describe("basics", () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e as Error;
|
error = e as Error;
|
||||||
}
|
}
|
||||||
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier
|
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier 'ctx'
|
||||||
|
|
||||||
generated code:
|
generated code:
|
||||||
function(app, bdom, helpers) {
|
function(app, bdom, helpers) {
|
||||||
@@ -182,7 +182,7 @@ function(app, bdom, helpers) {
|
|||||||
static components = { Child };
|
static components = { Child };
|
||||||
static template = xml`<Child/>`;
|
static template = xml`<Child/>`;
|
||||||
}
|
}
|
||||||
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier
|
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier 'ctx'
|
||||||
|
|
||||||
generated code:
|
generated code:
|
||||||
function(app, bdom, helpers) {
|
function(app, bdom, helpers) {
|
||||||
@@ -564,6 +564,82 @@ describe("can catch errors", () => {
|
|||||||
expect(mockConsoleWarn).toBeCalledTimes(0);
|
expect(mockConsoleWarn).toBeCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can catch an error in onmounted", async () => {
|
||||||
|
class ErrorComponent extends Component {
|
||||||
|
static template = xml`<div>Error!!!</div>`;
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
onMounted(() => {
|
||||||
|
throw new Error("error");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class PerfectComponent extends Component {
|
||||||
|
static template = xml`<div>perfect</div>`;
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Main extends Component {
|
||||||
|
static template = xml`Main<t t-if="state.ok" t-component="component"/>`;
|
||||||
|
component: any;
|
||||||
|
state: any;
|
||||||
|
setup() {
|
||||||
|
this.state = useState({ ok: false });
|
||||||
|
useLogLifecycle();
|
||||||
|
this.component = ErrorComponent;
|
||||||
|
onError(() => {
|
||||||
|
this.component = PerfectComponent;
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await mount(Main, fixture);
|
||||||
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
"Main:setup",
|
||||||
|
"Main:willStart",
|
||||||
|
"Main:willRender",
|
||||||
|
"Main:rendered",
|
||||||
|
"Main:mounted",
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
expect(fixture.innerHTML).toBe("Main");
|
||||||
|
(app as any).state.ok = true;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("Main<div>Error!!!</div>");
|
||||||
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
"Main:willRender",
|
||||||
|
"ErrorComponent:setup",
|
||||||
|
"ErrorComponent:willStart",
|
||||||
|
"Main:rendered",
|
||||||
|
"ErrorComponent:willRender",
|
||||||
|
"ErrorComponent:rendered",
|
||||||
|
"Main:willPatch",
|
||||||
|
"ErrorComponent:mounted",
|
||||||
|
"Main:willRender",
|
||||||
|
"PerfectComponent:setup",
|
||||||
|
"PerfectComponent:willStart",
|
||||||
|
"Main:rendered",
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
await nextTick();
|
||||||
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
"PerfectComponent:willRender",
|
||||||
|
"PerfectComponent:rendered",
|
||||||
|
"Main:willPatch",
|
||||||
|
"ErrorComponent:willUnmount",
|
||||||
|
"ErrorComponent:willDestroy",
|
||||||
|
"PerfectComponent:mounted",
|
||||||
|
"Main:patched",
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
expect(fixture.innerHTML).toBe("Main<div>perfect</div>");
|
||||||
|
});
|
||||||
|
|
||||||
test("calling a hook outside setup should crash", async () => {
|
test("calling a hook outside setup should crash", async () => {
|
||||||
class Root extends Component {
|
class Root extends Component {
|
||||||
static template = xml`<t t-esc="state.value"/>`;
|
static template = xml`<t t-esc="state.value"/>`;
|
||||||
@@ -1602,4 +1678,198 @@ describe("can catch errors", () => {
|
|||||||
`);
|
`);
|
||||||
expect(fixture.innerHTML).toBe("2");
|
expect(fixture.innerHTML).toBe("2");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("error in onMounted, graceful recovery", async () => {
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`abc`;
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OtherChild extends Component {
|
||||||
|
static template = xml`def`;
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Boom extends Component {
|
||||||
|
static template = xml`boom`;
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
onMounted(() => {
|
||||||
|
throw new Error("boom");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`parent<Child/><Boom/>`;
|
||||||
|
static components = { Child, Boom };
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Root extends Component {
|
||||||
|
static template = xml`<t t-component="component"/>`;
|
||||||
|
|
||||||
|
component: any = Parent;
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
onError(() => {
|
||||||
|
logStep("error");
|
||||||
|
this.component = OtherChild;
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Root, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("def");
|
||||||
|
|
||||||
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
"Root:setup",
|
||||||
|
"Root:willStart",
|
||||||
|
"Root:willRender",
|
||||||
|
"Parent:setup",
|
||||||
|
"Parent:willStart",
|
||||||
|
"Root:rendered",
|
||||||
|
"Parent:willRender",
|
||||||
|
"Child:setup",
|
||||||
|
"Child:willStart",
|
||||||
|
"Boom:setup",
|
||||||
|
"Boom:willStart",
|
||||||
|
"Parent:rendered",
|
||||||
|
"Child:willRender",
|
||||||
|
"Child:rendered",
|
||||||
|
"Boom:willRender",
|
||||||
|
"Boom:rendered",
|
||||||
|
"Boom:mounted",
|
||||||
|
"error",
|
||||||
|
"Root:willRender",
|
||||||
|
"OtherChild:setup",
|
||||||
|
"OtherChild:willStart",
|
||||||
|
"Root:rendered",
|
||||||
|
"OtherChild:willRender",
|
||||||
|
"OtherChild:rendered",
|
||||||
|
"OtherChild:mounted",
|
||||||
|
"Root:mounted",
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("error in onMounted, graceful recovery, variation", async () => {
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`abc`;
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OtherChild extends Component {
|
||||||
|
static template = xml`def`;
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Boom extends Component {
|
||||||
|
static template = xml`boom`;
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
onMounted(() => {
|
||||||
|
throw new Error("boom");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`parent<Child/><Boom/>`;
|
||||||
|
static components = { Child, Boom };
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Root extends Component {
|
||||||
|
static template = xml`R<t t-if="state.gogogo" t-component="component"/>`;
|
||||||
|
|
||||||
|
component: any = Parent;
|
||||||
|
state = useState({ gogogo: false });
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
onError(() => {
|
||||||
|
logStep("error");
|
||||||
|
this.component = OtherChild;
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = await mount(Root, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("R");
|
||||||
|
|
||||||
|
// standard mounting process
|
||||||
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
"Root:setup",
|
||||||
|
"Root:willStart",
|
||||||
|
"Root:willRender",
|
||||||
|
"Root:rendered",
|
||||||
|
"Root:mounted",
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
|
||||||
|
root.state.gogogo = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe("Rparentabcboom");
|
||||||
|
// rerender, root creates sub components, it crashes, tries to recover
|
||||||
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
"Root:willRender",
|
||||||
|
"Parent:setup",
|
||||||
|
"Parent:willStart",
|
||||||
|
"Root:rendered",
|
||||||
|
"Parent:willRender",
|
||||||
|
"Child:setup",
|
||||||
|
"Child:willStart",
|
||||||
|
"Boom:setup",
|
||||||
|
"Boom:willStart",
|
||||||
|
"Parent:rendered",
|
||||||
|
"Child:willRender",
|
||||||
|
"Child:rendered",
|
||||||
|
"Boom:willRender",
|
||||||
|
"Boom:rendered",
|
||||||
|
"Root:willPatch",
|
||||||
|
"Boom:mounted",
|
||||||
|
"error",
|
||||||
|
"Root:willRender",
|
||||||
|
"OtherChild:setup",
|
||||||
|
"OtherChild:willStart",
|
||||||
|
"Root:rendered",
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("Rdef");
|
||||||
|
|
||||||
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
"OtherChild:willRender",
|
||||||
|
"OtherChild:rendered",
|
||||||
|
"Root:willPatch",
|
||||||
|
"Child:willDestroy",
|
||||||
|
"Boom:willUnmount",
|
||||||
|
"Boom:willDestroy",
|
||||||
|
"Parent:willDestroy",
|
||||||
|
"OtherChild:mounted",
|
||||||
|
"Root:patched",
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { App, Component, mount, onMounted, onWillStart, useState, xml } from "../../src";
|
|
||||||
import {
|
import {
|
||||||
|
App,
|
||||||
|
Component,
|
||||||
|
mount,
|
||||||
|
useState,
|
||||||
|
xml,
|
||||||
onWillPatch,
|
onWillPatch,
|
||||||
onWillUnmount,
|
onWillUnmount,
|
||||||
onPatched,
|
onPatched,
|
||||||
@@ -7,7 +11,9 @@ import {
|
|||||||
onWillRender,
|
onWillRender,
|
||||||
onWillDestroy,
|
onWillDestroy,
|
||||||
onRendered,
|
onRendered,
|
||||||
} from "../../src/runtime/lifecycle_hooks";
|
onMounted,
|
||||||
|
onWillStart,
|
||||||
|
} from "../../src";
|
||||||
import { status } from "../../src/runtime/status";
|
import { status } from "../../src/runtime/status";
|
||||||
import {
|
import {
|
||||||
elem,
|
elem,
|
||||||
@@ -106,10 +112,10 @@ describe("lifecycle hooks", () => {
|
|||||||
await mount(Test, fixture);
|
await mount(Test, fixture);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("timeout in onWillStart emits a warning", async () => {
|
test("timeout in onWillStart emits a console log", async () => {
|
||||||
const { warn } = console;
|
const { log } = console;
|
||||||
let warnArgs: any[];
|
let logArgs: any[];
|
||||||
console.warn = jest.fn((...args) => (warnArgs = args));
|
console.log = jest.fn((...args) => (logArgs = args));
|
||||||
const { setTimeout } = window;
|
const { setTimeout } = window;
|
||||||
let timeoutCbs: any = {};
|
let timeoutCbs: any = {};
|
||||||
let timeoutId = 0;
|
let timeoutId = 0;
|
||||||
@@ -117,6 +123,7 @@ describe("lifecycle hooks", () => {
|
|||||||
timeoutCbs[++timeoutId] = cb;
|
timeoutCbs[++timeoutId] = cb;
|
||||||
return timeoutId;
|
return timeoutId;
|
||||||
}) as any;
|
}) as any;
|
||||||
|
try {
|
||||||
class Test extends Component {
|
class Test extends Component {
|
||||||
static template = xml`<span/>`;
|
static template = xml`<span/>`;
|
||||||
setup() {
|
setup() {
|
||||||
@@ -131,13 +138,48 @@ describe("lifecycle hooks", () => {
|
|||||||
}
|
}
|
||||||
await nextMicroTick();
|
await nextMicroTick();
|
||||||
await nextMicroTick();
|
await nextMicroTick();
|
||||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
expect(console.log).toHaveBeenCalledTimes(1);
|
||||||
expect(warnArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
|
expect(logArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
|
||||||
console.warn = warn;
|
} finally {
|
||||||
|
console.log = log;
|
||||||
window.setTimeout = setTimeout;
|
window.setTimeout = setTimeout;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("timeout in onWillUpdateProps emits a warning", async () => {
|
test("timeout in onWillStart doesn't emit a console log if app is destroyed", async () => {
|
||||||
|
const { log } = console;
|
||||||
|
console.log = jest.fn();
|
||||||
|
const { setTimeout } = window;
|
||||||
|
let timeoutCbs: any = {};
|
||||||
|
let timeoutId = 0;
|
||||||
|
window.setTimeout = ((cb: any) => {
|
||||||
|
timeoutCbs[++timeoutId] = cb;
|
||||||
|
return timeoutId;
|
||||||
|
}) as any;
|
||||||
|
try {
|
||||||
|
class Test extends Component {
|
||||||
|
static template = xml`<span/>`;
|
||||||
|
setup() {
|
||||||
|
onWillStart(() => new Promise(() => {}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const app = new App(Test, { test: true });
|
||||||
|
app.mount(fixture);
|
||||||
|
app.destroy();
|
||||||
|
for (const id in timeoutCbs) {
|
||||||
|
timeoutCbs[id]();
|
||||||
|
delete timeoutCbs[id];
|
||||||
|
}
|
||||||
|
await nextMicroTick();
|
||||||
|
await nextMicroTick();
|
||||||
|
expect(console.log).toHaveBeenCalledTimes(0);
|
||||||
|
} finally {
|
||||||
|
console.log = log;
|
||||||
|
window.setTimeout = setTimeout;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("timeout in onWillUpdateProps emits a console log", async () => {
|
||||||
class Child extends Component {
|
class Child extends Component {
|
||||||
static template = xml``;
|
static template = xml``;
|
||||||
setup() {
|
setup() {
|
||||||
@@ -151,9 +193,9 @@ describe("lifecycle hooks", () => {
|
|||||||
}
|
}
|
||||||
const parent = await mount(Parent, fixture, { test: true });
|
const parent = await mount(Parent, fixture, { test: true });
|
||||||
|
|
||||||
const { warn } = console;
|
const { log } = console;
|
||||||
let warnArgs: any[];
|
let logArgs: any[];
|
||||||
console.warn = jest.fn((...args) => (warnArgs = args));
|
console.log = jest.fn((...args) => (logArgs = args));
|
||||||
const { setTimeout } = window;
|
const { setTimeout } = window;
|
||||||
let timeoutCbs: any = {};
|
let timeoutCbs: any = {};
|
||||||
let timeoutId = 0;
|
let timeoutId = 0;
|
||||||
@@ -162,6 +204,7 @@ describe("lifecycle hooks", () => {
|
|||||||
return timeoutId;
|
return timeoutId;
|
||||||
}) as any;
|
}) as any;
|
||||||
|
|
||||||
|
try {
|
||||||
parent.state.prop = 2;
|
parent.state.prop = 2;
|
||||||
let tick = nextTick();
|
let tick = nextTick();
|
||||||
for (const id in timeoutCbs) {
|
for (const id in timeoutCbs) {
|
||||||
@@ -175,12 +218,14 @@ describe("lifecycle hooks", () => {
|
|||||||
delete timeoutCbs[id];
|
delete timeoutCbs[id];
|
||||||
}
|
}
|
||||||
await tick;
|
await tick;
|
||||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
expect(console.log).toHaveBeenCalledTimes(1);
|
||||||
expect(warnArgs![0]!.message).toBe(
|
expect(logArgs![0]!.message).toBe(
|
||||||
"onWillUpdateProps's promise hasn't resolved after 3 seconds"
|
"onWillUpdateProps's promise hasn't resolved after 3 seconds"
|
||||||
);
|
);
|
||||||
console.warn = warn;
|
} finally {
|
||||||
|
console.log = log;
|
||||||
window.setTimeout = setTimeout;
|
window.setTimeout = setTimeout;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("mounted hook is called if mounted in DOM", async () => {
|
test("mounted hook is called if mounted in DOM", async () => {
|
||||||
|
|||||||
@@ -299,6 +299,34 @@ test("bound functions are considered 'alike'", async () => {
|
|||||||
expect(fixture.innerHTML).toBe("3child");
|
expect(fixture.innerHTML).toBe("3child");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can use .translate suffix", async () => {
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`<t t-esc="props.message"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`<Child message.translate="some message"/>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Parent, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("some message");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(".translate props are translated", async () => {
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`<t t-esc="props.message"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`<Child message.translate="some message"/>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Parent, fixture, { translateFn: () => "translated message" });
|
||||||
|
expect(fixture.innerHTML).toBe("translated message");
|
||||||
|
});
|
||||||
|
|
||||||
test("throw if prop uses an unknown suffix", async () => {
|
test("throw if prop uses an unknown suffix", async () => {
|
||||||
class Child extends Component {
|
class Child extends Component {
|
||||||
static template = xml`<t t-esc="props.val"/>`;
|
static template = xml`<t t-esc="props.val"/>`;
|
||||||
@@ -311,7 +339,7 @@ test("throw if prop uses an unknown suffix", async () => {
|
|||||||
|
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await mount(Parent, fixture);
|
await mount(Parent, fixture);
|
||||||
}).rejects.toThrowError("Invalid prop suffix");
|
}).rejects.toThrowError("Invalid prop suffix: somesuffix");
|
||||||
});
|
});
|
||||||
|
|
||||||
test(".alike suffix in a simple case", async () => {
|
test(".alike suffix in a simple case", async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
|
import { makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
|
||||||
import { Component, onError, xml, mount, OwlError, useState } from "../../src";
|
import { Component, onError, xml, mount, OwlError, useState } from "../../src";
|
||||||
import { App, DEV_MSG } from "../../src/runtime/app";
|
import { App } from "../../src/runtime/app";
|
||||||
import { validateProps } from "../../src/runtime/template_helpers";
|
import { validateProps } from "../../src/runtime/template_helpers";
|
||||||
import { Schema } from "../../src/runtime/validation";
|
import { Schema } from "../../src/runtime/validation";
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ let mockConsoleWarn: any;
|
|||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
console.info = (message: any) => {
|
console.info = (message: any) => {
|
||||||
if (message === DEV_MSG()) {
|
if (message === `Owl is running in 'dev' mode.`) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
info(message);
|
info(message);
|
||||||
@@ -594,7 +594,7 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
test("props: can be defined with a boolean", async () => {
|
test("props: can be defined with a boolean", async () => {
|
||||||
class SubComp extends Component {
|
class SubComp extends Component {
|
||||||
static props = { message: true };
|
static props = { message: true } as const;
|
||||||
}
|
}
|
||||||
expect(() => {
|
expect(() => {
|
||||||
validateProps(SubComp as any, {});
|
validateProps(SubComp as any, {});
|
||||||
@@ -636,7 +636,7 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
test("props: extra props cause an error, part 2", async () => {
|
test("props: extra props cause an error, part 2", async () => {
|
||||||
class SubComp extends Component {
|
class SubComp extends Component {
|
||||||
static props = { message: true };
|
static props = { message: true } as const;
|
||||||
}
|
}
|
||||||
expect(() => {
|
expect(() => {
|
||||||
validateProps(SubComp as any, { message: 1, flag: true });
|
validateProps(SubComp as any, { message: 1, flag: true });
|
||||||
@@ -829,6 +829,52 @@ describe("props validation", () => {
|
|||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe("Invalid props for component 'Child': 'message' is missing");
|
expect(error!.message).toBe("Invalid props for component 'Child': 'message' is missing");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can use custom class as type", async () => {
|
||||||
|
class CustomClass {
|
||||||
|
val = "hey";
|
||||||
|
}
|
||||||
|
class Child extends Component {
|
||||||
|
static props = { customObj: CustomClass };
|
||||||
|
static template = xml`<t t-esc="props.customObj.val"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static components = { Child };
|
||||||
|
static template = xml`<Child customObj="customObj" />`;
|
||||||
|
customObj = new CustomClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App(Parent, { test: true });
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("hey");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can use custom class as type: validation failure", async () => {
|
||||||
|
class CustomClass {}
|
||||||
|
class Child extends Component {
|
||||||
|
static props = { customObj: CustomClass };
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static components = { Child };
|
||||||
|
static template = xml`<Child customObj="customObj" />`;
|
||||||
|
customObj = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App(Parent, { test: true });
|
||||||
|
let error: OwlError | undefined;
|
||||||
|
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
|
||||||
|
await expect(nextAppError(app)).resolves.toThrow(
|
||||||
|
"Invalid props for component 'Child': 'customObj' is not a customclass"
|
||||||
|
);
|
||||||
|
await mountProm;
|
||||||
|
expect(error!).toBeDefined();
|
||||||
|
expect(error!.message).toBe(
|
||||||
|
"Invalid props for component 'Child': 'customObj' is not a customclass"
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -179,6 +179,34 @@ describe("slots", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<span>default empty</span>");
|
expect(fixture.innerHTML).toBe("<span>default empty</span>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can use .translate suffix on slot props", async () => {
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`<t t-esc="props.slots.default.message"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`<Child><t t-set-slot="default" message.translate="some message"/></Child>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Parent, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("some message");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(".translate slot props are translated", async () => {
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`<t t-esc="props.slots.default.message"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`<Child><t t-set-slot="default" message.translate="some message"/></Child>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Parent, fixture, { translateFn: () => "translated message" });
|
||||||
|
expect(fixture.innerHTML).toBe("translated message");
|
||||||
|
});
|
||||||
|
|
||||||
test("default slot with slot scope: shorthand syntax", async () => {
|
test("default slot with slot scope: shorthand syntax", async () => {
|
||||||
let child: any;
|
let child: any;
|
||||||
class Child extends Component {
|
class Child extends Component {
|
||||||
@@ -1673,6 +1701,31 @@ describe("slots", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<div><div><p>Ablip</p><div><p>Bblip</p></div></div></div>");
|
expect(fixture.innerHTML).toBe("<div><div><p>Ablip</p><div><p>Bblip</p></div></div></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("named slot inside named slot in t-component", async () => {
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`<t t-slot="brol"/>`;
|
||||||
|
}
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<Child>
|
||||||
|
<t t-set-slot="brol">
|
||||||
|
outer
|
||||||
|
<t t-component="Child">
|
||||||
|
<t t-set-slot="brol">
|
||||||
|
<t t-esc="value"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</Child>`;
|
||||||
|
static components = { Child };
|
||||||
|
Child = Child;
|
||||||
|
value = "inner";
|
||||||
|
}
|
||||||
|
await mount(Parent, fixture);
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe(" outer inner");
|
||||||
|
});
|
||||||
|
|
||||||
test("can render only empty slot", async () => {
|
test("can render only empty slot", async () => {
|
||||||
class Parent extends Component {
|
class Parent extends Component {
|
||||||
static template = xml`<t t-slot="default"/>`;
|
static template = xml`<t t-slot="default"/>`;
|
||||||
|
|||||||
@@ -331,6 +331,32 @@ describe("t-model directive", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
|
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(".trim modifier implies .lazy modifier", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<input t-model.trim="state.text"/>
|
||||||
|
<span><t t-esc="state.text"/></span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
state = useState({ text: "" });
|
||||||
|
}
|
||||||
|
const comp = await mount(SomeComponent, fixture);
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
|
||||||
|
|
||||||
|
const input = fixture.querySelector("input")!;
|
||||||
|
input.value = "test ";
|
||||||
|
input.dispatchEvent(new Event("input"));
|
||||||
|
await nextTick();
|
||||||
|
expect(comp.state.text).toBe("");
|
||||||
|
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
|
||||||
|
input.dispatchEvent(new Event("change"));
|
||||||
|
await nextTick();
|
||||||
|
expect(comp.state.text).toBe("test");
|
||||||
|
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
|
||||||
|
});
|
||||||
|
|
||||||
test(".number modifier", async () => {
|
test(".number modifier", async () => {
|
||||||
class SomeComponent extends Component {
|
class SomeComponent extends Component {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "../../src";
|
} from "../../src";
|
||||||
import { xml } from "../../src/";
|
import { xml } from "../../src/";
|
||||||
import { DEV_MSG } from "../../src/runtime/app";
|
|
||||||
import { elem, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
|
import { elem, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
|
||||||
|
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
@@ -30,7 +29,7 @@ snapshotEverything();
|
|||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
console.info = (message: any) => {
|
console.info = (message: any) => {
|
||||||
if (message === DEV_MSG()) {
|
if (message === `Owl is running in 'dev' mode.`) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
info(message);
|
info(message);
|
||||||
|
|||||||
@@ -27,6 +27,58 @@ exports[`shadow_dom can mount app 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app in closed shadow dom 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app inside a separate HTML document 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app inside a shadow child element 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app inside an element in a shadow root inside an iframe 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`shadow_dom useRef hook 1`] = `
|
exports[`shadow_dom useRef hook 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -29,6 +29,24 @@ describe("shadow_dom", () => {
|
|||||||
expect(status(comp)).toBe("destroyed");
|
expect(status(comp)).toBe("destroyed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can mount app in closed shadow dom", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div class="my-div"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement("div");
|
||||||
|
fixture.appendChild(container);
|
||||||
|
const shadow = container.attachShadow({ mode: "closed" });
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const comp = await app.mount(shadow);
|
||||||
|
const div = shadow.querySelector(".my-div");
|
||||||
|
expect(div).not.toBe(null);
|
||||||
|
expect(shadow.contains(div)).toBe(true);
|
||||||
|
app.destroy();
|
||||||
|
expect(shadow.contains(div)).toBe(false);
|
||||||
|
expect(status(comp)).toBe("destroyed");
|
||||||
|
});
|
||||||
|
|
||||||
test("can bind event handler", async () => {
|
test("can bind event handler", async () => {
|
||||||
let a = 1;
|
let a = 1;
|
||||||
class SomeComponent extends Component {
|
class SomeComponent extends Component {
|
||||||
@@ -64,4 +82,73 @@ describe("shadow_dom", () => {
|
|||||||
await mountedProm;
|
await mountedProm;
|
||||||
expect(comp!.div.el).toBe(shadow.querySelector(".my-div"));
|
expect(comp!.div.el).toBe(shadow.querySelector(".my-div"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can mount app inside a shadow child element", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div class="my-div"/>`;
|
||||||
|
}
|
||||||
|
const shadow = fixture.attachShadow({ mode: "open" });
|
||||||
|
const shadowDiv = document.createElement("div");
|
||||||
|
shadow.append(shadowDiv);
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const comp = await app.mount(shadowDiv);
|
||||||
|
const div = shadow.querySelector(".my-div");
|
||||||
|
expect(div).not.toBe(null);
|
||||||
|
expect(shadow.contains(div)).toBe(true);
|
||||||
|
app.destroy();
|
||||||
|
expect(shadow.contains(div)).toBe(false);
|
||||||
|
expect(status(comp)).toBe("destroyed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can mount app inside a separate HTML document", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div class="my-div"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const separateDoc = document.implementation.createHTMLDocument();
|
||||||
|
const container = separateDoc.createElement("div");
|
||||||
|
separateDoc.body.appendChild(container);
|
||||||
|
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
let error: Error;
|
||||||
|
try {
|
||||||
|
await app.mount(container);
|
||||||
|
} catch (e) {
|
||||||
|
error = e as Error;
|
||||||
|
}
|
||||||
|
expect(error!).toBeDefined();
|
||||||
|
expect(error!.message).toBe(
|
||||||
|
"Cannot mount a component: the target document is not attached to a window (defaultView is missing)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can mount app inside an element in a shadow root inside an iframe", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div class="my-div"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iframe = document.createElement("iframe");
|
||||||
|
fixture.appendChild(iframe);
|
||||||
|
|
||||||
|
const iframeDoc = iframe.contentDocument!;
|
||||||
|
const container = iframeDoc.createElement("div");
|
||||||
|
iframeDoc.body.appendChild(container);
|
||||||
|
|
||||||
|
const shadow = container.attachShadow({ mode: "open" });
|
||||||
|
|
||||||
|
const shadowTarget = iframeDoc.createElement("div");
|
||||||
|
shadow.appendChild(shadowTarget);
|
||||||
|
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const comp = await app.mount(shadowTarget);
|
||||||
|
|
||||||
|
const div = shadow.querySelector(".my-div");
|
||||||
|
expect(div).not.toBe(null);
|
||||||
|
expect(shadow.contains(div)).toBe(true);
|
||||||
|
expect(iframeDoc.body.contains(container)).toBe(true);
|
||||||
|
|
||||||
|
app.destroy();
|
||||||
|
expect(shadow.contains(div)).toBe(false);
|
||||||
|
expect(status(comp)).toBe("destroyed");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { batched, EventBus } from "../src/runtime/utils";
|
import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils";
|
||||||
import { nextMicroTick } from "./helpers";
|
import { nextMicroTick } from "./helpers";
|
||||||
|
|
||||||
describe("event bus behaviour", () => {
|
describe("event bus behaviour", () => {
|
||||||
@@ -71,3 +71,94 @@ describe("batched", () => {
|
|||||||
expect(n).toBe(2);
|
expect(n).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const Markup = markup("").constructor;
|
||||||
|
describe("markup", () => {
|
||||||
|
test("string is flagged as safe", () => {
|
||||||
|
const html = markup("<blink>Hello</blink>");
|
||||||
|
expect(html).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
describe("htmlEscape", () => {
|
||||||
|
test("htmlEscape escapes text", () => {
|
||||||
|
const res = htmlEscape("<p>test</p>");
|
||||||
|
expect(res.toString()).toBe("<p>test</p>");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape keeps html markup", () => {
|
||||||
|
const res = htmlEscape(markup("<p>test</p>"));
|
||||||
|
expect(res.toString()).toBe("<p>test</p>");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape produces empty string on undefined", () => {
|
||||||
|
const res = htmlEscape(undefined);
|
||||||
|
expect(res.toString()).toBe("");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape produces string from number", () => {
|
||||||
|
const res = htmlEscape(10);
|
||||||
|
expect(res.toString()).toBe("10");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape produces string from boolean", () => {
|
||||||
|
const res = htmlEscape(false);
|
||||||
|
expect(res.toString()).toBe("false");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape correctly escapes various links", () => {
|
||||||
|
expect(htmlEscape("<a>this is a link</a>").toString()).toBe(
|
||||||
|
"<a>this is a link</a>"
|
||||||
|
);
|
||||||
|
expect(htmlEscape(`<a href="https://www.odoo.com">odoo<a>`).toString()).toBe(
|
||||||
|
`<a href="https://www.odoo.com">odoo<a>`
|
||||||
|
);
|
||||||
|
expect(htmlEscape(`<a href='https://www.odoo.com'>odoo<a>`).toString()).toBe(
|
||||||
|
`<a href='https://www.odoo.com'>odoo<a>`
|
||||||
|
);
|
||||||
|
expect(htmlEscape("<a href='https://www.odoo.com'>Odoo`s website<a>").toString()).toBe(
|
||||||
|
`<a href='https://www.odoo.com'>Odoo`s website<a>`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test("htmlEscape doesn't escape already escaped content", () => {
|
||||||
|
const res = htmlEscape("<p>test</p>");
|
||||||
|
expect(res.toString()).toBe("<p>test</p>");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
const res2 = htmlEscape(res);
|
||||||
|
expect(res2.toString()).toBe("<p>test</p>");
|
||||||
|
expect(res2).toBeInstanceOf(Markup);
|
||||||
|
expect(res2).toBe(res);
|
||||||
|
});
|
||||||
|
test("htmlEscape returns markup even for only-safe text", () => {
|
||||||
|
const res = htmlEscape("safe");
|
||||||
|
expect(res.toString()).toBe("safe");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("tag function", () => {
|
||||||
|
test("interpolated values are escaped", () => {
|
||||||
|
const maliciousInput = "<script>alert('💥💥')</script>";
|
||||||
|
const html = markup`<b>${maliciousInput}</b>`;
|
||||||
|
expect(html.toString()).toBe("<b><script>alert('💥💥')</script></b>");
|
||||||
|
expect(html).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("interpolated markups aren't escaped", () => {
|
||||||
|
const shouldBeEscaped = "<script>alert('should be escaped')</script>";
|
||||||
|
const shouldnt = markup("<b>this is safe</b>");
|
||||||
|
const html = markup`<div>${shouldBeEscaped} ${shouldnt}</div>`;
|
||||||
|
expect(html.toString()).toBe(
|
||||||
|
"<div><script>alert('should be escaped')</script> <b>this is safe</b></div>"
|
||||||
|
);
|
||||||
|
expect(html).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("quotes in interpolated values are escaped", () => {
|
||||||
|
const imgUrl = `lol" onerror="alert('xss')`;
|
||||||
|
const html = markup`<img src="${imgUrl}">`;
|
||||||
|
expect(html.toString()).toBe(`<img src="lol" onerror="alert('xss')">`);
|
||||||
|
});
|
||||||
|
test("already escaped content is not escaped again", () => {
|
||||||
|
const res = htmlEscape("<p>test</p>");
|
||||||
|
expect(res.toString()).toBe("<p>test</p>");
|
||||||
|
const html = markup`${res}`;
|
||||||
|
expect(html.toString()).toBe("<p>test</p>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// this is the "compile_owl_templates" command that owl makes available when
|
||||||
|
// installed as a node_module.
|
||||||
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||||
|
import { dirname } from "path";
|
||||||
|
import { compileTemplates } from "../dist/compile_templates.mjs";
|
||||||
|
import { parseArgs } from "util";
|
||||||
|
|
||||||
|
const { values, positionals } = parseArgs({
|
||||||
|
allowPositionals: true,
|
||||||
|
options: {
|
||||||
|
output: {
|
||||||
|
type: "string",
|
||||||
|
short: "o",
|
||||||
|
default: "templates.js",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (positionals.length) {
|
||||||
|
const result = await compileTemplates(positionals);
|
||||||
|
const outputPath = values.output;
|
||||||
|
const dir = dirname(outputPath);
|
||||||
|
if (!existsSync(dir)) {
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(outputPath, result);
|
||||||
|
} else {
|
||||||
|
console.log("Please provide a path");
|
||||||
|
}
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const jsdom = require("jsdom");
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// add global DOM stuff for compiler
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
var document = new jsdom.JSDOM("", {});
|
|
||||||
var window = document.window;
|
|
||||||
global.document = window.document;
|
|
||||||
global.window = window;
|
|
||||||
global.DOMParser = window.DOMParser;
|
|
||||||
global.Element = window.Element;
|
|
||||||
global.Node = window.Node;
|
|
||||||
// this needs to be below the jsdom stuff
|
|
||||||
const { compile } = require("../dist/compiler.js");
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// helpers
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
async function getXmlFiles(dir) {
|
|
||||||
let xmls = [];
|
|
||||||
const files = await fs.promises.readdir(dir);
|
|
||||||
const filesStats = await Promise.all(files.map((file) => fs.promises.stat(path.join(dir, file))));
|
|
||||||
for (let i in files) {
|
|
||||||
const name = path.join(dir, files[i]);
|
|
||||||
if (filesStats[i].isDirectory()) {
|
|
||||||
xmls = xmls.concat(await getXmlFiles(name));
|
|
||||||
} else {
|
|
||||||
if (name.endsWith(".xml")) {
|
|
||||||
xmls.push(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return xmls;
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeToFile(filepath, data) {
|
|
||||||
if (!fs.existsSync(path.dirname(filepath))) {
|
|
||||||
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
||||||
}
|
|
||||||
fs.writeFile(filepath, data, (err) => {
|
|
||||||
if (err) {
|
|
||||||
process.stdout.write(`Error while writing file ${filepath}: ${err}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
|
|
||||||
const a = "·-_,:;";
|
|
||||||
const p = new RegExp(a.split("").join("|"), "g");
|
|
||||||
|
|
||||||
function slugify(str) {
|
|
||||||
return str
|
|
||||||
.replace(/\//g, "") // remove /
|
|
||||||
.replace(/\./g, "_") // Replace . with _
|
|
||||||
.replace(p, (c) => '_') // Replace special characters
|
|
||||||
.replace(/&/g, "_and_") // Replace & with ‘and’
|
|
||||||
.replace(/[^\w\-]+/g, "") // Remove all non-word characters
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// main
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async function compileTemplates(files) {
|
|
||||||
process.stdout.write(`Processing ${files.length} files`);
|
|
||||||
let xmlStrings = await Promise.all(files.map((file) => fs.promises.readFile(file, "utf8")));
|
|
||||||
|
|
||||||
const templates = [];
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
const fileName = files[i];
|
|
||||||
const fileContent = xmlStrings[i];
|
|
||||||
process.stdout.write(`.`);
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(fileContent, "text/xml");
|
|
||||||
for (const template of doc.querySelectorAll("[t-name]")) {
|
|
||||||
const name = template.getAttribute("t-name");
|
|
||||||
if (template.hasAttribute("owl")) {
|
|
||||||
template.removeAttribute("owl")
|
|
||||||
}
|
|
||||||
const fnName = slugify(name);
|
|
||||||
try {
|
|
||||||
const fn = compile(template).toString().replace('anonymous', fnName);
|
|
||||||
templates.push(`"${name}": ${fn},\n`);
|
|
||||||
} catch (e) {
|
|
||||||
errors.push({ name, fileName, e });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
process.stdout.write(`\n`);
|
|
||||||
|
|
||||||
for (let { name, fileName, e } of errors) {
|
|
||||||
console.warn(`Error while compiling '${name}' (in file ${fileName})`);
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
console.log(`${templates.length} templates compiled`);
|
|
||||||
|
|
||||||
return `export const templates = {\n ${templates.join("\n")} \n}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const templatesPath = process.argv[2];
|
|
||||||
if (templatesPath && templatesPath.length) {
|
|
||||||
getXmlFiles(templatesPath).then(async (files) => {
|
|
||||||
const result = await compileTemplates(files);
|
|
||||||
writeToFile("templates.js", result);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log("Please provide a path");
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Owl devtools",
|
"name": "Owl devtools",
|
||||||
"version": "1.2.2",
|
"version": "1.3.0",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"description": "Chrome devtools extension for Odoo Owl framework",
|
"description": "Chrome devtools extension for Odoo Owl framework",
|
||||||
"icons": {
|
"icons": {
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"default_popup": "popup_app/popup.html"
|
"default_popup": "popup_app/popup.html"
|
||||||
},
|
},
|
||||||
"permissions": ["scripting", "storage"],
|
"permissions": ["scripting", "storage"],
|
||||||
"host_permissions": ["http://*/*", "https://*/*"],
|
"host_permissions": ["http://*/*", "https://*/*", "file://*"],
|
||||||
"content_security_policy": {
|
"content_security_policy": {
|
||||||
"script-src": "self",
|
"script-src": "self",
|
||||||
"object-src": "self"
|
"object-src": "self"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Owl devtools",
|
"name": "Owl devtools",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "Firefox devtools extension for Odoo Owl framework",
|
"description": "Firefox devtools extension for Odoo Owl framework",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useStore } from "../store/store";
|
||||||
|
|
||||||
|
const { Component, useEffect, useRef } = owl;
|
||||||
|
|
||||||
|
export class ContextMenu extends Component {
|
||||||
|
static template = "devtools.ContextMenu";
|
||||||
|
static props = {
|
||||||
|
items: Array,
|
||||||
|
};
|
||||||
|
setup() {
|
||||||
|
this.store = useStore();
|
||||||
|
this.contextMenu = useRef("contextmenu");
|
||||||
|
useEffect(
|
||||||
|
(position) => {
|
||||||
|
const menu = this.contextMenu.el;
|
||||||
|
const menuWidth = menu.offsetWidth;
|
||||||
|
const menuHeight = menu.offsetHeight;
|
||||||
|
let { x, y } = position;
|
||||||
|
if (x + menuWidth > window.innerWidth) {
|
||||||
|
x = window.innerWidth - menuWidth;
|
||||||
|
}
|
||||||
|
if (y + menuHeight > window.innerHeight) {
|
||||||
|
y = window.innerHeight - menuHeight;
|
||||||
|
}
|
||||||
|
menu.style.left = x + "px";
|
||||||
|
// Need 25px offset because of the main navbar from the browser devtools
|
||||||
|
menu.style.top = y + "px";
|
||||||
|
},
|
||||||
|
() => [this.store.contextMenu?.position]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
onClickItem(action) {
|
||||||
|
action();
|
||||||
|
this.store.contextMenu = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="devtools.ContextMenu">
|
||||||
|
<div class="custom-menu" t-ref="contextmenu">
|
||||||
|
<ul class="my-1">
|
||||||
|
<li t-foreach="props.items" t-as="item" t-key="item_index" t-if="item.show" t-esc="item.title" t-on-click.stop="() => this.onClickItem(item.action)" class="custom-menu-item py-1 px-4"/>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const { Component, useRef, useEffect } = owl;
|
const { Component } = owl;
|
||||||
import { useStore } from "../../../store/store";
|
import { useStore } from "../../../store/store";
|
||||||
import { ObjectTreeElement } from "./object_tree_element/object_tree_element";
|
import { ObjectTreeElement } from "./object_tree_element/object_tree_element";
|
||||||
|
|
||||||
@@ -7,23 +7,64 @@ export class DetailsWindow extends Component {
|
|||||||
static components = { ObjectTreeElement };
|
static components = { ObjectTreeElement };
|
||||||
setup() {
|
setup() {
|
||||||
this.store = useStore();
|
this.store = useStore();
|
||||||
this.contextMenu = useRef("contextmenu");
|
|
||||||
this.contextMenuId = this.store.contextMenu.id++;
|
|
||||||
this.contextMenuEvent;
|
|
||||||
// Open the context menu when the ids match
|
|
||||||
useEffect(
|
|
||||||
(menuId) => {
|
|
||||||
if (menuId === this.contextMenuId) {
|
|
||||||
this.store.contextMenu.open(this.contextMenuEvent, this.contextMenu.el);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get contextMenuItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: "Inspect source code",
|
||||||
|
show: true,
|
||||||
|
action: () => this.store.inspectComponent("source", this.store.activeComponent.path),
|
||||||
},
|
},
|
||||||
() => [this.store.contextMenu.activeMenu]
|
{
|
||||||
);
|
title: "Store as global variable",
|
||||||
|
show: this.store.activeComponent.path.length !== 1,
|
||||||
|
action: () =>
|
||||||
|
this.store.logObjectInConsole([
|
||||||
|
...this.store.activeComponent.path,
|
||||||
|
{ type: "item", value: "component" },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Inspect in Elements tab",
|
||||||
|
show: this.store.activeComponent.path.length !== 1,
|
||||||
|
action: () => this.store.inspectComponent("DOM", this.store.activeComponent.path),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Force rerender",
|
||||||
|
show: this.store.activeComponent.path.length !== 1,
|
||||||
|
action: () => this.store.refreshComponent(this.store.activeComponent.path),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Store observed states as global variable",
|
||||||
|
show: this.store.activeComponent.path.length !== 1,
|
||||||
|
action: () =>
|
||||||
|
this.store.logObjectInConsole([
|
||||||
|
...this.store.activeComponent.path,
|
||||||
|
{ type: "item", value: "subscriptions" },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Inspect compiled template",
|
||||||
|
show: this.store.activeComponent.path.length !== 1,
|
||||||
|
action: () =>
|
||||||
|
this.store.inspectComponent("compiled template", this.store.activeComponent.path),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Log raw template",
|
||||||
|
show: this.store.activeComponent.path.length !== 1,
|
||||||
|
action: () => this.store.inspectComponent("raw template", this.store.activeComponent.path),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Store as global variable",
|
||||||
|
show: this.store.activeComponent.path.length === 1,
|
||||||
|
action: () => this.store.logObjectInConsole([...this.store.activeComponent.path]),
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
openMenu(ev) {
|
openMenu(ev) {
|
||||||
this.contextMenuEvent = ev;
|
this.store.openContextMenu(ev, this.contextMenuItems);
|
||||||
this.store.contextMenu.activeMenu = this.contextMenuId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCategory(ev, category) {
|
toggleCategory(ev, category) {
|
||||||
|
|||||||
@@ -20,6 +20,17 @@
|
|||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
<div class="details-container">
|
<div class="details-container">
|
||||||
|
<div t-if="store.observedVariables.length and store.observedVariables.some((v) => v.visible)" id="observedVariables" class="details-panel ps-2 py-1">
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="w-100">
|
||||||
|
<b class="ps-2">observed variables</b>
|
||||||
|
</div>
|
||||||
|
<i title="Remove observed variables" class="fa fa-times utility-icon p-1" t-on-click.stop="() => this.store.clearObservedVariable()"></i>
|
||||||
|
</div>
|
||||||
|
<t t-foreach="store.observedVariables" t-as="observed" t-key="observed_index" t-if="observed.visible">
|
||||||
|
<ObjectTreeElement object="observed" index="observed_index"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
<div t-if="store.activeComponent.env.children.length > 0" id="env" class="details-panel ps-2 py-1">
|
<div t-if="store.activeComponent.env.children.length > 0" id="env" class="details-panel ps-2 py-1">
|
||||||
<div class="d-flex mb-2">
|
<div class="d-flex mb-2">
|
||||||
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'env')">
|
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'env')">
|
||||||
@@ -75,22 +86,19 @@
|
|||||||
<ObjectTreeElement object="instance"/>
|
<ObjectTreeElement object="instance"/>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
|
<div t-if="store.activeComponent.hooks?.children.length > 0" id="hooks" class="details-panel ps-2 py-1">
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'hooks')">
|
||||||
|
<i class="fa mx-1 pointer-icon"
|
||||||
|
t-att-class="{'fa-caret-right': !store.activeComponent.hooks.toggled, 'fa-caret-down': store.activeComponent.hooks.toggled}"
|
||||||
|
/><b>hooks</b>
|
||||||
</div>
|
</div>
|
||||||
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
|
<i title="Remove breakpoints" class="fa fa-times utility-icon p-1" t-on-click.stop="() => this.store.removeBreakpoints()"></i>
|
||||||
<ul class="my-1">
|
</div>
|
||||||
<li t-on-click.stop="() => this.store.inspectComponent('source', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
|
<t t-if="store.activeComponent.hooks.toggled" t-foreach="store.activeComponent.hooks.children" t-as="hook" t-key="hook_index">
|
||||||
<t t-if="store.activeComponent.path.length !== 1">
|
<ObjectTreeElement object="hook"/>
|
||||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
|
||||||
<li t-on-click.stop="() => this.store.inspectComponent('DOM', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
|
|
||||||
<li t-on-click.stop="() => this.store.refreshComponent(store.activeComponent.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
|
|
||||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
|
|
||||||
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
|
|
||||||
<li t-on-click.stop="() => this.store.inspectComponent('raw template', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
|
|
||||||
</t>
|
</t>
|
||||||
<t t-else="">
|
</div>
|
||||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
|
||||||
</t>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</templates>
|
</templates>
|
||||||