Quenty / NevermoreEngine

ModuleScript loader with reusable and easy unified server-client modules for faster game development on Roblox
https://quenty.github.io/NevermoreEngine/
MIT License
408 stars 124 forks source link

Two-way values for Blend with Blend.Bind(prop) #268

Closed OttoHatt closed 2 years ago

OttoHatt commented 2 years ago

Svelte is a frontend web library, with a feature I really like called Bindings. They allow you to define information as flowing both ways on a component's property.

Usage would be [Blend.Bind(string)] = ValueObject<any>. It would simply be a shorthand for defining [prop] = valueBase and [Blend.OnChange(prop)] = valueBase.

-- Before:
local state = Blend.State("hi")
Blend.New "TextBox" {
    ...
    Text = state;
    [Blend.OnChange "Text"] = state;
    ...
}:Subscribe()

-- After:
-- Identical functionality.
-- [Blend.Bind(string)] = ValueObject<any>.
local state = Blend.State("hi")
Blend.New "TextBox" {
    ...
    [Blend.Bind "Text"] = state;
    ...
}:Subscribe()

In this example, both will function identically; setting the textbox contents to "hi" immediately, then updating the value of the state from a GetPropertyChangedSingnal("Text") as the user enters text. And when the value of state changes, the property will be set again.

-- We can modify state and get dynamic reactions, just like normal!
local txState = Blend.state("")

Blend.New "Frame" {
    [Blend.Children] = {
        Blend.New "TextBox" {
            [Blend.Bind "Text"] = txState,
        },
        Blend.New "TextLabel" {
            Text = txState,
        },
        Blend.New "TextButton" {
            Text = "Reset",
            [Blend.OnEvent("Activated")] = function()
                txState.Value = ""
            end,
        }
    }
}
:Subscribe()

This could even be used to deprecate ValueObject-like classes as event handlers (as it isn't a very intuitive API imo. It feels like hidden functionality, as with typical roblox :Connect you think it'd only take a Callback)

local sizeState = Blend.state(nil)

Blend.New "Frame" {
    -- Potentially confusing.
    -- Don't we connect function callbacks to a changed event?
    [Blend.OnChange("AbsoluteSize")] = sizeState;
    -- Better?
    -- But we must note that unlike .OnChange, this will immediately set the value to what's stored in the sizeState ValueObject.
    -- However I think that's fine, as it'll typically be initialised to nil, so it won't change from the default property.
    -- So I think for 99% of cases, .Bind will be a drop-in replacement and won't break any code.
    [Blend.Bind("AbsoluteSize")] = sizeState;
}:Subscribe()
Quenty commented 2 years ago

Bindings can explicitly be two way in this scenario. Right now the canonical way to do bindings is this:

local state = Instance.new("StringValue")
state.Text = "Hi"

Blend.New "TextBox" {
      Text = state;
      [Blend.OnChange("Text")] = state;
}

Note this nicely allows for multi-directional bindings in an explicit way.

BInd can be a nice way to shortcut this, at some confusion to the user on when to use one over the other.

I'll consider this.

OttoHatt commented 2 years ago

Yep, been thinking about this since posting the issue and, I think defining it explicitly as two statements is better.

The concern I had that this fixes is that [...] =ValueObject is not great for readability; you can't tell at a glance if it's a function or ValueObject. However, I now think this is fine as it's short to write - UI code doesn't need to get any messier!