From 7d7568d254f4f9937e54f71a50df20fb7a0e2fd8 Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Fri, 22 Oct 2021 17:30:20 +0200 Subject: [PATCH] [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. --- src/app.ts | 5 +- src/blockdom/index.ts | 4 +- src/component/component_node.ts | 5 +- src/component/fibers.ts | 17 +++- src/component/props_validation.ts | 3 +- .../__snapshots__/basics.test.ts.snap | 42 +++++++++ tests/components/basics.test.ts | 92 ++++++------------- 7 files changed, 92 insertions(+), 76 deletions(-) diff --git a/src/app.ts b/src/app.ts index 569bf7fe..b1cbae41 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,6 @@ import { Component } from "./component/component"; import { ComponentNode } from "./component/component_node"; +import { MountOptions } from "./component/fibers"; import { Scheduler } from "./component/scheduler"; import { TemplateSet } from "./qweb/template_helpers"; @@ -47,7 +48,7 @@ export class App extends TemplateSet { } } - mount(target: HTMLElement): Promise> { + mount(target: HTMLElement, options?: MountOptions): Promise> { if (!(target instanceof HTMLElement)) { throw new Error("Cannot mount component: the target is not a valid DOM element"); } @@ -56,7 +57,7 @@ export class App extends TemplateSet { } const node = new ComponentNode(this.Root, this.props, this); this.root = node; - return node.mountComponent(target); + return node.mountComponent(target, options); } destroy() { diff --git a/src/blockdom/index.ts b/src/blockdom/index.ts index 1ac9b61e..aa459e4d 100644 --- a/src/blockdom/index.ts +++ b/src/blockdom/index.ts @@ -23,8 +23,8 @@ export interface VNode { export type BDom = VNode; -export function mount(vnode: VNode, fixture: HTMLElement) { - vnode.mount(fixture, null); +export function mount(vnode: VNode, fixture: HTMLElement, afterNode: Node | null = null) { + vnode.mount(fixture, afterNode); } export function patch(vnode1: VNode, vnode2: VNode, withBeforeRemove: boolean = false) { diff --git a/src/component/component_node.ts b/src/component/component_node.ts index f9f69139..5a298062 100644 --- a/src/component/component_node.ts +++ b/src/component/component_node.ts @@ -6,6 +6,7 @@ import { makeChildFiber, makeRootFiber, MountFiber, + MountOptions, RootFiber, __internal__destroyed, } from "./fibers"; @@ -92,8 +93,8 @@ export class ComponentNode implements VNode> { - const fiber = new MountFiber(this, target); + mountComponent(target: any, options?: MountOptions): Promise> { + const fiber = new MountFiber(this, target, options); this.app.scheduler.addFiber(fiber); this.initiateRender(fiber); return fiber.promise.then(() => this.component); diff --git a/src/component/fibers.ts b/src/component/fibers.ts index 9e7ee369..71912945 100644 --- a/src/component/fibers.ts +++ b/src/component/fibers.ts @@ -158,17 +158,30 @@ export class RootFiber extends Fiber { export let __internal__destroyed: ComponentNode[] = []; +type Position = "first-child" | "last-child"; + +export interface MountOptions { + position?: Position; +} + export class MountFiber extends RootFiber { target: HTMLElement; + position: Position; - constructor(node: ComponentNode, target: HTMLElement) { + constructor(node: ComponentNode, target: HTMLElement, options: MountOptions = {}) { super(node); this.target = target; + this.position = options.position || "last-child"; } complete() { const node = this.node; 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; this.appliedToDom = true; let current; diff --git a/src/component/props_validation.ts b/src/component/props_validation.ts index a7be4e35..223f5ffa 100644 --- a/src/component/props_validation.ts +++ b/src/component/props_validation.ts @@ -6,7 +6,7 @@ import { Component } from "./component"; * Note that this method does modify in place the props */ export function applyDefaultProps(props: { [key: string]: any }, ComponentClass: typeof Component) { - const defaultProps = (ComponentClass as any).defaultProps + const defaultProps = (ComponentClass as any).defaultProps; if (defaultProps) { for (let propName in defaultProps) { if (props![propName] === undefined) { @@ -16,7 +16,6 @@ export function applyDefaultProps(props: { [key: string]: any }, ComponentClass: } } - //------------------------------------------------------------------------------ // Prop validation helper //------------------------------------------------------------------------------ diff --git a/tests/components/__snapshots__/basics.test.ts.snap b/tests/components/__snapshots__/basics.test.ts.snap index ca0b2c8b..9af23e4c 100644 --- a/tests/components/__snapshots__/basics.test.ts.snap +++ b/tests/components/__snapshots__/basics.test.ts.snap @@ -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(\`
app
\`); + + 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(\`
app
\`); + + 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(\`
app
\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; + exports[`support svg components add proper namespace to svg 1`] = ` "function anonymous(bdom, helpers ) { diff --git a/tests/components/basics.test.ts b/tests/components/basics.test.ts index ff9c7644..5730bce1 100644 --- a/tests/components/basics.test.ts +++ b/tests/components/basics.test.ts @@ -902,78 +902,38 @@ describe("dynamic t-props", () => { }); }); -describe.skip("mount targets", () => { - test("can attach a component to an existing node (if same tagname)", async () => { - // class App extends Component { - // static template = xml`
app

another tag

`; - // state = useState({ customClass: "custom" }); - // } - // const div = document.createElement("div"); - // div.classList.add("arbitrary"); - // div.innerHTML = `

pre-existing

`; - // fixture.appendChild(div); - // const app = await mount(App, { target: div, position: "self" }); - // expect(fixture.innerHTML).toBe( - // `

pre-existing

app

another tag

` - // ); - // expect(div).toBe(app.el); - // app.state.customClass = "custom2"; - // await nextTick(); - // expect(fixture.innerHTML).toBe( - // `

pre-existing

app

another tag

` - // ); - // 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`app`; - // } - // 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)"); - }); - +describe("mount targets", () => { test("can mount a component (with position='first-child')", async () => { - // class App extends Component { - // static template = xml`
app
`; - // } - // const span = document.createElement("span"); - // fixture.appendChild(span); - // await mount(App, { target: fixture, position: "first-child" }); - // expect(fixture.innerHTML).toBe("
app
"); + class Root extends Component { + static template = xml`
app
`; + } + const span = document.createElement("span"); + fixture.appendChild(span); + const app = new App(Root); + await app.mount(fixture, { position: "first-child" }); + expect(fixture.innerHTML).toBe("
app
"); }); - test("can mount a component (with position='last-child')", async () => { - // class App extends Component { - // static template = xml`
app
`; - // } - // const span = document.createElement("span"); - // fixture.appendChild(span); - // await mount(App, { target: fixture, position: "last-child" }); - // expect(fixture.innerHTML).toBe("
app
"); + test("can mount a component (with default position='last-child')", async () => { + class Root extends Component { + static template = xml`
app
`; + } + const span = document.createElement("span"); + fixture.appendChild(span); + const app = new App(Root); + await app.mount(fixture, { position: "last-child" }); + expect(fixture.innerHTML).toBe("
app
"); }); test("default mount option is 'last-child'", async () => { - // class App extends Component { - // static template = xml`
app
`; - // } - // const span = document.createElement("span"); - // fixture.appendChild(span); - // await mount(App, { target: fixture }); - // expect(fixture.innerHTML).toBe("
app
"); + class Root extends Component { + static template = xml`
app
`; + } + const span = document.createElement("span"); + fixture.appendChild(span); + const app = new App(Root); + await app.mount(fixture); + expect(fixture.innerHTML).toBe("
app
"); }); });