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:
parent
e293594edb
commit
b25a3013d2
1 changed files with 46 additions and 48 deletions
94
skooma.js
94
skooma.js
|
@ -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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue