Compare commits

..

10 Commits

Author SHA1 Message Date
Géry Debongnie 1179e84971 [REL] v2.0.0-beta-5
# v2.0.0-beta-5

- fix: compiler, components: allow empty slots with default content
- fix: issue with delayed renders being left pending forever
- fix: dynamic t-slot with scope bug
- fix: protect against errors in onWillDestroy
- fix: protect against user code executing in critical sections
- fix: concurrency issue (more robust handling of children per render)
- fix: prevent rendering destroyed children in some cases
2022-04-07 15:36:22 +02:00
Géry Debongnie 24b1ea7604 [FIX] components: prevent rendering destroyed children in some cases 2022-04-01 15:02:57 +02:00
Géry Debongnie 3e4ebb6378 [REF] component: slightly simplify code logic 2022-04-01 15:02:57 +02:00
Géry Debongnie 859748aed9 [FIX] concurrency issue (more robust handling of children per render)
Before this commit, the list of all children was managed at the level of
the root fiber, but this could cause issue when subfibers would be
reused. With this commit, we use the childrenMap object that exists on
each fiber instead.
2022-04-01 13:40:24 +02:00
Géry Debongnie fd13277e1d [FIX] component: protect against user code executing in critical section
Canceling a fiber may cause user code to be run, which means that some
new renderings could be scheduled, but this could interfere with the
current renderings!
2022-04-01 13:40:24 +02:00
Géry Debongnie 7fb166bd50 [FIX] component: protect against errors in onWillDestroy 2022-04-01 13:40:24 +02:00
Lucas Perais (lpe) decf42c742 [FIX] compiler, component: dynamic t-slot with scope
A little typo was preventing a dynamic t-slot with a scope
(`<t t-slot="{{ state.name }}" myScope="someValue" />`)
to work properly.

This commit corrects this.
2022-03-31 16:15:47 +02:00
Géry Debongnie 989b0d6709 [REF] improve children handling in components/fibers 2022-03-31 08:54:59 +02:00
Géry Debongnie 9b93521da4 [FIX] issue with delayed renders being left pending forever 2022-03-31 08:54:59 +02:00
Lucas Perais (lpe) de240b1ebc [FIX] compiler, components: allow empty slot
Before this commit, a t-set-slot that has no content was not even compiled, and thus not passed
to the component for which it has was defined

After this commit, we allow a t-set-slot to have no content (because it can have slot props)
2022-03-29 16:24:05 +02:00
15 changed files with 1075 additions and 113 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "2.0.0-beta-4", "version": "2.0.0-beta-5",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js", "main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js", "browser": "dist/owl.iife.js",
+1 -1
View File
@@ -67,7 +67,7 @@ export class App<
} }
makeNode(Component: ComponentConstructor, props: any): ComponentNode { makeNode(Component: ComponentConstructor, props: any): ComponentNode {
return new ComponentNode(Component, props, this); return new ComponentNode(Component, props, this, null, null);
} }
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) { mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
+6 -3
View File
@@ -1115,8 +1115,11 @@ export class CodeGenerator {
let slotStr: string[] = []; let slotStr: string[] = [];
for (let slotName in ast.slots) { for (let slotName in ast.slots) {
const slotAst = ast.slots[slotName]; const slotAst = ast.slots[slotName];
const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on); const params = [];
const params = [`__render: ${name}, __ctx: ${ctxStr}`]; if (slotAst.content) {
const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on);
params.push(`__render: ${name}, __ctx: ${ctxStr}`);
}
const scope = ast.slots[slotName].scope; const scope = ast.slots[slotName].scope;
if (scope) { if (scope) {
params.push(`__scope: "${scope}"`); params.push(`__scope: "${scope}"`);
@@ -1231,7 +1234,7 @@ export class CodeGenerator {
if (dynamic) { if (dynamic) {
let name = this.generateId("slot"); let name = this.generateId("slot");
this.define(name, slotName); this.define(name, slotName);
blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}), ${dynamic}, ${scope})`; blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}, ${dynamic}, ${scope}))`;
} else { } else {
blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope})`; blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope})`;
} }
+17 -19
View File
@@ -118,7 +118,7 @@ export interface ASTTCall {
} }
interface SlotDefinition { interface SlotDefinition {
content: AST; content: AST | null;
scope: string | null; scope: string | null;
on: EventHandlers | null; on: EventHandlers | null;
attrs: Attrs | null; attrs: Attrs | null;
@@ -749,26 +749,24 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
slotNode.removeAttribute("t-set-slot"); slotNode.removeAttribute("t-set-slot");
slotNode.remove(); slotNode.remove();
const slotAst = parseNode(slotNode, ctx); const slotAst = parseNode(slotNode, ctx);
if (slotAst) { let on: SlotDefinition["on"] = null;
let on: SlotDefinition["on"] = null; let attrs: Attrs | null = null;
let attrs: Attrs | null = null; let scope: string | null = null;
let scope: string | null = null; for (let attributeName of slotNode.getAttributeNames()) {
for (let attributeName of slotNode.getAttributeNames()) { const value = slotNode.getAttribute(attributeName)!;
const value = slotNode.getAttribute(attributeName)!; if (attributeName === "t-slot-scope") {
if (attributeName === "t-slot-scope") { scope = value;
scope = value; continue;
continue; } else if (attributeName.startsWith("t-on-")) {
} else if (attributeName.startsWith("t-on-")) { on = on || {};
on = on || {}; on[attributeName.slice(5)] = value;
on[attributeName.slice(5)] = value; } else {
} else { attrs = attrs || {};
attrs = attrs || {}; attrs[attributeName] = value;
attrs[attributeName] = value;
}
} }
slots = slots || {};
slots[name] = { content: slotAst, on, attrs, scope };
} }
slots = slots || {};
slots[name] = { content: slotAst, on, attrs, scope };
} }
// default slot // default slot
+26 -32
View File
@@ -79,13 +79,8 @@ export function component<P extends object>(
let node: any = ctx.children[key]; let node: any = ctx.children[key];
let isDynamic = typeof name !== "string"; let isDynamic = typeof name !== "string";
if (node) { if (node && node.status === STATUS.DESTROYED) {
if (node.status < STATUS.MOUNTED) { node = undefined;
node.destroy();
node = undefined;
} else if (node.status === STATUS.DESTROYED) {
node = undefined;
}
} }
if (isDynamic && node && node.component.constructor !== name) { if (isDynamic && node && node.component.constructor !== name) {
node = undefined; node = undefined;
@@ -114,12 +109,11 @@ export function component<P extends object>(
throw new Error(`Cannot find the definition of component "${name}"`); throw new Error(`Cannot find the definition of component "${name}"`);
} }
} }
node = new ComponentNode(C, props, ctx.app, ctx); node = new ComponentNode(C, props, ctx.app, ctx, key);
ctx.children[key] = node; ctx.children[key] = node;
node.initiateRender(new Fiber(node, parentFiber)); node.initiateRender(new Fiber(node, parentFiber));
} }
parentFiber.root!.reachedChildren.add(node); parentFiber.childrenMap[key] = node;
return node; return node;
} }
@@ -137,6 +131,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
bdom: BDom | null = null; bdom: BDom | null = null;
status: STATUS = STATUS.NEW; status: STATUS = STATUS.NEW;
forceNextRender: boolean = false; forceNextRender: boolean = false;
parentKey: string | null;
renderFn: Function; renderFn: Function;
parent: ComponentNode | null; parent: ComponentNode | null;
@@ -153,10 +148,17 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
patched: LifecycleHook[] = []; patched: LifecycleHook[] = [];
willDestroy: LifecycleHook[] = []; willDestroy: LifecycleHook[] = [];
constructor(C: ComponentConstructor<P, E>, props: P, app: App, parent?: ComponentNode) { constructor(
C: ComponentConstructor<P, E>,
props: P,
app: App,
parent: ComponentNode | null,
parentKey: string | null
) {
currentNode = this; currentNode = this;
this.app = app; this.app = app;
this.parent = parent || null; this.parent = parent;
this.parentKey = parentKey;
this.level = parent ? parent.level + 1 : 0; this.level = parent ? parent.level + 1 : 0;
applyDefaultProps(props, C); applyDefaultProps(props, C);
const env = (parent && parent.childEnv) || app.env; const env = (parent && parent.childEnv) || app.env;
@@ -193,7 +195,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
async render(deep: boolean = false) { async render(deep: boolean = false) {
let current = this.fiber; let current = this.fiber;
if (current && current.root!.locked) { if (current && (current.root!.locked || (current as any).bdom === true)) {
await Promise.resolve(); await Promise.resolve();
// situation may have changed after the microtask tick // situation may have changed after the microtask tick
current = this.fiber; current = this.fiber;
@@ -215,6 +217,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
const fiber = makeRootFiber(this); const fiber = makeRootFiber(this);
fiber.deep = deep; fiber.deep = deep;
this.fiber = fiber; this.fiber = fiber;
this.app.scheduler.addFiber(fiber); this.app.scheduler.addFiber(fiber);
await Promise.resolve(); await Promise.resolve();
if (this.status === STATUS.DESTROYED) { if (this.status === STATUS.DESTROYED) {
@@ -254,8 +257,14 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
for (let child of Object.values(this.children)) { for (let child of Object.values(this.children)) {
child._destroy(); child._destroy();
} }
for (let cb of this.willDestroy) { if (this.willDestroy.length) {
cb.call(component); try {
for (let cb of this.willDestroy) {
cb.call(component);
}
} catch (e) {
handleError({ error: e, node: this });
}
} }
this.status = STATUS.DESTROYED; this.status = STATUS.DESTROYED;
} }
@@ -326,6 +335,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
bdom.mount(parent, anchor); bdom.mount(parent, anchor);
this.status = STATUS.MOUNTED; this.status = STATUS.MOUNTED;
this.fiber!.appliedToDom = true; this.fiber!.appliedToDom = true;
this.children = this.fiber!.childrenMap;
this.fiber = null; this.fiber = null;
} }
@@ -343,10 +353,8 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
} }
_patch() { _patch() {
const hasChildren = Object.keys(this.children).length > 0; const hasChildren = Object.keys(this.children).length > 0;
this.children = this.fiber!.childrenMap;
this.bdom!.patch(this!.fiber!.bdom!, hasChildren); this.bdom!.patch(this!.fiber!.bdom!, hasChildren);
if (hasChildren) {
this.cleanOutdatedChildren();
}
this.fiber!.appliedToDom = true; this.fiber!.appliedToDom = true;
this.fiber = null; this.fiber = null;
} }
@@ -359,20 +367,6 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
this.bdom!.remove(); this.bdom!.remove();
} }
cleanOutdatedChildren() {
const children = this.children;
for (const key in children) {
const node = children[key];
const status = node.status;
if (status !== STATUS.MOUNTED) {
delete children[key];
if (status !== STATUS.DESTROYED) {
node.destroy();
}
}
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Some debug helpers // Some debug helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+5 -9
View File
@@ -5,7 +5,7 @@ import type { Fiber } from "./fibers";
export const fibersInError: WeakMap<Fiber, any> = new WeakMap(); export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap(); export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
function _handleError(node: ComponentNode | null, error: any, isFirstRound = false): boolean { function _handleError(node: ComponentNode | null, error: any): boolean {
if (!node) { if (!node) {
return false; return false;
} }
@@ -16,23 +16,19 @@ function _handleError(node: ComponentNode | null, error: any, isFirstRound = fal
const errorHandlers = nodeErrorHandlers.get(node); const errorHandlers = nodeErrorHandlers.get(node);
if (errorHandlers) { if (errorHandlers) {
let stopped = false; let handled = false;
// execute in the opposite order // execute in the opposite order
for (let i = errorHandlers.length - 1; i >= 0; i--) { for (let i = errorHandlers.length - 1; i >= 0; i--) {
try { try {
errorHandlers[i](error); errorHandlers[i](error);
stopped = true; handled = true;
break; break;
} catch (e) { } catch (e) {
error = e; error = e;
} }
} }
if (stopped) { if (handled) {
if (isFirstRound && fiber && fiber.node.fiber) {
const root = fiber.root!;
root.setCounter(root.counter - 1);
}
return true; return true;
} }
} }
@@ -55,7 +51,7 @@ export function handleError(params: ErrorParams) {
fibersInError.set(fiber.root!, error); fibersInError.set(fiber.root!, error);
const handled = _handleError(node, error, true); const handled = _handleError(node, error);
if (!handled) { if (!handled) {
console.warn(`[Owl] Unhandled error. Destroying the root component`); console.warn(`[Owl] Unhandled error. Destroying the root component`);
try { try {
+21 -30
View File
@@ -8,10 +8,6 @@ export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
if (current) { if (current) {
cancelFibers(current.children); cancelFibers(current.children);
current.root = null; current.root = null;
if (current instanceof RootFiber && current.delayedRenders.length) {
let root = parent.root!;
root.delayedRenders = root.delayedRenders.concat(current.delayedRenders);
}
} }
return new Fiber(node, parent); return new Fiber(node, parent);
} }
@@ -20,12 +16,15 @@ export function makeRootFiber(node: ComponentNode): Fiber {
let current = node.fiber; let current = node.fiber;
if (current) { if (current) {
let root = current.root!; let root = current.root!;
// lock root fiber because canceling children fibers may destroy components,
// which means any arbitrary code can be run in onWillDestroy, which may
// trigger new renderings
root.locked = true;
root.setCounter(root.counter + 1 - cancelFibers(current.children)); root.setCounter(root.counter + 1 - cancelFibers(current.children));
root.locked = false;
current.children = []; current.children = [];
current.childrenMap = {};
current.bdom = null; current.bdom = null;
if (current === root) {
root.reachedChildren = new WeakSet();
}
if (fibersInError.has(current)) { if (fibersInError.has(current)) {
fibersInError.delete(current); fibersInError.delete(current);
fibersInError.delete(root); fibersInError.delete(root);
@@ -49,7 +48,11 @@ export function makeRootFiber(node: ComponentNode): Fiber {
function cancelFibers(fibers: Fiber[]): number { function cancelFibers(fibers: Fiber[]): number {
let result = 0; let result = 0;
for (let fiber of fibers) { for (let fiber of fibers) {
fiber.node.fiber = null; let node = fiber.node;
if (node.status === STATUS.NEW) {
node.destroy();
}
node.fiber = null;
if (fiber.bdom) { if (fiber.bdom) {
// if fiber has been rendered, this means that the component props have // if fiber has been rendered, this means that the component props have
// been updated. however, this fiber will not be patched to the dom, so // been updated. however, this fiber will not be patched to the dom, so
@@ -57,7 +60,7 @@ function cancelFibers(fibers: Fiber[]): number {
// the same props, and skip the render completely. With the next line, // the same props, and skip the render completely. With the next line,
// we kindly request the component code to force a render, so it works as // we kindly request the component code to force a render, so it works as
// expected. // expected.
fiber.node.forceNextRender = true; node.forceNextRender = true;
} else { } else {
result++; result++;
} }
@@ -74,6 +77,7 @@ export class Fiber {
children: Fiber[] = []; children: Fiber[] = [];
appliedToDom = false; appliedToDom = false;
deep: boolean = false; deep: boolean = false;
childrenMap: ComponentNode["children"] = {};
constructor(node: ComponentNode, parent: Fiber | null) { constructor(node: ComponentNode, parent: Fiber | null) {
this.node = node; this.node = node;
@@ -92,20 +96,16 @@ export class Fiber {
render() { render() {
// if some parent has a fiber => register in followup // if some parent has a fiber => register in followup
let prev = this.root!.node; let prev = this.root!.node;
let scheduler = prev.app.scheduler;
let current = prev.parent; let current = prev.parent;
while (current) { while (current) {
if (current.fiber) { if (current.fiber) {
let root = current.fiber.root!; let root = current.fiber.root!;
if (root.counter) { if (root.counter === 0 && prev.parentKey! in current.fiber.childrenMap) {
root.delayedRenders.push(this);
return;
} else {
if (!root.reachedChildren.has(prev)) {
// is dead
this.node.app.scheduler.shouldClear = true;
return;
}
current = root.node; current = root.node;
} else {
scheduler.delayedRenders.push(this);
return;
} }
} }
prev = current; prev = current;
@@ -121,11 +121,12 @@ export class Fiber {
const root = this.root; const root = this.root;
if (root) { if (root) {
try { try {
(this.bdom as any) = true;
this.bdom = node.renderFn(); this.bdom = node.renderFn();
root.setCounter(root.counter - 1);
} catch (e) { } catch (e) {
handleError({ node, error: e }); handleError({ node, error: e });
} }
root.setCounter(root.counter - 1);
} }
} }
} }
@@ -141,9 +142,6 @@ export class RootFiber extends Fiber {
// i.e.: render triggered in onWillUnmount or in willPatch will be delayed // i.e.: render triggered in onWillUnmount or in willPatch will be delayed
locked: boolean = false; locked: boolean = false;
delayedRenders: Fiber[] = [];
reachedChildren: WeakSet<ComponentNode> = new WeakSet();
complete() { complete() {
const node = this.node; const node = this.node;
this.locked = true; this.locked = true;
@@ -198,14 +196,6 @@ export class RootFiber extends Fiber {
setCounter(newValue: number) { setCounter(newValue: number) {
this.counter = newValue; this.counter = newValue;
if (newValue === 0) { if (newValue === 0) {
if (this.delayedRenders.length) {
for (let f of this.delayedRenders) {
if (f.root) {
f.render();
}
}
this.delayedRenders = [];
}
this.node.app.scheduler.flush(); this.node.app.scheduler.flush();
} }
} }
@@ -230,6 +220,7 @@ export class MountFiber extends RootFiber {
let current: Fiber | undefined = this; let current: Fiber | undefined = this;
try { try {
const node = this.node; const node = this.node;
node.children = this.childrenMap;
(node.app.constructor as any).validateTarget(this.target); (node.app.constructor as any).validateTarget(this.target);
if (node.bdom) { if (node.bdom) {
// this is a complicated situation: if we mount a fiber with an existing // this is a complicated situation: if we mount a fiber with an existing
+14 -7
View File
@@ -13,7 +13,7 @@ export class Scheduler {
tasks: Set<RootFiber> = new Set(); tasks: Set<RootFiber> = new Set();
requestAnimationFrame: Window["requestAnimationFrame"]; requestAnimationFrame: Window["requestAnimationFrame"];
frame: number = 0; frame: number = 0;
shouldClear: boolean = false; delayedRenders: Fiber[] = [];
constructor() { constructor() {
this.requestAnimationFrame = Scheduler.requestAnimationFrame; this.requestAnimationFrame = Scheduler.requestAnimationFrame;
@@ -28,16 +28,23 @@ export class Scheduler {
* Other tasks are left unchanged. * Other tasks are left unchanged.
*/ */
flush() { flush() {
if (this.delayedRenders.length) {
let renders = this.delayedRenders;
this.delayedRenders = [];
for (let f of renders) {
if (f.root && f.node.status !== STATUS.DESTROYED) {
f.render();
}
}
}
if (this.frame === 0) { if (this.frame === 0) {
this.frame = this.requestAnimationFrame(() => { this.frame = this.requestAnimationFrame(() => {
this.frame = 0; this.frame = 0;
this.tasks.forEach((fiber) => this.processFiber(fiber)); this.tasks.forEach((fiber) => this.processFiber(fiber));
if (this.shouldClear) { for (let task of this.tasks) {
this.shouldClear = false; if (task.node.status === STATUS.DESTROYED) {
for (let task of this.tasks) { this.tasks.delete(task);
if (task.node.status === STATUS.DESTROYED) {
this.tasks.delete(task);
}
} }
} }
}); });
+19
View File
@@ -1370,6 +1370,25 @@ describe("qweb parser", () => {
}); });
}); });
test("a component with an empty named slot", async () => {
expect(parse(`<MyComponent><t t-set-slot="mySlot"></t></MyComponent>`)).toEqual({
dynamicProps: null,
isDynamic: false,
name: "MyComponent",
on: null,
props: null,
slots: {
mySlot: {
attrs: null,
content: null,
on: null,
scope: null,
},
},
type: 11,
});
});
test("a component with a named slot", async () => { test("a component with a named slot", async () => {
expect(parse(`<MyComponent><t t-set-slot="name">foo</t></MyComponent>`)).toEqual({ expect(parse(`<MyComponent><t t-set-slot="name">foo</t></MyComponent>`)).toEqual({
type: ASTType.TComponent, type: ASTType.TComponent,
@@ -52,6 +52,50 @@ exports[`Cascading renders after microtaskTick 3`] = `
}" }"
`; `;
exports[`another scenario with delayed rendering 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\`A\`);
if (ctx['state'].value<15) {
b3 = component(\`B\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`another scenario with delayed rendering 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(ctx['props'].value);
const b3 = component(\`C\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`another scenario with delayed rendering 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<button block-handler-0=\\"click\\"><block-text-1/></button>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['increment'], ctx];
let txt1 = ctx['state'].val;
return block1([hdlr1, txt1]);
}
}"
`;
exports[`async rendering destroying a widget before start is over 1`] = ` exports[`async rendering destroying a widget before start is over 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -1120,6 +1164,69 @@ exports[`delayed rendering, but then initial rendering is cancelled by yet anoth
}" }"
`; `;
exports[`delayed rendering, destruction, stuff happens 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`A\`);
const b3 = component(\`B\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, destruction, stuff happens 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\`B\`);
if (ctx['state'].hasChild) {
b3 = component(\`C\`, {value: ctx['state'].someValue+ctx['props'].value}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, destruction, stuff happens 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block4 = createBlock(\`<p><block-text-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`C\`);
const b3 = component(\`D\`, {}, key + \`__1\`, node, ctx);
let txt1 = ctx['props'].value;
const b4 = block4([txt1]);
return multi([b2, b3, b4]);
}
}"
`;
exports[`delayed rendering, destruction, stuff happens 4`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block3 = createBlock(\`<button block-handler-0=\\"click\\"><block-text-1/></button>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`D\`);
let hdlr1 = [ctx['increment'], ctx];
let txt1 = ctx['state'].val;
const b3 = block3([hdlr1, txt1]);
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, reusing fiber and stuff 1`] = ` exports[`delayed rendering, reusing fiber and stuff 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -1245,6 +1352,46 @@ exports[`delayed rendering, then component is destroyed and stuff 3`] = `
}" }"
`; `;
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\` A \`);
if (ctx['state'].flag) {
const b4 = component(\`B\`, {value: ctx['state'].valueB}, key + \`__1\`, node, ctx);
const b5 = component(\`C\`, {value: ctx['state'].valueC}, key + \`__2\`, node, ctx);
b3 = multi([b4, b5]);
}
return multi([b2, b3]);
}
}"
`;
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].value);
}
}"
`;
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].val+ctx['props'].value);
}
}"
`;
exports[`destroying/recreating a subcomponent, other scenario 1`] = ` exports[`destroying/recreating a subcomponent, other scenario 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -1422,6 +1569,70 @@ exports[`rendering parent twice, with different props on child and stuff 2`] = `
}" }"
`; `;
exports[`renderings, destruction, patch, stuff, ... yet another variation 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`A\`);
const b3 = component(\`B\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
const b4 = component(\`D\`, {}, key + \`__2\`, node, ctx);
return multi([b2, b3, b4]);
}
}"
`;
exports[`renderings, destruction, patch, stuff, ... yet another variation 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\`B\`);
if (ctx['props'].value===33) {
b3 = component(\`C\`, {}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`renderings, destruction, patch, stuff, ... yet another variation 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block3 = createBlock(\`<p block-handler-0=\\"click\\"><block-text-1/></p>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`D\`);
let hdlr1 = [ctx['increment'], ctx];
let txt1 = ctx['state'].val;
const b3 = block3([hdlr1, txt1]);
return multi([b2, b3]);
}
}"
`;
exports[`renderings, destruction, patch, stuff, ... yet another variation 4`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block3 = createBlock(\`<span block-handler-0=\\"click\\"><block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`C\`);
let hdlr1 = [ctx['increment'], ctx];
let txt1 = ctx['state'].val;
const b3 = block3([hdlr1, txt1]);
return multi([b2, b3]);
}
}"
`;
exports[`t-foreach with dynamic async component 1`] = ` exports[`t-foreach with dynamic async component 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -85,6 +85,64 @@ exports[`basics simple catchError 2`] = `
}" }"
`; `;
exports[`can catch errors an error in onWillDestroy 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(ctx['state'].value);
if (ctx['state'].hasChild) {
b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`can catch errors an error in onWillDestroy 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div>abc</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`can catch errors an error in onWillDestroy, variation 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(ctx['state'].value);
if (ctx['state'].hasChild) {
b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`can catch errors an error in onWillDestroy, variation 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div>abc</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`can catch errors calling a hook outside setup should crash 1`] = ` exports[`can catch errors calling a hook outside setup should crash 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -626,7 +626,7 @@ exports[`slots dynamic t-slot call 2`] = `
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['toggle'], ctx]; let hdlr1 = [ctx['toggle'], ctx];
const slot1 = (ctx['current'].slot); const slot1 = (ctx['current'].slot);
const b2 = toggler(slot1, callSlot(ctx, node, key, slot1), true, null); const b2 = toggler(slot1, callSlot(ctx, node, key, slot1, true, null));
return block1([hdlr1], [b2]); return block1([hdlr1], [b2]);
} }
}" }"
@@ -1444,6 +1444,110 @@ exports[`slots simple default slot, variation 2`] = `
}" }"
`; `;
exports[`slots simple dynamic slot with slot scope 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { capture, markRaw } = 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: markRaw({'slotName': {__render: slot1, __ctx: ctx1, __scope: \\"slotScope\\"}})}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`slots simple dynamic 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 = \\"\\") {
const slot1 = ('slotName');
const b2 = toggler(slot1, callSlot(ctx, node, key, slot1, true, {bool: ctx['state'].bool}));
return block1([], [b2]);
}
}"
`;
exports[`slots simple named and empty slot -- 2 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { capture, markRaw } = helpers;
return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return component(\`Child\`, {slots: markRaw({'myEmptySlot': {myProp: 'myProp text'}})}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`slots simple named and empty slot -- 2 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>\`);
function defaultContent1(ctx, node, key = \\"\\") {
return text(\`default empty\`);
}
return function template(ctx, node, key = \\"\\") {
const b3 = callSlot(ctx, node, key, 'myEmptySlot', false, null, defaultContent1);
return block1([], [b3]);
}
}"
`;
exports[`slots simple named and empty slot 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { capture, markRaw } = helpers;
function slot1(ctx, node, key = \\"\\") {
return text(\`some text\`);
}
return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return component(\`Child\`, {slots: markRaw({'myEmptySlot': {}, 'default': {__render: slot1, __ctx: ctx1}})}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`slots simple named and empty slot 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/><block-child-1/></span>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = callSlot(ctx, node, key, 'default', false, null);
const b3 = callSlot(ctx, node, key, 'myEmptySlot', false, null);
return block1([], [b2, b3]);
}
}"
`;
exports[`slots simple slot with slot scope 1`] = ` exports[`slots simple slot with slot scope 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -1689,7 +1793,7 @@ exports[`slots slot content has different key from other content -- dynamic slot
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const b2 = component(\`Child\`, {parent: 'SlotDisplay'}, key + \`__1\`, node, ctx); const b2 = component(\`Child\`, {parent: 'SlotDisplay'}, key + \`__1\`, node, ctx);
const slot1 = (ctx['slotName']); const slot1 = (ctx['slotName']);
const b3 = toggler(slot1, callSlot(ctx, node, key, slot1), true, null); const b3 = toggler(slot1, callSlot(ctx, node, key, slot1, true, null));
return multi([b2, b3]); return multi([b2, b3]);
} }
}" }"
+401 -8
View File
@@ -4,6 +4,7 @@ import {
mount, mount,
onMounted, onMounted,
onRendered, onRendered,
onWillDestroy,
onWillStart, onWillStart,
onWillUnmount, onWillUnmount,
onWillUpdateProps, onWillUpdateProps,
@@ -114,8 +115,8 @@ test("destroying/recreating a subwidget with different props (if start is not ov
expect(n).toBe(2); expect(n).toBe(2);
expect([ expect([
"W:willRender",
"Child:willDestroy", "Child:willDestroy",
"W:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"W:rendered", "W:rendered",
@@ -172,6 +173,11 @@ test("destroying/recreating a subcomponent, other scenario", async () => {
await nextTick(); await nextTick();
expect([ expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willDestroy",
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
@@ -244,8 +250,8 @@ test("creating two async components, scenario 1", async () => {
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
expect([ expect([
"Parent:willRender",
"ChildA:willDestroy", "ChildA:willDestroy",
"Parent:willRender",
"ChildA:setup", "ChildA:setup",
"ChildA:willStart", "ChildA:willStart",
"ChildB:setup", "ChildB:setup",
@@ -696,8 +702,8 @@ test("rendering component again in next microtick", async () => {
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered", "Parent:rendered",
"Parent:willRender",
"Child:willDestroy", "Child:willDestroy",
"Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered", "Parent:rendered",
@@ -1725,6 +1731,7 @@ test("concurrent renderings scenario 10", async () => {
expect(fixture.innerHTML).toBe("<div><p></p></div>"); expect(fixture.innerHTML).toBe("<div><p></p></div>");
expect([ expect([
"ComponentA:willRender", "ComponentA:willRender",
"ComponentC:willDestroy",
"ComponentB:willUpdateProps", "ComponentB:willUpdateProps",
"ComponentA:rendered", "ComponentA:rendered",
]).toBeLogged(); ]).toBeLogged();
@@ -1735,7 +1742,6 @@ test("concurrent renderings scenario 10", async () => {
expect(rendered).toBe(1); expect(rendered).toBe(1);
expect([ expect([
"ComponentB:willRender", "ComponentB:willRender",
"ComponentC:willDestroy",
"ComponentC:setup", "ComponentC:setup",
"ComponentC:willStart", "ComponentC:willStart",
"ComponentB:rendered", "ComponentB:rendered",
@@ -2275,11 +2281,11 @@ test("concurrent renderings scenario 16", async () => {
"D:setup", "D:setup",
"D:willStart", "D:willStart",
"C:rendered", "C:rendered",
"D:willDestroy",
"B:willRender", "B:willRender",
"C:willUpdateProps", "C:willUpdateProps",
"B:rendered", "B:rendered",
"C:willRender", "C:willRender",
"D:willDestroy",
"D:setup", "D:setup",
"D:willStart", "D:willStart",
"C:rendered", "C:rendered",
@@ -2990,13 +2996,13 @@ test("t-key on dom node having a component", async () => {
expect(fixture.innerHTML).toBe("<div>3</div>"); expect(fixture.innerHTML).toBe("<div>3</div>");
expect([ expect([
"Child (2):willDestroy",
"Child (3):setup", "Child (3):setup",
"Child (3):willStart", "Child (3):willStart",
"Child (3):willRender", "Child (3):willRender",
"Child (3):rendered", "Child (3):rendered",
"Child (1):willUnmount", "Child (1):willUnmount",
"Child (1):willDestroy", "Child (1):willDestroy",
"Child (2):willDestroy",
"Child (3):mounted", "Child (3):mounted",
]).toBeLogged(); ]).toBeLogged();
}); });
@@ -3048,13 +3054,13 @@ test("t-key on dynamic async component (toggler is never patched)", async () =>
expect(fixture.innerHTML).toBe("<div>3</div>"); expect(fixture.innerHTML).toBe("<div>3</div>");
expect([ expect([
"Child (2):willDestroy",
"Child (3):setup", "Child (3):setup",
"Child (3):willStart", "Child (3):willStart",
"Child (3):willRender", "Child (3):willRender",
"Child (3):rendered", "Child (3):rendered",
"Child (1):willUnmount", "Child (1):willUnmount",
"Child (1):willDestroy", "Child (1):willDestroy",
"Child (2):willDestroy",
"Child (3):mounted", "Child (3):mounted",
]).toBeLogged(); ]).toBeLogged();
}); });
@@ -3107,13 +3113,13 @@ test("t-foreach with dynamic async component", async () => {
expect(fixture.innerHTML).toBe("<div>3</div>"); expect(fixture.innerHTML).toBe("<div>3</div>");
expect([ expect([
"Child (2):willDestroy",
"Child (3):setup", "Child (3):setup",
"Child (3):willStart", "Child (3):willStart",
"Child (3):willRender", "Child (3):willRender",
"Child (3):rendered", "Child (3):rendered",
"Child (1):willUnmount", "Child (1):willUnmount",
"Child (1):willDestroy", "Child (1):willDestroy",
"Child (2):willDestroy",
"Child (3):mounted", "Child (3):mounted",
]).toBeLogged(); ]).toBeLogged();
}); });
@@ -3591,6 +3597,393 @@ test("delayed rendering, reusing fiber then component is destroyed and stuff",
]).toBeLogged(); ]).toBeLogged();
}); });
test("another scenario with delayed rendering", async () => {
let prom1 = makeDeferred();
let onSecondRenderA = makeDeferred();
class C extends Component {
static template = xml`<button t-on-click="increment"><t t-esc="state.val"/></button>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class B extends Component {
static template = xml`<t t-esc="props.value"/><C />`;
static components = { C };
setup() {
useLogLifecycle();
onWillUpdateProps(() => prom1);
}
}
class A extends Component {
static template = xml`A<t t-if="state.value lt 15"><B value="state.value"/></t>`;
static components = { B };
state = useState({ value: 3 });
setup() {
useLogLifecycle();
let n = 0;
onRendered(() => {
n++;
if (n === 2) {
onSecondRenderA.resolve();
}
});
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("A3<button>1</button>");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// initiate a render in A, but is blocked in B
parent.state.value = 5;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
// initiate a render in C (will be delayed because of render in A)
fixture.querySelector("button")!.click();
await nextTick();
expect([]).toBeLogged();
// initiate a render in A, that will destroy B
parent.state.value = 23;
await onSecondRenderA;
await nextMicroTick();
expect(["A:willRender", "A:rendered"]).toBeLogged();
// rerender A, but without destroying B
parent.state.value = 7;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
prom1.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("A7<button>2</button>");
expect([
"B:willRender",
"B:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"B:willPatch",
"B:patched",
"A:patched",
"C:willPatch",
"C:patched",
]).toBeLogged();
});
test("destroyed component causes other soon to be destroyed component to rerender, weird stuff happens", async () => {
let def = makeDeferred();
let c: any = null;
class B extends Component {
static template = xml`<t t-esc="props.value"/>`;
setup() {
useLogLifecycle();
onRendered(() => {
def.resolve();
});
onWillDestroy(() => {
c.state.val++;
c.render();
});
}
}
class C extends Component {
static template = xml`<t t-esc="state.val + props.value"/>`;
state = useState({ val: 0 });
setup() {
c = this;
useLogLifecycle();
}
}
class A extends Component {
static template = xml`
A
<t t-if="state.flag">
<B value="state.valueB"/>
<C value="state.valueC"/>
</t>`;
static components = { B, C };
state = useState({ flag: false, valueB: 1, valueC: 2 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe(" A ");
expect(["A:setup", "A:willStart", "A:willRender", "A:rendered", "A:mounted"]).toBeLogged();
// initiate a render in A, but is blocked in B
parent.state.flag = true;
await def;
await nextMicroTick();
expect([
"A:willRender",
"B:setup",
"B:willStart",
"C:setup",
"C:willStart",
"A:rendered",
"B:willRender",
"B:rendered",
"C:willRender",
"C:rendered",
]).toBeLogged();
// initiate render in A => will cancel renders in B/C and restarts
parent.state.valueB = 2;
await nextTick();
expect([
"B:willDestroy",
"C:willDestroy",
"A:willRender",
"B:setup",
"B:willStart",
"C:setup",
"C:willStart",
"A:rendered",
"B:willRender",
"B:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"C:mounted",
"B:mounted",
"A:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe(" A 22");
});
test("delayed rendering, destruction, stuff happens", async () => {
const promC = makeDeferred();
let stateB: any = null;
class D extends Component {
static template = xml`D<button t-on-click="increment"><t t-esc="state.val"/></button>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class C extends Component {
static template = xml`C<D/><p><t t-esc="props.value"/></p>`;
static components = { D };
setup() {
useLogLifecycle();
onWillUpdateProps(() => promC);
}
}
class B extends Component {
static template = xml`B<t t-if="state.hasChild"><C value="state.someValue + props.value"/></t>`;
static components = { C };
state = useState({ someValue: 3, hasChild: true });
setup() {
useLogLifecycle();
stateB = this.state;
}
}
class A extends Component {
static template = xml`A<B value="state.value"/>`;
static components = { B };
state = useState({ value: 33 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("ABCD<button>1</button><p>36</p>");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"D:setup",
"D:willStart",
"C:rendered",
"D:willRender",
"D:rendered",
"D:mounted",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// render in A, it updates B and C, but render is blocked in C
parent.state.value = 50;
await nextTick();
expect([
"A:willRender",
"B:willUpdateProps",
"A:rendered",
"B:willRender",
"C:willUpdateProps",
"B:rendered",
]).toBeLogged();
// update B => removes child C
stateB.hasChild = false;
// update D => render should be delayed, because AB is currently rendering
fixture.querySelector("button")!.click();
await nextTick();
expect([
"B:willRender",
"B:rendered",
"A:willPatch",
"B:willPatch",
"C:willUnmount",
"D:willUnmount",
"D:willDestroy",
"C:willDestroy",
"B:patched",
"A:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("AB");
});
test("renderings, destruction, patch, stuff, ... yet another variation", async () => {
const promB = makeDeferred();
class D extends Component {
static template = xml`D<p t-on-click="increment"><t t-esc="state.val"/></p>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
// almost the same as D
class C extends Component {
static template = xml`C<span t-on-click="increment"><t t-esc="state.val"/></span>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class B extends Component {
static template = xml`B<t t-if="props.value === 33"><C/></t>`;
static components = { C };
setup() {
useLogLifecycle();
onWillUpdateProps(() => promB);
}
}
class A extends Component {
static template = xml`A<B value="state.value"/><D/>`;
static components = { B, D };
state = useState({ value: 33 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("ABC<span>1</span>D<p>1</p>");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"D:setup",
"D:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"D:willRender",
"D:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"D:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// render in A, it updates B, will remove C, stopped in B
parent.state.value = 50;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
// update C => render should be delayed, because AB is currently rendering
fixture.querySelector("span")!.click();
await nextTick();
expect([]).toBeLogged();
// resolve prom B => render is done, component C is destroyed
promB.resolve();
await nextTick();
expect([
"B:willRender",
"B:rendered",
"A:willPatch",
"B:willPatch",
"C:willUnmount",
"C:willDestroy",
"B:patched",
"A:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("ABD<p>1</p>");
// update D => should just render completely independently
fixture.querySelector("p")!.click();
await nextTick();
expect(["D:willRender", "D:rendered", "D:willPatch", "D:patched"]).toBeLogged();
expect(fixture.innerHTML).toBe("ABD<p>2</p>");
});
// test.skip("components with shouldUpdate=false", async () => { // test.skip("components with shouldUpdate=false", async () => {
// const state = { p: 1, cc: 10 }; // const state = { p: 1, cc: 10 };
+123 -1
View File
@@ -1,4 +1,4 @@
import { Component, mount } from "../../src"; import { Component, mount, onWillDestroy } from "../../src";
import { import {
onError, onError,
onMounted, onMounted,
@@ -1197,4 +1197,126 @@ describe("can catch errors", () => {
expect(fixture.innerHTML).toBe("<div>Child 2</div>"); expect(fixture.innerHTML).toBe("<div>Child 2</div>");
expect(steps).toEqual(["Error Component"]); expect(steps).toEqual(["Error Component"]);
}); });
test("an error in onWillDestroy", async () => {
class Child extends Component {
static template = xml`<div>abc</div>`;
setup() {
useLogLifecycle();
onWillDestroy(() => {
throw new Error("boom");
});
}
}
class Parent extends Component {
static template = xml`
<t t-esc="state.value"/>
<t t-if="state.hasChild"><Child/></t>`;
static components = { Child };
state = useState({ value: 1, hasChild: true });
setup() {
useLogLifecycle();
onError(() => {
this.state.value++;
});
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1<div>abc</div>");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
parent.state.hasChild = false;
await nextTick();
await nextTick();
await nextTick();
await nextTick();
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("2");
});
test("an error in onWillDestroy, variation", async () => {
class Child extends Component {
static template = xml`<div>abc</div>`;
setup() {
useLogLifecycle();
onWillDestroy(() => {
throw new Error("boom");
});
}
}
class Parent extends Component {
static template = xml`
<t t-esc="state.value"/>
<t t-if="state.hasChild"><Child/></t>`;
static components = { Child };
state = useState({ value: 1, hasChild: false });
setup() {
useLogLifecycle();
onError(() => {
this.state.value++;
});
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
parent.state.hasChild = true;
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
]).toBeLogged();
parent.state.hasChild = false;
await nextTick();
expect([
"Child:willDestroy",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("2");
});
}); });
+66
View File
@@ -74,6 +74,72 @@ describe("slots", () => {
expect(fixture.innerHTML).toBe("<span>other text</span>"); expect(fixture.innerHTML).toBe("<span>other text</span>");
}); });
test("simple dynamic 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("simple named and empty slot", async () => {
class Child extends Component {
static template = xml`<span><t t-slot="default" /><t t-slot="myEmptySlot"/></span>`;
setup() {
expect(this.props.slots["myEmptySlot"]).toBeTruthy();
}
}
class Parent extends Component {
static template = xml`<Child>some text<t t-set-slot="myEmptySlot" /></Child>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span>some text</span>");
});
test("simple named and empty slot -- 2", async () => {
class Child extends Component {
static template = xml`<span><t t-slot="myEmptySlot">default empty</t></span>`;
setup() {
expect(this.props.slots["myEmptySlot"]).toBeTruthy();
expect(this.props.slots["myEmptySlot"].myProp).toBe("myProp text");
}
}
class Parent extends Component {
static template = xml`<Child><t t-set-slot="myEmptySlot" myProp="'myProp text'" /></Child>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span>default empty</span>");
});
test("default slot with slot scope: shorthand syntax", async () => { test("default slot with slot scope: shorthand syntax", async () => {
let child: any; let child: any;
class Child extends Component { class Child extends Component {