Skip to content

Commit

Permalink
class(Name) attribute now appends rather than overwrites.
Browse files Browse the repository at this point in the history
Fixes #60
  • Loading branch information
japgolly committed Jan 8, 2015
1 parent f0dd44d commit 9c2f24b
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import japgolly.scalajs.react.{ReactElement, ReactNode, React}

private[vdom] final class Builder {

private[this] var className: js.UndefOr[js.Any] = js.undefined
private[this] var props = new js.Object
private[this] var style = new js.Object
private[this] var children = new js.Array[ReactNode]()

def addClassName(n: js.Any): Unit =
className = className.fold(n)(m => s"$m $n")

def appendChild(c: ReactNode): Unit =
children.push(c)

Expand All @@ -21,9 +25,11 @@ private[vdom] final class Builder {
@inline private[this] def set(o: js.Object, k: String, v: js.Any): Unit =
o.asInstanceOf[js.Dynamic].updateDynamic(k)(v)

@inline private[this] def hasStyle = js.Object.keys(style).length != 0
@inline private[this] def hasStyle: Boolean =
js.Object.keys(style).length != 0

def render(tag: String): ReactElement = {
className.foreach(set(props, "className", _))
if (hasStyle)
set(props, "style", style)
React.createElement(tag, props, children: _*)
Expand Down
20 changes: 13 additions & 7 deletions core/src/main/scala/japgolly/scalajs/react/vdom/Extra.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ object Extra {
}

trait Attrs {
final val className = ClassNameAttr
final val cls = className
final val `class` = className

final val colSpan = "colSpan".attr
final val rowSpan = "rowSpan".attr
final val className = "className".attr // same as cls and `class`
final val htmlFor = "htmlFor".attr // same as `for`
final val ref = "ref".attr
final val key = "key".attr
Expand Down Expand Up @@ -126,19 +129,22 @@ object Extra {
final def compositeAttr[A](k: Attr, f: (A, List[A]) => A, e: => TagMod = EmptyTag) =
new CompositeAttr(k, f, e)

final val classSwitch = compositeAttr[String](className, (h,t) => (h::t) mkString " ")

final def classSet(ps: (String, Boolean)*): TagMod =
classSwitch(ps.map(p => if (p._2) Some(p._1) else None): _*)(stringAttrX)
classSetImpl(EmptyTag, ps)

final def classSet1(a: String, ps: (String, Boolean)*): TagMod =
classSet(((a, true) +: ps):_*)
classSetImpl(cls_=(a), ps)

final def classSetM(ps: Map[String, Boolean]): TagMod =
classSet(ps.toSeq: _*)
classSetImpl(EmptyTag, ps.toSeq)

final def classSet1M(a: String, ps: Map[String, Boolean]): TagMod =
classSet1(a, ps.toSeq: _*)
classSetImpl(cls_=(a), ps.toSeq)
}

@inline private[vdom] def cls_=(v: String): TagMod =
ClassNameAttr.:=(v)(stringAttrX)

private[vdom] def classSetImpl(z: TagMod, ps: Seq[(String, Boolean)]): TagMod =
ps.foldLeft(z)((q, p) => if (p._2) q + cls_=(p._1) else q)
}
28 changes: 14 additions & 14 deletions core/src/main/scala/japgolly/scalajs/react/vdom/HtmlAttrs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -270,20 +270,20 @@ trait HtmlAttrs {
*
*/
final val xmlns = "xmlns".attr
/**
* This attribute is a space-separated list of the classes of the element.
* Classes allows CSS and Javascript to select and access specific elements
* via the class selectors or functions like the DOM method
* document.getElementsByClassName. You can use cls as an alias for this
* attribute so you don't have to backtick-escape this attribute.
*
* MDN
*/
final val `class` = "className".attr
/**
* Shorthand for the `class` attribute
*/
final val cls = `class`
// /**
// * This attribute is a space-separated list of the classes of the element.
// * Classes allows CSS and Javascript to select and access specific elements
// * via the class selectors or functions like the DOM method
// * document.getElementsByClassName. You can use cls as an alias for this
// * attribute so you don't have to backtick-escape this attribute.
// *
// * MDN
// */
// final val `class` = "className".attr
// /**
// * Shorthand for the `class` attribute
// */
// final val cls = `class`
/**
* This attribute participates in defining the language of the element, the
* language that non-editable elements are written in or the language that
Expand Down
21 changes: 13 additions & 8 deletions core/src/main/scala/japgolly/scalajs/react/vdom/Scalatags.scala
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ final case class Attr(name: String) {
def :=[T](v: T)(implicit ev: AttrValue[T]): TagMod = AttrPair(this, v, ev)
}

object ClassNameAttr {
def :=[T](t: T)(implicit av: AttrValue[T]): TagMod = new TagMod {
override def applyTo(b: Builder): Unit =
av.apply(t, b.addClassName)
}
}

/**
* Wraps up a CSS style in a value.
*/
Expand Down Expand Up @@ -110,10 +117,9 @@ private[vdom] object Scalatags {
/**
* An [[Attr]], it's associated value, and an [[AttrValue]] of the correct type
*/
case class AttrPair[T](a: Attr, v: T, ev: AttrValue[T]) extends TagMod {
override def applyTo(t: Builder): Unit = {
ev.apply(t, a, v)
}
case class AttrPair[T](a: Attr, t: T, av: AttrValue[T]) extends TagMod {
override def applyTo(b: Builder): Unit =
av.apply(t, b.addAttr(a.name, _))
}
/**
* Used to specify how to handle a particular type [[T]] when it is used as
Expand All @@ -124,7 +130,7 @@ private[vdom] object Scalatags {
"No AttrValue defined for type ${T}; scalatags does not know how to use ${T} as an attribute"
)
trait AttrValue[T]{
def apply(t: Builder, a: Attr, v: T): Unit
def apply(v: T, b: js.Any => Unit): Unit
}

/**
Expand Down Expand Up @@ -182,7 +188,7 @@ private[vdom] object Scalatags {
}

final class OptionalAttrValue[T[_], A](ot: Optional[T], v: AttrValue[A]) extends AttrValue[T[A]] {
override def apply(b: Builder, s: Attr, t: T[A]) = ot.foreach(t)(v(b, s, _))
override def apply(ta: T[A], b: js.Any => Unit): Unit = ot.foreach(ta)(v(_, b))
}

final class OptionalStyleValue[T[_], A](ot: Optional[T], v: StyleValue[A]) extends StyleValue[T[A]] {
Expand Down Expand Up @@ -211,8 +217,7 @@ private[vdom] object Scalatags {
}

final class GenericAttr[T](f: T => js.Any) extends AttrValue[T] {
def apply(t: Builder, a: Attr, v: T): Unit =
t.addAttr(a.name, f(v))
def apply(v: T, b: js.Any => Unit): Unit = b(f(v))
}
object GenericAttr {
@inline def apply[T <% js.Any] = new GenericAttr[T](a => a)
Expand Down
2 changes: 2 additions & 0 deletions doc/CHANGELOG-0.7.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# 0.7.1 ([commit log](https://github.com/japgolly/scalajs-react/compare/v0.7.0...v0.7.1))

* Support custom tags, attributes and styles via `"string".react{Attr,Style,Tag}`.
* The `class` attribute now gets some special treatment in that it appends rather than overwrites.
`<.div(^.cls := "a", ^.cls := "b")` is now the same as `<.div(^.cls := "a b")`.


# 0.7.0 ([commit log](https://github.com/japgolly/scalajs-react/compare/v0.6.1...v0.7.0))
Expand Down
12 changes: 9 additions & 3 deletions test/src/test/scala/japgolly/scalajs/react/CoreTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ object CoreTest extends TestSuite {

'attributeChaining - test(
div(`class` := "thing lol", id := "cow"),
"""<div class="thing lol" id="cow"></div>""")
"""<div id="cow" class="thing lol"></div>""")

'mixingAttributesStylesAndChildren - test(
div(id := "cow", float.left, p("i am a cow")),
Expand All @@ -154,7 +154,7 @@ object CoreTest extends TestSuite {

'applyChaining - test(
a(tabIndex := 1, cls := "lol")(href := "boo", alt := "g"),
"""<a tabindex="1" class="lol" href="boo" alt="g"></a>""")
"""<a tabindex="1" href="boo" alt="g" class="lol"></a>""")
}

'customAttr - test(div("accept".reactAttr := "yay"), """<div accept="yay"></div>""")
Expand All @@ -168,13 +168,19 @@ object CoreTest extends TestSuite {
r((false, false)) shouldRender """<div>x</div>"""
r((true, false)) shouldRender """<div class="p1">x</div>"""
r((false, true)) shouldRender """<div class="p2">x</div>"""
r((true, true)) shouldRender """<div class="p1 p2">x</div>"""
r((true, true)) shouldRender """<div class="p2 p1">x</div>"""
}
'hasMandatory {
val r = ReactComponentB[Boolean]("C").render(p => div(classSet1("mmm", "ccc" -> p))("x")).build
r(false) shouldRender """<div class="mmm">x</div>"""
r(true) shouldRender """<div class="mmm ccc">x</div>"""
}
'appends {
val r = ReactComponentB[Boolean]("C").render(p =>
div(cls := "neat", classSet1("mmm", "ccc" -> p), cls := "slowclap", "x")).build
r(false) shouldRender """<div class="neat mmm slowclap">x</div>"""
r(true) shouldRender """<div class="neat mmm ccc slowclap">x</div>"""
}
}

'props {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ object PrefixedVdomTest extends TestSuite {

'attributeChaining - test(
<.div(^.`class` := "thing lol", ^.id := "cow"),
"""<div class="thing lol" id="cow"></div>""")
"""<div id="cow" class="thing lol"></div>""")

'mixingAttributesStylesAndChildren - test(
<.div(^.id := "cow", ^.float.left, <.p("i am a cow")),
Expand All @@ -154,7 +154,7 @@ object PrefixedVdomTest extends TestSuite {

'applyChaining - test(
<.a(^.tabIndex := 1, ^.cls := "lol")(^.href := "boo", ^.alt := "g"),
"""<a tabindex="1" class="lol" href="boo" alt="g"></a>""")
"""<a tabindex="1" href="boo" alt="g" class="lol"></a>""")
}
}

Expand All @@ -164,13 +164,19 @@ object PrefixedVdomTest extends TestSuite {
r((false, false)) shouldRender """<div>x</div>"""
r((true, false)) shouldRender """<div class="p1">x</div>"""
r((false, true)) shouldRender """<div class="p2">x</div>"""
r((true, true)) shouldRender """<div class="p1 p2">x</div>"""
r((true, true)) shouldRender """<div class="p2 p1">x</div>"""
}
'hasMandatory {
val r = ReactComponentB[Boolean]("C").render(p => <.div(^.classSet1("mmm", "ccc" -> p))("x")).build
r(false) shouldRender """<div class="mmm">x</div>"""
r(true) shouldRender """<div class="mmm ccc">x</div>"""
}
'appends {
val r = ReactComponentB[Boolean]("C").render(p =>
<.div(^.cls := "neat", ^.classSet1("mmm", "ccc" -> p), ^.cls := "slowclap", "x")).build
r(false) shouldRender """<div class="neat mmm slowclap">x</div>"""
r(true) shouldRender """<div class="neat mmm ccc slowclap">x</div>"""
}
}
}
}

0 comments on commit 9c2f24b

Please sign in to comment.