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.
This commit is contained in:
Talia 2024-01-24 14:50:51 +01:00
parent e293594edb
commit b25a3013d2

View file

@ -1,12 +1,16 @@
// 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)
} }
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 keyToPropName = key => key.replace(/^[A-Z]/, a => "-"+a).replace(/[A-Z]/g, a => '-'+a.toLowerCase())
@ -38,55 +42,56 @@ const getCustom = args => args.reduce(
,undefined ,undefined
) )
export const isReactive = object => !(object instanceof HTMLElement) export const isReactive = object => object
&& (object instanceof EventTarget) && typeof object == "object"
&& ("value" in object) && !(object instanceof HTMLElement)
&& object.subscribe
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 (arg instanceof HTMLElement) { else if (arg instanceof HTMLElement)
return arg return arg
} else if (isReactive(arg)) { else if (isReactive(arg))
return reactiveChild(arg) return reactiveChild(arg)
} else { else
return document.createComment("Placeholder for reactive content") return document.createComment("Placeholder for reactive content")
}
} }
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)
value.replaceWith(reactiveChild(reactive)) if (ref) ref.deref().replaceWith(child)
}, {once: true}) untilDeathDoThemPart(child, reactive)
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] = parseAttribute(value2)
} }
} }
}, },
shadowRoot: { shadowRoot: {
set: (element, value) => { set(value) {
parseArgs((element.shadowRoot || element.attachShadow({mode: "open"})), value) parseArgs((this.shadowRoot || this.attachShadow({mode: "open"})), value)
} }
} }
} }
@ -98,7 +103,7 @@ const setAttribute = (element, attribute, value, cleanupSignal) => {
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.replace(/^on[A-Z]/, x => x.charAt(x.length-1).toLowerCase()), value, {signal: cleanupSignal})
else if (special) { else if (special) {
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, '')}
@ -109,26 +114,19 @@ const setAttribute = (element, attribute, value, cleanupSignal) => {
} }
} }
const setReactiveAttribute = (element, attribute, reactive, abortController) => { 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", () => { if (special?.hook && reactive.set) {
const element = ref.deref() special.hook.call(element, () => {
if (element) const value = special.get.call(element, attribute)
setReactiveAttribute(element, attribute, reactive, abortController) if (value != old) reactive.set() = value
}, {once: true})
const special = specialAttributes[attribute]
if (special?.hook) {
special.hook(element, () => {
const value = special.get(element, attribute)
if (value != reactive.value) reactive.value = value
}) })
} }
} }