Skip to content

Commit

Permalink
[IMP] owl: add basic support for sub roots
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ged-odoo committed Sep 26, 2024
1 parent 2a22328 commit 781a7be
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 0 deletions.
19 changes: 19 additions & 0 deletions src/runtime/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class App<
props: P;
env: E;
scheduler = new Scheduler();
subRoots: Set<ComponentNode> = new Set();
root: ComponentNode<P, E> | null = null;
warnIfNoStaticProps: boolean;

Expand Down Expand Up @@ -101,6 +102,21 @@ export class App<
return prom;
}

mountSubRoot<SP, ST extends abstract new (...args: any) => any = any>(
SubRoot: ComponentConstructor<SP, E>,
subProps: SP,
target: HTMLElement | ShadowRoot
): Promise<Component<SP, E> & InstanceType<ST>> {
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);
}
Expand Down Expand Up @@ -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();
}
Expand Down
79 changes: 79 additions & 0 deletions tests/app/__snapshots__/sub_root.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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(\`<div>main app</div>\`);
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(\`<div>sub root</div>\`);
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(\`<div>main app</div>\`);
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(\`<div>sub root</div>\`);
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(\`<div>main app</div>\`);
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(\`<div>sub root</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
72 changes: 72 additions & 0 deletions tests/app/sub_root.test.ts
Original file line number Diff line number Diff line change
@@ -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`<div>main app</div>`;
}

class SubComponent extends Component {
static template = xml`<div>sub root</div>`;
}

describe("subroot", () => {
test("can mount subroot", async () => {
const app = new App(SomeComponent);
const comp = await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>main app</div>");
const subcomp = await app.mountSubRoot(SubComponent, null, fixture);
expect(fixture.innerHTML).toBe("<div>main app</div><div>sub root</div>");

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("<div>main app</div>");
const subcomp = await app.mountSubRoot(SubComponent, null, fixture.querySelector("div")!);
expect(fixture.innerHTML).toBe("<div>main app<div>sub root</div></div>");

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);
});
});

0 comments on commit 781a7be

Please sign in to comment.