keystone-dashboard-layout-angular
v1.0.0
Published
Angular port of the responsive & dynamic grid / dashboard layout with drag, drop and resizable actions.
Maintainers
Keywords
Readme
What this actually is
An Angular, TypeScript-native library for building draggable, resizable, responsive dashboard layouts — the kind of thing you'd use to let a user rearrange widgets, charts, or panels on a screen, with drag, resize, responsive breakpoints, multi-select, undo/redo, and collision handling built in.
It is not a data table/grid. If you're after sorting, filtering,
paging, or spreadsheet-style rows and columns, this isn't that. Built
as standalone components (no NgModule required) on the same native,
Pointer-Events-based drag/resize engine shared with the Vue and React
packages in this family, rather than a separate implementation of the
hard, easy-to-get-subtly-wrong parts (collision, compaction,
responsive breakpoint math).
Quick start
npm install keystone-dashboard-layout-angularimport { Component } from '@angular/core';
import { GridLayoutComponent, GridItemComponent } from 'keystone-dashboard-layout-angular';
import type { TLayout } from 'keystone-dashboard-layout-core';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [GridLayoutComponent, GridItemComponent],
template: `
<kdl-grid-layout [layout]="layout" (layoutChange)="layout = $event">
@for (item of layout; track item.i) {
<kdl-grid-item [i]="item.i" [x]="item.x" [y]="item.y" [w]="item.w" [h]="item.h">
Item {{ item.i }}
</kdl-grid-item>
}
</kdl-grid-layout>
`,
})
export class DashboardComponent {
layout: TLayout = [
{ i: 'a', x: 0, y: 0, w: 2, h: 2 },
{ i: 'b', x: 2, y: 0, w: 2, h: 2 },
];
}Don't forget the stylesheet, imported once wherever it'll load application-wide:
import 'keystone-dashboard-layout-angular/style.css';@angular/common/@angular/core (^17.0.0 || ^18.0.0 || ^19.0.0)
and rxjs (^7.8.0) are peer dependencies.
GridLayoutComponent is a fully controlled component — it never
mutates the layout array (or any item in it) you pass in; every
drag/resize tick (and the compaction that follows it) is reported via
a layoutChange @Output() with a brand-new array. GridItemComponent
takes i/x/y/w/h as its own required @Input()s — bind each
one explicitly per item in your own template's @for loop.
Using just the grid math, without Angular?
keystone-dashboard-layout-core exports the same collision detection,
compaction, movement, and alignment functions this package is built
on — zero Angular dependency, no live DOM required, plain data in and
out:
import { collides, compactLayout, moveElement } from 'keystone-dashboard-layout-core';Features
- Core layout — grid-unit positioning with automatic pixel
conversion, a fully controlled
layout/layoutChangecontract, auto-sizing container (autoSize/heightMode, including'scroll'/'fit'modes) and per-itemautoHeight, visible grid lines, visual alignment guides and magneticsnapToGrid(a real distinction — one shows where edges line up, the other actually moves the item), CSS transform positioning. - A grid-wide behavioral cascade —
isDraggable/isResizable/isBounded/isMirrored/maxRows/showCloseButton/enableEditMode/useBorderRadius/borderRadiusPx/ariaLabelscan each be set once onGridLayoutComponentand inherited by everyGridItemComponent, or overridden per-item when needed. - Drag and resize — drag from anywhere on an item by default, or
restrict it to a handle (
dragAllowFrom/dragIgnoreFrom); resize from all eight edges/corners with cursor affordance and optional visible handles (showResizeHandles/resizeHandleColor); bounded dragging (isBounded); aspect-ratio locking (preserveAspectRatio); per-item size constraints (minW/maxW/minH/maxH); keyboard move/resize (arrow keys / Shift+arrow, RTL-aware); amoveBlockedByCollision@Output()for shake/flash/toast feedback. - Collision and compaction — vertical/horizontal/none compaction
plus overlap variants (
compactType),preventCollision,horizontalShift, static items excluded from cascades, on-demandcompactNow()/rearrange(), collision-safeduplicateItem(id), a pluggablecompactorinput for replacing the algorithm entirely. - Responsive layouts — breakpoint-driven column counts
(
responsive/breakpoints/cols), predefined layouts per breakpoint (responsiveLayouts) with auto-generation for any breakpoint without one,distributeEvenlyfor spreading out-of-bounds items instead of clamping them. - Multi-select and group operations — click/Shift-click/Ctrl-click
selection, group move/resize, align/distribute commands
(
alignSelected/distributeSelected). - Multi-grid and drag-and-drop — drag items between independent
GridLayoutComponentinstances (allowCrossGridDrag) via a real first-fit bin-pack, and from outside the grid system entirely via native HTML5 drag-and-drop (allowOutsideDrop,outsideDropAcceptto reject incompatible drags). - Editing and lifecycle — a built-in close button (
showCloseButton- a
removeItem@Output()), an edit-mode toggle (enableEditMode), add/remove items without manual position math, opt-in undo/redo (enableUndoRedo/undoHistoryLimit) at committed-change granularity — including externally-drivenlayoutchanges, not just drag/resize.
- a
- Styling and customization — configurable border radius,
transition duration/easing, a
[kdlGridItemHeader]marker directive for a dedicated header region, standaloneGridItemCloseButtonComponent/GridItemDragHandleComponentutility components, automatic RTL mirroring (isMirrored, grid-wide or per-item). - Persistence —
GridLayoutStorageServicefor a single saved layout, plusGridLayoutPresetsServicefor saving and switching between several named arrangements — bothprovidedIn: 'root', taking/returning a plain layout value directly. - Export —
exportLayoutAsSvg(), a dependency-free grid-to-image export for a report, thumbnail, or "share my dashboard" feature. - Accessibility — keyboard move/resize,
aria-roledescription/role="group"on interactive items, localizable UI/ARIA strings (ariaLabels, grid-wide default + per-item override).
Imperative API
Reach GridLayoutComponent's own public methods via a template
reference variable:
import { Component, ViewChild } from '@angular/core';
import { GridLayoutComponent, GridItemComponent } from 'keystone-dashboard-layout-angular';
@Component({
standalone: true,
imports: [GridLayoutComponent, GridItemComponent],
template: `
<button (click)="grid.compactNow()">Tidy up</button>
<kdl-grid-layout #grid [layout]="layout" (layoutChange)="layout = $event">
<!-- ... -->
</kdl-grid-layout>
`,
})
export class DashboardComponent {
@ViewChild('grid') gridRef!: GridLayoutComponent;
}compactNow()/rearrange(), duplicateItem(id), undo()/redo()/
canUndo/canRedo, selectItem()/deselectItem()/
toggleItemSelection()/clearSelection()/selectedItemIds,
alignSelected(edge)/distributeSelected(axis),
exportLayoutAsSvg(options?), scrollToItem(id)/focusItem(id).
What this package exports
import {
GridLayoutComponent,
GridItemComponent,
GridItemHeaderDirective,
GridLayoutStorageService,
GridLayoutPresetsService,
GridItemDragHandleComponent,
GridItemCloseButtonComponent,
} from 'keystone-dashboard-layout-angular';Layout-level types (TLayout, ILayoutItem, ECompactType, and so
on) come from keystone-dashboard-layout-core instead — an Angular
@Component's own class already is its prop contract, so there's
no separate props-interface convention to import here the way React's
IGridLayoutProps/IGridItemProps work.
Idiomatic Angular, not a transliteration
Standalone components throughout (no NgModule required), a DI-scoped
GridEventBusService instead of Vue's provide/inject or React's
Context, providedIn: 'root' services instead of ref-bound composables/
hooks, and a marker directive (GridItemHeaderDirective) queried via
@ContentChild instead of a named slot or render prop — the same
underlying behavior, expressed the way Angular actually works.
Shared engine, three frameworks
This package, keystone-dashboard-layout-vue,
and keystone-dashboard-layout-react
all build on keystone-dashboard-layout-core
for collision detection, compaction, responsive breakpoint math, and
the native Pointer-Events-based drag/resize engine — one implementation
of the hard parts, not three independently-maintained copies that
could drift out of sync.
Testing
A real, extensive unit/component test suite (Jest + jest-preset-angular)
backs every feature, with Stryker mutation testing configured. A real
end-to-end browser test layer (Playwright) runs against a dedicated
e2e-fixture/ application — matching the Vue/React packages' own e2e
setup, not a Vite dev server the way those two use, since this
package's Angular target hit a genuinely unresolved upstream
@analogjs/vite-plugin-angular bug. Karma (still present in this
repo) predates the Playwright suite and is no longer needed for it —
kept for now rather than removed in the same pass that added real e2e
coverage.
Changelog
See CHANGELOG.md for release history.
Donate
If you enjoyed this project — or just feeling generous, consider buying me a 🍺. Cheers!
License
MIT
