Update example and fix bug with OO controllers

This commit is contained in:
Talia 2025-08-09 20:37:55 +02:00
parent 341c424941
commit 8c243adb8d
Signed by: darkwiiplayer
GPG key ID: 7808674088232B3E
2 changed files with 84 additions and 22 deletions

View file

@ -1,25 +1,63 @@
<script type="importmap">
{
"imports": {
"nyooom/render": "https://cdn.jsdelivr.net/npm/nyooom/render.js",
"controller-registry": "./src/controller-registry.js"
}
}
</script>
<script type="module">
import controllers from "./src/controller-registry.js"
import {html} from "https://cdn.jsdelivr.net/npm/nyooom/render.js"
// Import nyooom to easily generate HTML nodes
import {html} from "nyooom/render"
Object.defineProperty(HTMLElement.prototype, "controllers", {
get() { return controllers.list(this) }
// The actual library
import controllers from "controller-registry"
// Define a basic word filter controller for input elements
controllers.define("filter", (element, detach) => {
element.addEventListener("input", event => {
if (element.value.toLowerCase() === "scunthorpe") {
element.value = ""
alert("Evil word detected, deleting input permanently!")
}
}, detach) // Alternatively: {signal: detach.signal}
})
controllers.define("red", async (element, disconnected) => {
const before = element.style.color
element.style.color = "red"
element.addEventListener("click", () => element.controllers.remove("red"), disconnected)
await disconnected
element.style.color = before
})
controllers.define("asterisk", async (element, disconnected) => {
const asterisk = html.span("*", {controller: ["red"]})
element.append(asterisk)
await disconnected
asterisk.remove()
// Define a more complex filter for form elements to enable or disable filters
controllers.define("optional-filter", async (element, detach) => {
const checkBox = html.div(html.label(
html.input({
type: "checkbox",
checked: true,
input: ({target: {checked}}) => {
element
.querySelectorAll("input:not([type='checkbox'])")
.forEach(input => controllers.list(input).toggle("filter", checked))
}
}),
html.span("Enable filter")
))
element.append(checkBox)
// Detach is a promise, so we can just pause the function until
// it's time for cleanup to happen
await detach
checkBox.remove()
})
</script>
<h1 controller="asterisk">Hello, World!</h1>
<style>
[controller~="filter"] {
outline: 3px solid #82f6;
border-radius: .2em;
}
</style>
<form controller="optional-filter">
<div>
<label>
<span>Where are you from?</span>
<input controller="filter"></input>
</label>
</div>
</form>

View file

@ -22,18 +22,25 @@ export class ControllerList {
this.#element.setAttribute(this.#attribute, [...set].join(" "))
}
/** @param {String} name */
contains(name) {
return this.#set.has(name)
}
/** @param {String} name */
add(name) {
this.toggle(name, true)
}
/** @param {String} name */
remove(name) {
this.toggle(name, false)
}
/**
* @param {String} name
* @param {String} replacement
*/
replace(name, replacement) {
const set = this.#set
if (set.has(name)) {
@ -46,6 +53,10 @@ export class ControllerList {
}
}
/**
* @param {String} name
* @param {Boolean} force
*/
toggle(name, force) {
const set = this.#set
if (force === true) {
@ -83,29 +94,42 @@ export class ControllerRegistry {
/** @type {WeakMap<HTMLElement,Map<String,Object>>} */
#attached = new WeakMap()
/** @type {Map<String,Set<HTMLElement>} */
/** @type {Map<String,Set<HTMLElement>>} */
#waiting = new Map()
/** @type {Map<Strong,(HTMLElement, AbortSignal)=>void>} */
/** @type {Map<String,(element: HTMLElement, signal: AbortSignal)=>void>} */
#defined = new Map()
#attribute
/** @typedef {HTMLElement} Root */
/**
* @param {Root} root
* @param {String} attribute
*/
constructor(root, attribute="controller") {
this.#attribute = attribute
this.#observer.observe(root, {subtree: true, childList: true, attributes: true, attributeFilter: [attribute], attributeOldValue: false})
this.upgrade(root)
}
/**
* @param {Root} root
*/
upgrade(root) {
root.querySelectorAll(`[${this.#attribute}]`).forEach(element => this.#update(element))
}
/**
* @param {String} name
* @param {Function} callback
*/
define(name, callback) {
if (("function" == typeof callback) && callback.prototype) {
callback = async (element, disconnected) => {
const {proxy, revoke} = Proxy.reevocable(element)
const controller = new callback(proxy)
const {proxy, revoke} = Proxy.revocable(element, {})
const controller = new callback(proxy, disconnected)
await disconnected
revoke()
if ("detach" in controller) controller.detach(element)