Comments (6)
I have personally never done that kind of "trick", and certainly I don't remember that I used observe
in the past.
For what it's worth, if I had to implement the particular use case you mention myself, I would do something like
def childComponent(initialValue: Int): Div =
val childVar = Var(initial = initialValue)
println(s"current number: ${childVar.now()}")
div(
"Hello world",
child.text <-- childVar
)
def parentComponent(): Div = {
val numVar = Var(0)
div(
"Child: ",
child <-- numVar.signal.map(_ + 1).take(1).map(childComponent)
)
}
val app = parentComponent()
where that take(1)
would be an extension method that I would need to find how to implement...
from laminar.
Some notes / ideas, mostly for myself
We were talking with @armanbilge about this today, from the perspective of lifecycle management in observable / streaming systems.
He takes different tradeoffs in Calico, and it was interesting to learn those patterns and think about their possible applications in Laminar.
Very rudimentarily, one pattern we could employ in current Laminar is something like this:
def component(signal1: Signal[String], signal2: Signal[B]) =
div.observed(signal1, signal2) { (s1, s2, owner, thisRef) =>
// Here s1 and s2 are strict, and `owner` is available to make derivatives like s1.map(...) also strict
List(
"Hello " + s1.now().toUpperCsae,
child <-- s2.map(...)
)
}
def observed(signal1: Signal[A], signal2: Signal[B])(render: (StrictSignal[A], StrictSignal[B], Owner, Div) => List[Modifier]) = {
onMountInsert { ctx =>
thisNode => div(inContext { thisNode => render(signal1, signal2, ctx.owner, thisNode) } )
}
}
div(
"Parent here",
component(signal1, signal2)
)
As presented, this is essentially an abstraction over my onMountInsert
approach above. Laminar and Airstream lack the necessary properties to make this pattern more useful.
More generally, we talked about Airstream's observables being lazy (as a downside – managing their lifetimes, even though it's done automatically, can still be annoying at times as evidences by this very issue), and Calico's concept of elements-as-resources that allows their observables to be strict and yet not leak memory.
In Laminar you can mount and unmount a component, and this will stop and then restart the subscriptions, while retaining the same DOM element, and reviving the subscriptions in a reasonable and safe. It took me a lot of work to get to a point where this is working relatively smooth, but I think the upcoming version of Laminar nails down this pattern. Yet this pattern isn't really an end-goal, it's more of a necessity due to our observables being lazy. And the observables are lazy because otherwise they would leak in certain cases (see the linked discord thread, or #33), at least as long as we have direct element creation using div()
.
I think we should investigate borrowing some wisdom from Calico's approach. That would be an immense undertaking, and would need a lot of thought put into it to even just fully understand the tradeoffs, let alone iron out all the bugs, but it's good to talk it out even if I'm not going to do it anytime soon, or at all.
Consider this:
- If Laminar elements were lazy and had no "initial value", signals inside of those elements could evaluate their initial value strictly. We can't have both elements and signals strictly evaluated (because #33), so we're switching one from the other.
- As Arman said, dealing with lifecycles of observables (and resources in general) is a bigger problem than dealing with indirect DOM element references, so this might be a good tradeoff.
Then, we need:
class Element(tag: Tag):
- Same as today, 1-to-1 link to real DOM element
- Has a single
owner
(provided by the parent) (instead of adynamicOwner
that creates multiple owner-s) - Subscriptions in this element use this element's
owner
- Thus, subscriptions are initialized immediately upon element creation, NOT when it's mounted
- And the subscriptions are killed when the
owner
decides, not when this element is mounted.
class Resource[A](Owner => A):
- Call .build() to create an Owner and produce an A from it.
- For example, you can build an element this way.
Tag:
- div(mods) creates a Resource[Element](owner => div.create(owner, mods))
- div.withOwner(owner => mods) is the same but gives access to the owner if needed
- // div.fromOwner(owner)(mods) creates an Element(!) by creating a Resource and calling build(owner) on it
So essentially:
val user: Signal[User] = ???
val el: Resource[Element] = div.withOwner( owner =>
// if we use withOwner, we can use class-based components like TextInput and their props as usual
val nameInputVar = Var("world")
val formattedName = nameInputVar.signal.map(_.toUppercase).own(owner)
val textInput = TextInput(owner, formattedName)
textInput.inputNode.amend(
onInput.mapToValue --> nameInputVar,
value <-- nameInputVar
)
dom.console.log("Initial name: " + formattedName.now())
List(
"Hello, ",
b(child.text <-- formattedName),
textInput.node,
br(),
// onMountCallback equivalent (onMountBind and onMountInsert are not needed - just put that stuff here)
// onMountUnmountCallback is also possible with... "bimap" or whatever FP lords would call it.
div(cls := "widget").map((thisRef, owner) => setupThirdPartyComponent(thisRef.ref))
br(),
// child <-- Signal[Resource] provides a new Owner to the new child every time it emits
// that owner will die on the next event, or when the parent element's owner dies.
child <-- user.map(renderUserResource(_, formattedName)),
// If we wanted to render individual child elements, we would need to provide some
// owner, but there isn't any. the parent's owner would be unsuitable here, but that's the
// only owner available to the user. Does this mean child <-- should not accept Signal[Element]?
child <-- user.map(renderUserElement(_, formattedName), owner = ???)
// this one works just like our regular split, creating an element and an owner context
// allows for efficient rendering of user info as usual.
// do we need a similar implementation that requires a resource callback instead of element callback?
child <-- user.splitOne(_.id)(renderUserSplit(formattedName))
)
)
class TextInput(owner: Owner, formattedName: StrictSignal[String]) {
val inputNode: Element = input(cls := "input").build(owner)
val node: Element = div(
"Initial name: " + formattedName.now(),
inputEl
).build(owner)
}
def renderUserResource(user: User, formattedName: StrictSignal[String]): Resource[Element] = {
// ??? this userFormattedName still can't be strict, right?
// this can't be owned by the parent owner, because then we're leaking memory just like current Airstream would
// so we need some special syntax for this... Calico has flatMap but I'm looking for something lighter
val userFormattedName = formattedName.map(user.formatAgain)
div(
user.name + " (",
"Initial: " + userFormattedName.now(), // can't do this, need `div.withOwner ( owner =>` to make into StrictSignal
child.text <-- userFormattedName,
")"
)
}
def renderUser(user: User, formattedName: StrictSignal[String], owner: Owner): Element = {
div(
user.name + " (",
child.text <-- formattedName.map(userformat),
")"
).build(owner)
}
def renderUserSplit(id: String, userSignal: StrictSignal[User], owner: Owner)(formattedName: StrictSignal[String]): Element = {
div(
"Initial name: " + userSignal.now().name,
child.text <-- userSignal.map(_.name),
child.text <-- formattedName.map(userformat),
).build(owner)
}
For now, I still don't see how to get rid of laziness without paying the boilerplate price of flatmapping (the div.observed
I started with) to bind derived observables to one of the child elements / resources. I don't like this syntax very much because it breaks the regular freeform-style that we currently enjoy in Laminar – we don't force you to define resources in a certain location or in a certain order. I guess calling .own()
is slightly better visually, but then, we're stuck with strictSignal.map(foo)
needing to be lazy. And if it remains lazy (until we call .own()), how is that fundamentally different from our lazy signals and calling .observe(owner) on them to get a strict signal?
Ask Arman later about helloCounter.map(_.toString)
in calico example code – is that lazy or strict? And if that's strict, would it cause a memory leak if it was passed to a dynamic child resource? (screw that wording, give a specific example).
I think (but not sure) that Calico's observable transformations like .map can only have pure functions in them, and they don't have shared execution – maybe that somehow allows for a different memory model? I would understand if it was pull-based, but it appears to be push-based, meaning that parent observable needs to have references to child observables in order to propagate events. So, pure or not, it should still have GC-preventing links in the structure. Hrm.
from laminar.
I think (but not sure) that Calico's observable transformations like .map can only have pure functions in them, and they don't have shared execution – maybe that somehow allows for a different memory model?
Yes, that's right. Only pure functions, and responsibility for executing them actually falls to each subscriber to the signal. Mapping a signal basically only provides a "view", it does not allocate any state to cache an intermediate value.
I would understand if it was pull-based, but it appears to be push-based, meaning that parent observable needs to have references to child observables in order to propagate events.
Well, it's a bit of push-pull 🤔 subscribing to a signal returns something of Resource
-like shape. That means the listener is responsible for managing the lifecycle of its subscription (typically by binding this to the lifecycle of the element its being used with.
Furthermore, once a signal fires an event (push), it actually clears out all of its listeners. It's up to listeners to resubscribe (pull) whenever they are interested and ready for events again. This is implemented such that if there was at least one event during that window where a listener was unsubscribed, it will be notified immediately upon re-subscription.
Ask Arman later about
helloCounter.map(_.toString)
in calico example code – is that lazy or strict?
I guess you mean this one, used in the routing example?
In that case, the SignallingRef
is created when the entire app is created.
Then we setup a subscription here, which is bound to the lifecycle of the p(...)
(which itself is bound to all its parents).
As described by the push-pull dynamics above, I think I would say it's lazy? If nobody is listening to a signal (pull), then it is not firing events (push). However, it is still "alive" in the sense that you can .get
(read peakNow()
) it at any time to get its current value. If there are any transformations e.g. .map(_.toString)
these would be applied on-demand.
Hope that helps :)
from laminar.
Regarding this one:
def renderUserResource(user: User, formattedName: StrictSignal[String]): Resource[Element] = {
// ??? this userFormattedName still can't be strict, right?
// this can't be owned by the parent owner, because then we're leaking memory just like current Airstream would
// so we need some special syntax for this... Calico has flatMap but I'm looking for something lighter
val userFormattedName = formattedName.map(user.formatAgain)
div(
user.name + " (",
"Initial: " + userFormattedName.now(), // can't do this, need `div.withOwner ( owner =>` to make into StrictSignal
child.text <-- userFormattedName,
")"
)
}
Can't this be expressed something like this?
def renderUserResource(user: User, formattedName: StrictSignal[String]): Resource[Element] = Resource { owner =>
val userFormattedName = formattedName.map(user.formatAgain) // use owner here ?
div(
user.name + " (",
"Initial: " + userFormattedName.now(),
child.text <-- userFormattedName,
")"
).build(owner)
}
from laminar.
As described by the push-pull dynamics above, I think I would say it's lazy? If nobody is listening to a signal (pull), then it is not firing events (push). However, it is still "alive" in the sense that you can .get (read peakNow()) it at any time to get its current value. If there are any transformations e.g. .map(_.toString) these would be applied on-demand.
Great, that was my suspicion, thanks for confirming.
Can't this be expressed something like this?
Right, yes, we can just wrap it in another Resource
, makes sense, thanks!
from laminar.
Just a note to self – if we ever allow something like the peekNow() described in the original post, I will probably need to hide it behind a feature import, similar to unitArrows
in Laminar 15. I still see people wanting to use .now() on signals where they shouldn't, and don't want to make it too easy to go down the wrong path.
from laminar.
Related Issues (20)
- Docs: Algolia search
- RFC: Drop Scala 2.12 support HOT 2
- Laminar 15.0.0 pre-release testing & issues HOT 19
- Animation features
- [Question] - jQuery bind and oninput event HOT 2
- [DOM] Using certain reserved values for the `name` attribute breaks Scala types HOT 1
- onParentChange callbacks HOT 3
- Laminar Native for building mobile and desktop apps HOT 2
- ZIO / FS2 / etc. integration examples HOT 5
- tabIndex for SVG element HOT 3
- Handling of SVG attributes is subtly incorrect (and `xmlns` attribute is broken) (again)
- Post request fails to resolve HOT 6
- Helper to set multiple keys (props / attrs / event props) at the same time
- Expose child-specific owners to help users write strict state logic
- RFC: onMount* callbacks should fire even if the element is already mounted
- Laminar Roadmap
- Error: Maven resources not found for Scala version 2.12, but I'm using Scala version 2.13.12 HOT 8
- Support nesting of dynamic inserters
- RFC: Element.observe(signal) to safely get the signal's current value. HOT 1
- Easy `Var`: Providing implicit conversion from `Var[A]` into `Source[String]` ? HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from laminar.