diff --git a/docs/docs/usage.md b/docs/docs/usage.md index aea75ebc4..651ade28e 100644 --- a/docs/docs/usage.md +++ b/docs/docs/usage.md @@ -8,6 +8,30 @@ Web Awesome components are just regular HTML elements, or [custom elements](http If you're new to custom elements, often referred to as "web components," this section will familiarize you with how to use them. +## Awaiting Registration + +Unlike traditional frameworks, custom elements don't have a centralized initialization phase. This means you need to verify that a custom element has been properly registered before attempting to interact with its properties or methods. + +You can use the [`customElements.whenDefined()`](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/whenDefined) method to ensure a specific component is ready: + +```ts +await customElements.whenDefined('wa-button'); + +// is ready to use! +const button = document.querySelector('wa-button'); +``` + +When working with multiple components, checking each one individually can become tedious. For convenience, Web Awesome provides the `allDefined()` function which automatically detects and waits for all Web Awesome components in the DOM to be initialized before resolving. + +```ts +import { allDefined } from '/dist/webawesome.js'; + +// Waits for all Web Awesome components in the DOM to be registered +await allDefined(); + +// All Web Awesome components on the page are ready! +``` + ## Attributes & Properties Many components have properties that can be set using attributes. For example, buttons accept a `size` attribute that maps to the `size` property which dictates the button's size. diff --git a/src/utilities/defined.ts b/src/utilities/defined.ts new file mode 100644 index 000000000..277693085 --- /dev/null +++ b/src/utilities/defined.ts @@ -0,0 +1,64 @@ +interface AllDefinedOptions { + /** + * A callback that accepts a custom element tag name and returns `true` if the custom element should be defined before + * resolving or `false` to skip it. The tag name is always in lowercase. + */ + match: (tagName: string) => boolean; + + /** + * To wait for additional custom elements that may not be on the page when the function is called, provide them here. + */ + additionalElements: string | string[]; + + /** + * The root in which to look for custom elements. Defaults to `document`. By design, shadow roots are not traversed, + * but you can call this function and set `root` to a custom element's shadow root if needed. + */ + root: Document | ShadowRoot; +} + +/** + * Waits for custom elements that are currently on the page to be registered before resolving. This is sugar for + * awaiting `customElements.whenDefined()` multiple times. By default, the function waits for all undefined Web Awesome + * elements, but you can pass a custom match function to wait for other custom elements instead. + * + * The function returns with `Promise.all()`, so any loading errors will cause it to reject. Make sure you handle errors + * accordingly using a try/catch block or a `.catch()`. + * + * @example + * // Wait for Web Awesome elements + * await allDefined(); + * + * // Wait for all custom elements that start with `foo-` as well as the `` element + * await allDefined({ + * match: tagName => tagName.startsWith('foo-'), + * additionalElements: ['bar-button', 'baz-dialog'] + * }); + */ +export async function allDefined(options?: Partial) { + const opts: AllDefinedOptions = { + match: tagName => tagName.startsWith('wa-'), + additionalElements: [], + root: document, + ...options, + }; + + // Ensure additional elements is an array + const additionalElements = Array.isArray(opts.additionalElements) + ? opts.additionalElements + : [opts.additionalElements]; + + // Discover undefined elements in the document + const undefinedElements = [...opts.root.querySelectorAll(':not(:defined)')] + .map(el => el.localName) + .filter((tag, index, arr) => arr.indexOf(tag) === index) // make it unique + .filter(tag => opts.match(tag)); // make sure it matches + + const tagsToAwait = [...undefinedElements, ...additionalElements]; + + // Wait for all to be registered + await Promise.all(tagsToAwait.map(tag => customElements.whenDefined(tag))); + + // Wait a cycle for the first update + await new Promise(requestAnimationFrame); +} diff --git a/src/webawesome.ts b/src/webawesome.ts index 60e6562bc..287f9d02a 100644 --- a/src/webawesome.ts +++ b/src/webawesome.ts @@ -1,6 +1,7 @@ export { registerIconLibrary, unregisterIconLibrary } from './components/icon/library.js'; export { discover, preventTurboFouce, startLoader, stopLoader } from './utilities/autoloader.js'; export { getBasePath, getKitCode, setBasePath, setKitCode } from './utilities/base-path.js'; +export { allDefined } from './utilities/defined.js'; export { registerTranslation } from './utilities/localize.js'; // Utilities