Skip to content

RFC: Element.observe(signal) to safely get the signal's current value. #158

@raquo

Description

@raquo

Background

This is yet more rumination on the issue of lifetime evidences:

Problem

Trying to be brief this time, I want an easy and safe way to call .now() on signals, without sacrificing safety, and without complicating the architecture. The linked issues above explain why this is hard to achieve in the current design, and possible strategies for improving the situation.

Note that this problem is specific to limited-lifetime situations, e.g. all your components' code, that is linked to element mount-unmount lifetimes. For global things that never need to be destroyed, using unsafeWindowOwner is a perfectly valid strategy.

Proposed solution

// Example is a class just to show that you're not limited to functions returning a single element.

class MyTextInput(savedTextS: Signal[String], signal2: Signal[String]):

  val clickBus: EventBus[Unit] = new EventBus

  val node: Div =
    div(
      cls("MyTextInput"),
    ).observe(savedTextS, signal2):
      (thisNode, savedTextS_, signal2_) =>
        ...
        val textInputVar = Var(initial = savedTextS_.now())
        ...
        thisNode.amend(
          onClick.mapToUnit --> clickBus,
          input(
            onInput.mapToValue --> textInputVar,
            value <-- textInputVar
          )

div.observe is the new thing here. You can call this new observe method on any element, provide it 1...N signals, and provide a callback that receives this same element as well as all of these signals converted to StrictSignal, allowing you to query its current value (.now()) at any time. This signal will only update while the element is mounted. When the element is unmounted, it will stop updating, until it is mounted again.

Importantly, the render callback of this observe method ((thisNode, savedTextS_, signal2_) => ...) is called at most once – when the element is first mounted (or immediately when it is invoked, if the element is already mounted by then). This is key.

This is different from methods like onMountInsert – the callback in those methods is called every time the element is mounted – this is why we have several versions of this method like onMountBind, onMountSet, and onMountInsert – we can't just put arbitrary modifiers into a generic onMount method, because arbitrary modifiers are not necessarily idempotent.

But with this observe method, we can! We can put anything we want in thisNode.amend, or do any other things. The observe method will return the original element for easy chaining.

Observed signals' lifetime

To reiterate, the observe method's render callback is only called if and when the element is mounted. So if we create this element but never mount it, the element never starts observing the signals, and we never get access to them. So if we do have access to the signals, it means that they have some value, they can't have no value at all. This is an improvement over the peekNow() proposal which would see us face exceptions in such cases.

As a user, you could potentially allow observed signals (savedTextS_ and signal2_) to escape the scope of the render function, for other code to access it. This is not intended use, but it's possible. In that case, these signals will continue having updating their values for as long as the original element that observed them is mounted, but as mentioned before, when it is unmounted, the signals would stop updating. Unless some of your other code adds observers to them, of course.

Ergonomics

Unlike onMountInsert, the observe method returns the element itself, not an opaque Inserter type. So, it can be used inside the split method's callback, inside child <--, children <--, etc.

Unlike child-specific owners (#148), this can be used on any element, inside or outside of split.

I'm not a fan of the boilerplate (duplicating the lists of signals in the observe call's arguments), but I don't see how it can be avoided, and the benefits of the design outweigh the annoyance, I think.

Use cases

Currently this feature is exclusively about signals and getting their .now() value. I assume that raquo/Airstream#119 will obviate the need for supporting zoomed vars. I don't see other use cases, it seems to be pretty much just this one rough edge that needs fixing.

Implementation

Each Laminar element has a DynamicOwner. Calling observe would create a new DynamicSubscription that, for each provided signal, would create a special type of StrictSignal that would be updated whenever DynamicOwner is active.

I would need to check the type hierarchy of StrictSignal / ObservedSignal / OwnedSignal to see if it still makes sense, and clean up / amend as necessary.

We would need to source-generate observe methods with different arities like we do for e.g. combineWith methods.

Overall this seems like a pretty small change technically, compared to the other referenced issues.

Architecturally, this feature should have no negative impact on other planned features, it is quite self-contained.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions