diff --git a/src/runtime/app.ts b/src/runtime/app.ts
index 189ed5b3..9431e6e7 100644
--- a/src/runtime/app.ts
+++ b/src/runtime/app.ts
@@ -1,6 +1,6 @@
import { version } from "../version";
import { Component, ComponentConstructor, Props } from "./component";
-import { ComponentNode } from "./component_node";
+import { ComponentNode, saveCurrent } from "./component_node";
import { nodeErrorHandlers, handleError } from "./error_handling";
import { OwlError } from "../common/owl_error";
import { Fiber, RootFiber, MountOptions } from "./fibers";
@@ -52,7 +52,7 @@ declare global {
}
}
-interface Root
{
+interface Root
{
node: ComponentNode
;
mount(target: HTMLElement | ShadowRoot, options?: MountOptions): Promise>;
destroy(): void;
@@ -120,7 +120,10 @@ export class App<
if (config.env) {
this.env = config.env as any;
}
+
+ const restore = saveCurrent();
const node = this.makeNode(Root, props);
+ restore();
if (config.env) {
this.env = env;
}
diff --git a/src/runtime/component_node.ts b/src/runtime/component_node.ts
index bffc0fc5..31d4d933 100644
--- a/src/runtime/component_node.ts
+++ b/src/runtime/component_node.ts
@@ -10,6 +10,13 @@ import { batched, Callback } from "./utils";
let currentNode: ComponentNode | null = null;
+export function saveCurrent() {
+ let n = currentNode;
+ return () => {
+ currentNode = n;
+ };
+}
+
export function getCurrent(): ComponentNode {
if (!currentNode) {
throw new OwlError("No active component (a hook function should only be called in 'setup')");
diff --git a/src/runtime/scheduler.ts b/src/runtime/scheduler.ts
index d5fed0a4..72079dfe 100644
--- a/src/runtime/scheduler.ts
+++ b/src/runtime/scheduler.ts
@@ -16,6 +16,7 @@ export class Scheduler {
frame: number = 0;
delayedRenders: Fiber[] = [];
cancelledNodes: Set = new Set();
+ processing = false;
constructor() {
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
@@ -53,6 +54,10 @@ export class Scheduler {
}
processTasks() {
+ if (this.processing) {
+ return;
+ }
+ this.processing = true;
this.frame = 0;
for (let node of this.cancelledNodes) {
node._destroy();
@@ -66,6 +71,7 @@ export class Scheduler {
this.tasks.delete(task);
}
}
+ this.processing = false;
}
processFiber(fiber: RootFiber) {
diff --git a/tests/app/__snapshots__/app.test.ts.snap b/tests/app/__snapshots__/app.test.ts.snap
index dcee7a5e..21d48b9c 100644
--- a/tests/app/__snapshots__/app.test.ts.snap
+++ b/tests/app/__snapshots__/app.test.ts.snap
@@ -43,6 +43,33 @@ exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately
}"
`;
+exports[`app can call processTask twice in a row without crashing 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+ const comp1 = app.createComponent(\`Child\`, true, false, false, []);
+
+ return function template(ctx, node, key = \\"\\") {
+ const b2 = text(\`parent\`);
+ const b3 = comp1({}, key + \`__1\`, node, this, null);
+ return multi([b2, b3]);
+ }
+}"
+`;
+
+exports[`app can call processTask twice in a row without crashing 2`] = `
+"function anonymous(app, bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+
+ let block1 = createBlock(\`\`);
+
+ return function template(ctx, node, key = \\"\\") {
+ return block1();
+ }
+}"
+`;
+
exports[`app can configure an app with props 1`] = `
"function anonymous(app, bdom, helpers
) {
diff --git a/tests/app/__snapshots__/sub_root.test.ts.snap b/tests/app/__snapshots__/sub_root.test.ts.snap
index 7af7806b..efc4d563 100644
--- a/tests/app/__snapshots__/sub_root.test.ts.snap
+++ b/tests/app/__snapshots__/sub_root.test.ts.snap
@@ -1,5 +1,62 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`destroy a subroot while another component is mounted in main app 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+ const comp1 = app.createComponent(\`ChildB\`, true, false, false, []);
+ const comp2 = app.createComponent(\`ChildA\`, true, false, false, []);
+
+ return function template(ctx, node, key = \\"\\") {
+ let b2, b3;
+ if (ctx['state'].flag) {
+ b2 = comp1({}, key + \`__1\`, node, this, null);
+ } else {
+ b3 = comp2({}, key + \`__2\`, node, this, null);
+ }
+ return multi([b2, b3]);
+ }
+}"
+`;
+
+exports[`destroy a subroot while another component is mounted in main app 2`] = `
+"function anonymous(app, bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+
+ let block3 = createBlock(\`\`);
+
+ return function template(ctx, node, key = \\"\\") {
+ const b2 = text(\`a\`);
+ let ref1 = (el) => this.__owl__.setRef((\`elem\`), el);
+ const b3 = block3([ref1]);
+ return multi([b2, b3]);
+ }
+}"
+`;
+
+exports[`destroy a subroot while another component is mounted in main app 3`] = `
+"function anonymous(app, bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+
+ return function template(ctx, node, key = \\"\\") {
+ return text(\`c\`);
+ }
+}"
+`;
+
+exports[`destroy a subroot while another component is mounted in main app 4`] = `
+"function anonymous(app, bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+
+ return function template(ctx, node, key = \\"\\") {
+ return text(\`b\`);
+ }
+}"
+`;
+
exports[`subroot by default, env is the same in sub root 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -26,6 +83,28 @@ exports[`subroot by default, env is the same in sub root 2`] = `
}"
`;
+exports[`subroot can create a root in a setup function, then use a hook 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+
+ return function template(ctx, node, key = \\"\\") {
+ return text(\`a\`);
+ }
+}"
+`;
+
+exports[`subroot can create a root in a setup function, then use a hook 2`] = `
+"function anonymous(app, bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+
+ return function template(ctx, node, key = \\"\\") {
+ return text(\`c\`);
+ }
+}"
+`;
+
exports[`subroot can mount subroot 1`] = `
"function anonymous(app, bdom, helpers
) {
diff --git a/tests/app/app.test.ts b/tests/app/app.test.ts
index 6eab991b..0635eef9 100644
--- a/tests/app/app.test.ts
+++ b/tests/app/app.test.ts
@@ -1,4 +1,4 @@
-import { App, Component, mount, onWillStart, useState, xml } from "../../src";
+import { App, Component, mount, onWillPatch, onWillStart, useState, xml } from "../../src";
import { status } from "../../src/runtime/status";
import {
makeTestFixture,
@@ -184,4 +184,21 @@ describe("app", () => {
expect(Object.keys(app.templates)).toEqual(["hello"]);
expect(Object.keys(app.rawTemplates)).toEqual(["hello", "world"]);
});
+
+ test("can call processTask twice in a row without crashing", async () => {
+ class Child extends Component {
+ static template = xml`
`;
+ setup() {
+ onWillPatch(() => app.scheduler.processTasks());
+ }
+ }
+ class SomeComponent extends Component {
+ static template = xml`parent
`;
+ static components = { Child };
+ }
+
+ const app = new App(SomeComponent);
+ await app.mount(fixture);
+ expect(fixture.innerHTML).toBe("parent
");
+ });
});
diff --git a/tests/app/sub_root.test.ts b/tests/app/sub_root.test.ts
index 2f2146dc..0cb75810 100644
--- a/tests/app/sub_root.test.ts
+++ b/tests/app/sub_root.test.ts
@@ -1,6 +1,6 @@
-import { App, Component, xml } from "../../src";
+import { App, Component, onMounted, onWillDestroy, useRef, useState, xml } from "../../src";
import { status } from "../../src/runtime/status";
-import { makeTestFixture, snapshotEverything } from "../helpers";
+import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
let fixture: HTMLElement;
@@ -112,4 +112,65 @@ describe("subroot", () => {
expect(status(comp)).not.toBe("destroyed");
expect(status(subcomp)).toBe("destroyed");
});
+
+ test("can create a root in a setup function, then use a hook", async () => {
+ class C extends Component {
+ static template = xml`c`;
+ }
+
+ class A extends Component {
+ static template = xml`a`;
+ state: any;
+ setup() {
+ app.createRoot(C);
+ this.state = useState({ value: 1 });
+ }
+ }
+
+ const app = new App(A);
+ await app.mount(fixture);
+ expect(fixture.innerHTML).toBe("a");
+ });
+});
+
+test("destroy a subroot while another component is mounted in main app", async () => {
+ class C extends Component {
+ static template = xml`c`;
+ }
+
+ class ChildA extends Component {
+ static template = xml`a
`;
+ ref: any;
+ setup() {
+ this.ref = useRef("elem");
+ let root = app.createRoot(C);
+ onMounted(() => {
+ root.mount(this.ref.el);
+ });
+ onWillDestroy(() => {
+ root.destroy();
+ });
+ }
+ }
+ class ChildB extends Component {
+ static template = xml`b`;
+ }
+
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+ `;
+ static components = { ChildA, ChildB };
+ state = useState({ flag: false });
+ }
+
+ const app = new App(SomeComponent);
+ const comp = await app.mount(fixture);
+ expect(fixture.innerHTML).toBe("a
");
+ await nextTick();
+ expect(fixture.innerHTML).toBe("a
c
");
+ comp.state.flag = true;
+ await nextTick();
+ expect(fixture.innerHTML).toBe("b");
});