Compare commits

...

14 Commits

Author SHA1 Message Date
Talia 9983c57f23 Refactor 2024-02-01 14:00:26 +01:00
Talia 017cd9fbd2 Fix and refactor skooma.js 2024-02-01 14:00:26 +01:00
Talia 515ba550cf Fix bug in composed state 2024-02-01 14:00:26 +01:00
Talia 8cca0813d9 Refactor state module 2024-02-01 14:00:26 +01:00
Talia 97a89627b9 Make domLense error on non-element targets 2024-02-01 14:00:26 +01:00
Talia 904a5daf7c Stop removing "on" prefix from event names in skooma 2024-02-01 14:00:26 +01:00
Talia b25a3013d2 Change skooma to use svelte store contract
Since States are now also valid svelte stores, skooma now uses that API
to decide whether something is an observable state and to interact with
it.
2024-02-01 14:00:26 +01:00
Talia e293594edb Refactor skooma.js 2024-02-01 14:00:26 +01:00
Talia e75d90073e Add methods param to component generator 2024-02-01 14:00:26 +01:00
Talia 38b2127920 Add get/set filters to forward state & remove cache
Forwarded states are no longer cached, because caching would have to
happen individually for every attribute and methods combination.
2024-02-01 14:00:26 +01:00
Talia 3f838821e4 Add support for child generators as skooma args
Function arguments to skooma generators are now only treated as
initialisers if they take at least one named argument. Otherwise they
are called and their return value is evaluated as their replacement.

The process is recursive and can happen repeatedly for functions that
themselves return functions.
2024-02-01 14:00:26 +01:00
Talia 75fc1a7ce7 Refactor get/set to use `arguments` instead of ... 2024-02-01 13:35:57 +01:00
Talia 71e086cf04 Change domLense method semantics 2024-02-01 13:35:57 +01:00
Talia 2db5871cc9 Refactor API and Code
* Unified more features in SimpleState
* Flip "defer" into "synchronous" option
* Remove fallback from forward states
2024-02-01 13:35:57 +01:00
3 changed files with 279 additions and 258 deletions

View File

@ -12,8 +12,6 @@ class ChildObserver extends MutationObserver {
} }
} }
const childObserver = new ChildObserver()
export const lense = (methods, extra) => { export const lense = (methods, extra) => {
if (extra) return lense(extra)(methods) if (extra) return lense(extra)(methods)
@ -24,12 +22,12 @@ export const lense = (methods, extra) => {
} else if (prop === Symbol.iterator) { } else if (prop === Symbol.iterator) {
return function*() { return function*() {
for (const child of target.children) { for (const child of target.children) {
yield methods.get(child) yield methods.get.call(child)
} }
} }
} else if (prop.match?.call(prop, /^[0-9]+$/)) { } else if (prop.match?.call(prop, /^[0-9]+$/)) {
const child = target.children[prop] const child = target.children[prop]
if (child) return methods.get(child) if (child) return methods.get.call(child)
return child return child
} else { } else {
return Array.prototype[prop] return Array.prototype[prop]
@ -39,7 +37,7 @@ export const lense = (methods, extra) => {
if (prop.match?.call(prop, /^[0-9]+$/)) { if (prop.match?.call(prop, /^[0-9]+$/)) {
const child = target.children[prop] const child = target.children[prop]
if (child) { if (child) {
methods.set(child, value) methods.set.call(child, value)
return true return true
} else { } else {
for (let i = target.children.length; i < Number(prop); i++) { for (let i = target.children.length; i < Number(prop); i++) {
@ -47,8 +45,8 @@ export const lense = (methods, extra) => {
} }
const element = methods.new(value) const element = methods.new(value)
target.appendChild(element) target.appendChild(element)
if (methods.get(element) !== value) if (methods.get.call(element) !== value)
methods.set(element, value) methods.set.call(element, value)
return true return true
} }
} else if (prop == "length") { } else if (prop == "length") {
@ -71,14 +69,8 @@ export const lense = (methods, extra) => {
} }
return element => { return element => {
const proxy = new Proxy(element, traps) if (!(element instanceof Element)) throw(new Error("Creating domLense on non-element"))
return new Proxy(element, traps)
if (methods.event) childObserver.observe(element)
if (typeof methods.event === "function") element.addEventListener("change", event => {
methods.event(proxy, element, event.detail)
})
return proxy
} }
} }

137
skooma.js
View File

@ -1,37 +1,30 @@
/*
A functional HTML generation library.
Example:
html.label(
html.span("Delete everything", {class: ["warning", "important"]}),
html.button("Click", {onClick: e => document.body.innerHTML=""}),
)
or
html.ul([1, 2, 3, 4, 5].map(x => html.li(x)), {class: "numbers"})
*/
// Keep a referee alive until a referrer is collected // Keep a referee alive until a referrer is collected
const weakReferences = new WeakMap() const weakReferences = new WeakMap()
const untilDeathDoThemPart = (referrer, reference) => { const untilDeathDoThemPart = (referrer, reference) => {
if (!weakReferences.has(referrer)) { if (!weakReferences.has(referrer)) weakReferences.set(referrer, new Set())
weakReferences.set(referrer, new Set())
}
weakReferences.get(referrer).add(reference) weakReferences.get(referrer).add(reference)
} }
// Like AbortController, but resets after each abort
class MultiAbortController {
#controller = new AbortController()
get signal() { return this.#controller.signal }
abort() { this.#controller.abort(); this.#controller = new AbortController() }
}
export const empty = Symbol("Explicit empty argument for Skooma") export const empty = Symbol("Explicit empty argument for Skooma")
const keyToPropName = key => key.replace(/^[A-Z]/, a => "-"+a).replace(/[A-Z]/g, a => '-'+a.toLowerCase()) const snakeToCSS = key => key.replace(/^[A-Z]/, a => "-"+a).replace(/[A-Z]/g, a => '-'+a.toLowerCase())
const insertStyles = (rule, styles) => { const insertStyles = (rule, styles) => {
for (const [key, value] of Object.entries(styles)) for (const [key, value] of Object.entries(styles))
if (typeof value == "undefined") if (typeof value == "undefined")
rule.removeProperty(keyToPropName(key)) rule.removeProperty(snakeToCSS(key))
else else
rule.setProperty(keyToPropName(key), value.toString()) rule.setProperty(snakeToCSS(key), value.toString())
} }
const parseAttribute = (attribute) => { const processAttribute = (attribute) => {
if (typeof attribute == "string" || typeof attribute == "number") if (typeof attribute == "string" || typeof attribute == "number")
return attribute return attribute
else if (attribute && "join" in attribute) else if (attribute && "join" in attribute)
@ -50,56 +43,54 @@ const getCustom = args => args.reduce(
,undefined ,undefined
) )
const isElement = object => HTMLElement.prototype.isPrototypeOf(object) export const isReactive = object => object
&& typeof object == "object"
const isReactive = object => object && !(object instanceof HTMLElement)
&& (typeof object == "object") && object.subscribe
&& ("addEventListener" in object)
&& ("value" in object)
const toChild = arg => { const toChild = arg => {
if (typeof arg == "string" || typeof arg == "number") { if (typeof arg == "string" || typeof arg == "number")
return document.createTextNode(arg) return document.createTextNode(arg)
} else if (isElement(arg)) { else if (arg instanceof HTMLElement)
return arg return arg
} else if (isReactive(arg)) { else if (isReactive(arg))
return reactiveChild(arg) return reactiveChild(arg)
}
} }
const reactiveChild = reactive => { const reactiveChild = reactive => {
const ref = new WeakRef(toChild(reactive.value)) let ref
reactive.addEventListener("change", () => { const abort = reactive.subscribe(value => {
const value = ref.deref() if (ref && !ref.deref()) return abort()
if (value) const child = toChild(value) ?? document.createComment("Placeholder for reactive content")
value.replaceWith(reactiveChild(reactive)) untilDeathDoThemPart(child, reactive)
}, {once: true}) if (ref) ref.deref().replaceWith(child)
untilDeathDoThemPart(ref.deref(), reactive) ref = new WeakRef(child)
})
return ref.deref() return ref.deref()
} }
const specialAttributes = { const specialAttributes = {
value: { value: {
get: element => element.value, get() { return this.value },
set: (element, value) => { set(value) {
element.setAttribute("value", value) this.setAttribute("value", value)
element.value = value this.value = value
}, },
hook: (element, callback) => { element.addEventListener("input", callback) } hook(callback) { this.addEventListener("input", callback) }
}, },
style: { style: {
set: (element, value) => { insertStyles(element.style, value) } set(value) { insertStyles(this.style, value) }
}, },
dataset: { dataset: {
set: (element, value) => { set(value) {
for (const [attribute2, value2] of Object.entries(value)) { for (const [attribute2, value2] of Object.entries(value)) {
element.dataset[attribute2] = parseAttribute(value2) this.dataset[attribute2] = processAttribute(value2)
} }
} }
}, },
shadowRoot: { shadowRoot: {
set: (element, value) => { set(value) {
parseArgs((element.shadowRoot || element.attachShadow({mode: "open"})), null, value) processArgs((this.shadowRoot || this.attachShadow({mode: "open"})), value)
} }
} }
} }
@ -109,59 +100,57 @@ const setAttribute = (element, attribute, value, cleanupSignal) => {
if (isReactive(value)) if (isReactive(value))
setReactiveAttribute(element, attribute, value) setReactiveAttribute(element, attribute, value)
else if (typeof value === "function") else if (typeof value === "function")
element.addEventListener(attribute.replace(/^on[A-Z]/, x => x.charAt(x.length-1).toLowerCase()), value, {signal: cleanupSignal}) element.addEventListener(attribute, value, {signal: cleanupSignal})
else if (special) { else if (special?.set)
special.set(element, value) special.set.call(element, value)
}
else if (value === true) else if (value === true)
{if (!element.hasAttribute(attribute)) element.setAttribute(attribute, '')} {if (!element.hasAttribute(attribute)) element.setAttribute(attribute, '')}
else if (value === false) else if (value === false)
element.removeAttribute(attribute) element.removeAttribute(attribute)
else { else {
element.setAttribute(attribute, parseAttribute(value)) element.setAttribute(attribute, processAttribute(value))
} }
} }
const setReactiveAttribute = (element, attribute, reactive, abortController) => { // (Two-way) binding between an attribute and a state container
const setReactiveAttribute = (element, attribute, reactive) => {
untilDeathDoThemPart(element, reactive) untilDeathDoThemPart(element, reactive)
const multiAbort = new MultiAbortController()
if (abortController) abortController.abort() let old
abortController = new AbortController() reactive.subscribe(value => {
old = value
const ref = new WeakRef(element) multiAbort.abort()
setAttribute(element, attribute, reactive.value, abortController.signal) setAttribute(element, attribute, value, multiAbort.signal)
})
reactive.addEventListener("change", () => {
const element = ref.deref()
if (element)
setReactiveAttribute(element, attribute, reactive, abortController)
}, {once: true})
const special = specialAttributes[attribute] const special = specialAttributes[attribute]
if (special?.hook) { if (special?.hook && reactive.set) {
special.hook(element, () => { special.hook.call(element, () => {
const value = special.get(element, attribute) const value = special.get.call(element, attribute)
if (value != reactive.value) reactive.value = value if (value != old) reactive.set(value)
}) })
} }
} }
const parseArgs = (element, before, ...args) => { const processArgs = (element, ...args) => {
if (element.content) element = element.content if (element.content) element = element.content
for (const arg of args) if (arg !== empty) { for (const arg of args) if (arg !== empty) {
if (arg instanceof Array) {
processArgs(element, ...arg)
} else {
const child = toChild(arg) const child = toChild(arg)
if (child) if (child)
element.insertBefore(child, before) element.append(child)
else if (arg === undefined || arg == null) else if (arg === undefined || arg == null)
console.warn(`An argument of type ${typeof arg} has been ignored`, element) console.warn(`An argument of type ${typeof arg} has been ignored`, element)
else if (typeof arg == "function" && arg.length == 0)
processArgs(element, arg())
else if (typeof arg == "function") else if (typeof arg == "function")
arg(element) arg(element)
else if ("length" in arg)
parseArgs(element, before, ...arg)
else else
for (const key in arg) for (const key in arg)
setAttribute(element, key, arg[key]) setAttribute(element, key, arg[key])
} }
}
} }
const node = (name, args, options) => { const node = (name, args, options) => {
@ -174,7 +163,7 @@ const node = (name, args, options) => {
element = document.createElementNS(options.xmlns, name, opts) element = document.createElementNS(options.xmlns, name, opts)
else else
element = document.createElement(name, opts) element = document.createElement(name, opts)
parseArgs(element, null, args) processArgs(element, args)
return element return element
} }

360
state.js
View File

@ -5,82 +5,56 @@ const kebabToCamel = string => string.replace(/([a-z])-([a-z])/g, (_, a, b) => a
export class ChangeEvent extends Event { export class ChangeEvent extends Event {
#final #final
#values
constructor(...changes) { constructor(...changes) {
super('change') super('change')
this.changes = changes this.changes = changes
} }
get values() {
if (!this.#values) {
const values = new Map()
for (const {property, from, to} of this.changes) {
let list = values.get(property)
if (!list) {
list = [from]
values.set(property, list)
}
list.push(to)
}
this.#values = values
}
return this.#values
}
get final() { get final() {
if (!this.#final) { if (!this.#final) {
this.#final = new Map(this.changes) this.#final = new Map()
for (const [property, list] of this.values) {
if (list[0] !== list[list.length-1]) {
this.#final.set(property, list[list.length-1])
}
}
} }
return this.#final return this.#final
} }
} }
export class SimpleState extends EventTarget {} export class SimpleState extends EventTarget {
#synchronous
export class MapStorage extends Storage {
#map = new Map()
key(index) {
return [...this.#map.keys()][index]
}
getItem(keyName) {
if (this.#map.has(keyName))
return this.#map.get(keyName)
else
return null
}
setItem(keyName, keyValue) {
this.#map.set(keyName, String(keyValue))
}
removeItem(keyName) {
this.#map.delete(keyName)
}
clear() {
this.#map.clear()
}
}
export class State extends SimpleState {
#target
#options
#queue #queue
#forwardCache
#abortController
#nested = new Map() #nested = new Map()
#weakRef = new WeakRef(this) #weakRef = new WeakRef(this)
#abortController = new AbortController
static isState(object) { return SimpleState.prototype.isPrototypeOf(object) } constructor({synchronous, methods}={}) {
constructor(target={}, options={}) {
super() super()
this.#synchronous = !!synchronous
this.#abortController = new AbortController
abortRegistry.register(this, this.#abortController) abortRegistry.register(this, this.#abortController)
this.#options = options
this.#target = target
this.values = new Proxy(target, {
set: (_target, prop, value) => {
const old = this.get(prop)
if (old !== value) {
this.emit(prop, value)
if (this.#options.shallow) {
if (State.isState(old)) this.disown(prop, old)
if (State.isState(value)) this.adopt(prop, value)
}
this.set(prop, value)
}
return true
},
get: (_target, prop) => this.get(prop),
})
this.addEventListener
// Try running a "<name>Changed" method for every changed property // Try running a "<name>Changed" method for every changed property
// Can be disabled to maybe squeeze out some performance // Can be disabled to maybe squeeze out some performance
if (options.methods ?? true) { if (methods ?? true) {
this.addEventListener("change", ({final}) => { this.addEventListener("change", ({final}) => {
final.forEach((value, prop) => { final.forEach((value, prop) => {
if (`${prop}Changed` in this) this[`${prop}Changed`](value) if (`${prop}Changed` in this) this[`${prop}Changed`](value)
@ -89,77 +63,6 @@ export class State extends SimpleState {
} }
} }
// When you only need one value, you can skip the proxy.
set value(value) { this.values.value = value }
get value() { return this.values.value }
adopt(prop, state) {
let handlers = this.#nested.get(state)
if (!handlers) {
// Actual adoption
handlers = new Map()
this.#nested.set(state, handlers)
}
const ref = this.#weakRef
const handler = () => ref.deref()?.emit(prop, state)
handlers.set(prop, handler)
state.addEventListener("change", handler, {signal: this.#abortController.signal})
}
disown(prop, state) {
const handlers = this.#nested.get(state)
const handler = handlers.get(prop)
state.removeEventListener("change", handler)
handlers.delete(prop)
if (handlers.size == 0) {
this.#nested.delete(state)
}
}
// Anounces that a prop has changed
emit(prop, value) {
if (this.#options.defer ?? true) {
if (!this.#queue) {
this.#queue = []
queueMicrotask(() => {
this.dispatchEvent(new ChangeEvent(...this.#queue))
this.#queue = undefined
})
}
this.#queue.push([prop, value])
} else {
this.dispatchEvent(new ChangeEvent([prop, value]))
}
}
forward(property="value", fallback) {
if (!this.#forwardCache) this.#forwardCache = new Map()
const cached = this.#forwardCache.get(property)?.deref()
if (cached) {
return cached
} else {
const forwarded = new ForwardState(this, property, fallback)
const ref = new WeakRef(forwarded)
this.#forwardCache.set(property, ref)
forwardFinalizationRegistry.register(forwarded, [this.#forwardCache, property])
return forwarded
}
}
set(...args) {
if (args.length === 1) return this.set("value", ...args)
const [prop, value] = args
this.#target[prop] = value
}
get(...args) {
if (args.length === 0) return this.get("value")
const prop = args[0]
return this.#target[prop]
}
subscribe(prop, callback) { subscribe(prop, callback) {
if (!callback) return this.subscribe("value", prop) if (!callback) return this.subscribe("value", prop)
@ -171,32 +74,127 @@ export class State extends SimpleState {
return () => controller.abort() return () => controller.abort()
} }
// Backwards compatibility get() { return this.value }
get proxy() { return this.values } set(value) { this.value = value }
emit(property, from, to, options={}) {
const change = {property, from, to, ...options}
if (!this.synchronous) {
if (!this.#queue) {
this.#queue = []
queueMicrotask(() => {
this.dispatchEvent(new ChangeEvent(...this.#queue))
this.#queue = undefined
})
}
this.#queue.push(change)
} else {
this.dispatchEvent(new ChangeEvent([change]))
}
}
adopt(prop, state) {
let handlers = this.#nested.get(state)
if (!handlers) {
// Actual adoption
handlers = new Map()
this.#nested.set(state, handlers)
}
const ref = this.#weakRef
const handler = () => ref.deref()?.emit(prop, state, state, {state: true})
handlers.set(prop, handler)
state.addEventListener("change", handler, {signal: this.ignal})
}
disown(prop, state) {
const handlers = this.#nested.get(state)
const handler = handlers.get(prop)
state.removeEventListener("change", handler)
handlers.delete(prop)
if (handlers.size == 0) {
this.#nested.delete(state)
}
}
get signal() { return this.#abortController.signal }
get synchronous() { return this.#synchronous }
} }
const forwardFinalizationRegistry = new FinalizationRegistry(([cache, name]) => { export class State extends SimpleState {
cache.remove(name) #target
}) #shallow
static isState(object) { return SimpleState.prototype.isPrototypeOf(object) }
constructor(target={}, {shallow, ...options}={}) {
super(options)
this.#shallow = !!shallow
this.#target = target
this.values = new Proxy(target, {
set: (_target, prop, value) => {
const old = this.get(prop)
if (old !== value) {
this.emit(prop, old, value)
if (this.#shallow) {
if (State.isState(old)) this.disown(prop, old)
if (State.isState(value)) this.adopt(prop, value)
}
this.set(prop, value)
}
return true
},
get: (_target, prop) => this.get(prop),
})
}
forward(property="value", methods) {
return new ForwardState(this, property, methods)
}
set(prop, value) {
if (arguments.length === 1) return this.set("value", prop)
this.#target[prop] = value
}
get(prop="value") {
return this.#target[prop]
}
set value(value) { this.set(value) }
get value() { return this.get() }
}
export class ForwardState extends SimpleState { export class ForwardState extends SimpleState {
#backend #backend
#property #property
#fallback #methods
constructor(backend, property, fallback) { constructor(backend, property, methods = {}) {
super() super()
this.#methods = methods
this.#backend = backend this.#backend = backend
this.#property = property this.#property = property
this.#fallback = fallback
const ref = new WeakRef(this) const ref = new WeakRef(this)
const abortController = new AbortController() const abortController = new AbortController()
abortRegistry.register(this, abortController)
backend.addEventListener("change", event => { backend.addEventListener("change", event => {
const state = ref.deref() const state = ref.deref()
if (state) { if (state) {
const relevantChanges = event.changes let relevantChanges = event.changes
.filter(([name]) => name === property) .filter(({property: name}) => name === property)
.map(([_, value]) => ["value", value]) const get = methods.get
if (methods.get) {
relevantChanges = relevantChanges.map(
({from, to}) => ({property: "value", from: get(from), to: get(to)})
)
} else {
relevantChanges = relevantChanges.map(
({from, to}) => ({property: "value", from, to})
)
}
if (relevantChanges.length > 0) if (relevantChanges.length > 0)
state.dispatchEvent(new ChangeEvent(...relevantChanges)) state.dispatchEvent(new ChangeEvent(...relevantChanges))
} else { } else {
@ -205,8 +203,23 @@ export class ForwardState extends SimpleState {
}, {signal: abortController.signal}) }, {signal: abortController.signal})
} }
get value() { return this.#backend.values[this.#property] ?? this.#fallback } get value() {
set value(value) { this.#backend.values[this.#property] = value } const methods = this.#methods
if (methods.get) {
return methods.get(this.#backend.values[this.#property])
} else {
return this.#backend.values[this.#property]
}
}
set value(value) {
const methods = this.#methods
if (methods.set) {
this.#backend.values[this.#property] = methods.set(value)
} else {
this.#backend.values[this.#property] = value
}
}
} }
class StorageChangeEvent extends Event { class StorageChangeEvent extends Event {
@ -273,23 +286,30 @@ const attributeObserver = new MutationObserver(mutations => {
} }
}) })
export const component = (generator, name) => { export const component = (name, generator, methods) => {
name = name ?? camelToKebab(generator.name) if (typeof name === "function") {
methods = generator
generator = name
name = camelToKebab(generator.name)
}
const Element = class extends HTMLElement{ const Element = class extends HTMLElement{
constructor() { constructor() {
super() super()
this.state = new State(Object.fromEntries([...this.attributes].map(attribute => [kebabToCamel(attribute.name), attribute.value]))) this.state = new State(Object.fromEntries([...this.attributes].map(attribute => [kebabToCamel(attribute.name), attribute.value])))
this.state.addEventListener("change", event => { this.state.addEventListener("change", event => {
for (const [name, value] of event.changes) { for (const {property, to: value} of event.changes) {
const kebabName = camelToKebab(name) const kebabName = camelToKebab(property)
if (this.getAttribute(kebabName) !== String(value)) if (this.getAttribute(kebabName) !== String(value))
this.setAttribute(kebabName, value) this.setAttribute(kebabName, value)
} }
}) })
attributeObserver.observe(this, {attributes: true}) attributeObserver.observe(this, {attributes: true})
this.replaceChildren(generator(this)) this.replaceChildren(generator.call(this, this.state))
} }
} }
if (methods) {
Object.defineProperties(Element.prototype, Object.getOwnPropertyDescriptors(methods))
}
customElements.define(name, Element) customElements.define(name, Element)
return Element; return Element;
} }
@ -297,14 +317,12 @@ export const component = (generator, name) => {
class ComposedState extends SimpleState { class ComposedState extends SimpleState {
#func #func
#states #states
#options
constructor(func, options, ...states) { constructor(func, options, ...states) {
super() super(options)
this.#func = func this.#func = func
this.#states = states this.#states = states
this.#options = options
const abortController = new AbortController() const abortController = new AbortController()
abortRegistry.register(this, abortController) abortRegistry.register(this, abortController)
@ -322,7 +340,9 @@ class ComposedState extends SimpleState {
#microtaskQueued #microtaskQueued
scheduleUpdate() { scheduleUpdate() {
if (this.#options.defer) { if (this.synchronous) {
this.update()
} else {
if (!this.#microtaskQueued) { if (!this.#microtaskQueued) {
queueMicrotask(() => { queueMicrotask(() => {
this.#microtaskQueued = false this.#microtaskQueued = false
@ -330,18 +350,18 @@ class ComposedState extends SimpleState {
}) })
} }
this.#microtaskQueued = true this.#microtaskQueued = true
} else {
this.update()
} }
} }
update() { update() {
this.value = this.#func(...this.#states.map(state => state.value)) const value = this.#func(...this.#states.map(state => state.value))
this.dispatchEvent(new ChangeEvent([["value", this.value]])) const change = {property: "value", from: this.value, to: value}
this.value = value
this.dispatchEvent(new ChangeEvent(change))
} }
} }
export const compose = func => (...states) => new ComposedState(func, {defer: true}, ...states) export const compose = func => (...states) => new ComposedState(func, {}, ...states)
const eventName = "mutation" const eventName = "mutation"
@ -359,7 +379,6 @@ const mutationObserver = new MutationObserver(mutations => {
export class DOMState extends SimpleState { export class DOMState extends SimpleState {
#target #target
#defer
#getValue #getValue
#equal #equal
@ -367,8 +386,7 @@ export class DOMState extends SimpleState {
#changedValue = false #changedValue = false
constructor(target, options) { constructor(target, options) {
super() super(options)
this.#defer = options.defer ?? false
this.#target = target this.#target = target
this.#getValue = options.get ?? (target => target.value) this.#getValue = options.get ?? (target => target.value)
this.#equal = options.equal ?? ((a, b) => a===b) this.#equal = options.equal ?? ((a, b) => a===b)
@ -396,7 +414,9 @@ export class DOMState extends SimpleState {
this.#old = current this.#old = current
if (this.#defer) { if (this.synchronous) {
this.dispatchEvent(new ChangeEvent(["value", current]))
} else {
if (!this.#changedValue) { if (!this.#changedValue) {
queueMicrotask(() => { queueMicrotask(() => {
this.#changedValue = false this.#changedValue = false
@ -404,10 +424,30 @@ export class DOMState extends SimpleState {
}) })
this.#changedValue = current this.#changedValue = current
} }
} else {
this.dispatchEvent(new ChangeEvent(["value", current]))
} }
} }
} }
export class MapStorage extends Storage {
#map = new Map()
key(index) {
return [...this.#map.keys()][index]
}
getItem(keyName) {
if (this.#map.has(keyName))
return this.#map.get(keyName)
else
return null
}
setItem(keyName, keyValue) {
this.#map.set(keyName, String(keyValue))
}
removeItem(keyName) {
this.#map.delete(keyName)
}
clear() {
this.#map.clear()
}
}
export default State export default State