Skip to content

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.NewString creates a string binding that holds its own value. Set stores a new value and Get reads it. Both return an error, which conversions use to report a value that cannot be converted.
  • widget.NewEntryWithData shows the binding’s value and sets it as the user types.
  • binding.NewSprintf makes 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

ConstructorBinding
widget.NewLabelWithDatabinding.String
widget.NewEntryWithDatabinding.String
widget.NewSelectWithDatabinding.String
widget.NewCheckWithDatabinding.Bool
widget.NewSliderWithDatabinding.Float
widget.NewProgressBarWithDatabinding.Float
widget.NewListWithDatabinding.DataList
widget.NewGridWrapWithDatabinding.DataList
widget.NewTreeWithDatabinding.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, IntToString and BoolToString convert to text, and their …WithFormat forms take a fmt format. 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, StringToBool and StringToURI go the other way: they read a string binding as a number, a boolean or a URI.
  • IntToFloat and FloatToInt convert 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)
}