Replace bind function with proper reactivity
* Detects state elements * Two-Way binding for supported attributes
This commit is contained in:
parent
bc1383f07d
commit
79a728520e
1 changed files with 97 additions and 40 deletions
137
skooma.js
137
skooma.js
|
@ -31,7 +31,6 @@ const parseAttribute = (attribute) => {
|
||||||
return JSON.stringify(attribute)
|
return JSON.stringify(attribute)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const defined = (value, fallback) => typeof value != "undefined" ? value : fallback
|
const defined = (value, fallback) => typeof value != "undefined" ? value : fallback
|
||||||
const getCustom = args => args.reduce(
|
const getCustom = args => args.reduce(
|
||||||
(current, argument) => Array.isArray(argument)
|
(current, argument) => Array.isArray(argument)
|
||||||
|
@ -42,36 +41,113 @@ const getCustom = args => args.reduce(
|
||||||
,undefined
|
,undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const isReactive = object => object
|
||||||
|
&& (typeof object == "object")
|
||||||
|
&& ("addEventListener" in object)
|
||||||
|
&& ("value" in object)
|
||||||
|
|
||||||
|
const toChild = arg => {
|
||||||
|
if (typeof arg == "string" || typeof arg == "number") {
|
||||||
|
return document.createTextNode(arg)
|
||||||
|
} else if ("nodeName" in arg) {
|
||||||
|
return arg
|
||||||
|
} else if (isReactive(arg)) {
|
||||||
|
return reactiveChild(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reactiveChild = reactive => {
|
||||||
|
const ref = new WeakRef(toChild(reactive.value))
|
||||||
|
reactive.addEventListener("change", () => {
|
||||||
|
const value = ref.deref()
|
||||||
|
if (value)
|
||||||
|
value.replaceWith(reactiveChild(reactive))
|
||||||
|
}, {once: true})
|
||||||
|
return ref.deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
const specialAttributes = {
|
||||||
|
value: {
|
||||||
|
get: element => element.value,
|
||||||
|
set: (element, value) => {
|
||||||
|
element.setAttribute("value", value)
|
||||||
|
element.value = value
|
||||||
|
},
|
||||||
|
hook: (element, callback) => { element.addEventListener("input", callback) }
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
set: (element, value) => { insertStyles(element.style, value) }
|
||||||
|
},
|
||||||
|
dataset: {
|
||||||
|
set: (element, value) => {
|
||||||
|
for (const [attribute2, value2] of Object.entries(value)) {
|
||||||
|
element.dataset[attribute2] = parseAttribute(value2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shadowRoot: {
|
||||||
|
set: (element, value) => {
|
||||||
|
parseArgs((element.shadowRoot || element.attachShadow({mode: "open"})), null, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAttribute = (element, attribute, value, cleanupSignal) => {
|
||||||
|
const special = specialAttributes[attribute]
|
||||||
|
if (isReactive(value))
|
||||||
|
setReactiveAttribute(element, attribute, value)
|
||||||
|
else if (typeof value === "function")
|
||||||
|
element.addEventListener(attribute.replace(/^on[A-Z]/, x => x.charAt(x.length-1).toLowerCase()), value, {signal: cleanupSignal})
|
||||||
|
else if (special) {
|
||||||
|
special.set(element, value)
|
||||||
|
}
|
||||||
|
else if (value === true)
|
||||||
|
{if (!element.hasAttribute(attribute)) element.setAttribute(attribute, '')}
|
||||||
|
else if (value === false)
|
||||||
|
element.removeAttribute(attribute)
|
||||||
|
else {
|
||||||
|
element.setAttribute(attribute, parseAttribute(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setReactiveAttribute = (element, attribute, reactive, abortController) => {
|
||||||
|
if (abortController) abortController.abort()
|
||||||
|
abortController = new AbortController()
|
||||||
|
|
||||||
|
const ref = new WeakRef(element)
|
||||||
|
setAttribute(element, attribute, reactive.value, abortController.signal)
|
||||||
|
|
||||||
|
reactive.addEventListener("change", () => {
|
||||||
|
const element = ref.deref()
|
||||||
|
if (element)
|
||||||
|
setReactiveAttribute(element, attribute, reactive, abortController)
|
||||||
|
}, {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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const parseArgs = (element, before, ...args) => {
|
const parseArgs = (element, before, ...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 (typeof arg == "string" || typeof arg == "number")
|
const child = toChild(arg)
|
||||||
element.insertBefore(document.createTextNode(arg), before)
|
if (child)
|
||||||
|
element.insertBefore(child, before)
|
||||||
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")
|
else if (typeof arg == "function")
|
||||||
arg(element)
|
arg(element)
|
||||||
else if ("nodeName" in arg)
|
|
||||||
element.insertBefore(arg, before)
|
|
||||||
else if ("length" in arg)
|
else if ("length" in arg)
|
||||||
parseArgs(element, before, ...arg)
|
parseArgs(element, before, ...arg)
|
||||||
else
|
else
|
||||||
for (const key in arg)
|
for (const key in arg)
|
||||||
if (key == "style" && typeof(arg[key])=="object")
|
setAttribute(element, key, arg[key])
|
||||||
insertStyles(element.style, arg[key])
|
}
|
||||||
else if (key == "dataset" && typeof(arg[key])=="object")
|
|
||||||
for (const [key2, value] of Object.entries(arg[key]))
|
|
||||||
element.dataset[key2] = parseAttribute(value)
|
|
||||||
else if (key == "shadowRoot")
|
|
||||||
parseArgs((element.shadowRoot || element.attachShadow({mode: "open"})), null, arg[key])
|
|
||||||
else if (typeof arg[key] === "function")
|
|
||||||
element.addEventListener(key.replace(/^on[A-Z]/, x => x.charAt(x.length-1).toLowerCase()), arg[key])
|
|
||||||
else if (arg[key] === true)
|
|
||||||
{if (!element.hasAttribute(key)) element.setAttribute(key, '')}
|
|
||||||
else if (arg[key] === false)
|
|
||||||
element.removeAttribute(key)
|
|
||||||
else
|
|
||||||
element.setAttribute(key, parseAttribute(arg[key]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = (name, args, options) => {
|
const node = (name, args, options) => {
|
||||||
|
@ -98,25 +174,6 @@ export const svg = nameSpacedProxy({xmlns: "http://www.w3.org/2000/svg"})
|
||||||
|
|
||||||
// Other utility exports
|
// Other utility exports
|
||||||
|
|
||||||
export const bind = transform => {
|
|
||||||
let element
|
|
||||||
const inject = next => Object.defineProperty(next, 'current', {get: () => element})
|
|
||||||
const update = (...data) => {
|
|
||||||
const next = transform(...data)
|
|
||||||
if (next) {
|
|
||||||
if (typeof next == "string") {
|
|
||||||
element.innerText = next
|
|
||||||
return element
|
|
||||||
} else {
|
|
||||||
if (element) element.replaceWith(next)
|
|
||||||
element = inject(next)
|
|
||||||
return element
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return update
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wraps an event handler in a function that calls preventDefault on the event
|
// Wraps an event handler in a function that calls preventDefault on the event
|
||||||
export const handle = fn => event => { fn(event); event.preventDefault() }
|
export const handle = fn => event => { fn(event); event.preventDefault() }
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue