From 50b7feb264075344707d82cc6c3d0d6c65a2a0cf Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Thu, 26 May 2022 12:57:00 +0200 Subject: [PATCH 01/35] wip --- .../src/main/scala/snabbdom/CreateHook.scala | 2 +- .../src/main/scala/snabbdom/DestroyHook.scala | 2 +- .../src/main/scala/snabbdom/InitHook.scala | 2 +- .../src/main/scala/snabbdom/InsertHook.scala | 2 +- .../src/main/scala/snabbdom/PostHook.scala | 2 +- .../src/main/scala/snabbdom/PreHook.scala | 2 +- .../main/scala/snabbdom/PrePatchHook.scala | 2 +- .../src/main/scala/snabbdom/RemoveHook.scala | 2 +- .../src/main/scala/snabbdom/UpdateHook.scala | 2 +- snabbdom/src/main/scala/snabbdom/VNode.scala | 16 +- snabbdom/src/main/scala/snabbdom/h.scala | 16 +- snabbdom/src/main/scala/snabbdom/init.scala | 215 ++++++------ .../scala/snabbdom/modules/Attributes.scala | 14 +- .../main/scala/snabbdom/modules/Classes.scala | 14 +- .../main/scala/snabbdom/modules/Dataset.scala | 14 +- .../snabbdom/modules/EventListeners.scala | 39 ++- .../main/scala/snabbdom/modules/Props.scala | 14 +- .../main/scala/snabbdom/modules/Styles.scala | 14 +- snabbdom/src/main/scala/snabbdom/thunk.scala | 22 +- .../src/main/scala/snabbdom/toVNode.scala | 4 +- .../test/scala/snabbdom/SnabbdomSuite.scala | 323 ++++++++++-------- 21 files changed, 408 insertions(+), 315 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/CreateHook.scala b/snabbdom/src/main/scala/snabbdom/CreateHook.scala index 20b1023..e6af763 100644 --- a/snabbdom/src/main/scala/snabbdom/CreateHook.scala +++ b/snabbdom/src/main/scala/snabbdom/CreateHook.scala @@ -40,6 +40,6 @@ package snabbdom trait CreateHook { - def apply(emptyVNode: VNode, vNode: VNode): Any + def apply(vNode: VNode): Unit } diff --git a/snabbdom/src/main/scala/snabbdom/DestroyHook.scala b/snabbdom/src/main/scala/snabbdom/DestroyHook.scala index 4bd5226..68980e0 100644 --- a/snabbdom/src/main/scala/snabbdom/DestroyHook.scala +++ b/snabbdom/src/main/scala/snabbdom/DestroyHook.scala @@ -40,6 +40,6 @@ package snabbdom trait DestroyHook { - def apply(vNode: VNode): Any + def apply(vNode: VNode): Unit } diff --git a/snabbdom/src/main/scala/snabbdom/InitHook.scala b/snabbdom/src/main/scala/snabbdom/InitHook.scala index 47f137f..bfb5153 100644 --- a/snabbdom/src/main/scala/snabbdom/InitHook.scala +++ b/snabbdom/src/main/scala/snabbdom/InitHook.scala @@ -40,5 +40,5 @@ package snabbdom trait InitHook { - def apply(vNode: VNode): Any + def apply(vNode: VNode): Unit } diff --git a/snabbdom/src/main/scala/snabbdom/InsertHook.scala b/snabbdom/src/main/scala/snabbdom/InsertHook.scala index 2800faf..847a027 100644 --- a/snabbdom/src/main/scala/snabbdom/InsertHook.scala +++ b/snabbdom/src/main/scala/snabbdom/InsertHook.scala @@ -40,6 +40,6 @@ package snabbdom trait InsertHook { - def apply(vNode: VNode): Any + def apply(vNode: VNode): Unit } diff --git a/snabbdom/src/main/scala/snabbdom/PostHook.scala b/snabbdom/src/main/scala/snabbdom/PostHook.scala index 561a815..d47453b 100644 --- a/snabbdom/src/main/scala/snabbdom/PostHook.scala +++ b/snabbdom/src/main/scala/snabbdom/PostHook.scala @@ -40,6 +40,6 @@ package snabbdom trait PostHook { - def apply(): Any + def apply(): Unit } diff --git a/snabbdom/src/main/scala/snabbdom/PreHook.scala b/snabbdom/src/main/scala/snabbdom/PreHook.scala index 015d4e1..7ee76a0 100644 --- a/snabbdom/src/main/scala/snabbdom/PreHook.scala +++ b/snabbdom/src/main/scala/snabbdom/PreHook.scala @@ -40,6 +40,6 @@ package snabbdom trait PreHook { - def apply(): Any + def apply(): Unit } diff --git a/snabbdom/src/main/scala/snabbdom/PrePatchHook.scala b/snabbdom/src/main/scala/snabbdom/PrePatchHook.scala index 456e19d..31ee109 100644 --- a/snabbdom/src/main/scala/snabbdom/PrePatchHook.scala +++ b/snabbdom/src/main/scala/snabbdom/PrePatchHook.scala @@ -40,6 +40,6 @@ package snabbdom trait PrePatchHook { - def apply(oldVNode: VNode, vNode: VNode): Any + def apply(oldVNode: VNode, vNode: VNode): Unit } diff --git a/snabbdom/src/main/scala/snabbdom/RemoveHook.scala b/snabbdom/src/main/scala/snabbdom/RemoveHook.scala index 0669231..44ad10f 100644 --- a/snabbdom/src/main/scala/snabbdom/RemoveHook.scala +++ b/snabbdom/src/main/scala/snabbdom/RemoveHook.scala @@ -40,6 +40,6 @@ package snabbdom trait RemoveHook { - def apply(vNode: VNode, removeCallback: () => Unit): Any + def apply(vNode: VNode, removeCallback: () => Unit): Unit } diff --git a/snabbdom/src/main/scala/snabbdom/UpdateHook.scala b/snabbdom/src/main/scala/snabbdom/UpdateHook.scala index a5fc0e7..64ee9ff 100644 --- a/snabbdom/src/main/scala/snabbdom/UpdateHook.scala +++ b/snabbdom/src/main/scala/snabbdom/UpdateHook.scala @@ -40,6 +40,6 @@ package snabbdom trait UpdateHook { - def apply(oldVNode: VNode, vNode: VNode): Any + def apply(oldVNode: VNode, vNode: VNode): VNode } diff --git a/snabbdom/src/main/scala/snabbdom/VNode.scala b/snabbdom/src/main/scala/snabbdom/VNode.scala index d460db0..b9ababb 100644 --- a/snabbdom/src/main/scala/snabbdom/VNode.scala +++ b/snabbdom/src/main/scala/snabbdom/VNode.scala @@ -40,16 +40,16 @@ package snabbdom import org.scalajs.dom -class VNode private ( - var sel: Option[String], - var data: VNodeData, - var children: Option[Array[VNode]], - var elm: Option[ +case class VNode private ( + sel: Option[String], + data: VNodeData, + children: Option[Array[VNode]], + elm: Option[ dom.Node ], // can't be `dom.Element` unfortunately b/c of fragments - var text: Option[String], - val key: Option[KeyValue], - private[snabbdom] var listener: Option[Listener] + text: Option[String], + key: Option[KeyValue], + listener: Option[Listener] ) { override def toString: String = diff --git a/snabbdom/src/main/scala/snabbdom/h.scala b/snabbdom/src/main/scala/snabbdom/h.scala index 4990364..e99763e 100644 --- a/snabbdom/src/main/scala/snabbdom/h.scala +++ b/snabbdom/src/main/scala/snabbdom/h.scala @@ -87,16 +87,20 @@ object h { (sel.length == 3 || sel(3) == '.' || sel(3) == '#') ) { addNS(vnode) + } else { + vnode } - vnode } - private[snabbdom] def addNS(vnode: VNode): Unit = { + private[snabbdom] def addNS(vnode: VNode): VNode = { val ns = "http://www.w3.org/2000/svg" - vnode.data = vnode.data.copy(ns = Some(ns)) - if (vnode.sel.forall(_ != "foreignObject")) { - vnode.children.foreach(_.map(addNS)) - } + vnode.copy( + data = vnode.data.copy(ns = Some(ns)), + children = + if (vnode.sel.forall(_ != "foreignObject")) + vnode.children.map(_.map(addNS)) + else vnode.children + ) } } diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index 82c9337..6d6f794 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -109,7 +109,7 @@ object init { } } - def createElm(vnode: VNode, insertedVNodeQueue: VNodeQueue): dom.Node = { + def createElm(vnode: VNode, insertedVNodeQueue: VNodeQueue): VNode = { var data = vnode.data @@ -124,8 +124,10 @@ object init { val sel = vnode.sel sel match { case Some("!") => - vnode.text = Some(vnode.text.getOrElse("")) - vnode.elm = Some(api.createComment(vnode.text.get)) + vnode.copy( + text = Some(vnode.text.getOrElse("")), + elm = Some(api.createComment(vnode.text.get)) + ) case Some(sel) => val hashIdx = sel.indexOf("#") @@ -145,7 +147,11 @@ object init { } else { api.createElement(tag) // TODO what about data argument? } - vnode.elm = Some(elm) + val vnode0 = vnode.copy( + elm = Some(elm), + children = + vnode.children.map(_.map(ch => createElm(ch, insertedVNodeQueue))) + ) if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot)) if (dotIdx > 0) { elm.setAttribute( @@ -153,45 +159,52 @@ object init { sel.slice(dot + 1, sel.length).replaceAll("""\.""", " ") ) } - cbs.create.foreach(_.apply(emptyNode, vnode)) - vnode.children match { + cbs.create.foreach(_.apply(vnode0)) + vnode0.children match { case None => - vnode.text match { + vnode0.text match { case None => () case Some(text) => api.appendChild(elm, api.createTextNode(text)) } case Some(children) => children.foreach { child => - api.appendChild(elm, createElm(child, insertedVNodeQueue)) + api.appendChild(elm, child.elm.get) } } - vnode.data.hook.map { hooks => - hooks.create.foreach(hook => hook(emptyNode, vnode)) + vnode0.data.hook.map { hooks => + hooks.create.foreach(hook => hook(vnode)) hooks.insert.foreach { _ => insertedVNodeQueue.append(vnode) } } + vnode0 case None => vnode.children match { case None => - vnode.elm = Some(api.createTextNode(vnode.text.getOrElse(""))) + vnode.copy(elm = + Some(api.createTextNode(vnode.text.getOrElse(""))) + ) case Some(children) => val elm = api.createDocumentFragment - vnode.elm = Some(elm) - cbs.create.foreach(hook => hook(emptyNode, vnode)) - children.foreach { child => - api.appendChild( - elm, - createElm(child, insertedVNodeQueue) + val vnode0 = vnode.copy( + elm = Some(elm), + children = + Some(children.map(ch => createElm(ch, insertedVNodeQueue))) + ) + cbs.create.foreach(hook => hook(vnode0)) + vnode0.children.foreach { children => + children.foreach(ch => + api.appendChild( + elm, + ch.elm.get + ) ) - } + vnode0 } } - vnode.elm.get - } def addVnodes( @@ -204,8 +217,12 @@ object init { ): Unit = { var i = startIdx while (i <= endIdx) { - val ch = vnodes(i) - api.insertBefore(parentElm, createElm(ch, insertedVNodeQueue), before) + vnodes(i) = createElm(vnodes(i), insertedVNodeQueue) + api.insertBefore( + parentElm, + vnodes(i).elm.get, + before + ) i += 1 } } @@ -261,73 +278,56 @@ object init { var oldStartIdx = 0 var newStartIdx = 0 var oldEndIdx = oldCh.length - 1 - var oldStartVnode = oldCh(0) - var oldEndVnode = oldCh(oldEndIdx) var newEndIdx = newCh.length - 1 - var newStartVnode = newCh(0) - var newEndVnode = newCh(newEndIdx) var oldKeyToIdx: Map[String, Int] = null while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { - if (oldStartVnode == null) { - oldStartIdx += 1 + if (oldCh(oldStartIdx) == null) { // Vnode might have been moved left - if (oldStartIdx <= oldEndIdx) - oldStartVnode = oldCh(oldStartIdx) - } else if (oldEndVnode == null) { + oldStartIdx += 1 + } else if (oldCh(oldEndIdx) == null) { oldEndIdx -= 1 - if (oldStartIdx <= oldEndIdx) - oldEndVnode = oldCh(oldEndIdx) - } else if (sameVnode(oldStartVnode, newStartVnode)) { - patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) + } else if (sameVnode(oldCh(oldStartIdx), newCh(newStartIdx))) { + newCh(newStartIdx) = patchVnode( + oldCh(oldStartIdx), + newCh(newStartIdx), + insertedVnodeQueue + ) oldStartIdx += 1 - if (oldStartIdx <= oldEndIdx) - oldStartVnode = oldCh(oldStartIdx) newStartIdx += 1 - if (newStartIdx <= newEndIdx) - newStartVnode = newCh(newStartIdx) - } else if (sameVnode(oldEndVnode, newEndVnode)) { - patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) + } else if (sameVnode(oldCh(oldEndIdx), newCh(newEndIdx))) { + newCh(newEndIdx) = + patchVnode(oldCh(oldEndIdx), newCh(newEndIdx), insertedVnodeQueue) oldEndIdx -= 1 - if (oldStartIdx <= oldEndIdx) - oldEndVnode = oldCh(oldEndIdx) newEndIdx -= 1 - if (newStartIdx <= newEndIdx) - newEndVnode = newCh(newEndIdx) - } else if (sameVnode(oldStartVnode, newEndVnode)) { + } else if (sameVnode(oldCh(oldStartIdx), newCh(newEndIdx))) { // Vnode moved right - patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) + newCh(newEndIdx) = + patchVnode(oldCh(oldStartIdx), newCh(newEndIdx), insertedVnodeQueue) api.insertBefore( parentElm, - oldStartVnode.elm.get, - api.nextSibling(oldEndVnode.elm.get) + oldCh(oldStartIdx).elm.get, + api.nextSibling(oldCh(oldEndIdx).elm.get) ) oldStartIdx += 1 - if (oldStartIdx <= oldEndIdx) - oldStartVnode = oldCh(oldStartIdx) newEndIdx -= 1 - if (newStartIdx <= newEndIdx) - newEndVnode = newCh(newEndIdx) - } else if (sameVnode(oldEndVnode, newStartVnode)) { + } else if (sameVnode(oldCh(oldEndIdx), newCh(newStartIdx))) { // Vnode moved left - patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) + newCh(newStartIdx) = + patchVnode(oldCh(oldEndIdx), newCh(newStartIdx), insertedVnodeQueue) api.insertBefore( parentElm, - oldEndVnode.elm.get, - oldStartVnode.elm + oldCh(oldEndIdx).elm.get, + oldCh(oldStartIdx).elm ) oldEndIdx -= 1 - if (oldStartIdx <= oldEndIdx) - oldEndVnode = oldCh(oldEndIdx) newStartIdx += 1 - if (newStartIdx <= newEndIdx) - newStartVnode = newCh(newStartIdx) } else { if (oldKeyToIdx == null) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) } - val idxInOld = newStartVnode.key.flatMap { key => + val idxInOld = newCh(newStartIdx).key.flatMap { key => oldKeyToIdx.get(key) } idxInOld match { @@ -335,30 +335,31 @@ object init { // New element api.insertBefore( parentElm, - createElm(newStartVnode, insertedVnodeQueue), - oldStartVnode.elm + createElm(newCh(newStartIdx), insertedVnodeQueue).elm.get, + oldCh(oldStartIdx).elm ) case Some(idxInOld) => val elmToMove = oldCh(idxInOld) - if (elmToMove.sel != newStartVnode.sel) { + if (elmToMove.sel != newCh(newStartIdx).sel) { + newCh(newStartIdx) = + createElm(newCh(newStartIdx), insertedVnodeQueue) api.insertBefore( parentElm, - createElm(newStartVnode, insertedVnodeQueue), - oldStartVnode.elm + newCh(newStartIdx).elm.get, + oldCh(oldStartIdx).elm ) } else { - patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) + newCh(newStartIdx) = + patchVnode(elmToMove, newCh(newStartIdx), insertedVnodeQueue) oldCh(idxInOld) = null api.insertBefore( parentElm, elmToMove.elm.get, - oldStartVnode.elm + oldCh(oldStartIdx).elm ) } } newStartIdx += 1 - if (newStartIdx <= newEndIdx) - newStartVnode = newCh(newStartIdx) } } @@ -383,79 +384,104 @@ object init { def patchVnode( oldVnode: VNode, - vnode: VNode, + vnode0: VNode, insertedVNodeQueue: VNodeQueue - ): Unit = { - val hook = vnode.data.hook - hook.flatMap(_.prepatch).foreach(hook => hook(oldVnode, vnode)) + ): VNode = { + val hook = vnode0.data.hook + hook.flatMap(_.prepatch).foreach(hook => hook(oldVnode, vnode0)) val elm = oldVnode.elm.get - vnode.elm = Some(elm) val oldCh = oldVnode.children - val ch = vnode.children - if (oldVnode != vnode) { + if (oldVnode != vnode0) { + + val vnode = cbs.update.foldLeft(vnode0.copy(elm = Some(elm))) { + case (vnode, hook) => + hook(oldVnode, vnode) + } - cbs.update.foreach(hook => hook(oldVnode, vnode)) vnode.data.hook .flatMap(_.update) .foreach(hook => hook(oldVnode, vnode)) - vnode.text match { + val vnode1 = vnode.text match { case None => - (oldCh, ch) match { + (oldCh, vnode.children) match { case (Some(oldCh), Some(ch)) => if (oldCh != ch) { updateChildren(elm, oldCh, ch, insertedVNodeQueue) + vnode + } else { + vnode } case (None, Some(ch)) => oldVnode.text.foreach(_ => api.setTextContent(elm, Some(""))) - addVnodes(elm, None, ch, 0, ch.length - 1, insertedVNodeQueue) + Some( + addVnodes( + elm, + None, + ch, + 0, + ch.length - 1, + insertedVNodeQueue + ) + ) + vnode + case (Some(oldCh), None) => removeVnodes(elm, oldCh, 0, oldCh.length - 1) + vnode case (None, None) => oldVnode.text.foreach(_ => api.setTextContent(elm, Some(""))) + vnode } case Some(text) if oldVnode.text.forall(_ != text) => oldCh.foreach(oldChildren => removeVnodes(elm, oldChildren, 0, oldChildren.length - 1) ) api.setTextContent(elm, Some(text)) - case Some(_) => () + vnode + case Some(_) => vnode } - hook.flatMap(_.postpatch).foreach(hook => hook(oldVnode, vnode)) + hook.flatMap(_.postpatch).foreach(hook => hook(oldVnode, vnode1)) + + vnode1 + + } else { + + oldVnode } + } def patch(oldVnode: VNode, vnode: VNode): VNode = { val insertedVNodeQueue: VNodeQueue = mutable.ArrayBuffer.empty[VNode] - cbs.pre.foreach(hook => hook()) + cbs.pre.foreach(_()) - if (sameVnode(oldVnode, vnode)) { + val vnode0 = if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVNodeQueue) } else { val elm = oldVnode.elm.get val parent = api.parentNode(elm) - createElm(vnode, insertedVNodeQueue) + val vnode1 = createElm(vnode, insertedVNodeQueue) parent match { case Some(parent) => - api.insertBefore(parent, vnode.elm.get, api.nextSibling(elm)) + api.insertBefore(parent, vnode1.elm.get, api.nextSibling(elm)) removeVnodes(parent, Array(oldVnode), 0, 0) case None => () } + vnode1 } insertedVNodeQueue.foreach(vnode => - vnode.data.hook - .flatMap(_.insert) - .foreach(hook => hook(vnode)) + vnode.data.hook.flatMap(_.insert).foreach(_(vnode)) ) - cbs.post.foreach(hook => hook()) + cbs.post.foreach(_()) - vnode + vnode0 } @@ -474,9 +500,6 @@ object init { } - private val emptyNode = - VNode.create(Some(""), VNodeData.empty, None, None, None) - private def sameVnode(vnode1: VNode, vnode2: VNode): Boolean = { vnode1.key == vnode2.key && vnode1.data.is == vnode2.data.is && diff --git a/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala b/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala index 6142dbf..38bdca2 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala @@ -45,19 +45,21 @@ object Attributes { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(emptyVNode: VNode, vNode: VNode): Any = - updateAttrs(emptyVNode, vNode) + override def apply(vNode: VNode): Unit = + updateAttrs(None, vNode) }), update = Some(new UpdateHook { - override def apply(oldVNode: VNode, vNode: VNode): Any = - updateAttrs(oldVNode, vNode) + override def apply(oldVNode: VNode, vNode: VNode): VNode = { + updateAttrs(Some(oldVNode), vNode) + vNode + } }) ) private val xlinkNS = "http://www.w3.org/1999/xlink" private val xmlNS = "http://www.w3.org/XML/1998/namespace" - private def updateAttrs(oldVnode: VNode, vnode: VNode): Unit = { + private def updateAttrs(oldVnode: Option[VNode], vnode: VNode): Unit = { val elm = vnode.elm.get.asInstanceOf[dom.Element] @@ -93,7 +95,7 @@ object Attributes { } } - val oldAttrs = oldVnode.data.attrs + val oldAttrs = oldVnode.map(_.data.attrs).getOrElse(Map.empty) val attrs = vnode.data.attrs if (oldAttrs != attrs) { diff --git a/snabbdom/src/main/scala/snabbdom/modules/Classes.scala b/snabbdom/src/main/scala/snabbdom/modules/Classes.scala index 6476183..86ca80a 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Classes.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Classes.scala @@ -45,16 +45,18 @@ object Classes { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(emptyVNode: VNode, vNode: VNode): Any = - updateClasses(emptyVNode, vNode) + override def apply(vNode: VNode): Unit = + updateClasses(None, vNode) }), update = Some(new UpdateHook { - override def apply(oldVNode: VNode, vNode: VNode): Any = - updateClasses(oldVNode, vNode) + override def apply(oldVNode: VNode, vNode: VNode): VNode = { + updateClasses(Some(oldVNode), vNode) + vNode + } }) ) - private def updateClasses(oldVnode: VNode, vnode: VNode): Unit = { + private def updateClasses(oldVnode: Option[VNode], vnode: VNode): Unit = { val elm = vnode.elm.get.asInstanceOf[dom.Element] @@ -78,7 +80,7 @@ object Classes { } } - val oldClasses = oldVnode.data.classes + val oldClasses = oldVnode.map(_.data.classes).getOrElse(Map.empty) val classes = vnode.data.classes if (oldClasses != classes) { diff --git a/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala b/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala index ce7737b..8956e89 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala @@ -46,21 +46,23 @@ object Dataset { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(emptyVNode: VNode, vNode: VNode): Any = - updateDataset(emptyVNode, vNode) + override def apply(vNode: VNode): Unit = + updateDataset(None, vNode) }), update = Some(new UpdateHook { - override def apply(oldVNode: VNode, vNode: VNode): Any = - updateDataset(oldVNode, vNode) + override def apply(oldVNode: VNode, vNode: VNode): VNode = { + updateDataset(Some(oldVNode), vNode) + vNode + } }) ) private val CAPS_REGEX = "[A-Z]" - private def updateDataset(oldVnode: VNode, vnode: VNode): Unit = { + private def updateDataset(oldVnode: Option[VNode], vnode: VNode): Unit = { val elm = vnode.elm.get.asInstanceOf[dom.HTMLElement] - val oldDataset = oldVnode.data.dataset + val oldDataset = oldVnode.map(_.data.dataset).getOrElse(Map.empty) val dataset = vnode.data.dataset val d = elm.dataset diff --git a/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala b/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala index da418d3..789936b 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala @@ -45,16 +45,20 @@ object EventListeners { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(emptyVNode: VNode, vNode: VNode): Any = - updateEventListeners(emptyVNode, Some(vNode)) + override def apply(vNode: VNode): Unit = { + updateEventListeners(None, Some(vNode)) + () + } }), update = Some(new UpdateHook { - override def apply(oldVNode: VNode, vNode: VNode): Any = - updateEventListeners(oldVNode, Some(vNode)) + override def apply(oldVNode: VNode, vNode: VNode): VNode = + updateEventListeners(Some(oldVNode), Some(vNode)).get }), destroy = Some(new DestroyHook { - override def apply(vnode: VNode): Any = - updateEventListeners(vnode, None) + override def apply(vnode: VNode): Unit = { + updateEventListeners(Some(vnode), None) + () + } }) ) @@ -63,13 +67,13 @@ object EventListeners { } private def updateEventListeners( - oldVnode: VNode, + oldVnode: Option[VNode], vnode: Option[VNode] - ): Unit = { + ): Option[VNode] = { - val oldOn = oldVnode.data.on - val oldListener = oldVnode.listener - val oldElm = oldVnode.elm.map(_.asInstanceOf[dom.Element]) + val oldOn = oldVnode.map(_.data.on).getOrElse(Map.empty) + val oldListener = oldVnode.flatMap(_.listener) + val oldElm = oldVnode.flatMap(_.elm).map(_.asInstanceOf[dom.Element]) val on = vnode.map(_.data.on).getOrElse(Map.empty) val elm = vnode.flatMap(_.elm).map(_.asInstanceOf[dom.Element]) @@ -96,9 +100,10 @@ object EventListeners { if (on.nonEmpty) { - val listener = oldListener.getOrElse(createListener(vnode.get)) - listener.vnode = vnode.get - vnode.foreach(_.listener = Some(listener)) + val vnode0 = vnode.get + + val listener = oldListener.getOrElse(createListener(vnode0)) + listener.vnode = vnode0 if (oldOn.isEmpty) { on.foreach { case (name, _) => @@ -112,8 +117,14 @@ object EventListeners { } } + Some(vnode0.copy(listener = Some(listener))) + + } else { + None } + } else { + vnode } } diff --git a/snabbdom/src/main/scala/snabbdom/modules/Props.scala b/snabbdom/src/main/scala/snabbdom/modules/Props.scala index e811780..0294217 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Props.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Props.scala @@ -45,18 +45,20 @@ object Props { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(emptyVNode: VNode, vNode: VNode): Any = - updateProps(emptyVNode, vNode) + override def apply(vNode: VNode): Unit = + updateProps(None, vNode) }), update = Some(new UpdateHook { - override def apply(oldVNode: VNode, vNode: VNode): Any = - updateProps(oldVNode, vNode) + override def apply(oldVNode: VNode, vNode: VNode): VNode = { + updateProps(Some(oldVNode), vNode) + vNode + } }) ) - private def updateProps(oldVnode: VNode, vnode: VNode): Unit = { + private def updateProps(oldVnode: Option[VNode], vnode: VNode): Unit = { val elm = vnode.elm.get - val oldProps = oldVnode.data.props + val oldProps = oldVnode.map(_.data.props).getOrElse(Map.empty) val props = vnode.data.props def update( diff --git a/snabbdom/src/main/scala/snabbdom/modules/Styles.scala b/snabbdom/src/main/scala/snabbdom/modules/Styles.scala index a2e9fea..b0f0bf5 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Styles.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Styles.scala @@ -47,16 +47,18 @@ object Styles { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(emptyVNode: VNode, vNode: VNode): Any = - updateStyle(emptyVNode, vNode) + override def apply(vNode: VNode): Unit = + updateStyle(None, vNode) }), update = Some(new UpdateHook { - override def apply(oldVNode: VNode, vNode: VNode): Any = - updateStyle(oldVNode, vNode) + override def apply(oldVNode: VNode, vNode: VNode): VNode = { + updateStyle(Some(oldVNode), vNode) + vNode + } }) ) - private def updateStyle(oldVnode: VNode, vnode: VNode): Unit = { + private def updateStyle(oldVnode: Option[VNode], vnode: VNode): Unit = { val elm = vnode.elm.get @@ -100,7 +102,7 @@ object Styles { } - val oldStyle = oldVnode.data.style + val oldStyle = oldVnode.map(_.data.style).getOrElse(Map.empty) val style = vnode.data.style if (oldStyle != style) { diff --git a/snabbdom/src/main/scala/snabbdom/thunk.scala b/snabbdom/src/main/scala/snabbdom/thunk.scala index 1e708f6..9a7cd69 100644 --- a/snabbdom/src/main/scala/snabbdom/thunk.scala +++ b/snabbdom/src/main/scala/snabbdom/thunk.scala @@ -69,14 +69,14 @@ object thunk { h(sel, data) } - private def init0(thunk: VNode): Unit = { + private def init0(thunk: VNode): VNode = { val data = thunk.data val fn = data.fn.get val args = data.args.get - copyToThunk(fn(args), thunk) + fn(args) } - private def prepatch0(oldVnode: VNode, thunk: VNode): Unit = { + private def prepatch0(oldVnode: VNode, thunk: VNode): VNode = { val old = oldVnode.data val cur = thunk.data val oldArgs = old.args @@ -84,22 +84,10 @@ object thunk { val oldFn = old.fn val curFn = cur.fn if (oldFn != curFn || oldArgs != args) { - copyToThunk(curFn.get(args.get), thunk) + curFn.get(args.get) } else { - copyToThunk(oldVnode, thunk) + oldVnode } } - private def copyToThunk(vnode: VNode, thunk: VNode): Unit = { - val ns = thunk.data.ns - val fn = thunk.data.fn - val args = thunk.data.args - vnode.data = vnode.data.copy(fn = fn, args = args) - thunk.data = vnode.data - thunk.children = vnode.children - thunk.text = vnode.text - thunk.elm = vnode.elm - ns.foreach(_ => h.addNS(thunk)) - } - } diff --git a/snabbdom/src/main/scala/snabbdom/toVNode.scala b/snabbdom/src/main/scala/snabbdom/toVNode.scala index 1b53c7b..59c3ee6 100644 --- a/snabbdom/src/main/scala/snabbdom/toVNode.scala +++ b/snabbdom/src/main/scala/snabbdom/toVNode.scala @@ -93,10 +93,10 @@ object toVNode { ) == '#') ) { h.addNS(vnode) + } else { + vnode } - vnode - } else if (api.isText(node)) { val text = api.getTextContent(node).getOrElse("") VNode.create(None, VNodeData.empty, None, Some(text), Some(node)) diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index 21fa6da..3f686cf 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -343,8 +343,8 @@ class SnabbdomSuite extends BaseSuite { "i", VNodeData(classes = Map("i" -> true, "am" -> true, "horse" -> false)) ) - patch(vnode0, vnode1) - val elm = patch(vnode1, vnode2).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = patch(vnode1p, vnode2).elm.get assert(elm.asInstanceOf[dom.Element].classList.contains("i")) assert(elm.asInstanceOf[dom.Element].classList.contains("am")) assert(!elm.asInstanceOf[dom.Element].classList.contains("horse")) @@ -359,11 +359,12 @@ class SnabbdomSuite extends BaseSuite { VNodeData(classes = Map("i" -> true, "am" -> true, "horse" -> false)) val vnode1 = h("i", cachedClasses) val vnode2 = h("i", cachedClasses) - val elm = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.Element] + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get.asInstanceOf[dom.Element] assert(elm.classList.contains("i")) assert(elm.classList.contains("am")) assert(!elm.classList.contains("horse")) - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] assert(elm2.classList.contains("i")) assert(elm2.classList.contains("am")) assert(!elm2.classList.contains("horse")) @@ -378,8 +379,8 @@ class SnabbdomSuite extends BaseSuite { "i", VNodeData(classes = Map("i" -> true, "am" -> true)) ) - patch(vnode0, vnode1) - val elm = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.Element] + val vnode1p = patch(vnode0, vnode1) + val elm = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] assert(elm.classList.contains("i")) assert(elm.classList.contains("am")) assert(!elm.classList.contains("horse")) @@ -390,9 +391,9 @@ class SnabbdomSuite extends BaseSuite { h("a", VNodeData(props = Map("src" -> "http://other/"))) val vnode2 = h("a", VNodeData(props = Map("src" -> "http://localhost/"))) - patch(vnode0, vnode1) + val vnode1p = patch(vnode0, vnode1) val elm = - patch(vnode1, vnode2).elm.get.asInstanceOf[js.Dictionary[String]] + patch(vnode1p, vnode2).elm.get.asInstanceOf[js.Dictionary[String]] assertEquals(elm("src"), "http://localhost/") } @@ -400,12 +401,13 @@ class SnabbdomSuite extends BaseSuite { val cachedProps = VNodeData(props = Map("src" -> "http://other/")) val vnode1 = h("a", cachedProps) val vnode2 = h("a", cachedProps) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals( elm.asInstanceOf[js.Dictionary[String]]("src"), "http://other/" ) - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals( elm2.asInstanceOf[js.Dictionary[String]]("src"), "http://other/" @@ -415,14 +417,15 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("can set prop value to empty string") { vnode0 => val vnode1 = h("p", VNodeData(props = Map("textContent" -> "foo"))) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals( elm.asInstanceOf[dom.HTMLParagraphElement].textContent, "foo" ) val vnode2 = h("p", VNodeData(props = Map("textContent" -> ""))) - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.asInstanceOf[dom.HTMLParagraphElement].textContent, "") } @@ -432,8 +435,8 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("a", VNodeData(props = Map("src" -> "http://other/"))) val vnode2 = h("a") - patch(vnode0, vnode1) - val elm = patch(vnode1, vnode2).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = patch(vnode1p, vnode2).elm.get assert(!elm.asInstanceOf[js.Dictionary[String]].contains("src")) } @@ -441,13 +444,14 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("a", VNodeData(props = Map("href" -> "http://example.com/"))) val vnode2 = h("a") - val elm1 = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get assert(elm1.isInstanceOf[dom.HTMLAnchorElement]) assertEquals( elm1.asInstanceOf[dom.HTMLAnchorElement].href, "http://example.com/" ) - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assert(elm2.isInstanceOf[dom.HTMLAnchorElement]) assertEquals( elm2.asInstanceOf[dom.HTMLAnchorElement].href, @@ -459,10 +463,11 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("does not delete custom props") { vnode0 => val vnode1 = h("p", VNodeData(props = Map("a" -> "foo"))) val vnode2 = h("p") - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assert(elm.isInstanceOf[dom.HTMLParagraphElement]) assertEquals(elm.asInstanceOf[js.Dictionary[String]]("a"), "foo") - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.asInstanceOf[js.Dictionary[String]]("a"), "foo") } } @@ -645,9 +650,11 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("appends elements") { vnode0 => val vnode1 = h("span", Array("1").map(spanNum)) val vnode2 = h("span", Array("1", "2", "3").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 1) - val elm2 = patch(vnode1, vnode2).elm.get + val vnode12 = patch(vnode1p, vnode2) + val elm2 = vnode12.elm.get assertEquals(elm2.asInstanceOf[dom.Element].children.length, 3) assertEquals(elm2.asInstanceOf[dom.Element].children(1).innerHTML, "2") assertEquals(elm2.asInstanceOf[dom.Element].children(2).innerHTML, "3") @@ -656,9 +663,11 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("prepends elements") { vnode0 => val vnode1 = h("span", Array("4", "5").map(spanNum)) val vnode2 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 2) - val elm2 = patch(vnode1, vnode2).elm.get + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3", "4", "5") @@ -668,9 +677,11 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("add elements in the middle") { vnode0 => val vnode1 = h("span", Array("1", "2", "4", "5").map(spanNum)) val vnode2 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) - val elm2 = patch(vnode1, vnode2).elm.get + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3", "4", "5") @@ -680,9 +691,11 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("add elements at begin and end") { vnode0 => val vnode1 = h("span", Array("2", "3", "4").map(spanNum)) val vnode2 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 3) - val elm2 = patch(vnode1, vnode2).elm.get + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3", "4", "5") @@ -696,9 +709,11 @@ class SnabbdomSuite extends BaseSuite { VNodeData(key = Some("span")), Array("1", "2", "3").map(spanNum) ) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 0) - val elm2 = patch(vnode1, vnode2).elm.get + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3") @@ -712,12 +727,14 @@ class SnabbdomSuite extends BaseSuite { Array("1", "2", "3").map(spanNum) ) val vnode2 = h("span", VNodeData(key = Some("span"))) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals( elm.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3") ) - val elm2 = patch(vnode1, vnode2).elm.get + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get assertEquals(elm2.asInstanceOf[dom.Element].children.length, 0) } @@ -728,13 +745,15 @@ class SnabbdomSuite extends BaseSuite { val vnode2 = h("span", data, Array(spanNum("1"), h("i", data2, "2"), spanNum("3"))) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals( elm.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3") ) - val elm2 = patch(vnode1, vnode2).elm.get + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3") @@ -750,9 +769,11 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("removes elements from the beginning") { vnode0 => val vnode1 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) val vnode2 = h("span", Array("3", "4", "5").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 5) - val elm2 = patch(vnode1, vnode2).elm.get + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("3", "4", "5") @@ -762,9 +783,11 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("removes elements from the end") { vnode0 => val vnode1 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) val vnode2 = h("span", Array("1", "2", "3").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 5) - val elm2 = patch(vnode1, vnode2).elm.get + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3") @@ -774,9 +797,11 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("removes elements from the middle") { vnode0 => val vnode1 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) val vnode2 = h("span", Array("1", "2", "4", "5").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 5) - val elm2 = patch(vnode1, vnode2).elm.get + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get assertEquals(elm2.asInstanceOf[dom.Element].children.length, 4) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -789,9 +814,11 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("moves element forward") { vnode0 => val vnode1 = h("span", Array("1", "2", "3", "4").map(spanNum)) val vnode2 = h("span", Array("2", "3", "1", "4").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) - val elm2 = patch(vnode1, vnode2).elm.get + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get assertEquals(elm2.asInstanceOf[dom.Element].children.length, 4) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -802,9 +829,10 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("moves element to end") { vnode0 => val vnode1 = h("span", Array("1", "2", "3").map(spanNum)) val vnode2 = h("span", Array("2", "3", "1").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 3) - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.asInstanceOf[dom.Element].children.length, 3) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -815,9 +843,10 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("moves element backwards") { vnode0 => val vnode1 = h("span", Array("1", "2", "3", "4").map(spanNum)) val vnode2 = h("span", Array("1", "4", "2", "3").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.asInstanceOf[dom.Element].children.length, 4) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -828,9 +857,10 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("swaps first and last") { vnode0 => val vnode1 = h("span", Array("1", "2", "3", "4").map(spanNum)) val vnode2 = h("span", Array("4", "2", "3", "1").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.asInstanceOf[dom.Element].children.length, 4) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -843,9 +873,10 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("move to left and replace") { vnode0 => val vnode1 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) val vnode2 = h("span", Array("4", "1", "2", "3", "6").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 5) - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.asInstanceOf[dom.Element].children.length, 5) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -856,9 +887,10 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("moves to left and leaves hole") { vnode0 => val vnode1 = h("span", Array("1", "4", "5").map(spanNum)) val vnode2 = h("span", Array("4", "6").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 3) - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.asInstanceOf[dom.Element].children.length, 2) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -871,9 +903,10 @@ class SnabbdomSuite extends BaseSuite { ) { vnode0 => val vnode1 = h("span", Array("2", "4", "5").map(spanNum)) val vnode2 = h("span", Array("4", "5", "3").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 3) - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.asInstanceOf[dom.Element].children.length, 3) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -890,10 +923,11 @@ class SnabbdomSuite extends BaseSuite { spanNum("e") ) ) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) assertEquals(elm.textContent, "1abc") - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.asInstanceOf[dom.Element].children.length, 6) assertEquals(elm2.textContent, "dabc1e") } @@ -905,9 +939,10 @@ class SnabbdomSuite extends BaseSuite { h("span", Array("1", "2", "3", "4", "5", "6", "7", "8").map(spanNum)) val vnode2 = h("span", Array("8", "7", "6", "5", "4", "3", "2", "1").map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 8) - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.asInstanceOf[dom.Element].children.length, 8) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -920,9 +955,10 @@ class SnabbdomSuite extends BaseSuite { h("span", Array(0, 1, 2, 3, 4, 5).map(spanNum)) val vnode2 = h("span", Array(4, 3, 2, 1, 5, 0).map(spanNum)) - val elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 6) - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.asInstanceOf[dom.Element].children.length, 6) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -949,7 +985,8 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", arr.map(spanNumWithOpacity(_, "1"))) val shufArr = rng.shuffle(arr) val elm = dom.document.createElement("div") - val elm1 = patch(elm, vnode1).elm.get.asInstanceOf[dom.HTMLSpanElement] + val vnode1p = patch(elm, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLSpanElement] assertEquals( elm1.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), arr.map(_.toString).toList @@ -958,7 +995,7 @@ class SnabbdomSuite extends BaseSuite { val vnode2 = h("span", arr.map(n => spanNumWithOpacity(shufArr(n), opacities(n)))) val elm2 = - patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLSpanElement] + patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.HTMLSpanElement] (0 until elms).foreach { i => assertEquals(elm2.children(i).innerHTML, shufArr(i).toString) val opacity = @@ -973,72 +1010,80 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("appends elements") { vnode0 => val vnode1 = h("div", Array(h("span", "Hello"))) val vnode2 = h("div", Array(h("span", "Hello"), h("span", "World"))) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.Element] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] assertEquals(elm1.children.toSeq.map(_.innerHTML), List("Hello")) - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] assertEquals(elm2.children.toSeq.map(_.innerHTML), List("Hello", "World")) } vnode0.test("handles unmoved text nodes") { vnode0 => val vnode1 = h("div", Array[VNode]("Text", h("span", "Span"))) val vnode2 = h("div", Array[VNode]("Text", h("span", "Span"))) - val elm1 = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get assertEquals(elm1.childNodes(0).textContent, "Text") - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.childNodes(0).textContent, "Text") } vnode0.test("handles changing text children") { vnode0 => val vnode1 = h("div", Array[VNode]("Text", h("span", "Span"))) val vnode2 = h("div", Array[VNode]("Text2", h("span", "Span"))) - val elm1 = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get assertEquals(elm1.childNodes(0).textContent, "Text") - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.childNodes(0).textContent, "Text2") } vnode0.test("handles unmoved comment nodes") { vnode0 => val vnode1 = h("div", Array[VNode](h("!", "Text"), h("span", "Span"))) val vnode2 = h("div", Array[VNode](h("!", "Text"), h("span", "Span"))) - val elm1 = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get assertEquals(elm1.childNodes(0).textContent, "Text") - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.childNodes(0).textContent, "Text") } vnode0.test("handles changing comment text") { vnode0 => val vnode1 = h("div", Array[VNode](h("!", "Text"), h("span", "Span"))) val vnode2 = h("div", Array[VNode](h("!", "Text2"), h("span", "Span"))) - val elm1 = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get assertEquals(elm1.childNodes(0).textContent, "Text") - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.childNodes(0).textContent, "Text2") } vnode0.test("handles changing empty comment") { vnode0 => val vnode1 = h("div", Array[VNode](h("!"), h("span", "Span"))) val vnode2 = h("div", Array[VNode](h("!", "Test"), h("span", "Span"))) - val elm1 = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get assertEquals(elm1.childNodes(0).textContent, "") - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.childNodes(0).textContent, "Test") } vnode0.test("prepends element") { vnode0 => val vnode1 = h("div", Array(h("span", "World"))) val vnode2 = h("div", Array(h("span", "Hello"), h("span", "World"))) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.Element] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] assertEquals(elm1.children.toSeq.map(_.innerHTML), List("World")) - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] assertEquals(elm2.children.toSeq.map(_.innerHTML), List("Hello", "World")) } vnode0.test("prepends element of different tag type") { vnode0 => val vnode1 = h("div", Array(h("span", "World"))) val vnode2 = h("div", Array(h("div", "Hello"), h("span", "World"))) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.Element] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] assertEquals(elm1.children.toSeq.map(_.innerHTML), List("World")) - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] assertEquals(elm2.children.toSeq.map(_.innerHTML), List("Hello", "World")) assertEquals(elm2.children.toSeq.map(_.tagName), List("DIV", "SPAN")) } @@ -1047,21 +1092,23 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("div", Array(h("span", "One"), h("span", "Two"), h("span", "Three"))) val vnode2 = h("div", Array(h("span", "One"), h("span", "Three"))) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.Element] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] assertEquals( elm1.children.toSeq.map(_.innerHTML), List("One", "Two", "Three") ) - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] assertEquals(elm2.children.toSeq.map(_.innerHTML), List("One", "Three")) } vnode0.test("removes a single text node") { vnode0 => val vnode1 = h("div", "One") val vnode2 = h("div") - val elm1 = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get assertEquals(elm1.textContent, "One") - val elm2 = patch(vnode1, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.textContent, "") } @@ -1069,9 +1116,10 @@ class SnabbdomSuite extends BaseSuite { vnode0 => val vnode1 = h("div", "One") val vnode2 = h("div", Array(h("div", "Two"), h("span", "Three"))) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.Element] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] assertEquals(elm1.textContent, "One") - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] assertEquals( elm2.childNodes.toSeq.map(_.textContent), List("Two", "Three") @@ -1081,12 +1129,13 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("removes a text node among other elements") { vnode0 => val vnode1 = h("div", Array[VNode]("One", h("span", "Two"))) val vnode2 = h("div", Array(h("div", "Three"))) - val elm1 = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get assertEquals( elm1.childNodes.toSeq.map(_.textContent), List("One", "Two") ) - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] assertEquals(elm2.childNodes.length, 1) assertEquals(elm2.childNodes(0).asInstanceOf[dom.Element].tagName, "DIV") assertEquals(elm2.childNodes(0).textContent, "Three") @@ -1097,12 +1146,13 @@ class SnabbdomSuite extends BaseSuite { h("div", Array(h("span", "One"), h("div", "Two"), h("b", "Three"))) val vnode2 = h("div", Array(h("b", "Three"), h("span", "One"), h("div", "Two"))) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.Element] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] assertEquals( elm1.children.toSeq.map(_.innerHTML), List("One", "Two", "Three") ) - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] assertEquals(elm2.children.toSeq.map(_.tagName), List("B", "SPAN", "DIV")) assertEquals( elm2.children.toSeq.map(_.innerHTML), @@ -1122,14 +1172,17 @@ class SnabbdomSuite extends BaseSuite { val vnode2 = h("div", Array(VNode.text("I am an element"))) val vnode3 = fragment(Array("fragment ", "again")) - var elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + var elm = vnode1p.elm.get assertEquals(elm.nodeType, dom.Node.DOCUMENT_FRAGMENT_NODE) - elm = patch(vnode1, vnode2).elm.get + val vnode2p = patch(vnode1p, vnode2) + elm = vnode2p.elm.get assertEquals(elm.asInstanceOf[dom.Element].tagName, "DIV") assertEquals(elm.textContent, "I am an element") - elm = patch(vnode2, vnode3).elm.get + val vnode3p = patch(vnode2p, vnode3) + elm = vnode3p.elm.get assertEquals(elm.nodeType, dom.Node.DOCUMENT_FRAGMENT_NODE) assertEquals(elm.textContent, "fragment again") } @@ -1141,23 +1194,24 @@ class SnabbdomSuite extends BaseSuite { ) val vnode2 = h("div", "I am an element") - var elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + var elm = vnode1p.elm.get assertEquals(elm.nodeType, dom.Node.DOCUMENT_FRAGMENT_NODE) - elm = patch(vnode1, vnode2).elm.get + elm = patch(vnode1p, vnode2).elm.get assertEquals(elm.asInstanceOf[dom.Element].tagName, "DIV") } } group("element hooks") { vnode0.test( - "calls `create` listener before inserted into parent but after children" + "calls `create` listener before inserted into parent but after children".only ) { vnode0 => val result = List.newBuilder[VNode] - val cb: CreateHook = (_, vnode) => { - assert(vnode.elm.exists(_.isInstanceOf[dom.Element])) - assertEquals(vnode.elm.map(_.childNodes.length), Some(2)) - assertEquals(vnode.elm.map(_.parentNode), Some(null)) + val cb: CreateHook = vnode => { + //assert(vnode.elm.exists(_.isInstanceOf[dom.Element])) + //assertEquals(vnode.elm.map(_.childNodes.length), Some(2)) + //assertEquals(vnode.elm.map(_.parentNode), Some(null)) result.addOne(vnode) } val vnode1 = h( @@ -1246,8 +1300,8 @@ class SnabbdomSuite extends BaseSuite { ) ) ) - patch(vnode0, vnode1) - patch(vnode1, vnode2) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) assertEquals(result.result().length, 1) } @@ -1303,8 +1357,8 @@ class SnabbdomSuite extends BaseSuite { ) ) ) - patch(vnode0, vnode1) - patch(vnode1, vnode2) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) assertEquals(pre, 1) assertEquals(post, 1) } @@ -1317,6 +1371,7 @@ class SnabbdomSuite extends BaseSuite { assertEquals(result(result.length - 1), oldVnode) } result.addOne(vnode) + vnode } val vnode1 = h( "div", @@ -1354,8 +1409,8 @@ class SnabbdomSuite extends BaseSuite { ) ) ) - patch(vnode0, vnode1) - patch(vnode1, vnode2) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) assertEquals(result1.length, 1) assertEquals(result2.length, 1) } @@ -1386,8 +1441,8 @@ class SnabbdomSuite extends BaseSuite { ) ) val vnode2 = h("div", Array(h("span", "First sibling"))) - patch(vnode0, vnode1) - patch(vnode1, vnode2) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) assertEquals(result.result().length, 1) } @@ -1409,8 +1464,8 @@ class SnabbdomSuite extends BaseSuite { ) ) val vnode2 = h("div", "Text node") - patch(vnode0, vnode1) - patch(vnode1, vnode2) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) assertEquals(calls, 1) } @@ -1430,7 +1485,7 @@ class SnabbdomSuite extends BaseSuite { Some(Hooks(init = Some(init), prepatch = Some(prepatch))) ) ) - patch(vnode0, vnode1) + lazy val vnode1p = patch(vnode0, vnode1) assertEquals(count, 1) lazy val vnode2 = h( "span", @@ -1438,7 +1493,7 @@ class SnabbdomSuite extends BaseSuite { Some(Hooks(init = Some(init), prepatch = Some(prepatch))) ) ) - patch(vnode1, vnode2) + patch(vnode1p, vnode2) assertEquals(count, 2) } @@ -1461,9 +1516,10 @@ class SnabbdomSuite extends BaseSuite { ) ) val vnode2 = h("div", Array.empty[VNode]) - var elm = patch(vnode0, vnode1).elm.get + val vnode1p = patch(vnode0, vnode1) + var elm = vnode1p.elm.get assertEquals(elm.childNodes.length, 1) - elm = patch(vnode1, vnode2).elm.get + elm = patch(vnode1p, vnode2).elm.get assertEquals(elm.childNodes.length, 1) rm1() assertEquals(elm.childNodes.length, 1) @@ -1491,8 +1547,8 @@ class SnabbdomSuite extends BaseSuite { ) ) val vnode2 = h("span", Array(h("b", "Child 1"), h("i", "Child 2"))) - patch(vnode0, vnode1) - patch(vnode1, vnode2) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) assertEquals(result.result().length, 1) } } @@ -1537,8 +1593,8 @@ class SnabbdomSuite extends BaseSuite { ) ) val vnode2 = h("div") - patch(vnode0, vnode1) - patch(vnode1, vnode2) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) assertEquals(result.result().size, 1); } @@ -1546,8 +1602,8 @@ class SnabbdomSuite extends BaseSuite { vnode0 => val vnode1 = h("div", Array(VNode.text(" "))) val vnode2 = h("div", Array.empty[VNode]) - patch(vnode0, vnode1) - patch(vnode1, vnode2) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) } vnode0.test("invokes `destroy` module hook for all removed children") { @@ -1557,7 +1613,7 @@ class SnabbdomSuite extends BaseSuite { val patch = init( Seq( Module( - create = Some((_, _) => created += 1), + create = Some(_ => created += 1), destroy = Some(_ => destroyed += 1) ) ) @@ -1570,8 +1626,8 @@ class SnabbdomSuite extends BaseSuite { ) ) val vnode2 = h("div") - patch(vnode0, vnode1) - patch(vnode1, vnode2) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) assertEquals(created, 4) assertEquals(destroyed, 4) } @@ -1584,7 +1640,7 @@ class SnabbdomSuite extends BaseSuite { val patch = init( Seq( Module( - create = Some((_, _) => created += 1), + create = Some(_ => created += 1), remove = Some((_, _) => removed += 1) ) ) @@ -1598,8 +1654,8 @@ class SnabbdomSuite extends BaseSuite { ) ) val vnode2 = h("div") - patch(vnode0, vnode1) - patch(vnode1, vnode2) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) assertEquals(created, 2) assertEquals(removed, 2) } @@ -1611,7 +1667,7 @@ class SnabbdomSuite extends BaseSuite { val patch = init( Seq( Module( - create = Some((_, _) => created += 1), + create = Some(_ => created += 1), destroy = Some(_ => destroyed += 1) ) ) @@ -1630,8 +1686,8 @@ class SnabbdomSuite extends BaseSuite { ) ) val vnode2 = h("div") - patch(vnode0, vnode1) - patch(vnode1, vnode2) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) assertEquals(created, 4) assertEquals(destroyed, 4) } @@ -1640,8 +1696,9 @@ class SnabbdomSuite extends BaseSuite { group("short circuiting") { vnode0.test("does not update strictly equal vnodes") { vnode0 => val result = List.newBuilder[VNode] - val cb: UpdateHook = (vnode, _) => { + val cb: UpdateHook = (vnode, newVNode) => { result += vnode + newVNode } val vnode1 = h( "div", @@ -1654,15 +1711,16 @@ class SnabbdomSuite extends BaseSuite { h("span", "there") ) ) - patch(vnode0, vnode1) - patch(vnode1, vnode1) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode1) assertEquals(result.result().size, 0) } vnode0.test("does not update strictly equal children") { vnode0 => val result = List.newBuilder[VNode] - val cb: UpdateHook = (vnode, _) => { + val cb: UpdateHook = (vnode, newVNode) => { result += vnode + newVNode } val vnode1 = h( "div", @@ -1675,10 +1733,9 @@ class SnabbdomSuite extends BaseSuite { h("span", "there") ) ) - val vnode2 = h("div") - vnode2.children = vnode1.children - patch(vnode0, vnode1) - patch(vnode1, vnode2) + val vnode2 = h("div").copy(children = vnode1.children) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) assertEquals(result.result().size, 0) } } From b02d86e585d512ea90fda1d6a2e9977833a39e9e Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Thu, 26 May 2022 13:41:27 +0200 Subject: [PATCH 02/35] wip --- snabbdom/src/main/scala/snabbdom/init.scala | 25 +++++++++---------- .../test/scala/snabbdom/SnabbdomSuite.scala | 22 ++++++++-------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index 6d6f794..16935a2 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -124,9 +124,10 @@ object init { val sel = vnode.sel sel match { case Some("!") => + val text = vnode.text.getOrElse("") vnode.copy( - text = Some(vnode.text.getOrElse("")), - elm = Some(api.createComment(vnode.text.get)) + text = Some(text), + elm = Some(api.createComment(text)) ) case Some(sel) => @@ -173,8 +174,8 @@ object init { } } vnode0.data.hook.map { hooks => - hooks.create.foreach(hook => hook(vnode)) - hooks.insert.foreach { _ => insertedVNodeQueue.append(vnode) } + hooks.create.foreach(hook => hook(vnode0)) + hooks.insert.foreach { _ => insertedVNodeQueue.append(vnode0) } } vnode0 @@ -415,15 +416,13 @@ object init { } case (None, Some(ch)) => oldVnode.text.foreach(_ => api.setTextContent(elm, Some(""))) - Some( - addVnodes( - elm, - None, - ch, - 0, - ch.length - 1, - insertedVNodeQueue - ) + addVnodes( + elm, + None, + ch, + 0, + ch.length - 1, + insertedVNodeQueue ) vnode diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index 3f686cf..5f54bb6 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -844,7 +844,7 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("1", "2", "3", "4").map(spanNum)) val vnode2 = h("span", Array("1", "4", "2", "3").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm + val elm = vnode1p.elm.get assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) val elm2 = patch(vnode1p, vnode2).elm.get assertEquals(elm2.asInstanceOf[dom.Element].children.length, 4) @@ -1205,13 +1205,13 @@ class SnabbdomSuite extends BaseSuite { group("element hooks") { vnode0.test( - "calls `create` listener before inserted into parent but after children".only + "calls `create` listener before inserted into parent but after children" ) { vnode0 => val result = List.newBuilder[VNode] val cb: CreateHook = vnode => { - //assert(vnode.elm.exists(_.isInstanceOf[dom.Element])) - //assertEquals(vnode.elm.map(_.childNodes.length), Some(2)) - //assertEquals(vnode.elm.map(_.parentNode), Some(null)) + assert(vnode.elm.exists(_.isInstanceOf[dom.Element])) + assertEquals(vnode.elm.map(_.childNodes.length), Some(2)) + assertEquals(vnode.elm.map(_.parentNode), Some(null)) result.addOne(vnode) } val vnode1 = h( @@ -1471,23 +1471,25 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("calls `init` and `prepatch` listeners on root") { vnode0 => var count = 0 - lazy val init: InitHook = (vnode) => { + var vnode1: VNode = null + var vnode2: VNode = null + val init: InitHook = (vnode) => { assertEquals(vnode, vnode2) count += 1 } - lazy val prepatch: PrePatchHook = (_, vnode) => { + val prepatch: PrePatchHook = (_, vnode) => { assertEquals(vnode, vnode1) count += 1 } - lazy val vnode1 = h( + vnode1 = h( "div", VNodeData(hook = Some(Hooks(init = Some(init), prepatch = Some(prepatch))) ) ) - lazy val vnode1p = patch(vnode0, vnode1) + val vnode1p = patch(vnode0, vnode1) assertEquals(count, 1) - lazy val vnode2 = h( + vnode2 = h( "span", VNodeData(hook = Some(Hooks(init = Some(init), prepatch = Some(prepatch))) From 28f9e088e0b3a213cd2e83b38686163c870da65d Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Thu, 26 May 2022 16:19:57 +0200 Subject: [PATCH 03/35] fix unit tests --- .../snabbdom/modules/EventListeners.scala | 7 +-- .../test/scala/snabbdom/AttributesSuite.scala | 10 ++-- .../scala/snabbdom/EventListenersSuite.scala | 20 ++++--- .../src/test/scala/snabbdom/StyleSuite.scala | 53 ++++++++++++------- 4 files changed, 55 insertions(+), 35 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala b/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala index 789936b..40a36d4 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala @@ -103,7 +103,6 @@ object EventListeners { val vnode0 = vnode.get val listener = oldListener.getOrElse(createListener(vnode0)) - listener.vnode = vnode0 if (oldOn.isEmpty) { on.foreach { case (name, _) => @@ -117,10 +116,12 @@ object EventListeners { } } - Some(vnode0.copy(listener = Some(listener))) + val vnode1 = vnode0.copy(listener = Some(listener)) + listener.vnode = vnode1 + Some(vnode1) } else { - None + vnode } } else { diff --git a/snabbdom/src/test/scala/snabbdom/AttributesSuite.scala b/snabbdom/src/test/scala/snabbdom/AttributesSuite.scala index 1a5b8cc..3f3e930 100644 --- a/snabbdom/src/test/scala/snabbdom/AttributesSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/AttributesSuite.scala @@ -79,12 +79,13 @@ class AttributesSuite extends BaseSuite { ) val vnode1 = h("div", cachedAttrs) val vnode2 = h("div", cachedAttrs) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.Element] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] assertEquals(elm1.getAttribute("href"), "/foo") assertEquals(elm1.getAttribute("minlength"), "1") assertEquals(elm1.hasAttribute("selected"), true) - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] assertEquals(elm2.getAttribute("href"), "/foo") assertEquals(elm2.getAttribute("minlength"), "1") assertEquals(elm2.hasAttribute("selected"), true) @@ -189,12 +190,13 @@ class AttributesSuite extends BaseSuite { "is not considered as a boolean attribute and shouldn't be omitted" ) { vnode0 => val vnode1 = h("div", VNodeData(attrs = Map("constructor" -> true))) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.Element] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] assertEquals(elm1.hasAttribute("constructor"), true) assertEquals(elm1.getAttribute("constructor"), "") val vnode2 = h("div", VNodeData(attrs = Map("constructor" -> false))) - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] assertEquals(elm2.hasAttribute("constructor"), false) } diff --git a/snabbdom/src/test/scala/snabbdom/EventListenersSuite.scala b/snabbdom/src/test/scala/snabbdom/EventListenersSuite.scala index 70c8645..7289305 100644 --- a/snabbdom/src/test/scala/snabbdom/EventListenersSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/EventListenersSuite.scala @@ -86,9 +86,10 @@ class EventListenersSuite extends BaseSuite { VNodeData(on = Map("click" -> EventHandler(_ => result += 2))), Array(h("a", "Click my parent")) ) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] elm1.click() - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.HTMLElement] elm2.click() val result0 = result.result() assertEquals(result0, List(1, 2)) @@ -105,11 +106,12 @@ class EventListenersSuite extends BaseSuite { VNodeData(on = Map("click" -> clicked)), Array(h("a", "Click my parent")) ) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] elm1.click() assertEquals(result.length, 1) val vnode2 = h("div", VNodeData(), Array(h("a", "Click my parent"))) - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.HTMLElement] elm2.click() assertEquals(result.length, 1) } @@ -130,7 +132,8 @@ class EventListenersSuite extends BaseSuite { ), Array(h("a", "Click my parent")) ) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] elm1.click() assertEquals(called, 3) val vnode2 = h( @@ -140,7 +143,7 @@ class EventListenersSuite extends BaseSuite { ), Array(h("a", "Click my parent")) ) - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.HTMLElement] elm2.click() assertEquals(called, 5) } @@ -156,10 +159,11 @@ class EventListenersSuite extends BaseSuite { VNodeData(on = Map("click" -> clicked)), Array(h("a", "Click my parent")) ) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] elm1.click() assertEquals(result.length, 1) - assertEquals(result(0), vnode1) + assertEquals(result(0), vnode1p) } vnode0.test("shared handlers in parent and child nodes") { vnode0 => diff --git a/snabbdom/src/test/scala/snabbdom/StyleSuite.scala b/snabbdom/src/test/scala/snabbdom/StyleSuite.scala index 5a6baa6..eefab18 100644 --- a/snabbdom/src/test/scala/snabbdom/StyleSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/StyleSuite.scala @@ -79,10 +79,11 @@ class StyleSuite extends BaseSuite { VNodeData(style = Map("fontSize" -> "14px", "display" -> "inline")) val vnode1 = h("i", cachedStyles) val vnode2 = h("i", cachedStyles) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm1.style.fontSize, "14px") assertEquals(elm1.style.display, "inline") - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm2.style.fontSize, "14px") assertEquals(elm2.style.display, "inline") } @@ -103,13 +104,15 @@ class StyleSuite extends BaseSuite { VNodeData(style = Map("fontSize" -> "10px", "display" -> "block")) ) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm1.style.fontSize, "14px") assertEquals(elm1.style.display, "inline") - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm2.style.fontSize, "12px") assertEquals(elm2.style.display, "block") - val elm3 = patch(vnode2, vnode3).elm.get.asInstanceOf[dom.HTMLElement] + val elm3 = patch(vnode2p, vnode3).elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm3.style.fontSize, "10px") assertEquals(elm3.style.display, "block") @@ -122,11 +125,13 @@ class StyleSuite extends BaseSuite { h("i", VNodeData(style = Map("fontSize" -> ""))) val vnode3 = h("i", VNodeData(style = Map("fontSize" -> "10px"))) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm1.style.fontSize, "14px") - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm2.style.fontSize, "") - val elm3 = patch(vnode2, vnode3).elm.get.asInstanceOf[dom.HTMLElement] + val elm3 = patch(vnode2p, vnode3).elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm3.style.fontSize, "10px") } @@ -134,11 +139,13 @@ class StyleSuite extends BaseSuite { val vnode1 = h("i", VNodeData(style = Map("fontSize" -> "14px"))) val vnode2 = h("i", VNodeData()) val vnode3 = h("i", VNodeData(style = Map("fontSize" -> "10px"))) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm1.style.fontSize, "14px") - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm2.style.fontSize, "") - val elm3 = patch(vnode2, vnode3).elm.get.asInstanceOf[dom.HTMLElement] + val elm3 = patch(vnode2p, vnode3).elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm3.style.fontSize, "10px") } @@ -149,11 +156,13 @@ class StyleSuite extends BaseSuite { val vnode2 = h("div", VNodeData(style = Map("--myVar" -> "2"))) val vnode3 = h("div", VNodeData(style = Map("--myVar" -> "3"))) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm1.style.getPropertyValue("--myVar"), "1") - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm2.style.getPropertyValue("--myVar"), "2") - val elm3 = patch(vnode2, vnode3).elm.get.asInstanceOf[dom.HTMLElement] + val elm3 = patch(vnode2p, vnode3).elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm3.style.getPropertyValue("--myVar"), "3") } @@ -165,11 +174,13 @@ class StyleSuite extends BaseSuite { val vnode2 = h("i", VNodeData(style = Map("--myVar" -> ""))) val vnode3 = h("i", VNodeData(style = Map("--myVar" -> "3"))) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm1.style.getPropertyValue("--myVar"), "1") - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm2.style.getPropertyValue("--myVar"), "") - val elm3 = patch(vnode2, vnode3).elm.get.asInstanceOf[dom.HTMLElement] + val elm3 = patch(vnode2p, vnode3).elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm3.style.getPropertyValue("--myVar"), "3") } @@ -190,7 +201,8 @@ class StyleSuite extends BaseSuite { Array(h("i", VNodeData(style = Map("--myVar" -> "3")))) ) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals( elm1.firstChild .asInstanceOf[dom.HTMLElement] @@ -198,7 +210,8 @@ class StyleSuite extends BaseSuite { .getPropertyValue("--myVar"), "1" ) - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals( elm2.firstChild .asInstanceOf[dom.HTMLElement] @@ -206,7 +219,7 @@ class StyleSuite extends BaseSuite { .getPropertyValue("--myVar"), "" ) - val elm3 = patch(vnode2, vnode3).elm.get.asInstanceOf[dom.HTMLElement] + val elm3 = patch(vnode2p, vnode3).elm.get.asInstanceOf[dom.HTMLElement] assertEquals( elm3.firstChild .asInstanceOf[dom.HTMLElement] From 3fdc9413ad3c90fd4ccf0e861c73401676a84909 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Thu, 26 May 2022 17:19:13 +0200 Subject: [PATCH 04/35] all tests pass --- .../src/main/scala/snabbdom/InitHook.scala | 2 +- .../main/scala/snabbdom/PrePatchHook.scala | 2 +- snabbdom/src/main/scala/snabbdom/init.scala | 24 ++++----- snabbdom/src/main/scala/snabbdom/thunk.scala | 21 ++++++-- .../test/scala/snabbdom/SnabbdomSuite.scala | 11 ++-- .../src/test/scala/snabbdom/ThunkSuite.scala | 54 +++++++++++-------- 6 files changed, 66 insertions(+), 48 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/InitHook.scala b/snabbdom/src/main/scala/snabbdom/InitHook.scala index bfb5153..5ece860 100644 --- a/snabbdom/src/main/scala/snabbdom/InitHook.scala +++ b/snabbdom/src/main/scala/snabbdom/InitHook.scala @@ -40,5 +40,5 @@ package snabbdom trait InitHook { - def apply(vNode: VNode): Unit + def apply(vNode: VNode): VNode } diff --git a/snabbdom/src/main/scala/snabbdom/PrePatchHook.scala b/snabbdom/src/main/scala/snabbdom/PrePatchHook.scala index 31ee109..e5c622c 100644 --- a/snabbdom/src/main/scala/snabbdom/PrePatchHook.scala +++ b/snabbdom/src/main/scala/snabbdom/PrePatchHook.scala @@ -40,6 +40,6 @@ package snabbdom trait PrePatchHook { - def apply(oldVNode: VNode, vNode: VNode): Unit + def apply(oldVNode: VNode, vNode: VNode): VNode } diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index 16935a2..c9e39a9 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -109,17 +109,10 @@ object init { } } - def createElm(vnode: VNode, insertedVNodeQueue: VNodeQueue): VNode = { + def createElm(vnode0: VNode, insertedVNodeQueue: VNodeQueue): VNode = { - var data = vnode.data - - for { - h <- data.hook - init0 <- h.init - } yield { - init0(vnode) - data = vnode.data - } + val vnode = + vnode0.data.hook.flatMap(_.init).fold(vnode0)(hook => hook(vnode0)) val sel = vnode.sel sel match { @@ -140,9 +133,9 @@ object init { } else { sel } - val elm = if (data.ns.isDefined) { + val elm = if (vnode.data.ns.isDefined) { api.createElementNS( - data.ns.get, + vnode.data.ns.get, tag ) // TODO what about data? } else { @@ -385,11 +378,12 @@ object init { def patchVnode( oldVnode: VNode, - vnode0: VNode, + vnode00: VNode, insertedVNodeQueue: VNodeQueue ): VNode = { - val hook = vnode0.data.hook - hook.flatMap(_.prepatch).foreach(hook => hook(oldVnode, vnode0)) + val hook = vnode00.data.hook + val vnode0 = + hook.flatMap(_.prepatch).fold(vnode00)(hook => hook(oldVnode, vnode00)) val elm = oldVnode.elm.get val oldCh = oldVnode.children diff --git a/snabbdom/src/main/scala/snabbdom/thunk.scala b/snabbdom/src/main/scala/snabbdom/thunk.scala index 9a7cd69..b8b70d4 100644 --- a/snabbdom/src/main/scala/snabbdom/thunk.scala +++ b/snabbdom/src/main/scala/snabbdom/thunk.scala @@ -70,10 +70,15 @@ object thunk { } private def init0(thunk: VNode): VNode = { - val data = thunk.data - val fn = data.fn.get - val args = data.args.get - fn(args) + val fn = thunk.data.fn.get + val args = thunk.data.args.get + val vnode = fn(args) + thunk.copy( + children = vnode.children, + data = vnode.data.copy(fn = Some(fn), args = Some(args)), + text = vnode.text, + elm = vnode.elm + ) } private def prepatch0(oldVnode: VNode, thunk: VNode): VNode = { @@ -84,7 +89,13 @@ object thunk { val oldFn = old.fn val curFn = cur.fn if (oldFn != curFn || oldArgs != args) { - curFn.get(args.get) + val vnode = curFn.get(args.get) + thunk.copy( + children = vnode.children, + data = vnode.data.copy(fn = curFn, args = args), + text = vnode.text, + elm = vnode.elm + ) } else { oldVnode } diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index 5f54bb6..66c7d6d 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -1271,6 +1271,7 @@ class SnabbdomSuite extends BaseSuite { assertEquals(vnode1.children.map(_(1)), Some(oldVnode)) assertEquals(vnode2.children.map(_(1)), Some(vnode)) result.addOne(vnode) + vnode } lazy val vnode1 = h( "div", @@ -1324,8 +1325,8 @@ class SnabbdomSuite extends BaseSuite { VNodeData(hook = Some( Hooks( - prepatch = Some((_, _) => preCb()), - postpatch = Some((_, _) => postCb()) + prepatch = Some((_, vnode) => { preCb(); vnode }), + postpatch = Some((_, vnode) => { postCb(); vnode }) ) ) ), @@ -1345,8 +1346,8 @@ class SnabbdomSuite extends BaseSuite { VNodeData(hook = Some( Hooks( - prepatch = Some((_, _) => preCb()), - postpatch = Some((_, _) => postCb()) + prepatch = Some((_, vnode) => { preCb(); vnode }), + postpatch = Some((_, vnode) => { postCb(); vnode }) ) ) ), @@ -1476,10 +1477,12 @@ class SnabbdomSuite extends BaseSuite { val init: InitHook = (vnode) => { assertEquals(vnode, vnode2) count += 1 + vnode } val prepatch: PrePatchHook = (_, vnode) => { assertEquals(vnode, vnode1) count += 1 + vnode } vnode1 = h( "div", diff --git a/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala b/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala index 5c193c7..a185744 100644 --- a/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala @@ -73,9 +73,9 @@ class ThunkSuite extends BaseSuite { } val vnode1 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) val vnode2 = h("div", Array(thunk("span", "num", numberInSpan, Seq(2)))) - patch(vnode0, vnode1) + val vnode1p = patch(vnode0, vnode1) assertEquals(called, 1) - patch(vnode1, vnode2) + patch(vnode1p, vnode2) assertEquals(called, 2) } @@ -90,9 +90,9 @@ class ThunkSuite extends BaseSuite { } val vnode1 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) val vnode2 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) - patch(vnode0, vnode1) + val vnode1p = patch(vnode0, vnode1) assertEquals(called, 1) - patch(vnode1, vnode2) + patch(vnode1p, vnode2) assertEquals(called, 1) } @@ -105,9 +105,9 @@ class ThunkSuite extends BaseSuite { } val vnode1 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) val vnode2 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1, 2)))) - patch(vnode0, vnode1) + val vnode1p = patch(vnode0, vnode1) assertEquals(called, 1) - patch(vnode1, vnode2) + patch(vnode1p, vnode2) assertEquals(called, 2) } @@ -125,9 +125,9 @@ class ThunkSuite extends BaseSuite { } val vnode1 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) val vnode2 = h("div", Array(thunk("span", "num", numberInSpan2, Seq(1)))) - patch(vnode0, vnode1) + val vnode1p = patch(vnode0, vnode1) assertEquals(called, 1) - patch(vnode1, vnode2) + patch(vnode1p, vnode2) assertEquals(called, 2) } @@ -141,7 +141,8 @@ class ThunkSuite extends BaseSuite { val vnode1 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) val vnode2 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) val vnode3 = h("div", Array(thunk("span", "num", numberInSpan, Seq(2)))) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(called, 1) assertEquals( elm1.firstChild.asInstanceOf[dom.HTMLElement].tagName.toLowerCase, @@ -151,7 +152,8 @@ class ThunkSuite extends BaseSuite { elm1.firstChild.asInstanceOf[dom.HTMLElement].innerHTML, "Number is 1" ) - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals( elm2.firstChild.asInstanceOf[dom.HTMLElement].tagName.toLowerCase, "span" @@ -161,7 +163,8 @@ class ThunkSuite extends BaseSuite { "Number is 1" ) assertEquals(called, 1) - val elm3 = patch(vnode2, vnode3).elm.get.asInstanceOf[dom.HTMLElement] + val vnode3p = patch(vnode2p, vnode3) + val elm3 = vnode3p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals( elm3.firstChild.asInstanceOf[dom.HTMLElement].tagName.toLowerCase, "span" @@ -194,17 +197,20 @@ class ThunkSuite extends BaseSuite { val vnode2 = thunk("span", "num", numberInSpan, Seq(1)) val vnode3 = thunk("span", "num", numberInSpan, Seq(2)) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(called, 1) assertEquals(elm1.tagName.toLowerCase, "span") assertEquals(elm1.innerHTML, "Number is 1") - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm2.tagName.toLowerCase, "span") assertEquals(elm2.innerHTML, "Number is 1") assertEquals(called, 1) - val elm3 = patch(vnode2, vnode3).elm.get.asInstanceOf[dom.HTMLElement] + val vnode3p = patch(vnode2p, vnode3) + val elm3 = vnode3p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm3.tagName.toLowerCase, "span") assertEquals(elm3.innerHTML, "Number is 2") assertEquals(called, 2) @@ -225,7 +231,8 @@ class ThunkSuite extends BaseSuite { val vnode1 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) val vnode2 = h("div", Array(thunk("div", "oddEven", oddEven, Seq(4)))) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals( elm1.firstChild.asInstanceOf[dom.HTMLElement].tagName.toLowerCase, "span" @@ -235,7 +242,9 @@ class ThunkSuite extends BaseSuite { "Number is 1" ) - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] + println(elm2.innerHTML) assertEquals( elm2.firstChild.asInstanceOf[dom.HTMLElement].tagName.toLowerCase, "div" @@ -262,11 +271,12 @@ class ThunkSuite extends BaseSuite { val vnode1 = thunk("span", "num", numberInSpan, Seq(1)) val vnode2 = thunk("div", "oddEven", oddEven, Seq(4)) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm1.tagName.toLowerCase, "span") assertEquals(elm1.innerHTML, "Number is 1") - val elm2 = patch(vnode1, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.HTMLElement] assertEquals(elm2.tagName.toLowerCase, "div") assertEquals(elm2.innerHTML, "Even: 4") @@ -296,8 +306,8 @@ class ThunkSuite extends BaseSuite { ) ) val vnode2 = h("div", Array(h("div", "Foo"), h("div", "Foo"))) - patch(vnode0, vnode1) - patch(vnode1, vnode2) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) assertEquals(called, 1) } @@ -325,8 +335,8 @@ class ThunkSuite extends BaseSuite { ) ) val vnode2 = h("div", Array(h("div", "Foo"), h("div", "Foo"))) - patch(vnode0, vnode1) - patch(vnode1, vnode2) + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) assertEquals(called, 1) } From a94b23691a8e9a6b510729822784ead5f8d8afd5 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Thu, 26 May 2022 19:29:59 +0200 Subject: [PATCH 05/35] wip --- .../scala/snabbdom/examples/Example.scala | 4 +- .../src/main/scala/snabbdom/CreateHook.scala | 2 +- .../src/main/scala/snabbdom/DestroyHook.scala | 2 +- .../src/main/scala/snabbdom/InsertHook.scala | 2 +- snabbdom/src/main/scala/snabbdom/Patch.scala | 6 +- .../main/scala/snabbdom/PatchedVNode.scala | 43 +++ .../main/scala/snabbdom/PostPatchHook.scala | 2 +- .../main/scala/snabbdom/PrePatchHook.scala | 2 +- .../src/main/scala/snabbdom/RemoveHook.scala | 2 +- .../src/main/scala/snabbdom/UpdateHook.scala | 2 +- snabbdom/src/main/scala/snabbdom/VNode.scala | 21 +- .../src/main/scala/snabbdom/fragment.scala | 2 +- snabbdom/src/main/scala/snabbdom/h.scala | 16 +- snabbdom/src/main/scala/snabbdom/init.scala | 250 +++++++++++----- .../scala/snabbdom/modules/Attributes.scala | 44 ++- .../main/scala/snabbdom/modules/Classes.scala | 30 +- .../main/scala/snabbdom/modules/Dataset.scala | 34 ++- .../snabbdom/modules/EventListeners.scala | 47 ++- .../main/scala/snabbdom/modules/Props.scala | 30 +- .../main/scala/snabbdom/modules/Styles.scala | 35 ++- snabbdom/src/main/scala/snabbdom/thunk.scala | 15 +- .../src/main/scala/snabbdom/toVNode.scala | 24 +- .../test/scala/snabbdom/AttributesSuite.scala | 22 +- .../test/scala/snabbdom/DatasetSuite.scala | 2 +- .../scala/snabbdom/EventListenersSuite.scala | 20 +- .../test/scala/snabbdom/SnabbdomSuite.scala | 274 +++++++++--------- .../src/test/scala/snabbdom/StyleSuite.scala | 42 +-- .../src/test/scala/snabbdom/SvgSuite.scala | 6 +- .../src/test/scala/snabbdom/ThunkSuite.scala | 28 +- 29 files changed, 632 insertions(+), 377 deletions(-) create mode 100644 snabbdom/src/main/scala/snabbdom/PatchedVNode.scala diff --git a/examples/src/main/scala/snabbdom/examples/Example.scala b/examples/src/main/scala/snabbdom/examples/Example.scala index edaff38..06b7f53 100644 --- a/examples/src/main/scala/snabbdom/examples/Example.scala +++ b/examples/src/main/scala/snabbdom/examples/Example.scala @@ -56,7 +56,7 @@ object Example { ) ) // Patch into empty DOM element this modifies the DOM as a side effect - patch(container, vnode); + val vnodep = patch(container, vnode); val newVnode = h( "div#container.two.classes", @@ -79,7 +79,7 @@ object Example { ) // Second `patch` invocation - patch(vnode, newVnode) + patch(vnodep, newVnode) () diff --git a/snabbdom/src/main/scala/snabbdom/CreateHook.scala b/snabbdom/src/main/scala/snabbdom/CreateHook.scala index e6af763..1ea233a 100644 --- a/snabbdom/src/main/scala/snabbdom/CreateHook.scala +++ b/snabbdom/src/main/scala/snabbdom/CreateHook.scala @@ -40,6 +40,6 @@ package snabbdom trait CreateHook { - def apply(vNode: VNode): Unit + def apply(vNode: PatchedVNode): Unit } diff --git a/snabbdom/src/main/scala/snabbdom/DestroyHook.scala b/snabbdom/src/main/scala/snabbdom/DestroyHook.scala index 68980e0..3bc0486 100644 --- a/snabbdom/src/main/scala/snabbdom/DestroyHook.scala +++ b/snabbdom/src/main/scala/snabbdom/DestroyHook.scala @@ -40,6 +40,6 @@ package snabbdom trait DestroyHook { - def apply(vNode: VNode): Unit + def apply(vNode: PatchedVNode): Unit } diff --git a/snabbdom/src/main/scala/snabbdom/InsertHook.scala b/snabbdom/src/main/scala/snabbdom/InsertHook.scala index 847a027..e896ad7 100644 --- a/snabbdom/src/main/scala/snabbdom/InsertHook.scala +++ b/snabbdom/src/main/scala/snabbdom/InsertHook.scala @@ -40,6 +40,6 @@ package snabbdom trait InsertHook { - def apply(vNode: VNode): Unit + def apply(vNode: PatchedVNode): Unit } diff --git a/snabbdom/src/main/scala/snabbdom/Patch.scala b/snabbdom/src/main/scala/snabbdom/Patch.scala index fca901a..a3c74db 100644 --- a/snabbdom/src/main/scala/snabbdom/Patch.scala +++ b/snabbdom/src/main/scala/snabbdom/Patch.scala @@ -42,10 +42,10 @@ import org.scalajs.dom trait Patch { - def apply(oldVnode: VNode, vnode: VNode): VNode + def apply(oldVnode: PatchedVNode, vnode: VNode): PatchedVNode - def apply(elm: dom.Element, vnode: VNode): VNode + def apply(elm: dom.Element, vnode: VNode): PatchedVNode - def apply(frag: dom.DocumentFragment, vnode: VNode): VNode + def apply(frag: dom.DocumentFragment, vnode: VNode): PatchedVNode } diff --git a/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala b/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala new file mode 100644 index 0000000..cc12119 --- /dev/null +++ b/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala @@ -0,0 +1,43 @@ +/* + * Copyright 2022 buntec + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package snabbdom + +import org.scalajs.dom + +/** A `VNode` that has been patched into the DOM. */ +case class PatchedVNode private[snabbdom] ( + sel: Option[String], + data: VNodeData, + children: Option[Array[PatchedVNode]], + text: Option[String], + key: Option[KeyValue], + elm: dom.Node, // the corresponding node in the DOM - can't be `dom.Element` unfortunately b/c of fragments + listener: Option[ + Listener + ] // this is an optimization that allows re-using event listeners +) { + + override def toString: String = + s"sel=$sel, data=$data, text=$text, key=$key, children=$children, elm=$elm, listener=$listener" + + def toVNode: VNode = + VNode(sel, data, children.map(_.map(_.toVNode)), text, key) + + private[snabbdom] def isTextNode: Boolean = + sel.isEmpty && children.isEmpty && text.isDefined + +} diff --git a/snabbdom/src/main/scala/snabbdom/PostPatchHook.scala b/snabbdom/src/main/scala/snabbdom/PostPatchHook.scala index 3b693b4..8ba2fd2 100644 --- a/snabbdom/src/main/scala/snabbdom/PostPatchHook.scala +++ b/snabbdom/src/main/scala/snabbdom/PostPatchHook.scala @@ -40,6 +40,6 @@ package snabbdom trait PostPatchHook { - def apply(oldVNode: VNode, vNode: VNode): Any + def apply(oldVNode: PatchedVNode, vNode: PatchedVNode): Any } diff --git a/snabbdom/src/main/scala/snabbdom/PrePatchHook.scala b/snabbdom/src/main/scala/snabbdom/PrePatchHook.scala index e5c622c..eaa6ef3 100644 --- a/snabbdom/src/main/scala/snabbdom/PrePatchHook.scala +++ b/snabbdom/src/main/scala/snabbdom/PrePatchHook.scala @@ -40,6 +40,6 @@ package snabbdom trait PrePatchHook { - def apply(oldVNode: VNode, vNode: VNode): VNode + def apply(oldVNode: PatchedVNode, vNode: VNode): VNode } diff --git a/snabbdom/src/main/scala/snabbdom/RemoveHook.scala b/snabbdom/src/main/scala/snabbdom/RemoveHook.scala index 44ad10f..91a00e8 100644 --- a/snabbdom/src/main/scala/snabbdom/RemoveHook.scala +++ b/snabbdom/src/main/scala/snabbdom/RemoveHook.scala @@ -40,6 +40,6 @@ package snabbdom trait RemoveHook { - def apply(vNode: VNode, removeCallback: () => Unit): Unit + def apply(vNode: PatchedVNode, removeCallback: () => Unit): Unit } diff --git a/snabbdom/src/main/scala/snabbdom/UpdateHook.scala b/snabbdom/src/main/scala/snabbdom/UpdateHook.scala index 64ee9ff..e3a69ca 100644 --- a/snabbdom/src/main/scala/snabbdom/UpdateHook.scala +++ b/snabbdom/src/main/scala/snabbdom/UpdateHook.scala @@ -40,6 +40,6 @@ package snabbdom trait UpdateHook { - def apply(oldVNode: VNode, vNode: VNode): VNode + def apply(oldVNode: PatchedVNode, vNode: VNode): VNode } diff --git a/snabbdom/src/main/scala/snabbdom/VNode.scala b/snabbdom/src/main/scala/snabbdom/VNode.scala index b9ababb..4bc1c10 100644 --- a/snabbdom/src/main/scala/snabbdom/VNode.scala +++ b/snabbdom/src/main/scala/snabbdom/VNode.scala @@ -38,22 +38,16 @@ package snabbdom -import org.scalajs.dom - case class VNode private ( sel: Option[String], data: VNodeData, children: Option[Array[VNode]], - elm: Option[ - dom.Node - ], // can't be `dom.Element` unfortunately b/c of fragments text: Option[String], - key: Option[KeyValue], - listener: Option[Listener] + key: Option[KeyValue] ) { override def toString: String = - s"sel=$sel, data=$data, text=$text, key=$key, children=$children, elm=$elm" + s"sel=$sel, data=$data, text=$text, key=$key, children=$children" private[snabbdom] def isTextNode: Boolean = sel.isEmpty && children.isEmpty && text.isDefined @@ -62,20 +56,17 @@ case class VNode private ( object VNode { - def empty() = - new VNode(None, VNodeData.empty, None, None, Some(""), None, None) + val empty = VNode(None, VNodeData.empty, None, Some(""), None) def create( sel: Option[String], data: VNodeData, children: Option[Array[VNode]], - text: Option[String], - elm: Option[dom.Node] - ) = - new VNode(sel, data, children.filter(_.nonEmpty), elm, text, data.key, None) + text: Option[String] + ) = new VNode(sel, data, children.filter(_.nonEmpty), text, data.key) def text(text: String) = - new VNode(None, VNodeData.empty, None, None, Some(text), None, None) + new VNode(None, VNodeData.empty, None, Some(text), None) implicit def fromString(s: String): VNode = text(s) diff --git a/snabbdom/src/main/scala/snabbdom/fragment.scala b/snabbdom/src/main/scala/snabbdom/fragment.scala index 7eaa00f..b63fd1b 100644 --- a/snabbdom/src/main/scala/snabbdom/fragment.scala +++ b/snabbdom/src/main/scala/snabbdom/fragment.scala @@ -41,7 +41,7 @@ package snabbdom object fragment { def apply(children: Array[VNode]): VNode = { - VNode.create(None, VNodeData.empty, Some(children), None, None) + VNode.create(None, VNodeData.empty, Some(children), None) } } diff --git a/snabbdom/src/main/scala/snabbdom/h.scala b/snabbdom/src/main/scala/snabbdom/h.scala index e99763e..24f0fd6 100644 --- a/snabbdom/src/main/scala/snabbdom/h.scala +++ b/snabbdom/src/main/scala/snabbdom/h.scala @@ -79,8 +79,7 @@ object h { Some(sel), data.getOrElse(VNodeData.empty), children, - text, - None + text ) if ( sel.startsWith("svg") && @@ -92,6 +91,19 @@ object h { } } + private[snabbdom] def addNS(vnode: PatchedVNode): PatchedVNode = { + val ns = "http://www.w3.org/2000/svg" + vnode.copy( + data = vnode.data.copy(ns = Some(ns)), + children = + if (vnode.sel.forall(_ != "foreignObject")) + vnode.children.map(children => + children.map((child: PatchedVNode) => addNS(child)) + ) + else vnode.children + ) + } + private[snabbdom] def addNS(vnode: VNode): VNode = { val ns = "http://www.w3.org/2000/svg" vnode.copy( diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index c9e39a9..80c7e22 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -63,7 +63,7 @@ object init { domApi: Option[DomApi] = None ): Patch = { - type VNodeQueue = mutable.ArrayBuffer[VNode] + type VNodeQueue = mutable.ArrayBuffer[PatchedVNode] val api = domApi.getOrElse(DomApi.apply) @@ -79,23 +79,25 @@ object init { ) } - def emptyNodeAt(elm: dom.Element): VNode = { + def emptyNodeAt(elm: dom.Element): PatchedVNode = { val id = Option(elm.id).filter(_.nonEmpty).fold("")("#" + _) val classes = Option(elm.getAttribute("class")) val c = classes.map("." + _.split(" ").mkString(".")).getOrElse("") - VNode.create( + PatchedVNode( Some(api.tagName(elm).toLowerCase + id + c), VNodeData.empty, None, None, - Some(elm) + None, + elm, + None ) } - def emptyDocumentFragmentAt(frag: dom.DocumentFragment): VNode = { - VNode.create(None, VNodeData.empty, None, None, Some(frag)) + def emptyDocumentFragmentAt(frag: dom.DocumentFragment): PatchedVNode = { + PatchedVNode(None, VNodeData.empty, None, None, None, frag, None) } def createRmCb(childElm: dom.Node, listeners: Int): () => Unit = { @@ -109,7 +111,10 @@ object init { } } - def createElm(vnode0: VNode, insertedVNodeQueue: VNodeQueue): VNode = { + def createElm( + vnode0: VNode, + insertedVNodeQueue: VNodeQueue + ): PatchedVNode = { val vnode = vnode0.data.hook.flatMap(_.init).fold(vnode0)(hook => hook(vnode0)) @@ -118,9 +123,15 @@ object init { sel match { case Some("!") => val text = vnode.text.getOrElse("") - vnode.copy( - text = Some(text), - elm = Some(api.createComment(text)) + val elm = api.createComment(text) + PatchedVNode( + vnode.sel, + vnode.data, + None, // comment nodes can't have children + Some(text), + vnode.key, + elm, + None ) case Some(sel) => @@ -141,10 +152,16 @@ object init { } else { api.createElement(tag) // TODO what about data argument? } - val vnode0 = vnode.copy( - elm = Some(elm), - children = - vnode.children.map(_.map(ch => createElm(ch, insertedVNodeQueue))) + val vnode0 = PatchedVNode( + vnode.key, + vnode.data, + children = vnode.children.map( + _.map(ch => createElm(ch, insertedVNodeQueue)) + ), + text = vnode.text, + key = vnode.key, + elm = elm, + None ) if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot)) if (dotIdx > 0) { @@ -163,7 +180,7 @@ object init { } case Some(children) => children.foreach { child => - api.appendChild(elm, child.elm.get) + api.appendChild(elm, child.elm) } } vnode0.data.hook.map { hooks => @@ -175,22 +192,34 @@ object init { case None => vnode.children match { case None => - vnode.copy(elm = - Some(api.createTextNode(vnode.text.getOrElse(""))) + PatchedVNode( + vnode.sel, + vnode.data, + None, + vnode.text, + vnode.key, + api.createTextNode(vnode.text.getOrElse("")), + None ) + case Some(children) => val elm = api.createDocumentFragment - val vnode0 = vnode.copy( - elm = Some(elm), + val vnode0 = PatchedVNode( + vnode.sel, + vnode.data, children = - Some(children.map(ch => createElm(ch, insertedVNodeQueue))) + Some(children.map(ch => createElm(ch, insertedVNodeQueue))), + text = vnode.text, + key = vnode.key, + elm = elm, + None ) cbs.create.foreach(hook => hook(vnode0)) vnode0.children.foreach { children => children.foreach(ch => api.appendChild( elm, - ch.elm.get + ch.elm ) ) } @@ -208,20 +237,19 @@ object init { startIdx: Int, endIdx: Int, insertedVNodeQueue: VNodeQueue - ): Unit = { - var i = startIdx - while (i <= endIdx) { - vnodes(i) = createElm(vnodes(i), insertedVNodeQueue) + ): Array[PatchedVNode] = { + vnodes.slice(startIdx, endIdx + 1).map { vnode => + val pvnode = createElm(vnode, insertedVNodeQueue) api.insertBefore( parentElm, - vnodes(i).elm.get, + pvnode.elm, before ) - i += 1 + pvnode } } - def invokeDestroyHook(vnode: VNode): Unit = { + def invokeDestroyHook(vnode: PatchedVNode): Unit = { if (!vnode.isTextNode) { // detroy hooks should not be called on text nodes vnode.data.hook.flatMap(_.destroy).foreach(hook => hook(vnode)) cbs.destroy.foreach(hook => hook(vnode)) @@ -235,7 +263,7 @@ object init { def removeVnodes( parentElm: dom.Node, - vnodes: Array[VNode], + vnodes: Array[PatchedVNode], startIdx: Int, endIdx: Int ): Unit = { @@ -247,13 +275,13 @@ object init { case Some(_) => invokeDestroyHook(ch) val listeners = cbs.remove.length + 1 - val rm = createRmCb(ch.elm.get, listeners) + val rm = createRmCb(ch.elm, listeners) cbs.remove.foreach(hook => hook(ch, rm)) ch.data.hook .flatMap(_.remove) .fold(rm()) { hook => hook(ch, rm); () } case None => // text node - api.removeChild(parentElm, ch.elm.get) + api.removeChild(parentElm, ch.elm) } i += 1 } @@ -261,14 +289,16 @@ object init { def updateChildren( parentElm: dom.Node, - oldCh: Array[VNode], + oldCh: Array[PatchedVNode], newCh: Array[VNode], insertedVnodeQueue: VNodeQueue - ): Unit = { + ): Array[PatchedVNode] = { assert(oldCh.nonEmpty) assert(newCh.nonEmpty) + val result = Array.ofDim[PatchedVNode](newCh.length) + var oldStartIdx = 0 var newStartIdx = 0 var oldEndIdx = oldCh.length - 1 @@ -283,7 +313,7 @@ object init { } else if (oldCh(oldEndIdx) == null) { oldEndIdx -= 1 } else if (sameVnode(oldCh(oldStartIdx), newCh(newStartIdx))) { - newCh(newStartIdx) = patchVnode( + result(newStartIdx) = patchVnode( oldCh(oldStartIdx), newCh(newStartIdx), insertedVnodeQueue @@ -291,29 +321,29 @@ object init { oldStartIdx += 1 newStartIdx += 1 } else if (sameVnode(oldCh(oldEndIdx), newCh(newEndIdx))) { - newCh(newEndIdx) = + result(newEndIdx) = patchVnode(oldCh(oldEndIdx), newCh(newEndIdx), insertedVnodeQueue) oldEndIdx -= 1 newEndIdx -= 1 } else if (sameVnode(oldCh(oldStartIdx), newCh(newEndIdx))) { // Vnode moved right - newCh(newEndIdx) = + result(newEndIdx) = patchVnode(oldCh(oldStartIdx), newCh(newEndIdx), insertedVnodeQueue) api.insertBefore( parentElm, - oldCh(oldStartIdx).elm.get, - api.nextSibling(oldCh(oldEndIdx).elm.get) + oldCh(oldStartIdx).elm, + api.nextSibling(oldCh(oldEndIdx).elm) ) oldStartIdx += 1 newEndIdx -= 1 } else if (sameVnode(oldCh(oldEndIdx), newCh(newStartIdx))) { // Vnode moved left - newCh(newStartIdx) = + result(newStartIdx) = patchVnode(oldCh(oldEndIdx), newCh(newStartIdx), insertedVnodeQueue) api.insertBefore( parentElm, - oldCh(oldEndIdx).elm.get, - oldCh(oldStartIdx).elm + oldCh(oldEndIdx).elm, + Some(oldCh(oldStartIdx).elm) ) oldEndIdx -= 1 newStartIdx += 1 @@ -329,27 +359,27 @@ object init { // New element api.insertBefore( parentElm, - createElm(newCh(newStartIdx), insertedVnodeQueue).elm.get, - oldCh(oldStartIdx).elm + createElm(newCh(newStartIdx), insertedVnodeQueue).elm, + Some(oldCh(oldStartIdx).elm) ) case Some(idxInOld) => val elmToMove = oldCh(idxInOld) if (elmToMove.sel != newCh(newStartIdx).sel) { - newCh(newStartIdx) = + result(newStartIdx) = createElm(newCh(newStartIdx), insertedVnodeQueue) api.insertBefore( parentElm, - newCh(newStartIdx).elm.get, - oldCh(oldStartIdx).elm + result(newStartIdx).elm, + Some(oldCh(oldStartIdx).elm) ) } else { - newCh(newStartIdx) = + result(newStartIdx) = patchVnode(elmToMove, newCh(newStartIdx), insertedVnodeQueue) oldCh(idxInOld) = null api.insertBefore( parentElm, - elmToMove.elm.get, - oldCh(oldStartIdx).elm + elmToMove.elm, + Some(oldCh(oldStartIdx).elm) ) } } @@ -359,9 +389,9 @@ object init { if (newStartIdx <= newEndIdx) { val before = - if (newCh.length > newEndIdx + 1) newCh(newEndIdx + 1).elm + if (result.length > newEndIdx + 1) Some(result(newEndIdx + 1).elm) else None - addVnodes( + val patchedChildren = addVnodes( parentElm, before, newCh, @@ -369,29 +399,36 @@ object init { newEndIdx, insertedVnodeQueue ) + var i = newStartIdx + while (i <= newEndIdx) { + result(i) = patchedChildren(i - newStartIdx) + i += 1 + } } + if (oldStartIdx <= oldEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } + result + } def patchVnode( - oldVnode: VNode, + oldVnode: PatchedVNode, vnode00: VNode, insertedVNodeQueue: VNodeQueue - ): VNode = { + ): PatchedVNode = { val hook = vnode00.data.hook val vnode0 = hook.flatMap(_.prepatch).fold(vnode00)(hook => hook(oldVnode, vnode00)) - val elm = oldVnode.elm.get + val elm = oldVnode.elm val oldCh = oldVnode.children - if (oldVnode != vnode0) { + if (oldVnode.toVNode != vnode0) { - val vnode = cbs.update.foldLeft(vnode0.copy(elm = Some(elm))) { - case (vnode, hook) => - hook(oldVnode, vnode) + val vnode = cbs.update.foldLeft(vnode0) { case (vnode, hook) => + hook(oldVnode, vnode) } vnode.data.hook @@ -403,14 +440,30 @@ object init { (oldCh, vnode.children) match { case (Some(oldCh), Some(ch)) => if (oldCh != ch) { - updateChildren(elm, oldCh, ch, insertedVNodeQueue) - vnode + PatchedVNode( + vnode.sel, + vnode.data, + Some(updateChildren(elm, oldCh, ch, insertedVNodeQueue)), + vnode.text, + vnode.key, + elm, + None + ) + } else { - vnode + PatchedVNode( + vnode.sel, + vnode.data, + Some(oldCh), + vnode.text, + vnode.key, + elm, + None + ) } case (None, Some(ch)) => oldVnode.text.foreach(_ => api.setTextContent(elm, Some(""))) - addVnodes( + val patchedChildren = addVnodes( elm, None, ch, @@ -418,22 +471,63 @@ object init { ch.length - 1, insertedVNodeQueue ) - vnode + PatchedVNode( + vnode.sel, + vnode.data, + Some(patchedChildren), + vnode.text, + vnode.key, + elm, + None + ) case (Some(oldCh), None) => removeVnodes(elm, oldCh, 0, oldCh.length - 1) - vnode + PatchedVNode( + vnode.sel, + vnode.data, + None, + vnode.text, + vnode.key, + elm, + None + ) case (None, None) => oldVnode.text.foreach(_ => api.setTextContent(elm, Some(""))) - vnode + PatchedVNode( + vnode.sel, + vnode.data, + None, + vnode.text, + vnode.key, + elm, + None + ) } case Some(text) if oldVnode.text.forall(_ != text) => oldCh.foreach(oldChildren => removeVnodes(elm, oldChildren, 0, oldChildren.length - 1) ) api.setTextContent(elm, Some(text)) - vnode - case Some(_) => vnode + PatchedVNode( + vnode.sel, + vnode.data, + None, + vnode.text, + vnode.key, + elm, + None + ) + case Some(_) => + PatchedVNode( + vnode.sel, + vnode.data, + None, + vnode.text, + vnode.key, + elm, + None + ) } hook.flatMap(_.postpatch).foreach(hook => hook(oldVnode, vnode1)) @@ -448,20 +542,21 @@ object init { } - def patch(oldVnode: VNode, vnode: VNode): VNode = { + def patch(oldVnode: PatchedVNode, vnode: VNode): PatchedVNode = { - val insertedVNodeQueue: VNodeQueue = mutable.ArrayBuffer.empty[VNode] + val insertedVNodeQueue: VNodeQueue = + mutable.ArrayBuffer.empty[PatchedVNode] cbs.pre.foreach(_()) val vnode0 = if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVNodeQueue) } else { - val elm = oldVnode.elm.get + val elm = oldVnode.elm val parent = api.parentNode(elm) val vnode1 = createElm(vnode, insertedVNodeQueue) parent match { case Some(parent) => - api.insertBefore(parent, vnode1.elm.get, api.nextSibling(elm)) + api.insertBefore(parent, vnode1.elm, api.nextSibling(elm)) removeVnodes(parent, Array(oldVnode), 0, 0) case None => () } @@ -480,27 +575,30 @@ object init { new Patch { - override def apply(oldVnode: VNode, vnode: VNode): VNode = + override def apply(oldVnode: PatchedVNode, vnode: VNode): PatchedVNode = patch(oldVnode, vnode) - override def apply(elm: dom.Element, vnode: VNode): VNode = + override def apply(elm: dom.Element, vnode: VNode): PatchedVNode = patch(emptyNodeAt(elm), vnode) - override def apply(frag: dom.DocumentFragment, vnode: VNode): VNode = + override def apply( + frag: dom.DocumentFragment, + vnode: VNode + ): PatchedVNode = patch(emptyDocumentFragmentAt(frag), vnode) } } - private def sameVnode(vnode1: VNode, vnode2: VNode): Boolean = { + private def sameVnode(vnode1: PatchedVNode, vnode2: VNode): Boolean = { vnode1.key == vnode2.key && vnode1.data.is == vnode2.data.is && vnode1.sel == vnode2.sel } private def createKeyToOldIdx( - children: Array[VNode], + children: Array[PatchedVNode], beginIdx: Int, endIdx: Int ): Map[String, Int] = { diff --git a/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala b/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala index 38bdca2..d2b2f3c 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala @@ -45,12 +45,14 @@ object Attributes { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(vNode: VNode): Unit = - updateAttrs(None, vNode) + override def apply(vNode: PatchedVNode): Unit = setAttrs(vNode) }), update = Some(new UpdateHook { - override def apply(oldVNode: VNode, vNode: VNode): VNode = { - updateAttrs(Some(oldVNode), vNode) + override def apply( + oldVNode: PatchedVNode, + vNode: VNode + ): VNode = { + updateAttrs(oldVNode, vNode) vNode } }) @@ -59,9 +61,37 @@ object Attributes { private val xlinkNS = "http://www.w3.org/1999/xlink" private val xmlNS = "http://www.w3.org/XML/1998/namespace" - private def updateAttrs(oldVnode: Option[VNode], vnode: VNode): Unit = { + private def setAttrs(vnode: PatchedVNode): Unit = { - val elm = vnode.elm.get.asInstanceOf[dom.Element] + val elm = vnode.elm.asInstanceOf[dom.Element] + + val attrs = vnode.data.attrs + attrs.foreach { case (key, cur) => + if (cur == true) { + elm.setAttribute(key, "") + } else if (cur == false) { + elm.removeAttribute(key) + } else { + if (key.charAt(0) != 'x') { + elm.setAttribute(key, cur.toString) + } else if (key.length > 3 && key.charAt(3) == ':') { + elm.setAttributeNS(xmlNS, key, cur.toString) + } else if (key.length > 5 && key.charAt(5) == ':') { + elm.setAttributeNS(xlinkNS, key, cur.toString) + } else { + elm.setAttribute(key, cur.toString) + } + } + } + + } + + private def updateAttrs( + oldVnode: PatchedVNode, + vnode: VNode + ): Unit = { + + val elm = oldVnode.elm.asInstanceOf[dom.Element] def update( oldAttrs: Map[String, AttrValue], @@ -95,7 +125,7 @@ object Attributes { } } - val oldAttrs = oldVnode.map(_.data.attrs).getOrElse(Map.empty) + val oldAttrs = oldVnode.data.attrs val attrs = vnode.data.attrs if (oldAttrs != attrs) { diff --git a/snabbdom/src/main/scala/snabbdom/modules/Classes.scala b/snabbdom/src/main/scala/snabbdom/modules/Classes.scala index 86ca80a..fe1b2d2 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Classes.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Classes.scala @@ -45,20 +45,35 @@ object Classes { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(vNode: VNode): Unit = - updateClasses(None, vNode) + override def apply(vNode: PatchedVNode): Unit = + setClasses(vNode) }), update = Some(new UpdateHook { - override def apply(oldVNode: VNode, vNode: VNode): VNode = { - updateClasses(Some(oldVNode), vNode) + override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { + updateClasses(oldVNode, vNode) vNode } }) ) - private def updateClasses(oldVnode: Option[VNode], vnode: VNode): Unit = { + private def setClasses(vnode: PatchedVNode): Unit = { - val elm = vnode.elm.get.asInstanceOf[dom.Element] + val elm = vnode.elm.asInstanceOf[dom.Element] + val classes = vnode.data.classes + + classes.foreach { case (name, cur) => + if (cur) { + elm.classList.add(name) + } else { + elm.classList.remove(name) + } + } + + } + + private def updateClasses(oldVnode: PatchedVNode, vnode: VNode): VNode = { + + val elm = oldVnode.elm.asInstanceOf[dom.Element] def update( oldClass: Map[String, Boolean], @@ -80,12 +95,13 @@ object Classes { } } - val oldClasses = oldVnode.map(_.data.classes).getOrElse(Map.empty) + val oldClasses = oldVnode.data.classes val classes = vnode.data.classes if (oldClasses != classes) { update(oldClasses, classes) } + vnode } diff --git a/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala b/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala index 8956e89..dd7bbd0 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala @@ -46,12 +46,12 @@ object Dataset { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(vNode: VNode): Unit = - updateDataset(None, vNode) + override def apply(vNode: PatchedVNode): Unit = + setDataset(vNode) }), update = Some(new UpdateHook { - override def apply(oldVNode: VNode, vNode: VNode): VNode = { - updateDataset(Some(oldVNode), vNode) + override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { + updateDataset(oldVNode, vNode) vNode } }) @@ -59,10 +59,30 @@ object Dataset { private val CAPS_REGEX = "[A-Z]" - private def updateDataset(oldVnode: Option[VNode], vnode: VNode): Unit = { + private def setDataset(vnode: PatchedVNode): Unit = { - val elm = vnode.elm.get.asInstanceOf[dom.HTMLElement] - val oldDataset = oldVnode.map(_.data.dataset).getOrElse(Map.empty) + val elm = vnode.elm.asInstanceOf[dom.HTMLElement] + val d = elm.dataset + val dataset = vnode.data.dataset + + dataset.foreach { case (key, value) => + if (!js.isUndefined(d)) { // TODO: does this make sense? + d += (key -> value) + } else { + elm.setAttribute( + "data-" + key.replaceAll(CAPS_REGEX, "-$&").toLowerCase(), + value + ) + } + + } + + } + + private def updateDataset(oldVnode: PatchedVNode, vnode: VNode): Unit = { + + val elm = oldVnode.elm.asInstanceOf[dom.HTMLElement] + val oldDataset = oldVnode.data.dataset val dataset = vnode.data.dataset val d = elm.dataset diff --git a/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala b/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala index 40a36d4..9c86a04 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala @@ -45,18 +45,16 @@ object EventListeners { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(vNode: VNode): Unit = { - updateEventListeners(None, Some(vNode)) + override def apply(vNode: PatchedVNode): Unit = { () } }), update = Some(new UpdateHook { - override def apply(oldVNode: VNode, vNode: VNode): VNode = - updateEventListeners(Some(oldVNode), Some(vNode)).get + override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = + updateEventListeners(oldVNode, vNode) }), destroy = Some(new DestroyHook { - override def apply(vnode: VNode): Unit = { - updateEventListeners(Some(vnode), None) + override def apply(vnode: PatchedVNode): Unit = { () } }) @@ -67,15 +65,14 @@ object EventListeners { } private def updateEventListeners( - oldVnode: Option[VNode], - vnode: Option[VNode] - ): Option[VNode] = { + oldVnode: PatchedVNode, + vnode: VNode + ): VNode = { - val oldOn = oldVnode.map(_.data.on).getOrElse(Map.empty) - val oldListener = oldVnode.flatMap(_.listener) - val oldElm = oldVnode.flatMap(_.elm).map(_.asInstanceOf[dom.Element]) - val on = vnode.map(_.data.on).getOrElse(Map.empty) - val elm = vnode.flatMap(_.elm).map(_.asInstanceOf[dom.Element]) + val oldOn = oldVnode.data.on + val oldListener = oldVnode.listener + val elm = oldVnode.elm.asInstanceOf[dom.Element] + val on = vnode.data.on if (oldOn != on) { @@ -83,16 +80,12 @@ object EventListeners { val ol = oldListener.get if (on.isEmpty) { oldOn.foreach { case (name, _) => - oldElm.foreach { elm => - elm.removeEventListener(name, ol.jsFun, false) - } + elm.removeEventListener(name, ol.jsFun, false) } } else { oldOn.foreach { case (name, _) => if (on.get(name).isEmpty) { - oldElm.foreach( - _.removeEventListener(name, ol.jsFun, false) - ) + elm.removeEventListener(name, ol.jsFun, false) } } } @@ -100,25 +93,25 @@ object EventListeners { if (on.nonEmpty) { - val vnode0 = vnode.get - - val listener = oldListener.getOrElse(createListener(vnode0)) + val listener = oldListener.getOrElse(createListener(vnode)) if (oldOn.isEmpty) { on.foreach { case (name, _) => - elm.foreach(_.addEventListener(name, listener.jsFun, false)) + elm.addEventListener(name, listener.jsFun, false) } } else { on.foreach { case (name, _) => if (!oldOn.contains(name)) { - elm.foreach(_.addEventListener(name, listener.jsFun, false)) + elm.addEventListener(name, listener.jsFun, false) } } } - val vnode1 = vnode0.copy(listener = Some(listener)) + // TODO + // val vnode1 = vnode.copy(listener = Some(listener)) + val vnode1 = vnode listener.vnode = vnode1 - Some(vnode1) + vnode1 } else { vnode diff --git a/snabbdom/src/main/scala/snabbdom/modules/Props.scala b/snabbdom/src/main/scala/snabbdom/modules/Props.scala index 0294217..9003117 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Props.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Props.scala @@ -45,20 +45,36 @@ object Props { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(vNode: VNode): Unit = - updateProps(None, vNode) + override def apply(vNode: PatchedVNode): Unit = + setProps(vNode) }), update = Some(new UpdateHook { - override def apply(oldVNode: VNode, vNode: VNode): VNode = { - updateProps(Some(oldVNode), vNode) + override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { + updateProps(oldVNode, vNode) vNode } }) ) - private def updateProps(oldVnode: Option[VNode], vnode: VNode): Unit = { - val elm = vnode.elm.get - val oldProps = oldVnode.map(_.data.props).getOrElse(Map.empty) + private def setProps(vnode: PatchedVNode): Unit = { + + val elm = vnode.elm + val props = vnode.data.props + + props.foreach { case (key, cur) => + if ( + (key != "value" || elm + .asInstanceOf[js.Dictionary[Any]] + .get(key) + .forall(_ != cur)) + ) { elm.asInstanceOf[js.Dictionary[Any]](key) = cur } + } + + } + + private def updateProps(oldVnode: PatchedVNode, vnode: VNode): Unit = { + val elm = oldVnode.elm + val oldProps = oldVnode.data.props val props = vnode.data.props def update( diff --git a/snabbdom/src/main/scala/snabbdom/modules/Styles.scala b/snabbdom/src/main/scala/snabbdom/modules/Styles.scala index b0f0bf5..4d5158a 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Styles.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Styles.scala @@ -47,20 +47,41 @@ object Styles { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(vNode: VNode): Unit = - updateStyle(None, vNode) + override def apply(vNode: PatchedVNode): Unit = + setStyle(vNode) }), update = Some(new UpdateHook { - override def apply(oldVNode: VNode, vNode: VNode): VNode = { - updateStyle(Some(oldVNode), vNode) + override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { + updateStyle(oldVNode, vNode) vNode } }) ) - private def updateStyle(oldVnode: Option[VNode], vnode: VNode): Unit = { + private def setStyle(vnode: PatchedVNode): Unit = { + val elm = vnode.elm + val style = vnode.data.style + + style.foreach { case (name, cur) => + if (name(0) == '-' && name(1) == '-') { + + elm.asInstanceOf[dom.HTMLElement].style.setProperty(name, cur) + + } else { + + elm + .asInstanceOf[dom.HTMLElement] + .style + .asInstanceOf[js.Dictionary[String]](name) = cur + + } + } + + } + + private def updateStyle(oldVnode: PatchedVNode, vnode: VNode): Unit = { - val elm = vnode.elm.get + val elm = oldVnode.elm def update( oldStyle: Map[String, StyleValue], @@ -102,7 +123,7 @@ object Styles { } - val oldStyle = oldVnode.map(_.data.style).getOrElse(Map.empty) + val oldStyle = oldVnode.data.style val style = vnode.data.style if (oldStyle != style) { diff --git a/snabbdom/src/main/scala/snabbdom/thunk.scala b/snabbdom/src/main/scala/snabbdom/thunk.scala index b8b70d4..08607ef 100644 --- a/snabbdom/src/main/scala/snabbdom/thunk.scala +++ b/snabbdom/src/main/scala/snabbdom/thunk.scala @@ -61,8 +61,9 @@ object thunk { ): VNode = { val hook = Hooks().copy( init = Some((vNode: VNode) => init0(vNode)), - prepatch = - Some((oldVNode: VNode, vNode: VNode) => prepatch0(oldVNode, vNode)) + prepatch = Some((oldVNode: PatchedVNode, vNode: VNode) => + prepatch0(oldVNode, vNode) + ) ) val data = VNodeData(key = key, fn = Some(fn), args = Some(args), hook = Some(hook)) @@ -76,12 +77,11 @@ object thunk { thunk.copy( children = vnode.children, data = vnode.data.copy(fn = Some(fn), args = Some(args)), - text = vnode.text, - elm = vnode.elm + text = vnode.text ) } - private def prepatch0(oldVnode: VNode, thunk: VNode): VNode = { + private def prepatch0(oldVnode: PatchedVNode, thunk: VNode): VNode = { val old = oldVnode.data val cur = thunk.data val oldArgs = old.args @@ -93,11 +93,10 @@ object thunk { thunk.copy( children = vnode.children, data = vnode.data.copy(fn = curFn, args = args), - text = vnode.text, - elm = vnode.elm + text = vnode.text ) } else { - oldVnode + oldVnode.toVNode } } diff --git a/snabbdom/src/main/scala/snabbdom/toVNode.scala b/snabbdom/src/main/scala/snabbdom/toVNode.scala index 59c3ee6..b68aaf2 100644 --- a/snabbdom/src/main/scala/snabbdom/toVNode.scala +++ b/snabbdom/src/main/scala/snabbdom/toVNode.scala @@ -44,7 +44,7 @@ import scala.collection.mutable object toVNode { - def apply(node: dom.Node, domApi: Option[DomApi] = None): VNode = { + def apply(node: dom.Node, domApi: Option[DomApi] = None): PatchedVNode = { val api = domApi.getOrElse(DomApi.apply) @@ -57,7 +57,7 @@ object toVNode { val attrs = mutable.Map.empty[String, String] val datasets = mutable.Map.empty[String, String] - val children = new mutable.ArrayBuffer[VNode] + val children = new mutable.ArrayBuffer[PatchedVNode] val elmAttrs = elm.attributes val elmChildren = elm.childNodes elmAttrs.foreach { case (_, attr) => @@ -79,12 +79,14 @@ object toVNode { dataset = if (datasets.nonEmpty) datasets.toMap else Map.empty ) - val vnode = VNode.create( + val vnode = PatchedVNode( Some(sel), data, Some(children.toArray), None, - Some(node) + None, + node, + None ) if ( @@ -99,12 +101,20 @@ object toVNode { } else if (api.isText(node)) { val text = api.getTextContent(node).getOrElse("") - VNode.create(None, VNodeData.empty, None, Some(text), Some(node)) + PatchedVNode(None, VNodeData.empty, None, Some(text), None, node, None) } else if (api.isComment(node)) { val text = api.getTextContent(node).getOrElse("") - VNode.create(Some("!"), VNodeData.empty, None, Some(text), Some(node)) + PatchedVNode( + Some("!"), + VNodeData.empty, + None, + Some(text), + None, + node, + None + ) } else { - VNode.create(Some(""), VNodeData.empty, None, None, Some(node)) + PatchedVNode(Some(""), VNodeData.empty, None, None, None, node, None) } } diff --git a/snabbdom/src/test/scala/snabbdom/AttributesSuite.scala b/snabbdom/src/test/scala/snabbdom/AttributesSuite.scala index 3f3e930..be911fc 100644 --- a/snabbdom/src/test/scala/snabbdom/AttributesSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/AttributesSuite.scala @@ -65,7 +65,7 @@ class AttributesSuite extends BaseSuite { ) ) ) - val elm = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.Element] + val elm = patch(vnode0, vnode1).elm.asInstanceOf[dom.Element] assertEquals(elm.getAttribute("href"), "/foo") assertEquals(elm.getAttribute("minlength"), "1") assertEquals(elm.hasAttribute("selected"), true) @@ -80,12 +80,12 @@ class AttributesSuite extends BaseSuite { val vnode1 = h("div", cachedAttrs) val vnode2 = h("div", cachedAttrs) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] + val elm1 = vnode1p.elm.asInstanceOf[dom.Element] assertEquals(elm1.getAttribute("href"), "/foo") assertEquals(elm1.getAttribute("minlength"), "1") assertEquals(elm1.hasAttribute("selected"), true) - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.Element] assertEquals(elm2.getAttribute("href"), "/foo") assertEquals(elm2.getAttribute("minlength"), "1") assertEquals(elm2.hasAttribute("selected"), true) @@ -104,7 +104,7 @@ class AttributesSuite extends BaseSuite { ) ) ) - val elm = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.Element] + val elm = patch(vnode0, vnode1).elm.asInstanceOf[dom.Element] assertEquals(elm.hasAttribute("href"), true) assertEquals(elm.hasAttribute("minlength"), true) assertEquals(elm.hasAttribute("value"), true) @@ -114,7 +114,7 @@ class AttributesSuite extends BaseSuite { vnode0.test("are set correctly when namespaced") { vnode0 => val vnode1 = h("div", VNodeData(attrs = Map("xlink:href" -> "#foo"))) - val elm = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.Element] + val elm = patch(vnode0, vnode1).elm.asInstanceOf[dom.Element] assertEquals( elm.getAttributeNS("http://www.w3.org/1999/xlink", "href"), "#foo" @@ -131,7 +131,7 @@ class AttributesSuite extends BaseSuite { VNodeData(), Array[VNode]("Hello") ) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = patch(vnode0, vnode1).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm1.tagName, "DIV") assertEquals(elm1.id, "myId") assertEquals(elm1.className, "myClass") @@ -154,7 +154,7 @@ class AttributesSuite extends BaseSuite { ) ) ) - val elm = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val elm = patch(vnode0, vnode1).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm.hasAttribute("required"), true) assertEquals(elm.getAttribute("required"), "") assertEquals(elm.hasAttribute("readonly"), true) @@ -165,7 +165,7 @@ class AttributesSuite extends BaseSuite { vnode0.test("is omitted if the value is false") { vnode0 => val vnode1 = h("div", VNodeData(attrs = Map("required" -> false))) - val elm = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val elm = patch(vnode0, vnode1).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm.hasAttribute("required"), false) assertEquals(elm.getAttribute("required"), null) } @@ -176,7 +176,7 @@ class AttributesSuite extends BaseSuite { "div", VNodeData(attrs = Map("readonly" -> 0, "noresize" -> "")) ) - val elm = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLElement] + val elm = patch(vnode0, vnode1).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm.hasAttribute("readonly"), true) assertEquals(elm.hasAttribute("noresize"), true) } @@ -191,12 +191,12 @@ class AttributesSuite extends BaseSuite { ) { vnode0 => val vnode1 = h("div", VNodeData(attrs = Map("constructor" -> true))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] + val elm1 = vnode1p.elm.asInstanceOf[dom.Element] assertEquals(elm1.hasAttribute("constructor"), true) assertEquals(elm1.getAttribute("constructor"), "") val vnode2 = h("div", VNodeData(attrs = Map("constructor" -> false))) - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.Element] assertEquals(elm2.hasAttribute("constructor"), false) } diff --git a/snabbdom/src/test/scala/snabbdom/DatasetSuite.scala b/snabbdom/src/test/scala/snabbdom/DatasetSuite.scala index 74f3c23..9a3f1a1 100644 --- a/snabbdom/src/test/scala/snabbdom/DatasetSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/DatasetSuite.scala @@ -56,7 +56,7 @@ class DatasetSuite extends BaseSuite { val elm = patch( vnode0, h("div", VNodeData(dataset = Map("foo" -> "foo"))) - ).elm.get.asInstanceOf[dom.HTMLElement] + ).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm.dataset("foo"), "foo") } diff --git a/snabbdom/src/test/scala/snabbdom/EventListenersSuite.scala b/snabbdom/src/test/scala/snabbdom/EventListenersSuite.scala index 7289305..656dc42 100644 --- a/snabbdom/src/test/scala/snabbdom/EventListenersSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/EventListenersSuite.scala @@ -66,7 +66,7 @@ class EventListenersSuite extends BaseSuite { VNodeData(on = Map("click" -> clicked)), Array(h("a", "Click my parent")) ) - val elm = patch(vnode0, vnode).elm.get + val elm = patch(vnode0, vnode).elm elm.asInstanceOf[dom.HTMLElement].click() assertEquals(result.result().length, 1) } @@ -87,9 +87,9 @@ class EventListenersSuite extends BaseSuite { Array(h("a", "Click my parent")) ) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] elm1.click() - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.HTMLElement] elm2.click() val result0 = result.result() assertEquals(result0, List(1, 2)) @@ -107,11 +107,11 @@ class EventListenersSuite extends BaseSuite { Array(h("a", "Click my parent")) ) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] elm1.click() assertEquals(result.length, 1) val vnode2 = h("div", VNodeData(), Array(h("a", "Click my parent"))) - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.HTMLElement] elm2.click() assertEquals(result.length, 1) } @@ -133,7 +133,7 @@ class EventListenersSuite extends BaseSuite { Array(h("a", "Click my parent")) ) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] elm1.click() assertEquals(called, 3) val vnode2 = h( @@ -143,7 +143,7 @@ class EventListenersSuite extends BaseSuite { ), Array(h("a", "Click my parent")) ) - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.HTMLElement] elm2.click() assertEquals(called, 5) } @@ -160,10 +160,10 @@ class EventListenersSuite extends BaseSuite { Array(h("a", "Click my parent")) ) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] elm1.click() assertEquals(result.length, 1) - assertEquals(result(0), vnode1p) + assertEquals(result(0), vnode1p.toVNode) } vnode0.test("shared handlers in parent and child nodes") { vnode0 => @@ -183,7 +183,7 @@ class EventListenersSuite extends BaseSuite { ) ) ) - val elm1 = patch(vnode0, vnode1).elm.get.asInstanceOf[dom.HTMLDivElement] + val elm1 = patch(vnode0, vnode1).elm.asInstanceOf[dom.HTMLDivElement] elm1.click() assertEquals(result.length, 1) elm1.firstChild.asInstanceOf[dom.HTMLElement].click() diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index 66c7d6d..8657912 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -144,7 +144,7 @@ class SnabbdomSuite extends BaseSuite { group("created element") { vnode0.test("has tag") { vnode0 => - val elm = patch(vnode0, h("div")).elm.get.asInstanceOf[dom.Element] + val elm = patch(vnode0, h("div")).elm.asInstanceOf[dom.Element] assertEquals(elm.tagName, "DIV") } @@ -152,13 +152,13 @@ class SnabbdomSuite extends BaseSuite { val elm = dom.document.createElement("div") vnode0.appendChild(elm) val vnode1 = h("span#id") - val patched = patch(elm, vnode1).elm.get.asInstanceOf[dom.HTMLSpanElement] + val patched = patch(elm, vnode1).elm.asInstanceOf[dom.HTMLSpanElement] assertEquals(patched.tagName, "SPAN") assertEquals(patched.id, "id") } vnode0.test("has id") { vnode0 => - val elm = patch(vnode0, h("div", Array(h("div#unique")))).elm.get + val elm = patch(vnode0, h("div", Array(h("div#unique")))).elm assertEquals(elm.firstChild.asInstanceOf[dom.Element].id, "unique") } @@ -168,7 +168,7 @@ class SnabbdomSuite extends BaseSuite { val data = VNodeData(ns = Some(SVGNamespace)) - val elm1 = patch(vnode0, h("div", Array(h("div", data)))).elm.get + val elm1 = patch(vnode0, h("div", Array(h("div", data)))).elm assertEquals(elm1.firstChild.namespaceURI, SVGNamespace) // verify that svg tag automatically gets svg namespace @@ -183,24 +183,24 @@ class SnabbdomSuite extends BaseSuite { ) ) ) - ).elm.get + ).elm assertEquals(elm2.namespaceURI, SVGNamespace) assertEquals(elm2.firstChild.namespaceURI, SVGNamespace) assertEquals(elm2.firstChild.firstChild.namespaceURI, XHTMLNamespace) // verify that svg tag with extra selectors gets svg namespace - val elm3 = patch(vnode0, h("svg#some-id")).elm.get + val elm3 = patch(vnode0, h("svg#some-id")).elm assertEquals(elm3.namespaceURI, SVGNamespace); // verify that non-svg tag beginning with 'svg' does NOT get namespace - val elm4 = patch(vnode0, h("svg-custom-el")).elm.get + val elm4 = patch(vnode0, h("svg-custom-el")).elm assertNotEquals(elm4.namespaceURI, SVGNamespace) } vnode0.test("receives classes in selector") { vnode0 => - val elm = patch(vnode0, h("div", Array(h("i.am.a.class")))).elm.get + val elm = patch(vnode0, h("div", Array(h("i.am.a.class")))).elm assert(elm.firstChild.asInstanceOf[dom.Element].classList.contains("am")) assert(elm.firstChild.asInstanceOf[dom.Element].classList.contains("a")) assert( @@ -217,7 +217,7 @@ class SnabbdomSuite extends BaseSuite { "not" -> false ) ) - val elm = patch(vnode0, h("i", data)).elm.get + val elm = patch(vnode0, h("i", data)).elm assert(elm.asInstanceOf[dom.Element].classList.contains("am")) assert(elm.asInstanceOf[dom.Element].classList.contains("a")) assert(elm.asInstanceOf[dom.Element].classList.contains("class")) @@ -225,7 +225,7 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("receives classes in selector when namespaced") { vnode0 => - val elm = patch(vnode0, h("svg", Array(h("g.am.a.class.too")))).elm.get + val elm = patch(vnode0, h("svg", Array(h("g.am.a.class.too")))).elm assert(elm.firstChild.asInstanceOf[dom.Element].classList.contains("am")) assert(elm.firstChild.asInstanceOf[dom.Element].classList.contains("a")) assert( @@ -243,7 +243,7 @@ class SnabbdomSuite extends BaseSuite { "not" -> false ) ) - val elm = patch(vnode0, h("svg", Array(h("g", data)))).elm.get + val elm = patch(vnode0, h("svg", Array(h("g", data)))).elm assert( elm.firstChild.asInstanceOf[dom.Element].classList.contains("am") ) @@ -258,7 +258,7 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("handles classes from both selector and property") { vnode0 => val data = VNodeData(classes = Map("classes" -> true)) - val elm = patch(vnode0, h("div", Array(h("i.has", data)))).elm.get + val elm = patch(vnode0, h("div", Array(h("i.has", data)))).elm assert(elm.firstChild.asInstanceOf[dom.Element].classList.contains("has")) assert( elm.firstChild.asInstanceOf[dom.Element].classList.contains("classes") @@ -266,13 +266,13 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("can create elements with text content") { vnode0 => - val elm = patch(vnode0, h("div", Array[VNode]("I am a string"))).elm.get + val elm = patch(vnode0, h("div", Array[VNode]("I am a string"))).elm assertEquals(elm.asInstanceOf[dom.Element].innerHTML, "I am a string") } vnode0.test("can create elements with span and text content") { vnode0 => val elm = - patch(vnode0, h("a", Array[VNode](h("span"), "I am a string"))).elm.get + patch(vnode0, h("a", Array[VNode](h("span"), "I am a string"))).elm assertEquals(elm.childNodes(0).asInstanceOf[dom.Element].tagName, "SPAN") assertEquals( elm.childNodes(1).asInstanceOf[dom.Element].textContent, @@ -281,13 +281,13 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("can create vnode with array String obj content") { vnode0 => - val elm = patch(vnode0, h("a", Array[VNode]("b", "c"))).elm.get + val elm = patch(vnode0, h("a", Array[VNode]("b", "c"))).elm assertEquals(elm.asInstanceOf[dom.Element].innerHTML, "bc") } vnode0.test("can create elements with props") { vnode0 => val data = VNodeData(props = Map("src" -> "http://localhost/")) - val elm = patch(vnode0, h("a", data)).elm.get + val elm = patch(vnode0, h("a", data)).elm assertEquals( elm.asInstanceOf[js.Dictionary[String]]("src"), "http://localhost/" @@ -304,7 +304,7 @@ class SnabbdomSuite extends BaseSuite { elmWithIdAndClass.asInstanceOf[dom.HTMLElement].className = "class" val vnode1 = h("div#id.class", Array(h("span", "Hi"))); val elm = - patch(elmWithIdAndClass, vnode1).elm.get.asInstanceOf[dom.Element] + patch(elmWithIdAndClass, vnode1).elm.asInstanceOf[dom.Element] assertEquals(elm, elmWithIdAndClass) assertEquals(elm.tagName, "DIV") assertEquals(elm.id, "id") @@ -312,7 +312,7 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("can create comments") { vnode0 => - val elm = patch(vnode0, h("!", "test")).elm.get + val elm = patch(vnode0, h("!", "test")).elm assertEquals(elm.nodeType, dom.Node.COMMENT_NODE) assertEquals(elm.textContent, "test") } @@ -326,7 +326,7 @@ class SnabbdomSuite extends BaseSuite { fragment( Array[VNode]("I am", h("span", Array[VNode](" a", " fragment"))) ) - val elm = patch(vnode0, vnode1).elm.get + val elm = patch(vnode0, vnode1).elm assertEquals(elm.nodeType, dom.Node.DOCUMENT_FRAGMENT_NODE) assertEquals(elm.textContent, "I am a fragment") } @@ -344,7 +344,7 @@ class SnabbdomSuite extends BaseSuite { VNodeData(classes = Map("i" -> true, "am" -> true, "horse" -> false)) ) val vnode1p = patch(vnode0, vnode1) - val elm = patch(vnode1p, vnode2).elm.get + val elm = patch(vnode1p, vnode2).elm assert(elm.asInstanceOf[dom.Element].classList.contains("i")) assert(elm.asInstanceOf[dom.Element].classList.contains("am")) assert(!elm.asInstanceOf[dom.Element].classList.contains("horse")) @@ -360,11 +360,11 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("i", cachedClasses) val vnode2 = h("i", cachedClasses) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get.asInstanceOf[dom.Element] + val elm = vnode1p.elm.asInstanceOf[dom.Element] assert(elm.classList.contains("i")) assert(elm.classList.contains("am")) assert(!elm.classList.contains("horse")) - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.Element] assert(elm2.classList.contains("i")) assert(elm2.classList.contains("am")) assert(!elm2.classList.contains("horse")) @@ -380,7 +380,7 @@ class SnabbdomSuite extends BaseSuite { VNodeData(classes = Map("i" -> true, "am" -> true)) ) val vnode1p = patch(vnode0, vnode1) - val elm = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] + val elm = patch(vnode1p, vnode2).elm.asInstanceOf[dom.Element] assert(elm.classList.contains("i")) assert(elm.classList.contains("am")) assert(!elm.classList.contains("horse")) @@ -393,7 +393,7 @@ class SnabbdomSuite extends BaseSuite { h("a", VNodeData(props = Map("src" -> "http://localhost/"))) val vnode1p = patch(vnode0, vnode1) val elm = - patch(vnode1p, vnode2).elm.get.asInstanceOf[js.Dictionary[String]] + patch(vnode1p, vnode2).elm.asInstanceOf[js.Dictionary[String]] assertEquals(elm("src"), "http://localhost/") } @@ -402,12 +402,12 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("a", cachedProps) val vnode2 = h("a", cachedProps) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals( elm.asInstanceOf[js.Dictionary[String]]("src"), "http://other/" ) - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals( elm2.asInstanceOf[js.Dictionary[String]]("src"), "http://other/" @@ -418,14 +418,14 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("p", VNodeData(props = Map("textContent" -> "foo"))) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals( elm.asInstanceOf[dom.HTMLParagraphElement].textContent, "foo" ) val vnode2 = h("p", VNodeData(props = Map("textContent" -> ""))) - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.asInstanceOf[dom.HTMLParagraphElement].textContent, "") } @@ -436,22 +436,22 @@ class SnabbdomSuite extends BaseSuite { h("a", VNodeData(props = Map("src" -> "http://other/"))) val vnode2 = h("a") val vnode1p = patch(vnode0, vnode1) - val elm = patch(vnode1p, vnode2).elm.get + val elm = patch(vnode1p, vnode2).elm assert(!elm.asInstanceOf[js.Dictionary[String]].contains("src")) } - vnode0.test("cannot remove native props") { vnode0 => + vnode0.test("cannot remove native props".only) { vnode0 => val vnode1 = h("a", VNodeData(props = Map("href" -> "http://example.com/"))) val vnode2 = h("a") val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get + val elm1 = vnode1p.elm assert(elm1.isInstanceOf[dom.HTMLAnchorElement]) assertEquals( elm1.asInstanceOf[dom.HTMLAnchorElement].href, "http://example.com/" ) - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assert(elm2.isInstanceOf[dom.HTMLAnchorElement]) assertEquals( elm2.asInstanceOf[dom.HTMLAnchorElement].href, @@ -464,10 +464,10 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("p", VNodeData(props = Map("a" -> "foo"))) val vnode2 = h("p") val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assert(elm.isInstanceOf[dom.HTMLParagraphElement]) assertEquals(elm.asInstanceOf[js.Dictionary[String]]("a"), "foo") - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.asInstanceOf[js.Dictionary[String]]("a"), "foo") } } @@ -484,7 +484,7 @@ class SnabbdomSuite extends BaseSuite { prevElm.appendChild(h2) val nextVNode = h("div#id.class", Array(h("span", "Hi"))) val elm = - patch(toVNode(prevElm), nextVNode).elm.get + patch(toVNode(prevElm), nextVNode).elm .asInstanceOf[dom.HTMLElement] assertEquals(elm, prevElm) assertEquals(elm.tagName, "DIV") @@ -507,11 +507,11 @@ class SnabbdomSuite extends BaseSuite { Some(""), VNodeData.empty, Some(Array(h("div#id.class", Array(h("span", "Hi"))))), - None, - Some(prevElm) + None + // Some(prevElm) ) val elm = - patch(toVNode(prevElm), nextVNode).elm.get + patch(toVNode(prevElm), nextVNode).elm .asInstanceOf[dom.DocumentFragment] assertEquals(elm, prevElm) assertEquals(elm.nodeType, 11) @@ -564,7 +564,7 @@ class SnabbdomSuite extends BaseSuite { prevElm.appendChild(h2) val nextVNode = h("div#id.class", Array[VNode]("Foobar")) val elm = - patch(toVNode(prevElm), nextVNode).elm.get.asInstanceOf[dom.HTMLElement] + patch(toVNode(prevElm), nextVNode).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm, prevElm) assertEquals(elm.tagName, "DIV") assertEquals(elm.id, "id") @@ -591,7 +591,7 @@ class SnabbdomSuite extends BaseSuite { prevElm.appendChild(h2) val nextVNode = h("div#id.class", Array(h("h2", "Hello"))) val elm = - patch(toVNode(prevElm), nextVNode).elm.get.asInstanceOf[dom.HTMLElement] + patch(toVNode(prevElm), nextVNode).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm, prevElm) assertEquals(elm.tagName, "DIV") @@ -651,10 +651,10 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("1").map(spanNum)) val vnode2 = h("span", Array("1", "2", "3").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 1) val vnode12 = patch(vnode1p, vnode2) - val elm2 = vnode12.elm.get + val elm2 = vnode12.elm assertEquals(elm2.asInstanceOf[dom.Element].children.length, 3) assertEquals(elm2.asInstanceOf[dom.Element].children(1).innerHTML, "2") assertEquals(elm2.asInstanceOf[dom.Element].children(2).innerHTML, "3") @@ -664,10 +664,10 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("4", "5").map(spanNum)) val vnode2 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 2) val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get + val elm2 = vnode2p.elm assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3", "4", "5") @@ -678,10 +678,10 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("1", "2", "4", "5").map(spanNum)) val vnode2 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get + val elm2 = vnode2p.elm assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3", "4", "5") @@ -692,10 +692,10 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("2", "3", "4").map(spanNum)) val vnode2 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 3) val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get + val elm2 = vnode2p.elm assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3", "4", "5") @@ -710,10 +710,10 @@ class SnabbdomSuite extends BaseSuite { Array("1", "2", "3").map(spanNum) ) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 0) val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get + val elm2 = vnode2p.elm assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3") @@ -728,13 +728,13 @@ class SnabbdomSuite extends BaseSuite { ) val vnode2 = h("span", VNodeData(key = Some("span"))) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals( elm.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3") ) val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get + val elm2 = vnode2p.elm assertEquals(elm2.asInstanceOf[dom.Element].children.length, 0) } @@ -746,14 +746,14 @@ class SnabbdomSuite extends BaseSuite { h("span", data, Array(spanNum("1"), h("i", data2, "2"), spanNum("3"))) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals( elm.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3") ) val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get + val elm2 = vnode2p.elm assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3") @@ -770,10 +770,10 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) val vnode2 = h("span", Array("3", "4", "5").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 5) val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get + val elm2 = vnode2p.elm assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("3", "4", "5") @@ -784,10 +784,10 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) val vnode2 = h("span", Array("1", "2", "3").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 5) val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get + val elm2 = vnode2p.elm assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), List("1", "2", "3") @@ -798,10 +798,10 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) val vnode2 = h("span", Array("1", "2", "4", "5").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 5) val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get + val elm2 = vnode2p.elm assertEquals(elm2.asInstanceOf[dom.Element].children.length, 4) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -815,10 +815,10 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("1", "2", "3", "4").map(spanNum)) val vnode2 = h("span", Array("2", "3", "1", "4").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get + val elm2 = vnode2p.elm assertEquals(elm2.asInstanceOf[dom.Element].children.length, 4) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -830,9 +830,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("1", "2", "3").map(spanNum)) val vnode2 = h("span", Array("2", "3", "1").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 3) - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.asInstanceOf[dom.Element].children.length, 3) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -844,9 +844,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("1", "2", "3", "4").map(spanNum)) val vnode2 = h("span", Array("1", "4", "2", "3").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.asInstanceOf[dom.Element].children.length, 4) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -858,9 +858,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("1", "2", "3", "4").map(spanNum)) val vnode2 = h("span", Array("4", "2", "3", "1").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.asInstanceOf[dom.Element].children.length, 4) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -874,9 +874,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) val vnode2 = h("span", Array("4", "1", "2", "3", "6").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 5) - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.asInstanceOf[dom.Element].children.length, 5) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -888,9 +888,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("1", "4", "5").map(spanNum)) val vnode2 = h("span", Array("4", "6").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 3) - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.asInstanceOf[dom.Element].children.length, 2) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -904,9 +904,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("span", Array("2", "4", "5").map(spanNum)) val vnode2 = h("span", Array("4", "5", "3").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 3) - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.asInstanceOf[dom.Element].children.length, 3) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -924,10 +924,10 @@ class SnabbdomSuite extends BaseSuite { ) ) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) assertEquals(elm.textContent, "1abc") - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.asInstanceOf[dom.Element].children.length, 6) assertEquals(elm2.textContent, "dabc1e") } @@ -940,9 +940,9 @@ class SnabbdomSuite extends BaseSuite { val vnode2 = h("span", Array("8", "7", "6", "5", "4", "3", "2", "1").map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 8) - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.asInstanceOf[dom.Element].children.length, 8) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -956,9 +956,9 @@ class SnabbdomSuite extends BaseSuite { val vnode2 = h("span", Array(4, 3, 2, 1, 5, 0).map(spanNum)) val vnode1p = patch(vnode0, vnode1) - val elm = vnode1p.elm.get + val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 6) - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.asInstanceOf[dom.Element].children.length, 6) assertEquals( elm2.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), @@ -986,7 +986,7 @@ class SnabbdomSuite extends BaseSuite { val shufArr = rng.shuffle(arr) val elm = dom.document.createElement("div") val vnode1p = patch(elm, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLSpanElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLSpanElement] assertEquals( elm1.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), arr.map(_.toString).toList @@ -995,7 +995,7 @@ class SnabbdomSuite extends BaseSuite { val vnode2 = h("span", arr.map(n => spanNumWithOpacity(shufArr(n), opacities(n)))) val elm2 = - patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.HTMLSpanElement] + patch(vnode1p, vnode2).elm.asInstanceOf[dom.HTMLSpanElement] (0 until elms).foreach { i => assertEquals(elm2.children(i).innerHTML, shufArr(i).toString) val opacity = @@ -1011,9 +1011,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("div", Array(h("span", "Hello"))) val vnode2 = h("div", Array(h("span", "Hello"), h("span", "World"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] + val elm1 = vnode1p.elm.asInstanceOf[dom.Element] assertEquals(elm1.children.toSeq.map(_.innerHTML), List("Hello")) - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.Element] assertEquals(elm2.children.toSeq.map(_.innerHTML), List("Hello", "World")) } @@ -1021,9 +1021,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("div", Array[VNode]("Text", h("span", "Span"))) val vnode2 = h("div", Array[VNode]("Text", h("span", "Span"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get + val elm1 = vnode1p.elm assertEquals(elm1.childNodes(0).textContent, "Text") - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.childNodes(0).textContent, "Text") } @@ -1031,9 +1031,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("div", Array[VNode]("Text", h("span", "Span"))) val vnode2 = h("div", Array[VNode]("Text2", h("span", "Span"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get + val elm1 = vnode1p.elm assertEquals(elm1.childNodes(0).textContent, "Text") - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.childNodes(0).textContent, "Text2") } @@ -1041,9 +1041,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("div", Array[VNode](h("!", "Text"), h("span", "Span"))) val vnode2 = h("div", Array[VNode](h("!", "Text"), h("span", "Span"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get + val elm1 = vnode1p.elm assertEquals(elm1.childNodes(0).textContent, "Text") - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.childNodes(0).textContent, "Text") } @@ -1051,9 +1051,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("div", Array[VNode](h("!", "Text"), h("span", "Span"))) val vnode2 = h("div", Array[VNode](h("!", "Text2"), h("span", "Span"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get + val elm1 = vnode1p.elm assertEquals(elm1.childNodes(0).textContent, "Text") - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.childNodes(0).textContent, "Text2") } @@ -1061,9 +1061,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("div", Array[VNode](h("!"), h("span", "Span"))) val vnode2 = h("div", Array[VNode](h("!", "Test"), h("span", "Span"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get + val elm1 = vnode1p.elm assertEquals(elm1.childNodes(0).textContent, "") - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.childNodes(0).textContent, "Test") } @@ -1071,9 +1071,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("div", Array(h("span", "World"))) val vnode2 = h("div", Array(h("span", "Hello"), h("span", "World"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] + val elm1 = vnode1p.elm.asInstanceOf[dom.Element] assertEquals(elm1.children.toSeq.map(_.innerHTML), List("World")) - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.Element] assertEquals(elm2.children.toSeq.map(_.innerHTML), List("Hello", "World")) } @@ -1081,9 +1081,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("div", Array(h("span", "World"))) val vnode2 = h("div", Array(h("div", "Hello"), h("span", "World"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] + val elm1 = vnode1p.elm.asInstanceOf[dom.Element] assertEquals(elm1.children.toSeq.map(_.innerHTML), List("World")) - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.Element] assertEquals(elm2.children.toSeq.map(_.innerHTML), List("Hello", "World")) assertEquals(elm2.children.toSeq.map(_.tagName), List("DIV", "SPAN")) } @@ -1093,12 +1093,12 @@ class SnabbdomSuite extends BaseSuite { h("div", Array(h("span", "One"), h("span", "Two"), h("span", "Three"))) val vnode2 = h("div", Array(h("span", "One"), h("span", "Three"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] + val elm1 = vnode1p.elm.asInstanceOf[dom.Element] assertEquals( elm1.children.toSeq.map(_.innerHTML), List("One", "Two", "Three") ) - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.Element] assertEquals(elm2.children.toSeq.map(_.innerHTML), List("One", "Three")) } @@ -1106,9 +1106,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("div", "One") val vnode2 = h("div") val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get + val elm1 = vnode1p.elm assertEquals(elm1.textContent, "One") - val elm2 = patch(vnode1p, vnode2).elm.get + val elm2 = patch(vnode1p, vnode2).elm assertEquals(elm2.textContent, "") } @@ -1117,9 +1117,9 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("div", "One") val vnode2 = h("div", Array(h("div", "Two"), h("span", "Three"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] + val elm1 = vnode1p.elm.asInstanceOf[dom.Element] assertEquals(elm1.textContent, "One") - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.Element] assertEquals( elm2.childNodes.toSeq.map(_.textContent), List("Two", "Three") @@ -1130,12 +1130,12 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h("div", Array[VNode]("One", h("span", "Two"))) val vnode2 = h("div", Array(h("div", "Three"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get + val elm1 = vnode1p.elm assertEquals( elm1.childNodes.toSeq.map(_.textContent), List("One", "Two") ) - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.Element] assertEquals(elm2.childNodes.length, 1) assertEquals(elm2.childNodes(0).asInstanceOf[dom.Element].tagName, "DIV") assertEquals(elm2.childNodes(0).textContent, "Three") @@ -1147,12 +1147,12 @@ class SnabbdomSuite extends BaseSuite { val vnode2 = h("div", Array(h("b", "Three"), h("span", "One"), h("div", "Two"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.Element] + val elm1 = vnode1p.elm.asInstanceOf[dom.Element] assertEquals( elm1.children.toSeq.map(_.innerHTML), List("One", "Two", "Three") ) - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.Element] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.Element] assertEquals(elm2.children.toSeq.map(_.tagName), List("B", "SPAN", "DIV")) assertEquals( elm2.children.toSeq.map(_.innerHTML), @@ -1173,16 +1173,16 @@ class SnabbdomSuite extends BaseSuite { val vnode3 = fragment(Array("fragment ", "again")) val vnode1p = patch(vnode0, vnode1) - var elm = vnode1p.elm.get + var elm = vnode1p.elm assertEquals(elm.nodeType, dom.Node.DOCUMENT_FRAGMENT_NODE) val vnode2p = patch(vnode1p, vnode2) - elm = vnode2p.elm.get + elm = vnode2p.elm assertEquals(elm.asInstanceOf[dom.Element].tagName, "DIV") assertEquals(elm.textContent, "I am an element") val vnode3p = patch(vnode2p, vnode3) - elm = vnode3p.elm.get + elm = vnode3p.elm assertEquals(elm.nodeType, dom.Node.DOCUMENT_FRAGMENT_NODE) assertEquals(elm.textContent, "fragment again") } @@ -1195,10 +1195,10 @@ class SnabbdomSuite extends BaseSuite { val vnode2 = h("div", "I am an element") val vnode1p = patch(vnode0, vnode1) - var elm = vnode1p.elm.get + var elm = vnode1p.elm assertEquals(elm.nodeType, dom.Node.DOCUMENT_FRAGMENT_NODE) - elm = patch(vnode1p, vnode2).elm.get + elm = patch(vnode1p, vnode2).elm assertEquals(elm.asInstanceOf[dom.Element].tagName, "DIV") } } @@ -1207,11 +1207,11 @@ class SnabbdomSuite extends BaseSuite { vnode0.test( "calls `create` listener before inserted into parent but after children" ) { vnode0 => - val result = List.newBuilder[VNode] + val result = List.newBuilder[PatchedVNode] val cb: CreateHook = vnode => { - assert(vnode.elm.exists(_.isInstanceOf[dom.Element])) - assertEquals(vnode.elm.map(_.childNodes.length), Some(2)) - assertEquals(vnode.elm.map(_.parentNode), Some(null)) + assert(vnode.elm.isInstanceOf[dom.Element]) + assertEquals(vnode.elm.childNodes.length, 2) + assertEquals(vnode.elm.parentNode, null) result.addOne(vnode) } val vnode1 = h( @@ -1236,13 +1236,13 @@ class SnabbdomSuite extends BaseSuite { vnode0.test( "calls `insert` listener after both parents, siblings and children have been inserted" ) { vnode0 => - val result = List.newBuilder[VNode] + val result = List.newBuilder[PatchedVNode] val cb: InsertHook = (vnode) => { - assert(vnode.elm.exists(_.isInstanceOf[dom.Element])) - assertEquals(vnode.elm.map(_.childNodes.length), Some(2)) + assert(vnode.elm.isInstanceOf[dom.Element]) + assertEquals(vnode.elm.childNodes.length, 2) assertEquals( - vnode.elm.flatMap(e => Option(e.parentNode)).map(_.childNodes.length), - Some(3) + vnode.elm.parentNode.childNodes.length, + 3 ) result.addOne(vnode) } @@ -1268,7 +1268,7 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("calls `prepatch` listener") { vnode0 => val result = List.newBuilder[VNode] lazy val cb: PrePatchHook = (oldVnode, vnode) => { - assertEquals(vnode1.children.map(_(1)), Some(oldVnode)) + assertEquals(vnode1.children.map(_(1)), Some(oldVnode.toVNode)) assertEquals(vnode2.children.map(_(1)), Some(vnode)) result.addOne(vnode) vnode @@ -1367,9 +1367,13 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("calls `update` listener") { vnode0 => val result1 = ListBuffer.empty[VNode] val result2 = ListBuffer.empty[VNode] - def cb(result: ListBuffer[VNode], oldVnode: VNode, vnode: VNode) = { + def cb( + result: ListBuffer[VNode], + oldVnode: PatchedVNode, + vnode: VNode + ) = { if (result.length > 0) { - assertEquals(result(result.length - 1), oldVnode) + assertEquals(result(result.length - 1), oldVnode.toVNode) } result.addOne(vnode) vnode @@ -1419,11 +1423,11 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("calls `remove` listener") { vnode0 => val result = List.newBuilder[VNode] val cb: RemoveHook = (vnode, rm) => { - val parent = vnode.elm.get.parentNode - assert(vnode.elm.exists(_.isInstanceOf[dom.Element])) - assertEquals(vnode.elm.map(_.childNodes.length), Some(2)) + val parent = vnode.elm.parentNode + assert(vnode.elm.isInstanceOf[dom.Element]) + assertEquals(vnode.elm.childNodes.length, 2) assertEquals(parent.childNodes.length, 2) - result.addOne(vnode) + result.addOne(vnode.toVNode) rm() assertEquals(parent.childNodes.length, 1) } @@ -1522,9 +1526,9 @@ class SnabbdomSuite extends BaseSuite { ) val vnode2 = h("div", Array.empty[VNode]) val vnode1p = patch(vnode0, vnode1) - var elm = vnode1p.elm.get + var elm = vnode1p.elm assertEquals(elm.childNodes.length, 1) - elm = patch(vnode1p, vnode2).elm.get + elm = patch(vnode1p, vnode2).elm assertEquals(elm.childNodes.length, 1) rm1() assertEquals(elm.childNodes.length, 1) @@ -1540,7 +1544,7 @@ class SnabbdomSuite extends BaseSuite { val vnode0 = dom.document.createElement("div") parent.appendChild(vnode0) val cb: RemoveHook = (vnode, rm) => { - result.addOne(vnode) + result.addOne(vnode.toVNode) rm() } val vnode1 = h( @@ -1578,7 +1582,7 @@ class SnabbdomSuite extends BaseSuite { vnode0 => val result = List.newBuilder[VNode] val cb: DestroyHook = (vnode) => { - result.addOne(vnode) + result.addOne(vnode.toVNode) } val vnode1 = h( "div", @@ -1702,7 +1706,7 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("does not update strictly equal vnodes") { vnode0 => val result = List.newBuilder[VNode] val cb: UpdateHook = (vnode, newVNode) => { - result += vnode + result += vnode.toVNode newVNode } val vnode1 = h( @@ -1724,7 +1728,7 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("does not update strictly equal children") { vnode0 => val result = List.newBuilder[VNode] val cb: UpdateHook = (vnode, newVNode) => { - result += vnode + result += vnode.toVNode newVNode } val vnode1 = h( diff --git a/snabbdom/src/test/scala/snabbdom/StyleSuite.scala b/snabbdom/src/test/scala/snabbdom/StyleSuite.scala index eefab18..5463be0 100644 --- a/snabbdom/src/test/scala/snabbdom/StyleSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/StyleSuite.scala @@ -70,7 +70,7 @@ class StyleSuite extends BaseSuite { val elm = patch( vnode0, h("div", VNodeData(style = Map("fontSize" -> "12px"))) - ).elm.get.asInstanceOf[dom.HTMLElement] + ).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm.style.fontSize, "12px") } @@ -80,10 +80,10 @@ class StyleSuite extends BaseSuite { val vnode1 = h("i", cachedStyles) val vnode2 = h("i", cachedStyles) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] assertEquals(elm1.style.fontSize, "14px") assertEquals(elm1.style.display, "inline") - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm2.style.fontSize, "14px") assertEquals(elm2.style.display, "inline") } @@ -105,14 +105,14 @@ class StyleSuite extends BaseSuite { ) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] assertEquals(elm1.style.fontSize, "14px") assertEquals(elm1.style.display, "inline") val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = vnode2p.elm.asInstanceOf[dom.HTMLElement] assertEquals(elm2.style.fontSize, "12px") assertEquals(elm2.style.display, "block") - val elm3 = patch(vnode2p, vnode3).elm.get.asInstanceOf[dom.HTMLElement] + val elm3 = patch(vnode2p, vnode3).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm3.style.fontSize, "10px") assertEquals(elm3.style.display, "block") @@ -126,12 +126,12 @@ class StyleSuite extends BaseSuite { val vnode3 = h("i", VNodeData(style = Map("fontSize" -> "10px"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] assertEquals(elm1.style.fontSize, "14px") val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = vnode2p.elm.asInstanceOf[dom.HTMLElement] assertEquals(elm2.style.fontSize, "") - val elm3 = patch(vnode2p, vnode3).elm.get.asInstanceOf[dom.HTMLElement] + val elm3 = patch(vnode2p, vnode3).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm3.style.fontSize, "10px") } @@ -140,12 +140,12 @@ class StyleSuite extends BaseSuite { val vnode2 = h("i", VNodeData()) val vnode3 = h("i", VNodeData(style = Map("fontSize" -> "10px"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] assertEquals(elm1.style.fontSize, "14px") val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = vnode2p.elm.asInstanceOf[dom.HTMLElement] assertEquals(elm2.style.fontSize, "") - val elm3 = patch(vnode2p, vnode3).elm.get.asInstanceOf[dom.HTMLElement] + val elm3 = patch(vnode2p, vnode3).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm3.style.fontSize, "10px") } @@ -157,12 +157,12 @@ class StyleSuite extends BaseSuite { val vnode3 = h("div", VNodeData(style = Map("--myVar" -> "3"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] assertEquals(elm1.style.getPropertyValue("--myVar"), "1") val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = vnode2p.elm.asInstanceOf[dom.HTMLElement] assertEquals(elm2.style.getPropertyValue("--myVar"), "2") - val elm3 = patch(vnode2p, vnode3).elm.get.asInstanceOf[dom.HTMLElement] + val elm3 = patch(vnode2p, vnode3).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm3.style.getPropertyValue("--myVar"), "3") } @@ -175,12 +175,12 @@ class StyleSuite extends BaseSuite { val vnode3 = h("i", VNodeData(style = Map("--myVar" -> "3"))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] assertEquals(elm1.style.getPropertyValue("--myVar"), "1") val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = vnode2p.elm.asInstanceOf[dom.HTMLElement] assertEquals(elm2.style.getPropertyValue("--myVar"), "") - val elm3 = patch(vnode2p, vnode3).elm.get.asInstanceOf[dom.HTMLElement] + val elm3 = patch(vnode2p, vnode3).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm3.style.getPropertyValue("--myVar"), "3") } @@ -202,7 +202,7 @@ class StyleSuite extends BaseSuite { ) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] assertEquals( elm1.firstChild .asInstanceOf[dom.HTMLElement] @@ -211,7 +211,7 @@ class StyleSuite extends BaseSuite { "1" ) val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = vnode2p.elm.asInstanceOf[dom.HTMLElement] assertEquals( elm2.firstChild .asInstanceOf[dom.HTMLElement] @@ -219,7 +219,7 @@ class StyleSuite extends BaseSuite { .getPropertyValue("--myVar"), "" ) - val elm3 = patch(vnode2p, vnode3).elm.get.asInstanceOf[dom.HTMLElement] + val elm3 = patch(vnode2p, vnode3).elm.asInstanceOf[dom.HTMLElement] assertEquals( elm3.firstChild .asInstanceOf[dom.HTMLElement] diff --git a/snabbdom/src/test/scala/snabbdom/SvgSuite.scala b/snabbdom/src/test/scala/snabbdom/SvgSuite.scala index 97311cf..bd868c5 100644 --- a/snabbdom/src/test/scala/snabbdom/SvgSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SvgSuite.scala @@ -55,7 +55,7 @@ class SvgSuite extends BaseSuite { vnode0.test("removes child svg elements") { vnode0 => val a = h("svg", VNodeData(), Array(h("g"), h("g"))) val b = h("svg", VNodeData(), Array(h("g"))) - val result = patch(patch(vnode0, a), b).elm.get.asInstanceOf[dom.SVGElement] + val result = patch(patch(vnode0, a), b).elm.asInstanceOf[dom.SVGElement] assertEquals(result.childNodes.length, 1) } @@ -73,7 +73,7 @@ class SvgSuite extends BaseSuite { ) ) ) - val result = patch(vnode0, a).elm.get.asInstanceOf[dom.SVGElement] + val result = patch(vnode0, a).elm.asInstanceOf[dom.SVGElement] assertEquals(result.childNodes.length, 1) val child = result.childNodes(0).asInstanceOf[dom.SVGUseElement] assertEquals(child.getAttribute("xlink:href"), testUrl) @@ -88,7 +88,7 @@ class SvgSuite extends BaseSuite { VNodeData(attrs = Map("xml:lang" -> testAttrValue)), Array[VNode]() ) - val result = patch(vnode0, a).elm.get.asInstanceOf[dom.SVGElement] + val result = patch(vnode0, a).elm.asInstanceOf[dom.SVGElement] assertEquals(result.getAttributeNS(xmlNS, "lang"), testAttrValue) assertEquals(result.getAttribute("xml:lang"), testAttrValue) } diff --git a/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala b/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala index a185744..ba0523e 100644 --- a/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala @@ -142,7 +142,7 @@ class ThunkSuite extends BaseSuite { val vnode2 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) val vnode3 = h("div", Array(thunk("span", "num", numberInSpan, Seq(2)))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] assertEquals(called, 1) assertEquals( elm1.firstChild.asInstanceOf[dom.HTMLElement].tagName.toLowerCase, @@ -153,7 +153,7 @@ class ThunkSuite extends BaseSuite { "Number is 1" ) val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = vnode2p.elm.asInstanceOf[dom.HTMLElement] assertEquals( elm2.firstChild.asInstanceOf[dom.HTMLElement].tagName.toLowerCase, "span" @@ -164,7 +164,7 @@ class ThunkSuite extends BaseSuite { ) assertEquals(called, 1) val vnode3p = patch(vnode2p, vnode3) - val elm3 = vnode3p.elm.get.asInstanceOf[dom.HTMLElement] + val elm3 = vnode3p.elm.asInstanceOf[dom.HTMLElement] assertEquals( elm3.firstChild.asInstanceOf[dom.HTMLElement].tagName.toLowerCase, "span" @@ -182,7 +182,7 @@ class ThunkSuite extends BaseSuite { h("span.number", s"Hello $s") } val vnode1 = thunk("span.number", vnodeFn, Seq("World!")) - val elm = patch(vnode0, vnode1).elm.get + val elm = patch(vnode0, vnode1).elm assertEquals(elm.innerText, "Hello World!") } @@ -198,19 +198,19 @@ class ThunkSuite extends BaseSuite { val vnode3 = thunk("span", "num", numberInSpan, Seq(2)) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] assertEquals(called, 1) assertEquals(elm1.tagName.toLowerCase, "span") assertEquals(elm1.innerHTML, "Number is 1") val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = vnode2p.elm.asInstanceOf[dom.HTMLElement] assertEquals(elm2.tagName.toLowerCase, "span") assertEquals(elm2.innerHTML, "Number is 1") assertEquals(called, 1) val vnode3p = patch(vnode2p, vnode3) - val elm3 = vnode3p.elm.get.asInstanceOf[dom.HTMLElement] + val elm3 = vnode3p.elm.asInstanceOf[dom.HTMLElement] assertEquals(elm3.tagName.toLowerCase, "span") assertEquals(elm3.innerHTML, "Number is 2") assertEquals(called, 2) @@ -232,7 +232,7 @@ class ThunkSuite extends BaseSuite { val vnode2 = h("div", Array(thunk("div", "oddEven", oddEven, Seq(4)))) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] assertEquals( elm1.firstChild.asInstanceOf[dom.HTMLElement].tagName.toLowerCase, "span" @@ -243,7 +243,7 @@ class ThunkSuite extends BaseSuite { ) val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = vnode2p.elm.asInstanceOf[dom.HTMLElement] println(elm2.innerHTML) assertEquals( elm2.firstChild.asInstanceOf[dom.HTMLElement].tagName.toLowerCase, @@ -272,11 +272,11 @@ class ThunkSuite extends BaseSuite { val vnode2 = thunk("div", "oddEven", oddEven, Seq(4)) val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm.get.asInstanceOf[dom.HTMLElement] + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] assertEquals(elm1.tagName.toLowerCase, "span") assertEquals(elm1.innerHTML, "Number is 1") - val elm2 = patch(vnode1p, vnode2).elm.get.asInstanceOf[dom.HTMLElement] + val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm2.tagName.toLowerCase, "div") assertEquals(elm2.innerHTML, "Even: 4") @@ -284,7 +284,7 @@ class ThunkSuite extends BaseSuite { vnode0.test("invokes destroy hook on thunks") { vnode0 => var called = 0 - val destroyHook: DestroyHook = (_: VNode) => { called += 1 } + val destroyHook: DestroyHook = (_: PatchedVNode) => { called += 1 } val numberInSpan = (arr: Seq[Any]) => { val n = arr(0).asInstanceOf[Int] h( @@ -313,7 +313,9 @@ class ThunkSuite extends BaseSuite { vnode0.test("invokes remove hook on thunks") { vnode0 => var called = 0 - val destroyHook: RemoveHook = (_: VNode, _: () => Unit) => { called += 1 } + val destroyHook: RemoveHook = (_: PatchedVNode, _: () => Unit) => { + called += 1 + } val numberInSpan = (arr: Seq[Any]) => { val n = arr(0).asInstanceOf[Int] h( From 197df346d20942c82cc9ed41c6ed1f1c0262c108 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Fri, 27 May 2022 08:05:17 +0200 Subject: [PATCH 06/35] fixes bug --- snabbdom/src/main/scala/snabbdom/init.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index 80c7e22..25addb1 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -153,7 +153,7 @@ object init { api.createElement(tag) // TODO what about data argument? } val vnode0 = PatchedVNode( - vnode.key, + vnode.sel, vnode.data, children = vnode.children.map( _.map(ch => createElm(ch, insertedVNodeQueue)) From 828ce2b3870c706a4de89c8cc3e598e73146ad8c Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Fri, 27 May 2022 10:32:05 +0200 Subject: [PATCH 07/35] use List instead of Array for child nodes --- .../scala/snabbdom/examples/Example.scala | 4 +- .../main/scala/snabbdom/PatchedVNode.scala | 4 +- snabbdom/src/main/scala/snabbdom/VNode.scala | 10 +- .../src/main/scala/snabbdom/fragment.scala | 4 +- snabbdom/src/main/scala/snabbdom/h.scala | 26 +- snabbdom/src/main/scala/snabbdom/init.scala | 269 +++++------------ .../src/main/scala/snabbdom/toVNode.scala | 8 +- .../test/scala/snabbdom/AttributesSuite.scala | 2 +- .../scala/snabbdom/EventListenersSuite.scala | 26 +- .../test/scala/snabbdom/SnabbdomSuite.scala | 271 +++++++++--------- .../src/test/scala/snabbdom/StyleSuite.scala | 6 +- .../src/test/scala/snabbdom/SvgSuite.scala | 10 +- .../src/test/scala/snabbdom/ThunkSuite.scala | 34 +-- 13 files changed, 277 insertions(+), 397 deletions(-) diff --git a/examples/src/main/scala/snabbdom/examples/Example.scala b/examples/src/main/scala/snabbdom/examples/Example.scala index 06b7f53..e0eb29a 100644 --- a/examples/src/main/scala/snabbdom/examples/Example.scala +++ b/examples/src/main/scala/snabbdom/examples/Example.scala @@ -41,7 +41,7 @@ object Example { val vnode = h( "div", VNodeData(on = Map("click" -> ((_: dom.Event) => println("foo")))), - Array[VNode]( + List[VNode]( h( "span", VNodeData(style = Map("fontWeight" -> "bold")), @@ -61,7 +61,7 @@ object Example { val newVnode = h( "div#container.two.classes", VNodeData(on = Map("click" -> ((_: dom.Event) => println("bar")))), - Array[VNode]( + List[VNode]( h( "span", VNodeData(style = diff --git a/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala b/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala index cc12119..16b3dd7 100644 --- a/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala +++ b/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala @@ -22,7 +22,7 @@ import org.scalajs.dom case class PatchedVNode private[snabbdom] ( sel: Option[String], data: VNodeData, - children: Option[Array[PatchedVNode]], + children: List[PatchedVNode], text: Option[String], key: Option[KeyValue], elm: dom.Node, // the corresponding node in the DOM - can't be `dom.Element` unfortunately b/c of fragments @@ -35,7 +35,7 @@ case class PatchedVNode private[snabbdom] ( s"sel=$sel, data=$data, text=$text, key=$key, children=$children, elm=$elm, listener=$listener" def toVNode: VNode = - VNode(sel, data, children.map(_.map(_.toVNode)), text, key) + VNode(sel, data, children.map(_.toVNode), text, key) private[snabbdom] def isTextNode: Boolean = sel.isEmpty && children.isEmpty && text.isDefined diff --git a/snabbdom/src/main/scala/snabbdom/VNode.scala b/snabbdom/src/main/scala/snabbdom/VNode.scala index 4bc1c10..3c8730b 100644 --- a/snabbdom/src/main/scala/snabbdom/VNode.scala +++ b/snabbdom/src/main/scala/snabbdom/VNode.scala @@ -41,7 +41,7 @@ package snabbdom case class VNode private ( sel: Option[String], data: VNodeData, - children: Option[Array[VNode]], + children: List[VNode], text: Option[String], key: Option[KeyValue] ) { @@ -56,17 +56,17 @@ case class VNode private ( object VNode { - val empty = VNode(None, VNodeData.empty, None, Some(""), None) + val empty = VNode(None, VNodeData.empty, Nil, Some(""), None) def create( sel: Option[String], data: VNodeData, - children: Option[Array[VNode]], + children: List[VNode], text: Option[String] - ) = new VNode(sel, data, children.filter(_.nonEmpty), text, data.key) + ) = new VNode(sel, data, children, text, data.key) def text(text: String) = - new VNode(None, VNodeData.empty, None, Some(text), None) + new VNode(None, VNodeData.empty, Nil, Some(text), None) implicit def fromString(s: String): VNode = text(s) diff --git a/snabbdom/src/main/scala/snabbdom/fragment.scala b/snabbdom/src/main/scala/snabbdom/fragment.scala index b63fd1b..2be7d31 100644 --- a/snabbdom/src/main/scala/snabbdom/fragment.scala +++ b/snabbdom/src/main/scala/snabbdom/fragment.scala @@ -40,8 +40,8 @@ package snabbdom object fragment { - def apply(children: Array[VNode]): VNode = { - VNode.create(None, VNodeData.empty, Some(children), None) + def apply(children: List[VNode]): VNode = { + VNode.create(None, VNodeData.empty, children, None) } } diff --git a/snabbdom/src/main/scala/snabbdom/h.scala b/snabbdom/src/main/scala/snabbdom/h.scala index 24f0fd6..9414927 100644 --- a/snabbdom/src/main/scala/snabbdom/h.scala +++ b/snabbdom/src/main/scala/snabbdom/h.scala @@ -42,36 +42,36 @@ object h { type VNodes = Array[VNode] - def apply(sel: String): VNode = h(sel, None, None, None) + def apply(sel: String): VNode = h(sel, None, Nil, None) def apply(sel: String, data: VNodeData): VNode = { - apply(sel, Some(data), None, None) + apply(sel, Some(data), Nil, None) } - def apply(sel: String, children: Array[VNode]): VNode = { - apply(sel, None, Some(children), None) + def apply(sel: String, children: List[VNode]): VNode = { + apply(sel, None, children, None) } def apply(sel: String, text: String): VNode = { - apply(sel, None, None, Some(text)) + apply(sel, None, Nil, Some(text)) } def apply(sel: String, data: VNodeData, text: String): VNode = { - apply(sel, Some(data), None, Some(text)) + apply(sel, Some(data), Nil, Some(text)) } - def apply(sel: String, data: VNodeData, children: Array[VNode]): VNode = { - apply(sel, Some(data), Some(children), None) + def apply(sel: String, data: VNodeData, children: List[VNode]): VNode = { + apply(sel, Some(data), children, None) } def apply(sel: String, data: VNodeData, child: VNode): VNode = { - apply(sel, Some(data), Some(Array(child)), None) + apply(sel, Some(data), List(child), None) } private def apply( sel: String, data: Option[VNodeData], - children: Option[Array[VNode]], + children: List[VNode], text: Option[String] ): VNode = { val vnode = @@ -97,9 +97,7 @@ object h { data = vnode.data.copy(ns = Some(ns)), children = if (vnode.sel.forall(_ != "foreignObject")) - vnode.children.map(children => - children.map((child: PatchedVNode) => addNS(child)) - ) + vnode.children.map(addNS) else vnode.children ) } @@ -110,7 +108,7 @@ object h { data = vnode.data.copy(ns = Some(ns)), children = if (vnode.sel.forall(_ != "foreignObject")) - vnode.children.map(_.map(addNS)) + vnode.children.map(addNS) else vnode.children ) } diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index 25addb1..6d734c6 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -87,7 +87,7 @@ object init { PatchedVNode( Some(api.tagName(elm).toLowerCase + id + c), VNodeData.empty, - None, + Nil, None, None, elm, @@ -97,7 +97,7 @@ object init { } def emptyDocumentFragmentAt(frag: dom.DocumentFragment): PatchedVNode = { - PatchedVNode(None, VNodeData.empty, None, None, None, frag, None) + PatchedVNode(None, VNodeData.empty, Nil, None, None, frag, None) } def createRmCb(childElm: dom.Node, listeners: Int): () => Unit = { @@ -127,7 +127,7 @@ object init { PatchedVNode( vnode.sel, vnode.data, - None, // comment nodes can't have children + Nil, // comment nodes can't have children Some(text), vnode.key, elm, @@ -155,9 +155,8 @@ object init { val vnode0 = PatchedVNode( vnode.sel, vnode.data, - children = vnode.children.map( - _.map(ch => createElm(ch, insertedVNodeQueue)) - ), + children = + vnode.children.map(ch => createElm(ch, insertedVNodeQueue)), text = vnode.text, key = vnode.key, elm = elm, @@ -172,13 +171,13 @@ object init { } cbs.create.foreach(_.apply(vnode0)) vnode0.children match { - case None => + case List() => vnode0.text match { case None => () case Some(text) => api.appendChild(elm, api.createTextNode(text)) } - case Some(children) => + case children => children.foreach { child => api.appendChild(elm, child.elm) } @@ -191,38 +190,31 @@ object init { case None => vnode.children match { - case None => + case Nil => PatchedVNode( vnode.sel, vnode.data, - None, + Nil, vnode.text, vnode.key, api.createTextNode(vnode.text.getOrElse("")), None ) - case Some(children) => + case children => val elm = api.createDocumentFragment val vnode0 = PatchedVNode( vnode.sel, vnode.data, children = - Some(children.map(ch => createElm(ch, insertedVNodeQueue))), + children.map(ch => createElm(ch, insertedVNodeQueue)), text = vnode.text, key = vnode.key, elm = elm, None ) cbs.create.foreach(hook => hook(vnode0)) - vnode0.children.foreach { children => - children.foreach(ch => - api.appendChild( - elm, - ch.elm - ) - ) - } + vnode0.children.foreach(ch => api.appendChild(elm, ch.elm)) vnode0 } @@ -230,47 +222,34 @@ object init { } - def addVnodes( + def addAllVnodes( parentElm: dom.Node, before: Option[dom.Node], - vnodes: Array[VNode], - startIdx: Int, - endIdx: Int, + vnodes: List[VNode], insertedVNodeQueue: VNodeQueue - ): Array[PatchedVNode] = { - vnodes.slice(startIdx, endIdx + 1).map { vnode => - val pvnode = createElm(vnode, insertedVNodeQueue) - api.insertBefore( - parentElm, - pvnode.elm, - before - ) - pvnode - } + ): List[PatchedVNode] = vnodes.map { vnode => + val pvnode = createElm(vnode, insertedVNodeQueue) + api.insertBefore( + parentElm, + pvnode.elm, + before + ) + pvnode } def invokeDestroyHook(vnode: PatchedVNode): Unit = { if (!vnode.isTextNode) { // detroy hooks should not be called on text nodes vnode.data.hook.flatMap(_.destroy).foreach(hook => hook(vnode)) cbs.destroy.foreach(hook => hook(vnode)) - vnode.children.foreach { - _.foreach { child => - invokeDestroyHook(child) - } - } + vnode.children.foreach { child => invokeDestroyHook(child) } } } - def removeVnodes( + def removeAllVnodes( parentElm: dom.Node, - vnodes: Array[PatchedVNode], - startIdx: Int, - endIdx: Int + vnodes: List[PatchedVNode] ): Unit = { - - var i = startIdx - while (i <= endIdx) { - val ch = vnodes(i) + vnodes.foreach { ch => ch.sel match { case Some(_) => invokeDestroyHook(ch) @@ -283,134 +262,38 @@ object init { case None => // text node api.removeChild(parentElm, ch.elm) } - i += 1 + } + } def updateChildren( parentElm: dom.Node, - oldCh: Array[PatchedVNode], - newCh: Array[VNode], + oldCh: List[PatchedVNode], + newCh: List[VNode], insertedVnodeQueue: VNodeQueue - ): Array[PatchedVNode] = { - - assert(oldCh.nonEmpty) - assert(newCh.nonEmpty) - - val result = Array.ofDim[PatchedVNode](newCh.length) - - var oldStartIdx = 0 - var newStartIdx = 0 - var oldEndIdx = oldCh.length - 1 - var newEndIdx = newCh.length - 1 - - var oldKeyToIdx: Map[String, Int] = null - - while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { - if (oldCh(oldStartIdx) == null) { - // Vnode might have been moved left - oldStartIdx += 1 - } else if (oldCh(oldEndIdx) == null) { - oldEndIdx -= 1 - } else if (sameVnode(oldCh(oldStartIdx), newCh(newStartIdx))) { - result(newStartIdx) = patchVnode( - oldCh(oldStartIdx), - newCh(newStartIdx), - insertedVnodeQueue - ) - oldStartIdx += 1 - newStartIdx += 1 - } else if (sameVnode(oldCh(oldEndIdx), newCh(newEndIdx))) { - result(newEndIdx) = - patchVnode(oldCh(oldEndIdx), newCh(newEndIdx), insertedVnodeQueue) - oldEndIdx -= 1 - newEndIdx -= 1 - } else if (sameVnode(oldCh(oldStartIdx), newCh(newEndIdx))) { - // Vnode moved right - result(newEndIdx) = - patchVnode(oldCh(oldStartIdx), newCh(newEndIdx), insertedVnodeQueue) - api.insertBefore( - parentElm, - oldCh(oldStartIdx).elm, - api.nextSibling(oldCh(oldEndIdx).elm) - ) - oldStartIdx += 1 - newEndIdx -= 1 - } else if (sameVnode(oldCh(oldEndIdx), newCh(newStartIdx))) { - // Vnode moved left - result(newStartIdx) = - patchVnode(oldCh(oldEndIdx), newCh(newStartIdx), insertedVnodeQueue) - api.insertBefore( - parentElm, - oldCh(oldEndIdx).elm, - Some(oldCh(oldStartIdx).elm) - ) - oldEndIdx -= 1 - newStartIdx += 1 - } else { - if (oldKeyToIdx == null) { - oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) - } - val idxInOld = newCh(newStartIdx).key.flatMap { key => - oldKeyToIdx.get(key) - } - idxInOld match { - case None => - // New element - api.insertBefore( - parentElm, - createElm(newCh(newStartIdx), insertedVnodeQueue).elm, - Some(oldCh(oldStartIdx).elm) - ) - case Some(idxInOld) => - val elmToMove = oldCh(idxInOld) - if (elmToMove.sel != newCh(newStartIdx).sel) { - result(newStartIdx) = - createElm(newCh(newStartIdx), insertedVnodeQueue) - api.insertBefore( - parentElm, - result(newStartIdx).elm, - Some(oldCh(oldStartIdx).elm) - ) - } else { - result(newStartIdx) = - patchVnode(elmToMove, newCh(newStartIdx), insertedVnodeQueue) - oldCh(idxInOld) = null - api.insertBefore( - parentElm, - elmToMove.elm, - Some(oldCh(oldStartIdx).elm) - ) - } - } - newStartIdx += 1 - } - } - - if (newStartIdx <= newEndIdx) { - val before = - if (result.length > newEndIdx + 1) Some(result(newEndIdx + 1).elm) - else None - val patchedChildren = addVnodes( - parentElm, - before, - newCh, - newStartIdx, - newEndIdx, - insertedVnodeQueue - ) - var i = newStartIdx - while (i <= newEndIdx) { - result(i) = patchedChildren(i - newStartIdx) - i += 1 + ): List[PatchedVNode] = { + + val (toDelete, patchedChildren) = + newCh.foldLeft((oldCh, List.empty[PatchedVNode])) { + case ((oh :: ot, acc), newCh) => + if (sameVnode(oh, newCh)) { + val pn = patchVnode(oh, newCh, insertedVnodeQueue) + (ot, pn :: acc) + } else { + val pn = createElm(newCh, insertedVnodeQueue) + api.insertBefore(parentElm, pn.elm, Some(oh.elm)) + (oh :: ot, pn :: acc) + } + case ((Nil, acc), newCh) => + val pn = createElm(newCh, insertedVnodeQueue) + api.insertBefore(parentElm, pn.elm, None) + (Nil, pn :: acc) } - } - if (oldStartIdx <= oldEndIdx) { - removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) - } + removeAllVnodes(parentElm, toDelete) - result + patchedChildren } @@ -438,12 +321,12 @@ object init { val vnode1 = vnode.text match { case None => (oldCh, vnode.children) match { - case (Some(oldCh), Some(ch)) => + case (oldCh @ _ :: _, ch @ _ :: _) => if (oldCh != ch) { PatchedVNode( vnode.sel, vnode.data, - Some(updateChildren(elm, oldCh, ch, insertedVNodeQueue)), + updateChildren(elm, oldCh, ch, insertedVNodeQueue), vnode.text, vnode.key, elm, @@ -454,50 +337,53 @@ object init { PatchedVNode( vnode.sel, vnode.data, - Some(oldCh), + oldCh, vnode.text, vnode.key, elm, None ) } - case (None, Some(ch)) => + case (Nil, ch @ _ :: _) => oldVnode.text.foreach(_ => api.setTextContent(elm, Some(""))) - val patchedChildren = addVnodes( - elm, - None, - ch, - 0, - ch.length - 1, - insertedVNodeQueue - ) + + val patchedChildren = ch.map { vnode => + val pvnode = createElm(vnode, insertedVNodeQueue) + api.insertBefore( + elm, + pvnode.elm, + None + ) + pvnode + } + PatchedVNode( vnode.sel, vnode.data, - Some(patchedChildren), + patchedChildren, vnode.text, vnode.key, elm, None ) - case (Some(oldCh), None) => - removeVnodes(elm, oldCh, 0, oldCh.length - 1) + case (_ :: _, Nil) => + removeAllVnodes(elm, oldCh) PatchedVNode( vnode.sel, vnode.data, - None, + Nil, vnode.text, vnode.key, elm, None ) - case (None, None) => + case (Nil, Nil) => oldVnode.text.foreach(_ => api.setTextContent(elm, Some(""))) PatchedVNode( vnode.sel, vnode.data, - None, + Nil, vnode.text, vnode.key, elm, @@ -505,14 +391,12 @@ object init { ) } case Some(text) if oldVnode.text.forall(_ != text) => - oldCh.foreach(oldChildren => - removeVnodes(elm, oldChildren, 0, oldChildren.length - 1) - ) + removeAllVnodes(elm, oldCh) api.setTextContent(elm, Some(text)) PatchedVNode( vnode.sel, vnode.data, - None, + Nil, vnode.text, vnode.key, elm, @@ -522,7 +406,7 @@ object init { PatchedVNode( vnode.sel, vnode.data, - None, + Nil, vnode.text, vnode.key, elm, @@ -557,7 +441,7 @@ object init { parent match { case Some(parent) => api.insertBefore(parent, vnode1.elm, api.nextSibling(elm)) - removeVnodes(parent, Array(oldVnode), 0, 0) + removeAllVnodes(parent, List(oldVnode)) case None => () } vnode1 @@ -598,9 +482,7 @@ object init { } private def createKeyToOldIdx( - children: Array[PatchedVNode], - beginIdx: Int, - endIdx: Int + children: List[PatchedVNode] ): Map[String, Int] = { children.zipWithIndex .map { case (ch, i) => @@ -608,7 +490,6 @@ object init { } .collect { case Some(a) => a } .toMap - .filter(kv => kv._2 >= beginIdx && kv._2 <= endIdx) } } diff --git a/snabbdom/src/main/scala/snabbdom/toVNode.scala b/snabbdom/src/main/scala/snabbdom/toVNode.scala index b68aaf2..8cba8fb 100644 --- a/snabbdom/src/main/scala/snabbdom/toVNode.scala +++ b/snabbdom/src/main/scala/snabbdom/toVNode.scala @@ -82,7 +82,7 @@ object toVNode { val vnode = PatchedVNode( Some(sel), data, - Some(children.toArray), + children.toList, None, None, node, @@ -101,20 +101,20 @@ object toVNode { } else if (api.isText(node)) { val text = api.getTextContent(node).getOrElse("") - PatchedVNode(None, VNodeData.empty, None, Some(text), None, node, None) + PatchedVNode(None, VNodeData.empty, Nil, Some(text), None, node, None) } else if (api.isComment(node)) { val text = api.getTextContent(node).getOrElse("") PatchedVNode( Some("!"), VNodeData.empty, - None, + Nil, Some(text), None, node, None ) } else { - PatchedVNode(Some(""), VNodeData.empty, None, None, None, node, None) + PatchedVNode(Some(""), VNodeData.empty, Nil, None, None, node, None) } } diff --git a/snabbdom/src/test/scala/snabbdom/AttributesSuite.scala b/snabbdom/src/test/scala/snabbdom/AttributesSuite.scala index be911fc..9e12277 100644 --- a/snabbdom/src/test/scala/snabbdom/AttributesSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/AttributesSuite.scala @@ -129,7 +129,7 @@ class AttributesSuite extends BaseSuite { val vnode1 = h( "div#myId.myClass", VNodeData(), - Array[VNode]("Hello") + List[VNode]("Hello") ) val elm1 = patch(vnode0, vnode1).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm1.tagName, "DIV") diff --git a/snabbdom/src/test/scala/snabbdom/EventListenersSuite.scala b/snabbdom/src/test/scala/snabbdom/EventListenersSuite.scala index 656dc42..8af9e42 100644 --- a/snabbdom/src/test/scala/snabbdom/EventListenersSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/EventListenersSuite.scala @@ -41,7 +41,7 @@ package snabbdom import snabbdom.modules._ import org.scalajs.dom -import scala.collection.mutable.ArrayBuffer +import scala.collection.mutable.ListBuffer class EventListenersSuite extends BaseSuite { @@ -64,7 +64,7 @@ class EventListenersSuite extends BaseSuite { val vnode = h( "div", VNodeData(on = Map("click" -> clicked)), - Array(h("a", "Click my parent")) + List(h("a", "Click my parent")) ) val elm = patch(vnode0, vnode).elm elm.asInstanceOf[dom.HTMLElement].click() @@ -79,12 +79,12 @@ class EventListenersSuite extends BaseSuite { VNodeData(on = Map("click" -> EventHandler((_: dom.Event) => result += 1)) ), - Array(h("a", "Click my parent")) + List(h("a", "Click my parent")) ) val vnode2 = h( "div", VNodeData(on = Map("click" -> EventHandler(_ => result += 2))), - Array(h("a", "Click my parent")) + List(h("a", "Click my parent")) ) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] @@ -96,7 +96,7 @@ class EventListenersSuite extends BaseSuite { } vnode0.test("detach attached click event handler to element") { vnode0 => - val result = ArrayBuffer[dom.Event]() + val result = ListBuffer[dom.Event]() val clicked = (ev: dom.Event) => { result += ev () @@ -104,13 +104,13 @@ class EventListenersSuite extends BaseSuite { val vnode1 = h( "div", VNodeData(on = Map("click" -> clicked)), - Array(h("a", "Click my parent")) + List(h("a", "Click my parent")) ) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] elm1.click() assertEquals(result.length, 1) - val vnode2 = h("div", VNodeData(), Array(h("a", "Click my parent"))) + val vnode2 = h("div", VNodeData(), List(h("a", "Click my parent"))) val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.HTMLElement] elm2.click() assertEquals(result.length, 1) @@ -130,7 +130,7 @@ class EventListenersSuite extends BaseSuite { VNodeData(on = Map("click" -> EventHandler.usingVNode(clicked, clicked, clicked)) ), - Array(h("a", "Click my parent")) + List(h("a", "Click my parent")) ) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] @@ -141,7 +141,7 @@ class EventListenersSuite extends BaseSuite { VNodeData(on = Map("click" -> EventHandler.usingVNode(clicked, clicked)) ), - Array(h("a", "Click my parent")) + List(h("a", "Click my parent")) ) val elm2 = patch(vnode1p, vnode2).elm.asInstanceOf[dom.HTMLElement] elm2.click() @@ -149,7 +149,7 @@ class EventListenersSuite extends BaseSuite { } vnode0.test("access to virtual node in event handler") { vnode0 => - val result = ArrayBuffer[VNode]() + val result = ListBuffer[VNode]() val clicked = (_: dom.Event, vnode: VNode) => { result += vnode () @@ -157,7 +157,7 @@ class EventListenersSuite extends BaseSuite { val vnode1 = h( "div", VNodeData(on = Map("click" -> clicked)), - Array(h("a", "Click my parent")) + List(h("a", "Click my parent")) ) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] @@ -167,7 +167,7 @@ class EventListenersSuite extends BaseSuite { } vnode0.test("shared handlers in parent and child nodes") { vnode0 => - val result = ArrayBuffer[dom.Event]() + val result = ListBuffer[dom.Event]() val clicked = (ev: dom.Event) => { result += ev () @@ -175,7 +175,7 @@ class EventListenersSuite extends BaseSuite { val vnode1 = h( "div", VNodeData(on = Map("click" -> clicked)), - Array( + List( h( "a", VNodeData(on = Map("click" -> clicked)), diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index 8657912..f060ac3 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -73,31 +73,31 @@ class SnabbdomSuite extends BaseSuite { } test("can create vnode with children") { - val vnode = h("div", Array(h("span#hello"), h("b.world"))) + val vnode = h("div", List(h("span#hello"), h("b.world"))) assertEquals(vnode.sel, Some("div")) val children = vnode.children - assertEquals(children.flatMap(_(0).sel), Some("span#hello")) - assertEquals(children.flatMap(_(1).sel), Some("b.world")) + assertEquals(children.head.sel, Some("span#hello")) + assertEquals(children.tail.head.sel, Some("b.world")) } test("can create vnode with one child vnode") { - val vnode = h("div", Array(h("span#hello"))) + val vnode = h("div", List(h("span#hello"))) assertEquals(vnode.sel, Some("div")) val children = vnode.children - assertEquals(children.flatMap(_(0).sel), Some("span#hello")) + assertEquals(children.head.sel, Some("span#hello")) } test("can create vnode with props and one child vnode") { val vnode = h("div", VNodeData(), h("span#hello")) assertEquals(vnode.sel, Some("div")) val children = vnode.children - assertEquals(children.flatMap(_(0).sel), Some("span#hello")) + assertEquals(children.head.sel, Some("span#hello")) } test("can create vnode with text content") { - val vnode = h("a", Array(VNode.text("I am a string"))) + val vnode = h("a", List(VNode.text("I am a string"))) val children = vnode.children - assertEquals(children.flatMap(_(0).text), Some("I am a string")) + assertEquals(children.head.text, Some("I am a string")) } test("can create vnode with text content in string") { @@ -130,7 +130,7 @@ class SnabbdomSuite extends BaseSuite { // test("can create vnode with null props") { // var vnode = h("a") // assertEquals(vnode.data, None) - // vnode = h("a", null, Array(VNode.text("I am a string"))) + // vnode = h("a", null, List(VNode.text("I am a string"))) // val children = vnode.children // assertEquals(children.flatMap(_(0).text), Some("I am a string")) // } @@ -158,7 +158,7 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("has id") { vnode0 => - val elm = patch(vnode0, h("div", Array(h("div#unique")))).elm + val elm = patch(vnode0, h("div", List(h("div#unique")))).elm assertEquals(elm.firstChild.asInstanceOf[dom.Element].id, "unique") } @@ -168,7 +168,7 @@ class SnabbdomSuite extends BaseSuite { val data = VNodeData(ns = Some(SVGNamespace)) - val elm1 = patch(vnode0, h("div", Array(h("div", data)))).elm + val elm1 = patch(vnode0, h("div", List(h("div", data)))).elm assertEquals(elm1.firstChild.namespaceURI, SVGNamespace) // verify that svg tag automatically gets svg namespace @@ -176,10 +176,10 @@ class SnabbdomSuite extends BaseSuite { vnode0, h( "svg", - Array( + List( h( "foreignObject", - Array(h("div", Array[VNode]("I am HTML embedded in SVG"))) + List(h("div", List[VNode]("I am HTML embedded in SVG"))) ) ) ) @@ -200,7 +200,7 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("receives classes in selector") { vnode0 => - val elm = patch(vnode0, h("div", Array(h("i.am.a.class")))).elm + val elm = patch(vnode0, h("div", List(h("i.am.a.class")))).elm assert(elm.firstChild.asInstanceOf[dom.Element].classList.contains("am")) assert(elm.firstChild.asInstanceOf[dom.Element].classList.contains("a")) assert( @@ -225,7 +225,7 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("receives classes in selector when namespaced") { vnode0 => - val elm = patch(vnode0, h("svg", Array(h("g.am.a.class.too")))).elm + val elm = patch(vnode0, h("svg", List(h("g.am.a.class.too")))).elm assert(elm.firstChild.asInstanceOf[dom.Element].classList.contains("am")) assert(elm.firstChild.asInstanceOf[dom.Element].classList.contains("a")) assert( @@ -243,7 +243,7 @@ class SnabbdomSuite extends BaseSuite { "not" -> false ) ) - val elm = patch(vnode0, h("svg", Array(h("g", data)))).elm + val elm = patch(vnode0, h("svg", List(h("g", data)))).elm assert( elm.firstChild.asInstanceOf[dom.Element].classList.contains("am") ) @@ -258,7 +258,7 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("handles classes from both selector and property") { vnode0 => val data = VNodeData(classes = Map("classes" -> true)) - val elm = patch(vnode0, h("div", Array(h("i.has", data)))).elm + val elm = patch(vnode0, h("div", List(h("i.has", data)))).elm assert(elm.firstChild.asInstanceOf[dom.Element].classList.contains("has")) assert( elm.firstChild.asInstanceOf[dom.Element].classList.contains("classes") @@ -266,13 +266,13 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("can create elements with text content") { vnode0 => - val elm = patch(vnode0, h("div", Array[VNode]("I am a string"))).elm + val elm = patch(vnode0, h("div", List[VNode]("I am a string"))).elm assertEquals(elm.asInstanceOf[dom.Element].innerHTML, "I am a string") } vnode0.test("can create elements with span and text content") { vnode0 => val elm = - patch(vnode0, h("a", Array[VNode](h("span"), "I am a string"))).elm + patch(vnode0, h("a", List[VNode](h("span"), "I am a string"))).elm assertEquals(elm.childNodes(0).asInstanceOf[dom.Element].tagName, "SPAN") assertEquals( elm.childNodes(1).asInstanceOf[dom.Element].textContent, @@ -281,7 +281,7 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("can create vnode with array String obj content") { vnode0 => - val elm = patch(vnode0, h("a", Array[VNode]("b", "c"))).elm + val elm = patch(vnode0, h("a", List[VNode]("b", "c"))).elm assertEquals(elm.asInstanceOf[dom.Element].innerHTML, "bc") } @@ -302,7 +302,7 @@ class SnabbdomSuite extends BaseSuite { val elmWithIdAndClass = dom.document.createElement("div") elmWithIdAndClass.id = "id" elmWithIdAndClass.asInstanceOf[dom.HTMLElement].className = "class" - val vnode1 = h("div#id.class", Array(h("span", "Hi"))); + val vnode1 = h("div#id.class", List(h("span", "Hi"))); val elm = patch(elmWithIdAndClass, vnode1).elm.asInstanceOf[dom.Element] assertEquals(elm, elmWithIdAndClass) @@ -324,7 +324,7 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("is an instance of DocumentFragment") { vnode0 => val vnode1 = fragment( - Array[VNode]("I am", h("span", Array[VNode](" a", " fragment"))) + List[VNode]("I am", h("span", List[VNode](" a", " fragment"))) ) val elm = patch(vnode0, vnode1).elm assertEquals(elm.nodeType, dom.Node.DOCUMENT_FRAGMENT_NODE) @@ -440,7 +440,7 @@ class SnabbdomSuite extends BaseSuite { assert(!elm.asInstanceOf[js.Dictionary[String]].contains("src")) } - vnode0.test("cannot remove native props".only) { vnode0 => + vnode0.test("cannot remove native props") { vnode0 => val vnode1 = h("a", VNodeData(props = Map("href" -> "http://example.com/"))) val vnode2 = h("a") @@ -482,7 +482,7 @@ class SnabbdomSuite extends BaseSuite { prevElm.id = "id" prevElm.className = "class" prevElm.appendChild(h2) - val nextVNode = h("div#id.class", Array(h("span", "Hi"))) + val nextVNode = h("div#id.class", List(h("span", "Hi"))) val elm = patch(toVNode(prevElm), nextVNode).elm .asInstanceOf[dom.HTMLElement] @@ -506,7 +506,7 @@ class SnabbdomSuite extends BaseSuite { val nextVNode = VNode.create( Some(""), VNodeData.empty, - Some(Array(h("div#id.class", Array(h("span", "Hi"))))), + List(h("div#id.class", List(h("span", "Hi")))), None // Some(prevElm) ) @@ -562,7 +562,7 @@ class SnabbdomSuite extends BaseSuite { text.asInstanceOf[js.Dictionary[Any]]("testProperty") = reference prevElm.appendChild(text) prevElm.appendChild(h2) - val nextVNode = h("div#id.class", Array[VNode]("Foobar")) + val nextVNode = h("div#id.class", List[VNode]("Foobar")) val elm = patch(toVNode(prevElm), nextVNode).elm.asInstanceOf[dom.HTMLElement] assertEquals(elm, prevElm) @@ -589,7 +589,7 @@ class SnabbdomSuite extends BaseSuite { val text = dom.document.createTextNode("Foobar") prevElm.appendChild(text) prevElm.appendChild(h2) - val nextVNode = h("div#id.class", Array(h("h2", "Hello"))) + val nextVNode = h("div#id.class", List(h("h2", "Hello"))) val elm = patch(toVNode(prevElm), nextVNode).elm.asInstanceOf[dom.HTMLElement] @@ -648,8 +648,8 @@ class SnabbdomSuite extends BaseSuite { group("addition of elements") { vnode0.test("appends elements") { vnode0 => - val vnode1 = h("span", Array("1").map(spanNum)) - val vnode2 = h("span", Array("1", "2", "3").map(spanNum)) + val vnode1 = h("span", List("1").map(spanNum)) + val vnode2 = h("span", List("1", "2", "3").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 1) @@ -661,8 +661,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("prepends elements") { vnode0 => - val vnode1 = h("span", Array("4", "5").map(spanNum)) - val vnode2 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) + val vnode1 = h("span", List("4", "5").map(spanNum)) + val vnode2 = h("span", List("1", "2", "3", "4", "5").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 2) @@ -675,8 +675,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("add elements in the middle") { vnode0 => - val vnode1 = h("span", Array("1", "2", "4", "5").map(spanNum)) - val vnode2 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) + val vnode1 = h("span", List("1", "2", "4", "5").map(spanNum)) + val vnode2 = h("span", List("1", "2", "3", "4", "5").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) @@ -689,8 +689,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("add elements at begin and end") { vnode0 => - val vnode1 = h("span", Array("2", "3", "4").map(spanNum)) - val vnode2 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) + val vnode1 = h("span", List("2", "3", "4").map(spanNum)) + val vnode2 = h("span", List("1", "2", "3", "4", "5").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 3) @@ -707,7 +707,7 @@ class SnabbdomSuite extends BaseSuite { val vnode2 = h( "span", VNodeData(key = Some("span")), - Array("1", "2", "3").map(spanNum) + List("1", "2", "3").map(spanNum) ) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm @@ -724,7 +724,7 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h( "span", VNodeData(key = Some("span")), - Array("1", "2", "3").map(spanNum) + List("1", "2", "3").map(spanNum) ) val vnode2 = h("span", VNodeData(key = Some("span"))) val vnode1p = patch(vnode0, vnode1) @@ -741,9 +741,9 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("update one child with same key but different sel") { vnode0 => val data = VNodeData(key = Some("span")) val data2 = VNodeData(key = Some("2")) - val vnode1 = h("span", data, Array("1", "2", "3").map(spanNum)) + val vnode1 = h("span", data, List("1", "2", "3").map(spanNum)) val vnode2 = - h("span", data, Array(spanNum("1"), h("i", data2, "2"), spanNum("3"))) + h("span", data, List(spanNum("1"), h("i", data2, "2"), spanNum("3"))) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm @@ -767,8 +767,8 @@ class SnabbdomSuite extends BaseSuite { group("removal of elements") { vnode0.test("removes elements from the beginning") { vnode0 => - val vnode1 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) - val vnode2 = h("span", Array("3", "4", "5").map(spanNum)) + val vnode1 = h("span", List("1", "2", "3", "4", "5").map(spanNum)) + val vnode2 = h("span", List("3", "4", "5").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 5) @@ -781,8 +781,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("removes elements from the end") { vnode0 => - val vnode1 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) - val vnode2 = h("span", Array("1", "2", "3").map(spanNum)) + val vnode1 = h("span", List("1", "2", "3", "4", "5").map(spanNum)) + val vnode2 = h("span", List("1", "2", "3").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 5) @@ -795,8 +795,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("removes elements from the middle") { vnode0 => - val vnode1 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) - val vnode2 = h("span", Array("1", "2", "4", "5").map(spanNum)) + val vnode1 = h("span", List("1", "2", "3", "4", "5").map(spanNum)) + val vnode2 = h("span", List("1", "2", "4", "5").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 5) @@ -812,8 +812,8 @@ class SnabbdomSuite extends BaseSuite { group("element reordering") { vnode0.test("moves element forward") { vnode0 => - val vnode1 = h("span", Array("1", "2", "3", "4").map(spanNum)) - val vnode2 = h("span", Array("2", "3", "1", "4").map(spanNum)) + val vnode1 = h("span", List("1", "2", "3", "4").map(spanNum)) + val vnode2 = h("span", List("2", "3", "1", "4").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) @@ -827,8 +827,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("moves element to end") { vnode0 => - val vnode1 = h("span", Array("1", "2", "3").map(spanNum)) - val vnode2 = h("span", Array("2", "3", "1").map(spanNum)) + val vnode1 = h("span", List("1", "2", "3").map(spanNum)) + val vnode2 = h("span", List("2", "3", "1").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 3) @@ -841,8 +841,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("moves element backwards") { vnode0 => - val vnode1 = h("span", Array("1", "2", "3", "4").map(spanNum)) - val vnode2 = h("span", Array("1", "4", "2", "3").map(spanNum)) + val vnode1 = h("span", List("1", "2", "3", "4").map(spanNum)) + val vnode2 = h("span", List("1", "4", "2", "3").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) @@ -855,8 +855,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("swaps first and last") { vnode0 => - val vnode1 = h("span", Array("1", "2", "3", "4").map(spanNum)) - val vnode2 = h("span", Array("4", "2", "3", "1").map(spanNum)) + val vnode1 = h("span", List("1", "2", "3", "4").map(spanNum)) + val vnode2 = h("span", List("4", "2", "3", "1").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 4) @@ -871,8 +871,8 @@ class SnabbdomSuite extends BaseSuite { group("combinations of additions, removals and reorderings") { vnode0.test("move to left and replace") { vnode0 => - val vnode1 = h("span", Array("1", "2", "3", "4", "5").map(spanNum)) - val vnode2 = h("span", Array("4", "1", "2", "3", "6").map(spanNum)) + val vnode1 = h("span", List("1", "2", "3", "4", "5").map(spanNum)) + val vnode2 = h("span", List("4", "1", "2", "3", "6").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 5) @@ -885,8 +885,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("moves to left and leaves hole") { vnode0 => - val vnode1 = h("span", Array("1", "4", "5").map(spanNum)) - val vnode2 = h("span", Array("4", "6").map(spanNum)) + val vnode1 = h("span", List("1", "4", "5").map(spanNum)) + val vnode2 = h("span", List("4", "6").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 3) @@ -901,8 +901,8 @@ class SnabbdomSuite extends BaseSuite { vnode0.test( "handles moved and set to undefined element ending at the end" ) { vnode0 => - val vnode1 = h("span", Array("2", "4", "5").map(spanNum)) - val vnode2 = h("span", Array("4", "5", "3").map(spanNum)) + val vnode1 = h("span", List("2", "4", "5").map(spanNum)) + val vnode2 = h("span", List("4", "5", "3").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 3) @@ -916,10 +916,10 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("moves a key in non-keyed nodes with a size up") { vnode0 => val vnode1 = - h("span", Array(spanNum(1)) ++ Array("a", "b", "c").map(spanNum)) + h("span", List(spanNum(1)) ++ List("a", "b", "c").map(spanNum)) val vnode2 = h( "span", - Array("d", "a", "b", "c").map(spanNum) ++ Array(spanNum(1)) ++ Array( + List("d", "a", "b", "c").map(spanNum) ++ List(spanNum(1)) ++ List( spanNum("e") ) ) @@ -936,9 +936,9 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("reverses elements") { vnode0 => val vnode1 = - h("span", Array("1", "2", "3", "4", "5", "6", "7", "8").map(spanNum)) + h("span", List("1", "2", "3", "4", "5", "6", "7", "8").map(spanNum)) val vnode2 = - h("span", Array("8", "7", "6", "5", "4", "3", "2", "1").map(spanNum)) + h("span", List("8", "7", "6", "5", "4", "3", "2", "1").map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 8) @@ -952,9 +952,9 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("something") { vnode0 => val vnode1 = - h("span", Array(0, 1, 2, 3, 4, 5).map(spanNum)) + h("span", List(0, 1, 2, 3, 4, 5).map(spanNum)) val vnode2 = - h("span", Array(4, 3, 2, 1, 5, 0).map(spanNum)) + h("span", List(4, 3, 2, 1, 5, 0).map(spanNum)) val vnode1p = patch(vnode0, vnode1) val elm = vnode1p.elm assertEquals(elm.asInstanceOf[dom.Element].children.length, 6) @@ -978,7 +978,7 @@ class SnabbdomSuite extends BaseSuite { test("handles random shuffles") { val elms = 14 val samples = 5 - val arr = Array.tabulate(elms)(i => i) + val arr = List.tabulate(elms)(i => i) val rng = new scala.util.Random (0 until samples).foreach { _ => @@ -991,7 +991,7 @@ class SnabbdomSuite extends BaseSuite { elm1.asInstanceOf[dom.Element].children.toList.map(_.innerHTML), arr.map(_.toString).toList ) - val opacities = Array.tabulate(elms)(_ => f"${rng.nextDouble()}%.5f") + val opacities = List.tabulate(elms)(_ => f"${rng.nextDouble()}%.5f") val vnode2 = h("span", arr.map(n => spanNumWithOpacity(shufArr(n), opacities(n)))) val elm2 = @@ -1008,8 +1008,8 @@ class SnabbdomSuite extends BaseSuite { group("updated children without keys") { vnode0.test("appends elements") { vnode0 => - val vnode1 = h("div", Array(h("span", "Hello"))) - val vnode2 = h("div", Array(h("span", "Hello"), h("span", "World"))) + val vnode1 = h("div", List(h("span", "Hello"))) + val vnode2 = h("div", List(h("span", "Hello"), h("span", "World"))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.Element] assertEquals(elm1.children.toSeq.map(_.innerHTML), List("Hello")) @@ -1018,8 +1018,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("handles unmoved text nodes") { vnode0 => - val vnode1 = h("div", Array[VNode]("Text", h("span", "Span"))) - val vnode2 = h("div", Array[VNode]("Text", h("span", "Span"))) + val vnode1 = h("div", List[VNode]("Text", h("span", "Span"))) + val vnode2 = h("div", List[VNode]("Text", h("span", "Span"))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm assertEquals(elm1.childNodes(0).textContent, "Text") @@ -1028,8 +1028,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("handles changing text children") { vnode0 => - val vnode1 = h("div", Array[VNode]("Text", h("span", "Span"))) - val vnode2 = h("div", Array[VNode]("Text2", h("span", "Span"))) + val vnode1 = h("div", List[VNode]("Text", h("span", "Span"))) + val vnode2 = h("div", List[VNode]("Text2", h("span", "Span"))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm assertEquals(elm1.childNodes(0).textContent, "Text") @@ -1038,8 +1038,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("handles unmoved comment nodes") { vnode0 => - val vnode1 = h("div", Array[VNode](h("!", "Text"), h("span", "Span"))) - val vnode2 = h("div", Array[VNode](h("!", "Text"), h("span", "Span"))) + val vnode1 = h("div", List[VNode](h("!", "Text"), h("span", "Span"))) + val vnode2 = h("div", List[VNode](h("!", "Text"), h("span", "Span"))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm assertEquals(elm1.childNodes(0).textContent, "Text") @@ -1048,8 +1048,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("handles changing comment text") { vnode0 => - val vnode1 = h("div", Array[VNode](h("!", "Text"), h("span", "Span"))) - val vnode2 = h("div", Array[VNode](h("!", "Text2"), h("span", "Span"))) + val vnode1 = h("div", List[VNode](h("!", "Text"), h("span", "Span"))) + val vnode2 = h("div", List[VNode](h("!", "Text2"), h("span", "Span"))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm assertEquals(elm1.childNodes(0).textContent, "Text") @@ -1058,8 +1058,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("handles changing empty comment") { vnode0 => - val vnode1 = h("div", Array[VNode](h("!"), h("span", "Span"))) - val vnode2 = h("div", Array[VNode](h("!", "Test"), h("span", "Span"))) + val vnode1 = h("div", List[VNode](h("!"), h("span", "Span"))) + val vnode2 = h("div", List[VNode](h("!", "Test"), h("span", "Span"))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm assertEquals(elm1.childNodes(0).textContent, "") @@ -1068,8 +1068,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("prepends element") { vnode0 => - val vnode1 = h("div", Array(h("span", "World"))) - val vnode2 = h("div", Array(h("span", "Hello"), h("span", "World"))) + val vnode1 = h("div", List(h("span", "World"))) + val vnode2 = h("div", List(h("span", "Hello"), h("span", "World"))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.Element] assertEquals(elm1.children.toSeq.map(_.innerHTML), List("World")) @@ -1078,8 +1078,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("prepends element of different tag type") { vnode0 => - val vnode1 = h("div", Array(h("span", "World"))) - val vnode2 = h("div", Array(h("div", "Hello"), h("span", "World"))) + val vnode1 = h("div", List(h("span", "World"))) + val vnode2 = h("div", List(h("div", "Hello"), h("span", "World"))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.Element] assertEquals(elm1.children.toSeq.map(_.innerHTML), List("World")) @@ -1090,8 +1090,8 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("removes elements") { vnode0 => val vnode1 = - h("div", Array(h("span", "One"), h("span", "Two"), h("span", "Three"))) - val vnode2 = h("div", Array(h("span", "One"), h("span", "Three"))) + h("div", List(h("span", "One"), h("span", "Two"), h("span", "Three"))) + val vnode2 = h("div", List(h("span", "One"), h("span", "Three"))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.Element] assertEquals( @@ -1115,7 +1115,7 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("removes a single text node when children are updated") { vnode0 => val vnode1 = h("div", "One") - val vnode2 = h("div", Array(h("div", "Two"), h("span", "Three"))) + val vnode2 = h("div", List(h("div", "Two"), h("span", "Three"))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.Element] assertEquals(elm1.textContent, "One") @@ -1127,8 +1127,8 @@ class SnabbdomSuite extends BaseSuite { } vnode0.test("removes a text node among other elements") { vnode0 => - val vnode1 = h("div", Array[VNode]("One", h("span", "Two"))) - val vnode2 = h("div", Array(h("div", "Three"))) + val vnode1 = h("div", List[VNode]("One", h("span", "Two"))) + val vnode2 = h("div", List(h("div", "Three"))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm assertEquals( @@ -1143,9 +1143,9 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("reorders elements") { vnode0 => val vnode1 = - h("div", Array(h("span", "One"), h("div", "Two"), h("b", "Three"))) + h("div", List(h("span", "One"), h("div", "Two"), h("b", "Three"))) val vnode2 = - h("div", Array(h("b", "Three"), h("span", "One"), h("div", "Two"))) + h("div", List(h("b", "Three"), h("span", "One"), h("div", "Two"))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.Element] assertEquals( @@ -1164,13 +1164,13 @@ class SnabbdomSuite extends BaseSuite { group("patching a fragment") { vnode0.test("can patch on document fragments") { vnode0 => val vnode1 = fragment( - Array( + List( "I am", - h("span", Array(VNode.text(" a"), VNode.text(" fragment"))) + h("span", List(VNode.text(" a"), VNode.text(" fragment"))) ) ) - val vnode2 = h("div", Array(VNode.text("I am an element"))) - val vnode3 = fragment(Array("fragment ", "again")) + val vnode2 = h("div", List(VNode.text("I am an element"))) + val vnode3 = fragment(List("fragment ", "again")) val vnode1p = patch(vnode0, vnode1) var elm = vnode1p.elm @@ -1190,7 +1190,7 @@ class SnabbdomSuite extends BaseSuite { test("allows a document fragment as a container") { val vnode0 = dom.document.createDocumentFragment() val vnode1 = fragment( - Array("I", "am", "a", h("span", Array(VNode.text("fragment")))) + List("I", "am", "a", h("span", List(VNode.text("fragment")))) ) val vnode2 = h("div", "I am an element") @@ -1216,12 +1216,12 @@ class SnabbdomSuite extends BaseSuite { } val vnode1 = h( "div", - Array( + List( h("span", "First sibling"), h( "div", VNodeData(hook = Some(Hooks(create = Some(cb)))), - Array( + List( h("span", "Child 1"), h("span", "Child 2") ) @@ -1248,12 +1248,12 @@ class SnabbdomSuite extends BaseSuite { } val vnode1 = h( "div", - Array( + List( h("span", "First sibling"), h( "div", VNodeData(hook = Some(Hooks(insert = Some(cb)))), - Array( + List( h("span", "Child 1"), h("span", "Child 2") ) @@ -1268,19 +1268,19 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("calls `prepatch` listener") { vnode0 => val result = List.newBuilder[VNode] lazy val cb: PrePatchHook = (oldVnode, vnode) => { - assertEquals(vnode1.children.map(_(1)), Some(oldVnode.toVNode)) - assertEquals(vnode2.children.map(_(1)), Some(vnode)) + assertEquals(vnode1.children.tail.head, oldVnode.toVNode) + assertEquals(vnode2.children.tail.head, vnode) result.addOne(vnode) vnode } lazy val vnode1 = h( "div", - Array( + List( h("span", "First sibling"), h( "div", VNodeData(hook = Some(Hooks(prepatch = Some(cb)))), - Array( + List( h("span", "Child 1"), h("span", "Child 2") ) @@ -1289,14 +1289,15 @@ class SnabbdomSuite extends BaseSuite { ) lazy val vnode2 = h( "div", - Array( + List( h("span", "First sibling"), h( "div", VNodeData(hook = Some(Hooks(prepatch = Some(cb)))), - Array( + List( h("span", "Child 1"), - h("span", "Child 2") + h("span", "Child 2"), + h("span", "Child 3") ) ) ) @@ -1318,7 +1319,7 @@ class SnabbdomSuite extends BaseSuite { } val vnode1 = h( "div", - Array( + List( h("span", "First sibling"), h( "div", @@ -1330,7 +1331,7 @@ class SnabbdomSuite extends BaseSuite { ) ) ), - Array( + List( h("span", "Child 1"), h("span", "Child 2") ) @@ -1339,7 +1340,7 @@ class SnabbdomSuite extends BaseSuite { ) val vnode2 = h( "div", - Array( + List( h("span", "First sibling"), h( "div", @@ -1351,7 +1352,7 @@ class SnabbdomSuite extends BaseSuite { ) ) ), - Array( + List( h("span", "Child 1"), h("span", "Child 2") ) @@ -1380,12 +1381,12 @@ class SnabbdomSuite extends BaseSuite { } val vnode1 = h( "div", - Array( + List( h("span", "First sibling"), h( "div", VNodeData(hook = Some(Hooks(update = Some(cb(result1, _, _))))), - Array( + List( h("span", "Child 1"), h( "span", @@ -1398,12 +1399,12 @@ class SnabbdomSuite extends BaseSuite { ) val vnode2 = h( "div", - Array( + List( h("span", "First sibling"), h( "div", VNodeData(hook = Some(Hooks(update = Some(cb(result1, _, _))))), - Array( + List( h("span", "Child 1"), h( "span", @@ -1433,19 +1434,19 @@ class SnabbdomSuite extends BaseSuite { } val vnode1 = h( "div", - Array( + List( h("span", "First sibling"), h( "div", VNodeData(hook = Some(Hooks(remove = Some(cb)))), - Array( + List( h("span", "Child 1"), h("span", "Child 2") ) ) ) ) - val vnode2 = h("div", Array(h("span", "First sibling"))) + val vnode2 = h("div", List(h("span", "First sibling"))) val vnode1p = patch(vnode0, vnode1) patch(vnode1p, vnode2) assertEquals(result.result().length, 1) @@ -1460,11 +1461,11 @@ class SnabbdomSuite extends BaseSuite { } val vnode1 = h( "div", - Array( + List( h( "div", VNodeData(hook = Some(Hooks(destroy = Some(_ => cb())))), - Array(h("span", "Child 1")) + List(h("span", "Child 1")) ) ) ) @@ -1517,14 +1518,14 @@ class SnabbdomSuite extends BaseSuite { ) val vnode1 = h( "div", - Array( + List( h( "a", VNodeData(hook = Some(Hooks(remove = Some((_, rm) => rm3 = rm)))) ) ) ) - val vnode2 = h("div", Array.empty[VNode]) + val vnode2 = h("div", List.empty[VNode]) val vnode1p = patch(vnode0, vnode1) var elm = vnode1p.elm assertEquals(elm.childNodes.length, 1) @@ -1550,12 +1551,12 @@ class SnabbdomSuite extends BaseSuite { val vnode1 = h( "div", VNodeData(hook = Some(Hooks(remove = Some(cb)))), - Array( + List( h("b", "Child 1"), h("i", "Child 2") ) ) - val vnode2 = h("span", Array(h("b", "Child 1"), h("i", "Child 2"))) + val vnode2 = h("span", List(h("b", "Child 1"), h("i", "Child 2"))) val vnode1p = patch(vnode0, vnode1) patch(vnode1p, vnode2) assertEquals(result.result().length, 1) @@ -1586,11 +1587,11 @@ class SnabbdomSuite extends BaseSuite { } val vnode1 = h( "div", - Array( + List( h("span", "First sibling"), h( "div", - Array( + List( h( "span", VNodeData(hook = Some(Hooks(destroy = Some(cb)))), @@ -1609,8 +1610,8 @@ class SnabbdomSuite extends BaseSuite { vnode0.test("handles text vnodes with `undefined` `data` property") { vnode0 => - val vnode1 = h("div", Array(VNode.text(" "))) - val vnode2 = h("div", Array.empty[VNode]) + val vnode1 = h("div", List(VNode.text(" "))) + val vnode2 = h("div", List.empty[VNode]) val vnode1p = patch(vnode0, vnode1) patch(vnode1p, vnode2) } @@ -1629,9 +1630,9 @@ class SnabbdomSuite extends BaseSuite { ) val vnode1 = h( "div", - Array( + List( h("span", "First sibling"), - h("div", Array(h("span", "Child 1"), h("span", "Child 2"))) + h("div", List(h("span", "Child 1"), h("span", "Child 2"))) ) ) val vnode2 = h("div") @@ -1656,7 +1657,7 @@ class SnabbdomSuite extends BaseSuite { ) val vnode1 = h( "div", - Array( + List( h("span", "First child"), VNode.text(""), h("span", "Third child") @@ -1683,13 +1684,13 @@ class SnabbdomSuite extends BaseSuite { ) val vnode1 = h( "div", - Array( + List( h("span", "First sibling"), h( "div", - Array( + List( h("span", "Child 1"), - h("span", Array(VNode.text("Text 1"), VNode.text("Text 2"))) + h("span", List(VNode.text("Text 1"), VNode.text("Text 2"))) ) ) ) @@ -1711,7 +1712,7 @@ class SnabbdomSuite extends BaseSuite { } val vnode1 = h( "div", - Array( + List( h( "span", VNodeData(hook = Some(Hooks(update = Some(cb)))), @@ -1733,7 +1734,7 @@ class SnabbdomSuite extends BaseSuite { } val vnode1 = h( "div", - Array( + List( h( "span", VNodeData(hook = Some(Hooks(update = Some(cb)))), diff --git a/snabbdom/src/test/scala/snabbdom/StyleSuite.scala b/snabbdom/src/test/scala/snabbdom/StyleSuite.scala index 5463be0..992d8f7 100644 --- a/snabbdom/src/test/scala/snabbdom/StyleSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/StyleSuite.scala @@ -191,14 +191,14 @@ class StyleSuite extends BaseSuite { val vnode1 = h( "div", - Array(h("i", VNodeData(style = Map("--myVar" -> "1")))) + List(h("i", VNodeData(style = Map("--myVar" -> "1")))) ) val vnode2 = - h("div", Array(h("i", VNodeData()))) + h("div", List(h("i", VNodeData()))) val vnode3 = h( "div", - Array(h("i", VNodeData(style = Map("--myVar" -> "3")))) + List(h("i", VNodeData(style = Map("--myVar" -> "3")))) ) val vnode1p = patch(vnode0, vnode1) diff --git a/snabbdom/src/test/scala/snabbdom/SvgSuite.scala b/snabbdom/src/test/scala/snabbdom/SvgSuite.scala index bd868c5..2e3261d 100644 --- a/snabbdom/src/test/scala/snabbdom/SvgSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SvgSuite.scala @@ -53,8 +53,8 @@ class SvgSuite extends BaseSuite { val patch = init(Seq(Attributes.module)) vnode0.test("removes child svg elements") { vnode0 => - val a = h("svg", VNodeData(), Array(h("g"), h("g"))) - val b = h("svg", VNodeData(), Array(h("g"))) + val a = h("svg", VNodeData(), List(h("g"), h("g"))) + val b = h("svg", VNodeData(), List(h("g"))) val result = patch(patch(vnode0, a), b).elm.asInstanceOf[dom.SVGElement] assertEquals(result.childNodes.length, 1) } @@ -65,11 +65,11 @@ class SvgSuite extends BaseSuite { val a = h( "svg", VNodeData(), - Array( + List( h( "use", VNodeData(attrs = Map("xlink:href" -> testUrl)), - Array[VNode]() + List[VNode]() ) ) ) @@ -86,7 +86,7 @@ class SvgSuite extends BaseSuite { val a = h( "svg", VNodeData(attrs = Map("xml:lang" -> testAttrValue)), - Array[VNode]() + List[VNode]() ) val result = patch(vnode0, a).elm.asInstanceOf[dom.SVGElement] assertEquals(result.getAttributeNS(xmlNS, "lang"), testAttrValue) diff --git a/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala b/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala index ba0523e..d398f42 100644 --- a/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala @@ -71,8 +71,8 @@ class ThunkSuite extends BaseSuite { val n = arr(0).asInstanceOf[Int] h("span", VNodeData(key = Some("num")), s"Number is ${n}") } - val vnode1 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) - val vnode2 = h("div", Array(thunk("span", "num", numberInSpan, Seq(2)))) + val vnode1 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) + val vnode2 = h("div", List(thunk("span", "num", numberInSpan, Seq(2)))) val vnode1p = patch(vnode0, vnode1) assertEquals(called, 1) patch(vnode1p, vnode2) @@ -88,8 +88,8 @@ class ThunkSuite extends BaseSuite { val n = arr(0).asInstanceOf[Int] h("span", VNodeData(key = Some("num")), s"Number is ${n}") } - val vnode1 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) - val vnode2 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) + val vnode1 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) + val vnode2 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) val vnode1p = patch(vnode0, vnode1) assertEquals(called, 1) patch(vnode1p, vnode2) @@ -103,8 +103,8 @@ class ThunkSuite extends BaseSuite { val n = arr(0).asInstanceOf[Int] h("span", VNodeData(key = Some("num")), s"Number is ${n}") } - val vnode1 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) - val vnode2 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1, 2)))) + val vnode1 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) + val vnode2 = h("div", List(thunk("span", "num", numberInSpan, Seq(1, 2)))) val vnode1p = patch(vnode0, vnode1) assertEquals(called, 1) patch(vnode1p, vnode2) @@ -123,8 +123,8 @@ class ThunkSuite extends BaseSuite { val n = arr(0).asInstanceOf[Int] h("span", VNodeData(key = Some("num")), s"Number is ${n}") } - val vnode1 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) - val vnode2 = h("div", Array(thunk("span", "num", numberInSpan2, Seq(1)))) + val vnode1 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) + val vnode2 = h("div", List(thunk("span", "num", numberInSpan2, Seq(1)))) val vnode1p = patch(vnode0, vnode1) assertEquals(called, 1) patch(vnode1p, vnode2) @@ -138,9 +138,9 @@ class ThunkSuite extends BaseSuite { val n = arr(0).asInstanceOf[Int] h("span", VNodeData(key = Some("num")), s"Number is ${n}") } - val vnode1 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) - val vnode2 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) - val vnode3 = h("div", Array(thunk("span", "num", numberInSpan, Seq(2)))) + val vnode1 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) + val vnode2 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) + val vnode3 = h("div", List(thunk("span", "num", numberInSpan, Seq(2)))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] assertEquals(called, 1) @@ -228,8 +228,8 @@ class ThunkSuite extends BaseSuite { h("span", VNodeData(key = Some("foo")), s"${prefix}: ${n}") } - val vnode1 = h("div", Array(thunk("span", "num", numberInSpan, Seq(1)))) - val vnode2 = h("div", Array(thunk("div", "oddEven", oddEven, Seq(4)))) + val vnode1 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) + val vnode2 = h("div", List(thunk("div", "oddEven", oddEven, Seq(4)))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] @@ -299,13 +299,13 @@ class ThunkSuite extends BaseSuite { val vnode1 = h( "div", - Array( + List( h("div", "Foo"), thunk("span", "num", numberInSpan, Seq(1)), h("div", "Foo") ) ) - val vnode2 = h("div", Array(h("div", "Foo"), h("div", "Foo"))) + val vnode2 = h("div", List(h("div", "Foo"), h("div", "Foo"))) val vnode1p = patch(vnode0, vnode1) patch(vnode1p, vnode2) assertEquals(called, 1) @@ -330,13 +330,13 @@ class ThunkSuite extends BaseSuite { val vnode1 = h( "div", - Array( + List( h("div", "Foo"), thunk("span", "num", numberInSpan, Seq(1)), h("div", "Foo") ) ) - val vnode2 = h("div", Array(h("div", "Foo"), h("div", "Foo"))) + val vnode2 = h("div", List(h("div", "Foo"), h("div", "Foo"))) val vnode1p = patch(vnode0, vnode1) patch(vnode1p, vnode2) assertEquals(called, 1) From ad1c835006bf310f8c627547bdadaaf814664187 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Fri, 27 May 2022 11:26:08 +0200 Subject: [PATCH 08/35] all tests pass --- snabbdom/src/main/scala/snabbdom/Module.scala | 1 + .../src/main/scala/snabbdom/ModuleHooks.scala | 3 +- .../main/scala/snabbdom/PostPatchHook.scala | 2 +- snabbdom/src/main/scala/snabbdom/init.scala | 27 +++++++----- .../snabbdom/modules/EventListeners.scala | 43 +++++++++++++------ .../src/test/scala/snabbdom/ThunkSuite.scala | 1 - 6 files changed, 52 insertions(+), 25 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/Module.scala b/snabbdom/src/main/scala/snabbdom/Module.scala index 71414af..8de7d08 100644 --- a/snabbdom/src/main/scala/snabbdom/Module.scala +++ b/snabbdom/src/main/scala/snabbdom/Module.scala @@ -42,6 +42,7 @@ final case class Module( pre: Option[PreHook] = None, create: Option[CreateHook] = None, update: Option[UpdateHook] = None, + postPatch: Option[PostPatchHook] = None, destroy: Option[DestroyHook] = None, remove: Option[RemoveHook] = None, post: Option[PostHook] = None diff --git a/snabbdom/src/main/scala/snabbdom/ModuleHooks.scala b/snabbdom/src/main/scala/snabbdom/ModuleHooks.scala index 04096c2..554105e 100644 --- a/snabbdom/src/main/scala/snabbdom/ModuleHooks.scala +++ b/snabbdom/src/main/scala/snabbdom/ModuleHooks.scala @@ -41,6 +41,7 @@ package snabbdom final case class ModuleHooks( create: List[CreateHook], update: List[UpdateHook], + postPatch: List[PostPatchHook], remove: List[RemoveHook], destroy: List[DestroyHook], pre: List[PreHook], @@ -49,6 +50,6 @@ final case class ModuleHooks( object ModuleHooks { - def empty = ModuleHooks(Nil, Nil, Nil, Nil, Nil, Nil) + def empty = ModuleHooks(Nil, Nil, Nil, Nil, Nil, Nil, Nil) } diff --git a/snabbdom/src/main/scala/snabbdom/PostPatchHook.scala b/snabbdom/src/main/scala/snabbdom/PostPatchHook.scala index 8ba2fd2..45358ae 100644 --- a/snabbdom/src/main/scala/snabbdom/PostPatchHook.scala +++ b/snabbdom/src/main/scala/snabbdom/PostPatchHook.scala @@ -40,6 +40,6 @@ package snabbdom trait PostPatchHook { - def apply(oldVNode: PatchedVNode, vNode: PatchedVNode): Any + def apply(oldVNode: PatchedVNode, vNode: PatchedVNode): PatchedVNode } diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index 6d734c6..54b5bc4 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -72,6 +72,8 @@ object init { hooks.copy( create = module.create.fold(hooks.create)(_ :: hooks.create), update = module.update.fold(hooks.update)(_ :: hooks.update), + postPatch = + module.postPatch.fold(hooks.postPatch)(_ :: hooks.postPatch), remove = module.remove.fold(hooks.remove)(_ :: hooks.remove), destroy = module.destroy.fold(hooks.destroy)(_ :: hooks.destroy), pre = module.pre.fold(hooks.pre)(_ :: hooks.pre), @@ -310,15 +312,16 @@ object init { if (oldVnode.toVNode != vnode0) { - val vnode = cbs.update.foldLeft(vnode0) { case (vnode, hook) => - hook(oldVnode, vnode) + val vnode = { + val afterModules = cbs.update.foldLeft(vnode0) { case (vnode, hook) => + hook(oldVnode, vnode) + } + vnode0.data.hook + .flatMap(_.update) + .fold(afterModules)(hook => hook(oldVnode, afterModules)) } - vnode.data.hook - .flatMap(_.update) - .foreach(hook => hook(oldVnode, vnode)) - - val vnode1 = vnode.text match { + val patchedVNode = vnode.text match { case None => (oldCh, vnode.children) match { case (oldCh @ _ :: _, ch @ _ :: _) => @@ -414,9 +417,13 @@ object init { ) } - hook.flatMap(_.postpatch).foreach(hook => hook(oldVnode, vnode1)) - - vnode1 + val afterModules = cbs.postPatch.foldLeft(patchedVNode) { + case (vnode, hook) => + hook(oldVnode, vnode) + } + patchedVNode.data.hook + .flatMap(_.postpatch) + .fold(afterModules)(hook => hook(oldVnode, afterModules)) } else { diff --git a/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala b/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala index 9c86a04..429b436 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala @@ -46,28 +46,49 @@ object EventListeners { val module: Module = Module().copy( create = Some(new CreateHook { override def apply(vNode: PatchedVNode): Unit = { - () + + val listener = createListener(vNode) + val elm = vNode.elm + + vNode.data.on.foreach { case (name, _) => + elm.addEventListener(name, listener.jsFun, false) + } + } }), - update = Some(new UpdateHook { - override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = + postPatch = Some(new PostPatchHook { + override def apply( + oldVNode: PatchedVNode, + vNode: PatchedVNode + ): PatchedVNode = updateEventListeners(oldVNode, vNode) }), destroy = Some(new DestroyHook { override def apply(vnode: PatchedVNode): Unit = { - () + + val oldListener = vnode.listener + val oldOn = vnode.data.on + val elm = vnode.elm + + if (oldOn.nonEmpty && oldListener.isDefined) { + val ol = oldListener.get + oldOn.foreach { case (name, _) => + elm.removeEventListener(name, ol.jsFun, false) + } + } + } }) ) - private def createListener(vnode: VNode) = { - new Listener(vnode) + private def createListener(vnode: PatchedVNode) = { + new Listener(vnode.toVNode) } private def updateEventListeners( oldVnode: PatchedVNode, - vnode: VNode - ): VNode = { + vnode: PatchedVNode + ): PatchedVNode = { val oldOn = oldVnode.data.on val oldListener = oldVnode.listener @@ -107,10 +128,8 @@ object EventListeners { } } - // TODO - // val vnode1 = vnode.copy(listener = Some(listener)) - val vnode1 = vnode - listener.vnode = vnode1 + val vnode1 = vnode.copy(listener = Some(listener)) + listener.vnode = vnode1.toVNode vnode1 } else { diff --git a/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala b/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala index d398f42..76180d2 100644 --- a/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala @@ -244,7 +244,6 @@ class ThunkSuite extends BaseSuite { val vnode2p = patch(vnode1p, vnode2) val elm2 = vnode2p.elm.asInstanceOf[dom.HTMLElement] - println(elm2.innerHTML) assertEquals( elm2.firstChild.asInstanceOf[dom.HTMLElement].tagName.toLowerCase, "div" From a1920255290aa31762b57986fd3c972214a59206 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Fri, 27 May 2022 13:14:18 +0200 Subject: [PATCH 09/35] bug fix --- snabbdom/src/main/scala/snabbdom/init.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index 54b5bc4..7fc0d09 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -295,7 +295,7 @@ object init { removeAllVnodes(parentElm, toDelete) - patchedChildren + patchedChildren.reverse } From c070689189716a3ba7bb97381b14ca814f611a3a Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Fri, 27 May 2022 15:12:41 +0200 Subject: [PATCH 10/35] fix event listener handling --- .../src/main/scala/snabbdom/CreateHook.scala | 2 +- snabbdom/src/main/scala/snabbdom/init.scala | 48 ++++++++----- .../scala/snabbdom/modules/Attributes.scala | 5 +- .../main/scala/snabbdom/modules/Classes.scala | 4 +- .../main/scala/snabbdom/modules/Dataset.scala | 4 +- .../snabbdom/modules/EventListeners.scala | 68 ++++++------------- .../main/scala/snabbdom/modules/Props.scala | 4 +- .../main/scala/snabbdom/modules/Styles.scala | 4 +- .../test/scala/snabbdom/SnabbdomSuite.scala | 7 +- 9 files changed, 73 insertions(+), 73 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/CreateHook.scala b/snabbdom/src/main/scala/snabbdom/CreateHook.scala index 1ea233a..dec40b5 100644 --- a/snabbdom/src/main/scala/snabbdom/CreateHook.scala +++ b/snabbdom/src/main/scala/snabbdom/CreateHook.scala @@ -40,6 +40,6 @@ package snabbdom trait CreateHook { - def apply(vNode: PatchedVNode): Unit + def apply(vNode: PatchedVNode): PatchedVNode } diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index 7fc0d09..a0c9bbf 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -171,24 +171,32 @@ object init { sel.slice(dot + 1, sel.length).replaceAll("""\.""", " ") ) } - cbs.create.foreach(_.apply(vnode0)) - vnode0.children match { - case List() => - vnode0.text match { - case None => () - case Some(text) => - api.appendChild(elm, api.createTextNode(text)) - } - case children => - children.foreach { child => - api.appendChild(elm, child.elm) - } + + // insert children into dom + vnode0.children.foreach(child => api.appendChild(elm, child.elm)) + + // consider `text` only if there are no other children + if (vnode0.children.isEmpty) { + vnode0.text match { + case None => () + case Some(text) => + api.appendChild(elm, api.createTextNode(text)) + } } - vnode0.data.hook.map { hooks => - hooks.create.foreach(hook => hook(vnode0)) + + val vnode1 = cbs.create.foldLeft(vnode0) { case (vnode, hook) => + hook(vnode) + } + + val vnode2 = vnode0.data.hook + .flatMap(_.create) + .fold(vnode1)(hook => hook(vnode1)) + + vnode0.data.hook.foreach { hooks => hooks.insert.foreach { _ => insertedVNodeQueue.append(vnode0) } } - vnode0 + + vnode2 case None => vnode.children match { @@ -203,6 +211,7 @@ object init { None ) + // node is a fragment case children => val elm = api.createDocumentFragment val vnode0 = PatchedVNode( @@ -215,9 +224,12 @@ object init { elm = elm, None ) - cbs.create.foreach(hook => hook(vnode0)) - vnode0.children.foreach(ch => api.appendChild(elm, ch.elm)) - vnode0 + val vnode1 = cbs.create.foldLeft(vnode0) { case (vnode, hook) => + hook(vnode) + } + // insert children into dom + vnode1.children.foreach(child => api.appendChild(elm, child.elm)) + vnode1 } } diff --git a/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala b/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala index d2b2f3c..64991eb 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala @@ -45,7 +45,10 @@ object Attributes { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(vNode: PatchedVNode): Unit = setAttrs(vNode) + override def apply(vNode: PatchedVNode): PatchedVNode = { + setAttrs(vNode) + vNode + } }), update = Some(new UpdateHook { override def apply( diff --git a/snabbdom/src/main/scala/snabbdom/modules/Classes.scala b/snabbdom/src/main/scala/snabbdom/modules/Classes.scala index fe1b2d2..7b27fbb 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Classes.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Classes.scala @@ -45,8 +45,10 @@ object Classes { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(vNode: PatchedVNode): Unit = + override def apply(vNode: PatchedVNode): PatchedVNode = { setClasses(vNode) + vNode + } }), update = Some(new UpdateHook { override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { diff --git a/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala b/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala index dd7bbd0..5497909 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala @@ -46,8 +46,10 @@ object Dataset { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(vNode: PatchedVNode): Unit = + override def apply(vNode: PatchedVNode): PatchedVNode = { setDataset(vNode) + vNode + } }), update = Some(new UpdateHook { override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { diff --git a/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala b/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala index 429b436..e076e40 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala @@ -45,15 +45,12 @@ object EventListeners { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(vNode: PatchedVNode): Unit = { - + override def apply(vNode: PatchedVNode): PatchedVNode = { val listener = createListener(vNode) - val elm = vNode.elm - vNode.data.on.foreach { case (name, _) => - elm.addEventListener(name, listener.jsFun, false) + vNode.elm.addEventListener(name, listener.jsFun, false) } - + vNode.copy(listener = Some(listener)) } }), postPatch = Some(new PostPatchHook { @@ -65,18 +62,13 @@ object EventListeners { }), destroy = Some(new DestroyHook { override def apply(vnode: PatchedVNode): Unit = { - - val oldListener = vnode.listener - val oldOn = vnode.data.on - val elm = vnode.elm - - if (oldOn.nonEmpty && oldListener.isDefined) { - val ol = oldListener.get - oldOn.foreach { case (name, _) => - elm.removeEventListener(name, ol.jsFun, false) - } + vnode.listener match { + case Some(listener) => + vnode.data.on.foreach { case (name, _) => + vnode.elm.removeEventListener(name, listener.jsFun, false) + } + case None => () } - } }) ) @@ -95,49 +87,33 @@ object EventListeners { val elm = oldVnode.elm.asInstanceOf[dom.Element] val on = vnode.data.on - if (oldOn != on) { - + if (oldOn == on) { + vnode // nothing to do + } else { if (oldOn.nonEmpty && oldListener.isDefined) { + // remove old listeners that are no longer used val ol = oldListener.get - if (on.isEmpty) { - oldOn.foreach { case (name, _) => + oldOn.foreach { case (name, _) => + if (!on.contains(name)) { elm.removeEventListener(name, ol.jsFun, false) } - } else { - oldOn.foreach { case (name, _) => - if (on.get(name).isEmpty) { - elm.removeEventListener(name, ol.jsFun, false) - } - } } } - if (on.nonEmpty) { - val listener = oldListener.getOrElse(createListener(vnode)) + listener.vnode = + vnode.toVNode // if we are reusing an old listener, we must point it to the new vnode - if (oldOn.isEmpty) { - on.foreach { case (name, _) => + // add any new listeners + on.foreach { case (name, _) => + if (!oldOn.contains(name)) { elm.addEventListener(name, listener.jsFun, false) } - } else { - on.foreach { case (name, _) => - if (!oldOn.contains(name)) { - elm.addEventListener(name, listener.jsFun, false) - } - } } - - val vnode1 = vnode.copy(listener = Some(listener)) - listener.vnode = vnode1.toVNode - vnode1 - + vnode.copy(listener = Some(listener)) } else { - vnode + vnode // new vnode has no listeners } - - } else { - vnode } } diff --git a/snabbdom/src/main/scala/snabbdom/modules/Props.scala b/snabbdom/src/main/scala/snabbdom/modules/Props.scala index 9003117..b0263a1 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Props.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Props.scala @@ -45,8 +45,10 @@ object Props { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(vNode: PatchedVNode): Unit = + override def apply(vNode: PatchedVNode): PatchedVNode = { setProps(vNode) + vNode + } }), update = Some(new UpdateHook { override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { diff --git a/snabbdom/src/main/scala/snabbdom/modules/Styles.scala b/snabbdom/src/main/scala/snabbdom/modules/Styles.scala index 4d5158a..69d55d8 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Styles.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Styles.scala @@ -47,8 +47,10 @@ object Styles { val module: Module = Module().copy( create = Some(new CreateHook { - override def apply(vNode: PatchedVNode): Unit = + override def apply(vNode: PatchedVNode): PatchedVNode = { setStyle(vNode) + vNode + } }), update = Some(new UpdateHook { override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index f060ac3..ce82962 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -1213,6 +1213,7 @@ class SnabbdomSuite extends BaseSuite { assertEquals(vnode.elm.childNodes.length, 2) assertEquals(vnode.elm.parentNode, null) result.addOne(vnode) + vnode } val vnode1 = h( "div", @@ -1623,7 +1624,7 @@ class SnabbdomSuite extends BaseSuite { val patch = init( Seq( Module( - create = Some(_ => created += 1), + create = Some(vnode => { created += 1; vnode }), destroy = Some(_ => destroyed += 1) ) ) @@ -1650,7 +1651,7 @@ class SnabbdomSuite extends BaseSuite { val patch = init( Seq( Module( - create = Some(_ => created += 1), + create = Some(vnode => { created += 1; vnode }), remove = Some((_, _) => removed += 1) ) ) @@ -1677,7 +1678,7 @@ class SnabbdomSuite extends BaseSuite { val patch = init( Seq( Module( - create = Some(_ => created += 1), + create = Some(vnode => { created += 1; vnode }), destroy = Some(_ => destroyed += 1) ) ) From a5c76bd8ba538d7f92e71c3f533edc185d82b661 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Fri, 27 May 2022 15:39:32 +0200 Subject: [PATCH 11/35] some cleanup --- snabbdom/src/main/scala/snabbdom/VNode.scala | 4 +- snabbdom/src/main/scala/snabbdom/init.scala | 47 +++++--------------- 2 files changed, 13 insertions(+), 38 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/VNode.scala b/snabbdom/src/main/scala/snabbdom/VNode.scala index 3c8730b..bbe227a 100644 --- a/snabbdom/src/main/scala/snabbdom/VNode.scala +++ b/snabbdom/src/main/scala/snabbdom/VNode.scala @@ -63,10 +63,10 @@ object VNode { data: VNodeData, children: List[VNode], text: Option[String] - ) = new VNode(sel, data, children, text, data.key) + ) = VNode(sel, data, children, text, data.key) def text(text: String) = - new VNode(None, VNodeData.empty, Nil, Some(text), None) + VNode(None, VNodeData.empty, Nil, Some(text), None) implicit def fromString(s: String): VNode = text(s) diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index a0c9bbf..f509343 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -123,7 +123,7 @@ object init { val sel = vnode.sel sel match { - case Some("!") => + case Some("!") => // a comment node val text = vnode.text.getOrElse("") val elm = api.createComment(text) PatchedVNode( @@ -236,21 +236,6 @@ object init { } - def addAllVnodes( - parentElm: dom.Node, - before: Option[dom.Node], - vnodes: List[VNode], - insertedVNodeQueue: VNodeQueue - ): List[PatchedVNode] = vnodes.map { vnode => - val pvnode = createElm(vnode, insertedVNodeQueue) - api.insertBefore( - parentElm, - pvnode.elm, - before - ) - pvnode - } - def invokeDestroyHook(vnode: PatchedVNode): Unit = { if (!vnode.isTextNode) { // detroy hooks should not be called on text nodes vnode.data.hook.flatMap(_.destroy).foreach(hook => hook(vnode)) @@ -281,6 +266,10 @@ object init { } + // TODO + // highly sub-optimal right now + // doesn't use keys at all + // just something that compiles and passes the tests def updateChildren( parentElm: dom.Node, oldCh: List[PatchedVNode], @@ -322,7 +311,11 @@ object init { val elm = oldVnode.elm val oldCh = oldVnode.children - if (oldVnode.toVNode != vnode0) { + if (vnode0 == oldVnode.toVNode) { + + oldVnode // nothing to do + + } else { val vnode = { val afterModules = cbs.update.foldLeft(vnode0) { case (vnode, hook) => @@ -337,7 +330,7 @@ object init { case None => (oldCh, vnode.children) match { case (oldCh @ _ :: _, ch @ _ :: _) => - if (oldCh != ch) { + if (oldCh.map(_.toVNode) != ch) { PatchedVNode( vnode.sel, vnode.data, @@ -347,7 +340,6 @@ object init { elm, None ) - } else { PatchedVNode( vnode.sel, @@ -361,7 +353,6 @@ object init { } case (Nil, ch @ _ :: _) => oldVnode.text.foreach(_ => api.setTextContent(elm, Some(""))) - val patchedChildren = ch.map { vnode => val pvnode = createElm(vnode, insertedVNodeQueue) api.insertBefore( @@ -371,7 +362,6 @@ object init { ) pvnode } - PatchedVNode( vnode.sel, vnode.data, @@ -437,10 +427,6 @@ object init { .flatMap(_.postpatch) .fold(afterModules)(hook => hook(oldVnode, afterModules)) - } else { - - oldVnode - } } @@ -500,15 +486,4 @@ object init { vnode1.sel == vnode2.sel } - private def createKeyToOldIdx( - children: List[PatchedVNode] - ): Map[String, Int] = { - children.zipWithIndex - .map { case (ch, i) => - ch.key.map { key => (key -> i) } - } - .collect { case Some(a) => a } - .toMap - } - } From 25f1c2557041006565932cf8c1da0af75855d9cb Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Fri, 27 May 2022 17:00:39 +0200 Subject: [PATCH 12/35] improve `updateChildren` --- .../main/scala/snabbdom/PatchedVNode.scala | 2 +- snabbdom/src/main/scala/snabbdom/VNode.scala | 2 +- snabbdom/src/main/scala/snabbdom/init.scala | 38 ++++++++++++------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala b/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala index 16b3dd7..aa464a9 100644 --- a/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala +++ b/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala @@ -32,7 +32,7 @@ case class PatchedVNode private[snabbdom] ( ) { override def toString: String = - s"sel=$sel, data=$data, text=$text, key=$key, children=$children, elm=$elm, listener=$listener" + s"PatchedVNode(sel=$sel, data=$data, text=$text, key=$key, children=$children, elm=$elm, listener=$listener)" def toVNode: VNode = VNode(sel, data, children.map(_.toVNode), text, key) diff --git a/snabbdom/src/main/scala/snabbdom/VNode.scala b/snabbdom/src/main/scala/snabbdom/VNode.scala index bbe227a..e081d1b 100644 --- a/snabbdom/src/main/scala/snabbdom/VNode.scala +++ b/snabbdom/src/main/scala/snabbdom/VNode.scala @@ -47,7 +47,7 @@ case class VNode private ( ) { override def toString: String = - s"sel=$sel, data=$data, text=$text, key=$key, children=$children" + s"VNode(sel=$sel, data=$data, text=$text, key=$key, children=$children)" private[snabbdom] def isTextNode: Boolean = sel.isEmpty && children.isEmpty && text.isDefined diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index f509343..3016cc2 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -266,10 +266,6 @@ object init { } - // TODO - // highly sub-optimal right now - // doesn't use keys at all - // just something that compiles and passes the tests def updateChildren( parentElm: dom.Node, oldCh: List[PatchedVNode], @@ -277,24 +273,40 @@ object init { insertedVnodeQueue: VNodeQueue ): List[PatchedVNode] = { - val (toDelete, patchedChildren) = - newCh.foldLeft((oldCh, List.empty[PatchedVNode])) { - case ((oh :: ot, acc), newCh) => + val (toDelete1, toDelete2, patchedChildren) = + newCh.foldLeft((oldCh, oldCh.reverse, List.empty[PatchedVNode])) { + case ((oh :: ot, oh2 :: ot2, acc), newCh) => if (sameVnode(oh, newCh)) { val pn = patchVnode(oh, newCh, insertedVnodeQueue) - (ot, pn :: acc) - } else { + if (oh == oh2) { // exhausted old child nodes + (Nil, Nil, pn :: acc) + } else { + (ot, oh2 :: ot2, pn :: acc) + } + } else if (sameVnode(oh2, newCh)) { + val pn = patchVnode(oh2, newCh, insertedVnodeQueue) + api.insertBefore(parentElm, pn.elm, Some(oh.elm)) + if (oh == oh2) { // exhausted old child nodes + (Nil, Nil, pn :: acc) + } else { + (oh :: ot, ot2, pn :: acc) + } + } else { // new node val pn = createElm(newCh, insertedVnodeQueue) api.insertBefore(parentElm, pn.elm, Some(oh.elm)) - (oh :: ot, pn :: acc) + (oh :: ot, oh2 :: ot2, pn :: acc) } - case ((Nil, acc), newCh) => + case ((Nil, _, acc), newCh) => // new node + val pn = createElm(newCh, insertedVnodeQueue) + api.insertBefore(parentElm, pn.elm, None) + (Nil, Nil, pn :: acc) + case ((_, Nil, acc), newCh) => // new node val pn = createElm(newCh, insertedVnodeQueue) api.insertBefore(parentElm, pn.elm, None) - (Nil, pn :: acc) + (Nil, Nil, pn :: acc) } - removeAllVnodes(parentElm, toDelete) + removeAllVnodes(parentElm, toDelete1.intersect(toDelete2)) patchedChildren.reverse From 8ef782d2fb807e40c73e4437617621de23360895 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Fri, 27 May 2022 17:55:25 +0200 Subject: [PATCH 13/35] make thunk render functions take `Any` --- .../src/main/scala/snabbdom/VNodeData.scala | 4 +- snabbdom/src/main/scala/snabbdom/thunk.scala | 12 +- .../src/test/scala/snabbdom/ThunkSuite.scala | 108 +++++++++--------- 3 files changed, 62 insertions(+), 62 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/VNodeData.scala b/snabbdom/src/main/scala/snabbdom/VNodeData.scala index ce2bcde..9550429 100644 --- a/snabbdom/src/main/scala/snabbdom/VNodeData.scala +++ b/snabbdom/src/main/scala/snabbdom/VNodeData.scala @@ -48,8 +48,8 @@ case class VNodeData( hook: Option[Hooks] = None, key: Option[String] = None, ns: Option[String] = None, // for SVG - fn: Option[Seq[Any] => VNode] = None, // for thunks - args: Option[Seq[Any]] = None, // for thunks + fn: Option[Any => VNode] = None, // for thunks + args: Option[Any] = None, // for thunks is: Option[String] = None ) diff --git a/snabbdom/src/main/scala/snabbdom/thunk.scala b/snabbdom/src/main/scala/snabbdom/thunk.scala index 08607ef..80e09c6 100644 --- a/snabbdom/src/main/scala/snabbdom/thunk.scala +++ b/snabbdom/src/main/scala/snabbdom/thunk.scala @@ -42,22 +42,22 @@ object thunk { def apply( sel: String, - fn: Seq[Any] => VNode, - args: Seq[Any] + fn: Any => VNode, + args: Any ): VNode = apply(sel, None, fn, args) def apply( sel: String, key: String, - fn: Seq[Any] => VNode, - args: Seq[Any] + fn: Any => VNode, + args: Any ): VNode = apply(sel, Some(key), fn, args) private def apply( sel: String, key: Option[String], - fn: Seq[Any] => VNode, - args: Seq[Any] + fn: Any => VNode, + args: Any ): VNode = { val hook = Hooks().copy( init = Some((vNode: VNode) => init0(vNode)), diff --git a/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala b/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala index 76180d2..68463bc 100644 --- a/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/ThunkSuite.scala @@ -53,26 +53,26 @@ class ThunkSuite extends BaseSuite { test("returns vnode with data and render function") { - val numberInSpan = (arr: Seq[Any]) => { - val n = arr(0).asInstanceOf[Int] + val numberInSpan = (arg: Any) => { + val n = arg.asInstanceOf[Int] h("span", s"Numbe is ${n}") } - val vnode = thunk("span", "num", numberInSpan, Seq(22)) + val vnode = thunk("span", "num", numberInSpan, 22) assertEquals(vnode.sel, Some("span")) assertEquals(vnode.data.key, Some("num")) - assertEquals(vnode.data.args, Some(Seq(22))) + assertEquals(vnode.data.args, Some(22)) } vnode0.test("calls render function once on data change") { vnode0 => var called = 0 - val numberInSpan = (arr: Seq[Any]) => { + val numberInSpan = (arg: Any) => { called += 1 - val n = arr(0).asInstanceOf[Int] + val n = arg.asInstanceOf[Int] h("span", VNodeData(key = Some("num")), s"Number is ${n}") } - val vnode1 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) - val vnode2 = h("div", List(thunk("span", "num", numberInSpan, Seq(2)))) + val vnode1 = h("div", List(thunk("span", "num", numberInSpan, 1))) + val vnode2 = h("div", List(thunk("span", "num", numberInSpan, 2))) val vnode1p = patch(vnode0, vnode1) assertEquals(called, 1) patch(vnode1p, vnode2) @@ -83,28 +83,28 @@ class ThunkSuite extends BaseSuite { vnode0 => var called = 0 // important: we need a stable render function reference! - val numberInSpan = (arr: Seq[Any]) => { + val numberInSpan = (arg: Any) => { called += 1 - val n = arr(0).asInstanceOf[Int] + val n = arg.asInstanceOf[Int] h("span", VNodeData(key = Some("num")), s"Number is ${n}") } - val vnode1 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) - val vnode2 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) + val vnode1 = h("div", List(thunk("span", "num", numberInSpan, 1))) + val vnode2 = h("div", List(thunk("span", "num", numberInSpan, 1))) val vnode1p = patch(vnode0, vnode1) assertEquals(called, 1) patch(vnode1p, vnode2) assertEquals(called, 1) } - vnode0.test("calls render function once on data-length change") { vnode0 => + vnode0.test("calls render function once on args change") { vnode0 => var called = 0 - val numberInSpan = (arr: Seq[Any]) => { + val numberInSpan = (arg: Any) => { called += 1 - val n = arr(0).asInstanceOf[Int] + val n = arg.asInstanceOf[Int] h("span", VNodeData(key = Some("num")), s"Number is ${n}") } - val vnode1 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) - val vnode2 = h("div", List(thunk("span", "num", numberInSpan, Seq(1, 2)))) + val vnode1 = h("div", List(thunk("span", "num", numberInSpan, 1))) + val vnode2 = h("div", List(thunk("span", "num", numberInSpan, 2))) val vnode1p = patch(vnode0, vnode1) assertEquals(called, 1) patch(vnode1p, vnode2) @@ -113,18 +113,18 @@ class ThunkSuite extends BaseSuite { vnode0.test("calls render function once on function change") { vnode0 => var called = 0 - val numberInSpan = (arr: Seq[Any]) => { + val numberInSpan = (arg: Any) => { called += 1 - val n = arr(0).asInstanceOf[Int] + val n = arg.asInstanceOf[Int] h("span", VNodeData(key = Some("num")), s"Number is ${n}") } - val numberInSpan2 = (arr: Seq[Any]) => { + val numberInSpan2 = (arg: Any) => { called += 1 - val n = arr(0).asInstanceOf[Int] + val n = arg.asInstanceOf[Int] h("span", VNodeData(key = Some("num")), s"Number is ${n}") } - val vnode1 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) - val vnode2 = h("div", List(thunk("span", "num", numberInSpan2, Seq(1)))) + val vnode1 = h("div", List(thunk("span", "num", numberInSpan, 1))) + val vnode2 = h("div", List(thunk("span", "num", numberInSpan2, 1))) val vnode1p = patch(vnode0, vnode1) assertEquals(called, 1) patch(vnode1p, vnode2) @@ -133,14 +133,14 @@ class ThunkSuite extends BaseSuite { vnode0.test("renders correctly") { vnode0 => var called = 0 - val numberInSpan = (arr: Seq[Any]) => { + val numberInSpan = (arg: Any) => { called += 1 - val n = arr(0).asInstanceOf[Int] + val n = arg.asInstanceOf[Int] h("span", VNodeData(key = Some("num")), s"Number is ${n}") } - val vnode1 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) - val vnode2 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) - val vnode3 = h("div", List(thunk("span", "num", numberInSpan, Seq(2)))) + val vnode1 = h("div", List(thunk("span", "num", numberInSpan, 1))) + val vnode2 = h("div", List(thunk("span", "num", numberInSpan, 1))) + val vnode3 = h("div", List(thunk("span", "num", numberInSpan, 2))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] assertEquals(called, 1) @@ -177,25 +177,25 @@ class ThunkSuite extends BaseSuite { } vnode0.test("supports leaving out the `key` argument") { vnode0 => - val vnodeFn = (args: Seq[Any]) => { - val s = args.head.asInstanceOf[String] + val vnodeFn = (args: Any) => { + val s = args.asInstanceOf[String] h("span.number", s"Hello $s") } - val vnode1 = thunk("span.number", vnodeFn, Seq("World!")) + val vnode1 = thunk("span.number", vnodeFn, "World!") val elm = patch(vnode0, vnode1).elm assertEquals(elm.innerText, "Hello World!") } vnode0.test("renders correctly when root") { vnode0 => var called = 0 - val numberInSpan = (arr: Seq[Any]) => { + val numberInSpan = (arg: Any) => { called += 1 - val n = arr(0).asInstanceOf[Int] + val n = arg.asInstanceOf[Int] h("span", VNodeData(key = Some("num")), s"Number is ${n}") } - val vnode1 = thunk("span", "num", numberInSpan, Seq(1)) - val vnode2 = thunk("span", "num", numberInSpan, Seq(1)) - val vnode3 = thunk("span", "num", numberInSpan, Seq(2)) + val vnode1 = thunk("span", "num", numberInSpan, 1) + val vnode2 = thunk("span", "num", numberInSpan, 1) + val vnode3 = thunk("span", "num", numberInSpan, 2) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] @@ -218,18 +218,18 @@ class ThunkSuite extends BaseSuite { } vnode0.test("can be replaced and removed") { vnode0 => - val numberInSpan = (arr: Seq[Any]) => { - val n = arr(0).asInstanceOf[Int] + val numberInSpan = (arg: Any) => { + val n = arg.asInstanceOf[Int] h("span", VNodeData(key = Some("num")), s"Number is ${n}") } - val oddEven = (arr: Seq[Any]) => { - val n = arr(0).asInstanceOf[Int] + val oddEven = (arg: Any) => { + val n = arg.asInstanceOf[Int] val prefix = if (n % 2 == 0) "Even" else "Odd" h("span", VNodeData(key = Some("foo")), s"${prefix}: ${n}") } - val vnode1 = h("div", List(thunk("span", "num", numberInSpan, Seq(1)))) - val vnode2 = h("div", List(thunk("div", "oddEven", oddEven, Seq(4)))) + val vnode1 = h("div", List(thunk("span", "num", numberInSpan, 1))) + val vnode2 = h("div", List(thunk("div", "oddEven", oddEven, 4))) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] @@ -256,19 +256,19 @@ class ThunkSuite extends BaseSuite { } vnode0.test("can be replaced and removed when root") { vnode0 => - val numberInSpan = (arr: Seq[Any]) => { - val n = arr(0).asInstanceOf[Int] + val numberInSpan = (arg: Any) => { + val n = arg.asInstanceOf[Int] h("span", VNodeData(key = Some("num")), s"Number is ${n}") } - val oddEven = (arr: Seq[Any]) => { - val n = arr(0).asInstanceOf[Int] + val oddEven = (arg: Any) => { + val n = arg.asInstanceOf[Int] val prefix = if (n % 2 == 0) "Even" else "Odd" h("span", VNodeData(key = Some("foo")), s"${prefix}: ${n}") } - val vnode1 = thunk("span", "num", numberInSpan, Seq(1)) - val vnode2 = thunk("div", "oddEven", oddEven, Seq(4)) + val vnode1 = thunk("span", "num", numberInSpan, 1) + val vnode2 = thunk("div", "oddEven", oddEven, 4) val vnode1p = patch(vnode0, vnode1) val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] @@ -284,8 +284,8 @@ class ThunkSuite extends BaseSuite { vnode0.test("invokes destroy hook on thunks") { vnode0 => var called = 0 val destroyHook: DestroyHook = (_: PatchedVNode) => { called += 1 } - val numberInSpan = (arr: Seq[Any]) => { - val n = arr(0).asInstanceOf[Int] + val numberInSpan = (arg: Any) => { + val n = arg.asInstanceOf[Int] h( "span", VNodeData( @@ -300,7 +300,7 @@ class ThunkSuite extends BaseSuite { "div", List( h("div", "Foo"), - thunk("span", "num", numberInSpan, Seq(1)), + thunk("span", "num", numberInSpan, 1), h("div", "Foo") ) ) @@ -315,8 +315,8 @@ class ThunkSuite extends BaseSuite { val destroyHook: RemoveHook = (_: PatchedVNode, _: () => Unit) => { called += 1 } - val numberInSpan = (arr: Seq[Any]) => { - val n = arr(0).asInstanceOf[Int] + val numberInSpan = (arg: Any) => { + val n = arg.asInstanceOf[Int] h( "span", VNodeData( @@ -331,7 +331,7 @@ class ThunkSuite extends BaseSuite { "div", List( h("div", "Foo"), - thunk("span", "num", numberInSpan, Seq(1)), + thunk("span", "num", numberInSpan, 1), h("div", "Foo") ) ) From 4344bfbee501eaf3a37dfbee7ffea8e075784e48 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Fri, 27 May 2022 21:21:07 +0200 Subject: [PATCH 14/35] add new unit test --- .../test/scala/snabbdom/SnabbdomSuite.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index ce82962..00aab1c 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -720,6 +720,24 @@ class SnabbdomSuite extends BaseSuite { ) } + vnode0.test("reverses order of children".only) { vnode0 => + val vnode1 = h("div", List(h("span", "1"), h("span", "2"))) + val vnode2 = h("div", List(h("span", "2"), h("span", "1"))) + val vnode3 = h("div", List(h("span", "1"), h("span", "2"))) + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm + assertEquals(elm1.asInstanceOf[dom.Element].children(0).innerHTML, "1") + assertEquals(elm1.asInstanceOf[dom.Element].children(1).innerHTML, "2") + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm + assertEquals(elm2.asInstanceOf[dom.Element].children(0).innerHTML, "2") + assertEquals(elm2.asInstanceOf[dom.Element].children(1).innerHTML, "1") + val vnode3p = patch(vnode2p, vnode3) + val elm3 = vnode3p.elm + assertEquals(elm3.asInstanceOf[dom.Element].children(0).innerHTML, "1") + assertEquals(elm3.asInstanceOf[dom.Element].children(1).innerHTML, "2") + } + vnode0.test("removes all children to parent") { vnode0 => val vnode1 = h( "span", From 5aae32497b1df66256d97195ecd40538f2afc149 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Fri, 27 May 2022 23:08:33 +0200 Subject: [PATCH 15/35] improve `updateChildren`; add very simple micro benchmark comparison with JS snabbdom --- benchmarks/index.html | 11 +++ benchmarks/index.js | 71 +++++++++++++++++++ .../snabbdom/benchmarks/Benchmarks.scala | 57 +++++++++++++++ build.sbt | 11 ++- snabbdom/src/main/scala/snabbdom/init.scala | 13 +++- .../test/scala/snabbdom/SnabbdomSuite.scala | 2 +- 6 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 benchmarks/index.html create mode 100644 benchmarks/index.js create mode 100644 benchmarks/src/main/scala/snabbdom/benchmarks/Benchmarks.scala diff --git a/benchmarks/index.html b/benchmarks/index.html new file mode 100644 index 0000000..1b1a9aa --- /dev/null +++ b/benchmarks/index.html @@ -0,0 +1,11 @@ + + + + + + + +

See console

+
+ + diff --git a/benchmarks/index.js b/benchmarks/index.js new file mode 100644 index 0000000..5e8bac4 --- /dev/null +++ b/benchmarks/index.js @@ -0,0 +1,71 @@ +import { init, classModule, propsModule, styleModule, eventListenersModule, h } from 'https://cdn.jsdelivr.net/npm/snabbdom/+esm' + +const patch = init([ + classModule, + propsModule, + styleModule, + eventListenersModule, +]); + +document.addEventListener("DOMContentLoaded", function() { + + const container = document.getElementById("container"); + + const n = 10000 + + for (let j = 0; j < 0; j++) { + + console.log("running js-snabbdom..."); + let t0 = performance.now() + for (let i = 0; i <= n; i++) { + + container.replaceChildren(); + const root = document.createElement("div"); + container.appendChild(root); + + let vnode0 = h("div", {}) + let vnode1 = h("div", {}, [h("span", "1"), h("span", "2"), h("span", "3")]); + let vnode2 = h("div", {}, [h("span", "2"), h("span", "3")]); + let vnode3 = h("div", {}, [h("span", "3")]); + let vnode4 = h("div", {}, [h("span", "2"), h("span", "3")]); + let vnode5 = h("div", {}, [h("span", "1"), h("span", "2"), h("span", "3")]); + let vnode6 = h("div", {}, [h("span", "0"), h("span", "1"), h("span", "2"), h("span", "3")]); + let vnode7 = h("div", {}, [h("span", "0"), h("span", "1"), h("span", "2"), h("span", "3"), h("span", "4")]); + + patch(root, vnode0); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + patch(vnode2, vnode3); + patch(vnode3, vnode4); + patch(vnode4, vnode5); + patch(vnode5, vnode6); + patch(vnode6, vnode7); + + } + + let t1 = performance.now() + console.log(`done: ${t1 - t0} ms`) + + } + + + for (let j = 0; j < 100; j++) { + + console.log("running scala-js-snabbdom..."); + let t0 = performance.now() + for (let i = 0; i <= n; i++) { + + container.replaceChildren(); + const root = document.createElement("div"); + container.appendChild(root); + + SnabbdomBenchmarks.benchmark1(root); + + } + let t1 = performance.now() + console.log(`done: ${t1 - t0} ms`) + + } +} + +) diff --git a/benchmarks/src/main/scala/snabbdom/benchmarks/Benchmarks.scala b/benchmarks/src/main/scala/snabbdom/benchmarks/Benchmarks.scala new file mode 100644 index 0000000..8d9e9e8 --- /dev/null +++ b/benchmarks/src/main/scala/snabbdom/benchmarks/Benchmarks.scala @@ -0,0 +1,57 @@ +package snabbdom.benchmarks + +import org.scalajs.dom + +import snabbdom._ +import snabbdom.modules._ + +import scalajs.js.annotation._ + +@JSExportTopLevel("SnabbdomBenchmarks") +object Benchmarks { + + val patch = init( + Seq( + Attributes.module, + Classes.module, + Props.module, + Styles.module, + EventListeners.module, + Dataset.module + ) + ) + + @JSExport + def benchmark1(container: dom.Element): Unit = { + + val vnode0p = patch(container, h("div")) + + val vnode1 = h("div", List(h("span", "1"), h("span", "2"), h("span", "3"))) + val vnode2 = h("div", List(h("span", "2"), h("span", "3"))) + val vnode3 = h("div", List(h("span", "3"))) + val vnode4 = h("div", List(h("span", "2"), h("span", "3"))) + val vnode5 = h("div", List(h("span", "1"), h("span", "2"), h("span", "3"))) + val vnode6 = h( + "div", + List(h("span", "0"), h("span", "1"), h("span", "2"), h("span", "3")) + ) + val vnode7 = h( + "div", + List( + h("span", "0"), + h("span", "1"), + h("span", "2"), + h("span", "3"), + h("span", "4") + ) + ) + + List(vnode1, vnode2, vnode3, vnode4, vnode5, vnode6, vnode7).foldLeft( + vnode0p + ) { case (acc, vnode) => patch(acc, vnode) } + + () + + } + +} diff --git a/build.sbt b/build.sbt index 0f346eb..5b96eda 100644 --- a/build.sbt +++ b/build.sbt @@ -39,7 +39,7 @@ ThisBuild / Test / jsEnv := { lazy val scalajsDomVersion = "2.1.0" lazy val munitVersion = "0.7.29" -lazy val root = tlCrossRootProject.aggregate(snabbdom, examples) +lazy val root = tlCrossRootProject.aggregate(snabbdom, examples, benchmarks) lazy val snabbdom = (project .in(file("snabbdom"))) @@ -69,3 +69,12 @@ lazy val examples = (project } ) .dependsOn(snabbdom) + +lazy val benchmarks = (project + .in(file("benchmarks"))) + .enablePlugins(ScalaJSPlugin, NoPublishPlugin) + .settings( + name := "scala-js-snabbdom-benchmarks", + scalaJSUseMainModuleInitializer := false + ) + .dependsOn(snabbdom) diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index 3016cc2..3ba25ff 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -306,7 +306,18 @@ object init { (Nil, Nil, pn :: acc) } - removeAllVnodes(parentElm, toDelete1.intersect(toDelete2)) + val (_, toDelete) = + toDelete1.reverse.foldLeft((toDelete2, List.empty[PatchedVNode])) { + case (((h :: t), acc), vnode) => + if (vnode == h) { + (t, h :: acc) + } else { + (h :: t, acc) + } + case ((Nil, acc), _) => (Nil, acc) + } + + removeAllVnodes(parentElm, toDelete) patchedChildren.reverse diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index 00aab1c..214946b 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -720,7 +720,7 @@ class SnabbdomSuite extends BaseSuite { ) } - vnode0.test("reverses order of children".only) { vnode0 => + vnode0.test("reverses order of children") { vnode0 => val vnode1 = h("div", List(h("span", "1"), h("span", "2"))) val vnode2 = h("div", List(h("span", "2"), h("span", "1"))) val vnode3 = h("div", List(h("span", "1"), h("span", "2"))) From 0208cadf889687966a768c44ef58f9daf1dc4aac Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Fri, 27 May 2022 23:19:38 +0200 Subject: [PATCH 16/35] minor change in benchmark --- benchmarks/index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/benchmarks/index.js b/benchmarks/index.js index 5e8bac4..24188f0 100644 --- a/benchmarks/index.js +++ b/benchmarks/index.js @@ -1,10 +1,12 @@ -import { init, classModule, propsModule, styleModule, eventListenersModule, h } from 'https://cdn.jsdelivr.net/npm/snabbdom/+esm' +import { init, attributesModule, classModule, propsModule, styleModule, eventListenersModule, datasetModule, h } from 'https://cdn.jsdelivr.net/npm/snabbdom/+esm' const patch = init([ + attributesModule, classModule, propsModule, styleModule, eventListenersModule, + datasetModule ]); document.addEventListener("DOMContentLoaded", function() { @@ -12,8 +14,9 @@ document.addEventListener("DOMContentLoaded", function() { const container = document.getElementById("container"); const n = 10000 + const runs = 10 - for (let j = 0; j < 0; j++) { + for (let j = 0; j < runs; j++) { console.log("running js-snabbdom..."); let t0 = performance.now() @@ -49,7 +52,7 @@ document.addEventListener("DOMContentLoaded", function() { } - for (let j = 0; j < 100; j++) { + for (let j = 0; j < runs; j++) { console.log("running scala-js-snabbdom..."); let t0 = performance.now() From f5def44f43e8d40f2afc3b32cbb12c5a1606bdc9 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Fri, 27 May 2022 23:57:07 +0200 Subject: [PATCH 17/35] clean up modules; inline `DomApi` methods --- snabbdom/src/main/scala/snabbdom/DomApi.scala | 37 +++++----- .../scala/snabbdom/modules/Attributes.scala | 67 ++++++++--------- .../main/scala/snabbdom/modules/Classes.scala | 52 ++++++-------- .../main/scala/snabbdom/modules/Dataset.scala | 54 +++++++------- .../snabbdom/modules/EventListeners.scala | 22 ++++-- .../main/scala/snabbdom/modules/Props.scala | 39 ++++------ .../main/scala/snabbdom/modules/Styles.scala | 72 +++++++------------ 7 files changed, 147 insertions(+), 196 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/DomApi.scala b/snabbdom/src/main/scala/snabbdom/DomApi.scala index 5c37f96..9dc5bd3 100644 --- a/snabbdom/src/main/scala/snabbdom/DomApi.scala +++ b/snabbdom/src/main/scala/snabbdom/DomApi.scala @@ -91,26 +91,26 @@ object DomApi { def apply: DomApi = new DomApi { - override def createElement(tagName: String): dom.HTMLElement = + @inline override def createElement(tagName: String): dom.HTMLElement = dom.document .createElement(tagName) .asInstanceOf[dom.HTMLElement] // TODO: check cast - override def createElementNS( + @inline override def createElementNS( namespaceURI: String, qualifiedName: String ): dom.Element = dom.document.createElementNS(namespaceURI, qualifiedName) - override def createDocumentFragment: dom.DocumentFragment = + @inline override def createDocumentFragment: dom.DocumentFragment = dom.document.createDocumentFragment() - override def createTextNode(text: String): dom.Text = + @inline override def createTextNode(text: String): dom.Text = dom.document.createTextNode(text) - override def createComment(text: String): dom.Comment = + @inline override def createComment(text: String): dom.Comment = dom.document.createComment(text) - override def insertBefore( + @inline override def insertBefore( parentNode: dom.Node, newNode: dom.Node, referenceNode: Option[dom.Node] @@ -119,41 +119,44 @@ object DomApi { () } - override def removeChild(node: dom.Node, child: dom.Node): Unit = { + @inline override def removeChild(node: dom.Node, child: dom.Node): Unit = { node.removeChild(child) () } - override def appendChild(node: dom.Node, child: dom.Node): Unit = { + @inline override def appendChild(node: dom.Node, child: dom.Node): Unit = { node.appendChild(child) () } - override def parentNode(node: dom.Node): Option[dom.Node] = + @inline override def parentNode(node: dom.Node): Option[dom.Node] = Option(node.parentNode) - override def nextSibling(node: dom.Node): Option[dom.Node] = + @inline override def nextSibling(node: dom.Node): Option[dom.Node] = Option(node.nextSibling) - override def tagName(elm: dom.Element): String = elm.tagName + @inline override def tagName(elm: dom.Element): String = elm.tagName - override def setTextContent(node: dom.Node, text: Option[String]): Unit = { + @inline override def setTextContent( + node: dom.Node, + text: Option[String] + ): Unit = { node.textContent = text.getOrElse(null) } - override def getTextContent(node: dom.Node): Option[String] = + @inline override def getTextContent(node: dom.Node): Option[String] = Option(node.textContent) - override def isElement(node: dom.Node): Boolean = + @inline override def isElement(node: dom.Node): Boolean = node.nodeType == 1 - override def isText(node: dom.Node): Boolean = + @inline override def isText(node: dom.Node): Boolean = node.nodeType == 3 - override def isComment(node: dom.Node): Boolean = + @inline override def isComment(node: dom.Node): Boolean = node.nodeType == 8 - override def isDocumentFragement(node: dom.Node): Boolean = + @inline override def isDocumentFragement(node: dom.Node): Boolean = node.nodeType == 1 } diff --git a/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala b/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala index 64991eb..a88992a 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Attributes.scala @@ -46,7 +46,9 @@ object Attributes { val module: Module = Module().copy( create = Some(new CreateHook { override def apply(vNode: PatchedVNode): PatchedVNode = { - setAttrs(vNode) + if (vNode.data.attrs.nonEmpty) { + setAttrs(vNode) + } vNode } }), @@ -55,7 +57,9 @@ object Attributes { oldVNode: PatchedVNode, vNode: VNode ): VNode = { - updateAttrs(oldVNode, vNode) + if (vNode.data.attrs != oldVNode.data.attrs) { + updateAttrs(oldVNode, vNode) + } vNode } }) @@ -65,11 +69,8 @@ object Attributes { private val xmlNS = "http://www.w3.org/XML/1998/namespace" private def setAttrs(vnode: PatchedVNode): Unit = { - val elm = vnode.elm.asInstanceOf[dom.Element] - - val attrs = vnode.data.attrs - attrs.foreach { case (key, cur) => + vnode.data.attrs.foreach { case (key, cur) => if (cur == true) { elm.setAttribute(key, "") } else if (cur == false) { @@ -86,7 +87,6 @@ object Attributes { } } } - } private def updateAttrs( @@ -95,46 +95,35 @@ object Attributes { ): Unit = { val elm = oldVnode.elm.asInstanceOf[dom.Element] + val oldAttrs = oldVnode.data.attrs + val attrs = vnode.data.attrs - def update( - oldAttrs: Map[String, AttrValue], - attrs: Map[String, AttrValue] - ) = { - attrs.foreach { case (key, cur) => - val old = oldAttrs.get(key) - if (old.forall(_ != cur)) { - if (cur == true) { - elm.setAttribute(key, "") - } else if (cur == false) { - elm.removeAttribute(key) + attrs.foreach { case (key, cur) => + val old = oldAttrs.get(key) + if (old.forall(_ != cur)) { + if (cur == true) { + elm.setAttribute(key, "") + } else if (cur == false) { + elm.removeAttribute(key) + } else { + if (key.charAt(0) != 'x') { + elm.setAttribute(key, cur.toString) + } else if (key.length > 3 && key.charAt(3) == ':') { + elm.setAttributeNS(xmlNS, key, cur.toString) + } else if (key.length > 5 && key.charAt(5) == ':') { + elm.setAttributeNS(xlinkNS, key, cur.toString) } else { - if (key.charAt(0) != 'x') { - elm.setAttribute(key, cur.toString) - } else if (key.length > 3 && key.charAt(3) == ':') { - elm.setAttributeNS(xmlNS, key, cur.toString) - } else if (key.length > 5 && key.charAt(5) == ':') { - elm.setAttributeNS(xlinkNS, key, cur.toString) - } else { - elm.setAttribute(key, cur.toString) - } + elm.setAttribute(key, cur.toString) } } } - - oldAttrs.foreach { case (key, _) => - if (!attrs.contains(key)) { - elm.removeAttribute(key) - } - } } - val oldAttrs = oldVnode.data.attrs - val attrs = vnode.data.attrs - - if (oldAttrs != attrs) { - update(oldAttrs, attrs) + oldAttrs.foreach { case (key, _) => + if (!attrs.contains(key)) { + elm.removeAttribute(key) + } } - } } diff --git a/snabbdom/src/main/scala/snabbdom/modules/Classes.scala b/snabbdom/src/main/scala/snabbdom/modules/Classes.scala index 7b27fbb..8a4c4e3 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Classes.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Classes.scala @@ -46,65 +46,53 @@ object Classes { val module: Module = Module().copy( create = Some(new CreateHook { override def apply(vNode: PatchedVNode): PatchedVNode = { - setClasses(vNode) + if (vNode.data.classes.nonEmpty) { + setClasses(vNode) + } vNode } }), update = Some(new UpdateHook { override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { - updateClasses(oldVNode, vNode) + if (vNode.data.classes != oldVNode.data.classes) { + updateClasses(oldVNode, vNode) + } vNode } }) ) private def setClasses(vnode: PatchedVNode): Unit = { - val elm = vnode.elm.asInstanceOf[dom.Element] - val classes = vnode.data.classes - - classes.foreach { case (name, cur) => + vnode.data.classes.foreach { case (name, cur) => if (cur) { elm.classList.add(name) } else { elm.classList.remove(name) } } - } - private def updateClasses(oldVnode: PatchedVNode, vnode: VNode): VNode = { - + private def updateClasses(oldVnode: PatchedVNode, vnode: VNode): Unit = { val elm = oldVnode.elm.asInstanceOf[dom.Element] + val oldClasses = oldVnode.data.classes + val classes = vnode.data.classes - def update( - oldClass: Map[String, Boolean], - klass: Map[String, Boolean] - ): Unit = { - oldClass.foreach { case (name, flag) => - if (flag && !klass.contains(name)) { - elm.classList.remove(name) - } + oldClasses.foreach { case (name, flag) => + if (flag && !classes.contains(name)) { + elm.classList.remove(name) } - klass.foreach { case (name, cur) => - if (oldClass.get(name).forall(_ != cur)) { - if (cur) { - elm.classList.add(name) - } else { - elm.classList.remove(name) - } + } + classes.foreach { case (name, cur) => + if (oldClasses.get(name).forall(_ != cur)) { + if (cur) { + elm.classList.add(name) + } else { + elm.classList.remove(name) } } } - val oldClasses = oldVnode.data.classes - val classes = vnode.data.classes - - if (oldClasses != classes) { - update(oldClasses, classes) - } - vnode - } } diff --git a/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala b/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala index 5497909..a3cc07f 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala @@ -47,13 +47,17 @@ object Dataset { val module: Module = Module().copy( create = Some(new CreateHook { override def apply(vNode: PatchedVNode): PatchedVNode = { - setDataset(vNode) + if (vNode.data.dataset.nonEmpty) { + setDataset(vNode) + } vNode } }), update = Some(new UpdateHook { override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { - updateDataset(oldVNode, vNode) + if (vNode.data.dataset != oldVNode.data.dataset) { + updateDataset(oldVNode, vNode) + } vNode } }) @@ -88,41 +92,31 @@ object Dataset { val dataset = vnode.data.dataset val d = elm.dataset - def update( - oldDataset: Map[String, String], - dataset: Map[String, String] - ): Unit = { - - oldDataset.foreach { case (key, _) => - dataset.get(key) match { - case None => - if (!js.isUndefined(d)) { // TODO: does this make sense? - d -= key - } else { - elm.removeAttribute( - "data-" + key.replaceAll(CAPS_REGEX, "-$&").toLowerCase() - ) - } - case Some(_) => () - } - } - - dataset.foreach { case (key, value) => - if (oldDataset.get(key).forall(_ != value)) { + oldDataset.foreach { case (key, _) => + dataset.get(key) match { + case None => if (!js.isUndefined(d)) { // TODO: does this make sense? - d += (key -> value) + d -= key } else { - elm.setAttribute( - "data-" + key.replaceAll(CAPS_REGEX, "-$&").toLowerCase(), - value + elm.removeAttribute( + "data-" + key.replaceAll(CAPS_REGEX, "-$&").toLowerCase() ) } - } + case Some(_) => () } } - if (oldDataset != dataset) { - update(oldDataset, dataset) + dataset.foreach { case (key, value) => + if (oldDataset.get(key).forall(_ != value)) { + if (!js.isUndefined(d)) { // TODO: does this make sense? + d += (key -> value) + } else { + elm.setAttribute( + "data-" + key.replaceAll(CAPS_REGEX, "-$&").toLowerCase(), + value + ) + } + } } } diff --git a/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala b/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala index e076e40..9652e2f 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala @@ -46,11 +46,15 @@ object EventListeners { val module: Module = Module().copy( create = Some(new CreateHook { override def apply(vNode: PatchedVNode): PatchedVNode = { - val listener = createListener(vNode) - vNode.data.on.foreach { case (name, _) => - vNode.elm.addEventListener(name, listener.jsFun, false) + if (vNode.data.on.nonEmpty) { + val listener = createListener(vNode) + vNode.data.on.foreach { case (name, _) => + vNode.elm.addEventListener(name, listener.jsFun, false) + } + vNode.copy(listener = Some(listener)) + } else { + vNode } - vNode.copy(listener = Some(listener)) } }), postPatch = Some(new PostPatchHook { @@ -83,13 +87,17 @@ object EventListeners { ): PatchedVNode = { val oldOn = oldVnode.data.on - val oldListener = oldVnode.listener - val elm = oldVnode.elm.asInstanceOf[dom.Element] val on = vnode.data.on if (oldOn == on) { + vnode // nothing to do + } else { + + val oldListener = oldVnode.listener + val elm = oldVnode.elm.asInstanceOf[dom.Element] + if (oldOn.nonEmpty && oldListener.isDefined) { // remove old listeners that are no longer used val ol = oldListener.get @@ -99,6 +107,7 @@ object EventListeners { } } } + if (on.nonEmpty) { val listener = oldListener.getOrElse(createListener(vnode)) listener.vnode = @@ -114,6 +123,7 @@ object EventListeners { } else { vnode // new vnode has no listeners } + } } diff --git a/snabbdom/src/main/scala/snabbdom/modules/Props.scala b/snabbdom/src/main/scala/snabbdom/modules/Props.scala index b0263a1..b5a3c61 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Props.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Props.scala @@ -52,49 +52,38 @@ object Props { }), update = Some(new UpdateHook { override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { - updateProps(oldVNode, vNode) + if (vNode.data.props != oldVNode.data.props) { + updateProps(oldVNode, vNode) + } vNode } }) ) private def setProps(vnode: PatchedVNode): Unit = { - - val elm = vnode.elm - val props = vnode.data.props - - props.foreach { case (key, cur) => + vnode.data.props.foreach { case (key, cur) => if ( - (key != "value" || elm + (key != "value" || vnode.elm .asInstanceOf[js.Dictionary[Any]] .get(key) .forall(_ != cur)) - ) { elm.asInstanceOf[js.Dictionary[Any]](key) = cur } + ) { vnode.elm.asInstanceOf[js.Dictionary[Any]](key) = cur } } - } private def updateProps(oldVnode: PatchedVNode, vnode: VNode): Unit = { + val elm = oldVnode.elm val oldProps = oldVnode.data.props val props = vnode.data.props - def update( - oldProps: Map[String, PropValue], - props: Map[String, PropValue] - ): Unit = { - props.foreach { case (key, cur) => - if ( - oldProps.get(key).forall(_ != cur) && (key != "value" || elm - .asInstanceOf[js.Dictionary[Any]] - .get(key) - .forall(_ != cur)) - ) { elm.asInstanceOf[js.Dictionary[Any]](key) = cur } - } - } - - if (oldProps != props) { - update(oldProps, props) // TODO: remove old props + props.foreach { case (key, cur) => + if ( + oldProps.get(key).forall(_ != cur) && (key != "value" || elm + .asInstanceOf[js.Dictionary[Any]] + .get(key) + .forall(_ != cur)) + ) { elm.asInstanceOf[js.Dictionary[Any]](key) = cur } } } diff --git a/snabbdom/src/main/scala/snabbdom/modules/Styles.scala b/snabbdom/src/main/scala/snabbdom/modules/Styles.scala index 69d55d8..1809cab 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Styles.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Styles.scala @@ -54,28 +54,23 @@ object Styles { }), update = Some(new UpdateHook { override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { - updateStyle(oldVNode, vNode) + if (vNode.data.style != oldVNode.data.style) { + updateStyle(oldVNode, vNode) + } vNode } }) ) private def setStyle(vnode: PatchedVNode): Unit = { - val elm = vnode.elm - val style = vnode.data.style - - style.foreach { case (name, cur) => + vnode.data.style.foreach { case (name, cur) => if (name(0) == '-' && name(1) == '-') { - - elm.asInstanceOf[dom.HTMLElement].style.setProperty(name, cur) - + vnode.elm.asInstanceOf[dom.HTMLElement].style.setProperty(name, cur) } else { - - elm + vnode.elm .asInstanceOf[dom.HTMLElement] .style .asInstanceOf[js.Dictionary[String]](name) = cur - } } @@ -84,52 +79,35 @@ object Styles { private def updateStyle(oldVnode: PatchedVNode, vnode: VNode): Unit = { val elm = oldVnode.elm + val oldStyle = oldVnode.data.style + val style = vnode.data.style - def update( - oldStyle: Map[String, StyleValue], - style: Map[String, StyleValue] - ): Unit = { - - oldStyle.foreach { case (name, _) => - style.get(name) match { - case Some(_) => - case None => - if (name(0) == '-' && name(1) == '-') { - elm.asInstanceOf[dom.HTMLElement].style.removeProperty(name) - } else { - elm - .asInstanceOf[dom.HTMLElement] - .style - .asInstanceOf[js.Dictionary[String]](name) = "" - } - } - } - - style.foreach { case (name, cur) => - if (oldStyle.get(name).forall(_ != cur)) { - + oldStyle.foreach { case (name, _) => + style.get(name) match { + case Some(_) => + case None => if (name(0) == '-' && name(1) == '-') { - - elm.asInstanceOf[dom.HTMLElement].style.setProperty(name, cur) - + elm.asInstanceOf[dom.HTMLElement].style.removeProperty(name) } else { - elm .asInstanceOf[dom.HTMLElement] .style - .asInstanceOf[js.Dictionary[String]](name) = cur - + .asInstanceOf[js.Dictionary[String]](name) = "" } - } } - } - val oldStyle = oldVnode.data.style - val style = vnode.data.style - - if (oldStyle != style) { - update(oldStyle, style) + style.foreach { case (name, cur) => + if (oldStyle.get(name).forall(_ != cur)) { + if (name(0) == '-' && name(1) == '-') { + elm.asInstanceOf[dom.HTMLElement].style.setProperty(name, cur) + } else { + elm + .asInstanceOf[dom.HTMLElement] + .style + .asInstanceOf[js.Dictionary[String]](name) = cur + } + } } } From b831b442c4f40b8c24561b68e5a6dae1ad6ab625 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sat, 28 May 2022 15:55:48 +0200 Subject: [PATCH 18/35] improve `updateChildren` to guarantee patching of keyed nodes --- snabbdom/src/main/scala/snabbdom/init.scala | 89 ++++++++++++--- .../test/scala/snabbdom/SnabbdomSuite.scala | 108 ++++++++++++++---- 2 files changed, 162 insertions(+), 35 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index 3ba25ff..a3a8ef7 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -266,6 +266,12 @@ object init { } + // This would be much simpler if we didn't have to + // match up key'ed nodes even when their order and + // position has completely changed. + // Putting VNodes in Maps/Sets is expensive b/c of hashing + // so we want to avoid this as much as possible. + // TODO: try to simplify, use better variable names def updateChildren( parentElm: dom.Node, oldCh: List[PatchedVNode], @@ -273,37 +279,46 @@ object init { insertedVnodeQueue: VNodeQueue ): List[PatchedVNode] = { - val (toDelete1, toDelete2, patchedChildren) = - newCh.foldLeft((oldCh, oldCh.reverse, List.empty[PatchedVNode])) { - case ((oh :: ot, oh2 :: ot2, acc), newCh) => + val (toDelete1, toDelete2, patchedChildrenWithIndex, newKeyed) = + newCh.zipWithIndex.foldLeft( + ( + oldCh, + oldCh.reverse, + List.empty[(PatchedVNode, Int)], + List.empty[(VNode, Int)] + ) + ) { + case ((oh :: ot, oh2 :: ot2, acc, keyed), (newCh, i)) => if (sameVnode(oh, newCh)) { val pn = patchVnode(oh, newCh, insertedVnodeQueue) if (oh == oh2) { // exhausted old child nodes - (Nil, Nil, pn :: acc) + (Nil, Nil, (pn, i) :: acc, keyed) } else { - (ot, oh2 :: ot2, pn :: acc) + (ot, oh2 :: ot2, (pn, i) :: acc, keyed) } } else if (sameVnode(oh2, newCh)) { val pn = patchVnode(oh2, newCh, insertedVnodeQueue) api.insertBefore(parentElm, pn.elm, Some(oh.elm)) if (oh == oh2) { // exhausted old child nodes - (Nil, Nil, pn :: acc) + (Nil, Nil, (pn, i) :: acc, keyed) } else { - (oh :: ot, ot2, pn :: acc) + (oh :: ot, ot2, (pn, i) :: acc, keyed) } + } else if (newCh.key.isDefined) { + (oh :: ot, oh2 :: ot2, acc, (newCh, i) :: keyed) } else { // new node val pn = createElm(newCh, insertedVnodeQueue) api.insertBefore(parentElm, pn.elm, Some(oh.elm)) - (oh :: ot, oh2 :: ot2, pn :: acc) + (oh :: ot, oh2 :: ot2, (pn, i) :: acc, keyed) } - case ((Nil, _, acc), newCh) => // new node + case ((Nil, _, acc, keyed), (newCh, i)) => // new node val pn = createElm(newCh, insertedVnodeQueue) api.insertBefore(parentElm, pn.elm, None) - (Nil, Nil, pn :: acc) - case ((_, Nil, acc), newCh) => // new node + (Nil, Nil, (pn, i) :: acc, keyed) + case ((_, Nil, acc, keyed), (newCh, i)) => // new node val pn = createElm(newCh, insertedVnodeQueue) api.insertBefore(parentElm, pn.elm, None) - (Nil, Nil, pn :: acc) + (Nil, Nil, (pn, i) :: acc, keyed) } val (_, toDelete) = @@ -317,9 +332,55 @@ object init { case ((Nil, acc), _) => (Nil, acc) } - removeAllVnodes(parentElm, toDelete) + val (toDeleteKeyed, toDeleteUnkeyed) = toDelete.partition(_.key.isDefined) + + val oldKeyed = toDeleteKeyed.map(n => (n.key.get -> n)).toMap + + val (patchedKeyedChildrenWithIndex, toDeleteKeyed0) = + newKeyed.foldLeft( + (List.empty[(PatchedVNode, Int)], oldKeyed) + ) { case ((acc1, acc2), (vnode, i)) => + acc2.get(vnode.key.get) match { + case None => + ((createElm(vnode, insertedVnodeQueue), i) :: acc1, acc2) + case Some(pvnode) => + ( + (patchVnode(pvnode, vnode, insertedVnodeQueue), i) :: acc1, + acc2 - vnode.key.get + ) + } + } + + removeAllVnodes(parentElm, toDeleteUnkeyed) + removeAllVnodes(parentElm, toDeleteKeyed0.values.toList) + + def merge( + v1: List[(PatchedVNode, Int)], + v2: List[(PatchedVNode, Int)], + acc: List[PatchedVNode] + ): List[PatchedVNode] = { + (v1, v2) match { + case (((h1, i) :: t1), ((h2, j) :: t2)) => + if (j < i) { + api.insertBefore(parentElm, h2.elm, Some(h1.elm)) + merge(v1, t2, h2 :: acc) + } else { + assert(j > i) + merge(t1, v2, h1 :: acc) + } + case (Nil, ((h2, _) :: t2)) => + api.insertBefore(parentElm, h2.elm, None) + merge(Nil, t2, h2 :: acc) + case (((h1, _) :: t1), Nil) => merge(t1, Nil, h1 :: acc) + case (Nil, Nil) => acc + } + } - patchedChildren.reverse + merge( + patchedChildrenWithIndex.reverse, + patchedKeyedChildrenWithIndex, + List.empty + ).reverse } diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index 214946b..c6e8879 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -720,24 +720,6 @@ class SnabbdomSuite extends BaseSuite { ) } - vnode0.test("reverses order of children") { vnode0 => - val vnode1 = h("div", List(h("span", "1"), h("span", "2"))) - val vnode2 = h("div", List(h("span", "2"), h("span", "1"))) - val vnode3 = h("div", List(h("span", "1"), h("span", "2"))) - val vnode1p = patch(vnode0, vnode1) - val elm1 = vnode1p.elm - assertEquals(elm1.asInstanceOf[dom.Element].children(0).innerHTML, "1") - assertEquals(elm1.asInstanceOf[dom.Element].children(1).innerHTML, "2") - val vnode2p = patch(vnode1p, vnode2) - val elm2 = vnode2p.elm - assertEquals(elm2.asInstanceOf[dom.Element].children(0).innerHTML, "2") - assertEquals(elm2.asInstanceOf[dom.Element].children(1).innerHTML, "1") - val vnode3p = patch(vnode2p, vnode3) - val elm3 = vnode3p.elm - assertEquals(elm3.asInstanceOf[dom.Element].children(0).innerHTML, "1") - assertEquals(elm3.asInstanceOf[dom.Element].children(1).innerHTML, "2") - } - vnode0.test("removes all children to parent") { vnode0 => val vnode1 = h( "span", @@ -885,6 +867,25 @@ class SnabbdomSuite extends BaseSuite { List("4", "2", "3", "1") ) } + + vnode0.test("reverses order of children twice") { vnode0 => + val vnode1 = h("div", List(h("span", "1"), h("span", "2"))) + val vnode2 = h("div", List(h("span", "2"), h("span", "1"))) + val vnode3 = h("div", List(h("span", "1"), h("span", "2"))) + val vnode1p = patch(vnode0, vnode1) + val elm1 = vnode1p.elm + assertEquals(elm1.asInstanceOf[dom.Element].children(0).innerHTML, "1") + assertEquals(elm1.asInstanceOf[dom.Element].children(1).innerHTML, "2") + val vnode2p = patch(vnode1p, vnode2) + val elm2 = vnode2p.elm + assertEquals(elm2.asInstanceOf[dom.Element].children(0).innerHTML, "2") + assertEquals(elm2.asInstanceOf[dom.Element].children(1).innerHTML, "1") + val vnode3p = patch(vnode2p, vnode3) + val elm3 = vnode3p.elm + assertEquals(elm3.asInstanceOf[dom.Element].children(0).innerHTML, "1") + assertEquals(elm3.asInstanceOf[dom.Element].children(1).innerHTML, "2") + } + } group("combinations of additions, removals and reorderings") { @@ -995,10 +996,9 @@ class SnabbdomSuite extends BaseSuite { // it is odd that the orginal passes without using the styles module... test("handles random shuffles") { val elms = 14 - val samples = 5 + val samples = 50 val arr = List.tabulate(elms)(i => i) - val rng = new scala.util.Random - + val rng = new scala.util.Random(0) (0 until samples).foreach { _ => val vnode1 = h("span", arr.map(spanNumWithOpacity(_, "1"))) val shufArr = rng.shuffle(arr) @@ -1022,6 +1022,72 @@ class SnabbdomSuite extends BaseSuite { } } } + + vnode0.test("keyed nodes are patched") { vnode0 => + val result1b = List.newBuilder[VNode] + val result2b = List.newBuilder[VNode] + val cb: UpdateHook = (pvnode, newVNode) => { + result1b += pvnode.toVNode + result2b += newVNode + newVNode + } + + def keyedSpan(text: String, key: Int) = + h( + "span", + VNodeData( + key = Some(key.toString), + hook = Some(Hooks(update = Some(cb))) + ), + text + ) + + val vnode1 = + h( + "div", + List( + h("span", "a"), + keyedSpan("a", 1), + keyedSpan("b", 2), + keyedSpan("c", 3), + h("span", "d") + ) + ) + + val vnode2 = h( + "div", + List( + keyedSpan("aa", 1), + keyedSpan("bb", 2), + keyedSpan("cc", 3), + h("span", "d") + ) + ) + + val vnode1p = patch(vnode0, vnode1) + patch(vnode1p, vnode2) + + val result1 = result1b.result() + val result2 = result2b.result() + assertEquals( + result1.toSet, + Set( + keyedSpan("a", 1), + keyedSpan("b", 2), + keyedSpan("c", 3) + ) + ) + assertEquals( + result2.toSet, + Set( + keyedSpan("aa", 1), + keyedSpan("bb", 2), + keyedSpan("cc", 3) + ) + ) + + } + } group("updated children without keys") { From 01a666b4fc93066fd3d2d4dd27571bdca2f83385 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sat, 28 May 2022 17:13:17 +0200 Subject: [PATCH 19/35] add another randomized test --- .../test/scala/snabbdom/SnabbdomSuite.scala | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index c6e8879..6e99529 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -1023,6 +1023,82 @@ class SnabbdomSuite extends BaseSuite { } } + test("random shuffle of keyed and non-keyed nodes") { + + val rng = new scala.util.Random(0) + + def span(text: String) = h("span", text) + + def kSpan(text: String, key: Int, update: UpdateHook) = + h( + "span", + VNodeData( + key = Some(key.toString), + hook = Some(Hooks(update = Some(update))) + ), + text + ) + + for { + _ <- 0 until 1000 + } yield { + + val n1 = rng.nextInt(100) + val n2 = rng.nextInt(100) + + var counter = 0 + + val cb: UpdateHook = (_, vnode) => { + counter += 1 + vnode + } + + val spans = List.tabulate(n1)(i => span(s"a-$i")) + val kspans = List.tabulate(n2)(i => kSpan(s"k-$i", i, cb)) + + val spansAll = spans ::: kspans + + val vnode1 = h("div", rng.shuffle(spansAll)) + val vnode2 = h( + "div", + rng + .shuffle(spansAll) + .map(vnode => vnode.copy(text = vnode.text.map(_ + "X"))) + ) + val vnode3 = h("div", rng.shuffle(spansAll)) + + val elm = dom.document.createElement("div") + val vnode1p = patch(elm, vnode1) + assertEquals(vnode1p.toVNode, vnode1) + val elm1 = vnode1p.elm.asInstanceOf[dom.HTMLElement] + val innerHTML1 = vnode1.children.map { vnode => + s"${vnode.text.get}" + }.mkString + assertEquals(elm1.innerHTML, innerHTML1) + + val vnode2p = patch(vnode1p, vnode2) + assertEquals(vnode2p.toVNode, vnode2) + val elm2 = vnode2p.elm.asInstanceOf[dom.HTMLElement] + val innerHTML2 = vnode2.children.map { vnode => + s"${vnode.text.get}" + }.mkString + assertEquals(elm2.innerHTML, innerHTML2) + + val vnode3p = patch(vnode2p, vnode3) + assertEquals(vnode3p.toVNode, vnode3) + val elm3 = vnode3p.elm.asInstanceOf[dom.HTMLElement] + val innerHTML3 = vnode3.children.map { vnode => + s"${vnode.text.get}" + }.mkString + assertEquals(elm3.innerHTML, innerHTML3) + + // update hook should trigger twice, in patch from 1 -> 2 and from 2 -> 3 + assertEquals(counter, 2 * n2) + + } + + } + vnode0.test("keyed nodes are patched") { vnode0 => val result1b = List.newBuilder[VNode] val result2b = List.newBuilder[VNode] From 07d1972cd553c6c690afabf1f222e2ddce50ad42 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sat, 28 May 2022 17:15:08 +0200 Subject: [PATCH 20/35] run `githubWorkflowGenerate` --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcc98c1..84a8c2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,11 +91,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: mkdir -p examples/target target .js/target snabbdom/target .jvm/target .native/target project/target + run: mkdir -p benchmarks/target examples/target target .js/target snabbdom/target .jvm/target .native/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: tar cf targets.tar examples/target target .js/target snabbdom/target .jvm/target .native/target project/target + run: tar cf targets.tar benchmarks/target examples/target target .js/target snabbdom/target .jvm/target .native/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') From f114581c0e193948f0115ae56172e02bde8ff2b1 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sat, 28 May 2022 17:18:01 +0200 Subject: [PATCH 21/35] add missing headers --- .../scala/snabbdom/benchmarks/Benchmarks.scala | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/benchmarks/src/main/scala/snabbdom/benchmarks/Benchmarks.scala b/benchmarks/src/main/scala/snabbdom/benchmarks/Benchmarks.scala index 8d9e9e8..3e3af26 100644 --- a/benchmarks/src/main/scala/snabbdom/benchmarks/Benchmarks.scala +++ b/benchmarks/src/main/scala/snabbdom/benchmarks/Benchmarks.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2022 buntec + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package snabbdom.benchmarks import org.scalajs.dom From 5f6661655b779a5884fe39969547c5cf182a4dc5 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sat, 28 May 2022 17:21:29 +0200 Subject: [PATCH 22/35] bump base version to 0.2 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 5b96eda..1759b02 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -ThisBuild / tlBaseVersion := "0.1" +ThisBuild / tlBaseVersion := "0.2" val scala213 = "2.13.8" ThisBuild / scalaVersion := scala213 From 0bdc6f032f45558493dbaa6179700c16a209d675 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sat, 28 May 2022 20:18:16 +0200 Subject: [PATCH 23/35] clean up; add comments to `updateChildren`; remove unused props --- snabbdom/src/main/scala/snabbdom/init.scala | 51 ++++++++++++++----- .../main/scala/snabbdom/modules/Classes.scala | 20 +++----- .../main/scala/snabbdom/modules/Dataset.scala | 20 +++----- .../snabbdom/modules/EventListeners.scala | 44 +++++++--------- .../main/scala/snabbdom/modules/Props.scala | 40 +++++++-------- .../main/scala/snabbdom/modules/Styles.scala | 18 +++---- .../test/scala/snabbdom/SnabbdomSuite.scala | 6 +-- 7 files changed, 100 insertions(+), 99 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index a3a8ef7..3b7a98a 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -119,7 +119,7 @@ object init { ): PatchedVNode = { val vnode = - vnode0.data.hook.flatMap(_.init).fold(vnode0)(hook => hook(vnode0)) + vnode0.data.hook.flatMap(_.init).fold(vnode0)(_(vnode0)) val sel = vnode.sel sel match { @@ -188,9 +188,8 @@ object init { hook(vnode) } - val vnode2 = vnode0.data.hook - .flatMap(_.create) - .fold(vnode1)(hook => hook(vnode1)) + val vnode2 = + vnode0.data.hook.flatMap(_.create).fold(vnode1)(_(vnode1)) vnode0.data.hook.foreach { hooks => hooks.insert.foreach { _ => insertedVNodeQueue.append(vnode0) } @@ -269,9 +268,7 @@ object init { // This would be much simpler if we didn't have to // match up key'ed nodes even when their order and // position has completely changed. - // Putting VNodes in Maps/Sets is expensive b/c of hashing - // so we want to avoid this as much as possible. - // TODO: try to simplify, use better variable names + // TODO: Try to simplify and use better variable names. def updateChildren( parentElm: dom.Node, oldCh: List[PatchedVNode], @@ -279,6 +276,18 @@ object init { insertedVnodeQueue: VNodeQueue ): List[PatchedVNode] = { + // `toDelete1` - the old children in original order that were not + // matched against new children. + // `toDelete2` - the old children in reverse order that were not + // matched against new children. + // The intersection of toDelete1 and toDelete2 are nodes that + // should either be deleted or are key'ed nodes that have been moved around. + // `patchedChildrenWithIndex` - new children that are the result of + // patching old children. Zipped with the index in the list of all + // new children to allow merging with key'ed nodes later. + // `newKeyed` - new children with keys that have not been matched + // to old nodes. They are either new nodes or correspond to + // old key'ed nodes that have been moved around. val (toDelete1, toDelete2, patchedChildrenWithIndex, newKeyed) = newCh.zipWithIndex.foldLeft( ( @@ -304,23 +313,30 @@ object init { } else { (oh :: ot, ot2, (pn, i) :: acc, keyed) } - } else if (newCh.key.isDefined) { + } else if (newCh.key.isDefined) { // node with key - try to match later (oh :: ot, oh2 :: ot2, acc, (newCh, i) :: keyed) - } else { // new node + } else { // new node without key val pn = createElm(newCh, insertedVnodeQueue) api.insertBefore(parentElm, pn.elm, Some(oh.elm)) (oh :: ot, oh2 :: ot2, (pn, i) :: acc, keyed) } - case ((Nil, _, acc, keyed), (newCh, i)) => // new node + case ( + (Nil, _, acc, keyed), + (newCh, i) + ) => // old nodes are exhausted - must be new node val pn = createElm(newCh, insertedVnodeQueue) api.insertBefore(parentElm, pn.elm, None) (Nil, Nil, (pn, i) :: acc, keyed) - case ((_, Nil, acc, keyed), (newCh, i)) => // new node + case ( + (_, Nil, acc, keyed), + (newCh, i) + ) => // old nodes are exhausted - must be new node val pn = createElm(newCh, insertedVnodeQueue) api.insertBefore(parentElm, pn.elm, None) (Nil, Nil, (pn, i) :: acc, keyed) } + // `toDelete` is the efficiently computed intersection of `toDelete1` and `toDelete2`. val (_, toDelete) = toDelete1.reverse.foldLeft((toDelete2, List.empty[PatchedVNode])) { case (((h :: t), acc), vnode) => @@ -332,10 +348,17 @@ object init { case ((Nil, acc), _) => (Nil, acc) } + // We split the old nodes that are candidates for deletion + // into those with keys and those without. Those without keys + // should certainly be deleted. Those with keys we'll try to match + // up against new children with keys - any leftovers will be deleted val (toDeleteKeyed, toDeleteUnkeyed) = toDelete.partition(_.key.isDefined) val oldKeyed = toDeleteKeyed.map(n => (n.key.get -> n)).toMap + // Here we match new, unmatched children with keys against + // old children with keys. Any leftover old children with + // keys will be returned for deletion. val (patchedKeyedChildrenWithIndex, toDeleteKeyed0) = newKeyed.foldLeft( (List.empty[(PatchedVNode, Int)], oldKeyed) @@ -354,6 +377,10 @@ object init { removeAllVnodes(parentElm, toDeleteUnkeyed) removeAllVnodes(parentElm, toDeleteKeyed0.values.toList) + // A helper function to merge child nodes that were patched or created + // in the first fold, and those new key'ed child nodes that were + // matched up with old key'ed children. The result is the + // list of all patched children in reverse order. def merge( v1: List[(PatchedVNode, Int)], v2: List[(PatchedVNode, Int)], @@ -365,7 +392,7 @@ object init { api.insertBefore(parentElm, h2.elm, Some(h1.elm)) merge(v1, t2, h2 :: acc) } else { - assert(j > i) + assert(j > i) // v1 and v2 are disjoint! merge(t1, v2, h1 :: acc) } case (Nil, ((h2, _) :: t2)) => diff --git a/snabbdom/src/main/scala/snabbdom/modules/Classes.scala b/snabbdom/src/main/scala/snabbdom/modules/Classes.scala index 8a4c4e3..f0049e3 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Classes.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Classes.scala @@ -44,21 +44,17 @@ import org.scalajs.dom object Classes { val module: Module = Module().copy( - create = Some(new CreateHook { - override def apply(vNode: PatchedVNode): PatchedVNode = { - if (vNode.data.classes.nonEmpty) { - setClasses(vNode) - } - vNode + create = Some((vNode: PatchedVNode) => { + if (vNode.data.classes.nonEmpty) { + setClasses(vNode) } + vNode }), - update = Some(new UpdateHook { - override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { - if (vNode.data.classes != oldVNode.data.classes) { - updateClasses(oldVNode, vNode) - } - vNode + update = Some((oldVNode: PatchedVNode, vNode: VNode) => { + if (vNode.data.classes != oldVNode.data.classes) { + updateClasses(oldVNode, vNode) } + vNode }) ) diff --git a/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala b/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala index a3cc07f..f37a38f 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Dataset.scala @@ -45,21 +45,17 @@ import scalajs.js object Dataset { val module: Module = Module().copy( - create = Some(new CreateHook { - override def apply(vNode: PatchedVNode): PatchedVNode = { - if (vNode.data.dataset.nonEmpty) { - setDataset(vNode) - } - vNode + create = Some((vNode: PatchedVNode) => { + if (vNode.data.dataset.nonEmpty) { + setDataset(vNode) } + vNode }), - update = Some(new UpdateHook { - override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { - if (vNode.data.dataset != oldVNode.data.dataset) { - updateDataset(oldVNode, vNode) - } - vNode + update = Some((oldVNode: PatchedVNode, vNode: VNode) => { + if (vNode.data.dataset != oldVNode.data.dataset) { + updateDataset(oldVNode, vNode) } + vNode }) ) diff --git a/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala b/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala index 9652e2f..41dbec0 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/EventListeners.scala @@ -44,35 +44,27 @@ import org.scalajs.dom object EventListeners { val module: Module = Module().copy( - create = Some(new CreateHook { - override def apply(vNode: PatchedVNode): PatchedVNode = { - if (vNode.data.on.nonEmpty) { - val listener = createListener(vNode) - vNode.data.on.foreach { case (name, _) => - vNode.elm.addEventListener(name, listener.jsFun, false) - } - vNode.copy(listener = Some(listener)) - } else { - vNode + create = Some((vNode: PatchedVNode) => { + if (vNode.data.on.nonEmpty) { + val listener = createListener(vNode) + vNode.data.on.foreach { case (name, _) => + vNode.elm.addEventListener(name, listener.jsFun, false) } + vNode.copy(listener = Some(listener)) + } else { + vNode } }), - postPatch = Some(new PostPatchHook { - override def apply( - oldVNode: PatchedVNode, - vNode: PatchedVNode - ): PatchedVNode = - updateEventListeners(oldVNode, vNode) - }), - destroy = Some(new DestroyHook { - override def apply(vnode: PatchedVNode): Unit = { - vnode.listener match { - case Some(listener) => - vnode.data.on.foreach { case (name, _) => - vnode.elm.removeEventListener(name, listener.jsFun, false) - } - case None => () - } + postPatch = Some((oldVNode: PatchedVNode, vNode: PatchedVNode) => + updateEventListeners(oldVNode, vNode) + ), + destroy = Some((vnode: PatchedVNode) => { + vnode.listener match { + case Some(listener) => + vnode.data.on.foreach { case (name, _) => + vnode.elm.removeEventListener(name, listener.jsFun, false) + } + case None => () } }) ) diff --git a/snabbdom/src/main/scala/snabbdom/modules/Props.scala b/snabbdom/src/main/scala/snabbdom/modules/Props.scala index b5a3c61..269906a 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Props.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Props.scala @@ -44,30 +44,21 @@ import scalajs.js object Props { val module: Module = Module().copy( - create = Some(new CreateHook { - override def apply(vNode: PatchedVNode): PatchedVNode = { - setProps(vNode) - vNode - } + create = Some((vNode: PatchedVNode) => { + setProps(vNode) + vNode }), - update = Some(new UpdateHook { - override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { - if (vNode.data.props != oldVNode.data.props) { - updateProps(oldVNode, vNode) - } - vNode + update = Some((oldVNode: PatchedVNode, vNode: VNode) => { + if (vNode.data.props != oldVNode.data.props) { + updateProps(oldVNode, vNode) } + vNode }) ) private def setProps(vnode: PatchedVNode): Unit = { vnode.data.props.foreach { case (key, cur) => - if ( - (key != "value" || vnode.elm - .asInstanceOf[js.Dictionary[Any]] - .get(key) - .forall(_ != cur)) - ) { vnode.elm.asInstanceOf[js.Dictionary[Any]](key) = cur } + vnode.elm.asInstanceOf[js.Dictionary[Any]] += (key -> cur) } } @@ -78,12 +69,15 @@ object Props { val props = vnode.data.props props.foreach { case (key, cur) => - if ( - oldProps.get(key).forall(_ != cur) && (key != "value" || elm - .asInstanceOf[js.Dictionary[Any]] - .get(key) - .forall(_ != cur)) - ) { elm.asInstanceOf[js.Dictionary[Any]](key) = cur } + if (oldProps.get(key).forall(_ != cur)) { + elm.asInstanceOf[js.Dictionary[Any]] += (key -> cur) + } + } + + oldProps.foreach { case (key, cur) => + if (!props.contains(key)) { + elm.asInstanceOf[js.Dictionary[Any]] -= key + } } } diff --git a/snabbdom/src/main/scala/snabbdom/modules/Styles.scala b/snabbdom/src/main/scala/snabbdom/modules/Styles.scala index 1809cab..4be8519 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Styles.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Styles.scala @@ -46,19 +46,15 @@ import org.scalajs.dom object Styles { val module: Module = Module().copy( - create = Some(new CreateHook { - override def apply(vNode: PatchedVNode): PatchedVNode = { - setStyle(vNode) - vNode - } + create = Some((vNode: PatchedVNode) => { + setStyle(vNode) + vNode }), - update = Some(new UpdateHook { - override def apply(oldVNode: PatchedVNode, vNode: VNode): VNode = { - if (vNode.data.style != oldVNode.data.style) { - updateStyle(oldVNode, vNode) - } - vNode + update = Some((oldVNode: PatchedVNode, vNode: VNode) => { + if (vNode.data.style != oldVNode.data.style) { + updateStyle(oldVNode, vNode) } + vNode }) ) diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index 6e99529..f01c4f7 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -430,8 +430,7 @@ class SnabbdomSuite extends BaseSuite { } - // TODO: This appears to be a bug in the orignal: https://github.com/snabbdom/snabbdom/pull/1019 - vnode0.test("removes custom props".ignore) { vnode0 => + vnode0.test("removes custom props") { vnode0 => val vnode1 = h("a", VNodeData(props = Map("src" -> "http://other/"))) val vnode2 = h("a") @@ -460,7 +459,8 @@ class SnabbdomSuite extends BaseSuite { } - vnode0.test("does not delete custom props") { vnode0 => + // this test seems to assert the opposite of "removes custom props" ??? + vnode0.test("does not delete custom props".ignore) { vnode0 => val vnode1 = h("p", VNodeData(props = Map("a" -> "foo"))) val vnode2 = h("p") val vnode1p = patch(vnode0, vnode1) From f7ce50c64218da23faadf5f075b8ef8898861f4f Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sat, 28 May 2022 20:35:52 +0200 Subject: [PATCH 24/35] clean up --- snabbdom/src/main/scala/snabbdom/init.scala | 60 ++++++++++----------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index 3b7a98a..e328fbd 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -42,6 +42,7 @@ import org.scalajs.dom import scala.collection.mutable import snabbdom.modules._ +import scala.annotation.tailrec object init { @@ -146,19 +147,16 @@ object init { } else { sel } - val elm = if (vnode.data.ns.isDefined) { - api.createElementNS( - vnode.data.ns.get, - tag - ) // TODO what about data? - } else { - api.createElement(tag) // TODO what about data argument? + val elm = vnode.data.ns match { + case Some(ns) => + api.createElementNS(ns, tag) // TODO what about data? + case None => + api.createElement(tag) // TODO what about data argument? } val vnode0 = PatchedVNode( vnode.sel, vnode.data, - children = - vnode.children.map(ch => createElm(ch, insertedVNodeQueue)), + children = vnode.children.map(createElm(_, insertedVNodeQueue)), text = vnode.text, key = vnode.key, elm = elm, @@ -191,8 +189,8 @@ object init { val vnode2 = vnode0.data.hook.flatMap(_.create).fold(vnode1)(_(vnode1)) - vnode0.data.hook.foreach { hooks => - hooks.insert.foreach { _ => insertedVNodeQueue.append(vnode0) } + vnode0.data.hook.flatMap(_.insert).foreach { _ => + insertedVNodeQueue.append(vnode0) } vnode2 @@ -237,9 +235,9 @@ object init { def invokeDestroyHook(vnode: PatchedVNode): Unit = { if (!vnode.isTextNode) { // detroy hooks should not be called on text nodes - vnode.data.hook.flatMap(_.destroy).foreach(hook => hook(vnode)) - cbs.destroy.foreach(hook => hook(vnode)) - vnode.children.foreach { child => invokeDestroyHook(child) } + vnode.data.hook.flatMap(_.destroy).foreach(_(vnode)) + cbs.destroy.foreach(_(vnode)) + vnode.children.foreach(invokeDestroyHook) } } @@ -253,10 +251,10 @@ object init { invokeDestroyHook(ch) val listeners = cbs.remove.length + 1 val rm = createRmCb(ch.elm, listeners) - cbs.remove.foreach(hook => hook(ch, rm)) - ch.data.hook - .flatMap(_.remove) - .fold(rm()) { hook => hook(ch, rm); () } + cbs.remove.foreach(_(ch, rm)) + ch.data.hook.flatMap(_.remove).fold(rm()) { hook => + hook(ch, rm); () + } case None => // text node api.removeChild(parentElm, ch.elm) } @@ -377,11 +375,11 @@ object init { removeAllVnodes(parentElm, toDeleteUnkeyed) removeAllVnodes(parentElm, toDeleteKeyed0.values.toList) - // A helper function to merge child nodes that were patched or created + // A recursive helper function to merge child nodes that were patched or created // in the first fold, and those new key'ed child nodes that were // matched up with old key'ed children. The result is the // list of all patched children in reverse order. - def merge( + @tailrec def merge( v1: List[(PatchedVNode, Int)], v2: List[(PatchedVNode, Int)], acc: List[PatchedVNode] @@ -416,11 +414,13 @@ object init { vnode00: VNode, insertedVNodeQueue: VNodeQueue ): PatchedVNode = { - val hook = vnode00.data.hook - val vnode0 = - hook.flatMap(_.prepatch).fold(vnode00)(hook => hook(oldVnode, vnode00)) + + // apply prepatch hooks + val vnode0 = vnode00.data.hook + .flatMap(_.prepatch) + .fold(vnode00)(_(oldVnode, vnode00)) + val elm = oldVnode.elm - val oldCh = oldVnode.children if (vnode0 == oldVnode.toVNode) { @@ -439,7 +439,7 @@ object init { val patchedVNode = vnode.text match { case None => - (oldCh, vnode.children) match { + (oldVnode.children, vnode.children) match { case (oldCh @ _ :: _, ch @ _ :: _) => if (oldCh.map(_.toVNode) != ch) { PatchedVNode( @@ -483,7 +483,7 @@ object init { None ) - case (_ :: _, Nil) => + case (oldCh @ _ :: _, Nil) => removeAllVnodes(elm, oldCh) PatchedVNode( vnode.sel, @@ -507,7 +507,7 @@ object init { ) } case Some(text) if oldVnode.text.forall(_ != text) => - removeAllVnodes(elm, oldCh) + removeAllVnodes(elm, oldVnode.children) api.setTextContent(elm, Some(text)) PatchedVNode( vnode.sel, @@ -531,12 +531,11 @@ object init { } val afterModules = cbs.postPatch.foldLeft(patchedVNode) { - case (vnode, hook) => - hook(oldVnode, vnode) + case (vnode, hook) => hook(oldVnode, vnode) } patchedVNode.data.hook .flatMap(_.postpatch) - .fold(afterModules)(hook => hook(oldVnode, afterModules)) + .fold(afterModules)(_(oldVnode, afterModules)) } @@ -546,6 +545,7 @@ object init { val insertedVNodeQueue: VNodeQueue = mutable.ArrayBuffer.empty[PatchedVNode] + cbs.pre.foreach(_()) val vnode0 = if (sameVnode(oldVnode, vnode)) { From 4d8d312803aef8db9ca060256493b12bff85edfe Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sat, 28 May 2022 20:48:28 +0200 Subject: [PATCH 25/35] fix compiler warning --- snabbdom/src/main/scala/snabbdom/modules/Props.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snabbdom/src/main/scala/snabbdom/modules/Props.scala b/snabbdom/src/main/scala/snabbdom/modules/Props.scala index 269906a..5099a26 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Props.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Props.scala @@ -74,7 +74,7 @@ object Props { } } - oldProps.foreach { case (key, cur) => + oldProps.foreach { case (key, _) => if (!props.contains(key)) { elm.asInstanceOf[js.Dictionary[Any]] -= key } From c88bd6eda10a1d84a1fe4b74d4eba6d6b5652c8f Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sun, 29 May 2022 11:47:34 +0200 Subject: [PATCH 26/35] add another micro benchmark --- benchmarks/index.js | 109 ++++++++++-------- .../snabbdom/benchmarks/Benchmarks.scala | 66 +++++++++++ 2 files changed, 126 insertions(+), 49 deletions(-) diff --git a/benchmarks/index.js b/benchmarks/index.js index 24188f0..81c56f4 100644 --- a/benchmarks/index.js +++ b/benchmarks/index.js @@ -9,66 +9,77 @@ const patch = init([ datasetModule ]); -document.addEventListener("DOMContentLoaded", function() { - - const container = document.getElementById("container"); - - const n = 10000 - const runs = 10 +function benchmark1(root) { + + let vnode0 = h("div", {}) + let vnode1 = h("div", {}, [h("span", "1"), h("span", "2"), h("span", "3")]); + let vnode2 = h("div", {}, [h("span", "2"), h("span", "3")]); + let vnode3 = h("div", {}, [h("span", "3")]); + let vnode4 = h("div", {}, [h("span", "2"), h("span", "3")]); + let vnode5 = h("div", {}, [h("span", "1"), h("span", "2"), h("span", "3")]); + let vnode6 = h("div", {}, [h("span", "0"), h("span", "1"), h("span", "2"), h("span", "3")]); + let vnode7 = h("div", {}, [h("span", "0"), h("span", "1"), h("span", "2"), h("span", "3"), h("span", "4")]); + + patch(root, vnode0); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + patch(vnode2, vnode3); + patch(vnode3, vnode4); + patch(vnode4, vnode5); + patch(vnode5, vnode6); + patch(vnode6, vnode7); - for (let j = 0; j < runs; j++) { - - console.log("running js-snabbdom..."); - let t0 = performance.now() - for (let i = 0; i <= n; i++) { - - container.replaceChildren(); - const root = document.createElement("div"); - container.appendChild(root); - - let vnode0 = h("div", {}) - let vnode1 = h("div", {}, [h("span", "1"), h("span", "2"), h("span", "3")]); - let vnode2 = h("div", {}, [h("span", "2"), h("span", "3")]); - let vnode3 = h("div", {}, [h("span", "3")]); - let vnode4 = h("div", {}, [h("span", "2"), h("span", "3")]); - let vnode5 = h("div", {}, [h("span", "1"), h("span", "2"), h("span", "3")]); - let vnode6 = h("div", {}, [h("span", "0"), h("span", "1"), h("span", "2"), h("span", "3")]); - let vnode7 = h("div", {}, [h("span", "0"), h("span", "1"), h("span", "2"), h("span", "3"), h("span", "4")]); +} - patch(root, vnode0); - patch(vnode0, vnode1); - patch(vnode1, vnode2); - patch(vnode2, vnode3); - patch(vnode3, vnode4); - patch(vnode4, vnode5); - patch(vnode5, vnode6); - patch(vnode6, vnode7); +function benchmark2(root) { + + let vnode0 = h("div", {}) + let vnode1 = h("div", {}, [h("span", { key: "1" }, "1"), h("span", { key: "2" }, "2"), h("span", { key: "3" }, "3")]); + let vnode2 = h("div", {}, [h("span", { key: "2" }, "2a"), h("span", { key: "3" }, "3a")]); + let vnode3 = h("div", {}, [h("span", { key: "3" }, "3b")]); + let vnode4 = h("div", {}, [h("span", { key: "2" }, "2c"), h("span", { key: "3" }, "3c")]); + let vnode5 = h("div", {}, [h("span", { key: "1" }, "1d"), h("span", { key: "2" }, "2d"), h("span", { key: "3" }, "3d"), h("span", "b")]); + let vnode6 = h("div", {}, [h("span", { key: "1" }, "1e"), h("span", { key: "3" }, "3e"), h("span", { key: "2" }, "2e")]); + let vnode7 = h("div", {}, [h("span", "a"), h("span", "b"), h("span", { key: "1" }, "1f"), h("span", { key: "2f" }, "2"), h("span", { key: "3f" }, "3")]); + let vnode8 = h("div", {}, [h("span", { key: "1" }, "1g"), h("span", { key: "2" }, "2g"), h("span", { key: "3" }, "3g"), h("span", { key: "4" }, "4g")]); + + patch(root, vnode0); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + patch(vnode2, vnode3); + patch(vnode3, vnode4); + patch(vnode4, vnode5); + patch(vnode5, vnode6); + patch(vnode6, vnode7); + patch(vnode7, vnode8); - } +} - let t1 = performance.now() - console.log(`done: ${t1 - t0} ms`) +function run(name, fn, nRuns) { + const container = document.getElementById("container"); + console.log(`running ${name}...`); + const t0 = performance.now() + for (let i = 0; i <= nRuns; i++) { + const root = document.createElement("div"); + container.appendChild(root); + fn(root); + container.replaceChildren(); } + const t1 = performance.now() + console.log(`done: ${t1 - t0} ms`) +} - for (let j = 0; j < runs; j++) { - - console.log("running scala-js-snabbdom..."); - let t0 = performance.now() - for (let i = 0; i <= n; i++) { - - container.replaceChildren(); - const root = document.createElement("div"); - container.appendChild(root); +document.addEventListener("DOMContentLoaded", function() { - SnabbdomBenchmarks.benchmark1(root); + const n = 10000 // number of runs - } - let t1 = performance.now() - console.log(`done: ${t1 - t0} ms`) + run("snabbdom - benchmark1", benchmark1, n) + run("sjs-snabbdom - benchmark1", (root) => SnabbdomBenchmarks.benchmark1(root), n) + run("snabbdom - benchmark2", benchmark2, n) + run("sjs-snabbdom - benchmark2", (root) => SnabbdomBenchmarks.benchmark2(root), n) - } } ) diff --git a/benchmarks/src/main/scala/snabbdom/benchmarks/Benchmarks.scala b/benchmarks/src/main/scala/snabbdom/benchmarks/Benchmarks.scala index 3e3af26..7e8fb03 100644 --- a/benchmarks/src/main/scala/snabbdom/benchmarks/Benchmarks.scala +++ b/benchmarks/src/main/scala/snabbdom/benchmarks/Benchmarks.scala @@ -70,4 +70,70 @@ object Benchmarks { } + @JSExport + def benchmark2(container: dom.Element): Unit = { + + def key(k: String) = VNodeData(key = Some(k)) + + val vnode0p = patch(container, h("div")) + + val vnode1 = h( + "div", + List( + h("span", key("1"), "1"), + h("span", key("2"), "2"), + h("span", key("3"), "3") + ) + ) + val vnode2 = + h("div", List(h("span", key("2"), "2a"), h("span", key("3"), "3a"))) + val vnode3 = h("div", List(h("span", key("3"), "3b"))) + val vnode4 = + h("div", List(h("span", key("2"), "2c"), h("span", key("3"), "3c"))) + val vnode5 = h( + "div", + List( + h("span", key("1"), "1d"), + h("span", key("2"), "2d"), + h("span", key("3"), "3d"), + h("span", "b") + ) + ) + val vnode6 = h( + "div", + List( + h("span", key("1"), "1e"), + h("span", key("3"), "3e"), + h("span", key("2"), "2e") + ) + ) + val vnode7 = h( + "div", + List( + h("span", "a"), + h("span", "b"), + h("span", key("1"), "1f"), + h("span", key("2"), "2f"), + h("span", key("3"), "3f") + ) + ) + val vnode8 = h( + "div", + List( + h("span", key("1"), "1g"), + h("span", key("2"), "2g"), + h("span", key("3"), "3g"), + h("span", key("4"), "4g") + ) + ) + + List(vnode1, vnode2, vnode3, vnode4, vnode5, vnode6, vnode7, vnode8) + .foldLeft( + vnode0p + ) { case (acc, vnode) => patch(acc, vnode) } + + () + + } + } From b5021aae1f3d5563b449489857baa33168d908f7 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sun, 29 May 2022 11:59:38 +0200 Subject: [PATCH 27/35] clean up --- .../test/scala/snabbdom/SnabbdomSuite.scala | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index f01c4f7..ea642b7 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -120,21 +120,6 @@ class SnabbdomSuite extends BaseSuite { assertEquals(vnode.text, Some("b")) } - // TODO: do these make sense even in Scala.js? - - // test("can create vnode with Number obj content") { - // val vnode = h("a", new Number(1)) - // assertEquals(vnode.text, "1") - // } - - // test("can create vnode with null props") { - // var vnode = h("a") - // assertEquals(vnode.data, None) - // vnode = h("a", null, List(VNode.text("I am a string"))) - // val children = vnode.children - // assertEquals(children.flatMap(_(0).text), Some("I am a string")) - // } - test("can create vnode for comment") { val vnode = h("!", "test") assertEquals(vnode.sel, Some("!")) @@ -1043,8 +1028,8 @@ class SnabbdomSuite extends BaseSuite { _ <- 0 until 1000 } yield { - val n1 = rng.nextInt(100) - val n2 = rng.nextInt(100) + val n1 = rng.nextInt(10) + val n2 = rng.nextInt(10) var counter = 0 From 70336122d375ed105b0b66835d71770c77937ba2 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sun, 29 May 2022 17:07:49 +0200 Subject: [PATCH 28/35] add scalacheck-based test; reusing same sequence of keys is *not* working --- build.sbt | 4 +- .../test/scala/snabbdom/SnabbdomSuite.scala | 119 ++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 1759b02..cca58b6 100644 --- a/build.sbt +++ b/build.sbt @@ -38,6 +38,7 @@ ThisBuild / Test / jsEnv := { lazy val scalajsDomVersion = "2.1.0" lazy val munitVersion = "0.7.29" +lazy val scalacheckVersion = "1.16.0" lazy val root = tlCrossRootProject.aggregate(snabbdom, examples, benchmarks) @@ -48,7 +49,8 @@ lazy val snabbdom = (project name := "scala-js-snabbdom", libraryDependencies ++= Seq( "org.scala-js" %%% "scalajs-dom" % scalajsDomVersion, - "org.scalameta" %%% "munit" % munitVersion % Test + "org.scalameta" %%% "munit" % munitVersion % Test, + "org.scalacheck" %%% "scalacheck" % scalacheckVersion % Test ) ) diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index ea642b7..b2de1b3 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -44,8 +44,81 @@ import org.scalajs.dom import scalajs.js import scala.collection.mutable.ListBuffer +import org.scalacheck.Arbitrary +import org.scalacheck.Gen +import org.scalacheck.Gen.lzy + class SnabbdomSuite extends BaseSuite { + // used only for key generation to ensure that the same + // sequence of keys is used for different vnodes + val keyRng = new scala.util.Random(0) + + val genClasses: Gen[Map[String, Boolean]] = Gen.mapOfN( + 3, + for { + n <- Gen.choose(5, 10) + name <- Gen.stringOfN(n, Gen.alphaChar) + value <- Gen.oneOf(true, false) + } yield (name, value) + ) + + val genProps: Gen[Map[String, String]] = Gen.mapOfN( + 3, + for { + name <- Gen.stringOfN(10, Gen.alphaChar) + value <- Gen.stringOfN(10, Gen.alphaChar) + } yield (name, value) + ) + + val genVNodeData: Gen[VNodeData] = for { + key <- Gen.frequency( + (1 -> Gen.const(None)), + (1 -> Gen.some( + Gen.uuid + .withPerturb( + _.reseed(keyRng.nextLong()) + ) // this does not seem to work! + .map(_.toString.take(8)) + )) + ) + } yield VNodeData(key = key) + + val genLeaf: Gen[VNode] = for { + n <- Gen.choose(3, 20) + text <- Gen.stringOfN(n, Gen.alphaChar) + data <- genVNodeData + } yield h("span", data, text) + + val genTree: Gen[VNode] = for { + children <- Gen.listOfN(8, genVNodePre) + data <- genVNodeData + } yield h("div", data, children) + + // Dies out fast enough not to explode, but has 10-1 odds of being just a leaf. + // We therefore define `genVNode` by conditioning on `size(vnode) > 1`. + def genVNodePre: Gen[VNode] = + Gen.frequency((10, genLeaf), (1, lzy(genTree))) + + // We reset the key rng after every evaluation so that + // successive vnodes will use the same sequence of keys + implicit val genVNode = + genVNodePre + .flatMap { vnode => + Gen.delay { keyRng.setSeed(0); Gen.const(vnode) } + } + .retryUntil(vnode => size(vnode) > 1) + + implicit val arbVNode: Arbitrary[VNode] = Arbitrary(genVNode) + + def size(vnode: VNode): Long = 1 + vnode.children.map(size).sum + + def keys(vnode: VNode): Set[String] = + vnode.key.toSet.union(vnode.children.map(keys).toList.flatten.toSet) + + def depth(vnode: VNode): Long = + 1 + vnode.children.map(depth).maxOption.getOrElse(0L) + def spanNum(s: String) = h("span", VNodeData(), s) def spanNum(i: Int) = h("span", VNodeData(key = Some(i.toString)), i.toString) @@ -1896,4 +1969,50 @@ class SnabbdomSuite extends BaseSuite { } } + group("patching of random sequence of vnodes") { + + // for reproducability, we want deterministic tests + scala.util.Random.setSeed(0) + keyRng.setSeed(0) + + val nodesGen = for { + n <- Gen.choose(2, 10) + vnodes <- Gen.listOfN(n, genVNode) + } yield vnodes + + test("results in correct innerHTML") { + + for { + _ <- 0 until 1000 + vnodes <- nodesGen.sample + } { + + // println(s"iteration: $i") + // println(s"# patches: ${vnodes.length}") + // println(s"vnode sizes: ${vnodes.map(vnode => size(vnode))}") + // println(keys(vnodes.head)) + // println(keys(vnodes.tail.head)) + // println() + + val elm = dom.document.createElement("div") + val vnode = vnodes.tail.foldLeft(patch(elm, vnodes.head)) { + case (pvnode, vnode) => patch(pvnode, vnode) + } + + val refElm = patch(dom.document.createElement("div"), vnodes.last).elm + .asInstanceOf[dom.Element] + + // NOTE: Comparing `innerHTML` only works in the absence of + // classes b/c `class=""` is semantically the same as not + // having a `class` attribute at all. + assertEquals( + vnode.elm.asInstanceOf[dom.Element].innerHTML, + refElm.innerHTML + ) + + } + } + + } + } From 6611d18c428e36f7b8413929ba8cb7663d2b361c Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sun, 29 May 2022 22:16:05 +0200 Subject: [PATCH 29/35] fix key handling in scalacheck test case generation - and boom it fails! --- .../test/scala/snabbdom/SnabbdomSuite.scala | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index b2de1b3..68f857e 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -47,6 +47,7 @@ import scala.collection.mutable.ListBuffer import org.scalacheck.Arbitrary import org.scalacheck.Gen import org.scalacheck.Gen.lzy +import org.scalacheck.rng.Seed class SnabbdomSuite extends BaseSuite { @@ -71,18 +72,24 @@ class SnabbdomSuite extends BaseSuite { } yield (name, value) ) - val genVNodeData: Gen[VNodeData] = for { - key <- Gen.frequency( - (1 -> Gen.const(None)), - (1 -> Gen.some( - Gen.uuid - .withPerturb( - _.reseed(keyRng.nextLong()) - ) // this does not seem to work! - .map(_.toString.take(8)) - )) - ) - } yield VNodeData(key = key) + val genVNodeData: Gen[VNodeData] = { + // dirty hack to allow temporary switching of rngs + var globalSeed: Seed = null + for { + key <- Gen + .frequency( + (1 -> Gen.const(None)), + (1 -> Gen.some(Gen.uuid.map(_.toString.take(8)))) + ) + .withPerturb { seed => + globalSeed = seed + Seed(keyRng.nextLong()) // use seed driven by key rng + } + _ <- Gen.const(()).withPerturb(_ => globalSeed) // back to global seed + } yield { + VNodeData(key = key) + } + } val genLeaf: Gen[VNode] = for { n <- Gen.choose(3, 20) @@ -1971,28 +1978,28 @@ class SnabbdomSuite extends BaseSuite { group("patching of random sequence of vnodes") { - // for reproducability, we want deterministic tests - scala.util.Random.setSeed(0) - keyRng.setSeed(0) - val nodesGen = for { n <- Gen.choose(2, 10) vnodes <- Gen.listOfN(n, genVNode) } yield vnodes - test("results in correct innerHTML") { + test("results in correct innerHTML".only) { + + // for reproducability, we want deterministic tests + scala.util.Random.setSeed(0) + keyRng.setSeed(0) for { - _ <- 0 until 1000 + i <- 0 until 1000 vnodes <- nodesGen.sample } { - // println(s"iteration: $i") - // println(s"# patches: ${vnodes.length}") - // println(s"vnode sizes: ${vnodes.map(vnode => size(vnode))}") + println(s"iteration: $i") + println(s"# patches: ${vnodes.length}") + println(s"vnode sizes: ${vnodes.map(vnode => size(vnode))}") // println(keys(vnodes.head)) // println(keys(vnodes.tail.head)) - // println() + println() val elm = dom.document.createElement("div") val vnode = vnodes.tail.foldLeft(patch(elm, vnodes.head)) { @@ -2002,6 +2009,12 @@ class SnabbdomSuite extends BaseSuite { val refElm = patch(dom.document.createElement("div"), vnodes.last).elm .asInstanceOf[dom.Element] + if (vnode.elm.asInstanceOf[dom.Element].innerHTML != refElm.innerHTML) { + println(vnode.elm.asInstanceOf[dom.Element].innerHTML) + println("-----------------------------------------") + println(refElm.innerHTML) + } + // NOTE: Comparing `innerHTML` only works in the absence of // classes b/c `class=""` is semantically the same as not // having a `class` attribute at all. From 761d649b1c1fa2b63e2c60f1c987cd8963402d8f Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sun, 29 May 2022 22:33:56 +0200 Subject: [PATCH 30/35] fixes bug in `updateChildren` --- snabbdom/src/main/scala/snabbdom/init.scala | 6 ++--- .../test/scala/snabbdom/SnabbdomSuite.scala | 22 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index e328fbd..b794455 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -362,13 +362,13 @@ object init { (List.empty[(PatchedVNode, Int)], oldKeyed) ) { case ((acc1, acc2), (vnode, i)) => acc2.get(vnode.key.get) match { - case None => - ((createElm(vnode, insertedVnodeQueue), i) :: acc1, acc2) - case Some(pvnode) => + case Some(pvnode) if sameVnode(pvnode, vnode) => ( (patchVnode(pvnode, vnode, insertedVnodeQueue), i) :: acc1, acc2 - vnode.key.get ) + case _ => + ((createElm(vnode, insertedVnodeQueue), i) :: acc1, acc2) } } diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index 68f857e..7a56724 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -1983,23 +1983,23 @@ class SnabbdomSuite extends BaseSuite { vnodes <- Gen.listOfN(n, genVNode) } yield vnodes - test("results in correct innerHTML".only) { + test("results in correct innerHTML") { // for reproducability, we want deterministic tests scala.util.Random.setSeed(0) keyRng.setSeed(0) for { - i <- 0 until 1000 + _ <- 0 until 1000 vnodes <- nodesGen.sample } { - println(s"iteration: $i") - println(s"# patches: ${vnodes.length}") - println(s"vnode sizes: ${vnodes.map(vnode => size(vnode))}") + // println(s"iteration: $i") + // println(s"# patches: ${vnodes.length}") + // println(s"vnode sizes: ${vnodes.map(vnode => size(vnode))}") // println(keys(vnodes.head)) // println(keys(vnodes.tail.head)) - println() + // println() val elm = dom.document.createElement("div") val vnode = vnodes.tail.foldLeft(patch(elm, vnodes.head)) { @@ -2009,11 +2009,11 @@ class SnabbdomSuite extends BaseSuite { val refElm = patch(dom.document.createElement("div"), vnodes.last).elm .asInstanceOf[dom.Element] - if (vnode.elm.asInstanceOf[dom.Element].innerHTML != refElm.innerHTML) { - println(vnode.elm.asInstanceOf[dom.Element].innerHTML) - println("-----------------------------------------") - println(refElm.innerHTML) - } + // if (vnode.elm.asInstanceOf[dom.Element].innerHTML != refElm.innerHTML) { + // println(vnode.elm.asInstanceOf[dom.Element].innerHTML) + // println("-----------------------------------------") + // println(refElm.innerHTML) + // } // NOTE: Comparing `innerHTML` only works in the absence of // classes b/c `class=""` is semantically the same as not From e8720eb555d8ecb160463cdd4c929a1f66f0a051 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sun, 29 May 2022 22:43:57 +0200 Subject: [PATCH 31/35] fix Scala 3 compilation error --- snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index 7a56724..d52a4b3 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -44,7 +44,6 @@ import org.scalajs.dom import scalajs.js import scala.collection.mutable.ListBuffer -import org.scalacheck.Arbitrary import org.scalacheck.Gen import org.scalacheck.Gen.lzy import org.scalacheck.rng.Seed @@ -109,15 +108,13 @@ class SnabbdomSuite extends BaseSuite { // We reset the key rng after every evaluation so that // successive vnodes will use the same sequence of keys - implicit val genVNode = + val genVNode: Gen[VNode] = genVNodePre .flatMap { vnode => Gen.delay { keyRng.setSeed(0); Gen.const(vnode) } } .retryUntil(vnode => size(vnode) > 1) - implicit val arbVNode: Arbitrary[VNode] = Arbitrary(genVNode) - def size(vnode: VNode): Long = 1 + vnode.children.map(size).sum def keys(vnode: VNode): Set[String] = From 0d8794483cdb30b19fcb683fc766f85bc008abf7 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sun, 5 Jun 2022 15:13:26 +0200 Subject: [PATCH 32/35] expand scalacheck-based tests --- .../main/scala/snabbdom/modules/Styles.scala | 24 +- .../test/scala/snabbdom/SnabbdomSuite.scala | 250 +++++++++++++----- 2 files changed, 197 insertions(+), 77 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/modules/Styles.scala b/snabbdom/src/main/scala/snabbdom/modules/Styles.scala index 4be8519..ae799a0 100644 --- a/snabbdom/src/main/scala/snabbdom/modules/Styles.scala +++ b/snabbdom/src/main/scala/snabbdom/modules/Styles.scala @@ -60,7 +60,7 @@ object Styles { private def setStyle(vnode: PatchedVNode): Unit = { vnode.data.style.foreach { case (name, cur) => - if (name(0) == '-' && name(1) == '-') { + if (name.startsWith("--")) { vnode.elm.asInstanceOf[dom.HTMLElement].style.setProperty(name, cur) } else { vnode.elm @@ -79,23 +79,21 @@ object Styles { val style = vnode.data.style oldStyle.foreach { case (name, _) => - style.get(name) match { - case Some(_) => - case None => - if (name(0) == '-' && name(1) == '-') { - elm.asInstanceOf[dom.HTMLElement].style.removeProperty(name) - } else { - elm - .asInstanceOf[dom.HTMLElement] - .style - .asInstanceOf[js.Dictionary[String]](name) = "" - } + if (!style.contains(name)) { + if (name.startsWith("--")) { + elm.asInstanceOf[dom.HTMLElement].style.removeProperty(name) + } else { + elm + .asInstanceOf[dom.HTMLElement] + .style + .asInstanceOf[js.Dictionary[String]](name) = "" + } } } style.foreach { case (name, cur) => if (oldStyle.get(name).forall(_ != cur)) { - if (name(0) == '-' && name(1) == '-') { + if (name.startsWith("--")) { elm.asInstanceOf[dom.HTMLElement].style.setProperty(name, cur) } else { elm diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index d52a4b3..68fbe6a 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -54,65 +54,149 @@ class SnabbdomSuite extends BaseSuite { // sequence of keys is used for different vnodes val keyRng = new scala.util.Random(0) - val genClasses: Gen[Map[String, Boolean]] = Gen.mapOfN( - 3, - for { - n <- Gen.choose(5, 10) - name <- Gen.stringOfN(n, Gen.alphaChar) - value <- Gen.oneOf(true, false) - } yield (name, value) + case class VNodeGenConfig( + keys: Boolean, + props: Boolean, + attrs: Boolean, + classes: Boolean, + style: Boolean, + dataset: Boolean ) - val genProps: Gen[Map[String, String]] = Gen.mapOfN( - 3, - for { - name <- Gen.stringOfN(10, Gen.alphaChar) - value <- Gen.stringOfN(10, Gen.alphaChar) - } yield (name, value) + val genClasses: Gen[Map[String, Boolean]] = Gen.choose(0, 3).flatMap { n => + Gen.mapOfN( + n, + for { + n <- Gen.choose(4, 8) + name <- Gen.stringOfN(n, Gen.alphaChar) + value <- Gen.oneOf(true, false) + } yield (name, value) + ) + } + + val genProps: Gen[Map[String, String]] = Gen.choose(0, 3).flatMap { n => + Gen.mapOfN( + n, + for { + n <- Gen.choose(4, 8) + name <- Gen.stringOfN(n, Gen.alphaChar) + k <- Gen.choose(4, 8) + value <- Gen.stringOfN(k, Gen.alphaChar) + } yield (name, value) + ) + } + + val genAttrs = genProps + val genDataset = genProps + + val cssProps = Map( + "fontWeight" -> List("normal", "bold"), + "background" -> List("green", "red"), + "border" -> List("solid", "2px dotted", "medium dashed green"), + "padding" -> List("1em", "5% 10%", "1em 2em 2em", "5px 1em 0 2em"), + // some CSS custom properties + "--first-color" -> List("#16f", "#ff7", "#290"), + "--second-color" -> List("#16f", "#ff7", "#290") ) - val genVNodeData: Gen[VNodeData] = { + val genStyle: Gen[Map[String, String]] = Gen.choose(0, 3).flatMap { n => + Gen.mapOfN( + n, + Gen.oneOf(cssProps).flatMap { case (key, values) => + Gen.oneOf(values).map(key -> _) + } + ) + } + + def genVNodeData(implicit config: VNodeGenConfig): Gen[VNodeData] = { // dirty hack to allow temporary switching of rngs var globalSeed: Seed = null for { - key <- Gen - .frequency( - (1 -> Gen.const(None)), - (1 -> Gen.some(Gen.uuid.map(_.toString.take(8)))) - ) - .withPerturb { seed => - globalSeed = seed - Seed(keyRng.nextLong()) // use seed driven by key rng - } + _ <- Gen.const(()).withPerturb { seed => + globalSeed = seed + Seed(keyRng.nextLong()) // use seed driven by key rng + } + key <- + if (config.keys) + Gen + .frequency( + (1 -> Gen.const(None)), + (1 -> Gen.some(Gen.uuid.map(_.toString.take(8)))) + ) + else Gen.const(None) + props <- + if (config.props) genProps else Gen.const(Map.empty[String, String]) + attrs <- + if (config.attrs) genAttrs else Gen.const(Map.empty[String, String]) + classes <- + if (config.classes) genClasses + else Gen.const(Map.empty[String, Boolean]) + style <- + if (config.style) genStyle else Gen.const(Map.empty[String, String]) + dataset <- + if (config.dataset) genDataset else Gen.const(Map.empty[String, String]) _ <- Gen.const(()).withPerturb(_ => globalSeed) // back to global seed } yield { - VNodeData(key = key) + VNodeData( + key = key, + attrs = attrs, + props = props, + classes = classes, + style = style, + dataset = dataset + ) } } - val genLeaf: Gen[VNode] = for { - n <- Gen.choose(3, 20) - text <- Gen.stringOfN(n, Gen.alphaChar) - data <- genVNodeData - } yield h("span", data, text) + val flowContent = + Set("div", "a", "h1", "h2", "h3", "h4", "span", "select", "button", "input") + val phrasingContent = Set("span", "button", "input", "cite") + val allContent = flowContent ++ phrasingContent + val contentModel = Map( + "div" -> flowContent, + "a" -> allContent, + "h1" -> phrasingContent, + "h2" -> phrasingContent, + "h3" -> phrasingContent, + "h4" -> phrasingContent, + "span" -> phrasingContent, + "button" -> phrasingContent, + "cite" -> phrasingContent, + "select" -> Set("option"), + "input" -> Set.empty[String], + "option" -> Set.empty[String] + ) - val genTree: Gen[VNode] = for { - children <- Gen.listOfN(8, genVNodePre) - data <- genVNodeData - } yield h("div", data, children) + def genLeaf(tags: Set[String])(implicit config: VNodeGenConfig): Gen[VNode] = + for { + n <- Gen.choose(3, 20) + tag <- Gen.oneOf(tags) + text <- Gen.stringOfN(n, Gen.alphaChar) + data <- genVNodeData + } yield h(tag, data, text) - // Dies out fast enough not to explode, but has 10-1 odds of being just a leaf. - // We therefore define `genVNode` by conditioning on `size(vnode) > 1`. - def genVNodePre: Gen[VNode] = - Gen.frequency((10, genLeaf), (1, lzy(genTree))) + def genTree(tags: Set[String])(implicit config: VNodeGenConfig): Gen[VNode] = + for { + n <- Gen.choose(1, 10) + tag <- Gen.oneOf(tags) + childTags = contentModel(tag) + children <- + if (childTags.nonEmpty) Gen.listOfN(n, genVNodePre(childTags)) + else Gen.const(Nil) + data <- genVNodeData + } yield h(tag, data, children) + + // Dies out fast enough not to explode, but has 5-1 odds of being just a leaf. + // We therefore define `genVNode` by conditioning this generator on `size(vnode) > 1`. + def genVNodePre(tags: Set[String])(implicit + config: VNodeGenConfig + ): Gen[VNode] = Gen.frequency((5, genLeaf(tags)), (1, lzy(genTree(tags)))) // We reset the key rng after every evaluation so that // successive vnodes will use the same sequence of keys - val genVNode: Gen[VNode] = - genVNodePre - .flatMap { vnode => - Gen.delay { keyRng.setSeed(0); Gen.const(vnode) } - } + def genVNode(implicit config: VNodeGenConfig): Gen[VNode] = + genVNodePre(allContent) + .flatMap { vnode => Gen.delay { keyRng.setSeed(0); Gen.const(vnode) } } .retryUntil(vnode => size(vnode) > 1) def size(vnode: VNode): Long = 1 + vnode.children.map(size).sum @@ -1975,29 +2059,35 @@ class SnabbdomSuite extends BaseSuite { group("patching of random sequence of vnodes") { - val nodesGen = for { - n <- Gen.choose(2, 10) - vnodes <- Gen.listOfN(n, genVNode) - } yield vnodes - test("results in correct innerHTML") { - // for reproducability, we want deterministic tests + // for reproducibility, we want deterministic tests scala.util.Random.setSeed(0) keyRng.setSeed(0) + // NOTE: Comparing `innerHTML` only works in the absence of + // classes and styles b/c `class=""` is semantically the same as not + // having a `class` attribute at all, and similarly for `style=""`. + implicit val config: VNodeGenConfig = + VNodeGenConfig( + keys = true, + props = true, + attrs = true, + classes = false, + style = false, + dataset = true + ) + + val nodesGen = for { + n <- Gen.choose(2, 10) + vnodes <- Gen.listOfN(n, genVNode) + } yield vnodes + for { _ <- 0 until 1000 vnodes <- nodesGen.sample } { - // println(s"iteration: $i") - // println(s"# patches: ${vnodes.length}") - // println(s"vnode sizes: ${vnodes.map(vnode => size(vnode))}") - // println(keys(vnodes.head)) - // println(keys(vnodes.tail.head)) - // println() - val elm = dom.document.createElement("div") val vnode = vnodes.tail.foldLeft(patch(elm, vnodes.head)) { case (pvnode, vnode) => patch(pvnode, vnode) @@ -2006,15 +2096,6 @@ class SnabbdomSuite extends BaseSuite { val refElm = patch(dom.document.createElement("div"), vnodes.last).elm .asInstanceOf[dom.Element] - // if (vnode.elm.asInstanceOf[dom.Element].innerHTML != refElm.innerHTML) { - // println(vnode.elm.asInstanceOf[dom.Element].innerHTML) - // println("-----------------------------------------") - // println(refElm.innerHTML) - // } - - // NOTE: Comparing `innerHTML` only works in the absence of - // classes b/c `class=""` is semantically the same as not - // having a `class` attribute at all. assertEquals( vnode.elm.asInstanceOf[dom.Element].innerHTML, refElm.innerHTML @@ -2023,6 +2104,47 @@ class SnabbdomSuite extends BaseSuite { } } + test("results in correct vnode after calling `toVNode`") { + + scala.util.Random.setSeed(0) + keyRng.setSeed(0) + + implicit val config: VNodeGenConfig = + VNodeGenConfig( + keys = true, + props = true, + attrs = true, + classes = true, + style = + false, // doesn't work here b/c adding and removing a CSS property results in an empty-string attribute value + dataset = true + ) + + val nodesGen = for { + n <- Gen.choose(2, 10) + vnodes <- Gen.listOfN(n, genVNode) + } yield vnodes + + for { + _ <- 0 until 1000 + vnodes <- nodesGen.sample + } { + + val elm = dom.document.createElement("div") + val vnode = vnodes.tail.foldLeft(patch(elm, vnodes.head)) { + case (pvnode, vnode) => patch(pvnode, vnode) + } + + val refElm = patch(dom.document.createElement("div"), vnodes.last).elm + + val v1 = toVNode(vnode.elm).toVNode + val v2 = toVNode(refElm).toVNode + + assertEquals(v1, v2) + + } + } + } } From 14e009bfef511a5faccc99640b27ffe40dacc4a2 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Sun, 5 Jun 2022 15:25:51 +0200 Subject: [PATCH 33/35] increase timeout for scalacheck-based tests --- snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index 68fbe6a..559a319 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -43,6 +43,7 @@ import snabbdom.modules._ import org.scalajs.dom import scalajs.js import scala.collection.mutable.ListBuffer +import scala.concurrent.duration._ import org.scalacheck.Gen import org.scalacheck.Gen.lzy @@ -50,6 +51,9 @@ import org.scalacheck.rng.Seed class SnabbdomSuite extends BaseSuite { + // generous timeout for scalacheck-based tests with large number of samples + override val munitTimeout = 5.minutes + // used only for key generation to ensure that the same // sequence of keys is used for different vnodes val keyRng = new scala.util.Random(0) From 7954e24f5fd29d97b523da667729159e12127ea4 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Mon, 6 Jun 2022 09:27:25 +0200 Subject: [PATCH 34/35] remove redundant `key` field --- .../main/scala/snabbdom/PatchedVNode.scala | 9 ++----- snabbdom/src/main/scala/snabbdom/VNode.scala | 16 +++++------ .../src/main/scala/snabbdom/VNodeData.scala | 2 +- snabbdom/src/main/scala/snabbdom/init.scala | 27 ++++++------------- .../src/main/scala/snabbdom/toVNode.scala | 6 ++--- .../test/scala/snabbdom/SnabbdomSuite.scala | 2 +- 6 files changed, 20 insertions(+), 42 deletions(-) diff --git a/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala b/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala index aa464a9..7f14a2a 100644 --- a/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala +++ b/snabbdom/src/main/scala/snabbdom/PatchedVNode.scala @@ -19,23 +19,18 @@ package snabbdom import org.scalajs.dom /** A `VNode` that has been patched into the DOM. */ -case class PatchedVNode private[snabbdom] ( +final case class PatchedVNode private[snabbdom] ( sel: Option[String], data: VNodeData, children: List[PatchedVNode], text: Option[String], - key: Option[KeyValue], elm: dom.Node, // the corresponding node in the DOM - can't be `dom.Element` unfortunately b/c of fragments listener: Option[ Listener ] // this is an optimization that allows re-using event listeners ) { - override def toString: String = - s"PatchedVNode(sel=$sel, data=$data, text=$text, key=$key, children=$children, elm=$elm, listener=$listener)" - - def toVNode: VNode = - VNode(sel, data, children.map(_.toVNode), text, key) + def toVNode: VNode = VNode(sel, data, children.map(_.toVNode), text) private[snabbdom] def isTextNode: Boolean = sel.isEmpty && children.isEmpty && text.isDefined diff --git a/snabbdom/src/main/scala/snabbdom/VNode.scala b/snabbdom/src/main/scala/snabbdom/VNode.scala index e081d1b..0522368 100644 --- a/snabbdom/src/main/scala/snabbdom/VNode.scala +++ b/snabbdom/src/main/scala/snabbdom/VNode.scala @@ -38,17 +38,13 @@ package snabbdom -case class VNode private ( +final case class VNode private ( sel: Option[String], data: VNodeData, children: List[VNode], - text: Option[String], - key: Option[KeyValue] + text: Option[String] ) { - override def toString: String = - s"VNode(sel=$sel, data=$data, text=$text, key=$key, children=$children)" - private[snabbdom] def isTextNode: Boolean = sel.isEmpty && children.isEmpty && text.isDefined @@ -56,17 +52,17 @@ case class VNode private ( object VNode { - val empty = VNode(None, VNodeData.empty, Nil, Some(""), None) + val empty: VNode = VNode(None, VNodeData.empty, Nil, Some("")) def create( sel: Option[String], data: VNodeData, children: List[VNode], text: Option[String] - ) = VNode(sel, data, children, text, data.key) + ): VNode = VNode(sel, data, children, text) - def text(text: String) = - VNode(None, VNodeData.empty, Nil, Some(text), None) + def text(text: String): VNode = + VNode(None, VNodeData.empty, Nil, Some(text)) implicit def fromString(s: String): VNode = text(s) diff --git a/snabbdom/src/main/scala/snabbdom/VNodeData.scala b/snabbdom/src/main/scala/snabbdom/VNodeData.scala index 9550429..23295be 100644 --- a/snabbdom/src/main/scala/snabbdom/VNodeData.scala +++ b/snabbdom/src/main/scala/snabbdom/VNodeData.scala @@ -38,7 +38,7 @@ package snabbdom -case class VNodeData( +final case class VNodeData( props: Map[String, PropValue] = Map.empty, attrs: Map[String, AttrValue] = Map.empty, classes: Map[String, ClassValue] = Map.empty, diff --git a/snabbdom/src/main/scala/snabbdom/init.scala b/snabbdom/src/main/scala/snabbdom/init.scala index b794455..104a275 100644 --- a/snabbdom/src/main/scala/snabbdom/init.scala +++ b/snabbdom/src/main/scala/snabbdom/init.scala @@ -92,7 +92,6 @@ object init { VNodeData.empty, Nil, None, - None, elm, None ) @@ -100,7 +99,7 @@ object init { } def emptyDocumentFragmentAt(frag: dom.DocumentFragment): PatchedVNode = { - PatchedVNode(None, VNodeData.empty, Nil, None, None, frag, None) + PatchedVNode(None, VNodeData.empty, Nil, None, frag, None) } def createRmCb(childElm: dom.Node, listeners: Int): () => Unit = { @@ -132,7 +131,6 @@ object init { vnode.data, Nil, // comment nodes can't have children Some(text), - vnode.key, elm, None ) @@ -158,7 +156,6 @@ object init { vnode.data, children = vnode.children.map(createElm(_, insertedVNodeQueue)), text = vnode.text, - key = vnode.key, elm = elm, None ) @@ -203,7 +200,6 @@ object init { vnode.data, Nil, vnode.text, - vnode.key, api.createTextNode(vnode.text.getOrElse("")), None ) @@ -217,7 +213,6 @@ object init { children = children.map(ch => createElm(ch, insertedVNodeQueue)), text = vnode.text, - key = vnode.key, elm = elm, None ) @@ -311,7 +306,7 @@ object init { } else { (oh :: ot, ot2, (pn, i) :: acc, keyed) } - } else if (newCh.key.isDefined) { // node with key - try to match later + } else if (newCh.data.key.isDefined) { // node with key - try to match later (oh :: ot, oh2 :: ot2, acc, (newCh, i) :: keyed) } else { // new node without key val pn = createElm(newCh, insertedVnodeQueue) @@ -350,9 +345,10 @@ object init { // into those with keys and those without. Those without keys // should certainly be deleted. Those with keys we'll try to match // up against new children with keys - any leftovers will be deleted - val (toDeleteKeyed, toDeleteUnkeyed) = toDelete.partition(_.key.isDefined) + val (toDeleteKeyed, toDeleteUnkeyed) = + toDelete.partition(_.data.key.isDefined) - val oldKeyed = toDeleteKeyed.map(n => (n.key.get -> n)).toMap + val oldKeyed = toDeleteKeyed.map(n => (n.data.key.get -> n)).toMap // Here we match new, unmatched children with keys against // old children with keys. Any leftover old children with @@ -361,11 +357,11 @@ object init { newKeyed.foldLeft( (List.empty[(PatchedVNode, Int)], oldKeyed) ) { case ((acc1, acc2), (vnode, i)) => - acc2.get(vnode.key.get) match { + acc2.get(vnode.data.key.get) match { case Some(pvnode) if sameVnode(pvnode, vnode) => ( (patchVnode(pvnode, vnode, insertedVnodeQueue), i) :: acc1, - acc2 - vnode.key.get + acc2 - vnode.data.key.get ) case _ => ((createElm(vnode, insertedVnodeQueue), i) :: acc1, acc2) @@ -447,7 +443,6 @@ object init { vnode.data, updateChildren(elm, oldCh, ch, insertedVNodeQueue), vnode.text, - vnode.key, elm, None ) @@ -457,7 +452,6 @@ object init { vnode.data, oldCh, vnode.text, - vnode.key, elm, None ) @@ -478,7 +472,6 @@ object init { vnode.data, patchedChildren, vnode.text, - vnode.key, elm, None ) @@ -490,7 +483,6 @@ object init { vnode.data, Nil, vnode.text, - vnode.key, elm, None ) @@ -501,7 +493,6 @@ object init { vnode.data, Nil, vnode.text, - vnode.key, elm, None ) @@ -514,7 +505,6 @@ object init { vnode.data, Nil, vnode.text, - vnode.key, elm, None ) @@ -524,7 +514,6 @@ object init { vnode.data, Nil, vnode.text, - vnode.key, elm, None ) @@ -592,7 +581,7 @@ object init { } private def sameVnode(vnode1: PatchedVNode, vnode2: VNode): Boolean = { - vnode1.key == vnode2.key && + vnode1.data.key == vnode2.data.key && vnode1.data.is == vnode2.data.is && vnode1.sel == vnode2.sel } diff --git a/snabbdom/src/main/scala/snabbdom/toVNode.scala b/snabbdom/src/main/scala/snabbdom/toVNode.scala index 8cba8fb..32cf17d 100644 --- a/snabbdom/src/main/scala/snabbdom/toVNode.scala +++ b/snabbdom/src/main/scala/snabbdom/toVNode.scala @@ -84,7 +84,6 @@ object toVNode { data, children.toList, None, - None, node, None ) @@ -101,7 +100,7 @@ object toVNode { } else if (api.isText(node)) { val text = api.getTextContent(node).getOrElse("") - PatchedVNode(None, VNodeData.empty, Nil, Some(text), None, node, None) + PatchedVNode(None, VNodeData.empty, Nil, Some(text), node, None) } else if (api.isComment(node)) { val text = api.getTextContent(node).getOrElse("") PatchedVNode( @@ -109,12 +108,11 @@ object toVNode { VNodeData.empty, Nil, Some(text), - None, node, None ) } else { - PatchedVNode(Some(""), VNodeData.empty, Nil, None, None, node, None) + PatchedVNode(Some(""), VNodeData.empty, Nil, None, node, None) } } diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index 559a319..56d9f13 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -206,7 +206,7 @@ class SnabbdomSuite extends BaseSuite { def size(vnode: VNode): Long = 1 + vnode.children.map(size).sum def keys(vnode: VNode): Set[String] = - vnode.key.toSet.union(vnode.children.map(keys).toList.flatten.toSet) + vnode.data.key.toSet.union(vnode.children.map(keys).toList.flatten.toSet) def depth(vnode: VNode): Long = 1 + vnode.children.map(depth).maxOption.getOrElse(0L) From 72574ae729296aed698230b846506e50fe3c4e10 Mon Sep 17 00:00:00 2001 From: Christoph Bunte Date: Mon, 6 Jun 2022 10:06:31 +0200 Subject: [PATCH 35/35] introduce async boundaries and macrotask EC to avoid rendering timeouts in long-running tests --- build.sbt | 3 +- .../test/scala/snabbdom/SnabbdomSuite.scala | 72 +++++++++++-------- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/build.sbt b/build.sbt index cca58b6..a65f9c3 100644 --- a/build.sbt +++ b/build.sbt @@ -50,7 +50,8 @@ lazy val snabbdom = (project libraryDependencies ++= Seq( "org.scala-js" %%% "scalajs-dom" % scalajsDomVersion, "org.scalameta" %%% "munit" % munitVersion % Test, - "org.scalacheck" %%% "scalacheck" % scalacheckVersion % Test + "org.scalacheck" %%% "scalacheck" % scalacheckVersion % Test, + "org.scala-js" %%% "scala-js-macrotask-executor" % "1.0.0" % Test ) ) diff --git a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala index 56d9f13..4a89b97 100644 --- a/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala +++ b/snabbdom/src/test/scala/snabbdom/SnabbdomSuite.scala @@ -48,6 +48,7 @@ import scala.concurrent.duration._ import org.scalacheck.Gen import org.scalacheck.Gen.lzy import org.scalacheck.rng.Seed +import scala.concurrent.Future class SnabbdomSuite extends BaseSuite { @@ -2065,7 +2066,9 @@ class SnabbdomSuite extends BaseSuite { test("results in correct innerHTML") { - // for reproducibility, we want deterministic tests + // need macrotask EC to allow rendering between async boundaries to avoid rendering timeouts + import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits._ + scala.util.Random.setSeed(0) keyRng.setSeed(0) @@ -2087,29 +2090,34 @@ class SnabbdomSuite extends BaseSuite { vnodes <- Gen.listOfN(n, genVNode) } yield vnodes - for { - _ <- 0 until 1000 - vnodes <- nodesGen.sample - } { - - val elm = dom.document.createElement("div") - val vnode = vnodes.tail.foldLeft(patch(elm, vnodes.head)) { - case (pvnode, vnode) => patch(pvnode, vnode) + Future.sequence { + (0 until 2000).map { _ => + Future(nodesGen.sample).map { + case None => Future.unit + case Some(vnodes) => + val elm = dom.document.createElement("div") + val vnode = vnodes.tail.foldLeft(patch(elm, vnodes.head)) { + case (pvnode, vnode) => patch(pvnode, vnode) + } + + val refElm = + patch(dom.document.createElement("div"), vnodes.last).elm + .asInstanceOf[dom.Element] + + assertEquals( + vnode.elm.asInstanceOf[dom.Element].innerHTML, + refElm.innerHTML + ) + } } - - val refElm = patch(dom.document.createElement("div"), vnodes.last).elm - .asInstanceOf[dom.Element] - - assertEquals( - vnode.elm.asInstanceOf[dom.Element].innerHTML, - refElm.innerHTML - ) - } } test("results in correct vnode after calling `toVNode`") { + // need macrotask EC to allow rendering between async boundaries to avoid rendering timeouts + import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits._ + scala.util.Random.setSeed(0) keyRng.setSeed(0) @@ -2129,23 +2137,27 @@ class SnabbdomSuite extends BaseSuite { vnodes <- Gen.listOfN(n, genVNode) } yield vnodes - for { - _ <- 0 until 1000 - vnodes <- nodesGen.sample - } { + Future.sequence { + (0 until 2000).map { _ => + Future(nodesGen.sample).map { + case None => Future.unit + case Some(vnodes) => + val elm = dom.document.createElement("div") - val elm = dom.document.createElement("div") - val vnode = vnodes.tail.foldLeft(patch(elm, vnodes.head)) { - case (pvnode, vnode) => patch(pvnode, vnode) - } + val vnode = vnodes.tail.foldLeft(patch(elm, vnodes.head)) { + case (pvnode, vnode) => patch(pvnode, vnode) + } - val refElm = patch(dom.document.createElement("div"), vnodes.last).elm + val refElm = + patch(dom.document.createElement("div"), vnodes.last).elm - val v1 = toVNode(vnode.elm).toVNode - val v2 = toVNode(refElm).toVNode + val v1 = toVNode(vnode.elm).toVNode + val v2 = toVNode(refElm).toVNode - assertEquals(v1, v2) + assertEquals(v1, v2) + } + } } }