Compare commits

...

9 Commits

Author SHA1 Message Date
Géry Debongnie aeed79c7e5 [REL] v2.5.2
# v2.5.2

 - [FIX] runtime: make error recovery more robust
2024-12-02 16:51:14 +01:00
Géry Debongnie 1c5b6f2573 [FIX] runtime: make error recovery more robust
The error recovery process is necessary, but is particularly subtle. An
error may occur basically any time some user code is called, which may
leave the internal owl state in an invalid situation.

In this commit, we try to improve Owl behaviour when a crash occurs in a
onMounted hook. Before this commit, it was possible for other component
mounted hooks to be called even though the component was not mounted, or
for willUnmount hooks to be called even though the component was
mounted, but its onMounted hooks had not been called.

All these situations need to be handled when we catch an error. In this
commit, we try to clean more internal state in the error handling code,
to make Owl more consistent.
2024-11-28 16:27:56 +01:00
Romeo Fragomeli cd9b72158b [REL] v2.5.1
# v2.5.1

 - [FIX] compiler: multiple modifiers on t-custom
2024-11-26 09:42:49 +01:00
Jorge Pinna Puissant 7fc552e2f8 [FIX] compiler: multiple modifiers on t-custom
Since [1] it's possible to have a custom directive, this directive only
supported one modifier. This commit, add the support to have multiple
modifiers on a t-custom directive.

[1] : https://github.com/odoo/owl/commit/7e687234bf40bdcf5e6598401d8604d66b14ad32
2024-11-25 16:14:56 +01:00
Romeo Fragomeli e6768501cd [REL] v2.5.0
# v2.5.0

 - [IMP] app: add global values at compile time
 - [IMP] parser: add support for custom directives
 - [FIX] runtime: properly handle error caused in mounted hook
2024-11-25 10:30:53 +01:00
Jorge Pinna Puissant 6b2486473f [IMP] app: add global values at compile time
This commit, add a new configuration on the App: globalValues.
It's a global object of elements available at compilations.

For instance:
```js
    const app = new App(SomeComponent, {
      globalValues: {
        plop: (string: any) => {
          steps.push(string);
        },
      },
    });
```

The plop function will be available at the compilation, so it can be
used on the templates :
```xml
<div t-on-click="() => __globals__.plop('click')" class="my-div"/>
```
2024-11-25 09:57:54 +01:00
Jorge Pinna Puissant 7e687234bf [IMP] parser: add support for custom directives
This commit adds the support for custom directives. To use the
custom directive, an Object of functions needs to be configured on
the owl APP:
```js
 new App(..., {
    customDirectives: {
     test_directive: function (el, value) {
            el.setAttribute("t-on-click", value);
            return el;
       }
   }
  });
```
The functions will be called when a custom directive with the name of the
function is found. The original element will be replaced with the one
returned by the function.
This :
```xml
<div t-custom-test_directive="click" />
```
will be replace by :
```xml
<div t-on-click="value"/>
```

issue : https://github.com/odoo/owl/issues/1650
2024-11-25 09:57:54 +01:00
Géry Debongnie 968e96ad08 [FIX] runtime: properly handle error caused in mounted hook
If a component onMounted hook causes an error, and an error handler
tries to reinitiate a new correct render, the fiber may be recycled. But
the scheduler removes it from its own set of tasks, so it will be
ignored.

We simply check for this condition before removing it from the tasks
set.
2024-11-25 09:56:34 +01:00
Romeo Fragomeli 26c7856d5d [REL] v2.4.1
# v2.4.1

 - [FIX] app: make subroots more robust
 - [IMP] devtools: show objects class name and add special extension cases
 - [IMP] devtools: allow expansion of empty class
 - [IMP] Bump support for Node 20+
2024-10-31 10:42:38 +01:00
22 changed files with 789 additions and 30 deletions
+3
View File
@@ -66,6 +66,9 @@ The `config` object is an object with some of the following keys:
needs a template. If undefined is returned, owl looks into the app templates.
- **`warnIfNoStaticProps (boolean, default=false)`**: if true, Owl will log a warning
whenever it encounters a component that does not provide a [static props description](props.md#props-validation).
- **`customDirectives (object)`**: if given, the corresponding function on the object will be called
on the template custom directives: `t-custom-*` (see [Custom Directives](templates.md#custom-directives)).
- **`globalValues (object)`**: Global object of elements available at compilations.
## `mount` helper
+31
View File
@@ -18,6 +18,7 @@
- [Sub Templates](#sub-templates)
- [Dynamic Sub Templates](#dynamic-sub-templates)
- [Debugging](#debugging)
- [Custom Directives](#custom-directives)
- [Fragments](#fragments)
- [Inline templates](#inline-templates)
- [Rendering svg](#rendering-svg)
@@ -80,6 +81,7 @@ needs. Here is a list of all Owl specific directives:
| `t-slot`, `t-set-slot`, `t-slot-scope` | [Rendering a slot](slots.md) |
| `t-model` | [Form input bindings](input_bindings.md) |
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
| `t-custom-*` | [Rendering nodes with custom directives](#custom-directives) |
## QWeb Template Reference
@@ -588,6 +590,35 @@ will stop execution if the browser dev tools are open.
will print 42 to the console.
### Custom Directives
Owl 2 supports the declaration of custom directives. To use them, an Object of functions needs to be configured on the owl APP:
```js
new App(..., {
customDirectives: {
test_directive: function (el, value) {
el.setAttribute("t-on-click", value);
}
}
});
```
The functions will be called when a custom directive with the name of the
function is found. The original element will be replaced with the one
modified by the function.
This :
```xml
<div t-custom-test_directive="click" />
```
will be replaced by :
```xml
<div t-on-click="value"/>
```
## Fragments
Owl 2 supports templates with an arbitrary number of root elements, or even just
+95 -15
View File
@@ -1625,6 +1625,13 @@ function makeRootFiber(node) {
fibersInError.delete(current);
fibersInError.delete(root);
current.appliedToDom = false;
if (current instanceof RootFiber) {
// it is possible that this fiber is a fiber that crashed while being
// mounted, so the mounted list is possibly corrupted. We restore it to
// its normal initial state (which is empty list or a list with a mount
// fiber.
current.mounted = current instanceof MountFiber ? [current] : [];
}
}
return current;
}
@@ -1741,6 +1748,7 @@ class RootFiber extends Fiber {
const node = this.node;
this.locked = true;
let current = undefined;
let mountedFibers = this.mounted;
try {
// Step 1: calling all willPatch lifecycle hooks
for (current of this.willPatch) {
@@ -1760,7 +1768,6 @@ class RootFiber extends Fiber {
node._patch();
this.locked = false;
// Step 4: calling all mounted lifecycle hooks
let mountedFibers = this.mounted;
while ((current = mountedFibers.pop())) {
current = current;
if (current.appliedToDom) {
@@ -1781,6 +1788,15 @@ class RootFiber extends Fiber {
}
}
catch (e) {
// if mountedFibers is not empty, this means that a crash occured while
// calling the mounted hooks of some component. So, there may still be
// some component that have been mounted, but for which the mounted hooks
// have not been called. Here, we remove the willUnmount hooks for these
// specific component to prevent a worse situation (willUnmount being
// called even though mounted has not been called)
for (let fiber of mountedFibers) {
fiber.node.willUnmount = [];
}
this.locked = false;
node.app.handleError({ fiber: current || this, error: e });
}
@@ -2270,6 +2286,12 @@ function collectionsProxyHandler(target, callback, targetRawType) {
}
let currentNode = null;
function saveCurrent() {
let n = currentNode;
return () => {
currentNode = n;
};
}
function getCurrent() {
if (!currentNode) {
throw new OwlError("No active component (a hook function should only be called in 'setup')");
@@ -3221,6 +3243,9 @@ class TemplateSet {
}
}
this.getRawTemplate = config.getTemplate;
this.customDirectives = config.customDirectives || {};
this.runtimeUtils = { ...helpers, __globals__: config.globalValues || {} };
this.hasGlobalValues = Boolean(config.globalValues && Object.keys(config.globalValues).length);
}
static registerTemplate(name, fn) {
globalTemplates[name] = fn;
@@ -3277,7 +3302,7 @@ class TemplateSet {
this.templates[name] = function (context, parent) {
return templates[name].call(this, context, parent);
};
const template = templateFn(this, bdom, helpers);
const template = templateFn(this, bdom, this.runtimeUtils);
this.templates[name] = template;
}
return this.templates[name];
@@ -3328,7 +3353,7 @@ TemplateSet.registerTemplate("__portal__", portalTemplate);
//------------------------------------------------------------------------------
// Misc types, constants and helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date".split(",");
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date,__globals__".split(",");
const WORD_REPLACEMENT = Object.assign(Object.create(null), {
and: "&&",
or: "||",
@@ -3800,6 +3825,9 @@ class CodeGenerator {
this.dev = options.dev || false;
this.ast = ast;
this.templateName = options.name;
if (options.hasGlobalValues) {
this.helpers.add("__globals__");
}
}
generateCode() {
const ast = this.ast;
@@ -4838,29 +4866,33 @@ class CodeGenerator {
// Parser
// -----------------------------------------------------------------------------
const cache = new WeakMap();
function parse(xml) {
function parse(xml, customDir) {
const ctx = {
inPreTag: false,
customDirectives: customDir,
};
if (typeof xml === "string") {
const elem = parseXML(`<t>${xml}</t>`).firstChild;
return _parse(elem);
return _parse(elem, ctx);
}
let ast = cache.get(xml);
if (!ast) {
// we clone here the xml to prevent modifying it in place
ast = _parse(xml.cloneNode(true));
ast = _parse(xml.cloneNode(true), ctx);
cache.set(xml, ast);
}
return ast;
}
function _parse(xml) {
function _parse(xml, ctx) {
normalizeXML(xml);
const ctx = { inPreTag: false };
return parseNode(xml, ctx) || { type: 0 /* Text */, value: "" };
}
function parseNode(node, ctx) {
if (!(node instanceof Element)) {
return parseTextCommentNode(node, ctx);
}
return (parseTDebugLog(node, ctx) ||
return (parseTCustom(node, ctx) ||
parseTDebugLog(node, ctx) ||
parseTForEach(node, ctx) ||
parseTIf(node, ctx) ||
parseTPortal(node, ctx) ||
@@ -4902,6 +4934,35 @@ function parseTextCommentNode(node, ctx) {
}
return null;
}
function parseTCustom(node, ctx) {
if (!ctx.customDirectives) {
return null;
}
const nodeAttrsNames = node.getAttributeNames();
for (let attr of nodeAttrsNames) {
if (attr === "t-custom" || attr === "t-custom-") {
throw new OwlError("Missing custom directive name with t-custom directive");
}
if (attr.startsWith("t-custom-")) {
const directiveName = attr.split(".")[0].slice(9);
const customDirective = ctx.customDirectives[directiveName];
if (!customDirective) {
throw new OwlError(`Custom directive "${directiveName}" is not defined`);
}
const value = node.getAttribute(attr);
const modifiers = attr.split(".").slice(1);
node.removeAttribute(attr);
try {
customDirective(node, value, modifiers);
}
catch (error) {
throw new OwlError(`Custom directive "${directiveName}" throw the following error: ${error}`);
}
return parseNode(node, ctx);
}
}
return null;
}
// -----------------------------------------------------------------------------
// debugging
// -----------------------------------------------------------------------------
@@ -5533,9 +5594,11 @@ function normalizeXML(el) {
normalizeTEscTOut(el);
}
function compile(template, options = {}) {
function compile(template, options = {
hasGlobalValues: false,
}) {
// parsing
const ast = parse(template);
const ast = parse(template, options.customDirectives);
// some work
const hasSafeContext = template instanceof Node
? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
@@ -5557,7 +5620,7 @@ function compile(template, options = {}) {
}
// do not modify manually. This file is generated by the release script.
const version = "2.4.0";
const version = "2.5.2";
// -----------------------------------------------------------------------------
// Scheduler
@@ -5568,6 +5631,7 @@ class Scheduler {
this.frame = 0;
this.delayedRenders = [];
this.cancelledNodes = new Set();
this.processing = false;
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
}
addFiber(fiber) {
@@ -5598,6 +5662,10 @@ class Scheduler {
}
}
processTasks() {
if (this.processing) {
return;
}
this.processing = true;
this.frame = 0;
for (let node of this.cancelledNodes) {
node._destroy();
@@ -5611,6 +5679,7 @@ class Scheduler {
this.tasks.delete(task);
}
}
this.processing = false;
}
processFiber(fiber) {
if (fiber.root !== fiber) {
@@ -5630,7 +5699,14 @@ class Scheduler {
if (!hasError) {
fiber.complete();
}
this.tasks.delete(fiber);
// at this point, the fiber should have been applied to the DOM, so we can
// remove it from the task list. If it is not the case, it means that there
// was an error and an error handler triggered a new rendering that recycled
// the fiber, so in that case, we actually want to keep the fiber around,
// otherwise it will just be ignored.
if (fiber.appliedToDom) {
this.tasks.delete(fiber);
}
}
}
}
@@ -5686,7 +5762,9 @@ class App extends TemplateSet {
if (config.env) {
this.env = config.env;
}
const restore = saveCurrent();
const node = this.makeNode(Root, props);
restore();
if (config.env) {
this.env = env;
}
@@ -6012,12 +6090,14 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
dev: this.dev,
translateFn: this.translateFn,
translatableAttributes: this.translatableAttributes,
customDirectives: this.customDirectives,
hasGlobalValues: this.hasGlobalValues,
});
};
export { App, Component, EventBus, OwlError, __info__, batched, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
__info__.date = '2024-09-30T08:49:29.420Z';
__info__.hash = 'eb2b32a';
__info__.date = '2024-12-02T15:51:07.157Z';
__info__.hash = '1c5b6f2';
__info__.url = 'https://github.com/odoo/owl';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.4.0",
"version": "2.5.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.4.0",
"version": "2.5.2",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
+4
View File
@@ -0,0 +1,4 @@
export type customDirectives = Record<
string,
(node: Element, value: string, modifier: string[]) => void
>;
+4
View File
@@ -43,6 +43,7 @@ export interface Config {
export interface CodeGenOptions extends Config {
hasSafeContext?: boolean;
name?: string;
hasGlobalValues: boolean;
}
// using a non-html document so that <inner/outer>HTML serializes as XML instead
@@ -286,6 +287,9 @@ export class CodeGenerator {
this.dev = options.dev || false;
this.ast = ast;
this.templateName = options.name;
if (options.hasGlobalValues) {
this.helpers.add("__globals__");
}
}
generateCode(): string {
+7 -2
View File
@@ -1,3 +1,4 @@
import type { customDirectives } from "../common/types";
import type { TemplateSet } from "../runtime/template_set";
import type { BDom } from "../runtime/blockdom";
import { CodeGenerator, Config } from "./code_generator";
@@ -10,13 +11,17 @@ export type TemplateFunction = (app: TemplateSet, bdom: any, helpers: any) => Te
interface CompileOptions extends Config {
name?: string;
customDirectives?: customDirectives;
hasGlobalValues: boolean;
}
export function compile(
template: string | Element,
options: CompileOptions = {}
options: CompileOptions = {
hasGlobalValues: false,
}
): TemplateFunction {
// parsing
const ast = parse(template);
const ast = parse(template, options.customDirectives);
// some work
const hasSafeContext =
+1 -1
View File
@@ -28,7 +28,7 @@ import { OwlError } from "../common/owl_error";
//------------------------------------------------------------------------------
const RESERVED_WORDS =
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date".split(
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date,__globals__".split(
","
);
+42 -5
View File
@@ -1,4 +1,5 @@
import { OwlError } from "../common/owl_error";
import type { customDirectives } from "../common/types";
import { parseXML } from "../common/utils";
// -----------------------------------------------------------------------------
@@ -198,23 +199,26 @@ export type AST =
// -----------------------------------------------------------------------------
const cache: WeakMap<Element, AST> = new WeakMap();
export function parse(xml: string | Element): AST {
export function parse(xml: string | Element, customDir?: customDirectives): AST {
const ctx = {
inPreTag: false,
customDirectives: customDir,
};
if (typeof xml === "string") {
const elem = parseXML(`<t>${xml}</t>`).firstChild as Element;
return _parse(elem);
return _parse(elem, ctx);
}
let ast = cache.get(xml);
if (!ast) {
// we clone here the xml to prevent modifying it in place
ast = _parse(xml.cloneNode(true) as Element);
ast = _parse(xml.cloneNode(true) as Element, ctx);
cache.set(xml, ast);
}
return ast;
}
function _parse(xml: Element): AST {
function _parse(xml: Element, ctx: ParsingContext): AST {
normalizeXML(xml);
const ctx = { inPreTag: false };
return parseNode(xml, ctx) || { type: ASTType.Text, value: "" };
}
@@ -222,6 +226,7 @@ interface ParsingContext {
tModelInfo?: TModelInfo | null;
nameSpace?: string;
inPreTag: boolean;
customDirectives?: customDirectives;
}
function parseNode(node: Node, ctx: ParsingContext): AST | null {
@@ -229,6 +234,7 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
return parseTextCommentNode(node, ctx);
}
return (
parseTCustom(node, ctx) ||
parseTDebugLog(node, ctx) ||
parseTForEach(node, ctx) ||
parseTIf(node, ctx) ||
@@ -277,6 +283,37 @@ function parseTextCommentNode(node: Node, ctx: ParsingContext): AST | null {
return null;
}
function parseTCustom(node: Element, ctx: ParsingContext): AST | null {
if (!ctx.customDirectives) {
return null;
}
const nodeAttrsNames = node.getAttributeNames();
for (let attr of nodeAttrsNames) {
if (attr === "t-custom" || attr === "t-custom-") {
throw new OwlError("Missing custom directive name with t-custom directive");
}
if (attr.startsWith("t-custom-")) {
const directiveName = attr.split(".")[0].slice(9);
const customDirective = ctx.customDirectives[directiveName];
if (!customDirective) {
throw new OwlError(`Custom directive "${directiveName}" is not defined`);
}
const value = node.getAttribute(attr)!;
const modifiers = attr.split(".").slice(1);
node.removeAttribute(attr);
try {
customDirective(node, value, modifiers);
} catch (error) {
throw new OwlError(
`Custom directive "${directiveName}" throw the following error: ${error}`
);
}
return parseNode(node, ctx);
}
}
return null;
}
// -----------------------------------------------------------------------------
// debugging
// -----------------------------------------------------------------------------
+2
View File
@@ -12,5 +12,7 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(
dev: this.dev,
translateFn: this.translateFn,
translatableAttributes: this.translatableAttributes,
customDirectives: this.customDirectives,
hasGlobalValues: this.hasGlobalValues,
});
};
+17 -1
View File
@@ -30,6 +30,13 @@ export function makeRootFiber(node: ComponentNode): Fiber {
fibersInError.delete(current);
fibersInError.delete(root);
current.appliedToDom = false;
if (current instanceof RootFiber) {
// it is possible that this fiber is a fiber that crashed while being
// mounted, so the mounted list is possibly corrupted. We restore it to
// its normal initial state (which is empty list or a list with a mount
// fiber.
current.mounted = current instanceof MountFiber ? [current] : [];
}
}
return current;
}
@@ -152,6 +159,7 @@ export class RootFiber extends Fiber {
const node = this.node;
this.locked = true;
let current: Fiber | undefined = undefined;
let mountedFibers = this.mounted;
try {
// Step 1: calling all willPatch lifecycle hooks
for (current of this.willPatch) {
@@ -173,7 +181,6 @@ export class RootFiber extends Fiber {
this.locked = false;
// Step 4: calling all mounted lifecycle hooks
let mountedFibers = this.mounted;
while ((current = mountedFibers.pop())) {
current = current;
if (current.appliedToDom) {
@@ -194,6 +201,15 @@ export class RootFiber extends Fiber {
}
}
} catch (e) {
// if mountedFibers is not empty, this means that a crash occured while
// calling the mounted hooks of some component. So, there may still be
// some component that have been mounted, but for which the mounted hooks
// have not been called. Here, we remove the willUnmount hooks for these
// specific component to prevent a worse situation (willUnmount being
// called even though mounted has not been called)
for (let fiber of mountedFibers) {
fiber.node.willUnmount = [];
}
this.locked = false;
node.app.handleError({ fiber: current || this, error: e });
}
+8 -1
View File
@@ -93,7 +93,14 @@ export class Scheduler {
if (!hasError) {
fiber.complete();
}
this.tasks.delete(fiber);
// at this point, the fiber should have been applied to the DOM, so we can
// remove it from the task list. If it is not the case, it means that there
// was an error and an error handler triggered a new rendering that recycled
// the fiber, so in that case, we actually want to keep the fiber around,
// otherwise it will just be ignored.
if (fiber.appliedToDom) {
this.tasks.delete(fiber);
}
}
}
}
+10 -1
View File
@@ -5,6 +5,7 @@ import { Portal, portalTemplate } from "./portal";
import { helpers } from "./template_helpers";
import { OwlError } from "../common/owl_error";
import { parseXML } from "../common/utils";
import type { customDirectives } from "../common/types";
const bdom = { text, createBlock, list, multi, html, toggler, comment };
@@ -14,6 +15,8 @@ export interface TemplateSetConfig {
translateFn?: (s: string) => string;
templates?: string | Document | Record<string, string>;
getTemplate?: (s: string) => Element | Function | string | void;
customDirectives?: customDirectives;
globalValues?: object;
}
export class TemplateSet {
@@ -27,6 +30,9 @@ export class TemplateSet {
translateFn?: (s: string) => string;
translatableAttributes?: string[];
Portal = Portal;
customDirectives: customDirectives;
runtimeUtils: object;
hasGlobalValues: boolean;
constructor(config: TemplateSetConfig = {}) {
this.dev = config.dev || false;
@@ -42,6 +48,9 @@ export class TemplateSet {
}
}
this.getRawTemplate = config.getTemplate;
this.customDirectives = config.customDirectives || {};
this.runtimeUtils = { ...helpers, __globals__: config.globalValues || {} };
this.hasGlobalValues = Boolean(config.globalValues && Object.keys(config.globalValues).length);
}
addTemplate(name: string, template: string | Element) {
@@ -97,7 +106,7 @@ export class TemplateSet {
this.templates[name] = function (context, parent) {
return templates[name].call(this, context, parent);
};
const template = templateFn(this, bdom, helpers);
const template = templateFn(this, bdom, this.runtimeUtils);
this.templates[name] = template;
}
return this.templates[name];
+1 -1
View File
@@ -1,2 +1,2 @@
// do not modify manually. This file is generated by the release script.
export const version = "2.4.0";
export const version = "2.5.2";
+15
View File
@@ -43,6 +43,21 @@ exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately
}"
`;
exports[`app can add functions to the bdom 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { __globals__ } = helpers;
let block1 = createBlock(\`<div class=\\"my-div\\" block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [()=>__globals__.plop('click'), ctx];
return block1([hdlr1]);
}
}"
`;
exports[`app can call processTask twice in a row without crashing 1`] = `
"function anonymous(app, bdom, helpers
) {
+18
View File
@@ -201,4 +201,22 @@ describe("app", () => {
await app.mount(fixture);
expect(fixture.innerHTML).toBe("parent<div></div>");
});
test("can add functions to the bdom", async () => {
const steps: string[] = [];
class SomeComponent extends Component {
static template = xml`<div t-on-click="() => __globals__.plop('click')" class="my-div"/>`;
}
const app = new App(SomeComponent, {
globalValues: {
plop: (string: any) => {
steps.push(string);
},
},
});
await app.mount(fixture);
expect(fixture.innerHTML).toBe(`<div class="my-div"></div>`);
fixture.querySelector("div")!.click();
expect(steps).toEqual(["click"]);
});
});
@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`t-custom can use t-custom directive on a node 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"my-div\\" block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['click'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-custom can use t-custom directive with modifiers on a node 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"my-div\\" block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['click'], ctx];
return block1([hdlr1]);
}
}"
`;
+57
View File
@@ -0,0 +1,57 @@
import { App, Component, xml } from "../../src";
import { makeTestFixture, snapshotEverything } from "../helpers";
let fixture: HTMLElement;
snapshotEverything();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("t-custom", () => {
test("can use t-custom directive on a node", async () => {
const steps: string[] = [];
class SomeComponent extends Component {
static template = xml`<div t-custom-plop="click" class="my-div"/>`;
click() {
steps.push("clicked");
}
}
const app = new App(SomeComponent, {
customDirectives: {
plop: (node, value) => {
node.setAttribute("t-on-click", value);
},
},
});
await app.mount(fixture);
expect(fixture.innerHTML).toBe(`<div class="my-div"></div>`);
fixture.querySelector("div")!.click();
expect(steps).toEqual(["clicked"]);
});
test("can use t-custom directive with modifiers on a node", async () => {
const steps: string[] = [];
class SomeComponent extends Component {
static template = xml`<div t-custom-plop.mouse.stop="click" class="my-div"/>`;
click() {
steps.push("clicked");
}
}
const app = new App(SomeComponent, {
customDirectives: {
plop: (node, value, modifiers) => {
node.setAttribute("t-on-click", value);
for (let mod of modifiers) {
steps.push(mod);
}
},
},
});
await app.mount(fixture);
expect(fixture.innerHTML).toBe(`<div class="my-div"></div>`);
fixture.querySelector("div")!.click();
expect(steps).toEqual(["mouse", "stop", "clicked"]);
});
});
@@ -320,6 +320,50 @@ exports[`can catch errors can catch an error in a component render function 3`]
}"
`;
exports[`can catch errors can catch an error in onmounted 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(null, false, false, false, []);
return function template(ctx, node, key = \\"\\") {
let b2, b3;
b2 = text(\`Main\`);
if (ctx['state'].ok) {
const Comp1 = ctx['component'];
b3 = toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
}
return multi([b2, b3]);
}
}"
`;
exports[`can catch errors can catch an error in onmounted 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>Error!!!</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`can catch errors can catch an error in onmounted 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>perfect</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`can catch errors can catch an error in the constructor call of a component render function 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -1147,6 +1191,135 @@ exports[`can catch errors error in mounted on a component with a sibling (proper
}"
`;
exports[`can catch errors error in onMounted, graceful recovery 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(null, false, false, false, []);
return function template(ctx, node, key = \\"\\") {
const Comp1 = ctx['component'];
return toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
const comp2 = app.createComponent(\`Boom\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`parent\`);
const b3 = comp1({}, key + \`__1\`, node, this, null);
const b4 = comp2({}, key + \`__2\`, node, this, null);
return multi([b2, b3, b4]);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`abc\`);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`boom\`);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery 5`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`def\`);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery, variation 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(null, false, false, false, []);
return function template(ctx, node, key = \\"\\") {
let b2, b3;
b2 = text(\`R\`);
if (ctx['state'].gogogo) {
const Comp1 = ctx['component'];
b3 = toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
}
return multi([b2, b3]);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery, variation 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
const comp2 = app.createComponent(\`Boom\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`parent\`);
const b3 = comp1({}, key + \`__1\`, node, this, null);
const b4 = comp2({}, key + \`__2\`, node, this, null);
return multi([b2, b3, b4]);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery, variation 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`abc\`);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery, variation 5`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`boom\`);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery, variation 6`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`def\`);
}
}"
`;
exports[`can catch errors onError in class inheritance is called if rethrown 1`] = `
"function anonymous(app, bdom, helpers
) {
+270
View File
@@ -564,6 +564,82 @@ describe("can catch errors", () => {
expect(mockConsoleWarn).toBeCalledTimes(0);
});
test("can catch an error in onmounted", async () => {
class ErrorComponent extends Component {
static template = xml`<div>Error!!!</div>`;
setup() {
useLogLifecycle();
onMounted(() => {
throw new Error("error");
});
}
}
class PerfectComponent extends Component {
static template = xml`<div>perfect</div>`;
setup() {
useLogLifecycle();
}
}
class Main extends Component {
static template = xml`Main<t t-if="state.ok" t-component="component"/>`;
component: any;
state: any;
setup() {
this.state = useState({ ok: false });
useLogLifecycle();
this.component = ErrorComponent;
onError(() => {
this.component = PerfectComponent;
this.render();
});
}
}
const app = await mount(Main, fixture);
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Main:setup",
"Main:willStart",
"Main:willRender",
"Main:rendered",
"Main:mounted",
]
`);
expect(fixture.innerHTML).toBe("Main");
(app as any).state.ok = true;
await nextTick();
expect(fixture.innerHTML).toBe("Main<div>Error!!!</div>");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Main:willRender",
"ErrorComponent:setup",
"ErrorComponent:willStart",
"Main:rendered",
"ErrorComponent:willRender",
"ErrorComponent:rendered",
"Main:willPatch",
"ErrorComponent:mounted",
"Main:willRender",
"PerfectComponent:setup",
"PerfectComponent:willStart",
"Main:rendered",
]
`);
await nextTick();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"PerfectComponent:willRender",
"PerfectComponent:rendered",
"Main:willPatch",
"ErrorComponent:willUnmount",
"ErrorComponent:willDestroy",
"PerfectComponent:mounted",
"Main:patched",
]
`);
expect(fixture.innerHTML).toBe("Main<div>perfect</div>");
});
test("calling a hook outside setup should crash", async () => {
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
@@ -1602,4 +1678,198 @@ describe("can catch errors", () => {
`);
expect(fixture.innerHTML).toBe("2");
});
test("error in onMounted, graceful recovery", async () => {
class Child extends Component {
static template = xml`abc`;
setup() {
useLogLifecycle();
}
}
class OtherChild extends Component {
static template = xml`def`;
setup() {
useLogLifecycle();
}
}
class Boom extends Component {
static template = xml`boom`;
setup() {
useLogLifecycle();
onMounted(() => {
throw new Error("boom");
});
}
}
class Parent extends Component {
static template = xml`parent<Child/><Boom/>`;
static components = { Child, Boom };
setup() {
useLogLifecycle();
}
}
class Root extends Component {
static template = xml`<t t-component="component"/>`;
component: any = Parent;
setup() {
useLogLifecycle();
onError(() => {
logStep("error");
this.component = OtherChild;
this.render();
});
}
}
await mount(Root, fixture);
expect(fixture.innerHTML).toBe("def");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Root:setup",
"Root:willStart",
"Root:willRender",
"Parent:setup",
"Parent:willStart",
"Root:rendered",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Boom:setup",
"Boom:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Boom:willRender",
"Boom:rendered",
"Boom:mounted",
"error",
"Root:willRender",
"OtherChild:setup",
"OtherChild:willStart",
"Root:rendered",
"OtherChild:willRender",
"OtherChild:rendered",
"OtherChild:mounted",
"Root:mounted",
]
`);
});
test("error in onMounted, graceful recovery, variation", async () => {
class Child extends Component {
static template = xml`abc`;
setup() {
useLogLifecycle();
}
}
class OtherChild extends Component {
static template = xml`def`;
setup() {
useLogLifecycle();
}
}
class Boom extends Component {
static template = xml`boom`;
setup() {
useLogLifecycle();
onMounted(() => {
throw new Error("boom");
});
}
}
class Parent extends Component {
static template = xml`parent<Child/><Boom/>`;
static components = { Child, Boom };
setup() {
useLogLifecycle();
}
}
class Root extends Component {
static template = xml`R<t t-if="state.gogogo" t-component="component"/>`;
component: any = Parent;
state = useState({ gogogo: false });
setup() {
useLogLifecycle();
onError(() => {
logStep("error");
this.component = OtherChild;
this.render();
});
}
}
const root = await mount(Root, fixture);
expect(fixture.innerHTML).toBe("R");
// standard mounting process
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Root:setup",
"Root:willStart",
"Root:willRender",
"Root:rendered",
"Root:mounted",
]
`);
root.state.gogogo = true;
await nextTick();
expect(fixture.innerHTML).toBe("Rparentabcboom");
// rerender, root creates sub components, it crashes, tries to recover
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Root:willRender",
"Parent:setup",
"Parent:willStart",
"Root:rendered",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Boom:setup",
"Boom:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Boom:willRender",
"Boom:rendered",
"Root:willPatch",
"Boom:mounted",
"error",
"Root:willRender",
"OtherChild:setup",
"OtherChild:willStart",
"Root:rendered",
]
`);
await nextTick();
expect(fixture.innerHTML).toBe("Rdef");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"OtherChild:willRender",
"OtherChild:rendered",
"Root:willPatch",
"Child:willDestroy",
"Boom:willUnmount",
"Boom:willDestroy",
"Parent:willDestroy",
"OtherChild:mounted",
"Root:patched",
]
`);
});
});
-1
View File
@@ -140,7 +140,6 @@ async function startRelease() {
}
// ---------------------------------------------------------------------------
log(`Step ${step++}/${STEPS}: Creating the release...`);
const relaseResult = await execCommand(`gh release create v${next} dist/*.js dist/*.zip ${draft} -F ${file}`);
if (relaseResult !== 0) {