diff --git a/src/runtime/portal.ts b/src/runtime/portal.ts index 4723a8d7..e06b1009 100644 --- a/src/runtime/portal.ts +++ b/src/runtime/portal.ts @@ -1,4 +1,4 @@ -import { onWillUnmount } from "./lifecycle_hooks"; +import { onMounted, onWillUnmount } from "./lifecycle_hooks"; import { BDom, text, VNode } from "./blockdom"; import { Component } from "./component"; import { OwlError } from "./error_handling"; @@ -6,60 +6,55 @@ import { OwlError } from "./error_handling"; const VText: any = text("").constructor; class VPortal extends VText implements Partial> { - // selector: string; - realBDom: BDom | null; + content: BDom | null; + selector: string; target: HTMLElement | null = null; - constructor(selector: string, realBDom: BDom) { + constructor(selector: string, content: BDom) { super(""); this.selector = selector; - this.realBDom = realBDom; + this.content = content; } + mount(parent: HTMLElement, anchor: ChildNode) { super.mount(parent, anchor); this.target = document.querySelector(this.selector) as any; - if (!this.target) { - let el: any = this.el; - while (el && el.parentElement instanceof HTMLElement) { - el = el.parentElement; - } - this.target = el && el.querySelector(this.selector); - if (!this.target) { - throw new OwlError("invalid portal target"); - } + if (this.target) { + this.content!.mount(this.target!, null); + } else { + this.content!.mount(parent, anchor); } - this.realBDom!.mount(this.target!, null); } beforeRemove() { - this.realBDom!.beforeRemove(); + this.content!.beforeRemove(); } remove() { - if (this.realBDom) { + if (this.content) { super.remove(); - this.realBDom!.remove(); - this.realBDom = null; + this.content!.remove(); + this.content = null; } } patch(other: VPortal) { super.patch(other); - if (this.realBDom) { - this.realBDom.patch(other.realBDom!, true); + if (this.content) { + this.content.patch(other.content!, true); } else { - this.realBDom = other.realBDom; - this.realBDom!.mount(this.target!, null); + this.content = other.content; + this.content!.mount(this.target!, null); } } } /** - * + * kind of similar to , but it wraps it around a VPortal */ export function portalTemplate(app: any, bdom: any, helpers: any) { let { callSlot } = helpers; - return function template(ctx: any, node: any, key = "") { - return callSlot(ctx, node, key, "default", false, null); + return function template(ctx: any, node: any, key = ""): any { + return new VPortal(ctx.props.target, callSlot(ctx, node, key, "default", false, null)); }; } @@ -73,13 +68,23 @@ export class Portal extends Component { }; setup() { - const node = this.__owl__; - const renderFn = node.renderFn; - node.renderFn = () => new VPortal(this.props.target, renderFn()); - onWillUnmount(() => { - if (node.bdom) { - node.bdom.remove(); + const node: any = this.__owl__; + + onMounted(() => { + const portal: VPortal = node.bdom; + if (!portal.target) { + const target: HTMLElement = document.querySelector(this.props.target); + if (target) { + portal.content!.moveBefore(target, null); + } else { + throw new OwlError("invalid portal target"); + } } }); + + onWillUnmount(() => { + const portal: VPortal = node.bdom; + portal.remove(); + }); } } diff --git a/tests/misc/__snapshots__/portal.test.ts.snap b/tests/misc/__snapshots__/portal.test.ts.snap index dca22dd3..fd61e0f5 100644 --- a/tests/misc/__snapshots__/portal.test.ts.snap +++ b/tests/misc/__snapshots__/portal.test.ts.snap @@ -155,6 +155,44 @@ exports[`Portal Add and remove portals with t-foreach inside div 1`] = ` }" `; +exports[`Portal Child and Portal 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const comp1 = app.createComponent(\`Child\`, true, false, false, true); + + let block3 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const b2 = comp1({}, key + \`__1\`, node, this, null); + const b3 = block3(); + return multi([b2, b3]); + } +}" +`; + +exports[`Portal Child and Portal 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const Portal = app.Portal; + const comp1 = app.createComponent(null, false, true, false, false); + + let block2 = createBlock(\`child\`); + let block3 = createBlock(\`portal\`); + + function slot1(ctx, node, key = \\"\\") { + return block3(); + } + + return function template(ctx, node, key = \\"\\") { + const b2 = block2(); + const b4 = comp1({target: '.portal',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx, Portal); + return multi([b2, b4]); + } +}" +`; + exports[`Portal Portal composed with t-slot 1`] = ` "function anonymous(app, bdom, helpers ) { @@ -515,6 +553,44 @@ exports[`Portal lifecycle hooks of portal sub component are properly called 2`] }" `; +exports[`Portal portal and Child 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const comp1 = app.createComponent(\`Child\`, true, false, false, true); + + let block2 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const b2 = block2(); + const b3 = comp1({}, key + \`__1\`, node, this, null); + return multi([b2, b3]); + } +}" +`; + +exports[`Portal portal and Child 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const Portal = app.Portal; + const comp1 = app.createComponent(null, false, true, false, false); + + let block2 = createBlock(\`child\`); + let block3 = createBlock(\`portal\`); + + function slot1(ctx, node, key = \\"\\") { + return block3(); + } + + return function template(ctx, node, key = \\"\\") { + const b2 = block2(); + const b4 = comp1({target: '.portal',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx, Portal); + return multi([b2, b4]); + } +}" +`; + exports[`Portal portal could have dynamically no content 1`] = ` "function anonymous(app, bdom, helpers ) { diff --git a/tests/misc/portal.test.ts b/tests/misc/portal.test.ts index 103aa669..33fdbe45 100644 --- a/tests/misc/portal.test.ts +++ b/tests/misc/portal.test.ts @@ -276,7 +276,7 @@ describe("Portal", () => { expect(error!).toBeDefined(); expect(error!.message).toBe("invalid portal target"); - expect(fixture.innerHTML).toBe(`
`); + expect(fixture.innerHTML).toBe(``); expect(mockConsoleWarn).toBeCalledTimes(1); }); @@ -874,6 +874,48 @@ describe("Portal", () => { await nextTick(); expect(fixture.innerHTML).toBe('
'); }); + + test("Child and Portal", async () => { + class Child extends Component { + static template = xml` + child + portal`; + } + class Parent extends Component { + static template = xml` + + +
+
`; + + static components = { Child }; + } + await mount(Parent, fixture); + expect(fixture.innerHTML).toBe( + 'child
portal' + ); + }); + + test("portal and Child", async () => { + class Child extends Component { + static template = xml` + child + portal`; + } + class Parent extends Component { + static template = xml` + +
+ +
`; + + static components = { Child }; + } + await mount(Parent, fixture); + expect(fixture.innerHTML).toBe( + '
portal
child' + ); + }); }); describe("Portal: UI/UX", () => {