GrDashboard
Overview
When to take it
- the layout is determined by the user — an analytics panel, a workspace, a summary for a particular role: the set and the arrangement of the widgets belong to a person rather than to the designer of the screen;
- the layout has to survive a reload —
useDashboardLayoutputs it intolocalStorageor onto the server through the storage interface; - a laptop and a tablet need different things — the layout is stored per breakpoint, and a missing one is derived from the neighbouring one above;
- the widgets are of different sizes —
minW/minH/maxW/maxHare declared by the widget itself, because only it knows that “this chart is unreadable below three rows”.
When to take something else
| Need | Take |
|---|---|
| The layout is fixed by the design | CSS Grid and GrCard |
| Two areas whose boundary is moved by the user | GrSplitter |
| The order of the elements in a single list | GrSortableList |
| Sections switched with tabs | GrTabs |
| Table rows with a changeable order of columns | GrDataTable |
| Where a widget takes its data from | granularity-datasource |
| What a widget draws its content with | granularity-charts / granularity-chrono |
A dashboard is not a way to lay out a static screen. If the user moves nothing, a grid of widgets gives an extra layer of state, a storage and an edit mode where CSS Grid would have been enough.
Examples 2 each with a live demo and its code
A finance overview that keeps its layout
Switch to «Edit», move and resize the widgets, then reload the page:
useDashboardLayoutsaves the layout per breakpoint and restores it after mounting, so the server and the first render agree. The markup only names the widgets byitem-id;minW/minHcome from the widgets, and breakpoints are measured on the container — twelve columns from 540 px, two below. «Reset» goes back to the initial layout.Finance overviewDefault layoutRevenue, October€48,210+12.4%Overdue€6,940-18.2%Cash in bank€182,400+3.1%Overdue invoices- Northwind TradersINV-0418 · 6 days late€1,284.50
- Contoso LtdINV-0415 · 9 days late€432.00
- Fabrikam InteriorsINV-0409 · 17 days late€3,210.00
- Litware IncINV-0402 · 24 days late€2,013.50
Cash runway14 monthsAt September's burn of €13,000 a month, before new contracts.Moving widgets between two boards
Drag a widget by its handle past the edge of one board into the other.
transferablelets a widget leave anddroppablelets one arrive — separate rights. The source removes the widget by itself; the receiver emitsitem-dropwith the cell, the breakpoint and its layout options, and the page places it withaddItem— so it lands where the drop shadow showed it.My boardMy queue12 open · 3 waiting on customerSLA breaches2 today · both on chatSatisfaction94% · 312 ratings this weekTeam boardTeam backlog87 open · oldest 4 daysOn callLena until 18:00, then Omar
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
breakpoints | GrDashboardBreakpoints | undefined | GR_DASHBOARD_BREAKPOINTS | The container width thresholds at which a breakpoint begins. |
cols | number | GrDashboardCols | undefined | GR_DASHBOARD_COLS | The number of columns: a map by breakpoint or a single number for all of them. |
initialBreakpoint | string | undefined | "lg" | The breakpoint of the first render. There is no container width on the server, so both the server and the first client render take it — the refinement arrives after mounting, and hydration does not diverge. |
rowHeight | number | undefined | undefined | The grid row height, px. |
gap | number | undefined | undefined | The gap between widgets, px. |
mode | GrDashboardMode | undefined | undefined | `view` — the grid only displays; `edit` — the handles appear. |
draggable | boolean | undefined | undefined | Widgets are moved by the handle in `edit` mode; a widget narrows the rule with its own prop. Not set — from `GrConfigProvider`, otherwise `true`. |
resizable | boolean | undefined | undefined | Widgets are resized by the corner in `edit` mode; a widget narrows the rule with its own prop. Not set — from `GrConfigProvider`, otherwise `true`. |
compact | GrDashboardCompaction | undefined | undefined | Compaction after a move: `vertical` pulls the widgets up, `horizontal` — to the left, `both` — in both directions, `none` leaves them where they were dropped. Not set — from `GrConfigProvider`, otherwise `vertical`. |
preventCollision | boolean | undefined | undefined | A collision cancels the move instead of pushing the neighbours. |
lazy | boolean | undefined | undefined | The widget content mounts once it enters the viewport. |
droppable | boolean | undefined | undefined | The grid accepts widgets dragged from the catalog. Works only in `mode="edit"`. The prop is needed where a page has several grids but only one should accept. |
transferable | boolean | undefined | undefined | A widget can be dragged out of the grid into a neighbouring dashboard. Separate from `droppable`: accepting and giving away are different permissions. An archive dashboard accepts widgets but gives none of its own away. |
layoutrequiredv-model | GrDashboardResponsiveLayout | — | The layout for each breakpoint. `v-model:layout`. |
ariaLabel | string | undefined | — | The name of the grid group. Not set — from the locale ("Dashboard"). |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | `GrDashboardItem` widgets — one per layout entry. |
empty | any | What to show while the layout has no widgets at all. |
Events
| Event | Signature | Description |
|---|---|---|
itemMove |
| — |
itemResize |
| — |
itemAutoResize |
| — |
layoutChange | (event: "layoutChange", value: GrDashboardLayout, breakpoint: string): void | — |
breakpointChange | (event: "breakpointChange", breakpoint: string, cols: number): void | — |
update:layout | (event: "update:layout", value: GrDashboardResponsiveLayout): void | — |
itemSettings | (event: "itemSettings", id: string): void | — |
itemDrop | (event: "itemDrop", event: GrDashboardDropEvent): void | — |
itemTransferOut | (event: "itemTransferOut", id: string, transfer: GrDashboardTransfer): void | — |
Methods
| Methods | Signature | Description |
|---|---|---|
breakpoint | string | The current breakpoint: before the first measurement of the container — `initialBreakpoint`. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrDashboardConfigurableProps, GrDashboardMode, GrDashboardItemBounds } from '@feugene/granularity-dashboard/components/GrDashboard'Filedist/types/components/GrDashboard/config.d.ts
const grDashboardConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrDashboard">;Filedist/types/components/GrDashboard/defaults.d.ts
interface GrDashboardConfigurableProps {
rowHeight: number;
gap: number;
mode: GrDashboardMode;
draggable: boolean;
resizable: boolean;
compact: GrDashboardCompaction;
preventCollision: boolean;
lazy: boolean;
droppable: boolean;
transferable: boolean;
}Filedist/types/components/GrDashboard/grDashboardStyles.d.ts
type GrDashboardMode = 'view' | 'edit';Filedist/types/components/shared/context.d.ts
interface GrDashboardItemBounds {
minW?: number;
minH?: number;
maxW?: number;
maxH?: number;
static?: boolean;
draggable?: boolean;
resizable?: boolean;
title?: string;
}interface GrDashboardDropEvent {
transfer: GrDashboardTransfer;
cell: GrDashboardCell;
breakpoint: GrDashboardBreakpoint;
options: GrDashboardMoveOptions;
}interface GrDashboardContext {
mode: ComputedRef<GrDashboardMode>;
cols: ComputedRef<number>;
draggable: ComputedRef<boolean>;
resizable: ComputedRef<boolean>;
lazy: ComputedRef<boolean>;
layout: ComputedRef<GrDashboardLayout>;
order: ComputedRef<string[]>;
itemFor: (id: string) => GrDashboardItemLayout | undefined;const GR_DASHBOARD_KEY: InjectionKey<GrDashboardContext>;function useGrDashboardContext(): GrDashboardContext | null;Filedist/types/components/GrDashboard/safelist.d.ts
const grDashboardSafelist: string[];Filedist/types/components/GrDashboard/GrDashboard.vue.d.ts
interface GrDashboardProps {
layout: GrDashboardResponsiveLayout;
breakpoints?: GrDashboardBreakpoints;
cols?: GrDashboardCols | number;
initialBreakpoint?: GrDashboardBreakpoint;
rowHeight?: number;
gap?: number;
mode?: GrDashboardMode;
draggable?: boolean;
resizable?: boolean;
compact?: GrDashboardCompaction;
preventCollision?: boolean;
lazy?: boolean;
droppable?: boolean;
transferable?: boolean;
ariaLabel?: string;
}interface GrDashboardEmits {
(e: 'update:layout', value: GrDashboardResponsiveLayout): void;
(e: 'layoutChange', value: GrDashboardLayout, breakpoint: GrDashboardBreakpoint): void;
(e: 'itemMove', id: string, from: GrDashboardItemLayout, to: GrDashboardItemLayout): void;
(e: 'itemResize', id: string, from: GrDashboardItemLayout, to: GrDashboardItemLayout): void;
(e: 'itemAutoResize', id: string, from: GrDashboardItemLayout, to: GrDashboardItemLayout): void;
(e: 'breakpointChange', breakpoint: GrDashboardBreakpoint, cols: number): void;
(e: 'itemSettings', id: string): void;
(e: 'itemDrop', event: GrDashboardDropEvent): void;
(e: 'itemTransferOut', id: string, transfer: GrDashboardTransfer): void;
}Usage from the library prose
A widget cannot be moved into emptiness along the axis of compaction
compact knows four modes: 'vertical' (the default) pulls the widgets up, 'horizontal' to the
left, 'both' in both directions, and 'none' leaves them where they were put. A widget released
into emptiness moves back — down with 'vertical', to the right with 'horizontal', in both
directions with 'both'. That is not a defect but the definition of compaction. If a free grid with
holes is needed — compact: 'none'.
A push on a collision propagates along a chain and always downwards, in any mode: there are infinitely many rows, while to the right the right edge of the grid gets in the way. In the horizontal modes that gives a combination worth seeing once: a neighbour pushed downwards immediately moves to the left.
Where it was dropped is said by the grid, and the putting is done by the application
The grid does not write a widget dragged in from the catalogue into the layout itself: it emits
itemDrop with the cell, the breakpoint and its own layout options, and addItem is called by the
consumer — the same way as with a button in the catalogue. Otherwise a widget would appear in the
layout for which the application drew no markup.
While the pointer is over the grid, the underlay shows the place for real: the preview is computed
with the same addItem and the same options that will travel into the event. That is why the
options travel into the event: were the application to compute the layout with its own, the result
would diverge from what the person has just seen.
A collision with a static widget cancels the whole move
Moving a static widget is impossible by definition, and putting one on top of another would mean
silently spoiling the layout. preventCollision does the same for any collision. There is no
half-way outcome — “it moved as far as it fit” — here: the user would not see what exactly did not
fit.
itemAutoResize is not the same as itemResize
A widget with auto-height adjusts to its content itself, and its height changes with no
participation from a person: the data arrived, a section was expanded, the language changed. Such a
change travels into update:layout on a par with the rest — the layout remains the only truth — but
is accompanied by a separate event.
| Event | When |
|---|---|
itemResize | the user pulled a corner or pressed an arrow |
itemAutoResize | the content of the widget asked for a different height |
Telling them apart is necessary for anyone who decides from the edits whether the layout is dirty:
without that, “save the changes?” would pop up after data has loaded into a widget. The details of
the measurement — ./GrDashboardItem.md.
itemTransferOut — the widget went to the neighbours
Two dashboards on a page exchange widgets by dragging: a gesture by the handle that left the edge of its own grid into another becomes a transfer between them.
| Event | Whose | When |
|---|---|---|
itemDrop | the receiver | something was put into the grid — from the catalogue or from another dashboard |
itemTransferOut | the source | the widget left; the grid removed it from its own layout itself |
The receiver can tell the sources apart by transfer.source ('palette' or 'dashboard') and
transfer.from — the identifier of the grid the widget came from. The details —
../model.md.
Giving away and receiving are allowed separately: transferable on the source, droppable on the
receiver.
While a widget is being carried, the source holds its place: the widget is hidden, an underlay
stands on its cell, and the neighbours are not compacted. Were the source to compact at once,
everything below it, the receiver included, would slide up under the pointer, and the drop would
land wide of the mark. The widget is removed only after the receiver has accepted it; that is
decided at a single point — the receiver that is under the pointer at the moment of release. A
release outside all grids, Esc and a broken-off pointer (pointercancel) roll the transfer back,
and the widget returns to its place.
A zero width is not the narrowest screen
A hidden tab, a collapsed panel, a display: none give a zero width of the container, and the
breakpoint does not switch because of that. Taking one for the other would mean moving the
dashboard to two columns and writing that layout into the model — that is, losing the user’s setting
because they opened a neighbouring tab.
Limits
What is not in the package and will not be — written down so that the question does not come up again.
- virtualisation of the grid — there are dozens of widgets on a dashboard rather than thousands, and the price of the complexity does not pay off;
- nested dashboards — a grid inside a grid poses an unanswerable question about whose gesture to handle;
- a card of its own instead of
GrCard— a copy would diverge from the cards of the rest of the application; - a report builder with fields and aggregates — that is a product rather than a component.
Props in this example