npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

face-up

v0.0.4

Published

A Custom Element Feature that Adds Form Associated Behavior to a Custom Element

Readme

face-up

Form Associated Custom Element — Uplifted.

A Custom Element Feature that adds Form Associated behavior to a custom element via the ElementInternals API.

What it does

FaceUp enables any custom element to fully participate in HTML forms — matching the capabilities described in More Capable Form Controls:

  • Form submission — The control's value is automatically submitted with the form via setFormValue().
  • Form validation — The control participates in constraint validation with :valid/:invalid pseudo-classes.
  • Form reset — The control resets to its default state when the form is reset.
  • Form state restoration — The browser can restore the control's state after navigation or restart.
  • Disabled state — The control responds to disabled attribute changes on itself or ancestor <fieldset>.
  • Label association — The control can be labeled with <label> elements.

Usage

import 'assign-gingerly/assignFeatures.js';
import { FaceUp } from 'face-up/FaceUp.js';

class MyInput extends HTMLElement {
    #internals;

    static supportedFeatures = {
        faceUp: {
            fallbackSpawn: FaceUp,
            callbackForwarding: [
                'formDisabledCallback',
                'formResetCallback',
                'formStateRestoreCallback'
            ],
            getSharedContext(instance) {
                return {
                    internals: instance.#internals
                };
            }
        }
    };

    constructor() {
        super();
        this.#internals = this.attachInternals();
    }
}

// assignFeatures calls FaceUp.onAssigned which sets static formAssociated = true
await customElements.assignFeatures(MyInput, {
    faceUp: { spawn: FaceUp }
});

customElements.define('my-input', MyInput);

That's it. No propagator, no manual callback forwarding, no event wiring.

  • static formAssociated = true is set automatically by FaceUp.onAssigned.
  • Form lifecycle callbacks are forwarded automatically via callbackForwarding.
  • Property changes sync to ElementInternals via the feature's setters.

Setting Values

Because faceUp is a getter-only property, assignGingerly merges directly into the feature instance. Set properties however you like:

// Direct property access
el.faceUp.value = 'hello';

// Via assignGingerly (merges into the feature instance)
el.assignGingerly({ faceUp: { value: 'hello', required: true } });

Both approaches trigger the setter, which calls setFormValue() on the internals automatically.

Validation

Set a custom validation message:

el.faceUp.validationMessage = 'This value is not allowed.';

Or use the lower-level setValidity() API:

el.faceUp.setValidity({ rangeUnderflow: true }, 'Value must be at least 0.');

Clear validation:

el.faceUp.validationMessage = '';
// or
el.faceUp.setValidity({});

Built-in required validation is automatic — if required is true and value is null or '', the control is marked invalid with valueMissing.

Form State Restoration

Pass a state parameter alongside value to enable proper form restoration:

el.faceUp.state = 'palette/#7fff00';
el.faceUp.value = '#7fff00';

The state is stored internally by the browser and passed back to formStateRestoreCallback when the form is restored.

API

Properties

| Property | Type | Description | |----------|------|-------------| | value | string \| File \| FormData \| null | The submittable form value | | state | string \| File \| FormData \| null | Internal state for form restoration | | disabled | boolean | Whether the control is disabled | | required | boolean | Whether the control requires a value | | validationMessage | string | Custom validation error message | | form | HTMLFormElement \| null | The associated form (read-only) | | validity | ValidityState \| null | The validity state (read-only) | | willValidate | boolean | Whether the control will be validated (read-only) |

Methods

| Method | Returns | Description | |--------|---------|-------------| | checkValidity() | boolean | Returns true if the control is valid | | reportValidity() | boolean | Shows browser validation UI if invalid | | setValidity(flags, message?, anchor?) | void | Sets custom validity flags |

Form Lifecycle Callbacks (forwarded via callbackForwarding)

| Callback | Description | |----------|-------------| | formDisabledCallback(disabled) | Called when disabled state changes | | formResetCallback() | Called when the form is reset | | formStateRestoreCallback(state, mode) | Called when browser restores form state |

These are forwarded automatically by assign-gingerly's callbackForwarding mechanism — the host element does not need to implement them manually.

How it integrates

The key insight: because assignFeatures installs faceUp as a getter-only property on the prototype, assignGingerly automatically merges object values into the existing feature instance rather than replacing it. This means:

  1. The feature's setters fire on every property assignment.
  2. Each setter syncs to ElementInternals immediately.
  3. No event bus, no propagator, no intermediate layer — just property access.
// All of these trigger the setter → sync to internals:
el.faceUp.value = 'x';
el.assignGingerly({ faceUp: { value: 'x' } });

Requirements

The host custom element must:

  1. Call this.attachInternals() in its constructor
  2. Pass the internals via getSharedContext in supportedFeatures
  3. Include the form lifecycle callbacks in callbackForwarding

Both static formAssociated = true and form lifecycle callback forwarding are handled automatically — no manual boilerplate needed in the host element.

Roundabout Integration

For projects using roundabout, face-up exports merge rules and attribute patterns that wire up property forwarding automatically:

import { faceUpMerges, faceUpWithAttrs } from 'face-up/RAConfig.mjs';

export const raConfig = {
    //optional
    propagate: ['value', 'disabled', 'required', /* ... */],
    merges: [
        ...faceUpMerges,
        // ...your other merges
    ]
};

export const cef = {
    features: {
        roundabout: {
            customData: { raConfig },
            withAttrs: {
                ...faceUpWithAttrs,
                // ...your other attrs
            }
        }
    }
};

Merges

When value, state, disabled, required, or validationMessage change on the view model, roundabout forwards them to the faceUp feature's setters, which sync to ElementInternals automatically.

If you use a different feature key, call the function form:

import { getFaceUpMerges } from 'face-up/RAConfig.mjs';

merges: [...getFaceUpMerges('formControl')]

Attribute Patterns (faceUpWithAttrs)

Provides withAttrs configuration for parsing form-associated attributes from markup. All entries are sourceOfTruth: true with appropriate valIfNull defaults:

| Attribute | Property | Type | Default | |-----------|----------|------|---------| | value | value | String | null | | disabled | disabled | Boolean | false | | required | required | Boolean | false | | validation-message | validationMessage | String | '' |

Dev

npm install
npm run serve
# Open http://localhost:8000/tests/test1.html