From 781a7beba9f93c77dff810e5700335a575dfa8fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 26 Sep 2024 15:23:15 +0200 Subject: [PATCH] [IMP] owl: add basic support for sub roots In this commit, we extend the owl App class to support multiple sub roots. This is useful for situations where we want to mount sub components in non-managed DOM. This is exactly what the Knowledge app is doing, with mounting views in an html editor. Currently, this requires some difficult and fragile hacks, and still, the result is that it is very easy to mix components from the main App and a SubApp. But Knowledge does not actually care about creating a sub app. It only needs the possibility to mount sub components in dynamic places. closes #1640 --- src/runtime/app.ts | 19 +++++ tests/app/__snapshots__/sub_root.test.ts.snap | 79 +++++++++++++++++++ tests/app/sub_root.test.ts | 72 +++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 tests/app/__snapshots__/sub_root.test.ts.snap create mode 100644 tests/app/sub_root.test.ts diff --git a/src/runtime/app.ts b/src/runtime/app.ts index ace8fd1c3..901d83930 100644 --- a/src/runtime/app.ts +++ b/src/runtime/app.ts @@ -65,6 +65,7 @@ export class App< props: P; env: E; scheduler = new Scheduler(); + subRoots: Set = new Set(); root: ComponentNode | null = null; warnIfNoStaticProps: boolean; @@ -101,6 +102,21 @@ export class App< return prom; } + mountSubRoot any = any>( + SubRoot: ComponentConstructor, + subProps: SP, + target: HTMLElement | ShadowRoot + ): Promise & InstanceType> { + App.validateTarget(target); + if (this.dev) { + validateProps(SubRoot, subProps, { __owl__: { app: this } }); + } + const node = this.makeNode(SubRoot, subProps); + const prom = this.mountNode(node, target); + this.subRoots.add(node); + return prom; + } + makeNode(Component: ComponentConstructor, props: any): ComponentNode { return new ComponentNode(Component, props, this, null, null); } @@ -134,6 +150,9 @@ export class App< destroy() { if (this.root) { + for (let subroot of this.subRoots) { + subroot.destroy(); + } this.root.destroy(); this.scheduler.processTasks(); } diff --git a/tests/app/__snapshots__/sub_root.test.ts.snap b/tests/app/__snapshots__/sub_root.test.ts.snap new file mode 100644 index 000000000..6b9fe0630 --- /dev/null +++ b/tests/app/__snapshots__/sub_root.test.ts.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`subroot can mount subroot 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`
main app
\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; + +exports[`subroot can mount subroot 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`
sub root
\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; + +exports[`subroot can mount subroot inside own dom 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`
main app
\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; + +exports[`subroot can mount subroot inside own dom 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`
sub root
\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; + +exports[`subroot env is the same in sub root 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`
main app
\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; + +exports[`subroot env is the same in sub root 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`
sub root
\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; diff --git a/tests/app/sub_root.test.ts b/tests/app/sub_root.test.ts new file mode 100644 index 000000000..00e08d356 --- /dev/null +++ b/tests/app/sub_root.test.ts @@ -0,0 +1,72 @@ +import { App, Component, xml } from "../../src"; +import { status } from "../../src/runtime/status"; +import { + makeTestFixture, + snapshotEverything, +} from "../helpers"; + +let fixture: HTMLElement; + +snapshotEverything(); + +beforeEach(() => { + fixture = makeTestFixture(); +}); + +class SomeComponent extends Component { + static template = xml`
main app
`; +} + +class SubComponent extends Component { + static template = xml`
sub root
`; +} + +describe("subroot", () => { + test("can mount subroot", async () => { + const app = new App(SomeComponent); + const comp = await app.mount(fixture); + expect(fixture.innerHTML).toBe("
main app
"); + const subcomp = await app.mountSubRoot(SubComponent, null, fixture); + expect(fixture.innerHTML).toBe("
main app
sub root
"); + + app.destroy(); + expect(fixture.innerHTML).toBe(""); + expect(status(comp)).toBe("destroyed"); + expect(status(subcomp)).toBe("destroyed"); + }); + + test("can mount subroot inside own dom", async () => { + const app = new App(SomeComponent); + const comp = await app.mount(fixture); + expect(fixture.innerHTML).toBe("
main app
"); + const subcomp = await app.mountSubRoot(SubComponent, null, fixture.querySelector("div")!); + expect(fixture.innerHTML).toBe("
main app
sub root
"); + + app.destroy(); + expect(fixture.innerHTML).toBe(""); + expect(status(comp)).toBe("destroyed"); + expect(status(subcomp)).toBe("destroyed"); + }); + + test("env is the same in sub root", async () => { + let env, subenv; + class SC extends SomeComponent { + setup() { + env = this.env; + } + } + class Sub extends SubComponent { + setup() { + subenv = this.env; + } + } + + const app = new App(SC); + await app.mount(fixture); + await app.mountSubRoot(Sub, null, fixture); + + expect(env).toBeDefined(); + expect(subenv).toBeDefined(); + expect(env).toBe(subenv); + }); +});