Data binding
A binding is a value that tells its listeners when it changes. Widgets that are bound to it show its current value. Input widgets also write back to it when the user changes them.
Bind a string
name := binding.NewString()
_ = name.Set("World")
entry := widget.NewEntryWithData(name) // typing updates name
greeting := widget.NewLabelWithData(
binding.NewSprintf("Hello, %s!", name), // follows name
)binding.NewStringcreates a string binding that holds its own value.Setstores a new value andGetreads it. Both return an error, which conversions use to report a value that cannot be converted.widget.NewEntryWithDatashows the binding’s value and sets it as the user types.binding.NewSprintfmakes a new string from a format and other bindings. The label follows every keystroke.
There are bindings for Bool, Bytes, Float, Int, Rune, String,
URI and Untyped (any value). For your own types, use
binding.NewItem
with a function that says when two values are equal.
Widgets that accept bindings
| Constructor | Binding |
|---|---|
widget.NewLabelWithData | binding.String |
widget.NewEntryWithData | binding.String |
widget.NewSelectWithData | binding.String |
widget.NewCheckWithData | binding.Bool |
widget.NewSliderWithData | binding.Float |
widget.NewProgressBarWithData | binding.Float |
widget.NewListWithData | binding.DataList |
widget.NewGridWrapWithData | binding.DataList |
widget.NewTreeWithData | binding.DataTree |
The label, entry, select, check, slider and progress bar also have a Bind
method to connect an existing widget, and Unbind to disconnect it. Calling
Bind again replaces the previous binding.
Convert between types
A widget needs a binding of its own type. A label needs a string, for example, while your value may be a float. Conversion functions wrap one binding in another:
volume := binding.NewFloat()
_ = volume.Set(0.3)
slider := widget.NewSliderWithData(0, 1, volume)
bar := widget.NewProgressBarWithData(volume)
percent := widget.NewLabelWithData(
binding.FloatToStringWithFormat(volume, "Volume: %.2f"),
)The slider, the progress bar and the label all share volume. Dragging the
slider moves the bar and changes the text.
FloatToString,IntToStringandBoolToStringconvert to text, and their…WithFormatforms take afmtformat. Bind one to an entry to let the user type a number. Text that does not parse is not written to the source, and the entry shows it as invalid.StringToFloat,StringToInt,StringToBoolandStringToURIgo the other way: they read a string binding as a number, a boolean or a URI.IntToFloatandFloatToIntconvert between numbers.
Conversions work both ways: setting the converted binding parses the value and sets the source.
Boolean bindings can be combined:
muted := binding.NewBool()
mute := widget.NewCheckWithData("Mute", muted)
loud := widget.NewCheckWithData("Sound on", binding.Not(muted))binding.Not
inverts a binding, so the two checks always disagree.
binding.And
and binding.Or
combine several. Setting an And or Or binding sets every binding it was
made from to the same value.
Update from a background goroutine
You can call Set from any goroutine. Bellui runs the listeners, including
the widget updates, on the UI goroutine:
clock := binding.NewString()
go func() {
for t := range time.Tick(time.Second) {
_ = clock.Set(t.Format("15:04:05")) // safe from any goroutine
}
}()This is the main reason to prefer a binding over calling SetText from a
goroutine, which would need bellui.Do.
Listen yourself
To run your own code when a binding changes, add a listener.
binding.NewDataListener
turns a function into one:
changes := 0
counter := widget.NewLabel("")
onName := binding.NewDataListener(func() {
changes++
v, _ := name.Get()
counter.SetText(fmt.Sprintf("%q, change %d", v, changes))
})
name.AddListener(onName)
// Later, to stop listening: name.RemoveListener(onName)The listener runs once as soon as it is added, with the current value, and
again after each change. It always runs on the UI goroutine, so it can update
widgets. Keep the listener value if you want to remove it later with
RemoveListener. Adding the same listener twice has no effect.
A binding only notifies when the value actually changes. Setting the value it already has does nothing.
The full program:
// Data binding: widgets that share bound values, conversions between types,
// and updates from a background goroutine.
package main
import (
"fmt"
"time"
"github.com/belluikit/bellui"
"github.com/belluikit/bellui/app"
"github.com/belluikit/bellui/container"
"github.com/belluikit/bellui/data/binding"
"github.com/belluikit/bellui/widget"
)
func main() {
a := app.New()
w := a.NewWindow("Data binding")
name := binding.NewString()
_ = name.Set("World")
entry := widget.NewEntryWithData(name) // typing updates name
greeting := widget.NewLabelWithData(
binding.NewSprintf("Hello, %s!", name), // follows name
)
volume := binding.NewFloat()
_ = volume.Set(0.3)
slider := widget.NewSliderWithData(0, 1, volume)
bar := widget.NewProgressBarWithData(volume)
percent := widget.NewLabelWithData(
binding.FloatToStringWithFormat(volume, "Volume: %.2f"),
)
muted := binding.NewBool()
mute := widget.NewCheckWithData("Mute", muted)
loud := widget.NewCheckWithData("Sound on", binding.Not(muted))
clock := binding.NewString()
go func() {
for t := range time.Tick(time.Second) {
_ = clock.Set(t.Format("15:04:05")) // safe from any goroutine
}
}()
changes := 0
counter := widget.NewLabel("")
onName := binding.NewDataListener(func() {
changes++
v, _ := name.Get()
counter.SetText(fmt.Sprintf("%q, change %d", v, changes))
})
name.AddListener(onName)
// Later, to stop listening: name.RemoveListener(onName)
w.SetContent(container.NewVBox(
entry, greeting,
slider, bar, percent,
mute, loud,
widget.NewLabelWithData(clock),
counter,
))
bellui.ShowAndRun(w)
}