Lately, we have started working on v3.0 of ABP Framework and our ultimate goal as the frontend team is to introduce solutions that improve developer experience. In this context, one of the things we are working on is migrating the current grid in Angular to the feature-rich ngx-datatable. Nevertheless, when we tried to implement it in the project, we have realized that the amount of code duplicated in each CRUD page was troublesome.

<ngx-datatable
  [rows]="data$ | async"
  [count]="totalCount$ | async"
  [loadingIndicator]="list.isLoading$ | async"
  [limit]="list.maxResultCount"
  [offset]="list.page"
  (page)="list.page = $event.offset"
  (sort)="sort($event)"
  [externalPaging]="true"
  [externalSorting]="true"
  [headerHeight]="50"
  [footerHeight]="50"
  rowHeight="auto"
  columnMode="force"
  class="material"
>
  <!-- templates here -->
</ngx-datatable>

We have a ListService which makes it easier to work with remote pagination and sorting. It is a core feature and we are planning to keep it as UI independent as possible. Some properties of ngx-datatable fit really well, while others, sorting specifically, do not.

Nothing is wrong with ngx-datatable. It actually is an amazing work and probably one of the best grids you can use in Angular. And, to be fair, all of the bindings above are for rendering the content properly, so they are not useless after all. Still, from a developer experience perspective, this is painful. Here is how we see it:

  • CRUD pages in the community version of ABP would require this code to be copied manually over and over. We should avoid this somehow.
  • Although there is a nice code generator for ABP Commercial users, readability and maintenance of the generated code is an important aspect. Less is more.

Naturally, we started looking for a way to reduce the amount of code that will be necessary each time ngx-datatable is consumed.

Attribute Directives to the Rescue

The initial idea was to handle property and event bindings between the grid and the ListService instance, so we started worked on an attribute directive that works as an adapter. Later, we removed all appearance-related properties too. The following is what we came up with in the end:

<ngx-datatable
  [rows]="data$ | async"
  [count]="totalCount$ | async"
  [list]="list"
  default
>
  <!-- templates here -->
</ngx-datatable>

Sweet, right? Thanks to two attribute directives, we now have much less code to worry about and a better focus on what really matters. The first directive, which has ngx-datatable[list] as selector, provides a single point of communication between the DatatableComponent and the ListService. The second directive, ngx-datatable[default], eliminates the noise created by property bindings just to make ngx-datatable styles match our project. We could have built only one directive, but followed single responsibility principle and ended up creating one for appearance and another for functionality. Our intention is to grant ABP developers the flexibility to remove default appearance when they want to implement their own styles.

The Adapter Directive

I am not planning to show you the implementation details of the actual ListService. All you need to know is, it is built based on ABP backend and is UI independent. I will, however, describe what we needed to bind from it to the grid, and vice versa. First, let us take a look at the directive code:

@Directive({
  // tslint:disable-next-line
  selector: 'ngx-datatable[list]',
  exportAs: 'ngxDatatableList',
})
export class NgxDatatableListDirective implements OnChanges, OnDestroy, OnInit {
  private subscription = new Subscription();

  @Input() list: ListService;

  constructor(private table: DatatableComponent, private cdRef: ChangeDetectorRef) {
    this.table.externalPaging = true;
    this.table.externalSorting = true;
  }

  private subscribeToPage() {
    const sub = this.table.page.subscribe(({ offset }) => {
      this.list.page = offset;
      this.table.offset = offset;
    });
    this.subscription.add(sub);
  }

  private subscribeToSort() {
    const sub = this.table.sort.subscribe(({ sorts: [{ prop, dir }] }) => {
      this.list.sortKey = prop;
      this.list.sortOrder = dir;
    });
    this.subscription.add(sub);
  }

  private subscribeToIsLoading() {
    const sub = this.list.isLoading$.subscribe(loading => {
      this.table.loadingIndicator = loading;
      this.cdRef.markForCheck();
    });
    this.subscription.add(sub);
  }

  ngOnChanges({ list }: SimpleChanges) {
    if (!list.firstChange) return;

    const { maxResultCount, page } = list.currentValue;
    this.table.limit = maxResultCount;
    this.table.offset = page;
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  ngOnInit() {
    this.subscribeToPage();
    this.subscribeToSort();
    this.subscribeToIsLoading();
  }
}

Here is what every property and method does:

  • list is for binding the ListService instance.
  • table is the DatatableComponent instance, retrieved from the dependency injection system.
  • cdRef is the ChangeDetectorRef instance, through which we can mark the host for change detection.
  • ngOnChanges is sets limit and offset properties of the table at first change.
  • ngOnInit initializes subscriptions to page and sort events of the grid, as well as the isLoading$ of the service.
  • subscribeToPage, subscribeToSort, and subscribeToIsLoading are mapping observable properties at both end.
  • subscription is for collecting RxJS subscriptions, which are later unsubscribed from at ngOnDestroy lifecycle hook.

The key takeaway in this is the fact that an attribute directive in Angular can obtain the host instance, hook into its events, manipulate its properties, and even execute change detection manually on it when necessary. The possibilities are endless. You can adapt a component to any interface and avoid the performance penalty of the default change detection strategy, if you like.

Another important aspect is the directive selector. The selector queries only ngx-datatable elements with a list attribute, effectively leaving the datatables without the attribute or other elements with a list property alone. You will probably want to place // tslint:disable-next-line above the selector though, because the linters are usually configured to throw an error when directive selectors do not start with the app or library prefix.

The Default Properties Directive

Creating an Angular directive for default properties is much easier compared to an adapter directive and, in consequence, probably less exciting. It is quite advantageous though. This is what it looks like:

@Directive({
  // tslint:disable-next-line
  selector: 'ngx-datatable[default]',
  exportAs: 'ngxDatatableDefault',
})
export class NgxDatatableDefaultDirective {
  @Input() class = 'material bordered';

  @HostBinding('class')
  get classes(): string {
    return `ngx-datatable ${this.class}`;
  }

  constructor(private table: DatatableComponent) {
    this.table.columnMode = ColumnMode.force;
    this.table.footerHeight = 50;
    this.table.headerHeight = 50;
    this.table.rowHeight = 'auto';
  }
}

The DatatableComponent instance is again retrieved from Angular's dependency injection system. Then, several properties are set within the constructor, before any property binding to that instance occurs. Thus, the defaults defined by the DatatableComponent class are effectively overridden.

The only problematic property is class here and that is due to this implementation of host binding in ngx-datatable. The workaround is taking class as an input property and binding its value to host element after concatenation with ngx-datatable class. When no class is given, a material theme is applied and some custom borders are added. Setting another class through the template is still available, so the interface is unchanged.

Finally, it would work perfectly fine if we referred to just ngx-datatable as the selector, but we want to give the developer the opportunity to drop this directive altogether, so we did not.

Conclusion

People tend to adapt their components to UI kits and libraries, but UI may be altered in time. Core API decisions and business logic, on the other hand, do not change as frequently. I believe, we should adapt UI elements to our components and services, and not the other way around. Some libraries offer injectable adapters for this purpose, which is nice, because they can be employed in order to adjust the behavior. Most, though, do not. As you see, adapter attribute directives prove handy in such cases.

Placing same properties repeatedly in templates is also troubling. I have always thought this as a weak point of HTML. Now we have frontend frameworks and we keep doing the same. Angular provides us tools to avoid this and an attribute directive for default properties makes perfect sense in my opinion. Check your own project. You will surely find a good use for them.

I have created a StackBlitz playground with a dummy backend and a simplified list service. You can see both directives in action.

Thanks for reading. Have a nice day.


Read More:

  1. Real-Time Messaging In A Distributed Architecture Using ABP, SignalR & RabbitMQ
  2. ASP.NET Core 3.1 Webhook Implementation Using Pub/Sub Pattern
  3. Why You Should Prefer Singleton Pattern over a Static Class?