Open dphfox opened 2 years ago
The selector proposition is really interesting and something I think has a lot of potential. The design for it is what is complicated. Here's an early idea of what this CSS-like syntax might look like.
Select '(Foo) (My Child) $TextLabel !Frame'
The parser here ignores whitespace. Wrapping text in parentheses indicates it is the name of a child. Something prefixed with a $
indicates the selection will be all children that are that specific class. Then a prefix of !
indicates the selection will be all children that are not that specific class. Obviously this is just an example and actually defining the rules for this syntax will take some time.
The question then is how should this selector actually be used? How can we distinguish between a selector that should be static and one that is dynamic? How would other Fusion objects interact with selectors?
I'm not really sold on the idea of building a selection engine into Fusion - I think that we should focus on the use cases that people are hurting with, and build backwards from there. The only use case for selectors I've seen so far is hydrate-by-name, which is why I chose to highlight it in this issue. Think about what a Fusion developer actually needs to use - when would we need to select by class? Selection by class is awfully broad and imprecise, and is unrelated to the semantics of the UI. Class types are concerned with getting the Roblox engine to create what we want; names, on the other hand, are purely intended to allow developers to semantically label their instances for the purposes of scripting, navigation and debugging.
I do not think it is wise to hydrate multiple children with one property table. Where reuse is needed, components should remain the preferred coding pattern, as these can properly separate and encapsulate local state. Having multiple instances share a property table is a really bad idea to encourage, unless the actual intention is to provide multiple identical views of the same underlying state. For most people attempting to hydrate multiple things at once, I presume this is not what they intend to do.
Another thing to be careful of with selectors is 'write-only code' - you see this all the time with regexes. One of the primary goals of Fusion is to promote code that is both powerful and easy to write, but also easy to read and predict the behaviour of. Selectors do run the risk of making certain kinds of code much more obscure or over-engineered than they need to be. Consider that one of the top reasons for adding hydration was allowing people to build UIs in Studio and apply Fusion to those prefab hierarchies; in this case, hydrate-by-name is all that's needed and likely the best choice for readability and maintainability.
There is also a technical complexity angle here. Where possible, I try and keep the internal complexity of Fusion low. This doesn't always work (interacting with the Roblox engine is always a mess) but in general, where it's possible, it's done. I've created a selection syntax before for one of my own plugins, and from that I have the experience of what it's like to implement one. Generally it involves lexing the input string into tokens, parsing the tokens into some machine-friendly structure, then using the structure to determining filtering conditions for a list of instances. This is not straightforward or lightweight to do. If it is not strictly necessary for the betterment of the majority's developer experience, is it worth adding all of that into the core library?
This is why I'm not convinced selectors are a great idea for the core Fusion library. As a third-party library, it'd certainly be an interesting experiment, but I'm doubtful of it's real-world usefulness.
I will focus this feature request on child names. Pattern matching will not be considered, at least for the first iteration of this feature.
when would we need to select by class? Selection by class is awfully broad and imprecise, and is unrelated to the semantics of the UI.
I know you're working on a different solution to this problem now (in #206), but I expect to be able to use Fusion to locate things like Humanoids in characters that are holding tools. #134 did not yet exist back when your comment was made, though :)
Perhaps the best play here is to implement a new special key which accepts the props and does the selection automatically, requiring no retconning of the Children special key to include 'unresolved children':
New (thing) {
Prop1 = "foo",
Prop2 = true,
[WithChild "Foo"] = {
ChildProp = "bar",
[OnEvent "Activated"] = doSomething
}
}
Perhaps the best play here is to implement a new special key which accepts the props and does the selection automatically, requiring no retconning of the Children special key to include 'unresolved children':
New (thing) { Prop1 = "foo", Prop2 = true, [WithChild "Foo"] = { ChildProp = "bar", [OnEvent "Activated"] = doSomething } }
My use case was for a plugin similar to the studio's Tag Editor that automatically picks up children of a Folder in ServerStorage and uses it as a source of data.
Basically, I'm getting really really fed up of pasting the exact same template into every new script and then having to do a manual find and replace, so I wanted to make a plugin to do it for me, except Fusion doesn't seem up to the task yet.
Anyway I can't exactly go New 'ServerStorage'
....
When we say something like
WithChild "Thing"
, what do we mean? Is it the "Thing" at the current time, or whatever "Thing" will be at any point in time? The latter is more complicated, but also more Fusiony.
I actually don't think the latter would be too complicated to add after #244.
The WithChild
token can simply use a ChildOf
on the template instance, which it would then observe changes for. Upon a change, destroy the existing clone of the child (if one exists), create a new one, and then parent it to the clone of the initial clone.
Effectively, this:
New (gui) {
[WithChild "Thing"] = {
Value = ...
}
}
would be sort of like so:
local parent = New (gui) {
Value = ...
}
local child = ChildOf(gui, "Thing")
local existingClone
Observer(child):onBind(function()
if existingClone then
existingClone:Destroy()
existingClone = nil
end
local currentChild = peek(child)
if currentChild then
existingClone = New (currentChild) {
Value = ...
}
existingClone.Parent = parent
end
end)
With this, adding/removing children from the template will make it immediately reflect itself in the cloned instance.
A notable implementation detail is that currently in #224, applyInstanceProps
isn't given the template instance, as it isn't needed. However, the WithChild
token would need both the template instance and the clone, so both would need to be passed to applyInstanceProps
. When constructing an instance from class name, nil could be passed as the template instance.
There wouldn't be a need to clone the child instance manually; :Clone()
is recursive already.
I realized that a bit after posting it. The manual cloning can (in fact it must) be skipped the first time, but it still must be done whenever the child in the template changes.
I don't think we're on the same page. I've been under the assumption that New(instance)
== Hydrate(instance:Clone())
- at least, that was the original design intention.
I can perhaps see some value in propagating changes from template to generated instances automatically, but this sounds like it could be a bottomless pit of inefficiency if done wrong. What are the performance implications of this? What's the overhead of binding to everything in the template? Will anyone actually use this widely enough to make the overhead generally worth it?
These are valid points. This was mainly in response to the quote:
or whatever "Thing" will be at any point in time?
which sounds a to me like changing the instance that gets hydrated/cloned when the child changes. This quote was before Hydrate was planned to be replaced, but I think it still has merit.
As for usecases, the main ones I can think of are scripts running in ReplicatedFirst or in tools, where the entire UI may not yet be replicated when you run New on it.
The performance questions are hard to answer. I personally don’t think it would be too big of an issue, but without it actually being made, we can only speculate. Perhaps we should try making a basic implementation of it to benchmark?
Following on from #34 - the original issue where
Hydrate
was introduced. Alongside it, a separate construct was also proposed (tentatively calledWithChild
, open to bikeshedding later) which would allow for hydrating an child by name:The original motive for implementing such a feature is to reduce the burden of working with deeply nested Hydrate trees:
There are some questions about whether this is a good idea to implement, or whether it's the correct problem to solve:
There were also some alternate propositions:
Either way, while the idea seems to be palatable to Fusion users, the exact design is highly up for debate. For this reason, it was decided to ship
Hydrate
separately and leave this out until we could decide on a reasonable design direction.Feel free to share your thoughts on this below - all thoughts are welcome, since this is still a large unresolved question :)