Compare commits

...

36 Commits

Author SHA1 Message Date
Sam Degueldre 07eb958ef1 Revert "Update component.md"
This reverts commit 6891ad2773.
2025-03-14 08:48:02 -04:00
Steve Van Essche 6891ad2773 Update component.md
Replace `onWillUnmount` by `onWillDestroy` in the said paragraph
2025-03-14 08:47:30 -04:00
Géry Debongnie a9f29c4caa [FIX] refs in recursive templates now work properly 2022-02-09 10:26:23 +01:00
Géry Debongnie 74faa8bb82 [MISC] tools: fix version string 2022-02-08 13:44:18 +01:00
Bruno Boi 5940048a19 [FIX] can use t-out with String classes 2022-02-08 13:43:02 +01:00
Géry Debongnie 554157637b [FIX] slots: process slot params/values like normal props 2022-02-08 10:58:49 +01:00
Géry Debongnie 3c2f073c11 [DOC] improve changelog 2022-02-07 14:58:02 +01:00
Géry Debongnie f095f9e5aa [IMP] blockdom: apply dynamic part of block before inserting it 2022-02-04 13:55:44 +01:00
Géry Debongnie a402115227 [IMP] props validation: cannot set default value on mandatory props 2022-02-04 13:55:44 +01:00
Géry Debongnie 2afa1f4ace [DOC] add some info to the changelog 2022-02-04 13:55:44 +01:00
Géry Debongnie 5a7928d3e4 [IMP] compiler: translatableAttributes can be added/removed 2022-02-03 13:41:49 +01:00
Samuel Degueldre 9424312573 [IMP] compiler: allow to declare default slot scope on component 2022-02-02 09:09:23 +01:00
Géry Debongnie c5e197ddc1 [REF] allow external code to override validateTarget 2022-02-02 08:51:35 +01:00
Géry Debongnie fa7a393f27 [IMP] component: validate mounting target at patch time 2022-01-31 17:01:20 +01:00
Géry Debongnie efaa186544 [IMP] component: disallow calling hooks outside of setup
(and constructor)

Doing so could cause strange and difficult bugs
2022-01-31 15:08:31 +01:00
Bruno Boi 45ea6a195e [FIX] hooks: useSubEnv will not erase previous useChildSubEnv 2022-01-31 14:49:25 +01:00
Géry Debongnie 2ce0c39674 [IMP] app: introduce test mode
Same as `dev` mode, but without warning in console
2022-01-31 08:57:08 +01:00
Géry Debongnie 924dffeb36 [TEST] component: add test to make sure a specific issue does not arise 2022-01-27 15:02:44 +01:00
Géry Debongnie 0dbc807e01 [IMP] typing: make app and mount method properly generic 2022-01-27 14:43:33 +01:00
Bruno Boi 4b88787a72 [FIX] compiler: never add _ prefix to non variable token 2022-01-27 14:41:16 +01:00
Samuel Degueldre 6f0a4b1c92 [FIX] compiler: do not pass dynamic props object as is
The child receiving the props can observe changes made to the passed
t-props object which is not desirable.
2022-01-27 13:19:14 +01:00
Bruno Boi 3f575148b6 [FIX] compiler: add _ prefix to local variables while compiling an expression 2022-01-27 10:57:13 +01:00
Géry Debongnie 4a971f2963 [IMP] hooks: introduce useChildSubEnv and change useSubEnv 2022-01-27 10:35:06 +01:00
Géry Debongnie d7850aaf7a [FIX] compiler: does not modify xml doc in place 2022-01-27 10:34:41 +01:00
Bruno Boi 0e13b859d0 [FIX] component: properly capture expression of t-model 2022-01-27 09:23:52 +01:00
Géry Debongnie c813a1ce03 [DOC] fix error in slot documentation 2022-01-25 12:37:07 +01:00
Géry Debongnie 54d69c2dde [FIX] reactivity: clear callbacks at destroy time instead of unmount 2022-01-24 16:36:32 +01:00
Géry Debongnie 11d4aae8d2 [FIX] compiler: add missing ; in some places 2022-01-24 14:51:12 +01:00
Géry Debongnie b159a073d9 [FIX] component: proper error message in dev mode in some cases 2022-01-24 13:18:19 +01:00
Lucas Perais (lpe) d9b189bcba [FIX] compiler: force new block for svg nested in html 2022-01-24 12:56:43 +01:00
Géry Debongnie 7ef1fe0b99 [FIX] svg: allow path as root tag 2022-01-24 10:29:22 +01:00
Samuel Degueldre 211ecdf689 [FIX] blockdom: fix VHtml patching not setting its html correctly 2022-01-24 09:36:43 +01:00
Lucas Perais (lpe) b208894d38 [FIX] compiler: t-model on select with options with dynamic values 2022-01-21 16:38:58 +01:00
Géry Debongnie 0737bb39b4 [DOC] update changelog renderToString example 2022-01-21 15:40:27 +01:00
Géry Debongnie de584b01a8 [FIX] reactivity: do not observe eventtarget and other stuff 2022-01-21 15:40:27 +01:00
Géry Debongnie 525029b682 [IMP] reactivity: introduces markRaw and toRaw functions 2022-01-21 15:40:27 +01:00
69 changed files with 2301 additions and 547 deletions
+50 -14
View File
@@ -30,19 +30,32 @@ All changes are documented here in no particular order.
- breaking: `catchError` method is replaced by `onError` hook ([details](#36-catcherror-method-is-replaced-by-onerror-hook))
- breaking: Support for inline css (`css` tag and static `style`) has been removed ([details](#37-support-for-inline-css-css-tag-and-static-style-has-been-removed))
- new: prop validation system can now describe that additional props are allowed (with `*`) ([doc](doc/reference/props.md#props-validation))
- breaking: prop validation system does not allow default prop on a mandatory (not optional) prop ([doc](doc/reference/props.md#props-validation))
**Templates**
- breaking: `t-foreach` should always have a corresponding `t-key` ([details](#20-t-foreach-should-always-have-a-corresponding-t-key))
- breaking: `t-ref` does not work on components ([details](#29-t-ref-does-not-work-on-component))
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
- breaking: `t-raw` directive has been removed (replaced by `t-out`) ([details](#38-t-raw-directive-has-been-removed-replaced-by-t-out))
- new: add support for synthetic events ([doc](doc/reference/event_handling.md#synthetic-events))
- breaking: style/class on components are now regular props ([details](#10-styleclass-on-components-are-now-regular-props))
- new: components can use the `.bind` suffix to bind function props ([doc](doc/reference/props.md#binding-function-props))
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
- breaking: `t-on` does not work on components any more ([details](#12-t-on-does-not-work-on-components-any-more))
- new: an error is thrown if an handler defined in a `t-on-` directive is not a function (failed silently previously in some cases)
- breaking: `t-component` no longer accepts strings ([details](#17-t-component-no-longer-accepts-strings))
- new: the `this` variable in template expressions is now bound to the component
**Reactivity**
- finer grained reactivity: owl 2 tracks change per key/component
- finer grained reactivity: sub components can reobserve state ([doc](doc/reference/reactivity.md))
- new: `reactive` function: create reactive state (without being linked to a component) ([doc](doc/reference/reactivity.md#reactive))
- new: `markRaw` function: mark an object or array so that it is ignored by the reactivity system ([doc](doc/reference/reactivity.md#markraw))
- new: `toRaw` function: given a reactive objet, return the raw (non reactive) underlying object ([doc](doc/reference/reactivity.md#toraw))
**Slots**
@@ -66,13 +79,13 @@ All changes are documented here in no particular order.
- improved performance
- much simpler code
- finer grained reactivity: owl 2 tracks change per key/component
- finer grained reactivity: sub components can reobserve state
- new App class to encapsulate a root Owl component (with the config for that application) ([doc](doc/reference/app.md))
- new `Memo` component
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
- breaking: `Context` is removed ([details](#15-context-is-removed))
- breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty))
- breaking: `env` is now frozen ([details](#28-env-is-now-frozen))
- new hook: `useChildSubEnv` (only applies to child components) ([details](#27-usechildsubenv-only-applies-to-child-components))
- breaking: most exports are exported at top level ([details](#18-most-exports-are-exported-at-top-level))
- breaking: properties are no longer set as attributes ([details](#19-properties-are-no-longer-set-as-attributes))
- breaking: `EventBus` api changed: it is now an `EventTarget` ([details](#21-eventbus-api-changed-it-is-now-an-eventtarget))
@@ -81,8 +94,6 @@ All changes are documented here in no particular order.
- breaking: transition system is removed ([details](#24-transition-system-is-removed))
- breaking: no more global components or templates ([details](#25-no-more-global-components-or-templates))
- breaking: `AsyncRoot` utility component is removed ([details](#26-asyncroot-utility-component-is-removed))
- breaking: `useSubEnv` only applies to child components ([details](#27-usesubenv-only-applies-to-child-components))
- breaking: `env` is now frozen ([details](#28-env-is-now-frozen))
- breaking: `renderToString` function on qweb has been removed ([details](#32-rendertostring-on-qweb-has-been-removed))
- breaking: `debounce` utility function has been removed ([details](#34-debounce-utility-function-has-been-removed))
- breaking: `browser` object has been removed ([details](#39-browser-object-has-been-removed))
@@ -565,15 +576,12 @@ either a fallback when the data is not ready, or the actual component with data
as props. If there is no escape, and `AsyncRoot` is needed, please reach out to
us so we can study this usecase.
### 27. `useSubEnv` only applies to child components
### 27. `useChildSubEnv` (only applies to child components)
In Owl 1, a call to `useSubEnv` would define a new environment for the children
AND the component. It now only defines an environment for the children.
Rationale: This was a subtle cause for bugs: some code had to be rrun
before the call to `useSubEnv`, otherwise it could interfere with the sub environment.
Documentation: [Hooks](doc/reference/hooks.md#usesubenv)
In Owl, a call to `useSubEnv` would define a new environment for the children
AND the component. It is very useful, but in some cases, one only need to update
the children component environment. This can now be done with a new hook:
[`useChildSubEnv`](doc/reference/hooks.md#usesubenv-and-usechildsubenv)
### 28. `env` is now frozen
@@ -659,10 +667,14 @@ Also, this can easily be done in userspace, by mounting a component in a div. F
export async function renderToString(template, context) {
class C extends Component {
static template = template;
setup () {
Object.assign(this, context);
}
}
const div = document.createElement('div');
document.body.appendChild(div);
const component = await mount(C, div);
const app = new App(C);
await app.mount(div);
const result = div.innerHTML;
app.destroy();
div.remove();
@@ -670,6 +682,30 @@ export async function renderToString(template, context) {
}
```
The function above works for most cases, but is asynchronous. An alternative
function could look like this:
```js
const { App, blockDom } = owl;
const app = new App(Component); // act as a template repository
function renderToString(template, context = {}) {
app.addTemplate(template, template, { allowDuplicate: true });
const templateFn = app.getTemplate(template);
const bdom = templateFn(context, {});
const div = document.createElement('div')
blockDom.mount(bdom, div);
return div.innerHTML;
}
```
This is a synchronous function, so it will not work with components, but it should
be useful for most simple templates.
Also note that these two examples do not translate their templates. To do that,
they need to be modified to pass the proper translate function to the `App`
configuration.
### 33. Portal are now defined with `t-portal`
Before Owl 2, one could use the `Portal` component by importing it and using it.
+8 -2
View File
@@ -7,9 +7,14 @@ Main entities:
- [`App`](reference/app.md): represent an Owl application (mainly a root component,a set of templates, and a config)
- [`Component`](reference/component.md): the main class to define a concrete Owl component
- [`mount`](reference/app.md#mount-helper): main entry point for most application: mount a component to a target
- [`xml`](reference/templates.md#inline-templates): helper to define an inline template
Reactivity
- [`useState`](reference/reactivity.md#usestate): create a reactive object (hook, linked to a specific component)
- [`reactive`](reference/reactivity.md#reactive): create a reactive object (not linked to any component)
- [`xml`](reference/templates.md#inline-templates): helper to define an inline template
- [`markRaw`](reference/reactivity.md#markraw): mark an object or array so that it is ignored by the reactivity system
- [`toRaw`](reference/reactivity.md#toraw): given a reactive objet, return the raw (non reactive) underlying object
Lifecycle hooks:
@@ -31,7 +36,8 @@ Other hooks:
- [`useEnv`](reference/hooks.md#useenv): return a reference to the current env
- [`useExternalListener`](reference/hooks.md#useexternallistener): add a listener outside of a component DOM
- [`useRef`](reference/hooks.md#useref): get an object representing a reference (`t-ref`)
- [`useSubEnv`](reference/hooks.md#usesubenv): extend the current env with additional information for child components
- [`useChildSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for child components)
- [`useSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for current component and child components)
Utility/helpers:
+21 -16
View File
@@ -3,6 +3,7 @@
## Content
- [Overview](#overview)
- [API](#api)
- [Configuration](#configuration)
- [`mount` helper](#mount-helper)
- [Loading templates](#loading-templates)
@@ -26,24 +27,10 @@ The basic workflow is: create an `App` instance configured with the root
component, the templates, and possibly other settings. Then, we mount that
instance somewhere in the DOM.
## Configuration
## API
- **`constructor(Root[, config])`**: first argument should be a component class (not
an instance), and the optional second argument is a configuration object.
The `config` object is an object with some of the following keys:
- **`env (object)`**: if given, this will be the shared `env` given to each component
- **`props (object)`**: the props given to the root component
- **`dev (boolean, default=false)`**: if `true`, the application is rendered in `dev`
mode, which activates some additional checks (in particular, the props validation
code is only performed in dev mode)
- **`translatableAttributes (string[])`**: a list of additional attributes that should
be translated (see [translations](translations.md))
- **`translateFn (function)`**: a function that will be called by owl to translate
templates (see [translations](translations.md))
- **`templates (string | xml document)`**: all the templates that will be used by
the components created by the application.
an instance), and the optional second argument is a configuration object (see below).
- **`mount(target, options)`**: first argument is an html element, and the optional
second argument is an object with mounting options (see below). Mount the app
@@ -58,6 +45,24 @@ instance somewhere in the DOM.
- **`destroy()`**: destroys the application
## Configuration
The `config` object is an object with some of the following keys:
- **`env (object)`**: if given, this will be the shared `env` given to each component
- **`props (object)`**: the props given to the root component
- **`dev (boolean, default=false)`**: if `true`, the application is rendered in `dev`
mode, which activates some additional checks (in particular, the props validation
code is only performed in dev mode)
- **`test (boolean, default=false)`**: `test` mode is the same as `dev` mode, except
that Owll will not log a message to warn that Owl is in `dev` mode.
- **`translatableAttributes (string[])`**: a list of additional attributes that should
be translated (see [translations](translations.md))
- **`translateFn (function)`**: a function that will be called by owl to translate
templates (see [translations](translations.md))
- **`templates (string | xml document)`**: all the templates that will be used by
the components created by the application.
## `mount` helper
Note that there is a `mount` helper to do that in just a line:
+1 -1
View File
@@ -52,7 +52,7 @@ It is sometimes useful to add one (or more) specific keys to the environment,
from the perspective of a specific component and its children. In that case, the
solution presented above will not work, since it sets the global environment.
There is a hook for this situation: [`useSubEnv`](hooks.md#usesubenv).
There are two hooks for this situation: [`useSubEnv` and `useChildSubEnv`](hooks.md#usesubenv-and-usechildsubenv).
```js
class SomeComponent extends Component {
+18 -7
View File
@@ -8,7 +8,7 @@
- [Other hooks](#other-hooks)
- [`useState`](#usestate)
- [`useRef`](#useref)
- [`useSubEnv`](#usesubenv)
- [`useSubEnv` and `useChildSubEnv`](#usesubenv-and-usechildsubenv)
- [`useExternalListener`](#useexternallistener)
- [`useComponent`](#usecomponent)
- [`useEnv`](#useenv)
@@ -152,14 +152,14 @@ this.ref2 = useRef("component_2");
References are only guaranteed to be active while the parent component is mounted.
If this is not the case, accessing `el` or `comp` on it will return `null`.
### `useSubEnv`
### `useSubEnv` and `useChildSubEnv`
The environment is sometimes useful to share some common information between
all components. But sometimes, we want to _scope_ that knowledge to a subtree.
For example, if we have a form view component, maybe we would like to make some
`model` object available to all sub components, but not to the whole application.
This is where the `useSubEnv` hook may be useful: it lets a component add some
This is where the `useChildSubEnv` hook may be useful: it lets a component add some
information to the environment in a way that only its children
can access it:
@@ -167,15 +167,26 @@ can access it:
class FormComponent extends Component {
setup() {
const model = makeModel();
// model will be available on this.env for this component and all children
useSubEnv({ model });
// someKey will be available on this.env for all children
useChildSubEnv({ someKey: "value" });
}
}
```
The `useSubEnv` takes one argument: an object which contains some key/value that
will be added to the parent environment. Note that it will extend, not replace
the parent environment. And of course, the parent environment will not be
affected.
The `useSubEnv` and `useChildSubEnv` hooks take one argument: an object which
contains some key/value that will be added to the current environment. These hooks
will create a new env object with the new information:
- `useSubEnv` will assign this new `env` to itself and to all children components
- `useChildSubEnv` will only assign this new `env` to all children components.
As usual in Owl, [environments](environment.md) created with these two hooks are
frozen, to prevent unwanted modifications.
Note that both these hooks can be called an arbitrary number of times. The `env`
will then be updated accordingly.
### `useExternalListener`
+6 -1
View File
@@ -168,11 +168,15 @@ For each key, a `prop` definition is either a boolean, a constructor, a list of
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
- `validate`: this is a function which should return a boolean to determine if
the value is valid or not. Useful for custom validation logic.
- `optional`: if true, the prop is not mandatory
There is a special `*` prop that means that additional prop are allowed. This is
sometimes useful for generic components that will propagate some or all their
props to their child components.
Note that default values cannot be defined for a mandatory props. Doing so will
result in a prop validation error.
Examples:
```js
@@ -190,7 +194,8 @@ class ComponentB extends owl.Component {
element: {type: Object, shape: {id: Boolean, text: String }
},
date: Date,
combinedVal: [Number, Boolean]
combinedVal: [Number, Boolean],
optionalProp: { type: Number, optional: true }
};
...
+39
View File
@@ -5,6 +5,8 @@
- [Overview](#overview)
- [`useState`](#usestate)
- [`reactive`](#reactive)
- [`markRaw`](#markraw)
- [`toRaw`](#toraw)
## Overview
@@ -78,3 +80,40 @@ obj2.b = 3; // log 'observer1' and 'observer2'
Obviously, one can use `reactive` on the result of a `useState` if wanted, this
is the proper way to watch for some state changes.
## `markRaw`
Marks an object so that it is ignored by the reactivity system. This function returns its argument.
```js
const someObject = markRaw(...);
const state = useState({
a: 1,
obj: someObject
});
// here, state.obj === someObject
```
This is useful in some rare cases. For example, some complex and large object such
that going through the reactivity system may cause a non trivial performance slowdown.
However, use this function with caution: this is an escape hatch from the reactivity
system, and as such, using it may cause subtle and unintended issues!
## `toRaw`
Given a reactive object, this function returns the underlying, non-reactive,
corresponding object.
```js
// in setup
const state = useState({ value: 1 });
// later:
const rawState = toRaw(this.state);
rawState.value = 3; // will NOT be picked up by the reactivity system!!!
```
Here again, this is useful in some situations where we want to explicitely bypass
Owl, but using this function means that the responsability of coordinating
state update is given to the user code, instead of Owl. Subtle bugs may arise!
+28 -12
View File
@@ -87,8 +87,8 @@ grandparent of the slot content).
## Default Slot
The first element inside the component which is not a named slot will
be considered the `default` slot. For example:
All elements inside the component which are not a named slot will be treated as
part of the content of the `default` slot. For example:
```xml
<div t-name="Parent">
@@ -110,6 +110,7 @@ One can mix default slot and named slots:
default content
<t t-set-slot="footer">
content for footer slot here
</t>
</Child>
</div>
```
@@ -209,30 +210,45 @@ use this `Notebook` component:
</Notebook>
```
Slot params works like normal props, so one can use the `.bind` suffix to
bind a function if needed.
## Slot scopes
For other kind of advanced use cases, the content of a slot may depends on some
specific information specific to the generic component. This is the opposite
of the slot params.
For other kinds of advanced use cases, the content of a slot may depends on some
information specific to the generic component. This is the opposite of the slot
params.
To solve this kind of problems, one can use the `t-set-scope` directive along
To solve this kind of problems, one can use the `t-slot-scope` directive along
with the `t-set-slot`. This defines the name of a variable that can access
everything given by the child component:
```xml
<div>
<t t-set-slot="foo" t-set-scope="scope">
<MyComponent>
<t t-set-slot="foo" t-slot-scope="scope">
content
<t t-esc="scope.bool"/>
<t t-esc="scope.num"/>
</t>
</div>
</MyComponent>
```
And the child component that includes the slot can provide values like this:
```xml
<div>
<t t-slot="foo" bool="other_var" num="5">
</div>
<t t-slot="foo" bool="other_var" num="5">
```
In the case of the default slot, you may declare the slot scope directly on the
component itself:
```xml
<MyComponent t-slot-scope="scope">
content
<t t-esc="scope.bool"/>
<t t-esc="scope.num"/>
</MyComponent>
```
Slot values works like normal props, so one can use the `.bind` suffix to
bind a function if needed.
+6 -4
View File
@@ -674,7 +674,9 @@ This `RootNode` component will then display a live SVG representation of the
graph described by the `graph` property. Note that there is a recursive structure
here: the `Node` component uses itself as a subcomponent.
Note that since SVG needs to be handled in a specific way (its namespace needs
to be properly set), there is a small constraint for Owl components: if an owl
component is supposed to be a part of an svg graph, then its root node needs to
be a `g` tag, so Owl can properly set the namespace.
**Important note:** Owl needs to properly set the namespace for each svg elements.
Since Owl compile each template separately, it is not able to determine easily
if a template is supposed to be included in a svg namespace or not. Therefore,
Owl depends on a heuristic: if a tag is either `svg`, `g` or `path`, then it will
be considered as svg. In practice, this means that each component or each sub
templates (included with `t-call`) should have one of these tag as root tag.
+12 -1
View File
@@ -54,6 +54,17 @@ For example, one may want to also translate `data-title` attributes. To do that,
we can define additional attributes with the `translatableAttributes` option:
```js
const app = new App(Root, { templates, tranaslateFn, translatalbeAttributes: ["data-title"] });
const app = new App(Root, { templates, tranaslateFn, translatableAttributes: ["data-title"] });
// ...
```
It is also possible to remove an attribute from the default list by prefixing it with `-`:
```js
const app = new App(Root, {
templates,
tranaslateFn,
translatableAttributes: ["data-title", "-title"],
});
// data-title attribute will be translated, but not title attribute...
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.0.0-alpha1",
"version": "2.0.0-alpha.1",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js",
+32 -30
View File
@@ -1,9 +1,10 @@
import { Component } from "../component/component";
import { Component, ComponentConstructor } from "../component/component";
import { ComponentNode } from "../component/component_node";
import { MountOptions } from "../component/fibers";
import { Scheduler } from "../component/scheduler";
import { TemplateSet, TemplateSetConfig } from "./template_set";
import { nodeErrorHandlers } from "../component/error_handling";
import { validateTarget } from "../utils";
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
@@ -11,9 +12,10 @@ export interface Env {
[key: string]: any;
}
export interface AppConfig extends TemplateSetConfig {
env?: Env;
props?: any;
export interface AppConfig<P, E> extends TemplateSetConfig {
props?: P;
env?: E;
test?: boolean;
}
export const DEV_MSG = `Owl is running in 'dev' mode.
@@ -21,42 +23,42 @@ export const DEV_MSG = `Owl is running in 'dev' mode.
This is not suitable for production use.
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`;
export class App<T extends typeof Component = any> extends TemplateSet {
Root: T;
props: any;
env: Env;
scheduler = new Scheduler();
root: ComponentNode | null = null;
export class App<
T extends abstract new (...args: any) => any = any,
P = any,
E = any
> extends TemplateSet {
static validateTarget = validateTarget;
constructor(Root: T, config: AppConfig = {}) {
Root: ComponentConstructor<P, E>;
props: P;
env: E;
scheduler = new Scheduler();
root: ComponentNode<P, E> | null = null;
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
super(config);
this.Root = Root;
if (config.dev) {
if (config.test) {
this.dev = true;
}
if (this.dev && !config.test) {
console.info(DEV_MSG);
}
const descrs = Object.getOwnPropertyDescriptors(config.env || {});
this.env = Object.freeze(Object.defineProperties({}, descrs));
this.props = config.props || {};
this.env = Object.freeze(Object.defineProperties({}, descrs)) as E;
this.props = config.props || ({} as P);
}
mount(target: HTMLElement, options?: MountOptions): Promise<InstanceType<T>> {
this.checkTarget(target);
mount(target: HTMLElement, options?: MountOptions): Promise<Component<P, E> & InstanceType<T>> {
App.validateTarget(target);
const node = this.makeNode(this.Root, this.props);
const prom = this.mountNode(node, target, options);
this.root = node;
return prom;
}
checkTarget(target: HTMLElement) {
if (!(target instanceof HTMLElement)) {
throw new Error("Cannot mount component: the target is not a valid DOM element");
}
if (!document.body.contains(target)) {
throw new Error("Cannot mount a component on a detached dom node");
}
}
makeNode(Component: T, props: any): ComponentNode {
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
return new ComponentNode(Component, props, this);
}
@@ -96,10 +98,10 @@ export class App<T extends typeof Component = any> extends TemplateSet {
}
}
export async function mount<T extends typeof Component>(
C: T,
export async function mount<T extends abstract new (...args: any) => any = any, P = any, E = any>(
C: T & ComponentConstructor<P, E>,
target: HTMLElement,
config: AppConfig & MountOptions = {}
): Promise<InstanceType<T>> {
config: AppConfig<P, E> & MountOptions = {}
): Promise<Component<P, E> & InstanceType<T>> {
return new App(C, config).mount(target, config);
}
+1 -1
View File
@@ -135,7 +135,7 @@ export function safeOutput(value: any): ReturnType<typeof toggler> {
} else if (value instanceof LazyValue) {
safeKey = `lazy_value`;
block = value.evaluate();
} else if (typeof value === "string") {
} else if (value instanceof String || typeof value === "string") {
safeKey = "string_unsafe";
block = text(value);
} else {
+7 -4
View File
@@ -5,7 +5,7 @@ import { UTILS } from "./template_helpers";
const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
export const globalTemplates: { [key: string]: string | Node } = {};
export const globalTemplates: { [key: string]: string | Element } = {};
function parseXML(xml: string): Document {
const parser = new DOMParser();
@@ -67,7 +67,11 @@ export class TemplateSet {
}
}
addTemplate(name: string, template: string | Node, options: { allowDuplicate?: boolean } = {}) {
addTemplate(
name: string,
template: string | Element,
options: { allowDuplicate?: boolean } = {}
) {
if (name in this.rawTemplates && !options.allowDuplicate) {
throw new Error(`Template ${name} already defined`);
}
@@ -82,7 +86,6 @@ export class TemplateSet {
xml = xml instanceof Document ? xml : parseXML(xml);
for (const template of xml.querySelectorAll("[t-name]")) {
const name = template.getAttribute("t-name")!;
template.removeAttribute("t-name");
this.addTemplate(name, template, options);
}
}
@@ -106,7 +109,7 @@ export class TemplateSet {
return this.templates[name];
}
_compileTemplate(name: string, template: string | Node) {
_compileTemplate(name: string, template: string | Element) {
return compile(template, {
name,
dev: this.dev,
+73 -49
View File
@@ -299,12 +299,13 @@ interface BlockCtx {
locations: IndexedLocation[];
children: Child[];
cbRefs: number[];
refList: (() => void)[][];
}
function buildContext(tree: IntermediateTree, ctx?: BlockCtx, fromIdx?: number): BlockCtx {
if (!ctx) {
const children = new Array(tree.info.filter((v) => v.type === "child").length);
ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN };
ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN, refList: [] };
fromIdx = 0;
}
if (tree.refN) {
@@ -408,11 +409,11 @@ function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
break;
}
case "ref":
ctx.cbRefs.push(info.idx);
const index = ctx.cbRefs.push(info.idx) - 1;
ctx.locations.push({
idx: info.idx,
refIdx: info.refIdx!,
setData: setRef,
setData: makeRefSetter(index, ctx.refList),
updateData: NO_OP,
});
}
@@ -426,12 +427,21 @@ function buildBlock(template: HTMLElement, ctx: BlockCtx): BlockType {
let B = createBlockClass(template, ctx);
if (ctx.cbRefs.length) {
const refs = ctx.cbRefs;
const cbRefs = ctx.cbRefs;
const refList = ctx.refList;
let cbRefsNumber = cbRefs.length;
B = class extends B {
mount(parent: HTMLElement, afterNode: Node | null) {
refList.push(new Array(cbRefsNumber));
super.mount(parent, afterNode);
for (let cbRef of refList.pop()!) {
cbRef();
}
}
remove() {
super.remove();
for (let ref of refs) {
let fn = (this as any).data[ref];
for (let cbRef of cbRefs) {
let fn = (this as any).data[cbRef];
fn(null);
}
}
@@ -476,12 +486,12 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
const nodeInsertBefore = nodeProto.insertBefore;
const elementRemove = elementProto.remove;
return class Block {
class Block {
el: HTMLElement | undefined;
refs: Node[] | undefined;
data: any[] | undefined;
parentEl?: HTMLElement | undefined;
data: any[] | undefined;
children?: (VNode | undefined)[];
refs: Node[] | undefined;
constructor(data?: any[]) {
this.data = data;
@@ -502,46 +512,62 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
nodeInsertBefore.call(this.parentEl, this.el!, target);
}
toString() {
const div = document.createElement("div");
this.mount(div, null);
return div.innerHTML;
}
mount(parent: HTMLElement, afterNode: Node | null) {
const el = nodeCloneNode.call(template, true);
const el = nodeCloneNode.call(template, true) as HTMLElement;
nodeInsertBefore.call(parent, el, afterNode);
if (isDynamic) {
// collecting references
const refs: Node[] = new Array(refN);
this.refs = refs;
refs[0] = el;
for (let i = 0; i < colN; i++) {
const w = collectors[i];
refs[w.idx] = w.getVal.call(refs[w.prevIdx]);
}
this.el = el;
this.parentEl = parent;
}
patch(other: Block, withBeforeRemove: boolean) {}
}
// applying data to all update points
if (locN) {
const data = this.data!;
for (let i = 0; i < locN; i++) {
const loc = locations[i];
loc.setData.call(refs[loc.refIdx], data[i]);
}
}
if (isDynamic) {
Block.prototype.mount = function mount(parent: HTMLElement, afterNode: Node | null) {
const el = nodeCloneNode.call(template, true);
// collecting references
const refs: Node[] = new Array(refN);
this.refs = refs;
refs[0] = el;
for (let i = 0; i < colN; i++) {
const w = collectors[i];
refs[w.idx] = w.getVal.call(refs[w.prevIdx]);
}
// preparing all children
if (childN) {
const children = this.children;
for (let i = 0; i < childN; i++) {
const child = children![i];
if (child) {
const loc = childrenLocs[i];
const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;
child.isOnlyChild = loc.isOnlyChild;
child.mount(refs[loc.parentRefIdx] as any, afterNode);
}
// applying data to all update points
if (locN) {
const data = this.data!;
for (let i = 0; i < locN; i++) {
const loc = locations[i];
loc.setData.call(refs[loc.refIdx], data[i]);
}
}
nodeInsertBefore.call(parent, el, afterNode);
// preparing all children
if (childN) {
const children = this.children;
for (let i = 0; i < childN; i++) {
const child = children![i];
if (child) {
const loc = childrenLocs[i];
const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;
child.isOnlyChild = loc.isOnlyChild;
child.mount(refs[loc.parentRefIdx] as any, afterNode);
}
}
}
this.el = el as HTMLElement;
this.parentEl = parent;
}
patch(other: Block, withBeforeRemove: boolean) {
};
Block.prototype.patch = function patch(other: Block, withBeforeRemove: boolean) {
if (this === other) {
return;
}
@@ -586,19 +612,17 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
}
}
}
}
toString() {
const div = document.createElement("div");
this.mount(div, null);
return div.innerHTML;
}
};
};
}
return Block;
}
function setText(this: Text, value: any) {
characterDataSetData.call(this, toText(value));
}
function setRef(this: HTMLElement, fn: any) {
fn(this);
function makeRefSetter(index: number, refs: (() => void)[][]): Setter<HTMLElement> {
return function setRef(this: HTMLElement, fn: any) {
refs[refs.length - 1][index] = () => fn(this);
};
}
+1
View File
@@ -61,6 +61,7 @@ class VHtml {
// remove current content
this.remove();
this.content = content;
this.html = other.html;
}
}
+3 -3
View File
@@ -9,11 +9,11 @@ const characterDataSetData = getDescriptor(characterDataProto, "data").set!;
const nodeRemoveChild = nodeProto.removeChild;
abstract class VSimpleNode {
text: string;
text: string | String;
parentEl?: HTMLElement | undefined;
el?: any;
constructor(text: string) {
constructor(text: string | String) {
this.text = text;
}
@@ -65,7 +65,7 @@ class VComment extends VSimpleNode {
patch() {}
}
export function text(str: string): VNode<VText> {
export function text(str: string | String): VNode<VText> {
return new VText(str);
}
+92 -49
View File
@@ -128,6 +128,7 @@ interface Context {
translate: boolean;
tKeyExpr: string | null;
nameSpace?: string;
tModelSelectedExpr?: string;
}
function createContext(parentCtx: Context, params?: Partial<Context>) {
@@ -139,6 +140,7 @@ function createContext(parentCtx: Context, params?: Partial<Context>) {
translate: parentCtx.translate,
tKeyExpr: null,
nameSpace: parentCtx.nameSpace,
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
},
params
);
@@ -212,14 +214,24 @@ export class CodeGenerator {
templateName?: string;
dev: boolean;
translateFn: (s: string) => string;
translatableAttributes: string[];
translatableAttributes: string[] = TRANSLATABLE_ATTRS;
ast: AST;
staticCalls: { id: string; template: string }[] = [];
helpers: Set<string> = new Set();
constructor(ast: AST, options: CodeGenOptions) {
this.translateFn = options.translateFn || ((s: string) => s);
this.translatableAttributes = options.translatableAttributes || TRANSLATABLE_ATTRS;
if (options.translatableAttributes) {
const attrs = new Set(TRANSLATABLE_ATTRS);
for (let attr of options.translatableAttributes) {
if (attr.startsWith("-")) {
attrs.delete(attr.slice(1));
} else {
attrs.add(attr);
}
}
this.translatableAttributes = [...attrs];
}
this.hasSafeContext = options.hasSafeContext || false;
this.dev = options.dev || false;
this.ast = ast;
@@ -522,10 +534,10 @@ export class CodeGenerator {
compileTDomNode(ast: ASTDomNode, ctx: Context) {
let { block, forceNewBlock } = ctx;
const isNewBlock = !block || forceNewBlock || ast.dynamicTag !== null;
const isNewBlock = !block || forceNewBlock || ast.dynamicTag !== null || ast.ns;
let codeIdx = this.target.code.length;
if (isNewBlock) {
if ((ast.dynamicTag || ctx.tKeyExpr) && ctx.block) {
if ((ast.dynamicTag || ctx.tKeyExpr || ast.ns) && ctx.block) {
this.insertAnchor(ctx.block!);
}
block = this.createBlock(block, "block", ctx);
@@ -543,24 +555,36 @@ export class CodeGenerator {
// specific namespace uri
attrs["block-ns"] = nameSpace;
}
for (let key in ast.attrs) {
let expr, attrName;
if (key.startsWith("t-attf")) {
let expr = interpolate(ast.attrs[key]);
expr = interpolate(ast.attrs[key]);
const idx = block!.insertData(expr, "attr");
attrs["block-attribute-" + idx] = key.slice(7);
attrName = key.slice(7);
attrs["block-attribute-" + idx] = attrName;
} else if (key.startsWith("t-att")) {
let expr = compileExpr(ast.attrs[key]);
expr = compileExpr(ast.attrs[key]);
const idx = block!.insertData(expr, "attr");
if (key === "t-att") {
attrs[`block-attributes`] = String(idx);
} else {
attrs[`block-attribute-${idx}`] = key.slice(6);
attrName = key.slice(6);
attrs[`block-attribute-${idx}`] = attrName;
}
} else if (this.translatableAttributes.includes(key)) {
attrs[key] = this.translateFn(ast.attrs[key]);
} else {
expr = `"${ast.attrs[key]}"`;
attrName = key;
attrs[key] = ast.attrs[key];
}
if (attrName === "value" && ctx.tModelSelectedExpr) {
let selectedId = block!.insertData(`${ctx.tModelSelectedExpr} === ${expr}`, "attr");
attrs[`block-attribute-${selectedId}`] = "selected";
}
}
// event handlers
@@ -600,8 +624,10 @@ export class CodeGenerator {
}
// t-model
let tModelSelectedExpr;
if (ast.model) {
const {
hasDynamicChildren,
baseExpr,
expr,
eventType,
@@ -612,20 +638,25 @@ export class CodeGenerator {
} = ast.model;
const baseExpression = compileExpr(baseExpr);
const id = this.generateId();
this.addLine(`const bExpr${id} = ${baseExpression};`);
const bExprId = this.generateId("bExpr");
this.addLine(`const ${bExprId} = ${baseExpression};`);
const expression = compileExpr(expr);
const exprId = this.generateId("expr");
this.addLine(`const ${exprId} = ${expression};`);
const fullExpression = `${bExprId}[${exprId}]`;
let idx: number;
if (specialInitTargetAttr) {
idx = block!.insertData(
`${baseExpression}[${expression}] === '${attrs[targetAttr]}'`,
"attr"
);
idx = block!.insertData(`${fullExpression} === '${attrs[targetAttr]}'`, "attr");
attrs[`block-attribute-${idx}`] = specialInitTargetAttr;
} else if (hasDynamicChildren) {
const bValueId = this.generateId("bValue");
tModelSelectedExpr = `${bValueId}`;
this.addLine(`let ${tModelSelectedExpr} = ${fullExpression}`);
} else {
idx = block!.insertData(`${baseExpression}[${expression}]`, "attr");
idx = block!.insertData(`${fullExpression}`, "attr");
attrs[`block-attribute-${idx}`] = targetAttr;
}
this.helpers.add("toNumber");
@@ -633,7 +664,7 @@ export class CodeGenerator {
valueCode = shouldTrim ? `${valueCode}.trim()` : valueCode;
valueCode = shouldNumberize ? `toNumber(${valueCode})` : valueCode;
const handler = `[(ev) => { bExpr${id}[${expression}] = ${valueCode}; }]`;
const handler = `[(ev) => { ${fullExpression} = ${valueCode}; }]`;
idx = block!.insertData(handler, "hdlr");
attrs[`block-handler-${idx}`] = eventType;
}
@@ -658,6 +689,7 @@ export class CodeGenerator {
isLast: ctx.isLast && i === children.length - 1,
tKeyExpr: ctx.tKeyExpr,
nameSpace,
tModelSelectedExpr,
});
this.compileAST(child, subCtx);
}
@@ -1018,29 +1050,50 @@ export class CodeGenerator {
return parts.join("__");
}
/**
* Formats a prop name and value into a string suitable to be inserted in the
* generated code. For example:
*
* Name Value Result
* ---------------------------------------------------------
* "number" "state" "number: ctx['state']"
* "something" "" "something: undefined"
* "some-prop" "state" "'some-prop': ctx['state']"
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
*/
formatProp(name: string, value: string): string {
value = this.captureExpression(value);
if (name.includes(".")) {
let [_name, suffix] = name.split(".");
if (suffix === "bind") {
this.helpers.add("bind");
name = _name;
value = `bind(ctx, ${value || undefined})`;
} else {
throw new Error("Invalid prop suffix");
}
}
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
return `${name}: ${value || undefined}`;
}
formatPropObject(obj: { [prop: string]: any }): string {
const params = [];
for (const [n, v] of Object.entries(obj)) {
params.push(this.formatProp(n, v));
}
return params.join(", ");
}
compileComponent(ast: ASTComponent, ctx: Context) {
let { block } = ctx;
// props
const hasSlotsProp = "slots" in ast.props;
const props: string[] = [];
let hasSlotsProp = false;
for (let propName in ast.props) {
let propValue = this.captureExpression(ast.props[propName]) || undefined;
if (propName.includes(".")) {
let [name, suffix] = propName.split(".");
if (suffix === "bind") {
this.helpers.add("bind");
propName = name;
propValue = `bind(ctx, ${propValue})`;
} else {
throw new Error("Invalid prop suffix");
}
}
propName = /^[a-z_]+$/i.test(propName) ? propName : `'${propName}'`;
props.push(`${propName}: ${propValue}`);
if (propName === "slots") {
hasSlotsProp = true;
}
const propExpr = this.formatPropObject(ast.props);
if (propExpr) {
props.push(propExpr);
}
// slots
@@ -1063,9 +1116,7 @@ export class CodeGenerator {
params.push(`__scope: "${scope}"`);
}
if (ast.slots[slotName].attrs) {
for (const [n, v] of Object.entries(ast.slots[slotName].attrs!)) {
params.push(`${n}: ${compileExpr(v) || undefined}`);
}
params.push(this.formatPropObject(ast.slots[slotName].attrs!));
}
const slotInfo = `{${params.join(", ")}}`;
slotStr.push(`'${slotName}': ${slotInfo}`);
@@ -1082,7 +1133,7 @@ export class CodeGenerator {
let propString = propStr;
if (ast.dynamicProps) {
if (!props.length) {
propString = `${compileExpr(ast.dynamicProps)}`;
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)})`;
} else {
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}, ${propStr})`;
}
@@ -1091,7 +1142,7 @@ export class CodeGenerator {
let propVar: string;
if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {
propVar = this.generateId("props");
this.addLine(`const ${propVar!} = ${propString}`);
this.addLine(`const ${propVar!} = ${propString};`);
propString = propVar!;
}
@@ -1110,7 +1161,7 @@ export class CodeGenerator {
}
if (this.dev) {
this.addLine(`helpers.validateProps(${expr}, ${propVar!}, ctx)`);
this.addLine(`helpers.validateProps(${expr}, ${propVar!}, ctx);`);
}
if (block && (ctx.forceNewBlock === false || ctx.tKeyExpr)) {
@@ -1144,15 +1195,7 @@ export class CodeGenerator {
slotName = "'" + ast.name + "'";
}
let scope = null;
if (ast.attrs) {
const params = [];
for (const [n, v] of Object.entries(ast.attrs!)) {
params.push(`${n}: ${compileExpr(v) || undefined}`);
}
scope = `{${params.join(", ")}}`;
}
const scope = ast.attrs ? `{${this.formatPropObject(ast.attrs)}}` : null;
if (ast.defaultContent) {
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope}, ${name})`;
+4 -1
View File
@@ -9,7 +9,10 @@ export type TemplateFunction = (blocks: any, utils: any) => Template;
interface CompileOptions extends Config {
name?: string;
}
export function compile(template: string | Node, options: CompileOptions = {}): TemplateFunction {
export function compile(
template: string | Element,
options: CompileOptions = {}
): TemplateFunction {
// parsing
const ast = parse(template);
+3 -1
View File
@@ -329,7 +329,9 @@ export function compileExprToArray(expr: string): Token[] {
// Mark all variables that have been used locally.
// This assumes the expression has only one scope (incorrect but "good enough for now")
for (const token of tokens) {
if (token.type === "SYMBOL" && localVars.has(token.value)) {
if (token.type === "SYMBOL" && token.varName && localVars.has(token.value)) {
token.originalValue = token.value;
token.value = `_${token.value}`;
token.isLocal = true;
}
}
+60 -28
View File
@@ -33,6 +33,17 @@ export interface ASTComment {
value: string;
}
interface TModelInfo {
hasDynamicChildren?: boolean;
baseExpr: string;
expr: string;
targetAttr: string;
specialInitTargetAttr: string | null;
eventType: "change" | "click" | "input";
shouldTrim: boolean;
shouldNumberize: boolean;
}
export interface ASTDomNode {
type: ASTType.DomNode;
tag: string;
@@ -41,15 +52,7 @@ export interface ASTDomNode {
content: AST[];
ref: string | null;
on: { [key: string]: string };
model: {
baseExpr: string;
expr: string;
targetAttr: string;
specialInitTargetAttr: string | null;
eventType: "change" | "click" | "input";
shouldTrim: boolean;
shouldNumberize: boolean;
} | null;
model?: TModelInfo | null;
ns: string | null;
}
@@ -177,23 +180,35 @@ export type AST =
// -----------------------------------------------------------------------------
// Parser
// -----------------------------------------------------------------------------
interface ParsingContext {
inPreTag: boolean;
inSVG: boolean;
}
const cache: WeakMap<Element, AST> = new WeakMap();
export function parse(xml: string | Node): AST {
const node = xml instanceof Element ? xml : (parseXML(`<t>${xml}</t>`).firstChild! as Element);
normalizeXML(node);
const ctx = { inPreTag: false, inSVG: false };
const ast = parseNode(node, ctx);
export function parse(xml: string | Element): AST {
if (typeof xml === "string") {
const elem = parseXML(`<t>${xml}</t>`).firstChild as Element;
return _parse(elem);
}
let ast = cache.get(xml);
if (!ast) {
return { type: ASTType.Text, value: "" };
// we clone here the xml to prevent modifying it in place
ast = _parse(xml.cloneNode(true) as Element);
cache.set(xml, ast);
}
return ast;
}
function parseNode(node: ChildNode, ctx: ParsingContext): AST | null {
function _parse(xml: Element): AST {
normalizeXML(xml);
const ctx = { inPreTag: false, inSVG: false };
return parseNode(xml, ctx) || { type: ASTType.Text, value: "" };
}
interface ParsingContext {
tModelInfo?: TModelInfo | null;
inPreTag: boolean;
inSVG: boolean;
}
function parseNode(node: Node, ctx: ParsingContext): AST | null {
if (!(node instanceof Element)) {
return parseTextCommentNode(node, ctx);
}
@@ -233,7 +248,7 @@ function parseTNode(node: Element, ctx: ParsingContext): AST | null {
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null {
function parseTextCommentNode(node: Node, ctx: ParsingContext): AST | null {
if (node.nodeType === Node.TEXT_NODE) {
let value = node.textContent || "";
if (!ctx.inPreTag) {
@@ -281,6 +296,8 @@ function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null {
const hasDotAtTheEnd = /\.[\w_]+\s*$/;
const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
const ROOT_SVG_TAGS = new Set(["svg", "g", "path"]);
function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
const { tagName } = node;
const dynamicTag = node.getAttribute("t-tag");
@@ -292,18 +309,16 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
if (tagName === "pre") {
ctx.inPreTag = true;
}
const shouldAddSVGNS = tagName === "svg" || (tagName === "g" && !ctx.inSVG);
const shouldAddSVGNS = ROOT_SVG_TAGS.has(tagName) && !ctx.inSVG;
ctx.inSVG = ctx.inSVG || shouldAddSVGNS;
const ns = shouldAddSVGNS ? "http://www.w3.org/2000/svg" : null;
const ref = node.getAttribute("t-ref");
node.removeAttribute("t-ref");
const children = parseChildren(node, ctx);
const nodeAttrsNames = node.getAttributeNames();
const attrs: ASTDomNode["attrs"] = {};
const on: ASTDomNode["on"] = {};
let model: ASTDomNode["model"] = null;
let model: TModelInfo | null = null;
for (let attr of nodeAttrsNames) {
const value = node.getAttribute(attr)!;
@@ -351,13 +366,24 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
};
} else {
if (isSelect) {
// don't pollute the original ctx
ctx = Object.assign({}, ctx);
ctx.tModelInfo = model;
}
} else if (attr !== "t-name") {
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
throw new Error(`Unknown QWeb directive: '${attr}'`);
}
const tModel = ctx.tModelInfo;
if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
tModel.hasDynamicChildren = true;
}
attrs[attr] = value;
}
}
const children = parseChildren(node, ctx);
return {
type: ASTType.DomNode,
tag: tagName,
@@ -651,6 +677,9 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
const dynamicProps = node.getAttribute("t-props");
node.removeAttribute("t-props");
const defaultSlotScope = node.getAttribute("t-slot-scope");
node.removeAttribute("t-slot-scope");
const props: ASTComponent["props"] = {};
for (let name of node.getAttributeNames()) {
const value = node.getAttribute(name)!;
@@ -716,6 +745,9 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
const defaultContent = parseChildNodes(clone, ctx);
if (defaultContent) {
slots.default = { content: defaultContent };
if (defaultSlotScope) {
slots.default.scope = defaultSlotScope;
}
}
}
return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots };
@@ -786,7 +818,7 @@ function parseTPortal(node: Element, ctx: ParsingContext): AST | null {
/**
* Parse all the child nodes of a given node and return a list of ast elements
*/
function parseChildren(node: Node, ctx: ParsingContext): AST[] {
function parseChildren(node: Element, ctx: ParsingContext): AST[] {
const children: AST[] = [];
for (let child of node.childNodes) {
const childAst = parseNode(child, ctx);
@@ -805,7 +837,7 @@ function parseChildren(node: Node, ctx: ParsingContext): AST[] {
* Parse all the child nodes of a given node and return an ast if possible.
* In the case there are multiple children, they are wrapped in a astmulti.
*/
function parseChildNodes(node: Node, ctx: ParsingContext): AST | null {
function parseChildNodes(node: Element, ctx: ParsingContext): AST | null {
const children = parseChildren(node, ctx);
switch (children.length) {
case 0:
+16
View File
@@ -4,9 +4,25 @@ import type { ComponentNode } from "./component_node";
// Component Class
// -----------------------------------------------------------------------------
type Props = { [key: string]: any };
interface StaticComponentProperties {
template: string;
defaultProps?: any;
props?: any;
}
export type ComponentConstructor<P extends Props = any, E = any> = (new (
props: P,
env: E,
node: ComponentNode
) => Component<P, E>) &
StaticComponentProperties;
export class Component<Props = any, Env = any> {
static template: string = "";
static props?: any;
static defaultProps?: any;
props: Props;
env: Env;
+9 -7
View File
@@ -1,6 +1,6 @@
import type { App, Env } from "../app/app";
import { BDom, VNode } from "../blockdom";
import { Component } from "./component";
import { Component, ComponentConstructor } from "./component";
import {
Fiber,
makeChildFiber,
@@ -15,7 +15,10 @@ import { STATUS } from "./status";
let currentNode: ComponentNode | null = null;
export function getCurrent(): ComponentNode | null {
export function getCurrent(): ComponentNode {
if (!currentNode) {
throw new Error("No active component (a hook function should only be called in 'setup')");
}
return currentNode;
}
@@ -74,13 +77,11 @@ export function component(
type LifecycleHook = Function;
export class ComponentNode<T extends typeof Component = typeof Component>
implements VNode<ComponentNode>
{
export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E>> {
el?: HTMLElement | Text | undefined;
app: App;
fiber: Fiber | null = null;
component: InstanceType<T>;
component: Component<P, E>;
bdom: BDom | null = null;
status: STATUS = STATUS.NEW;
@@ -99,7 +100,7 @@ export class ComponentNode<T extends typeof Component = typeof Component>
patched: LifecycleHook[] = [];
willDestroy: LifecycleHook[] = [];
constructor(C: T, props: any, app: App, parent?: ComponentNode) {
constructor(C: ComponentConstructor<P, E>, props: P, app: App, parent?: ComponentNode) {
currentNode = this;
this.app = app;
this.parent = parent || null;
@@ -110,6 +111,7 @@ export class ComponentNode<T extends typeof Component = typeof Component>
this.component = new C(props, env, this) as any;
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
this.component.setup();
currentNode = null;
}
mountComponent(target: any, options?: MountOptions) {
+1
View File
@@ -159,6 +159,7 @@ export class MountFiber extends RootFiber {
let current: Fiber | undefined = this;
try {
const node = this.node;
(node.app.constructor as any).validateTarget(this.target);
if (node.bdom) {
// this is a complicated situation: if we mount a fiber with an existing
// bdom, this means that this same fiber was already completed, mounted,
+10 -10
View File
@@ -6,42 +6,42 @@ import { nodeErrorHandlers } from "./error_handling";
// -----------------------------------------------------------------------------
export function onWillStart(fn: () => Promise<void> | void | any) {
const node = getCurrent()!;
const node = getCurrent();
node.willStart.push(fn.bind(node.component));
}
export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) {
const node = getCurrent()!;
const node = getCurrent();
node.willUpdateProps.push(fn.bind(node.component));
}
export function onMounted(fn: () => void | any) {
const node = getCurrent()!;
const node = getCurrent();
node.mounted.push(fn.bind(node.component));
}
export function onWillPatch(fn: () => Promise<void> | any | void) {
const node = getCurrent()!;
const node = getCurrent();
node.willPatch.unshift(fn.bind(node.component));
}
export function onPatched(fn: () => void | any) {
const node = getCurrent()!;
const node = getCurrent();
node.patched.push(fn.bind(node.component));
}
export function onWillUnmount(fn: () => Promise<void> | void | any) {
const node = getCurrent()!;
const node = getCurrent();
node.willUnmount.unshift(fn.bind(node.component));
}
export function onWillDestroy(fn: () => Promise<void> | void | any) {
const node = getCurrent()!;
const node = getCurrent();
node.willDestroy.push(fn.bind(node.component));
}
export function onWillRender(fn: () => void | any) {
const node = getCurrent()!;
const node = getCurrent();
const renderFn = node.renderFn;
node.renderFn = () => {
fn.call(node.component);
@@ -50,7 +50,7 @@ export function onWillRender(fn: () => void | any) {
}
export function onRendered(fn: () => void | any) {
const node = getCurrent()!;
const node = getCurrent();
const renderFn = node.renderFn;
node.renderFn = () => {
const result = renderFn();
@@ -61,7 +61,7 @@ export function onRendered(fn: () => void | any) {
type OnErrorCallback = (error: any) => void | any;
export function onError(callback: OnErrorCallback) {
const node = getCurrent()!;
const node = getCurrent();
let handlers = nodeErrorHandlers.get(node);
if (!handlers) {
handlers = [];
+30 -13
View File
@@ -1,16 +1,16 @@
import { Component } from "./component";
import { ComponentConstructor } from "./component";
/**
* Apply default props (only top level).
*
* Note that this method does modify in place the props
*/
export function applyDefaultProps(props: { [key: string]: any }, ComponentClass: typeof Component) {
const defaultProps = (ComponentClass as any).defaultProps;
export function applyDefaultProps<P>(props: P, ComponentClass: ComponentConstructor<P>) {
const defaultProps = ComponentClass.defaultProps;
if (defaultProps) {
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
if ((props as any)[propName] === undefined) {
(props as any)[propName] = defaultProps[propName];
}
}
}
@@ -34,13 +34,20 @@ function getPropDescription(staticProps: any) {
* visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode.
*/
export const validateProps = function (name: string | typeof Component, props: any, parent?: any) {
const ComponentClass = (
typeof name !== "string" ? name : parent.constructor.components[name]
) as typeof Component;
export function validateProps<P>(name: string | ComponentConstructor<P>, props: P, parent?: any) {
const ComponentClass =
typeof name !== "string"
? name
: (parent.constructor.components[name] as ComponentConstructor<P> | undefined);
if (!ComponentClass) {
// this is an error, wrong component. We silently return here instead so the
// error is triggered by the usual path ('component' function)
return;
}
applyDefaultProps(props, ComponentClass);
const defaultProps = ComponentClass.defaultProps || {};
let propsDef = getPropDescription(ComponentClass.props);
const allowAdditionalProps = "*" in propsDef;
@@ -48,8 +55,18 @@ export const validateProps = function (name: string | typeof Component, props: a
if (propName === "*") {
continue;
}
if (props[propName] === undefined) {
if (propsDef[propName] && !propsDef[propName].optional) {
const propDef = propsDef[propName];
let isMandatory = !!propDef;
if (typeof propDef === "object" && "optional" in propDef) {
isMandatory = !propDef.optional;
}
if (isMandatory && propName in defaultProps) {
throw new Error(
`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name})`
);
}
if ((props as any)[propName] === undefined) {
if (isMandatory) {
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
} else {
continue;
@@ -57,7 +74,7 @@ export const validateProps = function (name: string | typeof Component, props: a
}
let isValid;
try {
isValid = isValidProp(props[propName], propsDef[propName]);
isValid = isValidProp((props as any)[propName], propDef);
} catch (e) {
(e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
(e as Error).message
@@ -75,7 +92,7 @@ export const validateProps = function (name: string | typeof Component, props: a
}
}
}
};
}
/**
* Check if an invidual prop value matches its (static) prop definition
+16 -7
View File
@@ -11,7 +11,7 @@ import { onMounted, onPatched, onWillUnmount } from "./component/lifecycle_hooks
* html node or component.
*/
export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el: T | null } {
const node = getCurrent()!;
const node = getCurrent();
const refs = node.refs;
return {
get el(): T | null {
@@ -29,7 +29,13 @@ export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el:
* need a reference to the env of the component calling them.
*/
export function useEnv<E extends Env>(): E {
return getCurrent()!.component.env as any;
return getCurrent().component.env as any;
}
function extendEnv(currentEnv: Object, extension: Object): Object {
const env = Object.create(currentEnv);
const descrs = Object.getOwnPropertyDescriptors(extension);
return Object.freeze(Object.defineProperties(env, descrs));
}
/**
@@ -38,12 +44,15 @@ export function useEnv<E extends Env>(): E {
* constructor method.
*/
export function useSubEnv(envExtension: Env) {
const node = getCurrent()!;
const env = Object.create(node.childEnv);
const descrs = Object.getOwnPropertyDescriptors(envExtension);
node.childEnv = Object.freeze(Object.defineProperties(env, descrs));
const node = getCurrent();
node.component.env = extendEnv(node.component.env as any, envExtension);
useChildSubEnv(envExtension);
}
export function useChildSubEnv(envExtension: Env) {
const node = getCurrent();
node.childEnv = extendEnv(node.childEnv, envExtension);
}
// -----------------------------------------------------------------------------
// useEffect
// -----------------------------------------------------------------------------
@@ -112,7 +121,7 @@ export function useExternalListener(
handler: EventListener,
eventParams?: AddEventListenerOptions
) {
const node = getCurrent()!;
const node = getCurrent();
const boundHandler = handler.bind(node.component);
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
+2 -2
View File
@@ -42,8 +42,8 @@ export { useComponent } from "./component/component_node";
export { status } from "./component/status";
export { Memo } from "./memo";
export { xml } from "./app/template_set";
export { useState, reactive } from "./reactivity";
export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks";
export { useState, reactive, markRaw, toRaw } from "./reactivity";
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
export { EventBus, whenReady, loadFile, markup } from "./utils";
export {
onWillStart,
+49 -13
View File
@@ -1,18 +1,27 @@
import { onWillUnmount } from "./component/lifecycle_hooks";
import { onWillDestroy } from "./component/lifecycle_hooks";
import { ComponentNode, getCurrent } from "./component/component_node";
import { batched, Callback } from "./utils";
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
const TARGET = Symbol("Target");
// Escape hatch to prevent reactivity system to turn something into a reactive
const SKIP = Symbol("Skip");
// Special key to subscribe to, to be notified of key creation/deletion
const KEYCHANGES = Symbol("Key changes");
type ObjectKey = string | number | symbol;
type Target = object;
export type Reactive<T extends Target = Target> = T & {
[TARGET]: any;
};
type NonReactive<T extends Target = Target> = T & {
[SKIP]: any;
};
const objectToString = Object.prototype.toString;
/**
* Checks whether a given value can be made into a reactive object.
*
@@ -20,14 +29,35 @@ export type Reactive<T extends Target = Target> = T & {
* @returns whether the value can be made reactive
*/
function canBeMadeReactive(value: any): boolean {
return (
typeof value === "object" &&
value !== null &&
!(value instanceof Date) &&
!(value instanceof Promise) &&
!(value instanceof String) &&
!(value instanceof Number)
);
if (typeof value !== "object") {
return false;
}
// extract "RawType" from strings like "[object RawType]" => this lets us
// ignore many native objects such as Promise (whose toString is [object Promise])
// or Date ([object Date]).
const rawType = objectToString.call(value).slice(8, -1);
return rawType === "Object" || rawType === "Array";
}
/**
* Mark an object or array so that it is ignored by the reactivity system
*
* @param value the value to mark
* @returns the object itself
*/
export function markRaw<T extends Target>(value: T): NonReactive<T> {
(value as any)[SKIP] = true;
return value as NonReactive<T>;
}
/**
* Given a reactive objet, return the raw (non reactive) underlying object
*
* @param value a reactive value
* @returns the underlying value
*/
export function toRaw<T extends object>(value: Reactive<T>): T {
return value[TARGET];
}
const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>>>();
@@ -130,10 +160,16 @@ const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive>>();
* reactive has changed
* @returns a proxy that tracks changes to it
*/
export function reactive<T extends Target>(target: T, callback: Callback = () => {}): Reactive<T> {
export function reactive<T extends Target>(
target: T,
callback: Callback = () => {}
): Reactive<T> | NonReactive<T> {
if (!canBeMadeReactive(target)) {
throw new Error(`Cannot make the given value reactive`);
}
if (SKIP in target) {
return target as NonReactive<T>;
}
const originalTarget = (target as Reactive)[TARGET];
if (originalTarget) {
return reactive(originalTarget, callback);
@@ -202,14 +238,14 @@ const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
* relevant changes
* @see reactive
*/
export function useState<T extends object>(state: T): Reactive<T> {
const node = getCurrent()!;
export function useState<T extends object>(state: T): Reactive<T> | NonReactive<T> {
const node = getCurrent();
if (!batchedRenderFunctions.has(node)) {
batchedRenderFunctions.set(
node,
batched(() => node.render())
);
onWillUnmount(() => clearReactivesForCallback(render));
onWillDestroy(() => clearReactivesForCallback(render));
}
const render = batchedRenderFunctions.get(node)!;
const reactiveState = reactive(state, render);
+9
View File
@@ -24,6 +24,15 @@ export function batched(callback: Callback): Callback {
};
}
export function validateTarget(target: HTMLElement) {
if (!(target instanceof HTMLElement)) {
throw new Error("Cannot mount component: the target is not a valid DOM element");
}
if (!document.body.contains(target)) {
throw new Error("Cannot mount a component on a detached dom node");
}
}
export class EventBus extends EventTarget {
trigger(name: string, payload?: any) {
this.dispatchEvent(new CustomEvent(name, { detail: payload }));
+13
View File
@@ -1,4 +1,5 @@
import { createBlock, mount, patch, remove } from "../../src/blockdom";
import { logStep } from "../helpers";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
@@ -55,3 +56,15 @@ test("is in dom when callback is called", async () => {
mount(tree, fixture);
});
test("callback ref in callback ref with same block", async () => {
const block = createBlock('<p block-ref="0"><block-text-1/><block-child-0/></p>');
let refFn = (el: HTMLParagraphElement) => logStep(el.outerHTML);
const child = block([refFn, "child"], []);
const parent = block([refFn, "parent"], [child]);
mount(parent, fixture);
expect(fixture.innerHTML).toBe("<p>parent<p>child</p></p>");
expect(["<p>child</p>", "<p>parent<p>child</p></p>"]).toBeLogged();
});
@@ -353,7 +353,7 @@ exports[`t-on t-on modifiers (native listener) t-on with prevent modifier in t-f
let key1 = ctx['project'];
const v1 = ctx['onEdit'];
const v2 = ctx['project'];
let hdlr1 = [\\"prevent\\", ev=>v1(v2.id,ev), ctx];
let hdlr1 = [\\"prevent\\", _ev=>v1(v2.id,_ev), ctx];
let txt1 = ctx['project'].name;
c_block2[i1] = withKey(block3([hdlr1, txt1]), key1);
}
@@ -30,11 +30,11 @@ exports[`misc complex template 1`] = `
b3 = block3();
}
ctx = Object.create(ctx);
const [k_block4, v_block4, l_block4, c_block4] = prepareList(ctx['batch'].slot_ids.filter(slot=>slot.build_id.id&&!slot.trigger_id.manual&&(ctx['options'].trigger_display[slot.trigger_id.id])));
const [k_block4, v_block4, l_block4, c_block4] = prepareList(ctx['batch'].slot_ids.filter(_slot=>_slot.build_id.id&&!_slot.trigger_id.manual&&(ctx['options'].trigger_display[_slot.trigger_id.id])));
for (let i1 = 0; i1 < l_block4; i1++) {
ctx[\`slot\`] = v_block4[i1];
let key1 = ctx['slot'].id;
c_block4[i1] = withKey(component(\`SlotButton\`, {class: ctx['slot_container'],slot: ctx['slot']}, key + \`__1__\${key1}\`, node, ctx), key1);
c_block4[i1] = withKey(component(\`SlotButton\`, {class: ctx['slot_container'], slot: ctx['slot']}, key + \`__1__\${key1}\`, node, ctx), key1);
}
ctx = ctx.__proto__;
b4 = list(c_block4);
@@ -316,8 +316,8 @@ exports[`misc other complex template 1`] = `
if (!ctx['project']) {
b24 = block24();
} else {
let b26 = component(\`BundlesList\`, {bundles: ctx['bundles'].sticky,category_custom_views: ctx['category_custom_views'],search: ctx['search']}, key + \`__2\`, node, ctx);
let b27 = component(\`BundlesList\`, {bundles: ctx['bundles'].dev,search: ctx['search']}, key + \`__3\`, node, ctx);
let b26 = component(\`BundlesList\`, {bundles: ctx['bundles'].sticky, category_custom_views: ctx['category_custom_views'], search: ctx['search']}, key + \`__2\`, node, ctx);
let b27 = component(\`BundlesList\`, {bundles: ctx['bundles'].dev, search: ctx['search']}, key + \`__3\`, node, ctx);
b25 = block25([], [b26, b27]);
}
return block1([attr1, txt1, hdlr2, hdlr3, attr8, hdlr4, hdlr5, ref1, hdlr6, ref2], [b2, b4, b14, b17, b22, b23, b24, b25]);
@@ -52,6 +52,70 @@ exports[`properly support svg namespace to svg tags added even if already in svg
}"
`;
exports[`properly support svg svg creates new block if it is within html -- 2 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><polygon fill=\\"#000000\\" points=\\"0 0 4 4 8 0\\" transform=\\"translate(5 7)\\"/><block-child-0/></svg>\`);
let block3 = createBlock(\`<path block-ns=\\"http://www.w3.org/2000/svg\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let b3;
if (ctx['hasPath']) {
b3 = block3();
}
let b2 = block2([], [b3]);
return block1([], [b2]);
}
}"
`;
exports[`properly support svg svg creates new block if it is within html 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><polygon fill=\\"#000000\\" points=\\"0 0 4 4 8 0\\" transform=\\"translate(5 7)\\"/></svg>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = block2();
return block1([], [b2]);
}
}"
`;
exports[`properly support svg svg namespace added to sub templates if root tag is path 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`path\`);
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><block-child-0/></svg>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
`;
exports[`properly support svg svg namespace added to sub templates if root tag is path 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<path block-ns=\\"http://www.w3.org/2000/svg\\"/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg svg namespace added to sub-blocks 1`] = `
"function anonymous(bdom, helpers
) {
@@ -407,6 +407,36 @@ exports[`t-out variable 1`] = `
}"
`;
exports[`t-out with a String class 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-out with an extended String class 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-raw is deprecated should warn 1`] = `
"function anonymous(bdom, helpers
) {
@@ -1,5 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loading templates addTemplates does not modify its xml document in place 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['value'];
return block1([txt1]);
}
}"
`;
exports[`loading templates can initialize qweb with a string 1`] = `
"function anonymous(bdom, helpers
) {
@@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`translation support can set translatable attributes 1`] = `
exports[`translation support can set and remove translatable attributes 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div tomato=\\"word\\" potato=\\"mot\\" title=\\"word\\">text</div>\`);
let block1 = createBlock(\`<div tomato=\\"word\\" potato=\\"mot\\" title=\\"mot\\" label=\\"word\\">text</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
+10 -6
View File
@@ -163,17 +163,21 @@ describe("expression evaluation", () => {
});
test("arrow functions", () => {
expect(compileExpr("list.map(e => e.val)")).toBe("ctx['list'].map(e=>e.val)");
expect(compileExpr("list.map(e => a + e)")).toBe("ctx['list'].map(e=>ctx['a']+e)");
expect(compileExpr("list.map((e) => e)")).toBe("ctx['list'].map((e)=>e)");
expect(compileExpr("list.map(e => e.val)")).toBe("ctx['list'].map(_e=>_e.val)");
expect(compileExpr("list.map(e => a + e)")).toBe("ctx['list'].map(_e=>ctx['a']+_e)");
expect(compileExpr("list.map((e) => e)")).toBe("ctx['list'].map((_e)=>_e)");
expect(compileExpr("list.map((elem, index) => elem + index)")).toBe(
"ctx['list'].map((elem,index)=>elem+index)"
"ctx['list'].map((_elem,_index)=>_elem+_index)"
);
expect(compileExpr("(ev => ev)(e)")).toBe("(_ev=>_ev)(ctx['e'])");
expect(compileExpr("(v1) => myFunc(v1)")).toBe("(_v1)=>ctx['myFunc'](_v1)");
expect(compileExpr("list.data.map((data) => data)")).toBe(
"ctx['list'].data.map((_data)=>_data)"
);
expect(compileExpr("(ev => ev)(e)")).toBe("(ev=>ev)(ctx['e'])");
});
test.skip("arrow functions: not yet supported", () => {
// e is added to localvars in inline_expression but not removed after the arrow func body
expect(compileExpr("(e => e)(e)")).toBe("(e=>e)(ctx['e'])");
expect(compileExpr("(e => e)(e)")).toBe("(_e=>_e)(ctx['e'])");
});
test("assignation", () => {
+75
View File
@@ -1132,6 +1132,81 @@ describe("qweb parser", () => {
});
});
// ---------------------------------------------------------------------------
// t-model
// ---------------------------------------------------------------------------
test("t-model select", async () => {
expect(parse(`<select t-model="state.model"><option value="1" /></select>`)).toEqual({
type: 2,
tag: "select",
dynamicTag: null,
attrs: {},
on: {},
ref: null,
content: [
{
type: 2,
tag: "option",
dynamicTag: null,
attrs: { value: "1" },
on: {},
ref: null,
content: [],
model: null,
ns: null,
},
],
model: {
baseExpr: "state",
expr: "'model'",
targetAttr: "value",
specialInitTargetAttr: null,
eventType: "change",
shouldTrim: false,
shouldNumberize: false,
},
ns: null,
});
});
test("t-model select dynamic options", async () => {
expect(
parse(`<select t-model="state.model"><option t-att-value="valueVar" /></select>`)
).toEqual({
type: 2,
tag: "select",
dynamicTag: null,
attrs: {},
on: {},
ref: null,
content: [
{
type: 2,
tag: "option",
dynamicTag: null,
attrs: { "t-att-value": "valueVar" },
on: {},
ref: null,
content: [],
model: null,
ns: null,
},
],
model: {
baseExpr: "state",
expr: "'model'",
targetAttr: "value",
specialInitTargetAttr: null,
eventType: "change",
shouldTrim: false,
shouldNumberize: false,
hasDynamicChildren: true,
},
ns: null,
});
});
// ---------------------------------------------------------------------------
// t-component
// ---------------------------------------------------------------------------
+62
View File
@@ -1,5 +1,7 @@
import { renderToString, renderToBdom, snapshotEverything, makeTestFixture } from "../helpers";
import { mount } from "../../src/blockdom";
import { mount as mountComponent, Component, xml } from "../../src/index";
// NB: check the snapshots to see where the SVG namespaces are added
snapshotEverything();
@@ -52,4 +54,64 @@ describe("properly support svg", () => {
expect(el.namespaceURI).toBe("http://www.w3.org/2000/svg");
}
});
test("svg namespace added to sub templates if root tag is path", async () => {
const templates = `<t>
<t t-name="svg"><svg><t t-call="path" /></svg></t>
<t t-name="path"><path /></t>
</t>
`;
const fixture = makeTestFixture();
class Svg extends Component {
static template = "svg";
}
await mountComponent(Svg, fixture, { templates });
const elems = fixture.querySelectorAll("svg, path");
expect(elems.length).toEqual(2);
for (const el of elems) {
expect(el.namespaceURI).toBe("http://www.w3.org/2000/svg");
}
});
test("svg creates new block if it is within html", async () => {
class Test extends Component {
static template = xml`
<div>
<svg>
<polygon fill="#000000" points="0 0 4 4 8 0" transform="translate(5 7)"/>
</svg>
</div>
`;
}
const fixture = makeTestFixture();
await mountComponent(Test, fixture);
const elems = fixture.querySelectorAll("svg, polygon");
expect(elems.length).toEqual(2);
for (const el of elems) {
expect(el.namespaceURI).toBe("http://www.w3.org/2000/svg");
}
});
test("svg creates new block if it is within html -- 2", async () => {
class Test extends Component {
static template = xml`
<div>
<svg>
<polygon fill="#000000" points="0 0 4 4 8 0" transform="translate(5 7)"/>
<path t-if="hasPath" />
</svg>
</div>
`;
hasPath = true;
}
const fixture = makeTestFixture();
await mountComponent(Test, fixture);
const elems = fixture.querySelectorAll("svg, polygon, path");
expect(elems.length).toEqual(3);
for (const el of elems) {
expect(el.namespaceURI).toBe("http://www.w3.org/2000/svg");
}
});
});
+20
View File
@@ -37,6 +37,26 @@ describe("t-out", () => {
expect(renderToString(template, { var: "ok" })).toBe("<span>ok</span>");
});
test("with a String class", () => {
const template = `<span><t t-out="var"/></span>`;
expect(renderToString(template, { var: new String("ok") })).toBe("<span>ok</span>");
});
test("with an extended String class", () => {
class LoveString extends String {
valueOf(): string {
return `<3 ${super.valueOf()} <3`;
}
toString(): string {
return this.valueOf();
}
}
const template = `<span><t t-out="var"/></span>`;
expect(renderToString(template, { var: new LoveString("ok") })).toBe(
"<span>&lt;3 ok &lt;3</span>"
);
});
test("not escaping", () => {
const template = `<div><t t-out="var"/></div>`;
expect(renderToString(template, { var: markup("<ok></ok>") })).toBe("<div><ok></ok></div>");
+11
View File
@@ -24,6 +24,17 @@ describe("loading templates", () => {
expect(context.renderToString("hey")).toBe("<div>jupiler</div>");
});
test("addTemplates does not modify its xml document in place", () => {
const data = `<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve"><div t-name="hey"><t t-esc="value"/></div></templates>`;
const xml = new DOMParser().parseFromString(data, "text/xml");
const context = new TestContext();
expect(xml.firstElementChild!.innerHTML).toBe(`<div t-name="hey"><t t-esc="value"/></div>`);
context.addTemplates(xml);
expect(context.renderToString("hey", { value: 123 })).toBe("<div>123</div>");
expect(xml.firstElementChild!.innerHTML).toBe(`<div t-name="hey"><t t-esc="value"/></div>`);
});
test("can load a few templates from a xml string", () => {
const data = `<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
+6 -4
View File
@@ -59,18 +59,20 @@ describe("translation support", () => {
);
});
test("can set translatable attributes", async () => {
test("can set and remove translatable attributes", async () => {
class SomeComponent extends Component {
static template = xml`
<div tomato="word" potato="word" title="word">text</div>
<div tomato="word" potato="word" title="word" label="word">text</div>
`;
}
await mount(SomeComponent, fixture, {
translateFn: (expr: string) => (expr === "word" ? "mot" : expr),
translatableAttributes: ["potato"],
translatableAttributes: ["potato", "-label"],
});
expect(fixture.innerHTML).toBe('<div tomato="word" potato="mot" title="word">text</div>');
expect(fixture.innerHTML).toBe(
'<div tomato="word" potato="mot" title="mot" label="word">text</div>'
);
});
test("translation is done on the trimmed text, with extra spaces readded after", async () => {
@@ -81,6 +81,19 @@ exports[`basics a class component inside a class component, no external dom 2`]
}"
`;
exports[`basics a component cannot be mounted in a detached node (even if node is detached later) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`basics a component inside a component 1`] = `
"function anonymous(bdom, helpers
) {
@@ -968,7 +981,7 @@ exports[`basics update props of component without concrete own node 1`] = `
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['childProps'].key;
let b2 = toggler(tKey_1, component(\`Child\`, ctx['childProps'], tKey_1 + key + \`__1\`, node, ctx));
let b2 = toggler(tKey_1, component(\`Child\`, Object.assign({}, ctx['childProps']), tKey_1 + key + \`__1\`, node, ctx));
return block1([], [b2]);
}
}"
@@ -981,7 +994,7 @@ exports[`basics update props of component without concrete own node 2`] = `
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['props'].subKey;
return toggler(tKey_1, component(\`Custom\`, {key: ctx['props'].key,subKey: ctx['props'].subKey}, tKey_1 + key + \`__1\`, node, ctx));
return toggler(tKey_1, component(\`Custom\`, {key: ctx['props'].key, subKey: ctx['props'].subKey}, tKey_1 + key + \`__1\`, node, ctx));
}
}"
`;
@@ -1217,6 +1230,20 @@ exports[`t-out in components can render list of t-out 1`] = `
}"
`;
exports[`t-out in components can switch the contents of two t-out repeatedly 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
let b2 = safeOutput(ctx['state'].a);
let b3 = safeOutput(ctx['state'].b);
return multi([b2, b3]);
}
}"
`;
exports[`t-out in components update properly on state changes 1`] = `
"function anonymous(bdom, helpers
) {
@@ -183,7 +183,7 @@ exports[`concurrent renderings scenario 1 2`] = `
let block1 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA,fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx);
let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA, fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -227,7 +227,7 @@ exports[`concurrent renderings scenario 2 2`] = `
let block1 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA,fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx);
let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA, fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -270,7 +270,7 @@ exports[`concurrent renderings scenario 2bis 2`] = `
let block1 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA,fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx);
let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA, fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -327,7 +327,7 @@ exports[`concurrent renderings scenario 3 3`] = `
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA,fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx);
let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA, fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -384,7 +384,7 @@ exports[`concurrent renderings scenario 4 3`] = `
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA,fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx);
let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA, fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -557,7 +557,7 @@ exports[`concurrent renderings scenario 9 3`] = `
let block1 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA,fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx);
let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA, fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -734,7 +734,7 @@ exports[`concurrent renderings scenario 14 2`] = `
let block1 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`C\`, {fromB: ctx['state'].fromB,fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx);
let b2 = component(\`C\`, {fromB: ctx['state'].fromB, fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -778,7 +778,7 @@ exports[`concurrent renderings scenario 15 2`] = `
let block1 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`C\`, {fromB: ctx['state'].fromB,fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx);
let b2 = component(\`C\`, {fromB: ctx['state'].fromB, fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -817,7 +817,7 @@ exports[`concurrent renderings scenario 16 2`] = `
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`C\`, {fromB: ctx['state'].fromB,fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx);
return component(\`C\`, {fromB: ctx['state'].fromB, fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx);
}
}"
`;
@@ -1,5 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`basics display a nice error if it cannot find component (in dev mode) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const props1 = {};
helpers.validateProps(\`SomeMispelledComponent\`, props1, ctx);
return component(\`SomeMispelledComponent\`, props1, key + \`__1\`, node, ctx);
}
}"
`;
exports[`basics display a nice error if it cannot find component 1`] = `
"function anonymous(bdom, helpers
) {
@@ -72,6 +85,17 @@ exports[`basics simple catchError 2`] = `
}"
`;
exports[`can catch errors calling a hook outside setup should crash 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors can catch an error in a component render function 1`] = `
"function anonymous(bdom, helpers
) {
@@ -799,7 +823,7 @@ exports[`can catch errors catching in child makes parent render 1`] = `
let key1 = ctx['elem'][0];
const v1 = ctx['elem'];
const ctx1 = capture(ctx);
c_block1[i1] = withKey(component(\`Catch\`, {onError: (error)=>this.onError(v1[0],error),slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2__\${key1}\`, node, ctx), key1);
c_block1[i1] = withKey(component(\`Catch\`, {onError: (_error)=>this.onError(v1[0],_error),slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2__\${key1}\`, node, ctx), key1);
}
return list(c_block1);
}
@@ -105,7 +105,7 @@ exports[`event handling objects from scope are properly captured by t-on 1`] = `
let key1 = ctx['item'];
const v1 = ctx['onClick'];
const v2 = ctx['item'];
let hdlr1 = [ev=>v1(v2.val,ev), ctx];
let hdlr1 = [_ev=>v1(v2.val,_ev), ctx];
c_block2[i1] = withKey(block3([hdlr1]), key1);
}
let b2 = list(c_block2);
@@ -146,7 +146,7 @@ exports[`event handling t-on with handler bound to dynamic argument on a t-forea
let key1 = ctx['item'];
const v1 = ctx['onClick'];
const v2 = ctx['item'];
let hdlr1 = [ev=>v1(v2,ev), ctx];
let hdlr1 = [_ev=>v1(v2,_ev), ctx];
c_block2[i1] = withKey(block3([hdlr1]), key1);
}
let b2 = list(c_block2);
@@ -103,7 +103,7 @@ exports[`hooks mounted callbacks should be called in reverse order from willUnmo
}"
`;
exports[`hooks parent and child env 1`] = `
exports[`hooks parent and child env (with useChildSubEnv then useSubEnv) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -116,7 +116,65 @@ exports[`hooks parent and child env 1`] = `
}"
`;
exports[`hooks parent and child env 2`] = `
exports[`hooks parent and child env (with useChildSubEnv then useSubEnv) 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block2 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2;
if (ctx['env'].hasParent) {
let txt1 = ctx['env'].val;
b2 = block2([txt1]);
}
return multi([b2]);
}
}"
`;
exports[`hooks parent and child env (with useChildSubEnv) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2 = text(ctx['env'].val);
let b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`hooks parent and child env (with useChildSubEnv) 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['env'].val;
return block1([txt1]);
}
}"
`;
exports[`hooks parent and child env (with useSubEnv) 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2 = text(ctx['env'].val);
let b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`hooks parent and child env (with useSubEnv) 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -144,7 +202,7 @@ exports[`hooks two different call to willPatch/patched should work 1`] = `
}"
`;
exports[`hooks use sub env does not pollute user env 1`] = `
exports[`hooks useChildSubEnv does not pollute user env 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -158,7 +216,7 @@ exports[`hooks use sub env does not pollute user env 1`] = `
}"
`;
exports[`hooks use sub env supports arbitrary descriptor 1`] = `
exports[`hooks useChildSubEnv supports arbitrary descriptor 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -169,7 +227,7 @@ exports[`hooks use sub env supports arbitrary descriptor 1`] = `
}"
`;
exports[`hooks use sub env supports arbitrary descriptor 2`] = `
exports[`hooks useChildSubEnv supports arbitrary descriptor 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -287,3 +345,43 @@ exports[`hooks useRef hook: basic use 1`] = `
}
}"
`;
exports[`hooks useSubEnv modifies user env 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['env'].val;
return block1([txt1]);
}
}"
`;
exports[`hooks useSubEnv supports arbitrary descriptor 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`hooks useSubEnv supports arbitrary descriptor 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/> <block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['env'].someVal;
let txt2 = ctx['env'].someVal2;
return block1([txt1, txt2]);
}
}"
`;
@@ -160,6 +160,34 @@ exports[`lifecycle hooks components are unmounted destroyed if no longer in DOM
}"
`;
exports[`lifecycle hooks destroy new children before being mountged 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3,b4;
b2 = text(\`before\`);
if (ctx['state'].flag) {
b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
b4 = text(\`after\`);
return multi([b2, b3, b4]);
}
}"
`;
exports[`lifecycle hooks destroy new children before being mountged 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`child\`);
}
}"
`;
exports[`lifecycle hooks hooks are called in proper order in widget creation/destruction 1`] = `
"function anonymous(bdom, helpers
) {
@@ -42,7 +42,7 @@ exports[`basics arrow functions as prop correctly capture their scope 1`] = `
let key1 = ctx['item'].val;
const v1 = ctx['onClick'];
const v2 = ctx['item'];
c_block1[i1] = withKey(component(\`Child\`, {onClick: ev=>v1(v2.val,ev)}, key + \`__1__\${key1}\`, node, ctx), key1);
c_block1[i1] = withKey(component(\`Child\`, {onClick: _ev=>v1(v2.val,_ev)}, key + \`__1__\${key1}\`, node, ctx), key1);
}
return list(c_block1);
}
@@ -273,7 +273,7 @@ exports[`bound functions is referentially equal after update 1`] = `
let { bind } = helpers;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {val: ctx['state'].val,fn: bind(ctx, ctx['someFunction'])}, key + \`__1\`, node, ctx);
return component(\`Child\`, {val: ctx['state'].val, fn: bind(ctx, ctx['someFunction'])}, key + \`__1\`, node, ctx);
}
}"
`;
@@ -1,6 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`default props can set default required boolean values 1`] = `
exports[`default props a default prop cannot be defined on a mandatory prop 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const props1 = {};
helpers.validateProps(\`Child\`, props1, ctx);
return component(\`Child\`, props1, key + \`__1\`, node, ctx);
}
}"
`;
exports[`default props can set default boolean values 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -8,15 +21,15 @@ exports[`default props can set default required boolean values 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`default props can set default required boolean values 2`] = `
exports[`default props can set default boolean values 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -44,8 +57,8 @@ exports[`default props can set default values 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -74,8 +87,8 @@ exports[`default props default values are also set whenever component is updated
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['state'].p}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['state'].p};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -102,8 +115,8 @@ exports[`props validation can specify that additional props are allowed (array)
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const props1 = {message: 'm',otherProp: 'o'}
helpers.validateProps(\`Child\`, props1, ctx)
const props1 = {message: 'm', otherProp: 'o'};
helpers.validateProps(\`Child\`, props1, ctx);
return component(\`Child\`, props1, key + \`__1\`, node, ctx);
}
}"
@@ -128,8 +141,8 @@ exports[`props validation can specify that additional props are allowed (object)
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const props1 = {message: 'm',otherProp: 'o'}
helpers.validateProps(\`Child\`, props1, ctx)
const props1 = {message: 'm', otherProp: 'o'};
helpers.validateProps(\`Child\`, props1, ctx);
return component(\`Child\`, props1, key + \`__1\`, node, ctx);
}
}"
@@ -156,8 +169,8 @@ exports[`props validation can validate a prop with multiple types 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -185,8 +198,8 @@ exports[`props validation can validate a prop with multiple types 3`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -214,8 +227,8 @@ exports[`props validation can validate a prop with multiple types 5`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -230,8 +243,8 @@ exports[`props validation can validate an array with given primitive type 1`] =
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -259,8 +272,8 @@ exports[`props validation can validate an array with given primitive type 3`] =
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -288,8 +301,8 @@ exports[`props validation can validate an array with given primitive type 5`] =
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -304,8 +317,8 @@ exports[`props validation can validate an array with given primitive type 6`] =
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -320,8 +333,8 @@ exports[`props validation can validate an array with multiple sub element types
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -349,8 +362,8 @@ exports[`props validation can validate an array with multiple sub element types
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -378,8 +391,8 @@ exports[`props validation can validate an array with multiple sub element types
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -407,8 +420,8 @@ exports[`props validation can validate an array with multiple sub element types
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -423,8 +436,8 @@ exports[`props validation can validate an object with simple shape 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -452,8 +465,8 @@ exports[`props validation can validate an object with simple shape 3`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -468,8 +481,8 @@ exports[`props validation can validate an object with simple shape 4`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -484,8 +497,8 @@ exports[`props validation can validate an object with simple shape 5`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -500,8 +513,8 @@ exports[`props validation can validate an optional props 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -529,8 +542,8 @@ exports[`props validation can validate an optional props 3`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -558,8 +571,8 @@ exports[`props validation can validate an optional props 5`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -574,8 +587,8 @@ exports[`props validation can validate recursively complicated prop def 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -603,8 +616,8 @@ exports[`props validation can validate recursively complicated prop def 3`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -632,8 +645,8 @@ exports[`props validation can validate recursively complicated prop def 5`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -648,8 +661,8 @@ exports[`props validation default values are applied before validating props at
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['state'].p}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['state'].p};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -678,8 +691,8 @@ exports[`props validation missing required boolean prop causes an error 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -694,8 +707,8 @@ exports[`props validation mix of optional and mandatory 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {}
helpers.validateProps(\`Child\`, props1, ctx)
const props1 = {};
helpers.validateProps(\`Child\`, props1, ctx);
let b2 = component(\`Child\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -710,8 +723,8 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {message: 1}
helpers.validateProps(\`Child\`, props1, ctx)
const props1 = {message: 1};
helpers.validateProps(\`Child\`, props1, ctx);
let b2 = component(\`Child\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -740,8 +753,8 @@ exports[`props validation props are validated whenever component is updated 1`]
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['state'].p}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['state'].p};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -770,8 +783,8 @@ exports[`props validation props: list of strings 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -786,8 +799,8 @@ exports[`props validation validate simple types 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -802,8 +815,8 @@ exports[`props validation validate simple types 2`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -831,8 +844,8 @@ exports[`props validation validate simple types 4`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -847,8 +860,8 @@ exports[`props validation validate simple types 5`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -863,8 +876,8 @@ exports[`props validation validate simple types 6`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -892,8 +905,8 @@ exports[`props validation validate simple types 8`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -908,8 +921,8 @@ exports[`props validation validate simple types 9`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -924,8 +937,8 @@ exports[`props validation validate simple types 10`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -953,8 +966,8 @@ exports[`props validation validate simple types 12`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -969,8 +982,8 @@ exports[`props validation validate simple types 13`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -985,8 +998,8 @@ exports[`props validation validate simple types 14`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1014,8 +1027,8 @@ exports[`props validation validate simple types 16`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1030,8 +1043,8 @@ exports[`props validation validate simple types 17`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1046,8 +1059,8 @@ exports[`props validation validate simple types 18`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1075,8 +1088,8 @@ exports[`props validation validate simple types 20`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1091,8 +1104,8 @@ exports[`props validation validate simple types 21`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1107,8 +1120,8 @@ exports[`props validation validate simple types 22`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1136,8 +1149,8 @@ exports[`props validation validate simple types 24`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1152,8 +1165,8 @@ exports[`props validation validate simple types, alternate form 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1168,8 +1181,8 @@ exports[`props validation validate simple types, alternate form 2`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1197,8 +1210,8 @@ exports[`props validation validate simple types, alternate form 4`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1213,8 +1226,8 @@ exports[`props validation validate simple types, alternate form 5`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1229,8 +1242,8 @@ exports[`props validation validate simple types, alternate form 6`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1258,8 +1271,8 @@ exports[`props validation validate simple types, alternate form 8`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1274,8 +1287,8 @@ exports[`props validation validate simple types, alternate form 9`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1290,8 +1303,8 @@ exports[`props validation validate simple types, alternate form 10`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1319,8 +1332,8 @@ exports[`props validation validate simple types, alternate form 12`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1335,8 +1348,8 @@ exports[`props validation validate simple types, alternate form 13`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1351,8 +1364,8 @@ exports[`props validation validate simple types, alternate form 14`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1380,8 +1393,8 @@ exports[`props validation validate simple types, alternate form 16`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1396,8 +1409,8 @@ exports[`props validation validate simple types, alternate form 17`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1412,8 +1425,8 @@ exports[`props validation validate simple types, alternate form 18`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1441,8 +1454,8 @@ exports[`props validation validate simple types, alternate form 20`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1457,8 +1470,8 @@ exports[`props validation validate simple types, alternate form 21`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1473,8 +1486,8 @@ exports[`props validation validate simple types, alternate form 22`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1502,8 +1515,8 @@ exports[`props validation validate simple types, alternate form 24`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {p: ctx['p']};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -1518,8 +1531,8 @@ exports[`props validation validation is only done in dev mode 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props1 = {}
helpers.validateProps(\`SubComp\`, props1, ctx)
const props1 = {};
helpers.validateProps(\`SubComp\`, props1, ctx);
let b2 = component(\`SubComp\`, props1, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
@@ -38,6 +38,26 @@ exports[`refs can use 2 refs with same name in a t-if/t-else situation 1`] = `
}"
`;
exports[`refs refs and recursive templates 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<p block-ref=\\"0\\"><block-text-1/><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
const refs = ctx.__owl__.refs;
const ref1 = (el) => refs[\`root\`] = el;
let b2;
let txt1 = ctx['props'].tree.value;
if (ctx['props'].tree.child) {
b2 = component(\`Test\`, {tree: ctx['props'].tree.child}, key + \`__1\`, node, ctx);
}
return block1([ref1, txt1], [b2]);
}
}"
`;
exports[`refs refs are properly bound in slots 1`] = `
"function anonymous(bdom, helpers
) {
+139 -18
View File
@@ -75,6 +75,37 @@ exports[`slots can define and call slots 2`] = `
}"
`;
exports[`slots can define and call slots with bound params 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { capture, bind } = helpers;
function slot1(ctx, node, key = \\"\\") {
return text(\`abc\`);
}
return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return component(\`Child\`, {slots: {'abc': {__render: slot1, __ctx: ctx1, getValue: bind(ctx, ctx['getValue'])}}}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`slots can define and call slots with bound params 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callSlot } = helpers;
return function template(ctx, node, key = \\"\\") {
let b2 = callSlot(ctx, node, key, 'abc', false, {});
let b3 = text(ctx['props'].slots['abc'].getValue());
return multi([b2, b3]);
}
}"
`;
exports[`slots can define and call slots with params 1`] = `
"function anonymous(bdom, helpers
) {
@@ -424,6 +455,69 @@ exports[`slots default slot next to named slot, with default content 2`] = `
}"
`;
exports[`slots default slot with params with - in it 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
function slot1(ctx, node, key = \\"\\") {
return text(ctx['slotScope']['some-value']);
}
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx, __scope: \\"slotScope\\"}}}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`slots default slot with params with - in it 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callSlot } = helpers;
return function template(ctx, node, key = \\"\\") {
return callSlot(ctx, node, key, 'default', false, {'some-value': ctx['state'].value});
}
}"
`;
exports[`slots default slot with slot scope: shorthand syntax 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
function slot1(ctx, node, key = \\"\\") {
let b2,b3;
if (ctx['slotScope'].bool) {
b2 = text(\`some text\`);
} else {
b3 = text(\`other text\`);
}
return multi([b2, b3]);
}
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx, __scope: \\"slotScope\\"}}}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`slots default slot with slot scope: shorthand syntax 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callSlot } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callSlot(ctx, node, key, 'default', false, {bool: ctx['state'].bool});
return block1([], [b2]);
}
}"
`;
exports[`slots default slot work with text nodes (variation) 1`] = `
"function anonymous(bdom, helpers
) {
@@ -1139,7 +1233,6 @@ exports[`slots simple default slot with params 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { capture } = helpers;
function slot1(ctx, node, key = \\"\\") {
let b2,b3;
@@ -1152,8 +1245,7 @@ exports[`slots simple default slot with params 1`] = `
}
return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx1, __scope: \\"slotScope\\"}}}, key + \`__1\`, node, ctx);
return component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
}
}"
`;
@@ -1173,38 +1265,29 @@ exports[`slots simple default slot with params 2`] = `
}"
`;
exports[`slots simple default slot with params 3`] = `
exports[`slots simple default slot with params and bound function 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
function slot1(ctx, node, key = \\"\\") {
let b2,b3;
if (ctx['slotScope'].bool) {
b2 = text(\`some text\`);
} else {
b3 = text(\`other text\`);
}
return multi([b2, b3]);
return text(ctx['slotScope'].fn());
}
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx, __scope: \\"slotScope\\"}}}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`slots simple default slot with params 4`] = `
exports[`slots simple default slot with params and bound function 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callSlot } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
let { callSlot, bind } = helpers;
return function template(ctx, node, key = \\"\\") {
let b2 = callSlot(ctx, node, key, 'default', false, {bool: ctx['state'].bool});
return block1([], [b2]);
return callSlot(ctx, node, key, 'default', false, {fn: bind(ctx, ctx['getValue'])});
}
}"
`;
@@ -1236,6 +1319,44 @@ exports[`slots simple default slot, variation 2`] = `
}"
`;
exports[`slots simple slot with slot scope 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { capture } = helpers;
function slot1(ctx, node, key = \\"\\") {
let b2,b3;
if (ctx['slotScope'].bool) {
b2 = text(\`some text\`);
} else {
b3 = text(\`other text\`);
}
return multi([b2, b3]);
}
return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return component(\`Child\`, {slots: {'slotName': {__render: slot1, __ctx: ctx1, __scope: \\"slotScope\\"}}}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`slots simple slot with slot scope 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callSlot } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callSlot(ctx, node, key, 'slotName', false, {bool: ctx['state'].bool});
return block1([], [b2]);
}
}"
`;
exports[`slots slot and (inline) t-call 1`] = `
"function anonymous(bdom, helpers
) {
@@ -147,7 +147,7 @@ exports[`style and class handling class on sub component, which is switched to a
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {class: 'someclass',child: ctx['state'].child}, key + \`__1\`, node, ctx);
return component(\`Child\`, {class: 'someclass', child: ctx['state'].child}, key + \`__1\`, node, ctx);
}
}"
`;
@@ -53,8 +53,8 @@ exports[`list of components crash on duplicate key in dev mode 1`] = `
let key1 = 'child';
if (keys1.has(key1)) { throw new Error(\`Got duplicate key in t-foreach: \${key1}\`)}
keys1.add(key1);
const props1 = {}
helpers.validateProps(\`Child\`, props1, ctx)
const props1 = {};
helpers.validateProps(\`Child\`, props1, ctx);
c_block1[i1] = withKey(component(\`Child\`, props1, key + \`__1__\${key1}\`, node, ctx), key1);
}
return list(c_block1);
@@ -177,7 +177,7 @@ exports[`list of components reconciliation alg works for t-foreach in t-foreach,
for (let i2 = 0; i2 < l_block4; i2++) {
ctx[\`col\`] = v_block4[i2];
let key2 = ctx['col'];
let b6 = component(\`Child\`, {row: ctx['row'],col: ctx['col']}, key + \`__1__\${key1}__\${key2}\`, node, ctx);
let b6 = component(\`Child\`, {row: ctx['row'], col: ctx['col']}, key + \`__1__\${key1}__\${key2}\`, node, ctx);
c_block4[i2] = withKey(block5([], [b6]), key2);
}
ctx = ctx.__proto__;
@@ -10,8 +10,9 @@ exports[`t-model directive .lazy modifier 1`] = `
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['text'];
let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value; }];
const expr1 = 'text';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
let txt1 = ctx['state'].text;
return block1([attr1, hdlr1, txt1]);
}
@@ -28,8 +29,9 @@ exports[`t-model directive .number modifier 1`] = `
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['number'];
let hdlr1 = [(ev) => { bExpr1['number'] = toNumber(ev.target.value); }];
const expr1 = 'number';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = toNumber(ev.target.value); }];
let txt1 = ctx['state'].number;
return block1([attr1, hdlr1, txt1]);
}
@@ -46,8 +48,9 @@ exports[`t-model directive .trim modifier 1`] = `
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['text'];
let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value.trim(); }];
const expr1 = 'text';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value.trim(); }];
let txt1 = ctx['state'].text;
return block1([attr1, hdlr1, txt1]);
}
@@ -64,8 +67,9 @@ exports[`t-model directive basic use, on an input 1`] = `
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['text'];
let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value; }];
const expr1 = 'text';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
let txt1 = ctx['state'].text;
return block1([attr1, hdlr1, txt1]);
}
@@ -82,8 +86,9 @@ exports[`t-model directive basic use, on an input with bracket expression 1`] =
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['text'];
let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value; }];
const expr1 = 'text';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
let txt1 = ctx['state'].text;
return block1([attr1, hdlr1, txt1]);
}
@@ -100,8 +105,9 @@ exports[`t-model directive basic use, on another key in component 1`] = `
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['some'];
let attr1 = ctx['some']['text'];
let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value; }];
const expr1 = 'text';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
let txt1 = ctx['some'].text;
return block1([attr1, hdlr1, txt1]);
}
@@ -119,8 +125,9 @@ exports[`t-model directive can also define t-on directive on same event, part 1
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['onInput'], ctx];
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['text'];
let hdlr2 = [(ev) => { bExpr1['text'] = ev.target.value; }];
const expr1 = 'text';
let attr1 = bExpr1[expr1];
let hdlr2 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
return block1([hdlr1, attr1, hdlr2]);
}
}"
@@ -137,16 +144,19 @@ exports[`t-model directive can also define t-on directive on same event, part 2
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['onClick'], ctx];
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['choice'] === 'One';
let hdlr2 = [(ev) => { bExpr1['choice'] = ev.target.value; }];
const expr1 = 'choice';
let attr1 = bExpr1[expr1] === 'One';
let hdlr2 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
let hdlr3 = [ctx['onClick'], ctx];
const bExpr2 = ctx['state'];
let attr2 = ctx['state']['choice'] === 'Two';
let hdlr4 = [(ev) => { bExpr2['choice'] = ev.target.value; }];
const expr2 = 'choice';
let attr2 = bExpr2[expr2] === 'Two';
let hdlr4 = [(ev) => { bExpr2[expr2] = ev.target.value; }];
let hdlr5 = [ctx['onClick'], ctx];
const bExpr3 = ctx['state'];
let attr3 = ctx['state']['choice'] === 'Three';
let hdlr6 = [(ev) => { bExpr3['choice'] = ev.target.value; }];
const expr3 = 'choice';
let attr3 = bExpr3[expr3] === 'Three';
let hdlr6 = [(ev) => { bExpr3[expr3] = ev.target.value; }];
return block1([hdlr1, attr1, hdlr2, hdlr3, attr2, hdlr4, hdlr5, attr3, hdlr6]);
}
}"
@@ -165,8 +175,9 @@ exports[`t-model directive following a scope protecting directive (e.g. t-set) 1
ctx[isBoundary] = 1
setContextValue(ctx, \\"admiral\\", 'Bruno');
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['text'];
let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value; }];
const expr1 = 'text';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
return block1([attr1, hdlr1]);
}
}"
@@ -188,8 +199,9 @@ exports[`t-model directive in a t-foreach 1`] = `
ctx[\`thing\`] = v_block2[i1];
let key1 = ctx['thing'].id;
const bExpr1 = ctx['thing'];
let attr1 = ctx['thing']['f'];
let hdlr1 = [(ev) => { bExpr1['f'] = ev.target.checked; }];
const expr1 = 'f';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.checked; }];
c_block2[i1] = withKey(block3([attr1, hdlr1]), key1);
}
let b2 = list(c_block2);
@@ -215,8 +227,37 @@ exports[`t-model directive in a t-foreach, part 2 1`] = `
ctx[\`thing_index\`] = i1;
let key1 = ctx['thing_index'];
const bExpr1 = ctx['state'];
let attr1 = ctx['state'][ctx['thing_index']];
let hdlr1 = [(ev) => { bExpr1[ctx['thing_index']] = ev.target.value; }];
const expr1 = ctx['thing_index'];
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
c_block2[i1] = withKey(block3([attr1, hdlr1]), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
}
}"
`;
exports[`t-model directive in a t-foreach, part 3 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, toNumber, withKey } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block3 = createBlock(\`<input block-attribute-0=\\"value\\" block-handler-1=\\"input\\"/>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['names']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`name\`] = v_block2[i1];
ctx[\`name_index\`] = i1;
let key1 = ctx['name_index'];
const bExpr1 = ctx['state'].values;
const expr1 = ctx['name'];
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
c_block2[i1] = withKey(block3([attr1, hdlr1]), key1);
}
let b2 = list(c_block2);
@@ -235,8 +276,9 @@ exports[`t-model directive on a select 1`] = `
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['color'];
let hdlr1 = [(ev) => { bExpr1['color'] = ev.target.value; }];
const expr1 = 'color';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
let txt1 = ctx['state'].color;
return block1([attr1, hdlr1, txt1]);
}
@@ -253,8 +295,9 @@ exports[`t-model directive on a select, initial state 1`] = `
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['color'];
let hdlr1 = [(ev) => { bExpr1['color'] = ev.target.value; }];
const expr1 = 'color';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
return block1([attr1, hdlr1]);
}
}"
@@ -270,8 +313,9 @@ exports[`t-model directive on a sub state key 1`] = `
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'].something;
let attr1 = ctx['state'].something['text'];
let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value; }];
const expr1 = 'text';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
let txt1 = ctx['state'].something.text;
return block1([attr1, hdlr1, txt1]);
}
@@ -288,11 +332,13 @@ exports[`t-model directive on an input type=radio 1`] = `
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['choice'] === 'One';
let hdlr1 = [(ev) => { bExpr1['choice'] = ev.target.value; }];
const expr1 = 'choice';
let attr1 = bExpr1[expr1] === 'One';
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
const bExpr2 = ctx['state'];
let attr2 = ctx['state']['choice'] === 'Two';
let hdlr2 = [(ev) => { bExpr2['choice'] = ev.target.value; }];
const expr2 = 'choice';
let attr2 = bExpr2[expr2] === 'Two';
let hdlr2 = [(ev) => { bExpr2[expr2] = ev.target.value; }];
let txt1 = ctx['state'].choice;
return block1([attr1, hdlr1, attr2, hdlr2, txt1]);
}
@@ -309,11 +355,13 @@ exports[`t-model directive on an input type=radio, with initial value 1`] = `
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['choice'] === 'One';
let hdlr1 = [(ev) => { bExpr1['choice'] = ev.target.value; }];
const expr1 = 'choice';
let attr1 = bExpr1[expr1] === 'One';
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
const bExpr2 = ctx['state'];
let attr2 = ctx['state']['choice'] === 'Two';
let hdlr2 = [(ev) => { bExpr2['choice'] = ev.target.value; }];
const expr2 = 'choice';
let attr2 = bExpr2[expr2] === 'Two';
let hdlr2 = [(ev) => { bExpr2[expr2] = ev.target.value; }];
return block1([attr1, hdlr1, attr2, hdlr2]);
}
}"
@@ -330,8 +378,9 @@ exports[`t-model directive on an input, type=checkbox 1`] = `
return function template(ctx, node, key = \\"\\") {
let b2,b3;
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['flag'];
let hdlr1 = [(ev) => { bExpr1['flag'] = ev.target.checked; }];
const expr1 = 'flag';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.checked; }];
if (ctx['state'].flag) {
b2 = text(\`yes\`);
} else {
@@ -352,14 +401,137 @@ exports[`t-model directive on an textarea 1`] = `
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['text'];
let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value; }];
const expr1 = 'text';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
let txt1 = ctx['state'].text;
return block1([attr1, hdlr1, txt1]);
}
}"
`;
exports[`t-model directive t-model on select with static options 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { toNumber } = helpers;
let block1 = createBlock(\`<div><select block-attribute-0=\\"value\\" block-handler-1=\\"change\\"><option value=\\"a\\"><block-text-2/></option><option value=\\"b\\"><block-text-3/></option><option value=\\"c\\"><block-text-4/></option></select></div>\`);
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
const expr1 = 'model';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
let txt1 = 'a';
let txt2 = 'b';
let txt3 = 'c';
return block1([attr1, hdlr1, txt1, txt2, txt3]);
}
}"
`;
exports[`t-model directive t-model with dynamic values on select options -- 2 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { toNumber } = helpers;
let block1 = createBlock(\`<div><select block-handler-0=\\"change\\"><option block-attribute-1=\\"value\\" block-attribute-2=\\"selected\\"><block-text-3/></option><option block-attribute-4=\\"value\\" block-attribute-5=\\"selected\\"><block-text-6/></option></select></div>\`);
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
const expr1 = 'model';
let bValue1 = bExpr1[expr1]
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
let attr1 = ctx['options'][0];
let attr2 = bValue1 === ctx['options'][0];
let txt1 = ctx['options'][0];
let attr3 = (ctx['options'][1]);
let attr4 = bValue1 === (ctx['options'][1]);
let txt2 = ctx['options'][1];
return block1([hdlr1, attr1, attr2, txt1, attr3, attr4, txt2]);
}
}"
`;
exports[`t-model directive t-model with dynamic values on select options -- 3 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { toNumber } = helpers;
let block1 = createBlock(\`<div><select block-handler-0=\\"change\\"><option block-attribute-1=\\"value\\" block-attribute-2=\\"selected\\"><block-text-3/></option><option value=\\"b\\" block-attribute-4=\\"selected\\"><block-text-5/></option></select></div>\`);
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
const expr1 = 'model';
let bValue1 = bExpr1[expr1]
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
let attr1 = ctx['options'][0];
let attr2 = bValue1 === ctx['options'][0];
let txt1 = ctx['options'][0];
let attr3 = bValue1 === \\"b\\";
let txt2 = ctx['options'][1];
return block1([hdlr1, attr1, attr2, txt1, attr3, txt2]);
}
}"
`;
exports[`t-model directive t-model with dynamic values on select options 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { toNumber } = helpers;
let block1 = createBlock(\`<div><select block-handler-0=\\"change\\"><option block-attribute-1=\\"value\\" block-attribute-2=\\"selected\\"><block-text-3/></option><option block-attribute-4=\\"value\\" block-attribute-5=\\"selected\\"><block-text-6/></option></select></div>\`);
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
const expr1 = 'model';
let bValue1 = bExpr1[expr1]
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
let attr1 = ctx['options'][0];
let attr2 = bValue1 === ctx['options'][0];
let txt1 = ctx['options'][0];
let attr3 = ctx['options'][1];
let attr4 = bValue1 === ctx['options'][1];
let txt2 = ctx['options'][1];
return block1([hdlr1, attr1, attr2, txt1, attr3, attr4, txt2]);
}
}"
`;
exports[`t-model directive t-model with dynamic values on select options in foreach 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { toNumber, prepareList, withKey } = helpers;
let block1 = createBlock(\`<div><select block-handler-0=\\"change\\"><block-child-0/></select></div>\`);
let block3 = createBlock(\`<option block-attribute-0=\\"value\\" block-attribute-1=\\"selected\\"><block-text-2/></option>\`);
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
const expr1 = 'model';
let bValue1 = bExpr1[expr1]
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['options']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`v\`] = v_block2[i1];
let key1 = ctx['v'];
let attr1 = ctx['v'];
let attr2 = bValue1 === ctx['v'];
let txt1 = ctx['v'];
c_block2[i1] = withKey(block3([attr1, attr2, txt1]), key1);
}
let b2 = list(c_block2);
return block1([hdlr1], [b2]);
}
}"
`;
exports[`t-model directive two inputs in a div alternating with a t-if 1`] = `
"function anonymous(bdom, helpers
) {
@@ -374,17 +546,38 @@ exports[`t-model directive two inputs in a div alternating with a t-if 1`] = `
let b2,b3;
if (ctx['state'].flag) {
const bExpr1 = ctx['state'];
let attr1 = ctx['state']['text1'];
let hdlr1 = [(ev) => { bExpr1['text1'] = ev.target.value; }];
const expr1 = 'text1';
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
b2 = block2([attr1, hdlr1]);
}
if (!ctx['state'].flag) {
const bExpr2 = ctx['state'];
let attr2 = ctx['state']['text2'];
let hdlr2 = [(ev) => { bExpr2['text2'] = ev.target.value; }];
const expr2 = 'text2';
let attr2 = bExpr2[expr2];
let hdlr2 = [(ev) => { bExpr2[expr2] = ev.target.value; }];
b3 = block3([attr2, hdlr2]);
}
return block1([], [b2, b3]);
}
}"
`;
exports[`t-model directive with expression having a changing key 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { toNumber } = helpers;
let block1 = createBlock(\`<div><input block-attribute-0=\\"value\\" block-handler-1=\\"input\\"/><span><block-text-2/></span></div>\`);
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'].something;
const expr1 = ctx['text'].key;
let attr1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
let txt1 = ctx['state'].something[ctx['text'].key];
return block1([attr1, hdlr1, txt1]);
}
}"
`;
@@ -8,7 +8,7 @@ exports[`t-props basic use 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, ctx['some'].obj, key + \`__1\`, node, ctx);
let b2 = component(\`Child\`, Object.assign({}, ctx['some'].obj), key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -28,6 +28,30 @@ exports[`t-props basic use 2`] = `
}"
`;
exports[`t-props child receives a copy of the t-props object, not the original 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, Object.assign({}, ctx['childProps']), key + \`__1\`, node, ctx);
}
}"
`;
exports[`t-props child receives a copy of the t-props object, not the original 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`t-props t-props and other props 1`] = `
"function anonymous(bdom, helpers
) {
@@ -65,7 +89,7 @@ exports[`t-props t-props only 1`] = `
let block1 = createBlock(\`<div><div><block-child-0/></div></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Comp\`, ctx['state'], key + \`__1\`, node, ctx);
let b2 = component(\`Comp\`, Object.assign({}, ctx['state']), key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -93,7 +117,7 @@ exports[`t-props t-props with props 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, Object.assign({}, ctx['childProps'], {a: 1,b: 2}), key + \`__1\`, node, ctx);
let b2 = component(\`Child\`, Object.assign({}, ctx['childProps'], {a: 1, b: 2}), key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
+48
View File
@@ -171,6 +171,27 @@ describe("basics", () => {
expect(error!.message).toBe("Cannot mount a component on a detached dom node");
});
test("a component cannot be mounted in a detached node (even if node is detached later)", async () => {
const warn = console.warn;
console.warn = jest.fn();
class Test extends Component {
static template = xml`<div/>`;
}
let error: Error;
const prom = mount(Test, fixture);
await Promise.resolve();
fixture.remove();
try {
await prom;
} catch (e) {
error = e as Error;
}
expect(error!).toBeDefined();
expect(error!.message).toBe("Cannot mount a component on a detached dom node");
expect(console.warn).toBeCalledTimes(1);
console.warn = warn;
});
test("crashes if it cannot find a template", async () => {
class Test extends Component {
static template = "wrongtemplate";
@@ -1006,4 +1027,31 @@ describe("t-out in components", () => {
"<div>&lt;b&gt;one&lt;/b&gt;<b>one</b>&lt;b&gt;two&lt;/b&gt;<b>two</b>&lt;b&gt;tree&lt;/b&gt;<b>tree</b></div>"
);
});
test("can switch the contents of two t-out repeatedly", async () => {
class Test extends Component {
static template = xml`
<t t-out="state.a"/>
<t t-out="state.b"/>
`;
state = useState({
a: markup("<div>1</div>"),
b: markup("<div>2</div>"),
});
reverse() {
const { state } = this;
[state.a, state.b] = [state.b, state.a];
}
}
const comp = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("<div>1</div><div>2</div>");
comp.reverse();
await nextTick();
expect(fixture.innerHTML).toBe("<div>2</div><div>1</div>");
comp.reverse();
await nextTick();
expect(fixture.innerHTML).toBe("<div>1</div><div>2</div>");
});
});
+45
View File
@@ -81,6 +81,29 @@ describe("basics", () => {
expect(mockConsoleWarn).toBeCalledTimes(1);
});
test("display a nice error if it cannot find component (in dev mode)", async () => {
const info = console.info;
console.info = jest.fn(() => {}); // dev mode message
class SomeComponent extends Component {}
class Parent extends Component {
static template = xml`<SomeMispelledComponent />`;
static components = { SomeComponent };
}
let error: Error;
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
expect(error!).toBeDefined();
expect(error!.message).toBe('Cannot find the definition of component "SomeMispelledComponent"');
expect(console.error).toBeCalledTimes(0);
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1);
expect(console.info).toBeCalledTimes(1);
console.info = info;
});
test("simple catchError", async () => {
class Boom extends Component {
static template = xml`<div t-esc="a.b.c"/>`;
@@ -351,6 +374,28 @@ describe("can catch errors", () => {
expect(mockConsoleWarn).toBeCalledTimes(0);
});
test("calling a hook outside setup should crash", async () => {
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onWillStart(() => {
this.state = useState({ value: 2 });
});
}
}
let e: any = null;
try {
await mount(Root, fixture);
} catch (error) {
e = error;
}
expect(e.message).toBe(
"No active component (a hook function should only be called in 'setup')"
);
});
test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
class ErrorComponent extends Component {
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
+87 -3
View File
@@ -14,6 +14,7 @@ import {
useExternalListener,
useRef,
useState,
useChildSubEnv,
useSubEnv,
xml,
} from "../../src/index";
@@ -180,7 +181,7 @@ describe("hooks", () => {
expect(fixture.innerHTML).toBe("<div>1</div>");
});
test("use sub env does not pollute user env", async () => {
test("useSubEnv modifies user env", async () => {
class Test extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
setup() {
@@ -190,11 +191,25 @@ describe("hooks", () => {
const env = { val: 3 };
const component = await mount(Test, fixture, { env });
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(component.env).toHaveProperty("val2");
expect(component.env).toHaveProperty("val");
});
test("useChildSubEnv does not pollute user env", async () => {
class Test extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
setup() {
useChildSubEnv({ val2: 1 });
}
}
const env = { val: 3 };
const component = await mount(Test, fixture, { env });
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(component.env).not.toHaveProperty("val2");
expect(component.env).toHaveProperty("val");
});
test("use sub env supports arbitrary descriptor", async () => {
test("useSubEnv supports arbitrary descriptor", async () => {
let someVal = "maggot";
let someVal2 = "brain";
@@ -213,6 +228,40 @@ describe("hooks", () => {
});
}
}
const env = {
get someVal() {
return someVal;
},
};
const component = await mount(Test, fixture, { env });
expect(fixture.innerHTML).toBe("<div>maggot brain</div>");
someVal = "brain";
someVal2 = "maggot";
component.render();
await nextTick();
expect(fixture.innerHTML).toBe("<div>brain maggot</div>");
});
test("useChildSubEnv supports arbitrary descriptor", async () => {
let someVal = "maggot";
let someVal2 = "brain";
class Child extends Component {
static template = xml`<div><t t-esc="env.someVal" /> <t t-esc="env.someVal2" /></div>`;
}
class Test extends Component {
static template = xml`<Child />`;
static components = { Child };
setup() {
useChildSubEnv({
get someVal2() {
return someVal2;
},
});
}
}
someVal = "maggot";
const env = {
get someVal() {
@@ -239,7 +288,7 @@ describe("hooks", () => {
await mount(Test, fixture);
});
test("parent and child env", async () => {
test("parent and child env (with useSubEnv)", async () => {
class Child extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
}
@@ -253,9 +302,44 @@ describe("hooks", () => {
}
const env = { val: 3 };
await mount(Parent, fixture, { env });
expect(fixture.innerHTML).toBe("5<div>5</div>");
});
test("parent and child env (with useChildSubEnv)", async () => {
class Child extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
}
class Parent extends Component {
static template = xml`<t t-esc="env.val"/><Child/>`;
static components = { Child };
setup() {
useChildSubEnv({ val: 5 });
}
}
const env = { val: 3 };
await mount(Parent, fixture, { env });
expect(fixture.innerHTML).toBe("3<div>5</div>");
});
test("parent and child env (with useChildSubEnv then useSubEnv)", async () => {
class Child extends Component {
static template = xml`<div t-if="env.hasParent"><t t-esc="env.val"/></div>`;
}
class Parent extends Component {
static template = xml`<t t-esc="env.val"/><Child/>`;
static components = { Child };
setup() {
useChildSubEnv({ hasParent: true });
useSubEnv({ val: 5 });
}
}
const env = { val: 3 };
await mount(Parent, fixture, { env });
expect(fixture.innerHTML).toBe("5<div>5</div>");
});
test("can use onWillStart, onWillUpdateProps", async () => {
const steps: string[] = [];
async function slow(): Promise<string> {
+52
View File
@@ -1253,4 +1253,56 @@ describe("lifecycle hooks", () => {
"onWillDestroy",
]).toBeLogged();
});
test("destroy new children before being mountged", async () => {
class Child extends Component {
static template = xml`child`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`before<Child t-if="state.flag"/>after`;
static components = { Child };
state = useState({ flag: false });
setup() {
useLogLifecycle();
onRendered(async () => {
// we destroy here the app after the new child component has been
// created, but before this rendering has been patched to the DOM
if (this.state.flag) {
await Promise.resolve();
app.destroy();
}
});
}
}
const app = new App(Parent);
const parent = await app.mount(fixture);
expect(fixture.innerHTML).toBe("beforeafter");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
parent.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Parent:willUnmount",
"Child:willDestroy",
"Parent:willDestroy",
]).toBeLogged();
});
});
+27 -3
View File
@@ -684,7 +684,7 @@ describe("props validation", () => {
test("default values are applied before validating props at update", async () => {
// need to do something about errors catched in render
class SubComp extends Component {
static props = { p: { type: Number } };
static props = { p: { type: Number, optional: true } };
static template = xml`<div><t t-esc="props.p"/></div>`;
static defaultProps = { p: 4 };
}
@@ -791,9 +791,9 @@ describe("default props", () => {
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
});
test("can set default required boolean values", async () => {
test("can set default boolean values", async () => {
class SubComp extends Component {
static props = ["p", "q"];
static props = ["p?", "q?"];
static defaultProps = { p: true, q: false };
static template = xml`<span><t t-if="props.p">hey</t><t t-if="!props.q">hey</t></span>`;
}
@@ -804,4 +804,28 @@ describe("default props", () => {
await mount(Parent, fixture, { dev: true });
expect(fixture.innerHTML).toBe("<div><span>heyhey</span></div>");
});
test("a default prop cannot be defined on a mandatory prop", async () => {
class Child extends Component {
static props = {
mandatory: Number,
};
static defaultProps = { mandatory: 3 };
static template = xml` <div><t t-esc="props.mandatory"/></div>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`<Child/>`;
}
let error: Error;
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
expect(error!).toBeDefined();
expect(error!.message).toBe(
"A default value cannot be defined for a mandatory prop (name: 'mandatory', component: Child)"
);
});
});
+25 -2
View File
@@ -1,5 +1,5 @@
import { Component, mount, useRef, useState } from "../../src/index";
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
import { Component, mount, onMounted, useRef, useState } from "../../src/index";
import { logStep, makeTestFixture, nextTick, snapshotEverything } from "../helpers";
import { xml } from "../../src/index";
snapshotEverything();
@@ -100,4 +100,27 @@ describe("refs", () => {
expect(console.warn).toBeCalledTimes(1);
console.warn = consoleWarn;
});
test("refs and recursive templates", async () => {
class Test extends Component {
static components = {};
static template = xml`
<p t-ref="root">
<t t-esc="props.tree.value"/>
<t t-if="props.tree.child"><Test tree="props.tree.child"/></t>
</p>`;
root = useRef("root");
setup() {
onMounted(() => logStep(this.root.el!.outerHTML));
}
}
Test.components = { Test };
const tree = { value: "a", child: { value: "b", child: null } };
await mount(Test, fixture, { props: { tree } });
expect(fixture.innerHTML).toBe("<p>a<p>b</p></p>");
expect(["<p>b</p>", "<p>a<p>b</p></p>"]).toBeLogged();
});
});
+92 -6
View File
@@ -45,7 +45,36 @@ describe("slots", () => {
expect(fixture.innerHTML).toBe("some text");
});
test("simple default slot with params", async () => {
test("simple slot with slot scope", async () => {
let child: any;
class Child extends Component {
static template = xml`<span><t t-slot="slotName" bool="state.bool"/></span>`;
state = useState({ bool: true });
setup() {
child = this;
}
}
class Parent extends Component {
static template = xml`
<Child>
<t t-set-slot="slotName" t-slot-scope="slotScope">
<t t-if="slotScope.bool">some text</t>
<t t-else="slotScope.bool">other text</t>
</t>
</Child>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span>some text</span>");
child.state.bool = false;
await nextTick();
expect(fixture.innerHTML).toBe("<span>other text</span>");
});
test("default slot with slot scope: shorthand syntax", async () => {
let child: any;
class Child extends Component {
static template = xml`<span><t t-slot="default" bool="state.bool"/></span>`;
@@ -57,11 +86,9 @@ describe("slots", () => {
class Parent extends Component {
static template = xml`
<Child>
<t t-set-slot="default" t-slot-scope="slotScope">
<t t-if="slotScope.bool">some text</t>
<t t-else="slotScope.bool">other text</t>
</t>
<Child t-slot-scope="slotScope">
<t t-if="slotScope.bool">some text</t>
<t t-else="slotScope.bool">other text</t>
</Child>`;
static components = { Child };
}
@@ -99,6 +126,41 @@ describe("slots", () => {
expect(mockConsoleWarn).toBeCalledTimes(1);
});
test("simple default slot with params and bound function", async () => {
class Child extends Component {
static template = xml`<t t-slot="default" fn.bind="getValue"/>`;
state = useState({ value: 123 });
getValue() {
return this.state.value;
}
}
class Parent extends Component {
static template = xml`
<Child t-slot-scope="slotScope"><t t-esc="slotScope.fn()"/></Child>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("123");
});
test("default slot with params with - in it", async () => {
class Child extends Component {
static template = xml`<t t-slot="default" some-value="state.value"/>`;
state = useState({ value: 123 });
}
class Parent extends Component {
static template = xml`
<Child t-slot-scope="slotScope"><t t-esc="slotScope['some-value']"/></Child>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("123");
});
test("fun: two calls to the same slot", async () => {
class Child extends Component {
static template = xml`<t t-slot="default"/><t t-slot="default"/>`;
@@ -215,6 +277,30 @@ describe("slots", () => {
);
});
test("can define and call slots with bound params", async () => {
class Child extends Component {
static template = xml`
<t t-slot="abc"/>
<t t-esc="props.slots['abc'].getValue()"/>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
<Child>
<t t-set-slot="abc" getValue.bind="getValue">abc</t>
</Child>`;
state = useState({ value: 444 });
getValue() {
return this.state.value;
}
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("abc444");
});
test("no named slot content => just no children", async () => {
class Dialog extends Component {
static template = xml`<span><t t-slot="header"/></span>`;
+160
View File
@@ -246,6 +246,34 @@ describe("t-model directive", () => {
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
});
test("with expression having a changing key", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state.something[text.key]"/>
<span><t t-esc="state.something[text.key]"/></span>
</div>
`;
state: { something: { [key: string]: string } } = useState({ something: {} });
text = useState({ key: "foo" });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
let input = fixture.querySelector("input")!;
await editInput(input, "footest");
expect(comp.state.something[comp.text.key]).toBe("footest");
expect(fixture.innerHTML).toBe("<div><input><span>footest</span></div>");
comp.text.key = "bar";
await nextTick();
input = fixture.querySelector("input")!;
await editInput(input, "test bar");
expect(comp.state.something[comp.text.key]).toBe("test bar");
expect(fixture.innerHTML).toBe("<div><input><span>test bar</span></div>");
});
test(".lazy modifier", async () => {
class SomeComponent extends Component {
static template = xml`
@@ -360,6 +388,26 @@ describe("t-model directive", () => {
expect(comp.state).toEqual(["zuko", "uncle iroh"]);
});
test("in a t-foreach, part 3", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t t-foreach="names" t-as="name" t-key="name_index">
<input t-model="state.values[name]"/>
</t>
</div>
`;
names = ["Crusher", "Data", "Riker", "Worf"];
state = useState({ values: {} });
}
const comp = await mount(SomeComponent, fixture);
expect(comp.state).toEqual({ values: {} });
const input = fixture.querySelectorAll("input")[1]!;
await editInput(input, "Commander");
expect(comp.state).toEqual({ values: { Data: "Commander" } });
});
test("two inputs in a div alternating with a t-if", async () => {
class SomeComponent extends Component {
static template = xml`
@@ -452,4 +500,116 @@ describe("t-model directive", () => {
expect(comp.state.choice).toBe("Three");
expect(comp.state.lastClicked).toBe("Three");
});
test("t-model on select with static options", async () => {
class Test extends Component {
static template = xml`
<div>
<select t-model="state.model">
<option value="a" t-esc="'a'"/>
<option value="b" t-esc="'b'"/>
<option value="c" t-esc="'c'"/>
</select>
</div>
`;
state: any;
options: any;
setup() {
this.state = useState({ model: "b" });
this.options = ["a", "b", "c"];
}
}
await mount(Test, fixture);
expect(fixture.querySelector("select")!.value).toEqual("b");
});
test("t-model with dynamic values on select options", async () => {
class Test extends Component {
static template = xml`
<div>
<select t-model="state.model">
<option t-att-value="options[0]" t-esc="options[0]"/>
<option t-att-value="options[1]" t-esc="options[1]"/>
</select>
</div>
`;
state: any;
options: any;
setup() {
this.state = useState({ model: "b" });
this.options = ["a", "b"];
}
}
await mount(Test, fixture);
expect(fixture.querySelector("select")!.value).toEqual("b");
});
test("t-model with dynamic values on select options -- 2", async () => {
class Test extends Component {
static template = xml`
<div>
<select t-model="state.model">
<option t-att-value="options[0]" t-esc="options[0]"/>
<option t-attf-value="{{ options[1] }}" t-esc="options[1]"/>
</select>
</div>
`;
state: any;
options: any;
setup() {
this.state = useState({ model: "b" });
this.options = ["a", "b"];
}
}
await mount(Test, fixture);
expect(fixture.querySelector("select")!.value).toEqual("b");
});
test("t-model with dynamic values on select options -- 3", async () => {
class Test extends Component {
static template = xml`
<div>
<select t-model="state.model">
<option t-att-value="options[0]" t-esc="options[0]"/>
<option value="b" t-esc="options[1]"/>
</select>
</div>
`;
state: any;
options: any;
setup() {
this.state = useState({ model: "b" });
this.options = ["a", "b"];
}
}
await mount(Test, fixture);
expect(fixture.querySelector("select")!.value).toEqual("b");
});
test("t-model with dynamic values on select options in foreach", async () => {
class Test extends Component {
static template = xml`
<div>
<select t-model="state.model">
<t t-foreach="options" t-as="v" t-key="v">
<option t-att-value="v" t-esc="v"/>
</t>
</select>
</div>
`;
state: any;
options: any;
setup() {
this.state = useState({ model: "b" });
this.options = ["a", "b", "c"];
}
}
await mount(Test, fixture);
expect(fixture.querySelector("select")!.value).toEqual("b");
});
});
+20 -2
View File
@@ -53,7 +53,7 @@ describe("t-props", () => {
});
test("basic use", async () => {
expect.assertions(5);
expect.assertions(4);
let props = { a: 1, b: 2 };
@@ -65,7 +65,6 @@ describe("t-props", () => {
`;
setup() {
expect(this.props).toEqual({ a: 1, b: 2 });
expect(this.props).toBe(props);
}
}
class Parent extends Component {
@@ -105,4 +104,23 @@ describe("t-props", () => {
await mount(Parent, fixture);
});
test("child receives a copy of the t-props object, not the original", async () => {
class Child extends Component {
static template = xml`<div/>`;
setup() {
expect(this.props).toEqual({ a: 1, b: 2 });
this.props.d = 5;
}
}
class Parent extends Component {
static template = xml`<Child t-props="childProps"/>`;
static components = { Child };
childProps = { a: 1, b: 2 };
}
const parent = await mount(Parent, fixture);
expect(parent.childProps).not.toHaveProperty("d");
});
});
+1 -1
View File
@@ -123,7 +123,7 @@ export function snapshotEverything() {
});
const originalCompileTemplate = TemplateSet.prototype._compileTemplate;
TemplateSet.prototype._compileTemplate = function (name: string, template: string | Node) {
TemplateSet.prototype._compileTemplate = function (name: string, template: string | Element) {
const fn = originalCompileTemplate.call(this, "", template);
if (!globalTemplateNames.has(name)) {
expect(fn.toString()).toMatchSnapshot();
+1 -1
View File
@@ -16,7 +16,7 @@ exports[`Memo if no prop change, prevent renderings from above 1`] = `
let b2 = text(ctx['state'].a);
let b3 = text(ctx['state'].b);
let b4 = text(ctx['state'].c);
let b9 = component(\`Memo\`, {a: ctx['state'].a,b: ctx['state'].b,slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
let b9 = component(\`Memo\`, {a: ctx['state'].a, b: ctx['state'].b,slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx);
return multi([b2, b3, b4, b9]);
}
}"
+41 -1
View File
@@ -6,8 +6,10 @@ import {
onWillUpdateProps,
useState,
xml,
markRaw,
toRaw,
} from "../src";
import { reactive } from "../src/reactivity";
import { reactive, Reactive } from "../src/reactivity";
import { batched } from "../src/utils";
import {
makeDeferred,
@@ -1091,6 +1093,44 @@ describe("Reactivity", () => {
expect(n).toBe(1);
expect(state.k).toEqual({ n: 2 });
});
test("can add collections set/weakset/map/weakmap in a reactive object", () => {
const rawSet = new Set();
const rawWeakSet = new WeakSet();
const rawMap = new Map();
const rawWeakMap = new WeakMap();
const obj = reactive({ rawSet, rawWeakSet, rawMap, rawWeakMap });
expect(obj.rawSet).toBe(rawSet);
expect(obj.rawWeakSet).toBe(rawWeakSet);
expect(obj.rawMap).toBe(rawMap);
expect(obj.rawWeakMap).toBe(rawWeakMap);
});
});
describe("markRaw", () => {
test("markRaw works as expected: value is not observed", () => {
const obj1: any = markRaw({ value: 1 });
const obj2 = { value: 1 };
let n = 0;
const r = reactive({ obj1, obj2 }, () => n++);
expect(n).toBe(0);
r.obj1.value = r.obj1.value + 1;
expect(n).toBe(0);
r.obj2.value = r.obj2.value + 1;
expect(n).toBe(1);
expect(r.obj1).toBe(obj1);
expect(r.obj2).not.toBe(obj2);
});
});
describe("toRaw", () => {
test("toRaw works as expected", () => {
const obj = { value: 1 };
const reactiveObj = reactive(obj);
expect(reactiveObj).not.toBe(obj);
expect(toRaw(reactiveObj as Reactive<typeof obj>)).toBe(obj);
});
});
describe("Reactivity: useState", () => {