[IMP] app: mount app in "first-child" position

We reintroduce the possibility to mount the app in first position in
a target. The option "self" has been dropped since it is now possible
for a component to have several top level nodes.
This commit is contained in:
Mathieu Duckerts-Antoine
2021-10-22 17:30:20 +02:00
committed by Géry Debongnie
parent 06d207f60e
commit 7d7568d254
7 changed files with 92 additions and 76 deletions
+3 -2
View File
@@ -1,5 +1,6 @@
import { Component } from "./component/component"; import { Component } from "./component/component";
import { ComponentNode } from "./component/component_node"; import { ComponentNode } from "./component/component_node";
import { MountOptions } from "./component/fibers";
import { Scheduler } from "./component/scheduler"; import { Scheduler } from "./component/scheduler";
import { TemplateSet } from "./qweb/template_helpers"; import { TemplateSet } from "./qweb/template_helpers";
@@ -47,7 +48,7 @@ export class App<T extends typeof Component = any> extends TemplateSet {
} }
} }
mount(target: HTMLElement): Promise<InstanceType<T>> { mount(target: HTMLElement, options?: MountOptions): Promise<InstanceType<T>> {
if (!(target instanceof HTMLElement)) { if (!(target instanceof HTMLElement)) {
throw new Error("Cannot mount component: the target is not a valid DOM element"); throw new Error("Cannot mount component: the target is not a valid DOM element");
} }
@@ -56,7 +57,7 @@ export class App<T extends typeof Component = any> extends TemplateSet {
} }
const node = new ComponentNode(this.Root, this.props, this); const node = new ComponentNode(this.Root, this.props, this);
this.root = node; this.root = node;
return node.mountComponent(target); return node.mountComponent(target, options);
} }
destroy() { destroy() {
+2 -2
View File
@@ -23,8 +23,8 @@ export interface VNode<T = any> {
export type BDom = VNode<any>; export type BDom = VNode<any>;
export function mount(vnode: VNode, fixture: HTMLElement) { export function mount(vnode: VNode, fixture: HTMLElement, afterNode: Node | null = null) {
vnode.mount(fixture, null); vnode.mount(fixture, afterNode);
} }
export function patch(vnode1: VNode, vnode2: VNode, withBeforeRemove: boolean = false) { export function patch(vnode1: VNode, vnode2: VNode, withBeforeRemove: boolean = false) {
+3 -2
View File
@@ -6,6 +6,7 @@ import {
makeChildFiber, makeChildFiber,
makeRootFiber, makeRootFiber,
MountFiber, MountFiber,
MountOptions,
RootFiber, RootFiber,
__internal__destroyed, __internal__destroyed,
} from "./fibers"; } from "./fibers";
@@ -92,8 +93,8 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
this.component.setup(); this.component.setup();
} }
mountComponent(target: any): Promise<InstanceType<T>> { mountComponent(target: any, options?: MountOptions): Promise<InstanceType<T>> {
const fiber = new MountFiber(this, target); const fiber = new MountFiber(this, target, options);
this.app.scheduler.addFiber(fiber); this.app.scheduler.addFiber(fiber);
this.initiateRender(fiber); this.initiateRender(fiber);
return fiber.promise.then(() => this.component); return fiber.promise.then(() => this.component);
+15 -2
View File
@@ -158,17 +158,30 @@ export class RootFiber extends Fiber {
export let __internal__destroyed: ComponentNode[] = []; export let __internal__destroyed: ComponentNode[] = [];
type Position = "first-child" | "last-child";
export interface MountOptions {
position?: Position;
}
export class MountFiber extends RootFiber { export class MountFiber extends RootFiber {
target: HTMLElement; target: HTMLElement;
position: Position;
constructor(node: ComponentNode, target: HTMLElement) { constructor(node: ComponentNode, target: HTMLElement, options: MountOptions = {}) {
super(node); super(node);
this.target = target; this.target = target;
this.position = options.position || "last-child";
} }
complete() { complete() {
const node = this.node; const node = this.node;
node.bdom = this.bdom; node.bdom = this.bdom;
mount(node.bdom!, this.target); if (this.position === "last-child" || this.target.childNodes.length === 0) {
mount(node.bdom!, this.target);
} else {
const firstChild = this.target.childNodes[0];
mount(node.bdom!, this.target, firstChild);
}
node.status = STATUS.MOUNTED; node.status = STATUS.MOUNTED;
this.appliedToDom = true; this.appliedToDom = true;
let current; let current;
+1 -2
View File
@@ -6,7 +6,7 @@ import { Component } from "./component";
* Note that this method does modify in place the props * Note that this method does modify in place the props
*/ */
export function applyDefaultProps(props: { [key: string]: any }, ComponentClass: typeof Component) { export function applyDefaultProps(props: { [key: string]: any }, ComponentClass: typeof Component) {
const defaultProps = (ComponentClass as any).defaultProps const defaultProps = (ComponentClass as any).defaultProps;
if (defaultProps) { if (defaultProps) {
for (let propName in defaultProps) { for (let propName in defaultProps) {
if (props![propName] === undefined) { if (props![propName] === undefined) {
@@ -16,7 +16,6 @@ export function applyDefaultProps(props: { [key: string]: any }, ComponentClass:
} }
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Prop validation helper // Prop validation helper
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -1133,6 +1133,48 @@ exports[`basics zero or one child components 2`] = `
}" }"
`; `;
exports[`mount targets can mount a component (with default position='last-child') 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div>app</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`mount targets can mount a component (with position='first-child') 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div>app</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`mount targets default mount option is 'last-child' 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div>app</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`support svg components add proper namespace to svg 1`] = ` exports[`support svg components add proper namespace to svg 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
+26 -66
View File
@@ -902,78 +902,38 @@ describe("dynamic t-props", () => {
}); });
}); });
describe.skip("mount targets", () => { describe("mount targets", () => {
test("can attach a component to an existing node (if same tagname)", async () => {
// class App extends Component {
// static template = xml`<div t-att-class="state.customClass">app<p>another tag</p></div>`;
// state = useState({ customClass: "custom" });
// }
// const div = document.createElement("div");
// div.classList.add("arbitrary");
// div.innerHTML = `<p>pre-existing</p>`;
// fixture.appendChild(div);
// const app = await mount(App, { target: div, position: "self" });
// expect(fixture.innerHTML).toBe(
// `<div class="arbitrary custom"><p>pre-existing</p>app<p>another tag</p></div>`
// );
// expect(div).toBe(app.el);
// app.state.customClass = "custom2";
// await nextTick();
// expect(fixture.innerHTML).toBe(
// `<div class="arbitrary custom2"><p>pre-existing</p>app<p>another tag</p></div>`
// );
// expect(div).toBe(app.el);
// app.unmount();
// // This assert is a best guess
// // The use case it covers was not really thought through
// // and may change in the future
// expect(fixture.innerHTML).toBe("");
});
test("cannot attach a component to an existing node (if not same tagname)", async () => {
// class App extends Component {
// static template = xml`<span>app</span>`;
// }
// const div = document.createElement("div");
// fixture.appendChild(div);
// let error;
// try {
// await mount(App, { target: div, position: "self" });
// } catch (e) {
// error = e;
// }
// expect(error).toBeDefined();
// expect(error.message).toBe("Cannot attach 'App' to target node (not same tag name)");
});
test("can mount a component (with position='first-child')", async () => { test("can mount a component (with position='first-child')", async () => {
// class App extends Component { class Root extends Component {
// static template = xml`<div>app</div>`; static template = xml`<div>app</div>`;
// } }
// const span = document.createElement("span"); const span = document.createElement("span");
// fixture.appendChild(span); fixture.appendChild(span);
// await mount(App, { target: fixture, position: "first-child" }); const app = new App(Root);
// expect(fixture.innerHTML).toBe("<div>app</div><span></span>"); await app.mount(fixture, { position: "first-child" });
expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
}); });
test("can mount a component (with position='last-child')", async () => { test("can mount a component (with default position='last-child')", async () => {
// class App extends Component { class Root extends Component {
// static template = xml`<div>app</div>`; static template = xml`<div>app</div>`;
// } }
// const span = document.createElement("span"); const span = document.createElement("span");
// fixture.appendChild(span); fixture.appendChild(span);
// await mount(App, { target: fixture, position: "last-child" }); const app = new App(Root);
// expect(fixture.innerHTML).toBe("<span></span><div>app</div>"); await app.mount(fixture, { position: "last-child" });
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
}); });
test("default mount option is 'last-child'", async () => { test("default mount option is 'last-child'", async () => {
// class App extends Component { class Root extends Component {
// static template = xml`<div>app</div>`; static template = xml`<div>app</div>`;
// } }
// const span = document.createElement("span"); const span = document.createElement("span");
// fixture.appendChild(span); fixture.appendChild(span);
// await mount(App, { target: fixture }); const app = new App(Root);
// expect(fixture.innerHTML).toBe("<span></span><div>app</div>"); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
}); });
}); });