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

controller-registry

v0.0.3

Published

A prototype for an alternative to custom elements.

Readme

Controller-Registry

A prototype for an alternative to custom elements.

// example.js
import controllers from "controller-registry"

controllers.define("clickable", (element, detached) => {
    element.addEventListener("click", () => {
        alert("The element has been clicked!")
    }, detached)
})
<script type="module" src="example.js"></script>

<button controller="clickable">Try clicking this button</button>

Concept

Similar to a custom element, a controller defines custom behaviours for HTML elements and is managed automatically by a registry.

Like custom built-in elements, controllers are controlled via an attribute on the element.

Unlike custom elements, controllers are external objects or functions that are attached to an object, meaning several different controllers can be managing the same element at once, and the class of the element does not change in the process.

Controllers can be added and removed as necessary.

API

The library exports a global ControllerRegistry attached to the document root with a similar API to the CustomElementRegistry class.

Controllers can be registered under any name as either a callback which gets called when the controller is added to an element or a constructor which gets called with new and passed a revokable proxy to the element.

controllers.define("showcase", class ShowcaseController {
    constructor(element, detached) {
        this.method(element)
        // detached promise is passed here too for convenience,
        // but the `detached` method is the preferred place
        // to put cleanup code.
    }

    method(element) {
        console.log("Calling method on:", element)
    }

    detached(element) {
        // Cleanup if necessary
    }
})

Note that only class controllers are given a revocable proxy: this is because their stateful nature and suitability for more complex handling makes them more likely candidates to retain references to the target past their detachment.

For complex function controllers, this can easily be done manually using Proxy.revocable(element, {}).

This behaviour might change in the future.

If the controller is a function, the second argument is a promise that resolves to the element when the controller is removed again. This promise has an additional property "signal" which returns an AbortSignal. This means the promise can be passed directly as the third argument to addEventListener function calls.

controllers.define("showcase", async (element, detached) => {
    console.log("Attached to element:", element)
    console.log("Detached promise:", detached)
    console.log("Detached signal:", detached.signal)
    element === await detached
    console.log("Detached from element:", element)
}

The registry also exposes a list function which, given an element, returns an object similar to a DomTokenList for easier management of the controller list.

The controller attribute is a space-separated list of controller names as registered in the registry.

Interactions between controllers

There is no direct way for controllers to interact with each other, as they should be mostly independent.

When signalling is needed, events are the way to go; when data needs to be shared, the element's dataset or a more semantic attribute should be used.

For anything even more complex, a custom element or a higher level component framework might be the better solution.