You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
To render dynamic data in Laminar, you use dynamic inserters like children <-- childrenObservable. Currently (v17), the children <-- receiver accepts Source[Collection[Component]], where:
Source is basically Observable
Collection is List, js.Array, or similar
Component is something that can be converted to a Laminar node (via RenderableNode)
Problem
The current functionality works fine to render many types that look like a sequence of elements, it even supports mutable sequences, but the RenderableNode interface places a significant restriction on what kind of components are considered element-like: any supported Component must have a corresponding Laminar element, and that element must be always available, and remain referentially the exact same element, i.e. you can't decide that the Component shall render an entirely different element at any point in its lifecycle. Such changing components are expected to expose Observable[Element] rather than a Component in Laminar – which works fine, but does not fit within the RenderableNode's idea of a Component.
In practice, this means that you can't put things like child <-- ... or onMountInsert(...) as one of the "components" in the observable in children <-- observable.
For example, I want to be able to do this (using the new children(...) <- boolObservable syntax of v17):
Currently neither of this is not possible, because child(div("world")) <-- boolVar1 and onMountInsert { ... } do not fit the definition of Component in RenderableNode. This is because onMountInsert { ... } can not implement RenderableNode's asNode() method: it requires a MountContext – evidence of the parent element having been mounted – to create its element / node. Moreover, it may create a new element (or multiple) on every mount, whereas the RenderableNode contract expects the component to retain the same element at all times, and only one element.
Ultimately, RenderableNode's design is driven by the needs of the children <-- observable diffing algorithm. The meat of it is located in the updateChildren method of ChildrenInserter. That algorithm needs two main pieces of data:
As you see, it has no concept of "Component" – we convert those to laminar elements (ChildNode.Base) before handing them off to this algorithm. Historically, that's because the concepts of RenderableNode and Component came into existence much later than this algorithm.
And so, this algorithm in its current state can not deal with components that can not be converted to a single static element. The question is whether we can generalize it without making it too complicated.
Motivation
On the surface, the issue can be avoided by simply wrapping your dynamic inserters in a static element like div, e.g.:
div(
children <-- observable.split(_.id) { (id, initial, childSignal) =>
div( // static wrap over dynamic contents
onMountInsert { ctx =>valchildVar= parentVar.zoom(...)(ctx.owner)
text <-- childVar
}
)
}
)
In simple cases like the above snippet, you may not even need to pay the performance overhead of the extra div, and even in more complex cases, that would often be inconsequential.
However, I believe the advantages in flexibility and reduced cognitive overhead could well be worth it. Especially now that the new-style children(...) <-- boolSignal API pretty much invites users to nest inserters inside children(...) as shown in the first snippet in this issue.
Potential solution: keep track of Inserter-s?
Laminar Inserter-s like onMountInsert { ... }, child <-- ..., and children <-- ... keep track of their own contents (DOM nodes). They also know where their content ends in the DOM, for example child <-- knows that it can only render at most one content node, and children <-- remembers how many nodes it has rendered last time (and in fact remembers the nodes themselves).
So, if children <-- observable accepted such inserters as part of the list in observable, the diffing algorithm could potentially coordinate with these inserters, and skip over the DOM nodes managed by them. For that, the algorithm would need to keep track of inserters instead of tracking the DOM nodes. ChildNode would probably need to extend Inserter, and we would need more subtypes of Inserter to apply correct behaviour in different cases.
This sounds fairly straightforward in principle, but the implementation would need to be very careful, especially to ensure that we correctly count the nodes managed by inserters even in the face of various external disturbances such as external code removing elements managed by nested inserters. Laminar has some resilience against such disturbances built-in, but I'm not sure if it will stand up to such nesting. We would also need to carefully watch performance.
Composability and Resource type equivalence
Supporting the nesting of inserters could be a good alternative solution to #148, and more generally, to the problem of lifecycle management discussed in the comments of #130. Would our reworked Inserter be able to stand in for a proper Resource type? I'm not yet sure. At least, it should solve the practical problems that users have been voicing, in a manner that is architecturally compatible with existing Laminar applications, and does not complicate the apparent API. But, I think the increased prevalence of Inserter type may induce demand for more functionality on that type, for example, if users start writing components that have onMountInsert as their top "element", they would return Inserter-s instead of elements, and so users would probably want some kind of amend method similar to what elements have. But... that wouldn't be possible because we don't know what kinds of nodes the inserter inserts. Those may not even be elements, they could be text nodes, for which amend functionality is not defined. Should Inserter be more typeful then? Inserter[N <: ChildNode.Base]? What about inserters that can potentially insert multiple nodes? Do they get a special type? Would their type param be a useless ChildNode.Base if they contain even one text node? What about sentinel comment nodes, ignore them I guess?
I don't know the answers to these questions yet. I think that over-relying on Inserter may be detreimental because it's too flexible – so while it's easy to create and hand off to Laminar without boilerplate, it's too opaque to be composable, so e.g. you wouldn't be able to call amend on it. That means that this type can't / shouldn't be the typical output of your reusable components.
I assume that this problem is mostly about the onMountInsert use case, as this is primarily the case that suffers from opaqueness. If your Inserter is e.g. child <-- observable, well you can just return the observable instead, and the consumer would be able to map it or something before calling the child <-- on it, if they wanted to. But a similar decomposition is not possible with onMountInsert.
So, while the proposed solution would still be useful in its own right, at least to render child <-- and children <-- in a nested way, I'm not sure that it would be a good solution for onMountInsert. I think that one needs more thought.
The text was updated successfully, but these errors were encountered:
I would love this feature so much. My Airstream/Laminar codes right now uses a lot of split's, and creating dummy element for them, although worked, feel very cumbersome to me.
I would love this feature so much. My Airstream/Laminar codes right now uses a lot of split's, and creating dummy element for them, although worked, feel very cumbersome to me.
@HollandDM Could you please clarify why do you need the dummy element in most cases – because you need an Owner for Vars / state manangement (so you have eg. div(onMountInsert(...))) – or do you actually want to return child <-- or children <-- from inside the split callback (and have to wrap them in div currently)? I would appreciate some details on what's driving this need (e.g. why do you need child <-- in split), as there are several potential solutions to these problems with different tradeoffs, and I still haven't figured out which one is better.
I think I wrote this before #116 was fully implemented.
In my case it's 90% of the time because of the need to have child <-- returned from inside split. For example if I have Option[Either[A, B]] or Seq[Option[T]], I have to use a intermediate element to split the signal.
Background
To render dynamic data in Laminar, you use dynamic inserters like
children <-- childrenObservable
. Currently (v17), thechildren <--
receiver acceptsSource[Collection[Component]]
, where:Source
is basicallyObservable
Collection
is List, js.Array, or similarComponent
is something that can be converted to a Laminar node (viaRenderableNode
)Problem
The current functionality works fine to render many types that look like a sequence of elements, it even supports mutable sequences, but the
RenderableNode
interface places a significant restriction on what kind of components are considered element-like: any supportedComponent
must have a corresponding Laminar element, and that element must be always available, and remain referentially the exact same element, i.e. you can't decide that the Component shall render an entirely different element at any point in its lifecycle. Such changing components are expected to exposeObservable[Element]
rather than aComponent
in Laminar – which works fine, but does not fit within theRenderableNode
's idea of aComponent
.In practice, this means that you can't put things like
child <-- ...
oronMountInsert(...)
as one of the "components" in theobservable
inchildren <-- observable
.For example, I want to be able to do this (using the new
children(...) <- boolObservable
syntax of v17):Or this:
Currently neither of this is not possible, because
child(div("world")) <-- boolVar1
andonMountInsert { ... }
do not fit the definition ofComponent
inRenderableNode
. This is becauseonMountInsert { ... }
can not implementRenderableNode
'sasNode()
method: it requires aMountContext
– evidence of the parent element having been mounted – to create its element / node. Moreover, it may create a new element (or multiple) on every mount, whereas the RenderableNode contract expects the component to retain the same element at all times, and only one element.Ultimately,
RenderableNode
's design is driven by the needs of thechildren <-- observable
diffing algorithm. The meat of it is located in theupdateChildren
method ofChildrenInserter
. That algorithm needs two main pieces of data:As you see, it has no concept of "Component" – we convert those to laminar elements (
ChildNode.Base
) before handing them off to this algorithm. Historically, that's because the concepts ofRenderableNode
andComponent
came into existence much later than this algorithm.And so, this algorithm in its current state can not deal with components that can not be converted to a single static element. The question is whether we can generalize it without making it too complicated.
Motivation
On the surface, the issue can be avoided by simply wrapping your dynamic inserters in a static element like
div
, e.g.:In simple cases like the above snippet, you may not even need to pay the performance overhead of the extra div, and even in more complex cases, that would often be inconsequential.
However, I believe the advantages in flexibility and reduced cognitive overhead could well be worth it. Especially now that the new-style
children(...) <-- boolSignal
API pretty much invites users to nest inserters insidechildren(...)
as shown in the first snippet in this issue.Potential solution: keep track of
Inserter
-s?Laminar
Inserter
-s likeonMountInsert { ... }
,child <-- ...
, andchildren <-- ...
keep track of their own contents (DOM nodes). They also know where their content ends in the DOM, for examplechild <--
knows that it can only render at most one content node, andchildren <--
remembers how many nodes it has rendered last time (and in fact remembers the nodes themselves).So, if
children <-- observable
accepted such inserters as part of the list inobservable
, the diffing algorithm could potentially coordinate with these inserters, and skip over the DOM nodes managed by them. For that, the algorithm would need to keep track of inserters instead of tracking the DOM nodes.ChildNode
would probably need to extendInserter
, and we would need more subtypes ofInserter
to apply correct behaviour in different cases.This sounds fairly straightforward in principle, but the implementation would need to be very careful, especially to ensure that we correctly count the nodes managed by inserters even in the face of various external disturbances such as external code removing elements managed by nested inserters. Laminar has some resilience against such disturbances built-in, but I'm not sure if it will stand up to such nesting. We would also need to carefully watch performance.
Composability and
Resource
type equivalenceSupporting the nesting of inserters could be a good alternative solution to #148, and more generally, to the problem of lifecycle management discussed in the comments of #130. Would our reworked
Inserter
be able to stand in for a properResource
type? I'm not yet sure. At least, it should solve the practical problems that users have been voicing, in a manner that is architecturally compatible with existing Laminar applications, and does not complicate the apparent API. But, I think the increased prevalence ofInserter
type may induce demand for more functionality on that type, for example, if users start writing components that haveonMountInsert
as their top "element", they would returnInserter
-s instead of elements, and so users would probably want some kind ofamend
method similar to what elements have. But... that wouldn't be possible because we don't know what kinds of nodes the inserter inserts. Those may not even be elements, they could be text nodes, for whichamend
functionality is not defined. Should Inserter be more typeful then?Inserter[N <: ChildNode.Base]
? What about inserters that can potentially insert multiple nodes? Do they get a special type? Would their type param be a uselessChildNode.Base
if they contain even one text node? What about sentinel comment nodes, ignore them I guess?I don't know the answers to these questions yet. I think that over-relying on
Inserter
may be detreimental because it's too flexible – so while it's easy to create and hand off to Laminar without boilerplate, it's too opaque to be composable, so e.g. you wouldn't be able to callamend
on it. That means that this type can't / shouldn't be the typical output of your reusable components.I assume that this problem is mostly about the
onMountInsert
use case, as this is primarily the case that suffers from opaqueness. If yourInserter
is e.g.child <-- observable
, well you can just return theobservable
instead, and the consumer would be able to map it or something before calling thechild <--
on it, if they wanted to. But a similar decomposition is not possible withonMountInsert
.So, while the proposed solution would still be useful in its own right, at least to render
child <--
andchildren <--
in a nested way, I'm not sure that it would be a good solution foronMountInsert
. I think that one needs more thought.The text was updated successfully, but these errors were encountered: