com-lihaoyi / scalatags

ScalaTags is a small XML/HTML construction library for Scala.
https://com-lihaoyi.github.io/scalatags/
MIT License
758 stars 117 forks source link

Weird behavior when passing rendered scalatags into other scalatags in scala.js #107

Closed tristanbuckner closed 8 years ago

tristanbuckner commented 8 years ago

I was attempting to use a helper method I wrote for inserting a separator between tags which worked fine until I needed the rendered element to pass into JQuery (dom.Element instead of TypedTag[dom.Element]). Although no errors are thrown only the last separator is rendered. Here is a test case showing what I'm talking about.

package client.view

import org.scalajs.dom
import utest._
import utest.framework.Test
import utest.util.Tree

import scala.collection.mutable.ArrayBuffer
import scalatags.JsDom.TypedTag
import scalatags.JsDom.all._

object DivJoinSuite extends TestSuite {

  override def tests: Tree[Test] = TestSuite {
    'JoinTest {
      val divsRendered = List(
        div("content1").render,
        div("content2").render,
        div("content3").render
      ).joinUsing(div("sep").render)

      val divsUnrendered = List(
        div("content1"),
        div("content2"),
        div("content3")
      ).joinUsing(div("sep"))

      val renderedDivs = div(divsRendered).render.outerHTML

      val unrenderedDivs = div(divsUnrendered).render.outerHTML

      println(renderedDivs)
      println(unrenderedDivs)

      assert(renderedDivs == unrenderedDivs)

    }

  }

  implicit class JoinAbleElementsTags(tags: Seq[dom.Element]) {
    def joinUsing(joinTag: dom.Element) = {
      addTags(new ArrayBuffer[dom.Element](tags.length), joinTag).toSeq
    }

    private def addTags(b: ArrayBuffer[dom.Element], sep: dom.Element): ArrayBuffer[dom.Element] = {
      var first = true

      for (x <- this.tags) {
        if (first) {
          b append x
          first = false
        }
        else {
          b append sep
          b append x
        }
      }
      b
    }
  }

  implicit class JoinAbleTypedTags(tags: Seq[TypedTag[dom.Element]]) {
    def joinUsing(joinTag: TypedTag[dom.Element]) = {
      addTags(new ArrayBuffer[TypedTag[dom.Element]](tags.length), joinTag).toSeq
    }

    private def addTags(b: ArrayBuffer[TypedTag[dom.Element]], sep: TypedTag[dom.Element]): ArrayBuffer[TypedTag[dom.Element]] = {
      var first = true

      for (x <- this.tags) {
        if (first) {
          b append x
          first = false
        }
        else {
          b append sep
          b append x
        }
      }
      b
    }
  }
}

Here's everything that might be in the classpath for the client:

"com.lihaoyi" %%% "upickle" % "0.2.8",
"com.lihaoyi" %%% "scalatags" % "0.5.3",
"com.lihaoyi"  %%% "autowire" % "0.2.5",
"org.scala-js" %%% "scalajs-dom" % "0.8.1",
"com.lihaoyi"  %%% "scalarx" % "0.2.8",
"be.doeraene"  %%% "scalajs-jquery" % "0.8.1",
"org.querki" %%% "querki-jsext" % "0.6",
"com.lihaoyi" %%% "utest" % "0.3.1"
lihaoyi commented 8 years ago

You're going to need to minimize this if you want someone to take a look and be able to understand what's happening =)

tristanbuckner commented 8 years ago

Fair enough :) Here it is without the helper functions:

    import scalatags.JsDom.all._
    import utest._
    import utest.framework.Test
    import utest.util.Tree

    object MinimizedSuite extends TestSuite {

      override def tests: Tree[Test] = TestSuite {
        'JoinTest {
          val renderedSep = div("sep").render

          val divsRendered = List(
            div("content1").render,
            renderedSep,
            div("content2").render,
            renderedSep,
            div("content3").render
          )

          val unrenderedSep = div("sep")

          val divsUnrendered = List(
            div("content1"),
            unrenderedSep,
            div("content2"),
            unrenderedSep,
            div("content3")
          )

          val renderedDivs = div(divsRendered).render.outerHTML
          val unrenderedDivs = div(divsUnrendered).render.outerHTML

          assert(renderedDivs == unrenderedDivs)
        }
      }
    }

For some reason only the last rendered separator is visible.

    utest.AssertionError: assert(renderedDivs == unrenderedDivs)
    renderedDivs: String = <div><div>content1</div><div>content2</div><div>sep</div><div>content3</div></div>
    unrenderedDivs: String = <div><div>content1</div><div>sep</div><div>content2</div><div>sep</div><div>content3</div></div>

If you change the vals to defs it works, so I gather render must be stateful and the tag mutable. Is this expected and documented behavior?

lihaoyi commented 8 years ago

Ah yes, render results in a single stateful, mutable tag. This isn't explicitly called out, but is demonstrated in the examples. e.g. http://lihaoyi.github.io/scalatags/#UsingtheDOM

val elem = div.render
assert(elem.children.length == 0)
elem.appendChild(p(1, "wtf", "bbq").render)
assert(elem.children.length == 1)
val pElem = elem.children(0).asInstanceOf[Paragraph]
assert(pElem.childNodes.length == 3)
assert(pElem.textContent == "1wtfbbq")

The fact that I can append stuff to it, or call standard DOM apis on it, suggests that they're normal mutable DOM nodes. And normal mutable DOM nodes, if inserted into the DOM multiple times, behave weirdly.

Kinda odd, but that's the DOM for ya