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",
"version": "2.0.0-beta-4",
"version": "2.0.0-beta-5",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js",
+1 -1
View File
@@ -67,7 +67,7 @@ export class App<
}
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) {
+6 -3
View File
@@ -1115,8 +1115,11 @@ export class CodeGenerator {
let slotStr: string[] = [];
for (let slotName in ast.slots) {
const slotAst = ast.slots[slotName];
const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on);
const params = [`__render: ${name}, __ctx: ${ctxStr}`];
const params = [];
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;
if (scope) {
params.push(`__scope: "${scope}"`);
@@ -1231,7 +1234,7 @@ export class CodeGenerator {
if (dynamic) {
let name = this.generateId("slot");
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 {
blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope})`;
}
+17 -19
View File
@@ -118,7 +118,7 @@ export interface ASTTCall {
}
interface SlotDefinition {
content: AST;
content: AST | null;
scope: string | null;
on: EventHandlers | null;
attrs: Attrs | null;
@@ -749,26 +749,24 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
slotNode.removeAttribute("t-set-slot");
slotNode.remove();
const slotAst = parseNode(slotNode, ctx);
if (slotAst) {
let on: SlotDefinition["on"] = null;
let attrs: Attrs | null = null;
let scope: string | null = null;
for (let attributeName of slotNode.getAttributeNames()) {
const value = slotNode.getAttribute(attributeName)!;
if (attributeName === "t-slot-scope") {
scope = value;
continue;
} else if (attributeName.startsWith("t-on-")) {
on = on || {};
on[attributeName.slice(5)] = value;
} else {
attrs = attrs || {};
attrs[attributeName] = value;
}
let on: SlotDefinition["on"] = null;
let attrs: Attrs | null = null;
let scope: string | null = null;
for (let attributeName of slotNode.getAttributeNames()) {
const value = slotNode.getAttribute(attributeName)!;
if (attributeName === "t-slot-scope") {
scope = value;
continue;
} else if (attributeName.startsWith("t-on-")) {
on = on || {};
on[attributeName.slice(5)] = value;
} else {
attrs = attrs || {};
attrs[attributeName] = value;
}
slots = slots || {};
slots[name] = { content: slotAst, on, attrs, scope };
}
slots = slots || {};
slots[name] = { content: slotAst, on, attrs, scope };
}
// default slot
+26 -32
View File
@@ -79,13 +79,8 @@ export function component<P extends object>(
let node: any = ctx.children[key];
let isDynamic = typeof name !== "string";
if (node) {
if (node.status < STATUS.MOUNTED) {
node.destroy();
node = undefined;
} else if (node.status === STATUS.DESTROYED) {
node = undefined;
}
if (node && node.status === STATUS.DESTROYED) {
node = undefined;
}
if (isDynamic && node && node.component.constructor !== name) {
node = undefined;
@@ -114,12 +109,11 @@ export function component<P extends object>(
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;
node.initiateRender(new Fiber(node, parentFiber));
}
parentFiber.root!.reachedChildren.add(node);
parentFiber.childrenMap[key] = node;
return node;
}
@@ -137,6 +131,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
bdom: BDom | null = null;
status: STATUS = STATUS.NEW;
forceNextRender: boolean = false;
parentKey: string | null;
renderFn: Function;
parent: ComponentNode | null;
@@ -153,10 +148,17 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
patched: 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;
this.app = app;
this.parent = parent || null;
this.parent = parent;
this.parentKey = parentKey;
this.level = parent ? parent.level + 1 : 0;
applyDefaultProps(props, C);
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) {
let current = this.fiber;
if (current && current.root!.locked) {
if (current && (current.root!.locked || (current as any).bdom === true)) {
await Promise.resolve();
// situation may have changed after the microtask tick
current = this.fiber;
@@ -215,6 +217,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
const fiber = makeRootFiber(this);
fiber.deep = deep;
this.fiber = fiber;
this.app.scheduler.addFiber(fiber);
await Promise.resolve();
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)) {
child._destroy();
}
for (let cb of this.willDestroy) {
cb.call(component);
if (this.willDestroy.length) {
try {
for (let cb of this.willDestroy) {
cb.call(component);
}
} catch (e) {
handleError({ error: e, node: this });
}
}
this.status = STATUS.DESTROYED;
}
@@ -326,6 +335,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
bdom.mount(parent, anchor);
this.status = STATUS.MOUNTED;
this.fiber!.appliedToDom = true;
this.children = this.fiber!.childrenMap;
this.fiber = null;
}
@@ -343,10 +353,8 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
}
_patch() {
const hasChildren = Object.keys(this.children).length > 0;
this.children = this.fiber!.childrenMap;
this.bdom!.patch(this!.fiber!.bdom!, hasChildren);
if (hasChildren) {
this.cleanOutdatedChildren();
}
this.fiber!.appliedToDom = true;
this.fiber = null;
}
@@ -359,20 +367,6 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
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
// ---------------------------------------------------------------------------
+5 -9
View File
@@ -5,7 +5,7 @@ import type { Fiber } from "./fibers";
export const fibersInError: WeakMap<Fiber, any> = 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) {
return false;
}
@@ -16,23 +16,19 @@ function _handleError(node: ComponentNode | null, error: any, isFirstRound = fal
const errorHandlers = nodeErrorHandlers.get(node);
if (errorHandlers) {
let stopped = false;
let handled = false;
// execute in the opposite order
for (let i = errorHandlers.length - 1; i >= 0; i--) {
try {
errorHandlers[i](error);
stopped = true;
handled = true;
break;
} catch (e) {
error = e;
}
}
if (stopped) {
if (isFirstRound && fiber && fiber.node.fiber) {
const root = fiber.root!;
root.setCounter(root.counter - 1);
}
if (handled) {
return true;
}
}
@@ -55,7 +51,7 @@ export function handleError(params: ErrorParams) {
fibersInError.set(fiber.root!, error);
const handled = _handleError(node, error, true);
const handled = _handleError(node, error);
if (!handled) {
console.warn(`[Owl] Unhandled error. Destroying the root component`);
try {
+21 -30
View File
@@ -8,10 +8,6 @@ export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
if (current) {
cancelFibers(current.children);
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);
}
@@ -20,12 +16,15 @@ export function makeRootFiber(node: ComponentNode): Fiber {
let current = node.fiber;
if (current) {
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.locked = false;
current.children = [];
current.childrenMap = {};
current.bdom = null;
if (current === root) {
root.reachedChildren = new WeakSet();
}
if (fibersInError.has(current)) {
fibersInError.delete(current);
fibersInError.delete(root);
@@ -49,7 +48,11 @@ export function makeRootFiber(node: ComponentNode): Fiber {
function cancelFibers(fibers: Fiber[]): number {
let result = 0;
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 has been rendered, this means that the component props have
// 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,
// we kindly request the component code to force a render, so it works as
// expected.
fiber.node.forceNextRender = true;
node.forceNextRender = true;
} else {
result++;
}
@@ -74,6 +77,7 @@ export class Fiber {
children: Fiber[] = [];
appliedToDom = false;
deep: boolean = false;
childrenMap: ComponentNode["children"] = {};
constructor(node: ComponentNode, parent: Fiber | null) {
this.node = node;
@@ -92,20 +96,16 @@ export class Fiber {
render() {
// if some parent has a fiber => register in followup
let prev = this.root!.node;
let scheduler = prev.app.scheduler;
let current = prev.parent;
while (current) {
if (current.fiber) {
let root = current.fiber.root!;
if (root.counter) {
root.delayedRenders.push(this);
return;
} else {
if (!root.reachedChildren.has(prev)) {
// is dead
this.node.app.scheduler.shouldClear = true;
return;
}
if (root.counter === 0 && prev.parentKey! in current.fiber.childrenMap) {
current = root.node;
} else {
scheduler.delayedRenders.push(this);
return;
}
}
prev = current;
@@ -121,11 +121,12 @@ export class Fiber {
const root = this.root;
if (root) {
try {
(this.bdom as any) = true;
this.bdom = node.renderFn();
root.setCounter(root.counter - 1);
} catch (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
locked: boolean = false;
delayedRenders: Fiber[] = [];
reachedChildren: WeakSet<ComponentNode> = new WeakSet();
complete() {
const node = this.node;
this.locked = true;
@@ -198,14 +196,6 @@ export class RootFiber extends Fiber {
setCounter(newValue: number) {
this.counter = newValue;
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();
}
}
@@ -230,6 +220,7 @@ export class MountFiber extends RootFiber {
let current: Fiber | undefined = this;
try {
const node = this.node;
node.children = this.childrenMap;
(node.app.constructor as any).validateTarget(this.target);
if (node.bdom) {
// 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();
requestAnimationFrame: Window["requestAnimationFrame"];
frame: number = 0;
shouldClear: boolean = false;
delayedRenders: Fiber[] = [];
constructor() {
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
@@ -28,16 +28,23 @@ export class Scheduler {
* Other tasks are left unchanged.
*/
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) {
this.frame = this.requestAnimationFrame(() => {
this.frame = 0;
this.tasks.forEach((fiber) => this.processFiber(fiber));
if (this.shouldClear) {
this.shouldClear = false;
for (let task of this.tasks) {
if (task.node.status === STATUS.DESTROYED) {
this.tasks.delete(task);
}
for (let task of this.tasks) {
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 () => {
expect(parse(`<MyComponent><t t-set-slot="name">foo</t></MyComponent>`)).toEqual({
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`] = `
"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`] = `
"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`] = `
"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`] = `
"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`] = `
"function anonymous(bdom, helpers
) {
@@ -626,7 +626,7 @@ exports[`slots dynamic t-slot call 2`] = `
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['toggle'], ctx];
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]);
}
}"
@@ -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`] = `
"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 = \\"\\") {
const b2 = component(\`Child\`, {parent: 'SlotDisplay'}, key + \`__1\`, node, ctx);
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]);
}
}"
+401 -8
View File
@@ -4,6 +4,7 @@ import {
mount,
onMounted,
onRendered,
onWillDestroy,
onWillStart,
onWillUnmount,
onWillUpdateProps,
@@ -114,8 +115,8 @@ test("destroying/recreating a subwidget with different props (if start is not ov
expect(n).toBe(2);
expect([
"W:willRender",
"Child:willDestroy",
"W:willRender",
"Child:setup",
"Child:willStart",
"W:rendered",
@@ -172,6 +173,11 @@ test("destroying/recreating a subcomponent, other scenario", async () => {
await nextTick();
expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willDestroy",
"Parent:willRender",
"Child:setup",
"Child:willStart",
@@ -244,8 +250,8 @@ test("creating two async components, scenario 1", async () => {
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"Parent:willRender",
"ChildA:willDestroy",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"ChildB:setup",
@@ -696,8 +702,8 @@ test("rendering component again in next microtick", async () => {
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Parent:willRender",
"Child:willDestroy",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
@@ -1725,6 +1731,7 @@ test("concurrent renderings scenario 10", async () => {
expect(fixture.innerHTML).toBe("<div><p></p></div>");
expect([
"ComponentA:willRender",
"ComponentC:willDestroy",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
]).toBeLogged();
@@ -1735,7 +1742,6 @@ test("concurrent renderings scenario 10", async () => {
expect(rendered).toBe(1);
expect([
"ComponentB:willRender",
"ComponentC:willDestroy",
"ComponentC:setup",
"ComponentC:willStart",
"ComponentB:rendered",
@@ -2275,11 +2281,11 @@ test("concurrent renderings scenario 16", async () => {
"D:setup",
"D:willStart",
"C:rendered",
"D:willDestroy",
"B:willRender",
"C:willUpdateProps",
"B:rendered",
"C:willRender",
"D:willDestroy",
"D:setup",
"D:willStart",
"C:rendered",
@@ -2990,13 +2996,13 @@ test("t-key on dom node having a component", async () => {
expect(fixture.innerHTML).toBe("<div>3</div>");
expect([
"Child (2):willDestroy",
"Child (3):setup",
"Child (3):willStart",
"Child (3):willRender",
"Child (3):rendered",
"Child (1):willUnmount",
"Child (1):willDestroy",
"Child (2):willDestroy",
"Child (3):mounted",
]).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([
"Child (2):willDestroy",
"Child (3):setup",
"Child (3):willStart",
"Child (3):willRender",
"Child (3):rendered",
"Child (1):willUnmount",
"Child (1):willDestroy",
"Child (2):willDestroy",
"Child (3):mounted",
]).toBeLogged();
});
@@ -3107,13 +3113,13 @@ test("t-foreach with dynamic async component", async () => {
expect(fixture.innerHTML).toBe("<div>3</div>");
expect([
"Child (2):willDestroy",
"Child (3):setup",
"Child (3):willStart",
"Child (3):willRender",
"Child (3):rendered",
"Child (1):willUnmount",
"Child (1):willDestroy",
"Child (2):willDestroy",
"Child (3):mounted",
]).toBeLogged();
});
@@ -3591,6 +3597,393 @@ test("delayed rendering, reusing fiber then component is destroyed and stuff",
]).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 () => {
// 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 {
onError,
onMounted,
@@ -1197,4 +1197,126 @@ describe("can catch errors", () => {
expect(fixture.innerHTML).toBe("<div>Child 2</div>");
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>");
});
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 () => {
let child: any;
class Child extends Component {