mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] Allow app to be mounted in shadow DOM
This commit allow owl Apps to be mounted into a shadow DOM.
This commit is contained in:
+5
-2
@@ -84,7 +84,10 @@ export class App<
|
|||||||
this.props = config.props || ({} as P);
|
this.props = config.props || ({} as P);
|
||||||
}
|
}
|
||||||
|
|
||||||
mount(target: HTMLElement, options?: MountOptions): Promise<Component<P, E> & InstanceType<T>> {
|
mount(
|
||||||
|
target: HTMLElement | ShadowRoot,
|
||||||
|
options?: MountOptions
|
||||||
|
): Promise<Component<P, E> & InstanceType<T>> {
|
||||||
App.validateTarget(target);
|
App.validateTarget(target);
|
||||||
if (this.dev) {
|
if (this.dev) {
|
||||||
validateProps(this.Root, this.props, { __owl__: { app: this } });
|
validateProps(this.Root, this.props, { __owl__: { app: this } });
|
||||||
@@ -99,7 +102,7 @@ export class App<
|
|||||||
return new ComponentNode(Component, props, this, null, null);
|
return new ComponentNode(Component, props, this, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
|
mountNode(node: ComponentNode, target: HTMLElement | ShadowRoot, options?: MountOptions) {
|
||||||
const promise: any = new Promise((resolve, reject) => {
|
const promise: any = new Promise((resolve, reject) => {
|
||||||
let isResolved = false;
|
let isResolved = false;
|
||||||
// manually set a onMounted callback.
|
// manually set a onMounted callback.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { inOwnerDocument } from "../utils";
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
|
|
||||||
type EventHandlerSetter = (this: HTMLElement, data: any) => void;
|
type EventHandlerSetter = (this: HTMLElement, data: any) => void;
|
||||||
@@ -28,7 +29,7 @@ function createElementHandler(evName: string, capture: boolean = false): EventHa
|
|||||||
|
|
||||||
function listener(ev: Event) {
|
function listener(ev: Event) {
|
||||||
const currentTarget = ev.currentTarget as HTMLElement;
|
const currentTarget = ev.currentTarget as HTMLElement;
|
||||||
if (!currentTarget || !currentTarget.ownerDocument.contains(currentTarget)) return;
|
if (!currentTarget || !inOwnerDocument(currentTarget)) return;
|
||||||
const data = (currentTarget as any)[eventKey];
|
const data = (currentTarget as any)[eventKey];
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
config.mainEventHandler(data, ev, currentTarget);
|
config.mainEventHandler(data, ev, currentTarget);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Env } from "./app";
|
import type { Env } from "./app";
|
||||||
import { getCurrent } from "./component_node";
|
import { getCurrent } from "./component_node";
|
||||||
import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks";
|
import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks";
|
||||||
|
import { inOwnerDocument } from "./utils";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// useRef
|
// useRef
|
||||||
@@ -16,7 +17,7 @@ export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el:
|
|||||||
return {
|
return {
|
||||||
get el(): T | null {
|
get el(): T | null {
|
||||||
const el = refs[name];
|
const el = refs[name];
|
||||||
return el?.ownerDocument.contains(el) ? el : null;
|
return inOwnerDocument(el) ? el : null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-3
@@ -27,13 +27,28 @@ export function batched(callback: Callback): Callback {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateTarget(target: HTMLElement) {
|
/**
|
||||||
|
* Determine whether the given element is contained in its ownerDocument:
|
||||||
|
* either directly or with a shadow root in between.
|
||||||
|
*/
|
||||||
|
export function inOwnerDocument(el?: HTMLElement) {
|
||||||
|
if (!el) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (el.ownerDocument.contains(el)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const rootNode = el.getRootNode();
|
||||||
|
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateTarget(target: HTMLElement | ShadowRoot) {
|
||||||
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
||||||
const document = target && target.ownerDocument;
|
const document = target && target.ownerDocument;
|
||||||
if (document) {
|
if (document) {
|
||||||
const HTMLElement = document.defaultView!.HTMLElement;
|
const HTMLElement = document.defaultView!.HTMLElement;
|
||||||
if (target instanceof HTMLElement) {
|
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
||||||
if (!document.body.contains(target)) {
|
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
|
||||||
throw new OwlError("Cannot mount a component on a detached dom node");
|
throw new OwlError("Cannot mount a component on a detached dom node");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { App, Component, useRef, xml } from "../../src";
|
||||||
|
import { status } from "../../src/runtime/status";
|
||||||
|
import { makeTestFixture, snapshotEverything } from "../helpers";
|
||||||
|
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
|
||||||
|
snapshotEverything();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shadow_dom", () => {
|
||||||
|
test("can mount app", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div class="my-div"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement("div");
|
||||||
|
fixture.appendChild(container);
|
||||||
|
const shadow = container.attachShadow({ mode: "open" });
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const comp = await app.mount(shadow);
|
||||||
|
const div = shadow.querySelector(".my-div");
|
||||||
|
expect(div).not.toBe(null);
|
||||||
|
expect(shadow.contains(div)).toBe(true);
|
||||||
|
app.destroy();
|
||||||
|
expect(shadow.contains(div)).toBe(false);
|
||||||
|
expect(status(comp)).toBe("destroyed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can bind event handler", async () => {
|
||||||
|
let a = 1;
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<button t-on-click="add">Click</button>`;
|
||||||
|
|
||||||
|
add() {
|
||||||
|
a = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const container = document.createElement("div");
|
||||||
|
fixture.appendChild(container);
|
||||||
|
const shadow = container.attachShadow({ mode: "open" });
|
||||||
|
await new App(SomeComponent).mount(shadow);
|
||||||
|
expect(a).toBe(1);
|
||||||
|
shadow.querySelector("button")!.click();
|
||||||
|
expect(a).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("useRef hook", async () => {
|
||||||
|
let comp: SomeComponent;
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div t-ref="refName" class="my-div"/>`;
|
||||||
|
div = useRef("refName");
|
||||||
|
setup() {
|
||||||
|
comp = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const container = document.createElement("div");
|
||||||
|
fixture.appendChild(container);
|
||||||
|
const shadow = container.attachShadow({ mode: "open" });
|
||||||
|
const mountedProm = new App(SomeComponent).mount(shadow);
|
||||||
|
expect(comp!.div.el).toBe(null);
|
||||||
|
await mountedProm;
|
||||||
|
expect(comp!.div.el).toBe(shadow.querySelector(".my-div"));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user