[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:
tsm-odoo
2023-04-06 10:41:01 +02:00
committed by Géry Debongnie
parent 606421776b
commit c0c40c0387
5 changed files with 94 additions and 7 deletions
+5 -2
View File
@@ -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.
+2 -1
View File
@@ -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);
+2 -1
View File
@@ -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
View File
@@ -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;
+67
View File
@@ -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"));
});
});