mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] portal: add support for .closest modifier
It is sometimes useful in practice to be able to configure the portal so that it looks for a target as close as possible as the portal location. For example, in odoo, a portal may be set up in a form view that should target the current form view. But if the form view itself is in a dialog, it may fail, because it may find a valid target in the form view that is located underneath. With this commit, we add a .closest modifier to the `t-portal` directive.
This commit is contained in:
@@ -15,3 +15,11 @@ class SomeComponent extends Component {
|
||||
The `t-portal` directive takes a valid css selector as argument. The content of
|
||||
the portalled template will be mounted at the corresponding location. Note that
|
||||
Owl need to insert an empty text node at the location of the portalled content.
|
||||
|
||||
The `t-portal` directive supports a `.closest` modifier. It is useful to select
|
||||
the closest target from the portal location: Owl will look for a target in the
|
||||
current parent element, then in its parent, and so on until it finds it.
|
||||
|
||||
```xml
|
||||
<div t-portal.closest="'.target'">some content</div>
|
||||
```
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+12
-3
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
+24
-7
@@ -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<VNode<VPortal>> {
|
||||
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<VNode<VPortal>> {
|
||||
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.target, this.props.isClosest);
|
||||
if (target) {
|
||||
portal.content!.moveBeforeDOMNode(target.firstChild, target);
|
||||
} else {
|
||||
|
||||
@@ -1998,6 +1998,7 @@ describe("qweb parser", () => {
|
||||
test("t-portal", async () => {
|
||||
expect(parse(`<t t-portal="target">Content</t>`)).toEqual({
|
||||
type: ASTType.TPortal,
|
||||
isClosest: false,
|
||||
target: "target",
|
||||
content: { type: ASTType.Text, value: "Content" },
|
||||
});
|
||||
@@ -2008,6 +2009,7 @@ describe("qweb parser", () => {
|
||||
condition: "condition",
|
||||
content: {
|
||||
content: { type: ASTType.Text, value: "Content" },
|
||||
isClosest: false,
|
||||
target: "target",
|
||||
type: ASTType.TPortal,
|
||||
},
|
||||
@@ -2016,4 +2018,13 @@ describe("qweb parser", () => {
|
||||
type: ASTType.TIf,
|
||||
});
|
||||
});
|
||||
|
||||
test("t-portal with .closest", async () => {
|
||||
expect(parse(`<t t-portal.closest="target">Content</t>`)).toEqual({
|
||||
type: ASTType.TPortal,
|
||||
isClosest: true,
|
||||
target: "target",
|
||||
content: { type: ASTType.Text, value: "Content" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(\`<p class=\\"target\\">far target</p>\`);
|
||||
let block3 = createBlock(\`<div><p class=\\"target\\">close target</p><block-child-0/></div>\`);
|
||||
|
||||
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]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -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`
|
||||
<p class="target">far target</p>
|
||||
<div>
|
||||
<p class="target">close target</p>
|
||||
<t t-portal.closest="'.target'">portal content</t>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<p class="target">far target</p><div><p class="target">close targetportal content</p></div>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user