Add faster update option, v 0.1.9

This commit is contained in:
2019-11-16 15:46:48 +01:00
parent fb4b7053cc
commit f9dd071315
5 changed files with 318 additions and 29 deletions

View File

@@ -0,0 +1,169 @@
package nl.astraeus.komp
import kotlinx.html.DefaultUnsafe
import kotlinx.html.Entities
import kotlinx.html.Tag
import kotlinx.html.TagConsumer
import kotlinx.html.Unsafe
import kotlinx.html.consumers.onFinalize
import org.w3c.dom.Document
import org.w3c.dom.HTMLElement
import org.w3c.dom.Node
import org.w3c.dom.asList
import org.w3c.dom.events.Event
@Suppress("NOTHING_TO_INLINE")
private inline fun HTMLElement.setEvent(name: String, noinline callback : (Event) -> Unit) : Unit {
asDynamic()[name] = callback
}
interface HtmlConsumer : TagConsumer<HTMLElement> {
fun append(node: Node)
}
class HtmlBuilder(val document : Document) : HtmlConsumer {
private val path = arrayListOf<HTMLElement>()
private var lastLeaved : HTMLElement? = null
override fun onTagStart(tag: Tag) {
val element: HTMLElement = when {
tag.namespace != null -> document.createElementNS(tag.namespace!!, tag.tagName).asDynamic()
else -> document.createElement(tag.tagName) as HTMLElement
}
tag.attributesEntries.forEach {
element.setAttribute(it.key, it.value)
}
if (path.isNotEmpty()) {
path.last().appendChild(element)
}
path.add(element)
}
override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) {
when {
path.isEmpty() -> throw IllegalStateException("No current tag")
path.last().tagName.toLowerCase() != tag.tagName.toLowerCase() -> throw IllegalStateException("Wrong current tag")
else -> path.last().let { node ->
if (value == null) {
node.removeAttribute(attribute)
} else {
node.setAttribute(attribute, value)
}
}
}
}
override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) {
when {
path.isEmpty() -> throw IllegalStateException("No current tag")
path.last().tagName.toLowerCase() != tag.tagName.toLowerCase() -> throw IllegalStateException("Wrong current tag")
else -> path.last().setEvent(event, value)
}
}
override fun onTagEnd(tag: Tag) {
if (path.isEmpty() || path.last().tagName.toLowerCase() != tag.tagName.toLowerCase()) {
throw IllegalStateException("We haven't entered tag ${tag.tagName} but trying to leave")
}
lastLeaved = path.removeAt(path.lastIndex)
}
override fun onTagContent(content: CharSequence) {
if (path.isEmpty()) {
throw IllegalStateException("No current DOM node")
}
path.last().appendChild(document.createTextNode(content.toString()))
}
override fun onTagContentEntity(entity: Entities) {
if (path.isEmpty()) {
throw IllegalStateException("No current DOM node")
}
// stupid hack as browsers doesn't support createEntityReference
val s = document.createElement("span") as HTMLElement
s.innerHTML = entity.text
path.last().appendChild(s.childNodes.asList().filter { it.nodeType == Node.TEXT_NODE }.first())
// other solution would be
// pathLast().innerHTML += entity.text
}
override fun append(node: Node) {
path.last().appendChild(node)
}
override fun onTagContentUnsafe(block: Unsafe.() -> Unit) {
with(DefaultUnsafe()) {
block()
path.last().innerHTML += toString()
}
}
override fun onTagComment(content: CharSequence) {
if (path.isEmpty()) {
throw IllegalStateException("No current DOM node")
}
path.last().appendChild(document.createComment(content.toString()))
}
override fun finalize(): HTMLElement = lastLeaved?.asR() ?: throw IllegalStateException("We can't finalize as there was no tags")
@Suppress("UNCHECKED_CAST")
private fun HTMLElement.asR(): HTMLElement = this.asDynamic()
}
fun Document.createTree() : TagConsumer<HTMLElement> = HtmlBuilder(this)
val Document.create : TagConsumer<HTMLElement>
get() = HtmlBuilder(this)
fun Node.append(block: TagConsumer<HTMLElement>.() -> Unit): List<HTMLElement> =
ArrayList<HTMLElement>().let { result ->
ownerDocumentExt.createTree().onFinalize { it, partial ->
if (!partial) {
result.add(it); appendChild(it)
}
}.block()
result
}
fun Node.prepend(block: TagConsumer<HTMLElement>.() -> Unit): List<HTMLElement> =
ArrayList<HTMLElement>().let { result ->
ownerDocumentExt.createTree().onFinalize { it, partial ->
if (!partial) {
result.add(it)
insertBefore(it, firstChild)
}
}.block()
result
}
val HTMLElement.append: TagConsumer<HTMLElement>
get() = ownerDocumentExt.createTree().onFinalize { element, partial ->
if (!partial) {
this@append.appendChild(element)
}
}
val HTMLElement.prepend: TagConsumer<HTMLElement>
get() = ownerDocumentExt.createTree().onFinalize { element, partial ->
if (!partial) {
this@prepend.insertBefore(element, this@prepend.firstChild)
}
}
private val Node.ownerDocumentExt: Document
get() = when {
this is Document -> this
else -> ownerDocument ?: throw IllegalStateException("Node has no ownerDocument")
}

View File

@@ -1,52 +1,52 @@
package nl.astraeus.komp
import kotlinx.html.HtmlBlockTag
import kotlinx.html.TagConsumer
import kotlinx.html.dom.create
import kotlinx.html.Tag
import org.w3c.dom.HTMLElement
import org.w3c.dom.Node
import kotlin.browser.document
fun HtmlBlockTag.include(component: Komponent) {
component.element = component.render(this.consumer as TagConsumer<HTMLElement>)
fun Tag.include(component: Komponent) {
component.update()
val consumer = this.consumer
val element = component.element
if (consumer is HtmlBuilder && element != null) {
consumer.append(element)
}
}
abstract class Komponent {
var element: Node? = null
open fun create(): HTMLElement {
val result = render(document.create)
val consumer = HtmlBuilder(document)
val result = render(consumer)
element = result
return result
}
abstract fun render(consumer: TagConsumer<HTMLElement>): HTMLElement
abstract fun render(consumer: HtmlBuilder): HTMLElement
open fun update() = refresh()
open fun refresh() {
if (element == null) {
console.log("Unable to refresh, element == null", this)
val oldElement = element
if (logRenderEvent) {
console.log("Rendering", this)
}
element?.let { element ->
if (logRenderEvent) {
console.log("Rendering", this)
}
val newElement = create()
val oldElement = element
val newElement = create()
if (logReplaceEvent) {
console.log("Replacing", oldElement, newElement)
}
element.parentNode?.replaceChild(newElement, oldElement)
if (oldElement != null) {
if (logReplaceEvent) {
console.log("Replacing", oldElement, newElement)
}
oldElement.parentNode?.replaceChild(newElement, oldElement)
}
}
open fun update() {
refresh()
}
@JsName("remove")
fun remove() {
element?.let {
@@ -61,7 +61,6 @@ abstract class Komponent {
}
companion object {
var logRenderEvent = false
var logReplaceEvent = false