A dock_board’s arrangement is two independent things,
and it takes two constructor arguments to match:
views — the structure: which
panels belong to each view (the global tabs across the top of the app)
and the view’s display name. One dock_view per view.grids — the geometry: how
each view’s panels are split, nested, tabbed and sized. One
dock_grid per view.This vignette covers the grid syntax you use to describe geometry, how the two slots fit together at construction, and the patterns you’ll reach for most.
A board stores structure and geometry as two aligned slots, keyed by
view id — read them with board_views() and
board_grids(). They are kept apart on purpose: membership
is owned by the update lifecycle (add / remove a block), geometry by the
client (you drag a sash, the board records it). A panel is
placed in a view when it is in both slots; the two are
reconciled only where the placement is read.
There is a third representation you should know the name of but will
rarely touch: dock_layout. It is
dockView’s own form — the
{grid, panels, activeGroup} payload the front end renders
and echoes back. It is the wire type at the client boundary, not
something you author with. Our model is dock_view +
dock_grid; dock_layout is what crosses to
dockView (?dock_layout).
The constructor is forgiving about which slot you fill:
| You pass | Result |
|---|---|
grids only |
one view per grid (id = the list name), members taken from the grid’s panels |
views only |
structure with no explicit geometry; each view renders a flat default over its members |
| both | aligned by id: views sets membership + names,
grids sets geometry |
| neither | a single default view (sidebar + main when there are extensions) |
So for a layout with real geometry you usually just pass
grids; the views come along for free. Reach for
views when you want a display name that differs from the
id, or a view without an explicit arrangement.
You describe geometry with dock_grid() (and its helpers
panels() and group()). Two rules cover
everything:
dock_grid()’s top level lays its children out horizontally;
one level of list() nesting flips to vertical; another
flips back, and so on.list() of IDs is split
into separate panels.A single ID fills the whole view:
┌────────────────────────────┐
│ Page [+] │
├────────────────────────────┤
│ │
│ a │
│ │
└────────────────────────────┘
The view is named after the grid’s list name (Page), and
its members are that grid’s panels (a).
Two top-level children → two columns split horizontally:
new_dock_board(
blocks = c(a = new_dataset_block(), b = new_head_block()),
grids = list(Page = dock_grid("a", "b"))
)┌────────────────────────────┐
│ Page [+] │
├─────────────┬──────────────┤
│ │ │
│ a │ b │
│ │ │
└─────────────┴──────────────┘
Wrap the children in one extra layer of
list() to introduce a vertical split:
new_dock_board(
blocks = c(a = new_dataset_block(), b = new_head_block()),
grids = list(Page = dock_grid(list("a", "b")))
)┌────────────────────────────┐
│ Page [+] │
├────────────────────────────┤
│ a │
├────────────────────────────┤
│ b │
└────────────────────────────┘
The outer level still describes a horizontal split, but with a single
child that “split” is one full-width column. The inner
list("a", "b") is at depth 1, so it splits
vertically: a stacks on top of
b.
Use a character vector (not a list) to put several panels in one DockView panel as tabs:
new_dock_board(
blocks = c(a = new_dataset_block(), b = new_head_block()),
grids = list(Page = dock_grid(c("a", "b")))
)┌────────────────────────────┐
│ Page [+] │
├────────────────────────────┤
│ ┌──────┐┌──────┐ │
│ │ a ││ b │ │
│ └──────┘└──────┘ │
│ │
│ (a is shown, b is a tab) │
│ │
└────────────────────────────┘
dock_grid(list("a", "b")) (panels split) and
dock_grid(c("a", "b")) (panels tabbed) read almost the same
but produce very different UIs — the list/vector distinction flips
between split a panel and tabify a panel.
Combine the two rules to build any arrangement.
new_dock_board(
blocks = c(
a = new_dataset_block(),
b = new_head_block(),
c = new_head_block()
),
grids = list(Page = dock_grid("a", list("b", "c")))
)┌────────────────────────────┐
│ Page [+] │
├─────────────┬──────────────┤
│ │ b │
│ a ├──────────────┤
│ │ c │
└─────────────┴──────────────┘
new_dock_board(
blocks = c(
a = new_dataset_block(),
b = new_head_block(),
c = new_head_block(),
d = new_head_block()
),
grids = list(Page = dock_grid(list("a", "b"), list("c", "d")))
)┌────────────────────────────┐
│ Page [+] │
├─────────────┬──────────────┤
│ a │ c │
├─────────────┼──────────────┤
│ b │ d │
└─────────────┴──────────────┘
A third level of nesting flips back to horizontal inside the vertical stack — a row holding two panels side by side:
new_dock_board(
blocks = c(
a = new_dataset_block(),
b = new_head_block(),
c = new_head_block()
),
grids = list(Page = dock_grid(list("a", list("b", "c"))))
)┌────────────────────────────┐
│ Page [+] │
├────────────────────────────┤
│ a │
├─────────────┬──────────────┤
│ b │ c │
└─────────────┴──────────────┘
Give grids more than one entry; each list name becomes a
separate page in the view-nav dropdown, and each becomes its own
view:
new_dock_board(
blocks = c(a = new_dataset_block(), b = new_head_block()),
extensions = new_edit_board_extension(),
grids = list(
Analysis = dock_grid("a", "b"),
Editor = dock_grid("edit_board")
)
)Analysis view (active by default — the first view
wins):
┌────────────────────────────┐
│ Analysis [+] ← view nav │
├────────────────────────────┤
│ a │ b │
└────────────────────────────┘
Switching to Editor via the dropdown:
┌────────────────────────────┐
│ Editor [+] │
├────────────────────────────┤
│ │
│ edit │
│ │
└────────────────────────────┘
Each grid follows exactly the same syntax as the single-view form: character vectors for tabs, nested lists for splits.
A grid’s list name is the view id, and it doubles as the
display label. When you want a label that differs from the id — or a
view with no geometry at all — fill the views slot too. A
dock_view() carries the membership and the display name;
align it to the grid by id:
new_dock_board(
blocks = c(a = new_dataset_block(), b = new_head_block()),
views = list(
main = dock_view(c("a", "b"), name = "Analysis")
),
grids = list(
main = dock_grid("a", "b")
)
)The nav shows Analysis; the stable id
main is what active, renames and updates
address.
The first view is active by default. To start elsewhere, name it with
new_dock_board(active = ), using its view id:
new_dock_board(
blocks = c(a = new_dataset_block(), b = new_head_block()),
extensions = new_edit_board_extension(),
grids = list(
Analysis = dock_grid("a", "b"),
Editor = dock_grid("edit_board")
),
active = "Editor"
)┌────────────────────────────┐
│ Editor [+] ← starts here│
├────────────────────────────┤
│ edit │
└────────────────────────────┘
Which view is active is a single field on the views collection, keyed
by id — it belongs to the collection, not to any one view, so it is
always exactly one. Change it at runtime with
active_view(board) <- id, and read it back with
active_view(board); index
board_grids(board)[[id]] if you then need that view’s
geometry.
Pass views alone (bare member vectors coerce to
dock_views) for structure with no explicit arrangement.
Each such view renders a flat default over its members until the client
arranges it (which the board then records):
new_dock_board(
blocks = c(a = new_dataset_block(), b = new_head_block()),
views = list(
Analysis = c("a", "b"),
Empty = character()
)
)An empty view shows the same watermark prompt as an empty board, scoped to that tab.
A pot-pourri: multiple views, nested grids, tabbed panels, an extension sidebar, and an explicit active view.
new_dock_board(
blocks = c(
raw = new_dataset_block(),
cleaned = new_head_block(),
summary = new_head_block(),
plot1 = new_scatter_block(),
plot2 = new_scatter_block()
),
extensions = new_edit_board_extension(),
links = list(
new_link("raw", "cleaned", "data"),
new_link("cleaned", "summary", "data"),
new_link("cleaned", "plot1", "data"),
new_link("cleaned", "plot2", "data")
),
grids = list(
Data = dock_grid(
"edit_board",
panels("raw", "cleaned", active = "cleaned"),
sizes = c(0.25, 0.75)
),
Analysis = dock_grid(
group("summary", "plot1", sizes = c(0.4, 0.6)),
"plot2",
sizes = c(0.55, 0.45)
),
Charts = dock_grid(panels("plot1", "plot2"))
),
active = "Charts"
)Three views; Charts is active, so the user lands there
first.
Charts (active on load): one tabbed panel,
plot1 shown, plot2 a tab.
┌────────────────────────────┐
│ Charts [+] │
├────────────────────────────┤
│ ┌───────┐┌───────┐ │
│ │ plot1 ││ plot2 │ │
│ └───────┘└───────┘ │
│ (plot1 shown, plot2 tab) │
└────────────────────────────┘
Data: a slim extension sidebar on the left, a wide
right column with raw / cleaned tabbed.
active = "cleaned" opens the cleaned tab;
sizes = c(0.25, 0.75) carves out the narrow sidebar.
┌────────────────────────────┐
│ Data [+] │
├──────┬─────────────────────┤
│ │┌─────┐┌─────────┐ │
│ edit ││ raw ││ cleaned │ │
│ │└─────┘└─────────┘ │
│ │ (cleaned shown) │
└──────┴─────────────────────┘
25% 75%
Analysis: two columns at 55/45. The left is a nested
group() with a 40/60 vertical split (summary over plot1);
the right is plot2.
┌────────────────────────────┐
│ Analysis [+] │
├─────────────┬──────────────┤
│ summary │ │ 40% / 55%
├─────────────┤ plot2 │
│ plot1 │ │ 60% / 45%
└─────────────┴──────────────┘
55% 45%
The bare list-of-IDs form splits space evenly and opens the first tab. For non-default ratios or a non-default open tab, use the typed helpers:
dock_grid(..., sizes = c(...)) — sizes parallel to the
root children. They are relative; they need not sum to 1
(they are normalised for you).group(..., sizes = c(...)) — the same, for a nested
branch below the root.panels(..., active = "...") — a tabbed leaf with an
explicit open tab.sizes runs parallel to the children in ....
Two children split horizontally; two sizes make the split uneven:
new_dock_board(
blocks = c(a = new_dataset_block(), b = new_head_block()),
grids = list(
Main = dock_grid("a", "b", sizes = c(0.3, 0.7))
)
)┌────────────────────────────┐
│ Main [+] │
├──────────┬─────────────────┤
│ │ │
│ a │ b │
│ │ │
└──────────┴─────────────────┘
30% 70%
Same idea, vertical: orientation = "vertical" makes the
root split run top-to-bottom.
new_dock_board(
blocks = c(a = new_dataset_block(), b = new_head_block()),
grids = list(
Main = dock_grid("a", "b",
orientation = "vertical",
sizes = c(0.25, 0.75))
)
)┌────────────────────────────┐
│ Main [+] │
├────────────────────────────┤
│ a │ 25%
├────────────────────────────┤
│ │
│ b │ 75%
│ │
└────────────────────────────┘
panels() builds a tabbed leaf. Without
active, the first ID opens — same as a bare character
vector. Pass active to open a different tab:
new_dock_board(
blocks = c(
a = new_dataset_block(),
b = new_head_block(),
c = new_head_block()
),
grids = list(
Main = dock_grid(panels("a", "b", "c", active = "b"))
)
)┌────────────────────────────┐
│ Main [+] │
├────────────────────────────┤
│ ┌────┐┌──────┐┌────┐ │
│ │ a ││ b ││ c │ │
│ └────┘└──────┘└────┘ │
│ ↑ │
│ b is open by default │
└────────────────────────────┘
group() is an inner list(...) with its own
sizes — use it for ratios on any non-root branch:
new_dock_board(
blocks = c(
a = new_dataset_block(),
b = new_head_block(),
c = new_head_block()
),
grids = list(
Main = dock_grid(
"a",
group("b", "c", sizes = c(0.6, 0.4)),
sizes = c(0.3, 0.7)
)
)
)┌────────────────────────────┐
│ Main [+] │
├────────┬───────────────────┤
│ │ b │ inner: 60% top
│ ├───────────────────┤
│ a │ c │ inner: 40% bottom
│ │ │
└────────┴───────────────────┘
30% 70%
Outer sizes = c(0.3, 0.7) controls the root split; inner
group(..., sizes = c(0.6, 0.4)) controls the right column’s
stack.
Geometry (dock_grid(...)):
| Goal | Syntax |
|---|---|
| One panel | dock_grid("a") |
| Two side-by-side | dock_grid("a", "b") |
| Two stacked | dock_grid(list("a", "b")) |
| Tabbed panel | dock_grid(c("a", "b")) |
| Sidebar + main | dock_grid("ext", "main") |
| Two columns, both stacked | dock_grid(list("a", "b"), list("c", "d")) |
| Custom split ratio | dock_grid("a", "b", sizes = c(0.3, 0.7)) |
| Custom open tab | dock_grid(panels("a", "b", active = "b")) |
| Vertical top-level split | dock_grid("a", "b", orientation = "vertical") |
Board (new_dock_board(...)):
| Goal | Syntax |
|---|---|
| One view with geometry | grids = list(Page = dock_grid(...)) |
| Several views | grids = list(A = dock_grid(...), B = dock_grid(...)) |
| A display name != id | views = list(id = dock_view(members, name = "...")) |
| Structure, no geometry | views = list(Page = c("a", "b")) |
Start on view B |
new_dock_board(grids = list(A = ..., B = ...), active = "B") |
| Empty starter view | views = list(Page = character()) |
?dock_grid for the geometry syntax (with
panels() and group()).?dock_view for the structure type, and
?active_view for switching the active view at runtime.?dock_layout for the dockView boundary form, and
as_dock_grid() / as_dock_layout() for the
casts across it.