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"> <script type="module">
import controllers from "./src/controller-registry.js" // Import nyooom to easily generate HTML nodes
import {html} from "https://cdn.jsdelivr.net/npm/nyooom/render.js" import {html} from "nyooom/render"
Object.defineProperty(HTMLElement.prototype, "controllers", { // The actual library
get() { return controllers.list(this) } 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) => { // Define a more complex filter for form elements to enable or disable filters
const before = element.style.color controllers.define("optional-filter", async (element, detach) => {
element.style.color = "red" const checkBox = html.div(html.label(
element.addEventListener("click", () => element.controllers.remove("red"), disconnected) html.input({
await disconnected type: "checkbox",
element.style.color = before checked: true,
}) input: ({target: {checked}}) => {
element
controllers.define("asterisk", async (element, disconnected) => { .querySelectorAll("input:not([type='checkbox'])")
const asterisk = html.span("*", {controller: ["red"]}) .forEach(input => controllers.list(input).toggle("filter", checked))
element.append(asterisk) }
await disconnected }),
asterisk.remove() 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> </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(" ")) this.#element.setAttribute(this.#attribute, [...set].join(" "))
} }
/** @param {String} name */
contains(name) { contains(name) {
return this.#set.has(name) return this.#set.has(name)
} }
/** @param {String} name */
add(name) { add(name) {
this.toggle(name, true) this.toggle(name, true)
} }
/** @param {String} name */
remove(name) { remove(name) {
this.toggle(name, false) this.toggle(name, false)
} }
/**
* @param {String} name
* @param {String} replacement
*/
replace(name, replacement) { replace(name, replacement) {
const set = this.#set const set = this.#set
if (set.has(name)) { if (set.has(name)) {
@ -46,6 +53,10 @@ export class ControllerList {
} }
} }
/**
* @param {String} name
* @param {Boolean} force
*/
toggle(name, force) { toggle(name, force) {
const set = this.#set const set = this.#set
if (force === true) { if (force === true) {
@ -83,29 +94,42 @@ export class ControllerRegistry {
/** @type {WeakMap<HTMLElement,Map<String,Object>>} */ /** @type {WeakMap<HTMLElement,Map<String,Object>>} */
#attached = new WeakMap() #attached = new WeakMap()
/** @type {Map<String,Set<HTMLElement>} */ /** @type {Map<String,Set<HTMLElement>>} */
#waiting = new Map() #waiting = new Map()
/** @type {Map<Strong,(HTMLElement, AbortSignal)=>void>} */ /** @type {Map<String,(element: HTMLElement, signal: AbortSignal)=>void>} */
#defined = new Map() #defined = new Map()
#attribute #attribute
/** @typedef {HTMLElement} Root */
/**
* @param {Root} root
* @param {String} attribute
*/
constructor(root, attribute="controller") { constructor(root, attribute="controller") {
this.#attribute = attribute this.#attribute = attribute
this.#observer.observe(root, {subtree: true, childList: true, attributes: true, attributeFilter: [attribute], attributeOldValue: false}) this.#observer.observe(root, {subtree: true, childList: true, attributes: true, attributeFilter: [attribute], attributeOldValue: false})
this.upgrade(root) this.upgrade(root)
} }
/**
* @param {Root} root
*/
upgrade(root) { upgrade(root) {
root.querySelectorAll(`[${this.#attribute}]`).forEach(element => this.#update(element)) root.querySelectorAll(`[${this.#attribute}]`).forEach(element => this.#update(element))
} }
/**
* @param {String} name
* @param {Function} callback
*/
define(name, callback) { define(name, callback) {
if (("function" == typeof callback) && callback.prototype) { if (("function" == typeof callback) && callback.prototype) {
callback = async (element, disconnected) => { callback = async (element, disconnected) => {
const {proxy, revoke} = Proxy.reevocable(element) const {proxy, revoke} = Proxy.revocable(element, {})
const controller = new callback(proxy) const controller = new callback(proxy, disconnected)
await disconnected await disconnected
revoke() revoke()
if ("detach" in controller) controller.detach(element) if ("detach" in controller) controller.detach(element)