Coverage Summary for Class: RenderKt (kweb.state)
Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
RenderKt |
85.7%
(6/7)
|
91.7%
(11/12)
|
93.5%
(29/31)
|
97.2%
(211/217)
|
RenderKt$closeOnElementCreatorCleanup$1 |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(10/10)
|
RenderKt$logger$1 |
0%
(0/1)
|
|
0%
(0/1)
|
RenderKt$render$1 |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(5/5)
|
RenderKt$render$2 |
100%
(1/1)
|
|
100%
(1/1)
|
RenderKt$render$3 |
100%
(1/1)
|
100%
(2/2)
|
100%
(2/2)
|
100%
(15/15)
|
RenderKt$render$listenerHandle$1 |
100%
(1/1)
|
75%
(3/4)
|
100%
(3/3)
|
90.6%
(29/32)
|
RenderKt$render$listenerHandle$1$WhenMappings |
|
RenderKt$render$renderFragment$1 |
100%
(1/1)
|
|
100%
(3/3)
|
100%
(41/41)
|
RenderKt$render$renderLoop$1 |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(13/13)
|
Total |
86.7%
(13/15)
|
88.9%
(16/18)
|
93.2%
(41/44)
|
97.3%
(324/333)
|
package kweb.state
import kweb.Element
import kweb.ElementCreator
import kweb.WebBrowser
import kweb.span
import kweb.state.RenderState.*
import mu.KotlinLogging
import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.ArrayList
import kotlin.collections.forEach
import kotlin.collections.plusAssign
/**
* Created by ian on 6/18/17.
*/
private val logger = KotlinLogging.logger {}
object RenderSpanNames{
const val startMarkerClassName = "rMStart"
const val endMarkerClassName = "rMEnd"
const val listStartMarkerClassName = "rLStart"
const val listEndMarkerClassName = "rLEnd"
}
/**
* Render the value of a [KVal] into DOM elements, and automatically re-render those
* elements whenever the value changes.
*/
fun <T : Any?> ElementCreator<*>.render(
value: KVal<T>,
block: ElementCreator<Element>.(T) -> Unit
) : RenderFragment {
val previousElementCreator: AtomicReference<ElementCreator<Element>?> = AtomicReference(null)
val renderState = AtomicReference(NOT_RENDERING)
//TODO this could possibly be improved
val renderFragment: RenderFragment = if (element.browser.isCatchingOutbound() == null) {
element.browser.batch(WebBrowser.CatcherType.RENDER) {
val startSpan = span().classes(RenderSpanNames.startMarkerClassName)
val endSpan = span().classes(RenderSpanNames.endMarkerClassName)
RenderFragment(startSpan.id, endSpan.id)
}
} else {
val startSpan = span().classes(RenderSpanNames.startMarkerClassName)
val endSpan = span().classes(RenderSpanNames.endMarkerClassName)
RenderFragment(startSpan.id, endSpan.id)
}
fun eraseBetweenSpans() {
element.removeChildrenBetweenSpans(renderFragment.startId, renderFragment.endId)
previousElementCreator.getAndSet(null)?.cleanup()
}
fun eraseAndRender() {
eraseBetweenSpans()
previousElementCreator.set(ElementCreator<Element>(this.element, this, insertBefore = renderFragment.endId))
renderState.set(RENDERING_NO_PENDING_CHANGE)
val elementCreator = previousElementCreator.get()
if (elementCreator != null) {
elementCreator.block(value.value)
} else {
logger.error("previousElementCreator.get() was null in eraseAndRender()")
//TODO This warning message could be made more helpful. I can't think of a situation where this could actually happen
//So I'm not sure what we need to say in this message.
}
if (renderState.get() == RENDERING_NO_PENDING_CHANGE) {
renderState.set(NOT_RENDERING)
}
}
//TODO this function could probably have a clearer name
//It's purpose is to monitor renderState, and call eraseAndRender() if the page is rendering.
fun renderLoop() {
do {
if (element.browser.isCatchingOutbound() == null) {
element.browser.batch(WebBrowser.CatcherType.RENDER) {
eraseAndRender()
}
} else {
eraseAndRender()
}
} while (renderState.get() != NOT_RENDERING)
}
val listenerHandle = value.addListener { _, _ ->
when (renderState.get()) {
NOT_RENDERING -> {
renderLoop()
}
RENDERING_NO_PENDING_CHANGE -> {
renderState.set(RENDERING_WITH_PENDING_CHANGE)
}
else -> {
// This space intentionally left blank
}
}
}
renderFragment.addDeletionListener {
value.removeListener(listenerHandle)
}
//we have to make sure to call renderLoop to start the initial render and begin monitoring renderState
renderLoop()
this.onCleanup(false) {
//TODO I'm not sure what cleanup needs to be done now that there is no container element
}
this.onCleanup(true) {
previousElementCreator.getAndSet(null)?.cleanup()
value.removeListener(listenerHandle)
}
return renderFragment
}
fun ElementCreator<*>.closeOnElementCreatorCleanup(kv: KVal<*>) {
this.onCleanup(withParent = true) {
kv.close(CloseReason("Closed because a parent ElementCreator was cleaned up"))
}
}
/**
* Render the value of a [KVar] into DOM elements, and automatically re-render those
* elements whenever the value changes.
*/
@Deprecated("Use kweb.components.Component instead, see: https://docs.kweb.io/book/components.html")
fun <PARENT_ELEMENT_TYPE : Element, RETURN_TYPE> ElementCreator<PARENT_ELEMENT_TYPE>.render(
component: AdvancedComponent<PARENT_ELEMENT_TYPE, RETURN_TYPE>
) : RETURN_TYPE {
return component.render(this)
}
/**
* [AdvancedComponent]s can be rendered into DOM elements by calling [AdvancedComponent.render].
*
* Unlike [Component], [AdvancedComponent]s allows the parent element type to be configured, and a return
* type to be specified.
*/
@Deprecated("Use kweb.components.Component instead, see: https://docs.kweb.io/book/components.html")
interface AdvancedComponent<in PARENT_ELEMENT_TYPE : Element, out RETURN_TYPE> {
/**
* Render this [Component] into DOM elements, returning an arbitrary
* value of type [RETURN_TYPE].
*/
fun render(elementCreator: ElementCreator<PARENT_ELEMENT_TYPE>) : RETURN_TYPE
}
/**
* [Component]s can be rendered into DOM elements by calling [Component.render].
*
* For more flexibility, see [AdvancedComponent].
*/
@Deprecated("Use kweb.components.Component instead, see: https://docs.kweb.io/book/components.html")
interface Component : AdvancedComponent<Element, Unit> {
/**
* Render this [Component] into DOM elements
*/
override fun render(elementCreator: ElementCreator<Element>)
}
class RenderFragment(val startId: String, val endId: String) {
private val deletionListeners = ArrayList<() -> Unit>()
internal fun addDeletionListener(listener: () -> Unit) {
synchronized(deletionListeners) {
deletionListeners += listener
}
}
fun delete() {
synchronized(deletionListeners) {
deletionListeners.forEach { it.invoke() }
}
}
}
class RenderHandle<ITEM : Any>(val renderFragment: RenderFragment, val kvar: KVar<ITEM>)
private enum class RenderState {
NOT_RENDERING, RENDERING_NO_PENDING_CHANGE, RENDERING_WITH_PENDING_CHANGE
}