Skip to content

Writing a table

Copy of sumaris-app/.agents/rules/table.md (rule written for AI coding agents, when creating or modifying table components (AppTable)). Do not edit here: run npm run site:sync-rules to update it.

  • A table component must extend AppTable<T, F> where T is the entity type and F is the filter type;
  • Use changeDetection: ChangeDetectionStrategy.OnPush;
  • Use the file suffix .table.<extension> (see common rules);

Providers

  • In the @Component decorator, declare the following providers:
    • A AppValidatorService provider mapped to a concrete validator service for the entity;
    • A AppTable provider using forwardRef(() => MyTableComponent);
      providers: [
        { provide: AppValidatorService, useClass: MyEntityValidatorService },
        { provide: AppTable, useExisting: forwardRef(() => MyTableComponent) }
      ]
      

Constructor

  • The constructor receives an Injector and passes to super():
    1. The injector;
    2. The list of columns, wrapped between RESERVED_START_COLUMNS and RESERVED_END_COLUMNS:
      super(injector, [
        ...RESERVED_START_COLUMNS,
        'column1',
        'column2',
        // ... user columns
        ...RESERVED_END_COLUMNS
      ], dataSource, null);
      
    3. An EntitiesTableDataSource<T> instance, built with the entity class, a data service, and the validator service;
    4. The initial filter (usually null);
  • Configure table options in the constructor:
    this.autoLoad = false;           // or true, depending on the use case
    this.inlineEdition = true;       // enable inline row editing
    this.i18nColumnPrefix = 'MY_TABLE.';
    this.confirmBeforeDelete = true;
    this.saveBeforeSort = true;
    this.saveBeforeFilter = true;
    

Validator service

  • Create a dedicated validator service that extends AppValidatorService (or a subclass like ReferentialValidatorService);
  • Implement getFormGroup(data?: T, opts?: any): UntypedFormGroup to define the form controls and their validators;
  • If the entity has nested properties, add sub-form groups:
    getFormGroup(data?: MyEntity, opts?: any): UntypedFormGroup {
      const form = super.getFormGroup(data, opts);
      form.addControl('properties', this.getPropertiesForm(data?.properties));
      return form;
    }
    

Lifecycle

  • In ngOnInit():
    • Call super.ngOnInit();
    • Register filter form value changes with this.registerSubscription(...);
    • Register autocomplete fields with this.registerAutocompleteField(...);
    • Call this.markAsReady() at the end;
  • In ngAfterViewInit():
    • Call super.ngAfterViewInit();
    • Restore filter from settings if needed;
    • Trigger initial data load;
  • In ngOnDestroy():
    • Call super.ngOnDestroy();
    • Clean up any custom subscriptions (timers, etc.);

HTML template

  • Use a <table mat-table matSort matSortDisableClear> element bound to [dataSource]="dataSource";
  • For each column, define an <ng-container matColumnDef="columnName"> with:
    • A <th mat-header-cell> with mat-sort-header (if sortable) and a translated label;
    • A <td mat-cell> with a <mat-form-field> bound to row.validator.controls['columnName'];
  • Use [appAutofocus]="row.editing && focusColumn === 'columnName'" and [readonly]="!row.editing" on inputs;
  • For the selection column, use matColumnDef="select" with <mat-checkbox> bound to selection;
  • For the ID column, use matColumnDef="id" displaying row.currentData?.id;
  • For action buttons, use the <app-actions-column> component with event bindings:
    <app-actions-column [stickyEnd]="true"
      (cancelOrDeleteClick)="cancelOrDelete($event.event, $event.row)"
      (confirmAndAddClick)="confirmAndAdd($event.event, $event.row)"
      (backward)="confirmAndBackward($event.event, $event.row)"
      (forward)="confirmAndForward($event.event, $event.row)">
    </app-actions-column>
    
  • Define the row templates:
    <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"
        [class.mat-row-selected]="row.editing"
        [class.mat-row-error]="row.validator?.invalid"
        [class.mat-row-disabled]="!row.editing"
        [class.mat-row-dirty]="row.validator?.dirty"
        (click)="clickRow($event, row)"
        (keydown.escape)="escapeEditingRow($event)">
    </tr>
    
  • Add a loading skeleton, a "no result" message, and optionally an <ion-infinite-scroll> for mobile;
  • Add a <mat-paginator> in the footer for desktop pagination;

Module

  • Declare the table component in a dedicated module importing CommonModule, SharedModule, CoreModule, and the standalone TranslatePipe (from @ngx-translate/core) — not TranslateModule.forChild(), which is no longer used in this codebase;

Reference example

  • See ngx-sumaris-components/src/app/core/table/testing/ for a complete working example:
    • table.testing.ts — component class;
    • table.testing.html — template;
    • table-validator.service.ts — validator;
    • table.testing.module.ts — module declaration;