Quenty/NevermoreEngine

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

OttoHatt opened this issue · 2 comments

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()

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.

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!