Every Angular admin screen I have built starts the same way: a mat-table, a mat-paginator, a matSort, a filter input, then a dialog to create or edit a row. Each piece is simple. Wiring them together, again, in every project, is where the time goes.
I got tired of it and packaged my own answer as an open-source library, easy-mat-ui-ng20. Full disclosure: I'm the author. This post shows what it does with real code, and where it is not the right tool.
What you get
For an Angular 20 app with Angular Material, one configuration object gives you:
- a table with sorting and pagination
- a text or multi-select filter under each column title
- a toolbar with search, add and clear buttons
- view, edit and delete buttons per row
- a create/edit/view dialog built from column definitions
Live demo: https://easy-mat-ui.web.app
Install
npm install easy-mat-ui-ng20 The library expects Angular, Angular Material and CDK 20, @ngx-translate/core 16, Bootstrap 5 and RxJS 7 (peer dependencies). The labels go through ngx-translate, but you do not need a dictionary to start. Without one, texts show as written.
// app.config.ts import { ApplicationConfig } from '@angular/core'; import { provideTranslateService } from '@ngx-translate/core'; import { provideEasyPaginatorIntl } from 'easy-mat-ui-ng20'; export const appConfig: ApplicationConfig = { providers: [ provideTranslateService({ defaultLanguage: 'en' }), // Optional: the paginator follows the language of ngx-translate. provideEasyPaginatorIntl() ] }; A users table
Columns first. The order of the array is the order on screen.
const columns = [ // Text filter under the title: type to narrow the rows. new MatColumnTyped({ name: 'name', translateTitle: 'Name', enableFilter: true, filterType: 'text' }), new MatColumnTyped({ name: 'email', translateTitle: 'Email', enableFilter: true, filterType: 'text' }), // Multi-select filter (the default): it lists the values found in the column. new MatColumnTyped({ name: 'role', translateTitle: 'Role', enableFilter: true }), // A boolean as an icon. new MatColumnTyped({ name: 'active', translateTitle: 'Active', type: ColumnType.ICON, enableIcon: 'check_circle', disableIcon: 'cancel', stickyClass: 'text-center', conditionFn: (user: User) => user.active, cssClassFn: (user: User) => (user.active ? 'text-success' : 'text-danger') }), // View, edit and delete buttons of each row. new MatColumnTyped({ name: 'actions', headerIcon: 'rule', stickyClass: 'text-center' }) ]; Then the table. One config object describes all of it:
@Component({ selector: 'app-users', imports: [EasyTableComponent], template: ` <lib-easy-table [config]="config()" (dialogResult)="onDialogResult($event)" (deleteItem)="onDelete($event)" /> ` }) export class Users { readonly users = signal<User[]>(USERS); readonly config = computed( () => new EasyTableConfig<User>({ appearance: 'outline', withToolbar: true, // search field, add and clear buttons withFilterInHeader: true, // the column filters changePageSize: 10, // rows per page sortedBy: '', // keep the order of your data canCreate: true, canUpdate: true, canDelete: true, displayedColumns: columns, toolbarConfig: new ToolbarConfig({ icon: 'group', translations: new ToolbarTranslations({ title: 'Users' }) }), data: this.users(), dialog: { add: true, edit: true, view: true, mode: 'open', form: userDialog } }) ); } The table never changes your data. It tells you what happened through outputs (dialogResult, deleteItem), and you update your own state. With signals, a computed config rebuilds when the data changes, so the table refreshes by itself.
The dialog
The dialog is described the same way, with columns:
function userDialog(request: EasyTableDialogRequest<User>): EasyDialogInit<User> { return { title: 'User', titleField: 'name', action: request.action, data: request.action === CRUD.CREATE ? ({ id: 0, name: '', email: '', role: '', active: true } as User) : request.data, fields: [ new MatColumnTyped({ name: 'name', order: 1, isRequired: true }), new MatColumnTyped({ name: 'email', order: 2 }), new MatColumnTyped({ name: 'role', order: 3 }), new MatColumnTyped({ name: 'active', order: 4, inputType: InputType.Checkbox }) ], withAuditFields: false }; } The same function serves the "add" button, the "edit" button and the "view" button, based on request.action.
How filtering works
Worth knowing before you choose a library:
- A text filter keeps rows whose value contains the typed text, ignoring case.
- Use
+to give alternatives:paris+romekeeps rows containing either. - Filters on several columns combine with AND.
- The toolbar search looks across columns and ORs the
+words.
Where it is not the right tool
I would rather you hear it from me:
- Data is client-side. The table works on the array you give it (
MatTableDataSourceunderneath). There is no server-side paging and no virtual scrolling. For tens of thousands of rows, or for an API that returns one page at a time, use AG Grid or TanStack Table. - No spreadsheet-style cell editing. Editing happens in the dialog.
- Young project. First release November 2026, one maintainer. Pin your version.
What else is in the box
Reusable standalone components (category buttons, segmented control, language picker, cards, key figures, page header), light and dark themes, right-to-left support, and a command that generates a page from a TypeScript model: npx easy-mat-ui add-page. I wrote about that one separately.
Try it
- Edit it in your browser, nothing to install (StackBlitz): https://stackblitz.com/github/chaibiredouane/easy-mat-ui/tree/main/examples/stackblitz-starter
- Demo and docs: https://easy-mat-ui.web.app
- Step-by-step for this table: https://easy-mat-ui.web.app/docs/angular-material-table/
- Source (MIT): https://github.com/chaibiredouane/easy-mat-ui
- npm: https://www.npmjs.com/package/easy-mat-ui-ng20
If something is unclear or missing for your use case, open an issue. A concrete use case helps me more than a star.
