UNPKG

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
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