Enhance your Angular Grid with Formatted values and Links

  |   Angular

Cover Photo by Mika Baumeister on Unsplash

Value Based cell formatting in ag-Grid

In this article, I explain how to do some cell content convention based dynamic formatting.

What is Content Convention Based Dynamic Formatting?

Wow, that sounds awesome, but what do I mean by all these fancy words?

First of all, we need to have some conventions for how we name our fields. For example, if we always name our car columns ‘car’, then we can look for that name in our code.

And when we have found that car column we can add the formatting code to our column definitions in the grid. So this means that we could run different reports in our grid. And for all reports where we find this car column we can dynamically format the cells.

The end product will be the grid below where we have some nice formatting of dates and numbers. Above the grid, we have a router outlet. When links are clicked we route to it and send the text from the cell via route parameters.


Example Code

If you want to play with the code along the way, I prepared a StackBlitz with the finished code.

Pipe Setup

In my article Custom Angular Pipes and Dynamic Locale, I showed how to create custom pipes for formatting your dates and numbers.

We can use them in our component by importing them from the library we created in that article and then injecting them in the constructor:

import { LocalDatePipe, LocalNumberPipe } from 'lib-shared';
constructor(private dateFormatter: LocalDatePipe, private numberFormatter: LocalNumberPipe)

Similarly, we can use the standard Angular pipes:

import { DatePipe, DecimalPipe } from '@angular/common';
constructor(private dateFormatter: DatePipe, private numberFormatter: DecimalPipe) {}

These pipes will work the same as the custom ones if you don’t need the extra locality functionality that comes with them.

Since we are injecting the pipes in the constructor, we need to add them to providers in the module file so that they become injectable. (see app.modules.ts in the StackBlitz)

@NgModule({

  ...
  
  providers: [
    DatePipe,
    DecimalPipe
  ],
  
  ...
})

Dates

By naming all our date columns ‘xxxDate,’ we can easily discover them. In our example, the column is named ‘releaseDate’. When we know which columns contain dates we can format them with the pipe transform method.

From the docs:

“A valueFormatter allows you to format or transform the value for display purposes.”

A value formatter is just what we need. We look for all columns that end with ‘Date’ and add the date pipe to the valueFormatter property (see app.components.ts in the StackBlitz):

} else if (column.endsWith('Date')) {
        definition.valueFormatter = (data) => this.dateFormatter.transform(data.value, 'shortDate');
      }

ColumnDefs and RowData

We can use the code from the last paragraph to start building on our method to define the columns: setColumns(columns: string[])

As you can see we need to have the columns before we can run this method. And to have the columns in a dynamic setting, we have to fetch the data first.

How can we extract the column names from an array of unknown type?

You could, for example, take the first object from the array and get the column names with Object.keys(). It returns an array of a given object’s property names.

const columns = Object.keys(data[0]);

In our example, we have hard-coded our data and column names. We run this code in the constructor.

const columns = ['car', 'bus', 'releaseDate', 'price'];this.setColumns(columns);

Numbers

Find out if there are any conventions in the reports for numbers. If not then maybe you can create some. In our example, we look for the ‘price’ column.

When we know which column names to look for we can use our number formatter to format these numbers as we like. Again we use valueFormatter:

(params) => this.numberFormatter.transform(params.value, '1.2-2'),

We can also set the column definition type to numericColumn, which aligns the column header and contents to the right. (see app.components.ts in the example)

} else if (column === 'price') {
        definition.valueFormatter = (data) => this.numberFormatter.transform(data.value, '1.2-2');
        definition.type = 'numericColumn';
      }

Routing Setup

Let’s set up a basic routing for our app. Add a router outlet to the template that holds our grid. It acts as a placeholder where the router should display our components.

<router-outlet></router-outlet>
<ag-grid-angular [rowData]="rowData" [columnDefs]="columnDefs">
</ag-grid-angular>

Next, we can set up the routes where we setup route parameters to our components that are going to be the text from the cells.

const appRoutes: Routes = [
{ path: 'car/:car', component: CarComponent },
{ path: 'bus/:bus', component: BusComponent }
];

And lastly, we create a simple component where we subscribe to the route parameters and show them in our template.

I want to use the text in cells for specific columns as links. To achieve this valueFormatter is not enough. Instead, we need to create a component for our ag-Grid cells.

From the docs:

“By default the grid will create the cell values using simple text. If you want more complex HTML inside the cells then this is achieved using cell renderers.”

For re-usability, we can make a component that passes a link in as an argument. We can do this with cellRendererParams.

We want our column definition to look like this:

cellRendererFramework: MyLinkRendererComponent,
cellRendererParams: {    inRouterLink: '/yourlinkhere',}

In our component, we need to implement AgRendererComponent. With it comes the agInit() and refresh() methods. In our refresh, we return false which means your component will be destroyed and recreated if the underlying data changes.

Then there is the template. We can implement an Angular RouterLink where ‘params.value’ is the value from the cell:

<a [routerLink]="[params.inRouterLink, params.value]">    {{params.value}}</a>

I once ran into some issues where the link was not working, and I had to jump start the Angular change detection. If you run into that kind of problems and like I can’t figure out why its happening you can check my work-around on stack overflow.

But hopefully you won’t have to monkey patch anything, and the code will look like this:

import { Component } from '@angular/core';
import { AgRendererComponent } from 'ag-grid-angular';

@Component({
    template: `<a [routerLink]="[params.inRouterLink,params.value]">{{params.value}}</a>`
})
export class MyLinkRendererComponent implements AgRendererComponent {    
    params: any;    

    agInit(params: any): void {
        this.params = params;
    }

    refresh(params: any): boolean {
        return false;
    }    
}

We have to add the renderer to the entry components for the module. (see app.module.ts in the StackBlitz example)

@NgModule({
...
  entryComponents: [MyLinkRendererComponent],
  bootstrap: [AppComponent]
})
export class AppModule { }

Column Definitions

Now that we have our cell renderer we can set it up in the column definitions. )See app.component.ts in the StackBlitz)

export class AppComponent {

  columnDefs: ColDef[];
  rowData = [
    { 'car': 'Audi', 'releaseDate': '2018-01-04', 'price': 99000 },
    { 'car': 'Tesla', 'releaseDate': '2020-03-01', 'price': 49000 },
    { 'bus': 'MAN', 'releaseDate': '2015-02-02', 'price': 1234500 },
    { 'bus': 'Volvo', 'releaseDate': '2016-06-03', 'price': 2234500 }
  ];

  constructor(private dateFormatter: DatePipe, private numberFormatter: DecimalPipe) {
    const columns = ['car', 'bus', 'releaseDate', 'price'];
    this.setColumns(columns);
   }

  setColumns(columns: string[]) {
    this.columnDefs = [];
    columns.forEach((column: string) => {
      let definition: ColDef = { headerName: column, field: column, width: 120 };
      if (column === 'car' || column === 'bus') {
        definition.cellRendererFramework = MyLinkRendererComponent;
        definition.cellRendererParams = {
          inRouterLink: column,
        };
      } else if (column.endsWith('Date')) {
        definition.valueFormatter = (data) => this.dateFormatter.transform(data.value, 'shortDate');
      } else if (column === 'price') {
        definition.valueFormatter = (data) => this.numberFormatter.transform(data.value, '1.2-2');
        definition.type = 'numericColumn';
      }
      this.columnDefs.push(definition);
    });
  }

}

And that’s it!


The StackBlitz example for this post is available here with the finished code.

Learn about AG Grid Angular Support here, we have an Angular Quick Start Guide and all our examples have Angular versions.

Read more posts about...