From b1d22bb718ae8f78d7d51a5d14be4c1517802c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 8 May 2023 11:33:50 +0200 Subject: [PATCH] wip --- src/compiler/code_generator.ts | 4 ++- src/compiler/parser.ts | 15 ++++++++-- src/runtime/portal.ts | 31 +++++++++++++++----- tests/misc/__snapshots__/portal.test.ts.snap | 23 +++++++++++++++ tests/misc/portal.test.ts | 18 ++++++++++++ 5 files changed, 80 insertions(+), 11 deletions(-) diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index d56bbb2b2..c0414597f 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -1371,7 +1371,9 @@ export class CodeGenerator { }); const target = compileExpr(ast.target); - const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`; + const blockString = `${id}({target: ${target},${ + ast.isClosest ? "isClosest: true," : "" + }slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`; if (block) { this.insertAnchor(block); } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 42a89f131..e3f7ca7c3 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -169,6 +169,7 @@ export interface ASTTranslation { export interface ASTTPortal { type: ASTType.TPortal; target: string; + isClosest: boolean; content: AST; } @@ -833,11 +834,18 @@ function parseTTranslation(node: Element, ctx: ParsingContext): AST | null { // ----------------------------------------------------------------------------- function parseTPortal(node: Element, ctx: ParsingContext): AST | null { - if (!node.hasAttribute("t-portal")) { + let target, isClosest; + if (node.hasAttribute("t-portal")) { + target = node.getAttribute("t-portal")!; + node.removeAttribute("t-portal"); + isClosest = false; + } else if (node.hasAttribute("t-portal.closest")) { + target = node.getAttribute("t-portal.closest")!; + node.removeAttribute("t-portal.closest"); + isClosest = true; + } else { return null; } - const target = node.getAttribute("t-portal")!; - node.removeAttribute("t-portal"); const content = parseNode(node, ctx); if (!content) { return { @@ -848,6 +856,7 @@ function parseTPortal(node: Element, ctx: ParsingContext): AST | null { return { type: ASTType.TPortal, target, + isClosest, content, }; } diff --git a/src/runtime/portal.ts b/src/runtime/portal.ts index 37ba8a325..737fe1be0 100644 --- a/src/runtime/portal.ts +++ b/src/runtime/portal.ts @@ -5,20 +5,34 @@ import { OwlError } from "./error_handling"; const VText: any = text("").constructor; +function getTarget( + currentParentEl: HTMLElement | Document, + selector: string, + isClosest: boolean +): HTMLElement | null { + if (!isClosest || currentParentEl === document) { + return document.querySelector(selector); + } + const attempt = currentParentEl.querySelector(selector) as HTMLElement | null; + return attempt || getTarget(currentParentEl.parentElement!, selector, true); +} + class VPortal extends VText implements Partial> { content: BDom | null; selector: string; + isClosest: boolean; target: HTMLElement | null = null; - constructor(selector: string, content: BDom) { + constructor(selector: string, isClosest: boolean, content: BDom) { super(""); this.selector = selector; + this.isClosest = isClosest; this.content = content; } mount(parent: HTMLElement, anchor: ChildNode) { super.mount(parent, anchor); - this.target = document.querySelector(this.selector) as any; + this.target = getTarget(parent, this.selector, this.isClosest); if (this.target) { this.content!.mount(this.target!, null); } else { @@ -54,16 +68,19 @@ class VPortal extends VText implements Partial> { export function portalTemplate(app: any, bdom: any, helpers: any) { let { callSlot } = helpers; return function template(ctx: any, node: any, key = ""): any { - return new VPortal(ctx.props.target, callSlot(ctx, node, key, "default", false, null)); + return new VPortal( + ctx.props.target, + ctx.props.isClosest, + callSlot(ctx, node, key, "default", false, null) + ); }; } export class Portal extends Component { static template = "__portal__"; static props = { - target: { - type: String, - }, + target: String, + isClosest: { type: Boolean, optional: true }, slots: true, }; @@ -73,7 +90,7 @@ export class Portal extends Component { onMounted(() => { const portal: VPortal = node.bdom; if (!portal.target) { - const target: HTMLElement = document.querySelector(this.props.target); + const target = getTarget(portal.parentEl, this.props.selector, this.props.isClosest); if (target) { portal.content!.moveBeforeDOMNode(target.firstChild, target); } else { diff --git a/tests/misc/__snapshots__/portal.test.ts.snap b/tests/misc/__snapshots__/portal.test.ts.snap index 5840ced65..8b28653d9 100644 --- a/tests/misc/__snapshots__/portal.test.ts.snap +++ b/tests/misc/__snapshots__/portal.test.ts.snap @@ -999,3 +999,26 @@ exports[`Portal: UI/UX focus is kept across re-renders 2`] = ` } }" `; + +exports[`portal .closest suffix basic use of .suffix 1`] = ` +"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(\`

far target

\`); + let block3 = createBlock(\`

close target

\`); + + function slot1(ctx, node, key = \\"\\") { + return text(\`portal content\`); + } + + return function template(ctx, node, key = \\"\\") { + const b2 = block2(); + const b5 = comp1({target: '.target',isClosest: true,slots: {'default': {__render: slot1.bind(this), __ctx: ctx}}}, key + \`__1\`, node, ctx, Portal); + const b3 = block3([], [b5]); + return multi([b2, b3]); + } +}" +`; diff --git a/tests/misc/portal.test.ts b/tests/misc/portal.test.ts index b5c230b48..626bb2304 100644 --- a/tests/misc/portal.test.ts +++ b/tests/misc/portal.test.ts @@ -1028,3 +1028,21 @@ describe("Portal: Props validation", () => { expect(error!.message).toBe(`invalid portal target`); }); }); + +describe("portal .closest suffix", () => { + test("basic use of .suffix", async () => { + class Parent extends Component { + static template = xml` +

far target

+
+

close target

+ portal content +
`; + } + + await mount(Parent, fixture); + expect(fixture.innerHTML).toBe( + '

far target

close targetportal content

' + ); + }); +});