Coverage Summary for Class: RenderEachKt (kweb.state)
Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
RenderEachKt |
100%
(4/4)
|
100%
(4/4)
|
100%
(32/32)
|
100%
(236/236)
|
RenderEachKt$renderEach$1$1$fragment$1 |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(6/6)
|
RenderEachKt$renderEach$1$2 |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(5/5)
|
RenderEachKt$renderEach$1$handle$1 |
100%
(1/1)
|
77.8%
(14/18)
|
90%
(27/30)
|
92.4%
(232/251)
|
RenderEachKt$renderEach$insertItem$newFragment$1 |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(6/6)
|
Total |
100%
(8/8)
|
81.8%
(18/22)
|
95.4%
(62/65)
|
96.2%
(485/504)
|
package kweb.state
import kotlinx.serialization.json.JsonPrimitive
import kweb.Element
import kweb.ElementCreator
import kweb.span
/**
* Similar to [render], but renders a list of items, and updates the DOM when the list changes.
*
* While [render] could be used for this with a `KVal<List<String>>`, this method is far more efficient
* because it only updates the DOM when the list changes, rather than re-rendering the entire list every time.
*/
fun <ITEM : Any, EL : Element> ElementCreator<EL>.renderEach(
observableList: ObservableList<ITEM>,
itemRenderer: ElementCreator<Element>.(ITEM) -> Unit
) {
//a wrapper made of 2 empty spans around the list of items that renderEach will draw
//This is needed to be able to use insertBefore on the end span to add items to the end of the list.
val listFragment = RenderFragment(
span().classes(RenderSpanNames.listStartMarkerClassName).id,
span().classes(RenderSpanNames.listEndMarkerClassName).id
)
fun insertItem(position: Int, newItem: ITEM,
renderHandles: ArrayList<RenderHandle<ITEM>>) {
val nextElementRenderMarkerStartId: String = if (position == renderHandles.size) {
listFragment.endId
} else {
renderHandles[position].renderFragment.startId
}
val itemElementCreator =
ElementCreator<Element>(this.element, this, nextElementRenderMarkerStartId)
val kvar = KVar(newItem)
val newFragment = itemElementCreator.render(kvar) { item ->
itemRenderer(item)
}
renderHandles.add(position, RenderHandle(newFragment, kvar))
}
fun <ITEM : Any> deleteItem(position: Int, renderHandles: ArrayList<RenderHandle<ITEM>>) {
val renderHandleToRemove = renderHandles[position].renderFragment
renderHandles.removeAt(position)
browser.callJsFunction("""
var start_id = {};
var end_id = {};
var start_element = document.getElementById(start_id);
var end_element = document.getElementById(end_id);
var parent = start_element.parentNode;
while (start_element.nextSibling != end_element) {
parent.removeChild(start_element.nextSibling);
}
parent.removeChild(start_element);
parent.removeChild(end_element);
""".trimIndent(), JsonPrimitive(renderHandleToRemove.startId),
JsonPrimitive(renderHandleToRemove.endId)
)
renderHandleToRemove.delete()
}
fun moveItemClientSide(itemStartMarker : String, itemEndMarker : String, newPosMarker : String) {
//This JavaScript takes all elements from one start span to another, denoted by startMarker and endMarker,
//and inserts them before the element that's ID is passed to the 'newPos' variable.
//language=JavaScript
val moveItemCode = """
var startMarker = document.getElementById({});
var endMarker = document.getElementById({});
var elementsToMove = [];
var currentElement = startMarker.nextSibling;
while(currentElement !== endMarker) {
elementsToMove.push(currentElement);
currentElement = currentElement.nextSibling;
}
var newPos = document.getElementById({});
var listParent = startMarker.parentNode;
listParent.insertBefore(startMarker, newPos);
listParent.insertBefore(endMarker, newPos);
elementsToMove.forEach(function (item){
listParent.insertBefore(item, endMarker);
});
""".trimIndent()
browser.callJsFunction(moveItemCode, JsonPrimitive(itemStartMarker),
JsonPrimitive(itemEndMarker), JsonPrimitive(newPosMarker)
)
}
ElementCreator<Element>(this.element, this.parentCreator, insertBefore = listFragment.endId).apply {
//These renderFragments must be kept in sync with the items in observableList that they're rendering
val renderHandles = ArrayList<RenderHandle<ITEM>>()
synchronized(renderHandles) {
//render the initial observableList to the DOM storing the Handles in renderHandles
for (item in observableList.getItems()) {
val kvar = KVar(item)
val fragment = render(kvar) { fragItem ->
itemRenderer(fragItem)
}
renderHandles += RenderHandle(fragment, kvar)
}
}
val handle = observableList.addListener { changes ->
synchronized(renderHandles) {
// TODO: Consider replacing change in changes with, "mods in modifications", to remove confusion between change, and Modification.Change
for (change in changes) {
// Apply change to DOM using renderHandles, and update renderHandles to keep it in sync with observableList
when (change) {
is ObservableList.Modification.Change -> {
renderHandles[change.position].kvar.value = KVar(change.newItem).value
}
is ObservableList.Modification.Deletion -> {
deleteItem(change.position, renderHandles)
}
is ObservableList.Modification.Insertion -> {
insertItem(change.position, change.item, renderHandles)
}
is ObservableList.Modification.Move -> {
if (change.oldPosition == change.newPosition) {
continue
}
if (change.oldPosition > change.newPosition) {
moveItemClientSide(renderHandles[change.oldPosition].renderFragment.startId,
renderHandles[change.oldPosition].renderFragment.endId,
renderHandles[change.newPosition].renderFragment.startId)
renderHandles.add(change.newPosition, RenderHandle(renderHandles[change.oldPosition].renderFragment, renderHandles[change.oldPosition].kvar))
renderHandles.removeAt(change.oldPosition+1)
}
else { //change.newPosition > change.oldPosition
val newRenderHandle = RenderHandle(renderHandles[change.oldPosition].renderFragment, renderHandles[change.oldPosition].kvar)
val startId = if (change.newPosition == renderHandles.size-1) {
listFragment.endId
} else {
renderHandles[change.newPosition+1].renderFragment.startId
}
moveItemClientSide(renderHandles[change.oldPosition].renderFragment.startId,
renderHandles[change.oldPosition].renderFragment.endId,
startId)
if (change.newPosition == renderHandles.size-1) {
renderHandles.add(newRenderHandle)
} else {
renderHandles.add(change.newPosition+1, newRenderHandle)
}
renderHandles.removeAt(change.oldPosition)
}
}
}
}
}
}
onCleanup(true) {
observableList.removeListener(handle)
}
}
}