Range Selection & Clipboard
Spreadsheet-style interaction: select rectangular cell ranges, copy them as tab-separated values, paste blocks straight from Excel, drag the fill handle to copy values or extend number and date series, and undo any of it. Every write runs the column's parser and validators and lands as one undoable batch through the regular editing events.
Ranges, copy, paste & fill
Click a cell, then Shift+click or drag to grow the range (long press first on touch); Shift+arrows extend it from the keyboard and Ctrl+click adds another range. Ctrl+C copies (with captions here — copyHeaders), Ctrl+V pastes a TSV block from the focused cell, Ctrl+D / Ctrl+R fill down / right, Ctrl+Z / Ctrl+Y undo and redo. The read-only Id column is skipped by every write.
No range selected
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeColumn, OgeGrid } from '@oge-ui/grid';
import type { OgeGridCellRange, OgeRangeSelectionChangedEvent } from '@oge-ui/grid';
@Component({
selector: 'demo-root',
imports: [OgeColumn, OgeGrid],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- click / Shift+click / drag / Shift+Arrow select ranges; Ctrl+C copies TSV,
Ctrl+V pastes from Excel, drag the corner handle (or Ctrl+D / Ctrl+R) to fill,
Ctrl+Z / Ctrl+Y undo and redo -->
<oge-grid [data]="employees" keyField="id"
selectionMode="cell"
[rangeSelection]="{ copyHeaders: true, pasteAddsRows: true }"
[editing]="{ mode: 'batch', allowUpdating: true }"
[(selectedRanges)]="ranges"
(rangeSelectionChanged)="onRanges($event)">
<oge-column field="firstName" caption="First name" />
<oge-column field="department" caption="Department" />
<oge-column field="salary" caption="Salary" dataType="number" />
<oge-column field="hireDate" caption="Hired" dataType="date" />
<oge-column field="id" caption="Id" [editable]="false" />
</oge-grid>
<p>{{ summary() }}</p>
`,
})
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 ranges = signal<readonly OgeGridCellRange[]>([]);
protected readonly summary = signal('');
protected onRanges(event: OgeRangeSelectionChangedEvent): void {
this.summary.set(`${event.cellCount} cells in ${event.ranges.length} range(s)`);
}
}Async validation
asyncValidators run like a server check: the editor turns aria-busy while it runs and a save waits for the answer. Type Taken as a last name to see the rejection. The Hired at column is dataType="datetime" — a date and time editor.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeColumn, OgeGrid } from '@oge-ui/grid';
import type { AbstractControl, ValidationErrors } from '@angular/forms';
@Component({
selector: 'demo-root',
imports: [OgeColumn, OgeGrid],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-grid [data]="employees" keyField="id"
[editing]="{ mode: 'row', allowUpdating: true }">
<!-- the editor is aria-busy while the check runs; Save waits for it -->
<oge-column field="lastName" caption="Last name (unique)"
[asyncValidators]="[uniqueName]" />
<oge-column field="hireDate" caption="Hired at" dataType="datetime" />
</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' },
];
/** Pretends to ask a server whether the name is taken. */
protected readonly uniqueName = (
control: AbstractControl,
): Promise<ValidationErrors | null> =>
new Promise((resolve) =>
setTimeout(
() => resolve(control.value === 'Taken' ? { taken: true } : null),
600,
),
);
}Notes
- In cell mode a single click selects; editing starts on double-click, F2 or Enter.
- Copied text is formula-guarded like the CSV export: a cell starting with
=,+,-or@is prefixed so a spreadsheet does not evaluate it. - A single copied value pasted onto a multi-cell range fills the whole range. Lines past the last row are dropped unless
pasteAddsRowsis on. - Range sizes are announced through the shared live announcer; the fill handle is a pointer affordance and Ctrl+D / Ctrl+R are its keyboard twins.