raquo / Laminar

Simple, expressive, and safe UI library for Scala.js

Home Page:https://laminar.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Interference when using same class name in multiple cls <-- source mods

ngbinh opened this issue · comments

I encounter this quite subtle bug when trying to set cls. I probably can do something else to avoid it but I think it is quite a big surprise for Laminar user

it("cls bug") {

    val bus = new EventBus[Int]

    mount(
      div(
        "hello",
        cls <-- bus.events.map { num =>
          if (num % 2 == 0) {
            "always even"
          } else {
            ""
          }
        },
        cls <-- bus.events.map { num =>
          if (num % 2 == 1) {
            "always odd"
          } else {
            ""
          }
        }
      )
    )

    expectNode(div like("hello"))

    bus.writer.onNext(1)

    expectNode(
      div like(
        "hello",
        cls is ("always odd")
      )
    )

    bus.writer.onNext(2)

    expectNode(
      div like(
        "hello",
        cls is ("always even")
      )
    )

  }

Ah, I see. It's a known limitation of the current design:

Each cls <-- source modifier should deal with a set of CSS classes that does not intersect with the sets used by other such modifiers. In other words, you should generally avoid adding classes to an element in one reactive modifier and then removing them in another reactive modifier. Basically, if you do this you have two competing sources of truth for whether an element should carry a class at any given time, and the latest source to emit will win.

Current implementation is simple – these two modifiers don't know about each other yet still work well together almost always – but well, "almost" is not "always", and here we are. I agree that it can be surprising if you actually run into it.

I think I can improve on this, but it would require for all cls-based modifiers to read-write some shared state on every element they touch, basically it would work like reference counting for individual class names. Same for other composite keys.

I'll keep the issue open, meanwhile you should be able to refactor your use of cls now that you know what the limitation is.

yep. Something like this

    mount(
      div(
        "hello",
        cls <-- bus.events.map { num =>
          if (num % 2 == 0) "always even" else "always odd"
        }
      )
    )

works as expected

Is anyone using cls.set or cls.remove? I'm considering removing one or both of those as part of fixing this, because they don't really fit in the non-conflicting paradigm. I may keep cls.forceRemove("class1") as an escape hatch though.

@raquo we are not using it. Always cls <-- clsSignal.