[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
This commit is contained in:
Géry Debongnie
2024-09-26 15:23:15 +02:00
committed by Sam Degueldre
parent 2a223288d4
commit eb2b32ab60
6 changed files with 381 additions and 9 deletions
@@ -0,0 +1,131 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`subroot by default, 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 by default, 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();
}
}"
`;
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 can be specified for sub roots 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 can be specified for sub roots 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 subcomponents can be destroyed, and it properly cleanup the subroots 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 subcomponents can be destroyed, and it properly cleanup the subroots 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();
}
}"
`;
+115
View File
@@ -0,0 +1,115 @@
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 subRoot = app.createRoot(SubComponent);
const subcomp = await subRoot.mount(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 subRoot = app.createRoot(SubComponent);
const subcomp = await subRoot.mount(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("by default, 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);
const subRoot = app.createRoot(Sub);
await subRoot.mount(fixture);
expect(env).toBeDefined();
expect(subenv).toBeDefined();
expect(env).toBe(subenv);
});
test("env can be specified for sub roots", async () => {
const env1 = { env1: true };
const env2 = {};
let someComponentEnv: any, subComponentEnv: any;
class SC extends SomeComponent {
setup() {
someComponentEnv = this.env;
}
}
class Sub extends SubComponent {
setup() {
subComponentEnv = this.env;
}
}
const app = new App(SC, { env: env1 });
await app.mount(fixture);
const subRoot = app.createRoot(Sub, { env: env2 });
await subRoot.mount(fixture);
// because env is different in app => it is given a sub object, frozen and all
// not sure it is a good idea, but it's the way owl 2 works. maybe we should
// avoid doing anything with the main env and let user code do it if they
// want. in that case, we can change the test here to assert that they are equal
expect(someComponentEnv).not.toBe(env1);
expect(someComponentEnv!.env1).toBe(true);
expect(subComponentEnv).toBe(env2);
});
test("subcomponents can be destroyed, and it properly cleanup the subroots", async () => {
const app = new App(SomeComponent);
const comp = await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>main app</div>");
const root = app.createRoot(SubComponent);
const subcomp = await root.mount(fixture.querySelector("div")!);
expect(fixture.innerHTML).toBe("<div>main app<div>sub root</div></div>");
root.destroy();
expect(fixture.innerHTML).toBe("<div>main app</div>");
expect(status(comp)).not.toBe("destroyed");
expect(status(subcomp)).toBe("destroyed");
});
});
@@ -97,6 +97,19 @@ exports[`basics a component cannot be mounted in a detached node (even if node i
}"
`;
exports[`basics a component cannot be mounted in a detached node 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`basics a component inside a component 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -261,6 +274,19 @@ exports[`basics can mount a simple component with props 1`] = `
}"
`;
exports[`basics cannot mount on a documentFragment 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>content</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`basics child can be updated 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -1002,6 +1028,19 @@ exports[`basics three level of components with collapsing root nodes 3`] = `
}"
`;
exports[`basics throws if mounting on target=null 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<span>simple vnode</span>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`basics two child components 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -924,6 +924,20 @@ exports[`props validation props: list of strings 1`] = `
}"
`;
exports[`props validation validate props for root component 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['message'];
return block1([txt1]);
}
}"
`;
exports[`props validation validate simple types 1`] = `
"function anonymous(app, bdom, helpers
) {