List
The <smart-list> component renders a list of entities. It is a wrapper that dispatches to one of three built-in child components based on options.mode or HardwareService.isMobile auto-detection. Each mode's child can be replaced via LIST_MODE_COMPONENTS_TOKEN, which provides a partial override map — any mode you specify replaces the default child; any mode you omit falls back to the built-in.
Usage
<smart-list [options]="options" />
Components
ListComponent (<smart-list>)
Main wrapper. Reads options.mode (or falls back to HardwareService.isMobile for auto mobile detection) and renders the appropriate child via NgComponentOutlet. Handles the empty-state display and loading skeleton before delegating to the mode child.
ListDesktopComponent (<smart-list-desktop>)
Default desktop implementation. Uses CDK Table with Tailwind-styled column cells, sortable column headers, and optional remove/detail/item action columns.
ListMobileComponent (<smart-list-mobile>)
Default mobile implementation. Renders a <ul role="list"> with flex rows, one row per field key, plus optional action icons.
ListMasonryGridComponent (<smart-list-masonry-grid>)
Default masonry-grid implementation. Renders a grid of cards that include image thumbnails and configurable field display.
ListBaseComponent (abstract)
Abstract base directive. Extend it to build custom list implementations for any mode. Exposes the following from IListInternalOptions<T>:
fields— computed array of{ key, options }from model decoratorskeys— string array of visible column keys (set byinitKeys())list—Signal<T[]>from providerloading—Signal<boolean>from providerpage—Signal<number>from pagination optionstotalPages—Signal<number>from pagination optionsprovider—IListProvider<T>itemHandler— resolved item navigation/select handlerremoveHandler— resolved remove handlercellPipe—ICellPipe<T>for cell value transformationcssClass— string input (aliasclass)
Methods:
initKeys()— populateskeysfrom computedfieldshandlePageChange(page: number)— triggers providergetDatawith updated page
API
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
options | InputSignal<IListOptions<T>> | required | Full list configuration |
class | InputSignal<string> | '' | External CSS classes forwarded via cssClass() |
IListOptions<T>
IListOptions property reference
| Property | Type | Default | Description |
|---|---|---|---|
provider | IListProvider<T> | required | Data provider (list signal, loading signal, getData callback) |
type | any | required | Model class decorated with @Model |
mode | ListMode | - | Force a specific mode; omit to auto-detect via HardwareService.isMobile |
pagination | IListPaginationOptions | - | Pagination config (mode, limit, page signal, totalPages signal, load callbacks) |
cellPipe | ICellPipe<T> | - | Pipe for transforming each cell value |
componentFactories | IListComponentFactories<T> | - | Top dynamic component factory |
sort | boolean | { default?: string; defaultDesc?: boolean } | - | Enable sorting; optionally set a default sort column and direction |
details | boolean | { provider?: IDetailsProvider<T>; componentFactories?: ...; component? } | - | Enable detail drill-down. The provider is required whenever this is set; the component is optional |
item | boolean | { options?: ItemOptions } | - | Enable item row action (navigate or custom select) |
remove | boolean | { provider?: IRemoveProvider<T> } | - | Enable row remove action; optionally provide a custom remove provider |
select | 'multi' | - | Enable multi-select mode |
LIST_MODE_COMPONENTS_TOKEN
InjectionToken that provides a Partial<Record<ListMode, Type<ListBaseComponent<any>>>> override map. Any mode key you include replaces the built-in default child for that mode; any mode key you omit continues to use the built-in default.
Preline mode presets
Every mode ships a Preline-styled preset alongside the default child:
| ListMode | Preset | Look |
|---|---|---|
desktop | ListDesktopPresetComponent | Preline table (smart-list-desktop-preset), styling via presentation |
mobile | ListMobilePresetComponent | Responsive card grid (smart-list-mobile-preset), image → card image |
masonryGrid | ListMasonryGridPresetComponent | Masonry card columns (smart-list-masonry-grid-preset), image → card |
Apply them via the ready-made map (covers all three modes):
The desktop preset reads the optional IListOptions.presentation field:
Notes:
presentationis consumed only by the desktop preset; the standard components ignore it.- The mobile preset maps fields onto card anatomy:
FieldType.image/logo→ card image, first non-image field → title, remaining fields → body text; the item action renders as the card's primary button (label i18n key:details). - The masonryGrid preset keeps the base masonry column algorithm and image mechanism (
NgOptimizedImagefrom the model's image field) and restyles items with the same card recipe as the mobile preset; it has no item action buttons (matching the standard masonry child). - Preline "Table with search" / "with pagination" / "selectable rows" variants are covered by the existing
searchbarfeature,smart-paging(PaginationMode.singlePage), andselect: 'multi'respectively — not bypresentation. Table caption/footer variants are not supported (no API channel inIListOptions).
Extending the base class
import {
ChangeDetectionStrategy,
Component,
computed,
signal,
ViewEncapsulation,
} from '@angular/core';
import {
IListOptions,
LIST_MODE_COMPONENTS_TOKEN,
ListBaseComponent,
ListComponent,
ListMode,
} from '@smartsoft001/angular';
import { Field, FieldType, Model } from '@smartsoft001/models';
// `<smart-list>` reads the columns off the model metadata, so only the fields
// marked `list: true` become keys of the rendered list.
@Model({})
export class DocsUser {
id = '';
@Field({ list: true, type: FieldType.text })
firstName = '';
@Field({ list: true, type: FieldType.email })
email = '';
@Field({ list: true, type: FieldType.text })
role = '';
}
/**
* A custom list built on `ListBaseComponent`.
*
* The base resolves `keys` from the model metadata, exposes the rows as the
* `list` signal and the busy flag as `loading`; the implementation only turns
* that into markup.
*/
@Component({
selector: 'docs-custom-list',
template: `
<table [class]="containerClasses()">
<thead>
<tr>
@for (key of keys; track key) {
<th class="docs-list__header">{{ key }}</th>
}
</tr>
</thead>
<tbody>
@for (row of rows(); track row.id) {
<tr class="docs-list__row">
@for (key of keys; track key) {
<td class="docs-list__cell">{{ cell(row, key) }}</td>
}
</tr>
}
</tbody>
</table>
`,
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomListComponent extends ListBaseComponent<DocsUser> {
// ListComponent passes the external class under its aliased name, so the
// inherited `cssClass` input is used as is - do not redeclare it here.
containerClasses = computed(() =>
['docs-list', this.cssClass()].filter(Boolean).join(' '),
);
// `list` is typed as a CDK table data source, which also covers observables
// and DataSource instances; this example only handles the plain array form.
rows = computed<DocsUser[]>(() => {
const value = this.list();
return Array.isArray(value) ? value : [];
});
cell(row: DocsUser, key: string): string {
return String((row as unknown as Record<string, unknown>)[key] ?? '');
}
}
/**
* The list wrapper resolves its body from a map of modes rather than from a
* single standard-component token, so the implementation is registered under
* the mode that `IListOptions.mode` asks for. The map is merged over the
* built-in one, so the other modes keep their own components.
*/
@Component({
selector: 'docs-list-custom-example',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ListComponent],
providers: [
{
provide: LIST_MODE_COMPONENTS_TOKEN,
useValue: { [ListMode.desktop]: CustomListComponent },
},
],
template: `<smart-list [options]="options" />`,
})
export class ListCustomExampleComponent {
// A provider is the list's data source: two signals plus the callback the
// list calls when it needs a page. A real application hands over an NgRx
// facade here instead of static rows.
options: IListOptions<DocsUser> = {
provider: {
list: signal<DocsUser[]>([
{ id: '1', firstName: 'Jan', email: 'jan@example.com', role: 'Admin' },
{
id: '2',
firstName: 'Anna',
email: 'anna@example.com',
role: 'User',
},
{
id: '3',
firstName: 'Piotr',
email: 'piotr@example.com',
role: 'User',
},
]),
loading: signal(false),
getData: () => undefined,
},
type: DocsUser,
mode: ListMode.desktop,
};
}
Source
The component lives in packages/shared/angular/src/lib/components/list and is documented for Claude Code by the angular-components-list skill.