Skip to content

Views & Navigation

A view is any screen the app can navigate to. Components (Badge, DataGrid, Form, …) are the building blocks inside a view; a view is the thing you push onto the navigation stack.

dado components satisfy tview.Primitive, but that alone is not enough to be navigable. A view must implement nav.Component:

type Component interface {
tview.Primitive // it can render and receive input
Name() string // label shown in breadcrumbs
Start() // called when the view becomes active
Stop() // called when the view is hidden
Hints() []components.KeyHint // key hints shown in the bottom bar
}

dado components do not implement these four methods on their own, so you write a small view type that wraps them. The simplest way to satisfy tview.Primitive is to embed an existing primitive — usually a *tview.Flex — and then add the four methods:

package main
import (
"github.com/atterpac/dado/components"
"github.com/rivo/tview"
)
// HomeView embeds *tview.Flex (which gives it tview.Primitive) and adds the
// four nav.Component methods, so it can be pushed onto the page stack.
type HomeView struct {
*tview.Flex
}
func NewHomeView() *HomeView {
v := &HomeView{Flex: tview.NewFlex().SetDirection(tview.FlexColumn)}
// Compose components into the view.
v.AddItem(components.NewBadge("Ready").SetVariant(components.BadgeSuccess), 0, 1, false)
v.AddItem(components.NewBadge("3").SetVariant(components.BadgeError).SetPill(true), 0, 1, false)
return v
}
func (v *HomeView) Name() string { return "Home" }
func (v *HomeView) Start() {}
func (v *HomeView) Stop() {}
func (v *HomeView) Hints() []components.KeyHint {
return []components.KeyHint{
{Key: "Enter", Description: "Open"},
{Key: "q", Description: "Quit"},
}
}

The app owns a stack of views, reached through app.Pages(). Pushing a view makes it the active screen; popping returns to the previous one.

app := layout.NewApp(layout.AppConfig{ShowCrumbs: true})
app.Pages().Push(NewHomeView())
if err := app.Run(); err != nil {
log.Fatal(err)
}

Pages() exposes the full stack API:

MethodEffect
Push(c Component)Push c as the new top view
PushFactory(f ComponentFactory)Push a view built lazily by f (constructed on entry)
Pop()Remove the top view, returning to the one beneath
Replace(c Component)Swap the top view without growing the stack
Clear()Empty the stack
Depth() intNumber of views currently stacked
Current() ComponentThe active (top) view
OnStackChange(fn func(depth int))Run fn whenever the depth changes

A typical drill-down flow:

func (v *ListView) Hints() []components.KeyHint {
return []components.KeyHint{{Key: "Enter", Description: "Details"}}
}
// when a row is activated:
v.app.Pages().Push(NewDetailView(selected))
// inside the detail view, going back:
v.app.Pages().Pop()

Start() runs when a view becomes active (including when it is revealed again after a Pop), and Stop() runs when it is hidden. Use them to manage anything tied to the view being on screen — background polling, tickers, subscriptions:

func (v *FeedView) Start() {
v.cancel = make(chan struct{})
go v.pollFeed(v.cancel) // updates via app.QueueUpdateDraw
}
func (v *FeedView) Stop() {
close(v.cancel) // tear down the goroutine when navigated away
}

Stateless views can leave both methods empty.

When AppConfig.ShowCrumbs is true, the top bar shows a breadcrumb trail built from each view’s Name() as the stack grows and shrinks — Home › Users › Details — giving users a sense of where they are with no extra wiring.

A view fills the region between the optional top and bottom bars. Set those once on the app and they persist across every navigation:

app := layout.NewApp(layout.AppConfig{
TopBar: myStatusBar, // any tview.Primitive
BottomBar: myMenu,
ShowCrumbs: true,
})

The bottom bar is where each view’s Hints() are surfaced, so navigating between views automatically updates the visible key hints.