Skip to content

Commit b1d22bb

Browse files
committed
wip
1 parent ba20267 commit b1d22bb

File tree

5 files changed

+80
-11
lines changed

5 files changed

+80
-11
lines changed

src/compiler/code_generator.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1371,7 +1371,9 @@ export class CodeGenerator {
13711371
});
13721372

13731373
const target = compileExpr(ast.target);
1374-
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`;
1374+
const blockString = `${id}({target: ${target},${
1375+
ast.isClosest ? "isClosest: true," : ""
1376+
}slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`;
13751377
if (block) {
13761378
this.insertAnchor(block);
13771379
}

src/compiler/parser.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export interface ASTTranslation {
169169
export interface ASTTPortal {
170170
type: ASTType.TPortal;
171171
target: string;
172+
isClosest: boolean;
172173
content: AST;
173174
}
174175

@@ -833,11 +834,18 @@ function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
833834
// -----------------------------------------------------------------------------
834835

835836
function parseTPortal(node: Element, ctx: ParsingContext): AST | null {
836-
if (!node.hasAttribute("t-portal")) {
837+
let target, isClosest;
838+
if (node.hasAttribute("t-portal")) {
839+
target = node.getAttribute("t-portal")!;
840+
node.removeAttribute("t-portal");
841+
isClosest = false;
842+
} else if (node.hasAttribute("t-portal.closest")) {
843+
target = node.getAttribute("t-portal.closest")!;
844+
node.removeAttribute("t-portal.closest");
845+
isClosest = true;
846+
} else {
837847
return null;
838848
}
839-
const target = node.getAttribute("t-portal")!;
840-
node.removeAttribute("t-portal");
841849
const content = parseNode(node, ctx);
842850
if (!content) {
843851
return {
@@ -848,6 +856,7 @@ function parseTPortal(node: Element, ctx: ParsingContext): AST | null {
848856
return {
849857
type: ASTType.TPortal,
850858
target,
859+
isClosest,
851860
content,
852861
};
853862
}

src/runtime/portal.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,34 @@ import { OwlError } from "./error_handling";
55

66
const VText: any = text("").constructor;
77

8+
function getTarget(
9+
currentParentEl: HTMLElement | Document,
10+
selector: string,
11+
isClosest: boolean
12+
): HTMLElement | null {
13+
if (!isClosest || currentParentEl === document) {
14+
return document.querySelector(selector);
15+
}
16+
const attempt = currentParentEl.querySelector(selector) as HTMLElement | null;
17+
return attempt || getTarget(currentParentEl.parentElement!, selector, true);
18+
}
19+
820
class VPortal extends VText implements Partial<VNode<VPortal>> {
921
content: BDom | null;
1022
selector: string;
23+
isClosest: boolean;
1124
target: HTMLElement | null = null;
1225

13-
constructor(selector: string, content: BDom) {
26+
constructor(selector: string, isClosest: boolean, content: BDom) {
1427
super("");
1528
this.selector = selector;
29+
this.isClosest = isClosest;
1630
this.content = content;
1731
}
1832

1933
mount(parent: HTMLElement, anchor: ChildNode) {
2034
super.mount(parent, anchor);
21-
this.target = document.querySelector(this.selector) as any;
35+
this.target = getTarget(parent, this.selector, this.isClosest);
2236
if (this.target) {
2337
this.content!.mount(this.target!, null);
2438
} else {
@@ -54,16 +68,19 @@ class VPortal extends VText implements Partial<VNode<VPortal>> {
5468
export function portalTemplate(app: any, bdom: any, helpers: any) {
5569
let { callSlot } = helpers;
5670
return function template(ctx: any, node: any, key = ""): any {
57-
return new VPortal(ctx.props.target, callSlot(ctx, node, key, "default", false, null));
71+
return new VPortal(
72+
ctx.props.target,
73+
ctx.props.isClosest,
74+
callSlot(ctx, node, key, "default", false, null)
75+
);
5876
};
5977
}
6078

6179
export class Portal extends Component {
6280
static template = "__portal__";
6381
static props = {
64-
target: {
65-
type: String,
66-
},
82+
target: String,
83+
isClosest: { type: Boolean, optional: true },
6784
slots: true,
6885
};
6986

@@ -73,7 +90,7 @@ export class Portal extends Component {
7390
onMounted(() => {
7491
const portal: VPortal = node.bdom;
7592
if (!portal.target) {
76-
const target: HTMLElement = document.querySelector(this.props.target);
93+
const target = getTarget(portal.parentEl, this.props.selector, this.props.isClosest);
7794
if (target) {
7895
portal.content!.moveBeforeDOMNode(target.firstChild, target);
7996
} else {

tests/misc/__snapshots__/portal.test.ts.snap

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -999,3 +999,26 @@ exports[`Portal: UI/UX focus is kept across re-renders 2`] = `
999999
}
10001000
}"
10011001
`;
1002+
1003+
exports[`portal .closest suffix basic use of .suffix 1`] = `
1004+
"function anonymous(app, bdom, helpers
1005+
) {
1006+
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
1007+
const Portal = app.Portal;
1008+
const comp1 = app.createComponent(null, false, true, false, false);
1009+
1010+
let block2 = createBlock(\`<p class=\\"target\\">far target</p>\`);
1011+
let block3 = createBlock(\`<div><p class=\\"target\\">close target</p><block-child-0/></div>\`);
1012+
1013+
function slot1(ctx, node, key = \\"\\") {
1014+
return text(\`portal content\`);
1015+
}
1016+
1017+
return function template(ctx, node, key = \\"\\") {
1018+
const b2 = block2();
1019+
const b5 = comp1({target: '.target',isClosest: true,slots: {'default': {__render: slot1.bind(this), __ctx: ctx}}}, key + \`__1\`, node, ctx, Portal);
1020+
const b3 = block3([], [b5]);
1021+
return multi([b2, b3]);
1022+
}
1023+
}"
1024+
`;

tests/misc/portal.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,3 +1028,21 @@ describe("Portal: Props validation", () => {
10281028
expect(error!.message).toBe(`invalid portal target`);
10291029
});
10301030
});
1031+
1032+
describe("portal .closest suffix", () => {
1033+
test("basic use of .suffix", async () => {
1034+
class Parent extends Component {
1035+
static template = xml`
1036+
<p class="target">far target</p>
1037+
<div>
1038+
<p class="target">close target</p>
1039+
<t t-portal.closest="'.target'">portal content</t>
1040+
</div>`;
1041+
}
1042+
1043+
await mount(Parent, fixture);
1044+
expect(fixture.innerHTML).toBe(
1045+
'<p class="target">far target</p><div><p class="target">close targetportal content</p></div>'
1046+
);
1047+
});
1048+
});

0 commit comments

Comments
 (0)