[REF] app: reorganize startup code

The goal of this refactoring is to delay the instantiation of the
component node until we are ready to mount the component. This is useful
so we know if a component will be attached or mounted (which means that
it needs to have a template, or not)
This commit is contained in:
Géry Debongnie
2024-10-29 13:47:52 +01:00
parent b8d09e523d
commit b4f84513a6
3 changed files with 40 additions and 45 deletions
+39 -33
View File
@@ -53,7 +53,7 @@ declare global {
} }
interface Root<P extends Props, E> { interface Root<P extends Props, E> {
node: ComponentNode<P, E>; node: ComponentNode<P, E> | null;
mount(target: HTMLElement | ShadowRoot, options?: MountOptions): Promise<Component<P, E>>; mount(target: HTMLElement | ShadowRoot, options?: MountOptions): Promise<Component<P, E>>;
destroy(): void; destroy(): void;
} }
@@ -74,8 +74,8 @@ export class App<
props: P; props: P;
env: E; env: E;
scheduler = new Scheduler(); scheduler = new Scheduler();
subRoots: Set<ComponentNode> = new Set(); subRoots: Set<Root<any, any>> = new Set();
root: ComponentNode<P, E> | null = null; root: Root<P, E> | null = null;
warnIfNoStaticProps: boolean; warnIfNoStaticProps: boolean;
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) { constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
@@ -101,10 +101,8 @@ export class App<
target: HTMLElement | ShadowRoot, target: HTMLElement | ShadowRoot,
options?: MountOptions options?: MountOptions
): Promise<Component<P, E> & InstanceType<T>> { ): Promise<Component<P, E> & InstanceType<T>> {
const root = this.createRoot(this.Root, { props: this.props }); this.root = this.createRoot(this.Root, { props: this.props });
this.root = root.node; return this.root.mount(target, options) as any;
this.subRoots.delete(root.node);
return root.mount(target, options) as any;
} }
createRoot<Props extends object, SubEnv = any>( createRoot<Props extends object, SubEnv = any>(
@@ -112,25 +110,24 @@ export class App<
config: RootConfig<Props, SubEnv> = {} config: RootConfig<Props, SubEnv> = {}
): Root<Props, SubEnv> { ): Root<Props, SubEnv> {
const props = config.props || ({} as Props); const props = config.props || ({} as Props);
// hack to make sure the sub root get the sub env if necessary. for owl 3,
// would be nice to rethink the initialization process to make sure that
// we can create a ComponentNode and give it explicitely the env, instead
// of looking it up in the app
const env = this.env; const env = this.env;
if (config.env) { const root: Root<Props, SubEnv> = {
this.env = config.env as any; node: null,
}
const restore = saveCurrent();
const node = this.makeNode(Root, props);
restore();
if (config.env) {
this.env = env;
}
this.subRoots.add(node);
return {
node,
mount: (target: HTMLElement | ShadowRoot, options?: MountOptions) => { mount: (target: HTMLElement | ShadowRoot, options?: MountOptions) => {
// hack to make sure the sub root get the sub env if necessary. for owl 3,
// would be nice to rethink the initialization process to make sure that
// we can create a ComponentNode and give it explicitely the env, instead
// of looking it up in the app
if (config.env) {
this.env = config.env as any;
}
const restore = saveCurrent();
const node = this.makeNode(Root, props);
root.node = node;
restore();
if (config.env) {
this.env = env;
}
App.validateTarget(target); App.validateTarget(target);
if (this.dev) { if (this.dev) {
validateProps(Root, props, { __owl__: { app: this } }); validateProps(Root, props, { __owl__: { app: this } });
@@ -139,11 +136,16 @@ export class App<
return prom; return prom;
}, },
destroy: () => { destroy: () => {
this.subRoots.delete(node); this.subRoots.delete(root);
node.destroy(); if (root.node) {
this.scheduler.processTasks(); root.node?.destroy();
this.scheduler.processTasks();
}
}, },
}; };
this.subRoots.add(root);
return root;
} }
makeNode(Component: ComponentConstructor, props: any): ComponentNode { makeNode(Component: ComponentConstructor, props: any): ComponentNode {
@@ -178,13 +180,17 @@ export class App<
} }
destroy() { destroy() {
if (this.root) { const roots = [...this.subRoots].reverse();
for (let subroot of this.subRoots) { for (let root of roots) {
subroot.destroy(); root.destroy();
}
this.root.destroy();
this.scheduler.processTasks();
} }
// if (this.root) {
// for (let subroot of this.subRoots) {
// subroot.destroy();
// }
// this.root.destroy();
this.scheduler.processTasks();
// }
apps.delete(this); apps.delete(this);
} }
@@ -94,17 +94,6 @@ exports[`subroot can create a root in a setup function, then use a hook 1`] = `
}" }"
`; `;
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`] = ` exports[`subroot can mount subroot 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
+1 -1
View File
@@ -702,7 +702,7 @@ describe("props validation", () => {
const app = new App(Parent, { test: true }); const app = new App(Parent, { test: true });
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("12"); expect(fixture.innerHTML).toBe("12");
expect(app.root!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]); expect(app.root!.node!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]);
}); });
test("props are validated whenever component is updated", async () => { test("props are validated whenever component is updated", async () => {