Pinned Rows
Keep rows in view: pin rows above or below the scrolling body, keep the current group headers under the header while scrolling, and drag rows between grids.
Pinned rows & pager options
pinnedTopRows / pinnedBottomRows take data objects or keys of rows on the loaded page (which then leave the body). They sit in sticky sections — virtual scrolling included — and are display rows. The pager here adds first / last buttons, a go-to-page input and a custom info text (*ogePagerInfoTemplate).
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeColumn, OgeGrid, OgePagerInfoTemplate } from '@oge-ui/grid';
@Component({
selector: 'demo-root',
imports: [OgeColumn, OgeGrid, OgePagerInfoTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- display rows in the sticky sections; a key of a loaded row
([pinnedTopRows]="[42]") moves that row out of the body instead -->
<oge-grid [data]="employees" keyField="id"
[pinnedTopRows]="[budget]"
[pinnedBottomRows]="[totals]"
[paging]="{ pageSize: 3, showFirstLastButtons: true, showPageInput: true }">
<oge-column field="firstName" caption="Name" />
<oge-column field="salary" caption="Salary" dataType="number" />
<span *ogePagerInfoTemplate="let info">
{{ info.firstRow }}–{{ info.lastRow }} of {{ info.totalCount }}
</span>
</oge-grid>
`,
})
export class Demo {
readonly employees = [
{ id: 1, firstName: 'Ali', lastName: 'Yılmaz', department: 'Engineering', city: 'İstanbul', salary: 8400, hireDate: '2019-03-11' },
{ id: 2, firstName: 'Ayşe', lastName: 'Kaya', department: 'Sales', city: 'Ankara', salary: 7200, hireDate: '2020-07-02' },
{ id: 3, firstName: 'Mehmet', lastName: 'Demir', department: 'Engineering', city: 'İzmir', salary: 9100, hireDate: '2018-01-23' },
{ id: 4, firstName: 'Zeynep', lastName: 'Şahin', department: 'Finance', city: 'İstanbul', salary: 6800, hireDate: '2021-11-15' },
{ id: 5, firstName: 'Emre', lastName: 'Çelik', department: 'Support', city: 'Bursa', salary: 5400, hireDate: '2022-05-09' },
];
protected readonly budget = { id: -1, firstName: 'Budget', salary: 1500000 };
protected readonly totals = { id: 0, firstName: 'Total', salary: 36900 };
}Sticky group rows
With stickyGroupRows the group rows enclosing the first visible row stay under the header while scrolling — at every level and under virtual scrolling. They are a visual aid: the real group rows stay in place for keyboard and screen-reader users, and a click on a sticky row scrolls to it. The second level groups hire dates by quarter (groupInterval="quarter").
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeColumn, OgeGrid } from '@oge-ui/grid';
@Component({
selector: 'demo-root',
imports: [OgeColumn, OgeGrid],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- the enclosing group rows stay under the header while you scroll -->
<oge-grid [data]="employees" keyField="id" style="height: 360px"
[virtualScroll]="true" [stickyGroupRows]="true"
[groupBy]="['department', 'hireDate']">
<oge-column field="department" caption="Department" />
<!-- quarter buckets: "Q2 2026"; numeric columns take a width, e.g. [groupInterval]="10000" -->
<oge-column field="hireDate" caption="Hired" dataType="date" groupInterval="quarter" />
<oge-column field="firstName" caption="Name" />
</oge-grid>
`,
})
export class Demo {
readonly employees = [
{ id: 1, firstName: 'Ali', lastName: 'Yılmaz', department: 'Engineering', city: 'İstanbul', salary: 8400, hireDate: '2019-03-11' },
{ id: 2, firstName: 'Ayşe', lastName: 'Kaya', department: 'Sales', city: 'Ankara', salary: 7200, hireDate: '2020-07-02' },
{ id: 3, firstName: 'Mehmet', lastName: 'Demir', department: 'Engineering', city: 'İzmir', salary: 9100, hireDate: '2018-01-23' },
{ id: 4, firstName: 'Zeynep', lastName: 'Şahin', department: 'Finance', city: 'İstanbul', salary: 6800, hireDate: '2021-11-15' },
{ id: 5, firstName: 'Emre', lastName: 'Çelik', department: 'Support', city: 'Bursa', salary: 5400, hireDate: '2022-05-09' },
];
}Drag rows between grids
Grids sharing a rowDragGroup accept each other's rows. The target fires rowDrop with the source row and the insert index; cross-grid drops move no data by themselves, so the handler moves the row (a drop inside the same grid is the usual reorder).
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeColumn, OgeGrid } from '@oge-ui/grid';
import type { OgeRowDropEvent } from '@oge-ui/grid';
interface Task {
id: number;
title: string;
}
@Component({
selector: 'demo-root',
imports: [OgeColumn, OgeGrid],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- rows of one rowDragGroup move between the grids -->
<oge-grid id="todo" [data]="todo()" keyField="id"
[rowDragging]="true" rowDragGroup="tasks" (rowDrop)="onDrop($event)">
<oge-column field="title" caption="To do" />
</oge-grid>
<oge-grid id="done" [data]="done()" keyField="id"
[rowDragging]="true" rowDragGroup="tasks" (rowDrop)="onDrop($event)">
<oge-column field="title" caption="Done" />
</oge-grid>
`,
})
export class Demo {
protected readonly todo = signal<Task[]>([
{ id: 1, title: 'Write specs' },
{ id: 2, title: 'Ship it' },
]);
protected readonly done = signal<Task[]>([{ id: 3, title: 'Plan' }]);
protected onDrop(event: OgeRowDropEvent): void {
if (event.sameComponent) return; // a reorder: the grid already moved it
const from = event.sourceComponentId === 'todo' ? this.todo : this.done;
const to = event.targetComponentId === 'todo' ? this.todo : this.done;
const row = event.sourceRow as Task;
from.update((rows) => rows.filter((r) => r.id !== row.id));
to.update((rows) => {
const next = [...rows];
next.splice(event.toIndex, 0, row);
return next;
});
}
}