Skip to content

Master-Detail

The master-detail pattern shows an entity list alongside a detail pane. Selecting an item in the list reveals its details without leaving the page. This page covers configuration, the selection model, route-driven detail panes, panel modes, and keyboard navigation.

Overview

The master-detail pattern is the default experience on tablet and desktop when EntitySelectionMode.responsive is configured. On phones, selecting a row navigates to a full-page detail route instead.

Configuration

Enable master-detail through EntityRouting<T>:

dart
final courseConfig = EntityConfiguration<Course>(
  metadata: EntityMetadata(
    identifier: 'courses',
    name: 'Course',
    pluralName: 'Courses',
    icon: FluentIcons.book_24_regular,
  ),
  api: const CourseApi(),
  routing: EntityRouting<Course>(
    path: NavigationPathBuilder.collection(prefix: '/lms/courses'),
    builder: StandardRouteBuilder<Course>(),
    mode: EntitySelectionMode.responsive,
  ),
  layouts: EntityLayouts<Course>(
    list: [courseTableLayout],
    details: [
      courseDetailLayout,
      courseParticipantsLayout,
      EntityVersionLayout<Course>(),
      EntityAuditLayout<Course>(),
    ],
  ),
  actions: EntityActions<Course>(
    inline: StandardEntityActions.inline<Course>(),
    header: StandardEntityActions.header<Course>(),
  ),
);

The entries in EntityLayouts.details become the tabs in the detail pane. Each one is an EntityLayout<T> with its own icon, title, optional Authorize gate, and build method.

Workspace Shells

Two shells host the master-detail layout:

  • StandardRouteBuilder<T> — the standard shell. Renders the list and detail in a single EntityShellLayout.
  • EntityWorkspaceRouteBuilder<T> — the newer workspace shell. Mounts the list in EntityWorkspace<T> with a generated EntityWorkspaceConfig, enabling workspace-wide chrome, side panes, and slot-based composition.

Use the workspace builder when you want richer chrome (saved-view state, draft banners, workspace lifecycle hooks, embedded sub-resources, etc.); use StandardRouteBuilder for the compact standard shell.

Selection Model

Selection lives on the EntityListController<T> that backs the list. Read it through EntityProvider:

dart
final controller = EntityProvider.listControllerOf<Course>(context);

// Single-selection (default)
final current = controller.selectedEntity; // Course?

// Multi-selection (when enableMultiSelect: true)
final isMulti = controller.multiSelectMode;
final selected = controller.selectedEntities; // Set<Course>

Single Selection

The default. Clicking a row sets selectedEntity. The detail pane updates without a navigation event. Clicking the same row again deselects it (and closes the pane).

Multi-Selection

When the layout sets enableMultiSelect: true, checkboxes appear and selectedEntities accumulates. The detail pane is replaced by the batch action bar (see Batch Operations).

Route-Driven Detail Panes

Detail tabs are routed, not stateful. Current navigation uses NavSlice: tab identifies the top-level partition (approved, drafts), while section identifies the detail subsection. Each detail tab adds ?section=<identifier> to the URL so the user can deep-link, share, or refresh without losing their place:

dart
// Build a URL that opens a course at the Participants tab
final route = vyuh.entity?.getConfig<Course>()?.route;
final url = route!.view(
  courseId,
  const NavSlice(section: 'participants'),
);
context.go(url);

The route builder reads the section query parameter and selects the corresponding EntityLayout<T> from EntityLayouts.details. Tabs gated by Authorize are filtered out of the bar before the route resolves.

Panel Modes

Docked (desktop, > 1200px)

Docked master-detail layoutOn wide screens, the sidebar, course table, and detail pane are visible side by side. The detail pane includes tabs across the top.SidebarCourse listtableDetail paneInfoPeopleAudit

Both surfaces are visible at once. The pane resizes with the window; the table column visibility adapts to the remaining space.

Overlay (tablet, 600 - 1200px)

The detail pane floats over the list. Tapping outside the pane closes it. The list stays visible but dimmed.

Full-Page (phone, < 600px, or EntitySelectionMode.navigate)

Selecting a row navigates to the detail route at /lms/courses/:id. The back button returns to the list.

Responsive Transitions

The shell transitions panel modes automatically when the window crosses a breakpoint:

The selected entity is preserved across transitions when possible.

Detail Tabs

dart
final courseDetailLayout = EntityDetailLayout<Course>(
  identifier: 'details',
  title: 'Details',
  icon: FluentIcons.info_24_regular,
  build: (context, course) => CourseDetailView(course: course),
);

final courseParticipantsLayout = EntityDetailLayout<Course>(
  identifier: 'participants',
  title: 'Participants',
  icon: FluentIcons.people_24_regular,
  authorize: Authorize.permission('lms.enrollments.view'),
  build: (context, course) => CourseParticipantsView(course: course),
);

Tabs that fail their authorize check are hidden from the tab strip entirely.

Keyboard Navigation

The shell registers shortcuts on desktop:

KeysAction
Cmd/Ctrl + KOpen the command palette
J / KMove selection down / up in the list
EnterOpen the focused row's detail (or first tab)
EscClear the selection / close the detail pane
19Switch detail tabs

Override or extend these by registering shortcuts at the Shortcuts / Actions widgets that wrap EntityShellLayout / EntityWorkspace.

Next Steps