lit-modal-portal
v0.7.0
Published
A custom portal directive for the Lit framework to render content elsewhere in the DOM
Downloads
285
Maintainers
Readme
lit-modal-portal
The lit-modal-portal
package provides a custom Lit directive, portal
, that renders a Lit template elsewhere in the DOM.
Its main goals are:
- to provide an API that is similar to React's
createPortal
function, and - to rely on the existing Lit API wherever possible.
This package also support asynchronous portal content.
:warning: Notice on version 0.6
This package was heavily altered between versions 0.4 and 0.6. Changes include:
- Add support for Lit v3 and fixed dependency declaration for v0.5. (Thanks, klasjersevi.)
- Removed the following code:
- Dependency of the immutable package.
- The
<modal-portal>
component and the singletonmodalController
. - All pre-made components, such as the
<confirm-modal>
.
- Refactor the
portal
directive to use Lit'srender
function.- This was primarily inspired by ronak-lm's lit-portal package, which more closely resembles React's portal API than previous versions of this package.
- This simplifies usage of the package and expands the potential use cases.
Installation and Usage
You can install lit-modal-portal
via NPM.
npm install lit-modal-portal
Suppose we have the following Lit application:
<!-- index.html -->
<!doctype html>
<html>
<head>
<title>lit-modal-portal Usage Example</title>
<!-- Your bundle/script -->
<script type="module" src="main.js"></script>
</head>
<body>
<!-- Your custom element -->
<app-root></app-root>
</body>
</html>
// index.ts (source code for main.js)
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { portal } from 'lit-modal-portal';
@customElement('app-root')
export class AppRoot extends LitElement {
render() {
return html`
<h1>lit-modal-portal Usage Example</h1>
${portal(html`<p>portal content</p>`, document.body)}
`;
}
}
When the <app-root>
component renders, it will activate the portal
directive, which will return nothing
but use Lit's API to asynchronously render the content in a container <div>
and append that container to document.body
.
When the portal's content is updated, the directive will re-render the new content in the same container. Additionally, if the target changes, then the container will be removed from the old target and appended to the new target.
API
type TargetOrSelector = Node | string;
type PortalOptions = {
placeholder?: unknown;
};
portal(
content: unknown | Promise<unknown>,
targetOrSelector: TargetOrSelector | Promise<TargetOrSelector>,
options?: PortalOptions,
): DirectiveResult<typeof PortalDirective>
Parameters:
content
: The content of the portal. This parameter is passed as thevalue
parameter in Lit'srender
function.Any renderable value typically a
TemplateResult
created by evaluating a template tag likehtml
orsvg
.targetOrSelector
: An element or a string that identifies the portal's target.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.options
: Configuration parameters for the portal.placeholder
: A value that will be rendered while thecontent
is resolving.
This function will always return Lit's nothing
value, because nothing is supposed to render where the portal is used.
Both the content
and the targetOrSelector
parameters may be promises.
The targetOrSelector
must resolve before the portal renders.
If the content
is a promise, then an optional placeholder
may be provided.
If no placeholder
is provided, then the portal will not render until the content
resolves.
See the docs for more information on how the portal
directive works.
Advanced Usage
Modals and dialogs
This package no longer provides modal components. Instead, it focuses on a directive that is simple to use in different ways and encourages users to implement their own modals.
One recommended approach is to use the dialog
element and its showModal
method,
which can be accessed using Lit's ref
directive.
Consider the following:
// example-app.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { ref, createRef } from 'lit/directives/ref.js';
import { portal } from 'lit-modal-portal';
import './lit-dialog';
@customElement('example-app')
export class ExampleApp extends LitElement {
dialogRef = createRef<HTMLDialogElement>();
render() {
return html`
<h1>lit-modal-portal Dialog Example</h1>
<button @click=${() => this.dialogRef.value?.showModal()}>Show Dialog</button>
${portal(html`<lit-dialog .dialogRef=${this.dialogRef}></lit-dialog>`, document.body)}
`;
}
}
// lit-dialog.ts
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { ref, createRef, Ref } from 'lit/directives/ref.js';
@customElement('lit-dialog')
export class LitDialog extends LitElement {
@property({ attribute: false })
dialogRef: Ref<HTMLDialogElement> = createRef();
render() {
return html`
<dialog ${ref(this.dialogRef)}>
<p>This is the dialog</p>
<button @click=${() => this.dialogRef.value?.close()}>Close Dialog</button>
</dialog>
`;
}
}
In this example, we have a <lit-dialog>
component that accepts a dialogRef
from the parent <example-app>
.
This allows the parent to open the dialog and the child to imperatively close it on a button's @click
event.
This basic pattern can be extended as necessary. Examples include:
- Listening to the dialog's
close
event, which would trigger if the dialog was closed with the Escape key. - Adding styles to the
<lit-dialog>
component. - Adding callback function properties to
<lit-dialog>
. - Using slotted content in the dialog component's template.
Targeting elements in the Shadow DOM
Using a DOM node in a Lit component as a target for a portal is tricky (and perhaps useless or inadvisable), for a number of reasons:
- The
querySelector
method does not penetrate through the shadow root, so running queries on thedocument
node won't return anything. - The
portal
directive is asynchronous, so if it renders at the same time as the component's first render, then the target might not even exist yet.
We cannot simply call querySelector
on a different render root, such as a component's shadow root, because it might be empty. However, this is still possible to accomplish safely with the use of Lit's queryAsync
decorator.
import { LitElement, html } from 'lit';
import { customElement, queryAsync } from 'lit/decorators.js';
import { portal } from 'lit-modal-portal';
@customElement('example-component')
export class ExampleComponent extends LitElement {
@queryAsync('#portal-target')
portalTarget: Promise<HTMLElement>;
render() {
return html`<div>
${portal(html`<p>Portal content</p>`, this.portalTarget)}
<p>The portal isn't rendered before this paragraph, but in the following div.</p>
<div id="portal-target"></div>
</div>`;
}
}
In this example, this.portalTarget
is a promise that resolves to the <div id="portal-target>
element after the <example-component>
renders.
See Lit's documentation for more information on components and the Shadow DOM.
Styling portal content
Another consquence of the Shadow DOM is that only inherited CSS properties affect elements inside a shadow root. Coupled with the fact that a portal serves to render content in a different location, this makes it difficult for a component that uses the portal
directive to style the portal's content.
It is recommended that the content of a portal should be another Lit component that can own its CSS. Alternatively, the styleMap
directive can be used in the template provided to the portal
directive.
See Lit's documentation for more information on working with CSS styles and the Shadow DOM.
Documentation
More in-depth documentation for this package is included in the repo, under the /docs
directory.
It is also hosted on GitHub Pages.
The documentation is generated using TypeDoc.
Please note the separate docs.tsconfig.json
and src/docs.index.ts
files if you plan to make changes related to the documentation.
Development
For demonstration and testing purposes, you can start a local development server by running npm run dev
.
There are multiple examples of the portal
directive that explain its features and supported usage.
The default host is localhost:8000
, and you may override the port number by setting the PORT environment variable.
The development server is Modern Web's server,
running in watch mode, so you can see code changes reloaded into the browser automatically.
Note the middleware in web-dev-server.config.mjs
that rewrites requests for the root so that dev/index.html
is served.
Testing
There are some tests for the portal
directive in src/portal.test.ts
. They use Modern Web's test runner and the Open Web Components @open-wc/testing
framework.
You can execute the tests by running npm run test
.
Contributing and Bug Reports
Currently, there is no standard procedure for contributing to this project. You are absolutely welcome to fork the repository and make a pull request, and to file issues if you encounter problems while using it.