Skip to content
Lists, structs and preferences

Lists, structs and preferences

Bound lists

A list binding holds a slice and notifies its listeners when items are added, removed or changed. Pass it to widget.NewListWithData:

tasks := binding.NewStringList()
_ = tasks.Set([]string{"Write the report", "Book the room"})

list := widget.NewListWithData(tasks,
	func() core.CanvasObject {
		return widget.NewLabel("template")
	},
	func(item binding.DataItem, row core.CanvasObject) {
		row.(*widget.Label).Bind(item.(binding.String))
	},
)

The list asks for a row template once per visible row, then calls the update function with the binding for one item and the row to fill. Rows are reused as the list scrolls. Calling Bind on the row’s label replaces whatever it was bound to before, so a reused row always shows the right item.

Change the list through the binding, and the widget follows:

n := 0
add := widget.NewButton("Add", func() {
	n++
	_ = tasks.Append(fmt.Sprintf("New task %d", n))
})
removeFirst := widget.NewButton("Remove first", func() {
	if tasks.Length() == 0 {
		return
	}
	first, _ := tasks.GetValue(0)
	_ = tasks.Remove(first)
})

A list binding has Append, Prepend, Remove, Set (replace all items), SetValue (replace one item), Get, GetValue and Length. Remove takes a value, not an index, and removes one item equal to it.

There are list bindings for every value type (NewStringList, NewFloatList, NewIntList and so on), and binding.NewList for any type. Trees work the same way with NewStringTree and widget.NewTreeWithData, and widget.NewGridWrapWithData shows a list as a wrapping grid.

Bind to your own variable

The Bind… functions bind to a variable you already have instead of holding their own value:

count := 0
bound := binding.BindInt(&count) // reads and writes count

label := widget.NewLabelWithData(binding.IntToString(bound))
viaBinding := widget.NewButton("Add via binding", func() {
	v, _ := bound.Get()
	_ = bound.Set(v + 1) // updates count and the label
})
direct := widget.NewButton("Add directly", func() {
	count++
	_ = bound.Reload() // tell the binding that count changed
})

Setting the binding writes to the variable. The reverse is not automatic: when your code changes the variable directly, call Reload so the binding notices and tells its listeners. BindString, BindFloat, BindBool and the other Bind… functions, as well as BindStringList and the other list forms, all work this way.

Bind struct fields

binding.BindStruct takes a pointer to a struct and gives one binding per exported field:

type contact struct {
	Name  string
	Email string
}
person := &contact{Name: "Ada", Email: "ada@example.com"}
fields := binding.BindStruct(person)

nameItem, _ := fields.GetItem("Name")
emailItem, _ := fields.GetItem("Email")
form := widget.NewForm(
	widget.NewFormItem("Name", widget.NewEntryWithData(nameItem.(binding.String))),
	widget.NewFormItem("Email", widget.NewEntryWithData(emailItem.(binding.String))),
)

GetItem returns the field’s binding as a binding.DataItem. Bool, int, float and string fields come back as binding.Bool, binding.Int, binding.Float and binding.String, so assert to the type you need. Editing the entries changes person. After you change person in code, call Reload on the struct binding.

For a map[string]any, use binding.NewUntypedMap or binding.BindUntypedMap.

Bind a preference

Preference bindings read their value from the app’s preferences and save every change back:

nickname := binding.BindPreferenceString("nickname", a.Preferences())
nickEntry := widget.NewEntryWithData(nickname) // saved as you type

The value is restored the next time the app starts. Preferences need an app ID, so create the app with app.NewWithID. Bindings for the same key share one underlying value, and binding one key with two different types logs an error.

There are BindPreferenceBool, …Float, …Int and …String, and list forms such as BindPreferenceStringList.

The full programs:

// A bound list shown in a widget.List, with items added and removed at runtime.
package main

import (
	"fmt"

	"github.com/belluikit/bellui"
	"github.com/belluikit/bellui/app"
	"github.com/belluikit/bellui/container"
	"github.com/belluikit/bellui/core"
	"github.com/belluikit/bellui/data/binding"
	"github.com/belluikit/bellui/widget"
)

func main() {
	a := app.New()
	w := a.NewWindow("Bound list")

	tasks := binding.NewStringList()
	_ = tasks.Set([]string{"Write the report", "Book the room"})

	list := widget.NewListWithData(tasks,
		func() core.CanvasObject {
			return widget.NewLabel("template")
		},
		func(item binding.DataItem, row core.CanvasObject) {
			row.(*widget.Label).Bind(item.(binding.String))
		},
	)

	n := 0
	add := widget.NewButton("Add", func() {
		n++
		_ = tasks.Append(fmt.Sprintf("New task %d", n))
	})
	removeFirst := widget.NewButton("Remove first", func() {
		if tasks.Length() == 0 {
			return
		}
		first, _ := tasks.GetValue(0)
		_ = tasks.Remove(first)
	})

	w.SetContent(container.NewBorder(container.NewHBox(add, removeFirst), nil, nil, nil, list))
	bellui.ShowAndRun(w)
}
// Binding to variables you own, to struct fields and to preferences.
package main

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

type contact struct {
	Name  string
	Email string
}


func main() {
	a := app.NewWithID("com.example.binding")
	w := a.NewWindow("External data")

	count := 0
	bound := binding.BindInt(&count) // reads and writes count

	label := widget.NewLabelWithData(binding.IntToString(bound))
	viaBinding := widget.NewButton("Add via binding", func() {
		v, _ := bound.Get()
		_ = bound.Set(v + 1) // updates count and the label
	})
	direct := widget.NewButton("Add directly", func() {
		count++
		_ = bound.Reload() // tell the binding that count changed
	})

	person := &contact{Name: "Ada", Email: "ada@example.com"}
	fields := binding.BindStruct(person)

	nameItem, _ := fields.GetItem("Name")
	emailItem, _ := fields.GetItem("Email")
	form := widget.NewForm(
		widget.NewFormItem("Name", widget.NewEntryWithData(nameItem.(binding.String))),
		widget.NewFormItem("Email", widget.NewEntryWithData(emailItem.(binding.String))),
	)

	nickname := binding.BindPreferenceString("nickname", a.Preferences())
	nickEntry := widget.NewEntryWithData(nickname) // saved as you type

	w.SetContent(container.NewVBox(label, viaBinding, direct, form, nickEntry))
	bellui.ShowAndRun(w)
}