Data View
oge-data-view renders a templated collection — products, people, files — in responsive columns or one per row, with search, sorting, a filter hook, paging and optional selection.
No WAI-ARIA pattern exists for a data view, so it is composed from list primitives: a list of list items without selection (templates may hold links and buttons), an APG listbox with one tab stop once items select. Columns come from a container query on the view's own width, so it lays itself out correctly inside a drawer, a dialog or a narrow column.
Basics & templates
Pass items and project an ogeDataViewItemTemplate; without one an item shows its displayExpr (or its title / name field). Without selection the view is a list of listitems, so links and buttons inside a template stay in the Tab order.
Furniture
Oak desk
Furniture
Mesh office chair
Lighting
Desk lamp
Storage
Wall shelf
Textiles
Wool rug
Lighting
Floor lamp
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeDataView, OgeDataViewItemTemplate } from '@oge-ui/layout';
interface Product {
id: number;
name: string;
category: string;
price: number;
rating: number;
inStock: boolean;
}
@Component({
selector: 'demo-root',
imports: [OgeDataView, OgeDataViewItemTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Without selection the view is a list of listitems: templates may
hold links and buttons, which stay in the Tab order. Columns follow
the view's own width (a container query), never the window. -->
<oge-data-view [items]="products" ariaLabel="Products">
<ng-template ogeDataViewItemTemplate let-item>
<p class="category">{{ item.category }}</p>
<h3>{{ item.name }}</h3>
<p>{{ item.price }} USD · ★ {{ item.rating }}</p>
</ng-template>
</oge-data-view>
`,
})
export class Demo {
protected readonly products: Product[] = [
{ id: 1, name: 'Oak desk', category: 'Furniture', price: 420, rating: 4.6, inStock: true },
{ id: 2, name: 'Mesh office chair', category: 'Furniture', price: 260, rating: 4.4, inStock: true },
{ id: 3, name: 'Desk lamp', category: 'Lighting', price: 45, rating: 4.1, inStock: false },
{ id: 4, name: 'Wall shelf', category: 'Storage', price: 89, rating: 3.9, inStock: true },
{ id: 5, name: 'Wool rug', category: 'Textiles', price: 150, rating: 4.8, inStock: true },
{ id: 6, name: 'Floor lamp', category: 'Lighting', price: 120, rating: 4.2, inStock: true },
];
}Grid and list layouts
showLayoutSwitch renders a toggle-button group (aria-pressed) bound to the two-way layout. Grid columns are auto-fill cells of at least minItemWidth px, resolved by a container query on the view itself — resize the browser and the column count follows the card, not the window. The list layout may use its own row template.
Furniture
Oak desk
Furniture
Mesh office chair
Lighting
Desk lamp
Storage
Wall shelf
Textiles
Wool rug
Lighting
Floor lamp
Layout: grid
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeDataView, OgeDataViewItemTemplate, OgeDataViewListItemTemplate } from '@oge-ui/layout';
import type { OgeDataViewLayout } from '@oge-ui/layout';
interface Product {
id: number;
name: string;
category: string;
price: number;
rating: number;
inStock: boolean;
}
@Component({
selector: 'demo-root',
imports: [OgeDataView, OgeDataViewItemTemplate, OgeDataViewListItemTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The switch is a real toggle-button group (aria-pressed). minItemWidth
sets the narrowest cell before another column fits; the list layout
can render its own row template. -->
<oge-data-view
[items]="products"
[(layout)]="layout"
[showLayoutSwitch]="true"
[minItemWidth]="200"
ariaLabel="Products by layout"
>
<ng-template ogeDataViewItemTemplate let-item>
<p class="category">{{ item.category }}</p>
<h3>{{ item.name }}</h3>
<p>{{ item.price }} USD · ★ {{ item.rating }}</p>
</ng-template>
<ng-template ogeDataViewListItemTemplate let-item>
<strong>{{ item.name }}</strong> — {{ item.category }}, {{ item.price }} USD
</ng-template>
</oge-data-view>
<p>Layout: {{ layout() }}</p>
`,
})
export class Demo {
protected readonly layout = signal<OgeDataViewLayout>('grid');
protected readonly products: Product[] = [
{ id: 1, name: 'Oak desk', category: 'Furniture', price: 420, rating: 4.6, inStock: true },
{ id: 2, name: 'Mesh office chair', category: 'Furniture', price: 260, rating: 4.4, inStock: true },
{ id: 3, name: 'Desk lamp', category: 'Lighting', price: 45, rating: 4.1, inStock: false },
{ id: 4, name: 'Wall shelf', category: 'Storage', price: 89, rating: 3.9, inStock: true },
{ id: 5, name: 'Wool rug', category: 'Textiles', price: 150, rating: 4.8, inStock: true },
{ id: 6, name: 'Floor lamp', category: 'Lighting', price: 120, rating: 4.2, inStock: true },
];
}Paging
pageSize turns paging on and the view renders its own pager: previous / next, a constant-width numeric window with ellipses and the range text. Focus stays on the button you pressed (or moves to the current page when it disables) and the new page is announced politely. For the full oge-pagination bar, bind it to [(pageIndex)] and set showPager to false.
Furniture
Oak desk
Furniture
Mesh office chair
Lighting
Desk lamp
Storage
Wall shelf
Page 1
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeDataView, OgeDataViewItemTemplate } from '@oge-ui/layout';
import type { OgeDataViewPageChangedEvent } from '@oge-ui/layout';
interface Product {
id: number;
name: string;
category: string;
price: number;
rating: number;
inStock: boolean;
}
@Component({
selector: 'demo-root',
imports: [OgeDataView, OgeDataViewItemTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- pageSize turns paging on; the built-in pager keeps focus on the
button you pressed and announces "Page 2 of 3". For the full
oge-pagination bar, bind it to [(pageIndex)] and set showPager=false. -->
<oge-data-view
[items]="products"
[pageSize]="2"
[(pageIndex)]="page"
ariaLabel="Paged products"
(pageChanged)="onPage($event)"
>
<ng-template ogeDataViewItemTemplate let-item>
<p class="category">{{ item.category }}</p>
<h3>{{ item.name }}</h3>
<p>{{ item.price }} USD · ★ {{ item.rating }}</p>
</ng-template>
</oge-data-view>
<p>Page {{ page() + 1 }}</p>
`,
})
export class Demo {
protected readonly page = signal(0);
protected readonly products: Product[] = [
{ id: 1, name: 'Oak desk', category: 'Furniture', price: 420, rating: 4.6, inStock: true },
{ id: 2, name: 'Mesh office chair', category: 'Furniture', price: 260, rating: 4.4, inStock: true },
{ id: 3, name: 'Desk lamp', category: 'Lighting', price: 45, rating: 4.1, inStock: false },
{ id: 4, name: 'Wall shelf', category: 'Storage', price: 89, rating: 3.9, inStock: true },
{ id: 5, name: 'Wool rug', category: 'Textiles', price: 150, rating: 4.8, inStock: true },
{ id: 6, name: 'Floor lamp', category: 'Lighting', price: 120, rating: 4.2, inStock: true },
];
protected onPage(event: OgeDataViewPageChangedEvent): void {
console.log('page', event.previousPageIndex, '→', event.pageIndex);
}
}Sorting, search & filtering
The search matches every word through the suite's text folding (case-, accent- and locale-insensitive) over searchExpr, and the result count is announced. sortOptions fill a native select with a direction toggle; text compares with the locale's collator, numbers numerically, empty values last. filter is a plain predicate hook.
Accessories
Cable tray
Lighting
Desk lamp
Textiles
Linen curtains
Storage
Wall shelf
Accessories
Monitor arm
Lighting
Floor lamp
Textiles
Wool rug
Lighting
Pendant light
Storage
Filing cabinet
Furniture
Mesh office chair
Furniture
Oak desk
Furniture
Standing desk
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeDataView, OgeDataViewItemTemplate } from '@oge-ui/layout';
import type { OgeDataViewSort, OgeDataViewSortOption } from '@oge-ui/layout';
interface Product {
id: number;
name: string;
category: string;
price: number;
rating: number;
inStock: boolean;
}
@Component({
selector: 'demo-root',
imports: [OgeDataView, OgeDataViewItemTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Search folds case and accents ("eclair" finds "Éclair"); the sort
compares text with the locale's collator and numbers numerically.
filter is a plain predicate hook. -->
<label>
<input type="checkbox" (change)="inStockOnly.set($any($event.target).checked)" />
In stock only
</label>
<oge-data-view
[items]="products"
[searchEnabled]="true"
[searchExpr]="['name', 'category']"
[sortOptions]="sortOptions"
[(sort)]="sort"
[filter]="inStockOnly() ? inStock : null"
ariaLabel="Searchable products"
>
<ng-template ogeDataViewItemTemplate let-item>
<p class="category">{{ item.category }}</p>
<h3>{{ item.name }}</h3>
<p>{{ item.price }} USD · ★ {{ item.rating }}</p>
</ng-template>
</oge-data-view>
`,
})
export class Demo {
protected readonly inStockOnly = signal(false);
protected readonly sort = signal<OgeDataViewSort | null>({ field: 'price', direction: 'asc' });
protected readonly sortOptions: OgeDataViewSortOption[] = [
{ field: 'name', label: 'Name' },
{ field: 'price', label: 'Price' },
{ field: 'rating', label: 'Rating' },
];
protected readonly inStock = (product: Product) => product.inStock;
protected readonly products: Product[] = [
{ id: 1, name: 'Oak desk', category: 'Furniture', price: 420, rating: 4.6, inStock: true },
{ id: 2, name: 'Mesh office chair', category: 'Furniture', price: 260, rating: 4.4, inStock: true },
{ id: 3, name: 'Desk lamp', category: 'Lighting', price: 45, rating: 4.1, inStock: false },
{ id: 4, name: 'Wall shelf', category: 'Storage', price: 89, rating: 3.9, inStock: true },
{ id: 5, name: 'Wool rug', category: 'Textiles', price: 150, rating: 4.8, inStock: true },
{ id: 6, name: 'Floor lamp', category: 'Lighting', price: 120, rating: 4.2, inStock: true },
];
}Selection
With selectionMode the view becomes an APG listbox with one tab stop: arrows walk the items in reading order (mirrored in RTL), Up / Down jump a row by the measured column count, Home / End, PageUp / PageDown turn the page, Space / Enter toggle and Ctrl+A selects everything. Options are leaves for assistive technology, so keep the template free of buttons and links.
Furniture
Oak desk
Furniture
Mesh office chair
Lighting
Desk lamp
Storage
Wall shelf
Textiles
Wool rug
Lighting
Floor lamp
Storage
Filing cabinet
Furniture
Standing desk
Selected: 2
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeDataView, OgeDataViewItemTemplate } from '@oge-ui/layout';
import type { OgeDataViewKey } from '@oge-ui/layout';
interface Product {
id: number;
name: string;
category: string;
price: number;
rating: number;
inStock: boolean;
}
@Component({
selector: 'demo-root',
imports: [OgeDataView, OgeDataViewItemTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- With selection the view is an APG listbox: one tab stop, arrows in
reading order (mirrored in RTL), Up/Down by a row, Space/Enter toggle,
Ctrl+A selects all. Options are leaves — keep the template free of
buttons and links. -->
<oge-data-view
[items]="products"
selectionMode="multiple"
[(selectedKeys)]="selected"
ariaLabel="Pick products"
>
<ng-template ogeDataViewItemTemplate let-item>
<p class="category">{{ item.category }}</p>
<h3>{{ item.name }}</h3>
<p>{{ item.price }} USD · ★ {{ item.rating }}</p>
</ng-template>
</oge-data-view>
<p>Selected: {{ selected().join(', ') || 'none' }}</p>
`,
})
export class Demo {
protected readonly selected = signal<readonly OgeDataViewKey[]>([2]);
protected readonly products: Product[] = [
{ id: 1, name: 'Oak desk', category: 'Furniture', price: 420, rating: 4.6, inStock: true },
{ id: 2, name: 'Mesh office chair', category: 'Furniture', price: 260, rating: 4.4, inStock: true },
{ id: 3, name: 'Desk lamp', category: 'Lighting', price: 45, rating: 4.1, inStock: false },
{ id: 4, name: 'Wall shelf', category: 'Storage', price: 89, rating: 3.9, inStock: true },
{ id: 5, name: 'Wool rug', category: 'Textiles', price: 150, rating: 4.8, inStock: true },
{ id: 6, name: 'Floor lamp', category: 'Lighting', price: 120, rating: 4.2, inStock: true },
];
}Remote operations
With remoteOperations the view neither sorts, searches nor pages: it emits optionsChanged (sort, search text, page, page size) and renders items as the current page of itemCount. loading marks the items aria-busy and draws skeleton tiles while there is nothing to show yet.
Furniture
Oak desk
Furniture
Mesh office chair
Lighting
Desk lamp
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeDataView, OgeDataViewItemTemplate } from '@oge-ui/layout';
import { ogeDataViewProcess } from '@oge-ui/behavior';
import type { OgeDataViewOptionsChangedEvent } from '@oge-ui/layout';
interface Product {
id: number;
name: string;
category: string;
price: number;
rating: number;
inStock: boolean;
}
@Component({
selector: 'demo-root',
imports: [OgeDataView, OgeDataViewItemTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- remoteOperations: the view neither sorts, searches nor pages. It
emits optionsChanged; the app answers with the page as items and the
total as itemCount. loading draws skeletons and sets aria-busy. -->
<oge-data-view
[items]="page()"
[itemCount]="total()"
[pageSize]="3"
[remoteOperations]="true"
[loading]="loading()"
[searchEnabled]="true"
[sortOptions]="[{ field: 'name', label: 'Name' }, { field: 'price', label: 'Price' }]"
ariaLabel="Server products"
(optionsChanged)="load($event)"
>
<ng-template ogeDataViewItemTemplate let-item>
<p class="category">{{ item.category }}</p>
<h3>{{ item.name }}</h3>
<p>{{ item.price }} USD · ★ {{ item.rating }}</p>
</ng-template>
</oge-data-view>
`,
})
export class Demo {
protected readonly page = signal<Product[]>([]);
protected readonly total = signal(0);
protected readonly loading = signal(false);
protected readonly products: Product[] = [
{ id: 1, name: 'Oak desk', category: 'Furniture', price: 420, rating: 4.6, inStock: true },
{ id: 2, name: 'Mesh office chair', category: 'Furniture', price: 260, rating: 4.4, inStock: true },
{ id: 3, name: 'Desk lamp', category: 'Lighting', price: 45, rating: 4.1, inStock: false },
{ id: 4, name: 'Wall shelf', category: 'Storage', price: 89, rating: 3.9, inStock: true },
{ id: 5, name: 'Wool rug', category: 'Textiles', price: 150, rating: 4.8, inStock: true },
{ id: 6, name: 'Floor lamp', category: 'Lighting', price: 120, rating: 4.2, inStock: true },
];
constructor() {
this.load({ sort: null, searchValue: '', pageIndex: 0, pageSize: 3 });
}
/** Stands in for an HTTP call: the "server" filters, sorts and slices. */
protected load(options: OgeDataViewOptionsChangedEvent): void {
this.loading.set(true);
setTimeout(() => {
const all = ogeDataViewProcess(this.products, {
searchValue: options.searchValue,
searchExpr: ['name', 'category'],
sort: options.sort,
});
const start = options.pageIndex * options.pageSize;
this.page.set(all.slice(start, start + options.pageSize));
this.total.set(all.length);
this.loading.set(false);
}, 400);
}
}