Skip to content

Commit 9c2f24b

Browse files
committed
class(Name) attribute now appends rather than overwrites.
Fixes #60
1 parent f0dd44d commit 9c2f24b

File tree

7 files changed

+67
-36
lines changed

7 files changed

+67
-36
lines changed

core/src/main/scala/japgolly/scalajs/react/vdom/Builder.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ import japgolly.scalajs.react.{ReactElement, ReactNode, React}
55

66
private[vdom] final class Builder {
77

8+
private[this] var className: js.UndefOr[js.Any] = js.undefined
89
private[this] var props = new js.Object
910
private[this] var style = new js.Object
1011
private[this] var children = new js.Array[ReactNode]()
1112

13+
def addClassName(n: js.Any): Unit =
14+
className = className.fold(n)(m => s"$m $n")
15+
1216
def appendChild(c: ReactNode): Unit =
1317
children.push(c)
1418

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

24-
@inline private[this] def hasStyle = js.Object.keys(style).length != 0
28+
@inline private[this] def hasStyle: Boolean =
29+
js.Object.keys(style).length != 0
2530

2631
def render(tag: String): ReactElement = {
32+
className.foreach(set(props, "className", _))
2733
if (hasStyle)
2834
set(props, "style", style)
2935
React.createElement(tag, props, children: _*)

core/src/main/scala/japgolly/scalajs/react/vdom/Extra.scala

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ object Extra {
4141
}
4242

4343
trait Attrs {
44+
final val className = ClassNameAttr
45+
final val cls = className
46+
final val `class` = className
47+
4448
final val colSpan = "colSpan".attr
4549
final val rowSpan = "rowSpan".attr
46-
final val className = "className".attr // same as cls and `class`
4750
final val htmlFor = "htmlFor".attr // same as `for`
4851
final val ref = "ref".attr
4952
final val key = "key".attr
@@ -126,19 +129,22 @@ object Extra {
126129
final def compositeAttr[A](k: Attr, f: (A, List[A]) => A, e: => TagMod = EmptyTag) =
127130
new CompositeAttr(k, f, e)
128131

129-
final val classSwitch = compositeAttr[String](className, (h,t) => (h::t) mkString " ")
130-
131132
final def classSet(ps: (String, Boolean)*): TagMod =
132-
classSwitch(ps.map(p => if (p._2) Some(p._1) else None): _*)(stringAttrX)
133+
classSetImpl(EmptyTag, ps)
133134

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

137138
final def classSetM(ps: Map[String, Boolean]): TagMod =
138-
classSet(ps.toSeq: _*)
139+
classSetImpl(EmptyTag, ps.toSeq)
139140

140141
final def classSet1M(a: String, ps: Map[String, Boolean]): TagMod =
141-
classSet1(a, ps.toSeq: _*)
142+
classSetImpl(cls_=(a), ps.toSeq)
142143
}
143144

145+
@inline private[vdom] def cls_=(v: String): TagMod =
146+
ClassNameAttr.:=(v)(stringAttrX)
147+
148+
private[vdom] def classSetImpl(z: TagMod, ps: Seq[(String, Boolean)]): TagMod =
149+
ps.foldLeft(z)((q, p) => if (p._2) q + cls_=(p._1) else q)
144150
}

core/src/main/scala/japgolly/scalajs/react/vdom/HtmlAttrs.scala

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -270,20 +270,20 @@ trait HtmlAttrs {
270270
*
271271
*/
272272
final val xmlns = "xmlns".attr
273-
/**
274-
* This attribute is a space-separated list of the classes of the element.
275-
* Classes allows CSS and Javascript to select and access specific elements
276-
* via the class selectors or functions like the DOM method
277-
* document.getElementsByClassName. You can use cls as an alias for this
278-
* attribute so you don't have to backtick-escape this attribute.
279-
*
280-
* MDN
281-
*/
282-
final val `class` = "className".attr
283-
/**
284-
* Shorthand for the `class` attribute
285-
*/
286-
final val cls = `class`
273+
// /**
274+
// * This attribute is a space-separated list of the classes of the element.
275+
// * Classes allows CSS and Javascript to select and access specific elements
276+
// * via the class selectors or functions like the DOM method
277+
// * document.getElementsByClassName. You can use cls as an alias for this
278+
// * attribute so you don't have to backtick-escape this attribute.
279+
// *
280+
// * MDN
281+
// */
282+
// final val `class` = "className".attr
283+
// /**
284+
// * Shorthand for the `class` attribute
285+
// */
286+
// final val cls = `class`
287287
/**
288288
* This attribute participates in defining the language of the element, the
289289
* language that non-editable elements are written in or the language that

core/src/main/scala/japgolly/scalajs/react/vdom/Scalatags.scala

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ final case class Attr(name: String) {
7979
def :=[T](v: T)(implicit ev: AttrValue[T]): TagMod = AttrPair(this, v, ev)
8080
}
8181

82+
object ClassNameAttr {
83+
def :=[T](t: T)(implicit av: AttrValue[T]): TagMod = new TagMod {
84+
override def applyTo(b: Builder): Unit =
85+
av.apply(t, b.addClassName)
86+
}
87+
}
88+
8289
/**
8390
* Wraps up a CSS style in a value.
8491
*/
@@ -110,10 +117,9 @@ private[vdom] object Scalatags {
110117
/**
111118
* An [[Attr]], it's associated value, and an [[AttrValue]] of the correct type
112119
*/
113-
case class AttrPair[T](a: Attr, v: T, ev: AttrValue[T]) extends TagMod {
114-
override def applyTo(t: Builder): Unit = {
115-
ev.apply(t, a, v)
116-
}
120+
case class AttrPair[T](a: Attr, t: T, av: AttrValue[T]) extends TagMod {
121+
override def applyTo(b: Builder): Unit =
122+
av.apply(t, b.addAttr(a.name, _))
117123
}
118124
/**
119125
* Used to specify how to handle a particular type [[T]] when it is used as
@@ -124,7 +130,7 @@ private[vdom] object Scalatags {
124130
"No AttrValue defined for type ${T}; scalatags does not know how to use ${T} as an attribute"
125131
)
126132
trait AttrValue[T]{
127-
def apply(t: Builder, a: Attr, v: T): Unit
133+
def apply(v: T, b: js.Any => Unit): Unit
128134
}
129135

130136
/**
@@ -182,7 +188,7 @@ private[vdom] object Scalatags {
182188
}
183189

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

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

213219
final class GenericAttr[T](f: T => js.Any) extends AttrValue[T] {
214-
def apply(t: Builder, a: Attr, v: T): Unit =
215-
t.addAttr(a.name, f(v))
220+
def apply(v: T, b: js.Any => Unit): Unit = b(f(v))
216221
}
217222
object GenericAttr {
218223
@inline def apply[T <% js.Any] = new GenericAttr[T](a => a)

doc/CHANGELOG-0.7.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# 0.7.1 ([commit log](https://github.com/japgolly/scalajs-react/compare/v0.7.0...v0.7.1))
22

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

57

68
# 0.7.0 ([commit log](https://github.com/japgolly/scalajs-react/compare/v0.6.1...v0.7.0))

test/src/test/scala/japgolly/scalajs/react/CoreTest.scala

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ object CoreTest extends TestSuite {
131131

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

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

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

160160
'customAttr - test(div("accept".reactAttr := "yay"), """<div accept="yay"></div>""")
@@ -168,13 +168,19 @@ object CoreTest extends TestSuite {
168168
r((false, false)) shouldRender """<div>x</div>"""
169169
r((true, false)) shouldRender """<div class="p1">x</div>"""
170170
r((false, true)) shouldRender """<div class="p2">x</div>"""
171-
r((true, true)) shouldRender """<div class="p1 p2">x</div>"""
171+
r((true, true)) shouldRender """<div class="p2 p1">x</div>"""
172172
}
173173
'hasMandatory {
174174
val r = ReactComponentB[Boolean]("C").render(p => div(classSet1("mmm", "ccc" -> p))("x")).build
175175
r(false) shouldRender """<div class="mmm">x</div>"""
176176
r(true) shouldRender """<div class="mmm ccc">x</div>"""
177177
}
178+
'appends {
179+
val r = ReactComponentB[Boolean]("C").render(p =>
180+
div(cls := "neat", classSet1("mmm", "ccc" -> p), cls := "slowclap", "x")).build
181+
r(false) shouldRender """<div class="neat mmm slowclap">x</div>"""
182+
r(true) shouldRender """<div class="neat mmm ccc slowclap">x</div>"""
183+
}
178184
}
179185

180186
'props {

test/src/test/scala/japgolly/scalajs/react/PrefixedVdomTest.scala

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ object PrefixedVdomTest extends TestSuite {
131131

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

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

155155
'applyChaining - test(
156156
<.a(^.tabIndex := 1, ^.cls := "lol")(^.href := "boo", ^.alt := "g"),
157-
"""<a tabindex="1" class="lol" href="boo" alt="g"></a>""")
157+
"""<a tabindex="1" href="boo" alt="g" class="lol"></a>""")
158158
}
159159
}
160160

@@ -164,13 +164,19 @@ object PrefixedVdomTest extends TestSuite {
164164
r((false, false)) shouldRender """<div>x</div>"""
165165
r((true, false)) shouldRender """<div class="p1">x</div>"""
166166
r((false, true)) shouldRender """<div class="p2">x</div>"""
167-
r((true, true)) shouldRender """<div class="p1 p2">x</div>"""
167+
r((true, true)) shouldRender """<div class="p2 p1">x</div>"""
168168
}
169169
'hasMandatory {
170170
val r = ReactComponentB[Boolean]("C").render(p => <.div(^.classSet1("mmm", "ccc" -> p))("x")).build
171171
r(false) shouldRender """<div class="mmm">x</div>"""
172172
r(true) shouldRender """<div class="mmm ccc">x</div>"""
173173
}
174+
'appends {
175+
val r = ReactComponentB[Boolean]("C").render(p =>
176+
<.div(^.cls := "neat", ^.classSet1("mmm", "ccc" -> p), ^.cls := "slowclap", "x")).build
177+
r(false) shouldRender """<div class="neat mmm slowclap">x</div>"""
178+
r(true) shouldRender """<div class="neat mmm ccc slowclap">x</div>"""
179+
}
174180
}
175181
}
176182
}

0 commit comments

Comments
 (0)