Update example and fix bug with OO controllers
This commit is contained in:
parent
341c424941
commit
8c243adb8d
2 changed files with 84 additions and 22 deletions
74
example.html
74
example.html
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue