Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 64 additions & 3 deletions src/main/scala/com/raquo/laminar/codecs/CompositeCodec.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.raquo.laminar.codecs

import com.raquo.ew.ewArray

import scala.scalajs.js.|
import scala.scalajs.js.JSStringOps._

/** Such codecs are used for space-separated or comma-separated values, e.g. in the `cls` attr. */
Expand All @@ -14,6 +13,54 @@ class CompositeCodec(separator: String) extends Codec[Iterable[String], String]
override def encode(scalaValue: Iterable[String]): String = {
scalaValue.mkString(separator)
}

/** Updates a composite value using this codec's decode and encode methods.
*
* @param domValue raw DOM value, `()` if the attribute is not set
* @param addItems must be normalized
*/
def encodeUpdated(
domValue: String | Unit,
removeItems: List[String],
addItems: List[String]
): String = {
encode(domValue.map(decode).getOrElse(Nil).filterNot(removeItems.contains) ++ addItems)
}
}

final private[laminar] class DefaultCompositeCodec(separator: String) extends CompositeCodec(separator) {

override def encodeUpdated(
domValue: String | Unit,
removeItems: List[String],
addItems: List[String]
): String = {
var result = ""
var isFirst = true
val append = (item: String) => {
if (isFirst) {
result = item
isFirst = false
} else {
result = result + separator + item
}
}
domValue.foreach { value =>
if (value.nonEmpty) {
val items = value.jsSplit(separator)
var i = 0
while (i < items.length) {
val item = items(i)
if (item.nonEmpty && !removeItems.contains(item)) {
append(item)
}
i += 1
}
}
}
addItems.foreach(append)
result
}
}

object CompositeCodec {
Expand All @@ -29,8 +76,22 @@ object CompositeCodec {
def normalize(items: String, separator: String): List[String] = {
if (items.isEmpty) {
Nil
} else if (items.indexOf(separator) == -1) {
// Fast path for the most common case: a single item, e.g. `cls := "active"`
items :: Nil
} else {
items.jsSplit(separator).ew.filter(_.nonEmpty).asScalaJs.toList
val splitItems = items.jsSplit(separator)
// Iterate backwards to build the List by prepending, preserving order
var result: List[String] = Nil
var i = splitItems.length - 1
while (i >= 0) {
val item = splitItems(i)
if (item.nonEmpty) {
result = item :: result
}
i -= 1
}
result
}
}
}
4 changes: 2 additions & 2 deletions src/main/scala/com/raquo/laminar/keys/CompositeKey.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.raquo.laminar.keys

import com.raquo.airstream.core.Source
import com.raquo.laminar.api.{MapValueMapper, StringOptionValueMapper, StringSeqValueMapper, StringValueMapper}
import com.raquo.laminar.codecs.CompositeCodec
import com.raquo.laminar.codecs.{CompositeCodec, DefaultCompositeCodec}
import com.raquo.laminar.modifiers.{CompositeKeySetter, CompositeKeyUpdater}
import com.raquo.laminar.nodes.ReactiveElement

Expand Down Expand Up @@ -32,7 +32,7 @@ abstract class CompositeKey[ //

val separator: String

val codec: CompositeCodec = new CompositeCodec(separator)
val codec: CompositeCodec = new DefaultCompositeCodec(separator)

def :=(items: String): CompositeKeySetter[Self, El] = {
addStaticItems(StringValueMapper.toNormalizedList(items, separator))
Expand Down
13 changes: 7 additions & 6 deletions src/main/scala/com/raquo/laminar/nodes/ReactiveElement.scala
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,14 @@ with ParentNode[Ref] {

val itemsToAdd = addItems.distinct
val itemsToRemoveFromDom = removeItems.filterNot(itemHasAnotherReason)
val newItems = _compositeValues
.getOrElse(key, Nil)
val newItems = keyItemsWithReason
.filterNot(t => removeItems.contains(t._1) && t._2 == reason) ++ itemsToAdd.map((_, reason))

val domValues = key.getRawDomValue(this).map(key.codec.decode).getOrElse(Nil)

val nextDomValues = domValues.filterNot(itemsToRemoveFromDom.contains) ++ itemsToAdd.filterNot(itemHasAnotherReason)
val nextDomValue = key.codec.encodeUpdated(
domValue = key.getRawDomValue(this),
removeItems = itemsToRemoveFromDom,
addItems = itemsToAdd.filterNot(itemHasAnotherReason)
)

// 1. Update Laminar's internal structure
_compositeValues = _compositeValues.updated(key, newItems)
Expand All @@ -142,7 +143,7 @@ with ParentNode[Ref] {
// #Note this logic is compatible with third parties setting classes on Laminar elements
// using raw JS methods as long as they don't remove classes managed by Laminar or add
// classes that were also added by Laminar.
key.setRawDomValue(this, key.codec.encode(nextDomValues))
key.setRawDomValue(this, nextDomValue)
}

val tag: Tag[ReactiveElement[Ref]]
Expand Down
29 changes: 29 additions & 0 deletions src/test/scala/com/raquo/laminar/tests/CompositeKeySpec.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,39 @@
package com.raquo.laminar.tests

import com.raquo.laminar.api.L._
import com.raquo.laminar.codecs.CompositeCodec
import com.raquo.laminar.keys.CompositeAttr
import com.raquo.laminar.nodes.ReactiveElement
import com.raquo.laminar.utils.UnitSpec

class CompositeKeySpec extends UnitSpec {

it("custom composite codec uses its encode and decode overrides") {
val customKey = new CompositeAttr[ReactiveElement.Base]("data-custom", " ") {
override val codec: CompositeCodec = new CompositeCodec(" ") {
override def decode(domValue: String): List[String] = {
super.decode(domValue.stripPrefix("(").stripSuffix(")"))
}

override def encode(scalaValue: Iterable[String]): String = {
"(" + super.encode(scalaValue) + ")"
}
}
}
val item = Var("bar")
val el = div(customKey := "foo", customKey <-- item)
mount(el)

withClue("Initial binding:") {
assert(el.ref.getAttribute("data-custom") == "(foo bar)")
}

withClue("Reactive update:") {
item.set("baz")
assert(el.ref.getAttribute("data-custom") == "(foo baz)")
}
}

it("cls - simple static modifiers") {
val el = div(className := "foo")
mount(el)
Expand Down