| name | build-angular-lists-and-forms |
| description | Build ABP Angular list and form pages the framework way — server-side paged/sorted/filtered lists via ListService, and reactive forms with ABP's automatic validation. USE FOR: ListService with component-scoped providers, hookToQuery returning PagedResultDto, get() refresh, page/maxResultCount/filter/sortKey/sortOrder setters, requestStatus$ (isLoading$ is deprecated), totalCount, LIST_QUERY_DEBOUNCE_TIME, reactive forms validated automatically by @ngx-validate/core, DEFAULT_VALIDATION_BLUEPRINTS + withValidationBluePrint localization keys, abp-validation-error, the skipValidation opt-out, and config-driven abp-dynamic-form (DynamicFormComponent + FormFieldConfig). DO NOT USE FOR: proxy consumption, menus, permission directives (angular-ui); proxy generation/regeneration (abp-cli-commands); adding entity/table/toolbar/form-prop extensions to a shipped module (extend-angular-module-ui); component testing (test-angular-ui); server-side application-service or DTO design (build-crud-application-services).
|
| license | MIT |
Build Angular Lists and Forms (ABP)
ABP's Angular UI ships two pieces of infrastructure that AI models routinely re-implement by hand: ListService for server-side paged data, and automatic reactive-form validation. Use them instead of hand-rolling paging state or manual error markup.
When to Use
- A list/table page bound to a paged ABP endpoint (
GetListAsync(input) returning PagedResultDto<T>).
- Server-side paging, sorting, and search whose subscriptions are cleaned up on component destroy (not leaked).
- A reactive create/edit form that should show ABP's localized validation messages automatically.
- An arbitrary (non-entity) form you want to drive from a config array rather than template markup.
When Not to Use
- Consuming generated proxies, menu routes, permission directives — use angular-ui.
- Generating/regenerating the proxies (
abp generate-proxy -t ng or the NX generator) — use abp-cli-commands.
- Adding columns/actions/form fields to a shipped module's page (Identity, etc.) — use extend-angular-module-ui; that's the extension system, a different API.
- Testing these components — use test-angular-ui.
- Designing the server-side app service / DTOs — use build-crud-application-services.
Lists with ListService
ListService (from @abp/ng.core) owns the query state (page, size, filter, sort) and re-runs the request when any of it changes. It must be component-scoped — put it in the component's providers, never providedIn: 'root' — so its subscriptions are cleaned up on destroy.
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, inject, OnInit } from '@angular/core';
import { BookService, BookDto } from '../proxy/books';
@Component({
selector: 'app-books',
templateUrl: './books.component.html',
providers: [ListService],
})
export class BooksComponent implements OnInit {
readonly list = inject(ListService);
private bookService = inject(BookService);
items: BookDto[] = [];
count = 0;
requestStatus$ = this.list.requestStatus$;
ngOnInit() {
this.list
.hookToQuery(query => this.bookService.getList(query))
.subscribe(res => {
this.items = res.items ?? [];
this.count = res.totalCount ?? 0;
});
}
}
The query object hookToQuery passes to your callback is { filter, maxResultCount, skipCount, sorting } — sorting is "field asc|desc" and skipCount = page * maxResultCount, which matches ABP's PagedAndSortedResultRequestDto.
- Search: two-way-bind an input to
list.filter; the setter re-runs the query.
- Sort: set
list.sortKey / list.sortOrder (from a table header click).
- Page/size: set
list.page / list.maxResultCount.
- Refresh after create/update/delete: call
list.get() (or list.getWithoutPageReset() to stay on the current page).
- Extra query params: spread the query —
query => this.bookService.getList({ ...query, authorId }).
Debounce is LIST_QUERY_DEBOUNCE_TIME (default 300 ms); provide the token alongside ListService to change it.
Reactive forms with automatic validation
ABP validates reactive forms through @ngx-validate/core and renders localized helper text automatically — you do not write per-field error *ngIf markup. Build a normal FormGroup with Angular validators; ABP shows the messages.
form = this.fb.group({
name: ['', [Validators.required, Validators.maxLength(128)]],
price: [0, [Validators.required, Validators.min(0)]],
});
Message text comes from DEFAULT_VALIDATION_BLUEPRINTS (from @abp/ng.theme.shared), which maps validator keys (required, email, minlength, …) to localization keys like AbpValidation::ThisFieldIsRequired. Override or extend them with withValidationBluePrint(...) passed into provideAbpThemeShared(...).
- Custom error rendering: subclass the
abp-validation-error component (ValidationErrorComponent from @abp/ng.theme.basic) and provide VALIDATION_ERROR_TEMPLATE.
- Opt out: put
skipValidation on a <form> (whole form) or on a single formControlName input.
Config-driven dynamic forms
For arbitrary (non-entity) forms, drive <abp-dynamic-form> (DynamicFormComponent from @abp/ng.components/dynamic-form) with a FormFieldConfig[] array — field types, validators, conditional visibility, nested groups/arrays — instead of writing template markup.
This is a separate validation path from the reactive-form @ngx-validate/core story above: abp-dynamic-form does not read the theme-shared validation blueprints. It validates from each field's FormFieldConfig.validators and renders its own error text (per-field message, with an English fallback), so don't expect the automatic localized blueprint messages here — set the message on the field config.
Validation
- Page a list and confirm the network request carries
MaxResultCount/SkipCount/Sorting and the grid updates; navigate away and back and confirm no leaked subscriptions (that's the component-scoped provider doing its job).
- Submit an invalid form and confirm ABP's localized message appears with no hand-written error markup.
Common Pitfalls
ListService provided in root. Then it never tears down and leaks across pages. Always component-scoped.
- Reaching for
totalCount$ or isLoading$. totalCount$ does not exist — read res.totalCount. isLoading$ exists but is deprecated; use requestStatus$.
- Hand-writing paging state (page index, skipCount math, debounce).
ListService already does all of it; wiring your own fights it.
- Manual validation markup.
@ngx-validate/core renders messages automatically once the control has Angular validators; adding your own *ngIf error blocks duplicates them.
- Confusing the two "dynamic form" APIs.
abp-dynamic-form (config-driven, here) is not the entity-extension form (generateFormFromProps / abp-extensible-form) — that belongs to extend-angular-module-ui.