lit-modal-portal
Version:
A custom portal directive for the Lit framework to render content elsewhere in the DOM
122 lines (121 loc) • 4.78 kB
JavaScript
import { render as litRender, nothing } from "lit";
import { directive } from "lit/directive.js";
import { AsyncDirective } from "lit/async-directive.js";
function getTarget(targetOrSelector) {
let target = targetOrSelector;
if (typeof target === "string") {
target = document.querySelector(target);
if (target === null) {
throw Error(`Could not locate portal target with selector "${targetOrSelector}".`);
}
}
return target;
}
class PortalDirective extends AsyncDirective {
constructor() {
super(...arguments);
this.containerId = `portal-${self.crypto.randomUUID()}`;
}
/**
* Main render function for the directive.
*
* For clarity's sake, here is the outline of the function body::
*
* - Resolve `targetOrSelector` to an element.
*
* - If the directive's `container` property is `undefined`,
* - then create the container element and store it in the property.
*
* - If `modifyContainer` is provided in the `options`,
* - then call `modifyContainer(container)`.
*
* - If the target has changed from one element to another,
* - then migrate `container` to the new target and reassign the directive's `target` property.
*
* - If the directive's `target` property is `undefined`,
* - then store the target element in the property.
*
* - If a `placeholder` is provided in the `options`,
* - then append `container` to `target` (if necessary) and render `placeholder` in `container`.
*
* - Resolve `content` (awaited).
*
* - Append `container` to `target` (if necessary) and render `content` in `container`.
*
* The steps are organized this way to balance the initalization and refreshing of crucial properties
* like `container` and `target` while ensuring that `container` isn't added to the DOM until
* the directive is about to render something (either `placeholder` or `content`).
*
* @param content - The content of the portal.
* This parameter is passed as the `value` parameter in [Lit's `render` function](https://lit.dev/docs/api/templates/#render).
*
* The `content` parameter can be a promise, which will be rendered in the portal once it resolves.
*
* @param targetOrSelector - The "target" for the portal.
* If the value is a string, then it is treated as a query selector and passed to `document.querySelector()` in order to locate the portal target.
* If no element is found with the selector, then an error is thrown.
*
* @param options - See {@link PortalOptions}.
*
* @returns This function always returns Lit's [`nothing`](https://lit.dev/docs/api/templates/#nothing) value,
* because nothing ever renders where the portal is used.
*/
render(content, targetOrSelector, options) {
Promise.resolve(targetOrSelector).then(async (targetOrSelector2) => {
if (!targetOrSelector2) {
throw Error(
"Target was falsy. Are you using a Lit ref before its value is defined? If so, try using Lit's @queryAsync decorator instead (https://lit.dev/docs/api/decorators/#queryAsync)."
);
}
const newTarget = getTarget(targetOrSelector2);
if (!this.container) {
const newContainer = document.createElement("div");
newContainer.id = this.containerId;
if (options?.modifyContainer) {
options.modifyContainer(newContainer);
}
this.container = newContainer;
}
if (this.target && this.target !== newTarget) {
this.target?.removeChild(this.container);
newTarget.appendChild(this.container);
this.target = newTarget;
}
if (!this.target) {
this.target = newTarget;
if (options?.placeholder) {
if (!this.target.contains(this.container)) {
this.target.appendChild(this.container);
}
litRender(options.placeholder, this.container);
}
}
const resolvedContent = await Promise.resolve(content);
if (!this.target.contains(this.container)) {
this.target.appendChild(this.container);
}
litRender(resolvedContent, this.container);
});
return nothing;
}
/** Remove container from target when the directive is disconnected. */
disconnected() {
if (this.target?.contains(this.container)) {
this.target?.removeChild(this.container);
} else {
console.warn(
"portal directive was disconnected after the portal container was removed from the target."
);
}
}
/** Append container to target when the directive is reconnected. */
reconnected() {
this.target?.appendChild(this.container);
}
}
const portal = directive(PortalDirective);
export {
PortalDirective,
portal
};
//# sourceMappingURL=portal.js.map