Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 34489a2494 | |||
| 398df543fe | |||
| aa6a4a5d46 | |||
| db3499f5e5 | |||
| 166cada8ff | |||
| acbe316689 | |||
| fb013ccc72 | |||
| fee3eecd7f | |||
| 6037018dd2 | |||
| eec7cc4ea7 | |||
| 035895b043 | |||
| ff5ef82ea2 | |||
| 0048205636 | |||
| 0d341d9e7b | |||
| 4c7f572dbe | |||
| 610c272104 | |||
| 32e4565131 | |||
| 9b9c15e4a9 | |||
| b1690f19cc |
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x, 16.x]
|
||||
node-version: [20.x, 22.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -15,7 +15,7 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
#ide's
|
||||
.vscode
|
||||
**/.vscode/*
|
||||
.idea
|
||||
|
||||
node_modules
|
||||
@@ -26,3 +26,10 @@ release-notes.md
|
||||
|
||||
# useful in some cases
|
||||
/temp
|
||||
|
||||
# owl-vision
|
||||
*/owl-vision/out/
|
||||
*/owl-vision/.vs/
|
||||
**/*.vsix
|
||||
!*/owl-vision/.vscode/launch.json
|
||||
!*/owl-vision/.vscode/tasks.json
|
||||
|
||||
@@ -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)
|
||||
- [`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
|
||||
- [`batched`](reference/utils.md#batched): utility function to batch function calls
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
- [API](#api)
|
||||
- [Configuration](#configuration)
|
||||
- [`mount` helper](#mount-helper)
|
||||
- [Roots](#roots)
|
||||
- [Loading templates](#loading-templates)
|
||||
|
||||
## Overview
|
||||
@@ -61,6 +62,8 @@ The `config` object is an object with some of the following keys:
|
||||
templates (see [translations](translations.md))
|
||||
- **`templates (string | xml document)`**: all the templates that will be used by
|
||||
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
|
||||
whenever it encounters a component that does not provide a [static props description](props.md#props-validation).
|
||||
|
||||
@@ -90,6 +93,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
|
||||
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
|
||||
|
||||
Most applications will need to load templates whenever they start. Here is
|
||||
|
||||
@@ -357,7 +357,7 @@ cleaning operation, since the component may be destroyed before it has even been
|
||||
mounted. The `willDestroy` hook is useful in that situation, since it is always
|
||||
called.
|
||||
|
||||
The `onWillUnmount` hook is used to register a function that will be executed at
|
||||
The `onWillDestroy` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
|
||||
@@ -51,8 +51,8 @@ that render its content, and a fallback if an error happened.
|
||||
```js
|
||||
class ErrorBoundary extends Component {
|
||||
static template = xml`
|
||||
<t t-if="error" t-slot="fallback">An error occurred</t>
|
||||
<t t-else="" t-slot="content"`;
|
||||
<t t-if="state.error" t-slot="fallback">An error occurred</t>
|
||||
<t t-else="" t-slot="default"/>`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ error: false });
|
||||
|
||||
@@ -190,12 +190,13 @@ will then be updated accordingly.
|
||||
### `useExternalListener`
|
||||
|
||||
The `useExternalListener` hook helps solve a very common problem: adding and removing
|
||||
a listener on some target whenever a component is mounted/unmounted. For example,
|
||||
a listener on some target whenever a component is mounted/unmounted. It takes a target
|
||||
as its first argument, forwards the other arguments to `addEventListener`. For example,
|
||||
a dropdown menu (or its parent) may need to listen to a `click` event on `window`
|
||||
to be closed:
|
||||
|
||||
```js
|
||||
useExternalListener(window, "click", this.closeMenu);
|
||||
useExternalListener(window, "click", this.closeMenu, { capture: true });
|
||||
```
|
||||
|
||||
### `useComponent`
|
||||
|
||||
@@ -140,6 +140,28 @@ class SomeComponent extends Component {
|
||||
The `.bind` suffix also implies `.alike`, so these props will not cause additional
|
||||
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
|
||||
|
||||
The `t-props` directive can be used to specify totally dynamic props:
|
||||
@@ -238,7 +260,7 @@ class ComponentB extends owl.Component {
|
||||
count: {type: Number},
|
||||
messages: {
|
||||
type: Array,
|
||||
element: {type: Object, shape: {id: Boolean, text: String }
|
||||
element: {type: Object, shape: {id: Boolean, text: String }}
|
||||
},
|
||||
date: Date,
|
||||
combinedVal: [Number, Boolean],
|
||||
@@ -276,7 +298,8 @@ class ComponentB extends owl.Component {
|
||||
id: Number,
|
||||
name: {type: String, optional: true},
|
||||
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: {
|
||||
type: Object,
|
||||
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
|
||||
class DoubleCounter extends Component {
|
||||
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.count2++">increment count 2</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`
|
||||
|
||||
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.
|
||||
|
||||
```js
|
||||
|
||||
@@ -133,7 +133,7 @@ Slots can define a default content, in case the parent did not define them:
|
||||
## Dynamic Slots
|
||||
|
||||
The `t-slot` directive is actually able to use any expressions, using string
|
||||
interplolation:
|
||||
interpolation:
|
||||
|
||||
```xml
|
||||
<t t-slot="{{current}}" />
|
||||
@@ -201,16 +201,17 @@ use this `Notebook` component:
|
||||
|
||||
```xml
|
||||
<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>
|
||||
</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>
|
||||
</t>
|
||||
</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.
|
||||
|
||||
## Slot scopes
|
||||
|
||||
@@ -9,6 +9,7 @@ functions are all available in the `owl.utils` namespace.
|
||||
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
||||
- [`EventBus`](#eventbus): a simple EventBus
|
||||
- [`validate`](#validate): a validation function
|
||||
- [`batched`](#batched): batch function calls
|
||||
|
||||
## `whenReady`
|
||||
|
||||
@@ -78,3 +79,22 @@ validate(
|
||||
// - 'id' is missing (should be a number),
|
||||
// - '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
|
||||
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
|
||||
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"/>
|
||||
|
||||
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 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
|
||||
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.
|
||||
@@ -64,25 +64,18 @@ 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
|
||||
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
|
||||
which will trigger a rerender of the component when it is modified. The keys represent which part of the
|
||||
variable is actually observed and the target is the actual variable. For simplicity, the properties
|
||||
that are not observed by the component are greyed out while the others are in bold. This means that
|
||||
editing bold ones will trigger a rerender while the greyed out ones will not.
|
||||
The observed state is actually information about which variables are being observed by the component:
|
||||
when any property of a reactive object is being read by the component, the component will subscribe
|
||||
to this property which means it will listen to any change that can occur on the property and render
|
||||
when such a change occurs. This can be visualized easily within the devtools inside of the observed
|
||||
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"/>
|
||||
|
||||
In the given example, we have two keys/target pairs for two different variables. The first one indicates
|
||||
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
|
||||
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
|
||||
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.
|
||||
@@ -96,8 +89,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
|
||||
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:
|
||||
89, "yes", undefined, null, \["hello", 15\], {"a": 1}, true, ...). Whether it has an impact on the
|
||||
component or not and whether it produces an error is the responsability of the user.
|
||||
89, "yes", undefined, null, \["hello", 15\], {"a": 1}, true, ...). Editing any value will produce a
|
||||
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"/>
|
||||
|
||||
@@ -117,7 +112,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
|
||||
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,
|
||||
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"/>
|
||||
|
||||
@@ -131,20 +127,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
|
||||
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
|
||||
of the modification
|
||||
of the modification.
|
||||
|
||||
<img src="screenshots/trace_rendering.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
|
||||
|
||||
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
|
||||
with the dark mode enabled. There is also a refresh button to completely reset the owl devtools.
|
||||
be toggled using the sun/moon icon at the top-right corner of the tab. There is also a refresh button to
|
||||
completely reset the owl devtools.
|
||||
|
||||
<img src="screenshots/darkmode.png"/>
|
||||
|
||||
## 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.
|
||||
|
||||
|
Before Width: | Height: | Size: 333 KiB After Width: | Height: | Size: 545 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 387 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 320 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 329 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 536 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: 177 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 182 KiB |
@@ -1850,8 +1850,9 @@ const NO_CALLBACK = () => {
|
||||
};
|
||||
const objectToString = Object.prototype.toString;
|
||||
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
|
||||
const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
|
||||
// Use arrays because Array.includes is faster than Set.has for small arrays
|
||||
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
|
||||
* many native objects such as Promise (whose toString is [object Promise])
|
||||
@@ -1874,7 +1875,7 @@ function canBeMadeReactive(value) {
|
||||
if (typeof value !== "object") {
|
||||
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,
|
||||
@@ -2044,7 +2045,7 @@ function reactive(target, callback = NO_CALLBACK) {
|
||||
const reactivesForTarget = reactiveCache.get(target);
|
||||
if (!reactivesForTarget.has(callback)) {
|
||||
const targetRawType = rawType(target);
|
||||
const handler = COLLECTION_RAWTYPES.has(targetRawType)
|
||||
const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
|
||||
? collectionsProxyHandler(target, callback, targetRawType)
|
||||
: basicProxyHandler(callback);
|
||||
const proxy = new Proxy(target, handler);
|
||||
@@ -2073,7 +2074,7 @@ function basicProxyHandler(callback) {
|
||||
set(target, key, value, receiver) {
|
||||
const hadKey = objectHasOwnProperty.call(target, key);
|
||||
const originalValue = Reflect.get(target, key, receiver);
|
||||
const ret = Reflect.set(target, key, value, receiver);
|
||||
const ret = Reflect.set(target, key, toRaw(value), receiver);
|
||||
if (!hadKey && objectHasOwnProperty.call(target, key)) {
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
}
|
||||
@@ -2269,6 +2270,12 @@ function collectionsProxyHandler(target, callback, targetRawType) {
|
||||
}
|
||||
|
||||
let currentNode = null;
|
||||
function saveCurrent() {
|
||||
let n = currentNode;
|
||||
return () => {
|
||||
currentNode = n;
|
||||
};
|
||||
}
|
||||
function getCurrent() {
|
||||
if (!currentNode) {
|
||||
throw new OwlError("No active component (a hook function should only be called in 'setup')");
|
||||
@@ -2597,42 +2604,47 @@ class ComponentNode {
|
||||
}
|
||||
|
||||
const TIMEOUT = Symbol("timeout");
|
||||
const HOOK_TIMEOUT = {
|
||||
onWillStart: 3000,
|
||||
onWillUpdateProps: 3000,
|
||||
};
|
||||
function wrapError(fn, hookName) {
|
||||
const error = new OwlError(`The following error occurred in ${hookName}: `);
|
||||
const timeoutError = new OwlError(`${hookName}'s promise hasn't resolved after 3 seconds`);
|
||||
const error = new OwlError();
|
||||
const timeoutError = new OwlError();
|
||||
const node = getCurrent();
|
||||
return (...args) => {
|
||||
const onError = (cause) => {
|
||||
error.cause = cause;
|
||||
if (cause instanceof Error) {
|
||||
error.message += `"${cause.message}"`;
|
||||
}
|
||||
else {
|
||||
error.message = `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
|
||||
}
|
||||
error.message =
|
||||
cause instanceof Error
|
||||
? `The following error occurred in ${hookName}: "${cause.message}"`
|
||||
: `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
|
||||
throw error;
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
const result = fn(...args);
|
||||
if (result instanceof Promise) {
|
||||
if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
|
||||
const fiber = node.fiber;
|
||||
Promise.race([
|
||||
result.catch(() => { }),
|
||||
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
|
||||
]).then((res) => {
|
||||
if (res === TIMEOUT && node.fiber === fiber) {
|
||||
console.warn(timeoutError);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result.catch(onError);
|
||||
}
|
||||
return result;
|
||||
result = fn(...args);
|
||||
}
|
||||
catch (cause) {
|
||||
onError(cause);
|
||||
}
|
||||
if (!(result instanceof Promise)) {
|
||||
return result;
|
||||
}
|
||||
const timeout = HOOK_TIMEOUT[hookName];
|
||||
if (timeout) {
|
||||
const fiber = node.fiber;
|
||||
Promise.race([
|
||||
result.catch(() => { }),
|
||||
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), timeout)),
|
||||
]).then((res) => {
|
||||
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
|
||||
timeoutError.message = `${hookName}'s promise hasn't resolved after ${timeout / 1000} seconds`;
|
||||
console.log(timeoutError);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result.catch(onError);
|
||||
};
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -3159,8 +3171,14 @@ const helpers = {
|
||||
makeRefWrapper,
|
||||
};
|
||||
|
||||
const bdom = { text, createBlock, list, multi, html, toggler, comment };
|
||||
function parseXML$1(xml) {
|
||||
/**
|
||||
* 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) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
@@ -3187,7 +3205,9 @@ function parseXML$1(xml) {
|
||||
throw new OwlError(msg);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
|
||||
const bdom = { text, createBlock, list, multi, html, toggler, comment };
|
||||
class TemplateSet {
|
||||
constructor(config = {}) {
|
||||
this.rawTemplates = Object.create(globalTemplates);
|
||||
@@ -3197,8 +3217,16 @@ class TemplateSet {
|
||||
this.translateFn = config.translateFn;
|
||||
this.translatableAttributes = config.translatableAttributes;
|
||||
if (config.templates) {
|
||||
this.addTemplates(config.templates);
|
||||
if (config.templates instanceof Document || typeof config.templates === "string") {
|
||||
this.addTemplates(config.templates);
|
||||
}
|
||||
else {
|
||||
for (const name in config.templates) {
|
||||
this.addTemplate(name, config.templates[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.getRawTemplate = config.getTemplate;
|
||||
}
|
||||
static registerTemplate(name, fn) {
|
||||
globalTemplates[name] = fn;
|
||||
@@ -3228,15 +3256,16 @@ class TemplateSet {
|
||||
// empty string
|
||||
return;
|
||||
}
|
||||
xml = xml instanceof Document ? xml : parseXML$1(xml);
|
||||
xml = xml instanceof Document ? xml : parseXML(xml);
|
||||
for (const template of xml.querySelectorAll("[t-name]")) {
|
||||
const name = template.getAttribute("t-name");
|
||||
this.addTemplate(name, template);
|
||||
}
|
||||
}
|
||||
getTemplate(name) {
|
||||
var _a;
|
||||
if (!(name in this.templates)) {
|
||||
const rawTemplate = this.rawTemplates[name];
|
||||
const rawTemplate = ((_a = this.getRawTemplate) === null || _a === void 0 ? void 0 : _a.call(this, name)) || this.rawTemplates[name];
|
||||
if (rawTemplate === undefined) {
|
||||
let extraInfo = "";
|
||||
try {
|
||||
@@ -3494,7 +3523,7 @@ function compileExprToArray(expr) {
|
||||
const localVars = new Set();
|
||||
const tokens = tokenize(expr);
|
||||
let i = 0;
|
||||
let stack = []; // to track last opening [ or {
|
||||
let stack = []; // to track last opening (, [ or {
|
||||
while (i < tokens.length) {
|
||||
let token = tokens[i];
|
||||
let prevToken = tokens[i - 1];
|
||||
@@ -3503,10 +3532,12 @@ function compileExprToArray(expr) {
|
||||
switch (token.type) {
|
||||
case "LEFT_BRACE":
|
||||
case "LEFT_BRACKET":
|
||||
case "LEFT_PAREN":
|
||||
stack.push(token.type);
|
||||
break;
|
||||
case "RIGHT_BRACE":
|
||||
case "RIGHT_BRACKET":
|
||||
case "RIGHT_PAREN":
|
||||
stack.pop();
|
||||
}
|
||||
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
|
||||
@@ -3618,6 +3649,13 @@ function isProp(tag, key) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Returns a template literal that evaluates to str. You can add interpolation
|
||||
* sigils into the string if required
|
||||
*/
|
||||
function toStringExpression(str) {
|
||||
return `\`${str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${")}\``;
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// BlockDescription
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -3798,15 +3836,14 @@ class CodeGenerator {
|
||||
mainCode.push(``);
|
||||
for (let block of this.blocks) {
|
||||
if (block.dom) {
|
||||
let xmlString = block.asXmlString();
|
||||
xmlString = xmlString.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
||||
let xmlString = toStringExpression(block.asXmlString());
|
||||
if (block.dynamicTagName) {
|
||||
xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
|
||||
xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
|
||||
mainCode.push(`let ${block.blockName} = tag => createBlock(\`${xmlString}\`);`);
|
||||
xmlString = xmlString.replace(/^`<\w+/, `\`<\${tag || '${block.dom.nodeName}'}`);
|
||||
xmlString = xmlString.replace(/\w+>`$/, `\${tag || '${block.dom.nodeName}'}>\``);
|
||||
mainCode.push(`let ${block.blockName} = tag => createBlock(${xmlString});`);
|
||||
}
|
||||
else {
|
||||
mainCode.push(`let ${block.blockName} = createBlock(\`${xmlString}\`);`);
|
||||
mainCode.push(`let ${block.blockName} = createBlock(${xmlString});`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3984,7 +4021,7 @@ class CodeGenerator {
|
||||
const isNewBlock = !block || forceNewBlock;
|
||||
if (isNewBlock) {
|
||||
block = this.createBlock(block, "comment", ctx);
|
||||
this.insertBlock(`comment(\`${ast.value}\`)`, block, {
|
||||
this.insertBlock(`comment(${toStringExpression(ast.value)})`, block, {
|
||||
...ctx,
|
||||
forceNewBlock: forceNewBlock && !block,
|
||||
});
|
||||
@@ -4006,7 +4043,7 @@ class CodeGenerator {
|
||||
}
|
||||
if (!block || forceNewBlock) {
|
||||
block = this.createBlock(block, "text", ctx);
|
||||
this.insertBlock(`text(\`${value}\`)`, block, {
|
||||
this.insertBlock(`text(${toStringExpression(value)})`, block, {
|
||||
...ctx,
|
||||
forceNewBlock: forceNewBlock && !block,
|
||||
});
|
||||
@@ -4227,7 +4264,8 @@ class CodeGenerator {
|
||||
expr = compileExpr(ast.expr);
|
||||
if (ast.defaultValue) {
|
||||
this.helpers.add("withDefault");
|
||||
expr = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
||||
// FIXME: defaultValue is not translated
|
||||
expr = `withDefault(${expr}, ${toStringExpression(ast.defaultValue)})`;
|
||||
}
|
||||
}
|
||||
if (!block || forceNewBlock) {
|
||||
@@ -4480,7 +4518,7 @@ class CodeGenerator {
|
||||
this.addLine(`${ctxVar}[zero] = ${bl};`);
|
||||
}
|
||||
}
|
||||
const key = `key + \`${this.generateComponentKey()}\``;
|
||||
const key = this.generateComponentKey();
|
||||
if (isDynamic) {
|
||||
const templateVar = generateId("template");
|
||||
if (!this.staticDefs.find((d) => d.id === "call")) {
|
||||
@@ -4532,12 +4570,12 @@ class CodeGenerator {
|
||||
else {
|
||||
let value;
|
||||
if (ast.defaultValue) {
|
||||
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
|
||||
const defaultValue = toStringExpression(ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue);
|
||||
if (ast.value) {
|
||||
value = `withDefault(${expr}, \`${defaultValue}\`)`;
|
||||
value = `withDefault(${expr}, ${defaultValue})`;
|
||||
}
|
||||
else {
|
||||
value = `\`${defaultValue}\``;
|
||||
value = defaultValue;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -4548,12 +4586,12 @@ class CodeGenerator {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
generateComponentKey() {
|
||||
generateComponentKey(currentKey = "key") {
|
||||
const parts = [generateId("__")];
|
||||
for (let i = 0; i < this.target.loopLevel; i++) {
|
||||
parts.push(`\${key${i + 1}}`);
|
||||
}
|
||||
return parts.join("__");
|
||||
return `${currentKey} + \`${parts.join("__")}\``;
|
||||
}
|
||||
/**
|
||||
* Formats a prop name and value into a string suitable to be inserted in the
|
||||
@@ -4567,7 +4605,12 @@ class CodeGenerator {
|
||||
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
||||
*/
|
||||
formatProp(name, value) {
|
||||
value = this.captureExpression(value);
|
||||
if (name.endsWith(".translate")) {
|
||||
value = toStringExpression(this.translateFn(value));
|
||||
}
|
||||
else {
|
||||
value = this.captureExpression(value);
|
||||
}
|
||||
if (name.includes(".")) {
|
||||
let [_name, suffix] = name.split(".");
|
||||
name = _name;
|
||||
@@ -4576,6 +4619,7 @@ class CodeGenerator {
|
||||
value = `(${value}).bind(this)`;
|
||||
break;
|
||||
case "alike":
|
||||
case "translate":
|
||||
break;
|
||||
default:
|
||||
throw new OwlError("Invalid prop suffix");
|
||||
@@ -4644,7 +4688,6 @@ class CodeGenerator {
|
||||
this.addLine(`${propVar}.slots = markRaw(Object.assign(${slotDef}, ${propVar}.slots))`);
|
||||
}
|
||||
// cmap key
|
||||
const key = this.generateComponentKey();
|
||||
let expr;
|
||||
if (ast.isDynamic) {
|
||||
expr = generateId("Comp");
|
||||
@@ -4660,7 +4703,7 @@ class CodeGenerator {
|
||||
// todo: check the forcenewblock condition
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
let keyArg = `key + \`${key}\``;
|
||||
let keyArg = this.generateComponentKey();
|
||||
if (ctx.tKeyExpr) {
|
||||
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
||||
}
|
||||
@@ -4733,7 +4776,7 @@ class CodeGenerator {
|
||||
}
|
||||
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
||||
if (isMultiple) {
|
||||
key = `${key} + \`${this.generateComponentKey()}\``;
|
||||
key = this.generateComponentKey(key);
|
||||
}
|
||||
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
||||
const scope = this.getPropString(props, dynProps);
|
||||
@@ -4774,7 +4817,6 @@ class CodeGenerator {
|
||||
}
|
||||
let { block } = ctx;
|
||||
const name = this.compileInNewTarget("slot", ast.content, ctx);
|
||||
const key = this.generateComponentKey();
|
||||
let ctxStr = "ctx";
|
||||
if (this.target.loopLevel || !this.hasSafeContext) {
|
||||
ctxStr = generateId("ctx");
|
||||
@@ -4787,7 +4829,8 @@ class CodeGenerator {
|
||||
expr: `app.createComponent(null, false, true, false, false)`,
|
||||
});
|
||||
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) {
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
@@ -4946,9 +4989,9 @@ function parseDOMNode(node, ctx) {
|
||||
const isSelect = tagName === "select";
|
||||
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
||||
const isRadioInput = isInput && typeAttr === "radio";
|
||||
const hasLazyMod = attr.includes(".lazy");
|
||||
const hasNumberMod = attr.includes(".number");
|
||||
const hasTrimMod = attr.includes(".trim");
|
||||
const hasLazyMod = hasTrimMod || attr.includes(".lazy");
|
||||
const hasNumberMod = attr.includes(".number");
|
||||
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
|
||||
model = {
|
||||
baseExpr,
|
||||
@@ -5279,14 +5322,14 @@ function parseComponent(node, ctx) {
|
||||
// be ignored)
|
||||
let el = slotNode.parentElement;
|
||||
let isInSubComponent = false;
|
||||
while (el !== clone) {
|
||||
while (el && el !== clone) {
|
||||
if (el.hasAttribute("t-component") || el.tagName[0] === el.tagName[0].toUpperCase()) {
|
||||
isInSubComponent = true;
|
||||
break;
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
if (isInSubComponent) {
|
||||
if (isInSubComponent || !el) {
|
||||
continue;
|
||||
}
|
||||
slotNode.removeAttribute("t-set-slot");
|
||||
@@ -5494,41 +5537,6 @@ function normalizeTEscTOut(el) {
|
||||
function normalizeXML(el) {
|
||||
normalizeTIf(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) {
|
||||
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;
|
||||
}
|
||||
|
||||
function compile(template, options = {}) {
|
||||
@@ -5555,7 +5563,7 @@ function compile(template, options = {}) {
|
||||
}
|
||||
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
const version = "2.2.6";
|
||||
const version = "2.4.1";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Scheduler
|
||||
@@ -5566,6 +5574,7 @@ class Scheduler {
|
||||
this.frame = 0;
|
||||
this.delayedRenders = [];
|
||||
this.cancelledNodes = new Set();
|
||||
this.processing = false;
|
||||
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
|
||||
}
|
||||
addFiber(fiber) {
|
||||
@@ -5596,6 +5605,10 @@ class Scheduler {
|
||||
}
|
||||
}
|
||||
processTasks() {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
this.processing = true;
|
||||
this.frame = 0;
|
||||
for (let node of this.cancelledNodes) {
|
||||
node._destroy();
|
||||
@@ -5609,6 +5622,7 @@ class Scheduler {
|
||||
this.tasks.delete(task);
|
||||
}
|
||||
}
|
||||
this.processing = false;
|
||||
}
|
||||
processFiber(fiber) {
|
||||
if (fiber.root !== fiber) {
|
||||
@@ -5650,6 +5664,7 @@ class App extends TemplateSet {
|
||||
constructor(Root, config = {}) {
|
||||
super(config);
|
||||
this.scheduler = new Scheduler();
|
||||
this.subRoots = new Set();
|
||||
this.root = null;
|
||||
this.name = config.name || "";
|
||||
this.Root = Root;
|
||||
@@ -5668,14 +5683,44 @@ class App extends TemplateSet {
|
||||
this.props = config.props || {};
|
||||
}
|
||||
mount(target, options) {
|
||||
App.validateTarget(target);
|
||||
if (this.dev) {
|
||||
validateProps(this.Root, this.props, { __owl__: { app: this } });
|
||||
const root = this.createRoot(this.Root, { props: this.props });
|
||||
this.root = root.node;
|
||||
this.subRoots.delete(root.node);
|
||||
return root.mount(target, options);
|
||||
}
|
||||
createRoot(Root, config = {}) {
|
||||
const props = config.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;
|
||||
}
|
||||
const node = this.makeNode(this.Root, this.props);
|
||||
const prom = this.mountNode(node, target, options);
|
||||
this.root = node;
|
||||
return prom;
|
||||
const restore = saveCurrent();
|
||||
const node = this.makeNode(Root, props);
|
||||
restore();
|
||||
if (config.env) {
|
||||
this.env = env;
|
||||
}
|
||||
this.subRoots.add(node);
|
||||
return {
|
||||
node,
|
||||
mount: (target, options) => {
|
||||
App.validateTarget(target);
|
||||
if (this.dev) {
|
||||
validateProps(Root, props, { __owl__: { app: this } });
|
||||
}
|
||||
const prom = this.mountNode(node, target, options);
|
||||
return prom;
|
||||
},
|
||||
destroy: () => {
|
||||
this.subRoots.delete(node);
|
||||
node.destroy();
|
||||
this.scheduler.processTasks();
|
||||
},
|
||||
};
|
||||
}
|
||||
makeNode(Component, props) {
|
||||
return new ComponentNode(Component, props, this, null, null);
|
||||
@@ -5707,6 +5752,9 @@ class App extends TemplateSet {
|
||||
}
|
||||
destroy() {
|
||||
if (this.root) {
|
||||
for (let subroot of this.subRoots) {
|
||||
subroot.destroy();
|
||||
}
|
||||
this.root.destroy();
|
||||
this.scheduler.processTasks();
|
||||
}
|
||||
@@ -5902,7 +5950,7 @@ function useChildSubEnv(envExtension) {
|
||||
*
|
||||
* @template T
|
||||
* @param {Effect<T>} effect the effect to run on component mount and/or patch
|
||||
* @param {()=>T} [computeDependencies=()=>[NaN]] a callback to compute
|
||||
* @param {()=>[...T]} [computeDependencies=()=>[NaN]] a callback to compute
|
||||
* dependencies that will decide if the effect needs to be cleaned up and
|
||||
* run again. If the dependencies did not change, the effect will not run
|
||||
* again. The default value returns an array containing only NaN because
|
||||
@@ -5981,9 +6029,9 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
|
||||
});
|
||||
};
|
||||
|
||||
export { App, Component, EventBus, OwlError, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
|
||||
export { App, Component, EventBus, OwlError, __info__, batched, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
|
||||
|
||||
|
||||
__info__.date = '2023-09-25T11:48:01.531Z';
|
||||
__info__.hash = '752160f';
|
||||
__info__.date = '2024-10-31T09:42:30.824Z';
|
||||
__info__.hash = 'b8d09e5';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
|
||||
@@ -41,9 +41,6 @@ const loadFile = (path) => {
|
||||
* Make an iframe, with all the js, css and xml properly injected.
|
||||
*/
|
||||
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");
|
||||
iframe.onload = () => {
|
||||
const doc = iframe.contentDocument;
|
||||
@@ -55,6 +52,8 @@ function makeCodeIframe(js, css, xml) {
|
||||
|
||||
const script = doc.createElement("script");
|
||||
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}`;
|
||||
doc.body.appendChild(script);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.2.6",
|
||||
"version": "2.4.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.2.6",
|
||||
"version": "2.4.1",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"module": "dist/owl.es.js",
|
||||
@@ -9,7 +9,7 @@
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12.18.3"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build:bundle": "rollup -c --failAfterWarnings",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -82,6 +82,14 @@ function isProp(tag: string, key: string): boolean {
|
||||
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
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -311,14 +319,13 @@ export class CodeGenerator {
|
||||
mainCode.push(``);
|
||||
for (let block of this.blocks) {
|
||||
if (block.dom) {
|
||||
let xmlString = block.asXmlString();
|
||||
xmlString = xmlString.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
||||
let xmlString = toStringExpression(block.asXmlString());
|
||||
if (block.dynamicTagName) {
|
||||
xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
|
||||
xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
|
||||
mainCode.push(`let ${block.blockName} = tag => createBlock(\`${xmlString}\`);`);
|
||||
xmlString = xmlString.replace(/^`<\w+/, `\`<\${tag || '${block.dom.nodeName}'}`);
|
||||
xmlString = xmlString.replace(/\w+>`$/, `\${tag || '${block.dom.nodeName}'}>\``);
|
||||
mainCode.push(`let ${block.blockName} = tag => createBlock(${xmlString});`);
|
||||
} else {
|
||||
mainCode.push(`let ${block.blockName} = createBlock(\`${xmlString}\`);`);
|
||||
mainCode.push(`let ${block.blockName} = createBlock(${xmlString});`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,7 +522,7 @@ export class CodeGenerator {
|
||||
const isNewBlock = !block || forceNewBlock;
|
||||
if (isNewBlock) {
|
||||
block = this.createBlock(block, "comment", ctx);
|
||||
this.insertBlock(`comment(\`${ast.value}\`)`, block, {
|
||||
this.insertBlock(`comment(${toStringExpression(ast.value)})`, block, {
|
||||
...ctx,
|
||||
forceNewBlock: forceNewBlock && !block,
|
||||
});
|
||||
@@ -539,7 +546,7 @@ export class CodeGenerator {
|
||||
|
||||
if (!block || forceNewBlock) {
|
||||
block = this.createBlock(block, "text", ctx);
|
||||
this.insertBlock(`text(\`${value}\`)`, block, {
|
||||
this.insertBlock(`text(${toStringExpression(value)})`, block, {
|
||||
...ctx,
|
||||
forceNewBlock: forceNewBlock && !block,
|
||||
});
|
||||
@@ -774,7 +781,8 @@ export class CodeGenerator {
|
||||
expr = compileExpr(ast.expr);
|
||||
if (ast.defaultValue) {
|
||||
this.helpers.add("withDefault");
|
||||
expr = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
||||
// FIXME: defaultValue is not translated
|
||||
expr = `withDefault(${expr}, ${toStringExpression(ast.defaultValue)})`;
|
||||
}
|
||||
}
|
||||
if (!block || forceNewBlock) {
|
||||
@@ -1039,7 +1047,7 @@ export class CodeGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
const key = `key + \`${this.generateComponentKey()}\``;
|
||||
const key = this.generateComponentKey();
|
||||
if (isDynamic) {
|
||||
const templateVar = generateId("template");
|
||||
if (!this.staticDefs.find((d) => d.id === "call")) {
|
||||
@@ -1091,11 +1099,13 @@ export class CodeGenerator {
|
||||
} else {
|
||||
let value: string;
|
||||
if (ast.defaultValue) {
|
||||
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
|
||||
const defaultValue = toStringExpression(
|
||||
ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue
|
||||
);
|
||||
if (ast.value) {
|
||||
value = `withDefault(${expr}, \`${defaultValue}\`)`;
|
||||
value = `withDefault(${expr}, ${defaultValue})`;
|
||||
} else {
|
||||
value = `\`${defaultValue}\``;
|
||||
value = defaultValue;
|
||||
}
|
||||
} else {
|
||||
value = expr;
|
||||
@@ -1106,12 +1116,12 @@ export class CodeGenerator {
|
||||
return null;
|
||||
}
|
||||
|
||||
generateComponentKey() {
|
||||
generateComponentKey(currentKey: string = "key") {
|
||||
const parts = [generateId("__")];
|
||||
for (let i = 0; i < this.target.loopLevel; i++) {
|
||||
parts.push(`\${key${i + 1}}`);
|
||||
}
|
||||
return parts.join("__");
|
||||
return `${currentKey} + \`${parts.join("__")}\``;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1126,7 +1136,11 @@ export class CodeGenerator {
|
||||
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
||||
*/
|
||||
formatProp(name: string, value: string): string {
|
||||
value = this.captureExpression(value);
|
||||
if (name.endsWith(".translate")) {
|
||||
value = toStringExpression(this.translateFn(value));
|
||||
} else {
|
||||
value = this.captureExpression(value);
|
||||
}
|
||||
if (name.includes(".")) {
|
||||
let [_name, suffix] = name.split(".");
|
||||
name = _name;
|
||||
@@ -1135,6 +1149,7 @@ export class CodeGenerator {
|
||||
value = `(${value}).bind(this)`;
|
||||
break;
|
||||
case "alike":
|
||||
case "translate":
|
||||
break;
|
||||
default:
|
||||
throw new OwlError("Invalid prop suffix");
|
||||
@@ -1214,7 +1229,6 @@ export class CodeGenerator {
|
||||
}
|
||||
|
||||
// cmap key
|
||||
const key = this.generateComponentKey();
|
||||
let expr: string;
|
||||
if (ast.isDynamic) {
|
||||
expr = generateId("Comp");
|
||||
@@ -1232,7 +1246,7 @@ export class CodeGenerator {
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
|
||||
let keyArg = `key + \`${key}\``;
|
||||
let keyArg = this.generateComponentKey();
|
||||
if (ctx.tKeyExpr) {
|
||||
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
||||
}
|
||||
@@ -1311,7 +1325,7 @@ export class CodeGenerator {
|
||||
}
|
||||
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
||||
if (isMultiple) {
|
||||
key = `${key} + \`${this.generateComponentKey()}\``;
|
||||
key = this.generateComponentKey(key);
|
||||
}
|
||||
|
||||
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
||||
@@ -1354,7 +1368,6 @@ export class CodeGenerator {
|
||||
|
||||
let { block } = ctx;
|
||||
const name = this.compileInNewTarget("slot", ast.content, ctx);
|
||||
const key = this.generateComponentKey();
|
||||
let ctxStr = "ctx";
|
||||
if (this.target.loopLevel || !this.hasSafeContext) {
|
||||
ctxStr = generateId("ctx");
|
||||
@@ -1368,7 +1381,8 @@ export class CodeGenerator {
|
||||
});
|
||||
|
||||
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) {
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ export function compileExprToArray(expr: string): Token[] {
|
||||
const localVars = new Set<string>();
|
||||
const tokens = tokenize(expr);
|
||||
let i = 0;
|
||||
let stack = []; // to track last opening [ or {
|
||||
let stack = []; // to track last opening (, [ or {
|
||||
|
||||
while (i < tokens.length) {
|
||||
let token = tokens[i];
|
||||
@@ -279,10 +279,12 @@ export function compileExprToArray(expr: string): Token[] {
|
||||
switch (token.type) {
|
||||
case "LEFT_BRACE":
|
||||
case "LEFT_BRACKET":
|
||||
case "LEFT_PAREN":
|
||||
stack.push(token.type);
|
||||
break;
|
||||
case "RIGHT_BRACE":
|
||||
case "RIGHT_BRACKET":
|
||||
case "RIGHT_PAREN":
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { OwlError } from "../common/owl_error";
|
||||
import { parseXML } from "../common/utils";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// AST Type definition
|
||||
@@ -366,9 +367,9 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
const isSelect = tagName === "select";
|
||||
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
||||
const isRadioInput = isInput && typeAttr === "radio";
|
||||
const hasLazyMod = attr.includes(".lazy");
|
||||
const hasNumberMod = attr.includes(".number");
|
||||
const hasTrimMod = attr.includes(".trim");
|
||||
const hasLazyMod = hasTrimMod || attr.includes(".lazy");
|
||||
const hasNumberMod = attr.includes(".number");
|
||||
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
|
||||
|
||||
model = {
|
||||
@@ -740,14 +741,14 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
// be ignored)
|
||||
let el = slotNode.parentElement!;
|
||||
let isInSubComponent = false;
|
||||
while (el !== clone) {
|
||||
while (el && el !== clone) {
|
||||
if (el!.hasAttribute("t-component") || el!.tagName[0] === el!.tagName[0].toUpperCase()) {
|
||||
isInSubComponent = true;
|
||||
break;
|
||||
}
|
||||
el = el.parentElement!;
|
||||
}
|
||||
if (isInSubComponent) {
|
||||
if (isInSubComponent || !el) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -972,40 +973,3 @@ function normalizeXML(el: Element) {
|
||||
normalizeTIf(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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { version } from "../version";
|
||||
import { Component, ComponentConstructor, Props } from "./component";
|
||||
import { ComponentNode } from "./component_node";
|
||||
import { ComponentNode, saveCurrent } from "./component_node";
|
||||
import { nodeErrorHandlers, handleError } from "./error_handling";
|
||||
import { OwlError } from "../common/owl_error";
|
||||
import { Fiber, RootFiber, MountOptions } from "./fibers";
|
||||
@@ -16,10 +16,13 @@ export interface Env {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface AppConfig<P, E> extends TemplateSetConfig {
|
||||
name?: string;
|
||||
export interface RootConfig<P, E> {
|
||||
props?: P;
|
||||
env?: E;
|
||||
}
|
||||
|
||||
export interface AppConfig<P, E> extends TemplateSetConfig, RootConfig<P, E> {
|
||||
name?: string;
|
||||
test?: boolean;
|
||||
warnIfNoStaticProps?: boolean;
|
||||
}
|
||||
@@ -49,6 +52,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 };
|
||||
|
||||
export class App<
|
||||
@@ -65,6 +74,7 @@ export class App<
|
||||
props: P;
|
||||
env: E;
|
||||
scheduler = new Scheduler();
|
||||
subRoots: Set<ComponentNode> = new Set();
|
||||
root: ComponentNode<P, E> | null = null;
|
||||
warnIfNoStaticProps: boolean;
|
||||
|
||||
@@ -91,14 +101,49 @@ export class App<
|
||||
target: HTMLElement | ShadowRoot,
|
||||
options?: MountOptions
|
||||
): Promise<Component<P, E> & InstanceType<T>> {
|
||||
App.validateTarget(target);
|
||||
if (this.dev) {
|
||||
validateProps(this.Root, this.props, { __owl__: { app: this } });
|
||||
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 node = this.makeNode(this.Root, this.props);
|
||||
const prom = this.mountNode(node, target, options);
|
||||
this.root = node;
|
||||
return prom;
|
||||
|
||||
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);
|
||||
if (this.dev) {
|
||||
validateProps(Root, props, { __owl__: { app: this } });
|
||||
}
|
||||
const prom = this.mountNode(node, target, options);
|
||||
return prom;
|
||||
},
|
||||
destroy: () => {
|
||||
this.subRoots.delete(node);
|
||||
node.destroy();
|
||||
this.scheduler.processTasks();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
|
||||
@@ -134,6 +179,9 @@ export class App<
|
||||
|
||||
destroy() {
|
||||
if (this.root) {
|
||||
for (let subroot of this.subRoots) {
|
||||
subroot.destroy();
|
||||
}
|
||||
this.root.destroy();
|
||||
this.scheduler.processTasks();
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export type ComponentConstructor<P extends Props = any, E = any> = (new (
|
||||
|
||||
export class Component<Props = any, Env = any> {
|
||||
static template: string = "";
|
||||
static props?: any;
|
||||
static props?: Schema;
|
||||
static defaultProps?: any;
|
||||
|
||||
props: Props;
|
||||
|
||||
@@ -10,6 +10,13 @@ import { batched, Callback } from "./utils";
|
||||
|
||||
let currentNode: ComponentNode | null = null;
|
||||
|
||||
export function saveCurrent() {
|
||||
let n = currentNode;
|
||||
return () => {
|
||||
currentNode = n;
|
||||
};
|
||||
}
|
||||
|
||||
export function getCurrent(): ComponentNode {
|
||||
if (!currentNode) {
|
||||
throw new OwlError("No active component (a hook function should only be called in 'setup')");
|
||||
|
||||
@@ -59,7 +59,7 @@ export function useChildSubEnv(envExtension: Env) {
|
||||
// useEffect
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
type EffectDeps<T extends any[]> = T | (T extends [...infer H, never] ? EffectDeps<H> : never);
|
||||
type EffectDeps<T extends unknown[]> = T | (T extends [...infer H, never] ? EffectDeps<H> : never);
|
||||
|
||||
/**
|
||||
* @template T
|
||||
@@ -67,7 +67,7 @@ type EffectDeps<T extends any[]> = T | (T extends [...infer H, never] ? EffectDe
|
||||
* @returns {void|(()=>void)} a cleanup function that reverses the side
|
||||
* effects of the effect callback.
|
||||
*/
|
||||
type Effect<T extends [...T]> = (...dependencies: EffectDeps<T>) => void | (() => void);
|
||||
type Effect<T extends unknown[]> = (...dependencies: EffectDeps<T>) => void | (() => void);
|
||||
|
||||
/**
|
||||
* This hook will run a callback when a component is mounted and patched, and
|
||||
@@ -76,15 +76,15 @@ type Effect<T extends [...T]> = (...dependencies: EffectDeps<T>) => void | (() =
|
||||
*
|
||||
* @template T
|
||||
* @param {Effect<T>} effect the effect to run on component mount and/or patch
|
||||
* @param {()=>T} [computeDependencies=()=>[NaN]] a callback to compute
|
||||
* @param {()=>[...T]} [computeDependencies=()=>[NaN]] a callback to compute
|
||||
* dependencies that will decide if the effect needs to be cleaned up and
|
||||
* run again. If the dependencies did not change, the effect will not run
|
||||
* again. The default value returns an array containing only NaN because
|
||||
* NaN !== NaN, which will cause the effect to rerun on every patch.
|
||||
*/
|
||||
export function useEffect<T extends [...T]>(
|
||||
export function useEffect<T extends unknown[]>(
|
||||
effect: Effect<T>,
|
||||
computeDependencies: () => T = () => [NaN] as never
|
||||
computeDependencies: () => [...T] = () => [NaN] as never
|
||||
) {
|
||||
let cleanup: (() => void) | void;
|
||||
let dependencies: T;
|
||||
|
||||
@@ -41,7 +41,7 @@ export { useComponent, useState } from "./component_node";
|
||||
export { status } from "./status";
|
||||
export { reactive, markRaw, toRaw } from "./reactivity";
|
||||
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
||||
export { EventBus, whenReady, loadFile, markup } from "./utils";
|
||||
export { batched, EventBus, whenReady, loadFile, markup } from "./utils";
|
||||
export {
|
||||
onWillStart,
|
||||
onMounted,
|
||||
|
||||
@@ -3,42 +3,50 @@ import { nodeErrorHandlers } from "./error_handling";
|
||||
import { OwlError } from "../common/owl_error";
|
||||
|
||||
const TIMEOUT = Symbol("timeout");
|
||||
const HOOK_TIMEOUT: { [key: string]: number } = {
|
||||
onWillStart: 3000,
|
||||
onWillUpdateProps: 3000,
|
||||
};
|
||||
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;
|
||||
};
|
||||
const timeoutError = new OwlError(`${hookName}'s promise hasn't resolved after 3 seconds`);
|
||||
const timeoutError = new OwlError();
|
||||
const node = getCurrent();
|
||||
return (...args: any[]) => {
|
||||
const onError = (cause: any) => {
|
||||
error.cause = cause;
|
||||
if (cause instanceof Error) {
|
||||
error.message += `"${cause.message}"`;
|
||||
} else {
|
||||
error.message = `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
|
||||
}
|
||||
error.message =
|
||||
cause instanceof Error
|
||||
? `The following error occurred in ${hookName}: "${cause.message}"`
|
||||
: `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
|
||||
throw error;
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
const result = fn(...args);
|
||||
if (result instanceof Promise) {
|
||||
if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
|
||||
const fiber = node.fiber;
|
||||
Promise.race([
|
||||
result.catch(() => {}),
|
||||
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
|
||||
]).then((res) => {
|
||||
if (res === TIMEOUT && node.fiber === fiber) {
|
||||
console.warn(timeoutError);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result.catch(onError);
|
||||
}
|
||||
return result;
|
||||
result = fn(...args);
|
||||
} catch (cause) {
|
||||
onError(cause);
|
||||
}
|
||||
if (!(result instanceof Promise)) {
|
||||
return result;
|
||||
}
|
||||
const timeout = HOOK_TIMEOUT[hookName];
|
||||
if (timeout) {
|
||||
const fiber = node.fiber;
|
||||
Promise.race([
|
||||
result.catch(() => {}),
|
||||
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), timeout)),
|
||||
]).then((res) => {
|
||||
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
|
||||
timeoutError.message = `${hookName}'s promise hasn't resolved after ${
|
||||
timeout / 1000
|
||||
} seconds`;
|
||||
console.log(timeoutError);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result.catch(onError);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export class Portal extends Component {
|
||||
type: String,
|
||||
},
|
||||
slots: true,
|
||||
};
|
||||
} as const;
|
||||
|
||||
setup() {
|
||||
const node: any = this.__owl__;
|
||||
|
||||
@@ -20,8 +20,9 @@ type CollectionRawType = "Set" | "Map" | "WeakMap";
|
||||
const objectToString = Object.prototype.toString;
|
||||
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
|
||||
const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
|
||||
// Use arrays because Array.includes is faster than Set.has for small arrays
|
||||
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
|
||||
@@ -45,7 +46,7 @@ function canBeMadeReactive(value: any): boolean {
|
||||
if (typeof value !== "object") {
|
||||
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,
|
||||
@@ -220,7 +221,7 @@ export function reactive<T extends Target>(target: T, callback: Callback = NO_CA
|
||||
const reactivesForTarget = reactiveCache.get(target)!;
|
||||
if (!reactivesForTarget.has(callback)) {
|
||||
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)
|
||||
: basicProxyHandler<T>(callback);
|
||||
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
||||
@@ -249,7 +250,7 @@ function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T
|
||||
set(target, key, value, receiver) {
|
||||
const hadKey = objectHasOwnProperty.call(target, key);
|
||||
const originalValue = Reflect.get(target, key, receiver);
|
||||
const ret = Reflect.set(target, key, value, receiver);
|
||||
const ret = Reflect.set(target, key, toRaw(value), receiver);
|
||||
if (!hadKey && objectHasOwnProperty.call(target, key)) {
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export class Scheduler {
|
||||
frame: number = 0;
|
||||
delayedRenders: Fiber[] = [];
|
||||
cancelledNodes: Set<ComponentNode> = new Set();
|
||||
processing = false;
|
||||
|
||||
constructor() {
|
||||
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
|
||||
@@ -53,6 +54,10 @@ export class Scheduler {
|
||||
}
|
||||
|
||||
processTasks() {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
this.processing = true;
|
||||
this.frame = 0;
|
||||
for (let node of this.cancelledNodes) {
|
||||
node._destroy();
|
||||
@@ -66,6 +71,7 @@ export class Scheduler {
|
||||
this.tasks.delete(task);
|
||||
}
|
||||
}
|
||||
this.processing = false;
|
||||
}
|
||||
|
||||
processFiber(fiber: RootFiber) {
|
||||
|
||||
@@ -4,44 +4,16 @@ import { getCurrent } from "./component_node";
|
||||
import { Portal, portalTemplate } from "./portal";
|
||||
import { helpers } from "./template_helpers";
|
||||
import { OwlError } from "../common/owl_error";
|
||||
import { parseXML } from "../common/utils";
|
||||
|
||||
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 {
|
||||
dev?: boolean;
|
||||
translatableAttributes?: string[];
|
||||
translateFn?: (s: string) => string;
|
||||
templates?: string | Document;
|
||||
templates?: string | Document | Record<string, string>;
|
||||
getTemplate?: (s: string) => Element | Function | string | void;
|
||||
}
|
||||
|
||||
export class TemplateSet {
|
||||
@@ -51,6 +23,7 @@ export class TemplateSet {
|
||||
dev: boolean;
|
||||
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
|
||||
templates: { [name: string]: Template } = {};
|
||||
getRawTemplate?: (s: string) => Element | Function | string | void;
|
||||
translateFn?: (s: string) => string;
|
||||
translatableAttributes?: string[];
|
||||
Portal = Portal;
|
||||
@@ -60,8 +33,15 @@ export class TemplateSet {
|
||||
this.translateFn = config.translateFn;
|
||||
this.translatableAttributes = config.translatableAttributes;
|
||||
if (config.templates) {
|
||||
this.addTemplates(config.templates);
|
||||
if (config.templates instanceof Document || typeof config.templates === "string") {
|
||||
this.addTemplates(config.templates);
|
||||
} else {
|
||||
for (const name in config.templates) {
|
||||
this.addTemplate(name, config.templates[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.getRawTemplate = config.getTemplate;
|
||||
}
|
||||
|
||||
addTemplate(name: string, template: string | Element) {
|
||||
@@ -100,7 +80,7 @@ export class TemplateSet {
|
||||
|
||||
getTemplate(name: string): Template {
|
||||
if (!(name in this.templates)) {
|
||||
const rawTemplate = this.rawTemplates[name];
|
||||
const rawTemplate = this.getRawTemplate?.(name) || this.rawTemplates[name];
|
||||
if (rawTemplate === undefined) {
|
||||
let extraInfo = "";
|
||||
try {
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import { OwlError } from "../common/owl_error";
|
||||
import { toRaw } from "./reactivity";
|
||||
|
||||
type BaseType =
|
||||
| typeof String
|
||||
| typeof Boolean
|
||||
| typeof Number
|
||||
| typeof Date
|
||||
| typeof Object
|
||||
| typeof Array
|
||||
| true
|
||||
| "*";
|
||||
type BaseType = { new (...args: any[]): any } | true | "*";
|
||||
|
||||
interface TypeInfo {
|
||||
type?: TypeDescription;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
export const version = "2.2.6";
|
||||
export const version = "2.4.1";
|
||||
|
||||
@@ -32,7 +32,7 @@ exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately on destroy 2`] = `
|
||||
exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately on destroy 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -43,6 +43,33 @@ exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately
|
||||
}"
|
||||
`;
|
||||
|
||||
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`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
@@ -57,6 +84,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`] = `
|
||||
"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 {
|
||||
makeTestFixture,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
useLogLifecycle,
|
||||
makeDeferred,
|
||||
nextMicroTick,
|
||||
steps,
|
||||
} from "../helpers";
|
||||
|
||||
let fixture: HTMLElement;
|
||||
@@ -123,23 +124,81 @@ describe("app", () => {
|
||||
|
||||
const app = new App(A);
|
||||
const comp = await app.mount(fixture);
|
||||
expect(["A:setup", "A:willStart", "A:willRender", "A:rendered", "A:mounted"]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A:setup",
|
||||
"A:willStart",
|
||||
"A:willRender",
|
||||
"A:rendered",
|
||||
"A:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
comp.state.value = true;
|
||||
await nextTick();
|
||||
expect(["A:willRender", "B:setup", "B:willStart", "A:rendered"]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A:willRender",
|
||||
"B:setup",
|
||||
"B:willStart",
|
||||
"A:rendered",
|
||||
]
|
||||
`);
|
||||
|
||||
// rerender to force the instantiation of a new B component (and cancelling the first)
|
||||
comp.render();
|
||||
await nextMicroTick();
|
||||
expect(["A:willRender", "B:setup", "B:willStart", "A:rendered"]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A:willRender",
|
||||
"B:setup",
|
||||
"B:willStart",
|
||||
"A:rendered",
|
||||
]
|
||||
`);
|
||||
|
||||
app.destroy();
|
||||
expect([
|
||||
"A:willUnmount",
|
||||
"B:willDestroy",
|
||||
"A:willDestroy",
|
||||
"B:willDestroy", // make sure the 2 B instances have been destroyed synchronously
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A:willUnmount",
|
||||
"B:willDestroy",
|
||||
"A:willDestroy",
|
||||
"B:willDestroy",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
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>");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
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`] = `
|
||||
"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`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -1,5 +1,41 @@
|
||||
// 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`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -1,5 +1,50 @@
|
||||
// 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`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -26,4 +26,19 @@ describe("comments", () => {
|
||||
</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(
|
||||
"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", () => {
|
||||
// e is added to localvars in inline_expression but not removed after the arrow func body
|
||||
|
||||
@@ -154,4 +154,19 @@ describe("simple templates, mostly static", () => {
|
||||
</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}");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,4 +121,19 @@ describe("t-esc", () => {
|
||||
mount(bdom, fixture);
|
||||
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");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const template = `
|
||||
<t>
|
||||
|
||||
@@ -78,4 +78,62 @@ describe("loading templates", () => {
|
||||
context.addTemplates(xml);
|
||||
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>");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("basic validation", () => {
|
||||
test("compilation error", () => {
|
||||
const template = `<div t-att-class="a b">test</div>`;
|
||||
expect(() => renderToString(template))
|
||||
.toThrow(`Failed to compile anonymous template: Unexpected identifier
|
||||
.toThrow(`Failed to compile anonymous template: Unexpected identifier 'ctx'
|
||||
|
||||
generated code:
|
||||
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`] = `
|
||||
"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`] = `
|
||||
"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`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -184,7 +184,7 @@ exports[`changing state before first render does not trigger a render (with pare
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`changing state before first render does not trigger a render (with parent) 2`] = `
|
||||
exports[`changing state before first render does not trigger a render (with parent) 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -254,7 +254,7 @@ exports[`components are not destroyed between animation frame 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`components are not destroyed between animation frame 2`] = `
|
||||
exports[`components are not destroyed between animation frame 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -268,7 +268,7 @@ exports[`components are not destroyed between animation frame 2`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`components are not destroyed between animation frame 3`] = `
|
||||
exports[`components are not destroyed between animation frame 5`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -748,7 +748,7 @@ exports[`concurrent renderings scenario 10 2`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`concurrent renderings scenario 10 3`] = `
|
||||
exports[`concurrent renderings scenario 10 4`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -993,7 +993,7 @@ exports[`concurrent renderings scenario 16 3`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`concurrent renderings scenario 16 4`] = `
|
||||
exports[`concurrent renderings scenario 16 6`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -1024,7 +1024,7 @@ exports[`creating two async components, scenario 1 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`creating two async components, scenario 1 2`] = `
|
||||
exports[`creating two async components, scenario 1 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -1038,7 +1038,7 @@ exports[`creating two async components, scenario 1 2`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`creating two async components, scenario 1 3`] = `
|
||||
exports[`creating two async components, scenario 1 5`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -1085,7 +1085,7 @@ exports[`creating two async components, scenario 2 2`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`creating two async components, scenario 2 3`] = `
|
||||
exports[`creating two async components, scenario 2 5`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -1133,7 +1133,7 @@ exports[`creating two async components, scenario 3 (patching in the same frame)
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`creating two async components, scenario 3 (patching in the same frame) 3`] = `
|
||||
exports[`creating two async components, scenario 3 (patching in the same frame) 5`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -1308,7 +1308,7 @@ exports[`delayed render does not go through when t-component value changed 2`] =
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`delayed render does not go through when t-component value changed 3`] = `
|
||||
exports[`delayed render does not go through when t-component value changed 4`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -1617,7 +1617,7 @@ exports[`destroyed component causes other soon to be destroyed component to rere
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 2`] = `
|
||||
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -1628,7 +1628,7 @@ exports[`destroyed component causes other soon to be destroyed component to rere
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 3`] = `
|
||||
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 4`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -1656,7 +1656,7 @@ exports[`destroying/recreating a subcomponent, other scenario 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`destroying/recreating a subcomponent, other scenario 2`] = `
|
||||
exports[`destroying/recreating a subcomponent, other scenario 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -1685,7 +1685,7 @@ exports[`destroying/recreating a subwidget with different props (if start is not
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`destroying/recreating a subwidget with different props (if start is not over) 2`] = `
|
||||
exports[`destroying/recreating a subwidget with different props (if start is not over) 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -1787,7 +1787,7 @@ exports[`rendering component again in next microtick 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`rendering component again in next microtick 2`] = `
|
||||
exports[`rendering component again in next microtick 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
@@ -241,7 +241,7 @@ exports[`can catch errors an error in onWillDestroy, variation 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can catch errors an error in onWillDestroy, variation 2`] = `
|
||||
exports[`can catch errors an error in onWillDestroy, variation 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
@@ -92,7 +92,7 @@ exports[`lifecycle hooks component semantics 5`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks component semantics 6`] = `
|
||||
exports[`lifecycle hooks component semantics 7`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -185,7 +185,7 @@ exports[`lifecycle hooks destroy new children before being mountged 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks destroy new children before being mountged 2`] = `
|
||||
exports[`lifecycle hooks destroy new children before being mountged 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -291,7 +291,7 @@ exports[`lifecycle hooks lifecycle semantics, part 2 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks lifecycle semantics, part 2 2`] = `
|
||||
exports[`lifecycle hooks lifecycle semantics, part 2 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -303,7 +303,7 @@ exports[`lifecycle hooks lifecycle semantics, part 2 2`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks lifecycle semantics, part 2 3`] = `
|
||||
exports[`lifecycle hooks lifecycle semantics, part 2 4`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -348,7 +348,7 @@ exports[`lifecycle hooks lifecycle semantics, part 4 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks lifecycle semantics, part 4 2`] = `
|
||||
exports[`lifecycle hooks lifecycle semantics, part 4 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -360,7 +360,7 @@ exports[`lifecycle hooks lifecycle semantics, part 4 2`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks lifecycle semantics, part 4 3`] = `
|
||||
exports[`lifecycle hooks lifecycle semantics, part 4 4`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -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
|
||||
) {
|
||||
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
|
||||
) {
|
||||
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
|
||||
) {
|
||||
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`] = `
|
||||
"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`] = `
|
||||
"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`] = `
|
||||
"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`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
// 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`] = `
|
||||
"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`] = `
|
||||
"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`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -180,7 +180,7 @@ exports[`t-component switching dynamic component 2`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-component switching dynamic component 3`] = `
|
||||
exports[`t-component switching dynamic component 4`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
@@ -44,7 +44,26 @@ exports[`t-model directive .trim modifier 1`] = `
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
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 = \\"\\") {
|
||||
const bExpr1 = ctx['state'];
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
nextAppError,
|
||||
nextTick,
|
||||
snapshotEverything,
|
||||
steps,
|
||||
useLogLifecycle,
|
||||
} from "../helpers";
|
||||
import { markup } from "../../src/runtime/utils";
|
||||
@@ -868,19 +869,26 @@ describe("basics", () => {
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
|
||||
expect([
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
parent.ifVar = false;
|
||||
parent.render();
|
||||
await nextTick();
|
||||
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(0);
|
||||
expect(["Child:willUnmount", "Child:willDestroy"]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Child:willUnmount",
|
||||
"Child:willDestroy",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("component children doesn't leak (t-key case)", async () => {
|
||||
@@ -899,27 +907,31 @@ describe("basics", () => {
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
|
||||
expect([
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
parent.keyVar = 2;
|
||||
parent.render();
|
||||
await nextTick();
|
||||
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
|
||||
expect([
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willUnmount",
|
||||
"Child:willDestroy",
|
||||
"Child:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willUnmount",
|
||||
"Child:willDestroy",
|
||||
"Child:mounted",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("GrandChild display is controlled by its GrandParent", async () => {
|
||||
@@ -943,20 +955,27 @@ describe("basics", () => {
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
expect([
|
||||
"GrandChild:setup",
|
||||
"GrandChild:willStart",
|
||||
"GrandChild:willRender",
|
||||
"GrandChild:rendered",
|
||||
"GrandChild:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"GrandChild:setup",
|
||||
"GrandChild:willStart",
|
||||
"GrandChild:willRender",
|
||||
"GrandChild:rendered",
|
||||
"GrandChild:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
parent.displayGrandChild = false;
|
||||
parent.render();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
|
||||
expect(["GrandChild:willUnmount", "GrandChild:willDestroy"]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"GrandChild:willUnmount",
|
||||
"GrandChild:willDestroy",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
snapshotEverything,
|
||||
useLogLifecycle,
|
||||
nextAppError,
|
||||
steps,
|
||||
} from "../helpers";
|
||||
import { OwlError } from "../../src/common/owl_error";
|
||||
|
||||
@@ -156,7 +157,7 @@ describe("basics", () => {
|
||||
} catch (e) {
|
||||
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:
|
||||
function(app, bdom, helpers) {
|
||||
@@ -181,7 +182,7 @@ function(app, bdom, helpers) {
|
||||
static components = { 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:
|
||||
function(app, bdom, helpers) {
|
||||
@@ -948,26 +949,28 @@ describe("can catch errors", () => {
|
||||
}
|
||||
await mount(Root, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||
expect([
|
||||
"Root:setup",
|
||||
"Root:willStart",
|
||||
"Root:willRender",
|
||||
"ErrorBoundary:setup",
|
||||
"ErrorBoundary:willStart",
|
||||
"Root:rendered",
|
||||
"ErrorBoundary:willRender",
|
||||
"ErrorComponent:setup",
|
||||
"ErrorComponent:willStart",
|
||||
"ErrorBoundary:rendered",
|
||||
"ErrorComponent:willRender",
|
||||
"ErrorComponent:rendered",
|
||||
"ErrorComponent:mounted",
|
||||
"boom",
|
||||
"ErrorBoundary:willRender",
|
||||
"ErrorBoundary:rendered",
|
||||
"ErrorBoundary:mounted",
|
||||
"Root:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Root:setup",
|
||||
"Root:willStart",
|
||||
"Root:willRender",
|
||||
"ErrorBoundary:setup",
|
||||
"ErrorBoundary:willStart",
|
||||
"Root:rendered",
|
||||
"ErrorBoundary:willRender",
|
||||
"ErrorComponent:setup",
|
||||
"ErrorComponent:willStart",
|
||||
"ErrorBoundary:rendered",
|
||||
"ErrorComponent:willRender",
|
||||
"ErrorComponent:rendered",
|
||||
"ErrorComponent:mounted",
|
||||
"boom",
|
||||
"ErrorBoundary:willRender",
|
||||
"ErrorBoundary:rendered",
|
||||
"ErrorBoundary:mounted",
|
||||
"Root:mounted",
|
||||
]
|
||||
`);
|
||||
expect(mockConsoleError).toBeCalledTimes(0);
|
||||
expect(mockConsoleWarn).toBeCalledTimes(0);
|
||||
});
|
||||
@@ -998,21 +1001,23 @@ describe("can catch errors", () => {
|
||||
}
|
||||
await mount(Root, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>Error handled</div>");
|
||||
expect([
|
||||
"Root:setup",
|
||||
"Root:willStart",
|
||||
"Root:willRender",
|
||||
"ErrorComponent:setup",
|
||||
"ErrorComponent:willStart",
|
||||
"Root:rendered",
|
||||
"ErrorComponent:willRender",
|
||||
"ErrorComponent:rendered",
|
||||
"ErrorComponent:mounted",
|
||||
"boom",
|
||||
"Root:willRender",
|
||||
"Root:rendered",
|
||||
"Root:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Root:setup",
|
||||
"Root:willStart",
|
||||
"Root:willRender",
|
||||
"ErrorComponent:setup",
|
||||
"ErrorComponent:willStart",
|
||||
"Root:rendered",
|
||||
"ErrorComponent:willRender",
|
||||
"ErrorComponent:rendered",
|
||||
"ErrorComponent:mounted",
|
||||
"boom",
|
||||
"Root:willRender",
|
||||
"Root:rendered",
|
||||
"Root:mounted",
|
||||
]
|
||||
`);
|
||||
expect(mockConsoleError).toBeCalledTimes(0);
|
||||
expect(mockConsoleWarn).toBeCalledTimes(0);
|
||||
});
|
||||
@@ -1059,31 +1064,33 @@ describe("can catch errors", () => {
|
||||
}
|
||||
await mount(A, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||
expect([
|
||||
"A:setup",
|
||||
"A:willStart",
|
||||
"A:willRender",
|
||||
"B:setup",
|
||||
"B:willStart",
|
||||
"A:rendered",
|
||||
"B:willRender",
|
||||
"C:setup",
|
||||
"C:willStart",
|
||||
"B:rendered",
|
||||
"C:willRender",
|
||||
"Boom:setup",
|
||||
"Boom:willStart",
|
||||
"C:rendered",
|
||||
"Boom:willRender",
|
||||
"Boom:rendered",
|
||||
"Boom:mounted",
|
||||
"boom",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"C:mounted",
|
||||
"B:mounted",
|
||||
"A:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A:setup",
|
||||
"A:willStart",
|
||||
"A:willRender",
|
||||
"B:setup",
|
||||
"B:willStart",
|
||||
"A:rendered",
|
||||
"B:willRender",
|
||||
"C:setup",
|
||||
"C:willStart",
|
||||
"B:rendered",
|
||||
"C:willRender",
|
||||
"Boom:setup",
|
||||
"Boom:willStart",
|
||||
"C:rendered",
|
||||
"Boom:willRender",
|
||||
"Boom:rendered",
|
||||
"Boom:mounted",
|
||||
"boom",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"C:mounted",
|
||||
"B:mounted",
|
||||
"A:mounted",
|
||||
]
|
||||
`);
|
||||
expect(mockConsoleError).toBeCalledTimes(0);
|
||||
expect(mockConsoleWarn).toBeCalledTimes(0);
|
||||
});
|
||||
@@ -1130,31 +1137,33 @@ describe("can catch errors", () => {
|
||||
}
|
||||
await mount(Root, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>OK<div>Error handled</div></div>");
|
||||
expect([
|
||||
"Root:setup",
|
||||
"Root:willStart",
|
||||
"Root:willRender",
|
||||
"OK:setup",
|
||||
"OK:willStart",
|
||||
"ErrorBoundary:setup",
|
||||
"ErrorBoundary:willStart",
|
||||
"Root:rendered",
|
||||
"OK:willRender",
|
||||
"OK:rendered",
|
||||
"ErrorBoundary:willRender",
|
||||
"ErrorComponent:setup",
|
||||
"ErrorComponent:willStart",
|
||||
"ErrorBoundary:rendered",
|
||||
"ErrorComponent:willRender",
|
||||
"ErrorComponent:rendered",
|
||||
"ErrorComponent:mounted",
|
||||
"boom",
|
||||
"ErrorBoundary:willRender",
|
||||
"ErrorBoundary:rendered",
|
||||
"ErrorBoundary:mounted",
|
||||
"OK:mounted",
|
||||
"Root:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Root:setup",
|
||||
"Root:willStart",
|
||||
"Root:willRender",
|
||||
"OK:setup",
|
||||
"OK:willStart",
|
||||
"ErrorBoundary:setup",
|
||||
"ErrorBoundary:willStart",
|
||||
"Root:rendered",
|
||||
"OK:willRender",
|
||||
"OK:rendered",
|
||||
"ErrorBoundary:willRender",
|
||||
"ErrorComponent:setup",
|
||||
"ErrorComponent:willStart",
|
||||
"ErrorBoundary:rendered",
|
||||
"ErrorComponent:willRender",
|
||||
"ErrorComponent:rendered",
|
||||
"ErrorComponent:mounted",
|
||||
"boom",
|
||||
"ErrorBoundary:willRender",
|
||||
"ErrorBoundary:rendered",
|
||||
"ErrorBoundary:mounted",
|
||||
"OK:mounted",
|
||||
"Root:mounted",
|
||||
]
|
||||
`);
|
||||
expect(mockConsoleError).toBeCalledTimes(0);
|
||||
expect(mockConsoleWarn).toBeCalledTimes(0);
|
||||
});
|
||||
@@ -1481,35 +1490,39 @@ describe("can catch errors", () => {
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("1<div>abc</div>");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
parent.state.hasChild = false;
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Child:willUnmount",
|
||||
"Child:willDestroy",
|
||||
"Parent:patched",
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Child:willUnmount",
|
||||
"Child:willDestroy",
|
||||
"Parent:patched",
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]
|
||||
`);
|
||||
expect(fixture.innerHTML).toBe("2");
|
||||
});
|
||||
|
||||
@@ -1542,13 +1555,15 @@ describe("can catch errors", () => {
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("1");
|
||||
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
parent.state.hasChild = true;
|
||||
await nextMicroTick();
|
||||
@@ -1556,26 +1571,35 @@ describe("can catch errors", () => {
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
]
|
||||
`);
|
||||
parent.state.hasChild = false;
|
||||
await nextTick();
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Child:willDestroy",
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Child:willDestroy",
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
]
|
||||
`);
|
||||
expect(fixture.innerHTML).toBe("1");
|
||||
await nextTick();
|
||||
expect(["Parent:willPatch", "Parent:patched"]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]
|
||||
`);
|
||||
expect(fixture.innerHTML).toBe("2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -641,6 +641,56 @@ describe("hooks", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("effect types are inferred from dependencies", async () => {
|
||||
// @ts-ignore (declared but never used)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
class MyComponent extends Component {
|
||||
static template = xml`<div/>`;
|
||||
|
||||
setup() {
|
||||
useEffect(
|
||||
(a, b) => {
|
||||
expectType<number>(a);
|
||||
expectType<string>(b);
|
||||
},
|
||||
() => [3, "hello"]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("effect type allows an effect with partial dependencies parameters", async () => {
|
||||
// @ts-ignore (declared but never used)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
class MyComponent extends Component {
|
||||
static template = xml`<div/>`;
|
||||
|
||||
setup() {
|
||||
useEffect(
|
||||
(a) => {
|
||||
expectType<number>(a);
|
||||
},
|
||||
() => [3, "hello"]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("effect type allows an effect with no dependency parameter", async () => {
|
||||
// @ts-ignore (declared but never used)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
class MyComponent extends Component {
|
||||
static template = xml`<div/>`;
|
||||
|
||||
setup() {
|
||||
useEffect(
|
||||
() => {},
|
||||
() => [3, "hello"]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("properly behaves when the effect function throws", async () => {
|
||||
let originalconsoleError = console.error;
|
||||
let originalconsoleWarn = console.warn;
|
||||
@@ -673,3 +723,5 @@ describe("hooks", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expectType<T>(t: T) {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, mount, onWillUpdateProps, useState, xml } from "../../src";
|
||||
import { makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
|
||||
import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers";
|
||||
|
||||
let fixture: HTMLElement;
|
||||
|
||||
@@ -272,29 +272,61 @@ test("bound functions are considered 'alike'", async () => {
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("1child");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
parent.state.val = 3;
|
||||
await nextTick();
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]
|
||||
`);
|
||||
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 () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-esc="props.val"/>`;
|
||||
@@ -330,29 +362,33 @@ test(".alike suffix in a simple case", async () => {
|
||||
}
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
expect(fixture.innerHTML).toBe("01");
|
||||
parent.state.counter++;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("11");
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test(".alike suffix in a list", async () => {
|
||||
@@ -388,36 +424,40 @@ test(".alike suffix in a list", async () => {
|
||||
}
|
||||
|
||||
await mount(Parent, fixture);
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Todo:setup",
|
||||
"Todo:willStart",
|
||||
"Todo:setup",
|
||||
"Todo:willStart",
|
||||
"Parent:rendered",
|
||||
"Todo:willRender",
|
||||
"Todo:rendered",
|
||||
"Todo:willRender",
|
||||
"Todo:rendered",
|
||||
"Todo:mounted",
|
||||
"Todo:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Todo:setup",
|
||||
"Todo:willStart",
|
||||
"Todo:setup",
|
||||
"Todo:willStart",
|
||||
"Parent:rendered",
|
||||
"Todo:willRender",
|
||||
"Todo:rendered",
|
||||
"Todo:willRender",
|
||||
"Todo:rendered",
|
||||
"Todo:mounted",
|
||||
"Todo:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
expect(fixture.innerHTML).toBe("<button>1</button><button>2V</button>");
|
||||
fixture.querySelector("button")?.click();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<button>1V</button><button>2V</button>");
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Todo:willRender",
|
||||
"Todo:rendered",
|
||||
"Todo:willPatch",
|
||||
"Todo:patched",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Todo:willRender",
|
||||
"Todo:rendered",
|
||||
"Todo:willPatch",
|
||||
"Todo:patched",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -594,7 +594,7 @@ describe("props validation", () => {
|
||||
|
||||
test("props: can be defined with a boolean", async () => {
|
||||
class SubComp extends Component {
|
||||
static props = { message: true };
|
||||
static props = { message: true } as const;
|
||||
}
|
||||
expect(() => {
|
||||
validateProps(SubComp as any, {});
|
||||
@@ -636,7 +636,7 @@ describe("props validation", () => {
|
||||
|
||||
test("props: extra props cause an error, part 2", async () => {
|
||||
class SubComp extends Component {
|
||||
static props = { message: true };
|
||||
static props = { message: true } as const;
|
||||
}
|
||||
expect(() => {
|
||||
validateProps(SubComp as any, { message: 1, flag: true });
|
||||
@@ -829,6 +829,52 @@ describe("props validation", () => {
|
||||
expect(error!).toBeDefined();
|
||||
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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
xml,
|
||||
toRaw,
|
||||
} from "../../src";
|
||||
import { makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
|
||||
import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers";
|
||||
|
||||
let fixture: HTMLElement;
|
||||
|
||||
@@ -175,30 +175,34 @@ describe("reactivity in lifecycle", () => {
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("2");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
parent.state.content = null;
|
||||
parent.state.renderChild = false;
|
||||
await nextTick();
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Child:willUnmount",
|
||||
"Child:willDestroy",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Child:willUnmount",
|
||||
"Child:willDestroy",
|
||||
"Parent:patched",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("Component is automatically subscribed to reactive object received as prop", async () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useLogLifecycle,
|
||||
makeDeferred,
|
||||
nextMicroTick,
|
||||
steps,
|
||||
} from "../helpers";
|
||||
|
||||
let fixture: HTMLElement;
|
||||
@@ -41,28 +42,32 @@ describe("rendering semantics", () => {
|
||||
const parent = await mount(Parent, fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("Achild");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
parent.state.value = "B";
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("Bchild");
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("can force a render to update sub tree", async () => {
|
||||
@@ -167,18 +172,20 @@ describe("rendering semantics", () => {
|
||||
const parent = await mount(Parent, fixture, { env });
|
||||
|
||||
expect(fixture.innerHTML).toBe("parentAchild3");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
value = 4;
|
||||
parent.render(true);
|
||||
@@ -187,30 +194,34 @@ describe("rendering semantics", () => {
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Child:willUpdateProps",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willRender",
|
||||
"Child:willUpdateProps",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
]
|
||||
`);
|
||||
|
||||
parent.state.value = "B";
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(fixture.innerHTML).toBe("parentBchild4");
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Child:willUpdateProps",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Parent:willPatch",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willRender",
|
||||
"Child:willUpdateProps",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Parent:willPatch",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
"Parent:patched",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("props are reactive", async () => {
|
||||
@@ -235,23 +246,32 @@ describe("rendering semantics", () => {
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("1");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
parent.state.b = 3;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("3");
|
||||
expect(["Child:willRender", "Child:rendered", "Child:willPatch", "Child:patched"]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("props are reactive (nested prop)", async () => {
|
||||
@@ -278,37 +298,48 @@ describe("rendering semantics", () => {
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("1");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
parent.state.b.c = 3; // parent is now subscribed to 'b' key
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("3");
|
||||
expect(["Child:willRender", "Child:rendered", "Child:willPatch", "Child:patched"]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
]
|
||||
`);
|
||||
|
||||
parent.state.b = { c: 444 }; // triggers a parent and a child render
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("444");
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("works as expected for dynamic number of props", async () => {
|
||||
@@ -366,41 +397,45 @@ describe("rendering semantics", () => {
|
||||
|
||||
const parent = await mount(A, fixture);
|
||||
expect(fixture.innerHTML).toBe("11");
|
||||
expect([
|
||||
"A:setup",
|
||||
"A:willStart",
|
||||
"A:willRender",
|
||||
"B:setup",
|
||||
"B:willStart",
|
||||
"A:rendered",
|
||||
"B:willRender",
|
||||
"C:setup",
|
||||
"C:willStart",
|
||||
"B:rendered",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"C:mounted",
|
||||
"B:mounted",
|
||||
"A:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A:setup",
|
||||
"A:willStart",
|
||||
"A:willRender",
|
||||
"B:setup",
|
||||
"B:willStart",
|
||||
"A:rendered",
|
||||
"B:willRender",
|
||||
"C:setup",
|
||||
"C:willStart",
|
||||
"B:rendered",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"C:mounted",
|
||||
"B:mounted",
|
||||
"A:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
parent.state.obj.val = 3;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("33");
|
||||
expect([
|
||||
"A:willRender",
|
||||
"A:rendered",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"A:willPatch",
|
||||
"A:patched",
|
||||
"C:willPatch",
|
||||
"C:patched",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A:willRender",
|
||||
"A:rendered",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"A:willPatch",
|
||||
"A:patched",
|
||||
"C:willPatch",
|
||||
"C:patched",
|
||||
]
|
||||
`);
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
expect([]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -431,51 +466,67 @@ test("force render in case of existing render", async () => {
|
||||
}
|
||||
const parent = await mount(A, fixture);
|
||||
expect(fixture.innerHTML).toBe("C1");
|
||||
expect([
|
||||
"A:setup",
|
||||
"A:willStart",
|
||||
"A:willRender",
|
||||
"B:setup",
|
||||
"B:willStart",
|
||||
"A:rendered",
|
||||
"B:willRender",
|
||||
"C:setup",
|
||||
"C:willStart",
|
||||
"B:rendered",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"C:mounted",
|
||||
"B:mounted",
|
||||
"A:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A:setup",
|
||||
"A:willStart",
|
||||
"A:willRender",
|
||||
"B:setup",
|
||||
"B:willStart",
|
||||
"A:rendered",
|
||||
"B:willRender",
|
||||
"C:setup",
|
||||
"C:willStart",
|
||||
"B:rendered",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"C:mounted",
|
||||
"B:mounted",
|
||||
"A:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
// trigger a new rendering, blocked in B
|
||||
parent.state.val = 2;
|
||||
await nextTick();
|
||||
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A:willRender",
|
||||
"B:willUpdateProps",
|
||||
"A:rendered",
|
||||
]
|
||||
`);
|
||||
|
||||
// initiate a new render with deep=true. it should cancel the current render
|
||||
// and also be blocked in B
|
||||
parent.render(true);
|
||||
await nextTick();
|
||||
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A:willRender",
|
||||
"B:willUpdateProps",
|
||||
"A:rendered",
|
||||
]
|
||||
`);
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
// we check here that the render reaches C (so, that it was properly forced)
|
||||
expect([
|
||||
"B:willRender",
|
||||
"C:willUpdateProps",
|
||||
"B:rendered",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"A:willPatch",
|
||||
"B:willPatch",
|
||||
"C:willPatch",
|
||||
"C:patched",
|
||||
"B:patched",
|
||||
"A:patched",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"B:willRender",
|
||||
"C:willUpdateProps",
|
||||
"B:rendered",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"A:willPatch",
|
||||
"B:willPatch",
|
||||
"C:willPatch",
|
||||
"C:patched",
|
||||
"B:patched",
|
||||
"A:patched",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("children, default props and renderings", async () => {
|
||||
@@ -503,26 +554,30 @@ test("children, default props and renderings", async () => {
|
||||
const parent = await mount(Parent, fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("Achild");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
parent.state.value = "B";
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("Bchild");
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -179,6 +179,34 @@ describe("slots", () => {
|
||||
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 () => {
|
||||
let child: any;
|
||||
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>");
|
||||
});
|
||||
|
||||
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 () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<t t-slot="default"/>`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, mount, useState, xml } from "../../src";
|
||||
import { makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
|
||||
import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers";
|
||||
|
||||
let fixture: HTMLElement;
|
||||
|
||||
@@ -29,18 +29,20 @@ describe("t-component", () => {
|
||||
await mount(Parent, fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div>child</div>");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("switching dynamic component", async () => {
|
||||
@@ -68,36 +70,40 @@ describe("t-component", () => {
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>child a</div>");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"ChildA:setup",
|
||||
"ChildA:willStart",
|
||||
"Parent:rendered",
|
||||
"ChildA:willRender",
|
||||
"ChildA:rendered",
|
||||
"ChildA:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"ChildA:setup",
|
||||
"ChildA:willStart",
|
||||
"Parent:rendered",
|
||||
"ChildA:willRender",
|
||||
"ChildA:rendered",
|
||||
"ChildA:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
parent.Child = ChildB;
|
||||
parent.render();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("child b");
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"ChildB:setup",
|
||||
"ChildB:willStart",
|
||||
"Parent:rendered",
|
||||
"ChildB:willRender",
|
||||
"ChildB:rendered",
|
||||
"Parent:willPatch",
|
||||
"ChildA:willUnmount",
|
||||
"ChildA:willDestroy",
|
||||
"ChildB:mounted",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willRender",
|
||||
"ChildB:setup",
|
||||
"ChildB:willStart",
|
||||
"Parent:rendered",
|
||||
"ChildB:willRender",
|
||||
"ChildB:rendered",
|
||||
"Parent:willPatch",
|
||||
"ChildA:willUnmount",
|
||||
"ChildA:willDestroy",
|
||||
"ChildB:mounted",
|
||||
"Parent:patched",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("can switch between dynamic components without the need for a t-key", async () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
nextAppError,
|
||||
nextTick,
|
||||
snapshotEverything,
|
||||
steps,
|
||||
useLogLifecycle,
|
||||
} from "../helpers";
|
||||
|
||||
@@ -91,23 +92,25 @@ describe("list of components", () => {
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><ul><li><div>1</div></li><li><div>2</div></li></ul></div>"
|
||||
);
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("reconciliation alg works for t-foreach in t-foreach", async () => {
|
||||
|
||||
@@ -331,6 +331,32 @@ describe("t-model directive", () => {
|
||||
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 () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
|
||||
@@ -137,7 +137,7 @@ export function snapshotEverything() {
|
||||
};
|
||||
}
|
||||
|
||||
const steps: string[] = [];
|
||||
export const steps: string[] = [];
|
||||
|
||||
export function logStep(step: string) {
|
||||
steps.push(step);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
nextMicroTick,
|
||||
nextTick,
|
||||
snapshotEverything,
|
||||
steps,
|
||||
useLogLifecycle,
|
||||
} from "./helpers";
|
||||
|
||||
@@ -992,32 +993,33 @@ describe("Reactivity", () => {
|
||||
const obj2 = createReactive({ b: {} }, () => n2++);
|
||||
const obj3 = createReactive({ c: {} }, () => n3++);
|
||||
|
||||
// assign the same object should'nt notify reactivity
|
||||
obj2.b = obj2.b;
|
||||
obj2.b;
|
||||
obj3.c = obj3.c;
|
||||
obj3.c;
|
||||
expect(n1).toBe(0);
|
||||
expect(n2).toBe(1);
|
||||
expect(n3).toBe(1);
|
||||
expect(n2).toBe(0);
|
||||
expect(n3).toBe(0);
|
||||
|
||||
obj2.b = obj1;
|
||||
obj2.b;
|
||||
obj3.c = obj1;
|
||||
obj3.c;
|
||||
expect(n1).toBe(0);
|
||||
expect(n2).toBe(2);
|
||||
expect(n3).toBe(2);
|
||||
expect(n2).toBe(1);
|
||||
expect(n3).toBe(1);
|
||||
|
||||
obj1.a = obj1.a + 2;
|
||||
obj1.a;
|
||||
expect(n1).toBe(1);
|
||||
expect(n2).toBe(2);
|
||||
expect(n3).toBe(2);
|
||||
expect(n2).toBe(1);
|
||||
expect(n3).toBe(1);
|
||||
|
||||
obj2.b.a = obj2.b.a + 1;
|
||||
expect(n1).toBe(2);
|
||||
expect(n2).toBe(3);
|
||||
expect(n3).toBe(2);
|
||||
expect(n2).toBe(2);
|
||||
expect(n3).toBe(1);
|
||||
});
|
||||
|
||||
test("reactive inside other: reading the inner reactive from outer doesn't affect the inner's subscriptions", async () => {
|
||||
@@ -1850,37 +1852,41 @@ describe("Reactivity: useState", () => {
|
||||
}
|
||||
}
|
||||
await mount(Parent, fixture);
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
|
||||
testContext.value = 321;
|
||||
await nextTick();
|
||||
expect([
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
]
|
||||
`);
|
||||
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
|
||||
});
|
||||
|
||||
@@ -1904,38 +1910,49 @@ describe("Reactivity: useState", () => {
|
||||
}
|
||||
|
||||
await mount(Parent, fixture);
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
|
||||
testContext.value = 321;
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect([
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
]
|
||||
`);
|
||||
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
|
||||
|
||||
await nextTick();
|
||||
expect(["Child:willPatch", "Child:patched", "Child:willPatch", "Child:patched"]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
]
|
||||
`);
|
||||
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
|
||||
});
|
||||
|
||||
@@ -1969,43 +1986,54 @@ describe("Reactivity: useState", () => {
|
||||
|
||||
await mount(GrandFather, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>123</span><div><span>123</span></div></div>");
|
||||
expect([
|
||||
"GrandFather:setup",
|
||||
"GrandFather:willStart",
|
||||
"GrandFather:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"GrandFather:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
"Child:mounted",
|
||||
"GrandFather:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"GrandFather:setup",
|
||||
"GrandFather:willStart",
|
||||
"GrandFather:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"GrandFather:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
"Child:mounted",
|
||||
"GrandFather:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
testContext.value = 321;
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>123</span><div><span>123</span></div></div>");
|
||||
expect([
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
]
|
||||
`);
|
||||
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>321</span><div><span>321</span></div></div>");
|
||||
expect(["Child:willPatch", "Child:patched", "Child:willPatch", "Child:patched"]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("one components can subscribe twice to same context", async () => {
|
||||
@@ -2181,38 +2209,49 @@ describe("Reactivity: useState", () => {
|
||||
}
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:mounted",
|
||||
"Parent:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
testContext.a = 321;
|
||||
await nextTick();
|
||||
expect(["Child:willRender", "Child:rendered", "Child:willPatch", "Child:patched"]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willPatch",
|
||||
"Child:patched",
|
||||
]
|
||||
`);
|
||||
|
||||
parent.state.flag = false;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Child:willUnmount",
|
||||
"Child:willDestroy",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Child:willUnmount",
|
||||
"Child:willDestroy",
|
||||
"Parent:patched",
|
||||
]
|
||||
`);
|
||||
|
||||
testContext.a = 456;
|
||||
await nextTick();
|
||||
expect([]).toBeLogged();
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
||||
});
|
||||
|
||||
test("destroyed component before being mounted is inactive", async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Owl devtools",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.2",
|
||||
"manifest_version": 3,
|
||||
"description": "Chrome devtools extension for Odoo Owl framework",
|
||||
"icons": {
|
||||
@@ -14,7 +14,7 @@
|
||||
"default_popup": "popup_app/popup.html"
|
||||
},
|
||||
"permissions": ["scripting", "storage"],
|
||||
"host_permissions": ["http://*/*", "https://*/*"],
|
||||
"host_permissions": ["http://*/*", "https://*/*", "file://*"],
|
||||
"content_security_policy": {
|
||||
"script-src": "self",
|
||||
"object-src": "self"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Owl devtools",
|
||||
"version": "1.0",
|
||||
"version": "1.0.0",
|
||||
"description": "Firefox devtools extension for Odoo Owl framework",
|
||||
"manifest_version": 2,
|
||||
"browser_specific_settings": {
|
||||
@@ -29,7 +29,7 @@
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"devtools_page": "devtools_app/devtools.html",
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval' blob:; object-src 'self'",
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { IS_FIREFOX, getActiveTabURL } from "./utils";
|
||||
import { IS_FIREFOX, getActiveTabURL, browserInstance } from "./utils";
|
||||
|
||||
let owlStatus = 0;
|
||||
|
||||
const browserInstance = IS_FIREFOX ? browser : chrome;
|
||||
|
||||
// Used to keep track of the tabs where the owl devtools have been opened
|
||||
const activePanels = new Map();
|
||||
|
||||
@@ -71,6 +69,9 @@ function checkOwlStatus(tabId) {
|
||||
browserInstance.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
|
||||
// Send back the owl status to the sender
|
||||
if (message.type === "getOwlStatus") {
|
||||
if (IS_FIREFOX) {
|
||||
return { result: owlStatus };
|
||||
}
|
||||
sendResponse({ result: owlStatus });
|
||||
return true;
|
||||
} else if (message.type === "owlStatus") {
|
||||
@@ -112,11 +113,10 @@ browserInstance.runtime.onMessage.addListener(async (message, sender, sendRespon
|
||||
}, 750);
|
||||
activePanels.set(message.id, { port: port, expirationTimeout: expirationTimeout });
|
||||
// This is solely for firefox which doesnt allow access to the chrome.tabs api inside devtools
|
||||
// We therefore only use the firefox syntax to send the response here
|
||||
} else if (message.type === "getActiveTabURL") {
|
||||
getActiveTabURL().then((tab) => {
|
||||
sendResponse({ result: tab });
|
||||
});
|
||||
return true;
|
||||
const tab = await getActiveTabURL();
|
||||
return { result: tab };
|
||||
} else {
|
||||
const destinationPanel = activePanels.get(sender.tab.id);
|
||||
if (destinationPanel) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import globalHook from "./page_scripts/owl_devtools_global_hook";
|
||||
import { IS_FIREFOX } from "./utils";
|
||||
import { IS_FIREFOX, browserInstance } from "./utils";
|
||||
|
||||
// Relays the owlDevtools__... type top window messages to the background script so that it can relay it to the devtools app
|
||||
window.addEventListener(
|
||||
@@ -7,7 +7,7 @@ window.addEventListener(
|
||||
function (event) {
|
||||
if (event.data.type && event.data.source === "owl-devtools") {
|
||||
try {
|
||||
chrome.runtime.sendMessage(
|
||||
browserInstance.runtime.sendMessage(
|
||||
event.data.data
|
||||
? { type: event.data.type, data: event.data.data, origin: event.data.origin }
|
||||
: { type: event.data.type, origin: event.data.origin }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { IS_FIREFOX } from "../utils";
|
||||
import { IS_FIREFOX, browserInstance } from "../utils";
|
||||
|
||||
let created = false;
|
||||
let browserInstance = IS_FIREFOX ? browser : chrome;
|
||||
|
||||
// Try to load the owl panel each 1000 ms in case it (re)appears on the page later on
|
||||
const checkInterval = setInterval(createPanelsIfOwl, 1000);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.ComponentSearchBar" owl="1">
|
||||
<div class="pointer-icon ms-1 px-2 py-1" t-on-click.stop='() => this.store.toggleSelector()'>
|
||||
<i title="Select an element in the page to inspect the corresponding component" class="fa fa-mouse-pointer" t-attf-style="color: {{store.componentSearch.activeSelector ? 'var(--active-icon)' : 'var(--text-color)'}};"></i>
|
||||
<div class="mouse-icon p-1" t-on-click.stop='() => this.store.toggleSelector()'>
|
||||
<i title="Select an element in the page to inspect the corresponding component" class="fa fa-fw fa-mouse-pointer" t-attf-style="color: {{store.componentSearch.activeSelector ? 'var(--active-icon)' : 'var(--text-color)'}};"></i>
|
||||
</div>
|
||||
<div class="icons-separator"/>
|
||||
<div class="d-flex align-items-center ms-2 flex-grow-1">
|
||||
|
||||
@@ -15,6 +15,7 @@ export class ComponentsTab extends Component {
|
||||
this.store = useStore();
|
||||
this.flushRendersTimeout = false;
|
||||
useExternalListener(document, "keydown", this.onKeyboardEvent);
|
||||
useExternalListener(window, "resize", this.onWindowResize);
|
||||
|
||||
onWillUnmount(() => {
|
||||
window.removeEventListener("mousemove", this.onMouseMove);
|
||||
@@ -53,9 +54,11 @@ export class ComponentsTab extends Component {
|
||||
|
||||
// Adjust the position of the split between the left and right right window of the components tab
|
||||
onMouseMove = (event) => {
|
||||
const minWidth = (147 / window.innerWidth) * 100;
|
||||
const maxWidth = 100 - (100 / window.innerWidth) * 100;
|
||||
this.store.splitPosition = Math.max(
|
||||
Math.min((event.clientX / window.innerWidth) * 100, 85),
|
||||
15
|
||||
Math.min((event.clientX / window.innerWidth) * 100, maxWidth),
|
||||
minWidth
|
||||
);
|
||||
};
|
||||
|
||||
@@ -64,4 +67,11 @@ export class ComponentsTab extends Component {
|
||||
window.removeEventListener("mousemove", this.onMouseMove);
|
||||
window.removeEventListener("mouseup", this.onMouseUp);
|
||||
};
|
||||
|
||||
onWindowResize = () => {
|
||||
const minWidth = (147 / window.innerWidth) * 100;
|
||||
if (minWidth <= 100) {
|
||||
this.store.splitPosition = Math.max(this.store.splitPosition, minWidth);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.ObjectTreeElement" owl="1">
|
||||
<div class="m-0 p-0 text-nowrap w-100 object-line"
|
||||
t-att-class="props.class"
|
||||
t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)"
|
||||
t-att-class="props.class + (props.object.hasChildren ? ' bg-feedback' : '')"
|
||||
t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)"
|
||||
t-on-contextmenu.prevent="openMenu"
|
||||
>
|
||||
<div t-attf-style="padding-left: {{objectPadding}}rem">
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { isElementInCenterViewport, minimizeKey, IS_FIREFOX } from "../../../../utils";
|
||||
import { isElementInCenterViewport, minimizeKey, browserInstance } from "../../../../utils";
|
||||
import { useStore } from "../../../store/store";
|
||||
import { HighlightText } from "./highlight_text/highlight_text";
|
||||
|
||||
const browserInstance = IS_FIREFOX ? browser : chrome;
|
||||
|
||||
const { Component, useRef, useState, useEffect, onMounted } = owl;
|
||||
|
||||
export class TreeElement extends Component {
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
<option t-att-value="frame"><t t-esc="frame"/></option>
|
||||
</t>
|
||||
</select>
|
||||
<i class="ms-auto p-1 me-1 lg-icon fa fa-question-circle pointer-icon" title="Open devtools doc" t-on-click.stop="() => this.store.openDocumentation()"></i>
|
||||
<i class="p-1 me-1 lg-icon fa pointer-icon" title="Toggle dark mode" t-att-class="{ 'fa-sun-o': store.settings.darkMode, 'fa-moon-o' : !store.settings.darkMode}" t-on-click.stop="() => this.store.toggleDarkMode()"></i>
|
||||
<i class="p-1 me-1 lg-icon fa fa-repeat pointer-icon" title="Refresh extension" t-on-click.stop="() => this.store.refreshExtension()"></i>
|
||||
<i class="ms-auto p-1 me-1 lg-icon fa fa-question-circle navbar-icon" title="Open devtools doc" t-on-click.stop="() => this.store.openDocumentation()"></i>
|
||||
<i class="p-1 me-1 lg-icon fa navbar-icon" title="Toggle dark mode" t-att-class="{ 'fa-sun-o': store.settings.darkMode, 'fa-moon-o' : !store.settings.darkMode}" t-on-click.stop="() => this.store.toggleDarkMode()"></i>
|
||||
<i class="p-1 me-1 lg-icon fa fa-repeat navbar-icon" title="Refresh extension" t-on-click.stop="() => this.store.refreshExtension()"></i>
|
||||
</div>
|
||||
<ComponentsTab t-if="store.page === 'ComponentsTab'"/>
|
||||
<ProfilerTab t-if="store.page === 'ProfilerTab'"/>
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
<t t-name="devtools.ProfilerTab" owl="1">
|
||||
<div class="position-relative overflow-hidden d-flex flex-column h-100">
|
||||
<div class="panel-top d-flex align-items-center">
|
||||
<i title="Start/Stop Recording" class="fa fa-circle pointer-icon ms-1 p-1" t-attf-style="color: {{store.activeRecorder ? 'var(--active-recorder)' : 'var(--text-color)'}};" t-on-click.stop="() => this.store.toggleRecording()" aria-hidden="true"></i>
|
||||
<i title="Clear events" class="fa fa-ban pointer-icon p-1 px-2" t-on-click.stop="() => this.store.clearEventsConsole()" aria-hidden="true"></i>
|
||||
<div class="icons-separator mx-1"/>
|
||||
<select class="form-select form-select-sm custom-select border-0" t-on-change="selectDisplayMode">
|
||||
<i title="Start/Stop Recording" class="fa fa-circle profiler-icon p-2" t-attf-style="color: {{store.activeRecorder ? 'var(--active-recorder)' : 'var(--text-color)'}};" t-on-click.stop="() => this.store.toggleRecording()" aria-hidden="true"></i>
|
||||
<i title="Clear events" class="fa fa-ban profiler-icon p-2" t-on-click.stop="() => this.store.clearEventsConsole()" aria-hidden="true"></i>
|
||||
<div class="icons-separator"/>
|
||||
<select class="form-select form-select-sm custom-select pointer-icon border-0" t-on-change="selectDisplayMode">
|
||||
<option t-att-selected="store.eventsTreeView" value="Tree">Tree view</option>
|
||||
<option t-att-selected="!store.eventsTreeView" value="List">Events log</option>
|
||||
</select>
|
||||
<i title="Collapse All" type="button" class="fa fa-list me-2" t-on-click="() => this.store.collapseAll()" t-attf-style="{{store.eventsTreeView ? '' : 'visibility: hidden;'}}"></i>
|
||||
<i title="Collapse All" class="fa fa-list p-2 profiler-icon" t-on-click="() => this.store.collapseAll()" t-attf-style="{{store.eventsTreeView ? '' : 'display: none;'}}"></i>
|
||||
<div class="icons-separator"/>
|
||||
<label class="mx-2 form-check-label pointer-icon" title="Trace renderings in console">
|
||||
<input type="checkbox" class="form-check-input me-1" t-att-checked="store.traceRenderings" t-on-input="() => this.store.toggleTracing()"/> Trace Renderings
|
||||
<label class="p-1 mx-1 form-check-label pointer-icon" title="Trace renderings in console">
|
||||
<input type="checkbox" class="form-check-input me-1 pointer-icon" t-att-checked="store.traceRenderings" t-on-input="() => this.store.toggleTracing()"/> Trace Renderings
|
||||
</label>
|
||||
<label class="mx-2 form-check-label pointer-icon" title="Trace subscriptions in console (warning: it is VERY verbose)">
|
||||
<input type="checkbox" class="form-check-input me-1" t-att-checked="store.traceSubscriptions" t-on-input="() => this.store.toggleSubscriptionTracing()"/> Trace Subscriptions
|
||||
<label class="p-1 mx-1 form-check-label pointer-icon" title="Trace subscriptions in console (warning: it is VERY verbose)">
|
||||
<input type="checkbox" class="form-check-input me-1 pointer-icon" t-att-checked="store.traceSubscriptions" t-on-input="() => this.store.toggleSubscriptionTracing()"/> Trace Subscriptions
|
||||
</label>
|
||||
<!-- <EventSearchBar/> -->
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const { reactive, useState, toRaw } = owl;
|
||||
import { fuzzySearch, IS_FIREFOX, getActiveTabURL } from "../../utils";
|
||||
import { fuzzySearch, IS_FIREFOX, getActiveTabURL, browserInstance } from "../../utils";
|
||||
import globalHook from "../../page_scripts/owl_devtools_global_hook";
|
||||
|
||||
const browserInstance = IS_FIREFOX ? browser : chrome;
|
||||
|
||||
// Main store which contains all states that needs to be maintained throughout all components in the devtools app
|
||||
export const store = reactive({
|
||||
devtoolsId: 0,
|
||||
@@ -115,6 +113,9 @@ export const store = reactive({
|
||||
}
|
||||
keepEnvLit(details);
|
||||
this.activeComponent = details;
|
||||
if (this.componentSearch.search.length) {
|
||||
this.updateSearch(this.componentSearch.search);
|
||||
}
|
||||
},
|
||||
|
||||
// Select a component by retrieving its details from the page based on its path
|
||||
@@ -146,6 +147,7 @@ export const store = reactive({
|
||||
}
|
||||
component.selected = true;
|
||||
highlightChildren(component);
|
||||
this.highlightComponent(path);
|
||||
const details = await evalFunctionInWindow(
|
||||
"getComponentDetails",
|
||||
[component.path],
|
||||
@@ -346,7 +348,7 @@ export const store = reactive({
|
||||
|
||||
// Expand the children of the input object property and load it from page if necessary
|
||||
async toggleObjectTreeElementsDisplay(obj) {
|
||||
if (!obj.hasChildren) {
|
||||
if (!obj.hasChildren || window.getSelection().toString().length) {
|
||||
return;
|
||||
}
|
||||
// Since it is sometimes impossible (and always ineffective) to load all descendants of a property
|
||||
@@ -939,10 +941,9 @@ function arraysEqual(arr1, arr2) {
|
||||
|
||||
async function getTabURL() {
|
||||
if (IS_FIREFOX) {
|
||||
// This happens in firefox when the method is called inside devtools so we ask the background to execute it instead
|
||||
browserInstance.runtime.sendMessage({ type: "getActiveTabURL" }).then((response) => {
|
||||
return response.result;
|
||||
});
|
||||
// It is not possible to run getActiveTabURL inside the devtools when using firefox so we ask the background to execute it instead
|
||||
const response = await browserInstance.runtime.sendMessage({ type: "getActiveTabURL" });
|
||||
return response.result;
|
||||
} else {
|
||||
return await getActiveTabURL();
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
--text-selected: white;
|
||||
--menu-highlight-bg: rgb(201, 201, 201);
|
||||
--version-bg: teal;
|
||||
--hover-bg: #ebebeb;
|
||||
--navbar-hover-bg: #d3d3d3;
|
||||
/* to change the color here, put it in stroke='%23[color in hexadecimal]' */
|
||||
--select-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23444444' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
|
||||
}
|
||||
@@ -52,6 +54,8 @@
|
||||
--text-selected: #c9c9c9;
|
||||
--menu-highlight-bg: rgb(65, 65, 65);
|
||||
--version-bg: #805900ad;
|
||||
--hover-bg: #575757;
|
||||
--navbar-hover-bg: #6b6b6b;
|
||||
color-scheme: dark;
|
||||
/* to change the color here, put it in stroke='%23[color in hexadecimal]' */
|
||||
--select-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23c9c9c9' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
|
||||
@@ -82,6 +86,10 @@
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.navbar-btn {
|
||||
height: 23px;
|
||||
padding: 0.5rem;
|
||||
@@ -244,7 +252,7 @@
|
||||
.search-input {
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
padding: 0.4rem 0rem;
|
||||
padding: 0.37rem 0rem;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
@@ -255,6 +263,35 @@
|
||||
.utility-icon {
|
||||
cursor: pointer;
|
||||
font-size: 1.2em;
|
||||
border-radius: 35%;
|
||||
}
|
||||
|
||||
.mouse-icon {
|
||||
border-radius: 35%;
|
||||
cursor: pointer;
|
||||
padding-left: 0.4rem !important;
|
||||
}
|
||||
|
||||
.profiler-icon {
|
||||
border-radius: 35%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.utility-icon:hover, .mouse-icon:hover, .profiler-icon:hover, .bg-feedback:hover {
|
||||
background-color: var(--hover-bg);
|
||||
}
|
||||
|
||||
.navbar-icon {
|
||||
border-radius: 35%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navbar-icon:hover {
|
||||
background-color: var(--navbar-hover-bg);
|
||||
}
|
||||
|
||||
.bg-feedback {
|
||||
border-radius: 5%;
|
||||
}
|
||||
|
||||
.lg-icon {
|
||||
@@ -281,7 +318,7 @@
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
border: 1px solid gray;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
box-shadow: 1px 2px 5px #888;
|
||||
font-family: var(--bs-font-sans-serif);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
this.traceRenderings = false;
|
||||
this.traceSubscriptions = false;
|
||||
this.requestedFrame = false;
|
||||
this.enabledSelector = false;
|
||||
this.eventsBatch = [];
|
||||
// Object which defines how different types of data should be displayed when passed to the devtools
|
||||
this.serializer = {
|
||||
@@ -108,23 +109,34 @@
|
||||
object(obj) {
|
||||
const result = [];
|
||||
let length = 0;
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (length > 25) {
|
||||
result.push("...");
|
||||
break;
|
||||
if (obj instanceof String) {
|
||||
result[0] = `'${obj.toString()}'`;
|
||||
} else if (obj instanceof Array) {
|
||||
return `${obj.constructor.name} ${this.array([...obj])}`;
|
||||
} else if (obj instanceof Number) {
|
||||
result[0] = obj.toString();
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (length > 25) {
|
||||
result.push("...");
|
||||
break;
|
||||
}
|
||||
const element = key + ": " + this.serializeItem(value);
|
||||
length += element.length;
|
||||
result.push(element);
|
||||
}
|
||||
for (const key of Object.getOwnPropertySymbols(obj)) {
|
||||
if (length > 25) {
|
||||
result.push("...");
|
||||
break;
|
||||
}
|
||||
const element = key.toString() + ": " + this.serializeItem(obj[key]);
|
||||
length += element.length;
|
||||
result.push(element);
|
||||
}
|
||||
const element = key + ": " + this.serializeItem(value);
|
||||
length += element.length;
|
||||
result.push(element);
|
||||
}
|
||||
for (const key of Object.getOwnPropertySymbols(obj)) {
|
||||
if (length > 25) {
|
||||
result.push("...");
|
||||
break;
|
||||
}
|
||||
const element = key.toString() + ": " + this.serializeItem(obj[key]);
|
||||
length += element.length;
|
||||
result.push(element);
|
||||
if (obj.constructor.name !== "Object") {
|
||||
return obj.constructor.name + " {" + result.join(", ") + "}";
|
||||
}
|
||||
return "{" + result.join(", ") + "}";
|
||||
},
|
||||
@@ -175,6 +187,7 @@
|
||||
|
||||
initDevtools(frame = "top") {
|
||||
if (!this.devtoolsInit) {
|
||||
document.addEventListener("mouseover", this.HTMLSelector, { capture: true });
|
||||
this.frame = frame;
|
||||
const self = this;
|
||||
// Flush the events batcher when a root render is completed
|
||||
@@ -606,28 +619,33 @@
|
||||
// Identify the hovered component based on the corresponding DOM element and send the Select message
|
||||
// when the target changes
|
||||
HTMLSelector = (ev) => {
|
||||
const target = ev.target;
|
||||
if (!this.currentSelectedElement || !target.isEqualNode(this.currentSelectedElement)) {
|
||||
const path = this.getElementPath(target);
|
||||
this.highlightComponent(path);
|
||||
this.currentSelectedElement = target;
|
||||
window.top.postMessage({
|
||||
source: "owl-devtools",
|
||||
type: "SelectElement",
|
||||
data: path,
|
||||
});
|
||||
if (this.enabledSelector) {
|
||||
const target = ev.target;
|
||||
if (!this.currentSelectedElement || !target.isEqualNode(this.currentSelectedElement)) {
|
||||
const path = this.getElementPath(target);
|
||||
this.highlightComponent(path);
|
||||
this.currentSelectedElement = target;
|
||||
window.top.postMessage({
|
||||
source: "owl-devtools",
|
||||
type: "SelectElement",
|
||||
data: path,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.removeHighlights();
|
||||
}
|
||||
};
|
||||
|
||||
// Activate the HTML selector tool
|
||||
enableHTMLSelector() {
|
||||
document.addEventListener("mouseover", this.HTMLSelector, { capture: true });
|
||||
this.enabledSelector = true;
|
||||
document.addEventListener("click", this.disableHTMLSelector, { capture: true });
|
||||
document.addEventListener("mouseout", this.removeHighlights, { capture: true });
|
||||
document.addEventListener("scroll", this.removeHighlights, { capture: true });
|
||||
}
|
||||
|
||||
// Diasble the HTML selector tool
|
||||
disableHTMLSelector = (ev = undefined) => {
|
||||
this.enabledSelector = false;
|
||||
if (ev) {
|
||||
if (!ev.isTrusted) {
|
||||
return;
|
||||
@@ -636,9 +654,8 @@
|
||||
ev.preventDefault();
|
||||
}
|
||||
this.removeHighlights();
|
||||
document.removeEventListener("mouseover", this.HTMLSelector, { capture: true });
|
||||
document.removeEventListener("click", this.disableHTMLSelector, { capture: true });
|
||||
document.removeEventListener("mouseout", this.removeHighlights, { capture: true });
|
||||
document.removeEventListener("scroll", this.removeHighlights, { capture: true });
|
||||
window.top.postMessage({
|
||||
source: "owl-devtools",
|
||||
type: "StopSelector",
|
||||
@@ -748,7 +765,7 @@
|
||||
child.contentType = "object";
|
||||
child.content = this.serializer.serializeItem(Object.getPrototypeOf(parentObj), true);
|
||||
child.hasChildren = true;
|
||||
if (!oldTree && type === "env") {
|
||||
if (!oldTree && type === "env" && Object.getPrototypeOf(parentObj) !== Object.prototype) {
|
||||
child.toggled = true;
|
||||
}
|
||||
break;
|
||||
@@ -817,7 +834,7 @@
|
||||
child.contentType = "set";
|
||||
child.hasChildren = true;
|
||||
break;
|
||||
case obj instanceof Array:
|
||||
case obj.constructor.name === "Array":
|
||||
child.contentType = "array";
|
||||
child.hasChildren = obj.length > 0;
|
||||
break;
|
||||
@@ -828,7 +845,9 @@
|
||||
case obj instanceof Object:
|
||||
child.contentType = "object";
|
||||
child.hasChildren =
|
||||
Object.keys(obj).length || Object.getOwnPropertySymbols(obj).length;
|
||||
Object.keys(obj).length ||
|
||||
Object.getOwnPropertySymbols(obj).length ||
|
||||
obj.constructor.name !== "Object";
|
||||
break;
|
||||
default:
|
||||
child.contentType = typeof obj;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const IS_FIREFOX = navigator.userAgent.indexOf("Firefox") !== -1;
|
||||
|
||||
const browserInstance = IS_FIREFOX ? browser : chrome;
|
||||
export const browserInstance = IS_FIREFOX ? browser : chrome;
|
||||
|
||||
export async function getOwlStatus() {
|
||||
const response = await browserInstance.runtime.sendMessage({ type: "getOwlStatus" });
|
||||
|
||||