NSGo

Writing components

Parts and states

Hold no state the app owns, style every part, and tell assistive technology what the component is.

An unstyled component decides how a control behaves and leaves how it looks to whoever uses it. This page builds Toggle, a pill that is on or off, the way base builds its controls.

Controlled values

A control holds no value the app owns: the app passes it as a function and sets it from a callback. The component reads it where it is shown, so only that part updates when it changes.

// Toggle is a pill that is on or off. Its parts are the pill and a dot
// inside it, then its children, the label.
type Toggle struct {
	On       func() bool
	Disabled func() bool
	// OnChange receives the value a press asks for, the opposite of On's.
	OnChange func(on bool)

	Class        string
	Style        core.Style
	StyleFunc    func() core.Style
	DotClass     string
	DotStyle     core.Style
	DotStyleFunc func() core.Style
	Children     []core.Node
	Label        string
}

An app keeps the value in a signal: chips.Toggle{On: bold.Get, OnChange: bold.Set, Label: "Bold"}.

Parts

Each part a component draws takes a Class, a Style and a StyleFunc, named after the part (DotClass, DotStyle, DotStyleFunc). Style is fixed, StyleFunc follows the signals it reads, and the class goes under both, so an app can use whichever it writes.

A part takes the control’s state through PartStates rather than reporting its own, so checked: classes and Checked styles apply to it while assistive technology sees one control:

func (t Toggle) Render(*core.Scope) core.Node {
	on := func() bool { return t.On != nil && t.On() }
	off := func() bool { return t.Disabled != nil && t.Disabled() }
	shows := func() core.PartStates { return core.PartStates{Checked: on(), Disabled: off()} }
	dot := core.View{Class: t.DotClass, Style: t.DotStyle, StyleFunc: t.DotStyleFunc, PartStates: shows}
	return core.View{
		Class: t.Class, Style: t.Style, StyleFunc: t.StyleFunc,
		Role: core.RoleSwitch, Label: t.Label, Checked: on, Disabled: t.Disabled,
		OnPress: func(core.PressEvent) {
			if t.OnChange != nil {
				t.OnChange(!on())
			}
		},
		Children: append([]core.Node{dot}, t.Children...),
	}
}

An app styles it with classes alone:

chips.Toggle{
	On: bold.Get, OnChange: bold.Set, Label: "Bold",
	Class:    "flex-row items-center gap-2 rounded-full border border-zinc-300 px-3 py-1 checked:bg-blue-50",
	DotClass: "size-2 rounded-full bg-zinc-300 checked:bg-blue-500",
	Children: []core.Node{core.Text{Content: "Bold"}},
}

Accessibility

The root of a control says what it is: a Role, a Label when its text does not say it, a Hint for what activating it does, and Checked, Selected, Disabled or Expanded as they apply. Its parts stay silent. A control that moves between items with the arrows is one Tab stop, as a tab list is. Accessibility lists the roles and actions.

What to pass through

A component whose root is a view of its own takes that view’s OnLayout, Tooltip and TestID and hands them on, so apps measure, explain and test it as any view. Callbacks are optional: a nil OnChange makes a control that shows a value without changing it.

Edit this page on GitHub