Skip to content

Commit

Permalink
API: replace cls.toggle("foo") with cls("foo")
Browse files Browse the repository at this point in the history
  • Loading branch information
raquo committed Nov 27, 2023
1 parent 002ddba commit a20ba75
Show file tree
Hide file tree
Showing 9 changed files with 65 additions and 26 deletions.
5 changes: 3 additions & 2 deletions src/main/scala/com/raquo/laminar/keys/CompositeKey.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.raquo.laminar.keys
import com.raquo.airstream.core.Source
import com.raquo.ew.ewArray
import com.raquo.laminar.api.L.{MapValueMapper, StringValueMapper}
import com.raquo.laminar.api.StringSeqValueMapper
import com.raquo.laminar.codecs.Codec
import com.raquo.laminar.keys.CompositeKey.{CompositeCodec, CompositeValueMapper}
import com.raquo.laminar.modifiers.{CompositeKeySetter, KeyUpdater}
Expand Down Expand Up @@ -46,6 +47,7 @@ class CompositeKey[K <: Key, -El <: ReactiveElement.Base](
this.:=(items: _*)
}

@deprecated("""toggle("foo") attribute method is not necessary anymore: use cls("foo"), it now supports everything that toggle supported.""", since = "17.0.0-M1")
def toggle(items: String*): LockedCompositeKey[K, El] = {
new LockedCompositeKey(this, items.toList)
}
Expand Down Expand Up @@ -74,8 +76,7 @@ class CompositeKey[K <: Key, -El <: ReactiveElement.Base](
private def addStaticItems(normalizedItems: List[String]): CompositeKeySetter[K, El] = {
new CompositeKeySetter(
key = this,
itemsToAdd = normalizedItems,
itemsToRemove = Nil
itemsToAdd = normalizedItems
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.raquo.laminar.modifiers.{CompositeKeySetter, KeyUpdater, Setter}
import com.raquo.laminar.nodes.ReactiveElement

/** Laminar key specific to a particular set of CompositeAttr values */
@deprecated("""LockedCompositeKey is deprecated. Attributes' toggle("foo") method is not necessary anymore: use cls("foo"), CompositeKeySetter now supports everything that LockedCompositeKey supported.""", since = "17.0.0-M1")
class LockedCompositeKey[K <: Key, -El <: ReactiveElement.Base](
val key: CompositeKey[K, El],
val items: List[String]
Expand Down
3 changes: 2 additions & 1 deletion src/main/scala/com/raquo/laminar/keys/LockedEventKey.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class LockedEventKey[Ev <: dom.Event, -In, +Out](

def -->(sink: Sink[Out]): Binder.Base = {
Binder { el =>
ReactiveElement.bindSink[Out](el, composer(el.events(eventProcessor)))(sink)
val observable = composer(el.events(eventProcessor))
ReactiveElement.bindSink[Out](el, observable)(sink)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.raquo.laminar.modifiers

import com.raquo.airstream.core.Source
import com.raquo.laminar.api.StringSeqValueMapper
import com.raquo.laminar.keys.{CompositeKey, Key}
import com.raquo.laminar.nodes.ReactiveElement

Expand All @@ -16,21 +18,55 @@ import com.raquo.laminar.nodes.ReactiveElement
*
* Note: for dynamic subscriptions (<--), we use [[KeyUpdater]] for all keys including
* composite attributes.
*
* @param itemsToAdd Note: must be normalized (no empty strings; one value per item)
*/
class CompositeKeySetter[K <: Key, -El <: ReactiveElement.Base](
val key: CompositeKey[K, El],
val itemsToAdd: List[String],
val itemsToRemove: List[String]
val itemsToAdd: List[String]
) extends Setter[El] {

/** This is called by Laminar when composite key setter is used as a modifier,
* i.e. when you say `div(cls := "foo")` – much like the usual KeySetter-s.
*/
override def apply(element: El): Unit = {
if (itemsToAdd.nonEmpty || itemsToRemove.nonEmpty) {
if (itemsToAdd.nonEmpty) {
element.updateCompositeValue(
key = key,
reason = null, // @Note using null to avoid keeping a reference to this setter
addItems = itemsToAdd,
removeItems = itemsToRemove
removeItems = Nil
)
}
}

// The methods below let us use this CompositeKeySetter as a key,
// instead of as a setter, supporting syntax like:
//
// cls("foo") := myBoolean
// cls("foo", "bar") <-- myBooleanStream
//
// which sets one or more classes conditionally.

/** If `include` is true, the items will be added, if false, they will not be added */
@inline def apply(include: Boolean): CompositeKeySetter[K, El] = {
this := include
}

/** If `include` is true, the items will be added, if false, they will not be added */
def :=(include: Boolean): CompositeKeySetter[K, El] = {
if (include) {
key(itemsToAdd: _*)
} else {
key()
}
}

/** If the `include` observable emits true, value(s) will be added,
* if it emits false, they will be removed (but only if they were
* previously added - see docs).
*/
def <--(include: Source[Boolean]): KeyUpdater[El, CompositeKey[K, El], List[String]] = {
key <-- include.toObservable.map(include => if (include) itemsToAdd else Nil)
}
}
16 changes: 8 additions & 8 deletions src/test/scala/com/raquo/laminar/CompositeKeySpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,10 @@ class CompositeKeySpec extends UnitSpec {
val bus = new EventBus[Boolean]
val el = div(
cls := "foo faa",
cls.toggle("bar bax") := true,
cls.toggle("bar nope") := false,
cls.toggle("bar baz") <-- bus.events,
cls.toggle("qux") <-- bus.events
cls("bar bax") := true,
cls("bar nope") := false,
cls("bar baz") <-- bus,
cls("qux") <-- bus.events
)
mount(el)
expectNode(div.of(cls is "foo faa bar bax"))
Expand All @@ -169,17 +169,17 @@ class CompositeKeySpec extends UnitSpec {
expectNode(div.of(cls is "foo faa bar bax baz qux"))

// This does not actually do anything since Laminar v0.12.0
el.amend(cls.toggle("foo faa") := false)
el.amend(cls("foo faa") := false)
expectNode(div.of(cls is "foo faa bar bax baz qux"))
}

it("cls - toggle - var") {
val bus = Var(false)
val el = div(
cls := "foo faa",
cls.toggle("bar bax") := true,
cls.toggle("bar nope") := false,
cls.toggle("foo baz") <-- bus
cls("bar bax") := true,
cls("bar nope") := false,
cls("foo baz") <-- bus
)
mount(el)
expectNode(div.of(cls is "foo faa bar bax")) // Var starts with false
Expand Down
6 changes: 3 additions & 3 deletions src/test/scala/com/raquo/laminar/SyntaxSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -391,9 +391,9 @@ class SyntaxSpec extends UnitSpec {
//implicit def xxxx[A](obs: Observable[_]#Self[A]): Source[A] = obs: Observable[A]

div(
cls.toggle("cls1") <-- boolSignal,
cls.toggle("cls1") <-- boolStream,
cls.toggle("cls1") <-- boolBus,
cls("cls1") <-- boolSignal,
cls("cls1") <-- boolStream,
cls("cls1") <-- boolBus,
focus <-- boolStream,
focus <-- boolBus,
child <-- divObservable,
Expand Down
12 changes: 6 additions & 6 deletions website/docs/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -1161,9 +1161,9 @@ cls := (Seq("class1" -> true, "class2" -> someBoolean), Seq("class3" -> someBool
cls := Map("class1" -> true, "class2" -> false)

// Add class names conditionally (true = add, false = do nothing)
cls.toggle("class1") := true
cls.toggle("class1 class2") := someBoolean
cls.toggle("class1", "class2") := true
cls("class1") := true
cls("class1 class2") := someBoolean
cls("class1", "class2") := true
```

Of course, the reactive layer is similarly considerate in regard to `cls`. Consider this use case:
Expand All @@ -1175,7 +1175,7 @@ val isSelectedSignal: Signal[Boolean] = ???
div(
cls := "MyComponent",
cls <-- classesStream,
cls.toggle("class1", "class2") <-- isSelectedSignal,
cls("class1", "class2") <-- isSelectedSignal,
cls <-- boolSignal.map { isSelected =>
if (isSelected) "always x-selected" else "always"
},
Expand All @@ -1192,7 +1192,7 @@ Once again, we don't want the CSS class names coming in from `classesStream` to

So for example, when `classesStream` emits `List("class1", "class2")`, we will _add_ those classes to the element. When it subsequently emits `List("class1", "class3")`, we will remove `class2` and add `class3` to the element's class list.

The **`<--`** method can be called with Observables of `String`, `Seq[String]`, `Seq[(String, Boolean)]`, `Map[String, Boolean]`, `Seq[Seq[String]]`, `Seq[Seq[(String, Boolean)]]`. The ones involving booleans let you issue events that instruct Laminar to remove certain classes **that were previously added by this same modifier** (by setting their value to `false`). **Importantly, cls modifiers never remove classes added by other modifiers.** So if you said `cls := "foo"` somewhere, no other modifier can later remove this `foo` class. If you need to add and remove `foo` over time, use `cls.toggle("foo") <-- shouldUseFooStream` or similar dynamic modifiers.
The **`<--`** method can be called with Observables of `String`, `Seq[String]`, `Seq[(String, Boolean)]`, `Map[String, Boolean]`, `Seq[Seq[String]]`, `Seq[Seq[(String, Boolean)]]`. The ones involving booleans let you issue events that instruct Laminar to remove certain classes **that were previously added by this same modifier** (by setting their value to `false`). **Importantly, cls modifiers never remove classes added by other modifiers.** So if you said `cls := "foo"` somewhere, no other modifier can later remove this `foo` class. If you need to add and remove `foo` over time, use `cls("foo") <-- shouldUseFooStream` or similar dynamic modifiers.

If you (or a third party library you're using) are adding or removing class names without Laminar, using native JS APIs like `ref.className = ???` and `ref.classList.add(???)`, and you are **also** using `cls` modifiers on this **same** element, you must take care to avoid manually adding or removing the same classes as you're setting using the `cls` modifiers. Doing so may cause unexpected behaviour. Basically, **a given class name on a given element should be managed either via Laminar `cls` modifiers or externally via JS APIs, but not both**. See the `cls - third party interference` test in `CompositeKeySpec` for a simple example.

Expand Down Expand Up @@ -2571,7 +2571,7 @@ This kind of pattern does not work, because Laminar's DOM updates are granular d

```scala
div(
cls.toggle("active") <-- userSignal.map(_.isActive),
cls("active") <-- userSignal.map(_.isActive),
opacity <-- userSignal.map(if (_.isActive) 1 else 0.5)
)
```
Expand Down
2 changes: 1 addition & 1 deletion website/docs/examples/form-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def renderInputRow(error: FormState => Option[String])(mods: Modifier[HtmlElemen
val errorSignal = stateVar.signal.map(_.displayError(error))
div(
cls("-inputRow"),
cls.toggle("x-hasError") <-- errorSignal.map(_.nonEmpty),
cls("x-hasError") <-- errorSignal.map(_.nonEmpty),
p(mods),
child.maybe <-- errorSignal.map(_.map(err => div(cls("-error"), err)))
)
Expand Down
2 changes: 1 addition & 1 deletion website/docs/examples/todomvc.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ object TodoMvcApp {

private def renderFilterButton(filter: Filter) =
a(
cls.toggle("selected") <-- filterVar.signal.map(_ == filter),
cls("selected") <-- filterVar.signal.map(_ == filter),
onClick.preventDefault.mapTo(filter) --> filterVar.writer,
filter.name
)
Expand Down

0 comments on commit a20ba75

Please sign in to comment.