Views and grids

library(blockr.core)
library(blockr.dock)

A dock_board’s arrangement is two independent things, and it takes two constructor arguments to match:

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.

The big picture

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.

Grid syntax

You describe geometry with dock_grid() (and its helpers panels() and group()). Two rules cover everything:

  1. List nesting alternates orientation. dock_grid()’s top level lays its children out horizontally; one level of list() nesting flips to vertical; another flips back, and so on.
  2. Character vectors create tabs. A vector of IDs shares one DockView panel as tabs; a list() of IDs is split into separate panels.

One panel

A single ID fills the whole view:

new_dock_board(
  blocks = c(a = new_dataset_block()),
  grids = list(Page = dock_grid("a"))
)
┌────────────────────────────┐
│  Page  [+]                 │
├────────────────────────────┤
│                            │
│            a               │
│                            │
└────────────────────────────┘

The view is named after the grid’s list name (Page), and its members are that grid’s panels (a).

Two panels side by side

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       │
│             │              │
└─────────────┴──────────────┘

Two panels stacked vertically

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.

Tabs (multiple panels in one)

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.

Nested grids

Combine the two rules to build any arrangement.

Two columns, the right one stacked

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       │
└─────────────┴──────────────┘

Two columns, both stacked

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       │
└─────────────┴──────────────┘

Three rows in one column

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       │
└─────────────┴──────────────┘

Multiple views (pages)

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.

Display names distinct from the id

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.

Choosing the initially active view

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.

Views without 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.

Putting it all together

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%

Custom split ratios and active tabs

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:

A 30/70 split

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%

Stacking with sizes

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%
│                            │
└────────────────────────────┘

Choosing the open tab

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     │
└────────────────────────────┘

Sizes inside a nested branch

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.

Cheat-sheet

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

Where to go from here