Overview

Introduction

This page is an introduction to the ce-part-utils library. It provides a description of the library and acts as a reference for documentation and examples.

Library

The ce-part-utils library contains utilities for adding part and exportparts attributes to a custom HTML element, using classes, ids, tags and other element properties.

The library is intended to help with managing part assignment for custom elements that have a lot of shadow DOM child elements, and for custom elements that use child custom elements that are entirely private to the parent element.

To use the ce-part-utils functions, import each function individually from the library's file.

Provide your custom element's shadow root to the functions to assign parts to the children of those shadow roots.

Installation

This library is available as a single file that can be referenced using a <script> element. It can also be installed using any of the javascript package managers below:

npm bun pnpm deno yarn npm install ce-part-utils

Documentation

Library Documentation

Use the library documentation to learn about each of the features that ce-part-utils provides.

Follow the README link to view the library's repository page and get a look at the source directly.

Quick Reference

Quick Start Snippet

Copy the code from this example and paste it into your project to have a simple starting point for working with this library.

import { assignClassAndIdToPart } from 'ce-part-utils';

Quick Reference Tables

Use the tables, below, to reference the key identifiers used by this library, including enumerators, assignments, constants, and other "well-known" content.

Configs

Name Description Values
InputTypePartMap

Maps an <input>'s type to a string value that will be used as a part value for <input>s of that type.

Optional second parameter of the assignInputTypeToPart() function.

{
    button?:string;
    checkbox?:string;
    color?:string;
    date?:string;
    ["datetime-local"]?:string;
    email?:string;
    file?:string;
    hidden?:string;
    image?:string;
    month?:string;
    number?:string;
    password?:string;
    radio?:string;
    range?:string;
    reset?:string;
    search?:string;
    submit?:string;
    tel?:string;
    text?:string;
    time?:string;
    url?:string;
    week?:string;
    ["text-numeric"]:string = "number";
}
TagPartMap

Maps an element's tag to a string value that will be used as a part value for elements with that tag.

Tag names are lowercase. Can map any number or tags to part values.

Optional second parameter of the assignTagToPart() function.

{
    [HTMLElement tag]: string;
}
PartExportPartMap

Maps how an element's part values are assigned as exportparts values.

Optional third parameter of the assignPartsAsExportPartsAttribute() function.

{
    [part]: string;
}

Enumerators

This library does not expose any enumerators or enumerator-like objects.

Constants

Name Description Value
DEFAULT_ELEMENT_SELECTOR
The default selector value when querying for the decendants in a shadow root.
":not(slot,defs,g,rect,path,circle,ellipse,line,polygon,text,tspan,use,svg image,svg title,desc,template,template *)"

Assignments

This library does not use any values directly from source code to assign values, properties, or attributes. All assignments, if any, are sourced from library input or from configuration content.

Examples

These examples are provided as simple references for practical use cases.

For specific details and feature maps, see the documentation.

Assign id and class Attributes to part

These functions take a shadow root as a parameter and then query for any of their decendants that have id or class attributes.

For each element returned by the query, the assignClassAndIdToPart() function assigns the element's id attribute value, and each of its class values, to its part attribute.

To only assign id, or class attributes, use the assignIdToPart() or assignClassToPart() functions, respectively.

import { assignClassAndIdToPart, assignIdToPart, assignClassToPart } from 'https://cdn.jsdelivr.net/npm/ce-part-utils/+esm'; const COMPONENT_TAG_NAME = 'custom-element'; export class MyCustomElement extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); this.shadowRoot.innerHTML = `<div id="first-element" class="container"></div>` } connectedCallback() { assignClassAndIdToPart(this.shadowRoot); } } if(customElements.get(COMPONENT_TAG_NAME) == null) { customElements.define(COMPONENT_TAG_NAME, MyCustomElement); } custom-element::part(first-element) { background: oklch(36.107% 0.12345 256.044); width: 100px; height: 100px; } custom-element::part(container) { border: solid 5px oklch(16.107% 0.12345 256.044); border-radius: 2px; }

Assign Element Tags

This function takes a shadow root and an optional configuration object as parameters and then queries for all of its decendant nodes.

For each element returned by the query, the function assigns the element's tag to its part attribute.

Use the TagPartMap configuration object to map tags to other values for the part attribute.

import { assignTagToPart } from 'https://cdn.jsdelivr.net/npm/ce-part-utils/+esm'; const COMPONENT_TAG_NAME = 'custom-element'; export class MyCustomElement extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); this.shadowRoot.innerHTML = `<span></span><div></div>` } connectedCallback() { assignTagToPart(this.shadowRoot, { 'span': 'custom-part-name'}); } } if(customElements.get(COMPONENT_TAG_NAME) == null) { customElements.define(COMPONENT_TAG_NAME, MyCustomElement); } custom-element::part(custom-part-name) { background: oklch(16.107% 0.12345 256.044); display: block; width: 50px; height: 50px; border: solid 5px oklch(36.107% 0.12345 256.044); border-radius: 2px; } custom-element::part(div) { background: oklch(36.107% 0.12345 256.044); width: 100px; height: 100px; border: solid 5px oklch(16.107% 0.12345 256.044); border-radius: 2px; }

Forms and Inputs

The assignInputTypeToPart() function takes a shadow root and an optional configuration object as parameters and then queries for all of the child <input> elements.

For each element returned by the query, the function assigns the element's type attribute value to its part attribute.

Use the InputTypePartMap configuration object to map type values to other values for the part attribute.

import { assignInputTypeToPart } from 'https://cdn.jsdelivr.net/npm/ce-part-utils/+esm'; const COMPONENT_TAG_NAME = 'custom-element'; export class MyCustomElement extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); this.shadowRoot.innerHTML = `<form> <input type="text" placeholder="Custom Input" /> <input type="text" inputmode="numeric" placeholder="Custom Input" /> <input type="checkbox" /> <input type="submit" disabled /> </form>` } connectedCallback() { assignInputTypeToPart(this.shadowRoot, { 'submit': 'button' }); } } if(customElements.get(COMPONENT_TAG_NAME) == null) { customElements.define(COMPONENT_TAG_NAME, MyCustomElement); } custom-element::part(text) { border: solid 2px oklch(36.107% 0.12345 256.044); } custom-element::part(checkbox) { appearance: none; width: 15px; height: 15px; border: solid 2px oklch(36.107% 0.12345 256.044); } custom-element::part(button) { border: solid 2px oklch(36.107% 0.12345 256.044); }

Assigning parts to the exportparts Attribute

Converting part values to exportparts values can be done with the assignPartsAsExportPartsAttribute() function.

This type of part exporting is useful for exposing the parts of a "nested" element. Unlike a "child" element, a "nested" element is one that is owned by a custom element's shadow root. That nested element's parts would be inaccessible if they were not assigned to the element's exportparts attribute.

For each element in the shadow root used as the function's first parameter, the function assigns the element's part attribute values to its parent's exportparts attribute. Elements that have had their parts added to the exportparts attribute of their parents can be selected using a ::part() selector.

By default, the function will not add newlines to the exportparts attribute value. For debugging purposes, though, that is an option that can be toggled with the function's second parameter.

import { assignPartsAsExportPartsAttribute } from 'https://cdn.jsdelivr.net/npm/ce-part-utils/+esm'; const COMPONENT_TAG_NAME_A = 'custom-element-a'; const COMPONENT_TAG_NAME_B = 'custom-element-b'; export class MyCustomElementA extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); this.shadowRoot.innerHTML = `<custom-element-b></custom-element-b>` } connectedCallback() { customElements.whenDefined('custom-element-b') .then(() => { const targetShadowRoot = this.shadowRoot.querySelector('custom-element-b').shadowRoot; assignPartsAsExportPartsAttribute(targetShadowRoot, false, { 'unsanitized-part': 'sanitized-part' }); }); } } export class MyCustomElementB extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); this.shadowRoot.innerHTML = `<div part="exported-part-name unsanitized-part">Nested Element</div>` } connectedCallback() { } } if(customElements.get(COMPONENT_TAG_NAME_A) == null) { customElements.define(COMPONENT_TAG_NAME_A, MyCustomElementA); } if(customElements.get(COMPONENT_TAG_NAME_B) == null) { customElements.define(COMPONENT_TAG_NAME_B, MyCustomElementB); } custom-element-a::part(exported-part-name) { background: oklch(36.107% 0.12345 256.044); width: 100px; height: 100px; padding: 5px; } custom-element-a::part(sanitized-part) { color: white; font-family: sans-serif; border: solid 5px oklch(16.107% 0.12345 256.044); border-radius: 2px; }

Questions & Answers

Why would I want to assign ids, classes, tags, etc as part values?
Simplified: To allow custom styling.
Details:

Parts are weird features in the custom element specification. I can appreciate the intention of the public/private split for this kind of thing, but with styling features like display: grid, and the need for developers to be able to design the modules that make up their apps, there never seems to be any reason to disallow style overrides.

To allow external styling of any arbitrary element in the shadow DOM, parts are added. Unfortunately, parts are limited. Not only in the external selectors (no cascading), but also in the internal selectors. An acutal "part" selector doesn't exist. Internally, the only way to select them is by using the attribute selector. Which is limiting because you would have to use wildcard selectors and chaining in order to get a simple dual-part selection.

This library makes it easy to use ids and classes with internal styling and scripting, and then systematically apply those attribute values as part values.

Should I use this for developing custom elements (web components)?
Simplified: Only if your custom element is particularly complex, or is an entire web app.
Details:

Generally speaking, this library acts more like a kludge or a hack than a trustworthy solution. Assigning parts should be a very targeted and very deliberate process. It is meant to differentiate the internal parts of your design from the external. It is unlikely that every id and class would need to be assigned as a part for every element.

On a more practical side, the library is not very comprehensive. It does not account for the very common case of inserting content. You can query every element and assign parts, and you can do that as many times as you like since parts act like sets and disallow duplicates. But if you add a new element to the DOM, it is pretty inefficient to query for every element just to apply parts to a single new element.

So you end up having to run the assignment once, and then having to manage each new element's parts anyway. It just is not very useful for most custom element development.

But there are exceptions to every rule and sometimes lots of shadow DOM child elements are unavoidable, or "nested" custom elements need to expose the nested parts for external styling. In those cases, it is better to have a single library that can be referenced, rather than to recreate the methods for each custom element.

What is this library for?
Simplified: Shimming in styling functionality for apps that are built as web components.
Details

If a particularly intricate custom element requires a lot of elements, manual assignment can be too inexact to be useful. In that case, this library makes sure that each id and class are ported to parts identically. It limits the places that require changes later.

Alternatively, if one custom element is nested inside of another custom element, assigning the parts and then copying those parts to the exportparts attribute would be just as inexact. So if you have completely nested custom elements (custom elements that are never used by implementing developers, only the parent custom element), then this library can be useful in that situation, too.