package nl.astraeus.komp import kotlinx.browser.document import kotlinx.browser.window import kotlinx.html.div import org.w3c.dom.HTMLDivElement import org.w3c.dom.HTMLElement import org.w3c.dom.Node import org.w3c.dom.css.CSSStyleDeclaration import kotlin.reflect.KProperty typealias CssStyle = CSSStyleDeclaration.() -> Unit class StateDelegate<T>( val komponent: Komponent, initialValue: T ) { var value: T = initialValue operator fun getValue(thisRef: Any?, property: KProperty<*>): T { return value } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { if (this.value?.equals(value) != true) { this.value = value komponent.requestUpdate() } } } inline fun <reified T> Komponent.state(initialValue: T): StateDelegate<T> = StateDelegate(this, initialValue) fun HtmlConsumer.include(component: Komponent) { append(component.create()) } class DummyKomponent: Komponent() { override fun HtmlBuilder.render() { div { + "dummy" } } } abstract class Komponent { private var createIndex = getNextCreateIndex() private var dirty: Boolean = true var vdom: VDOMElement? = null var element: Node? = null val declaredStyles: MutableMap<String, CSSStyleDeclaration> = HashMap() open fun create(): VDOMElement { val consumer = HtmlBuilder(this.createIndex) try { consumer.render() } catch (e: Throwable) { println("Exception occurred in ${this::class.simpleName}.render() call!") throw e } val result = consumer.finalize() if (result.id.isBlank()) { result.id = "komp_${createIndex}" } result.komponent = this vdom = result dirty = false return result } abstract fun HtmlBuilder.render() fun requestUpdate() { dirty = true scheduleForUpdate(this) } open fun style(className: String, vararg imports: CssStyle, block: CssStyle = {}) { val style = (document.createElement("div") as HTMLDivElement).style for (imp in imports) { imp(style) } block(style) declaredStyles[className] = style } open fun update() { refresh() } internal fun refresh() { val oldElement = vdom if (logRenderEvent) { console.log("Rendering", this) } val newElement = create() element = if (oldElement != null && element != null) { if (logReplaceEvent) { console.log("DomDiffing", oldElement, newElement) } DiffPatch.updateNode(element!!, oldElement, newElement) } else { if (logReplaceEvent) { console.log("Create", newElement) } newElement.createElement() } vdom = newElement dirty = false } companion object { private var nextCreateIndex: Int = 1 private var updateCallback: Int? = null private var scheduledForUpdate = mutableSetOf<Komponent>() var logRenderEvent = false var logReplaceEvent = false fun create(parent: HTMLElement, component: Komponent, insertAsFirst: Boolean = false) { val vdomElement = component.create() val element = vdomElement.createElement() component.element = element if (insertAsFirst && parent.childElementCount > 0) { parent.insertBefore(element, parent.firstChild) } else { parent.appendChild(element) } } private fun getNextCreateIndex() = nextCreateIndex++ private fun scheduleForUpdate(komponent: Komponent) { scheduledForUpdate.add(komponent) if (updateCallback == null) { window.setTimeout({ runUpdate() }, 0) } } private fun runUpdate() { val todo = scheduledForUpdate.sortedBy { komponent -> komponent.createIndex } if (logRenderEvent) { console.log("runUpdate") } todo.forEach { next -> val element = next.element if (element is HTMLElement) { if (document.getElementById(element.id) != null) { if (next.dirty) { if (logRenderEvent) { console.log("Update dirty ${next.createIndex}") } next.update() } else { if (logRenderEvent) { console.log("Skip ${next.createIndex}") } } } else { console.log("Komponent element has no id, ", next, element) } } else { console.log("Komponent element is null", next) } } scheduledForUpdate.clear() updateCallback = null } } }