Click here to Skip to main content
14,580,573 members

agGrid for Angular (The Missing Manual)

Rate this:
0.00 (No votes)
Please Sign up or sign in to vote.
0.00 (No votes)
1 May 2020CPOL
Getting started with agGrid for Angular
In this tutorial, I'll show you how to get started with using agGrid with Angular, and explain how to get around the frustrating pieces that are missing.

Introduction

I've recently been getting up to speed with using agGrid in an Angular 9 application, and despite its huge popularity, I've been amazed at what a struggle it's been.

The agGrid website looks great, it has an Enterprise edition, and the demos look really cool... but once you start using it, you'll suddenly find the need for lots of Googling, head scratching and strong alcohol. It comes as no surprise that the agGrid website doesn't let developers like myself post comments or ask for help...

This article will walk you through the steps to create a new Angular app, add agGrid to it, and then walk you through some of the problems you'll face.

Our end result will be this grid, with custom renderers for dates, checkboxes, and a pull down list of reference data.

Image 1

To follow this tutorial, I expect you to have a knowledge of:

  • Angular
  • TypeScript
  • HTML
  • a copy of Visual Studio Code

Let's get started!

The agElephant in the Room

If you're interested enough to read this article, chances are that you'll know that agGrid already provides a webpage showing how to set yourself up with agGrid with Angular. You'll find it at this link.

Ah, heck.

Shall I stop typing now, and head for the pub? Sadly not.

Although agGrid's website looks slick and polished, it deliberately avoids mentioning many of the problems which you'll hit when you start learning agGrid.

In this tutorial, I'm going to load some "real-world" data from a web service, which will neatly demonstrate the problems, and show you how to solve them. Here's an example record:

{
   "id": 3000,
   "jobRol_ID": 1001,
   "firstName": "Michael",
   "lastName": "Gledhill",
   "imageURL": 
   "https://process.filestackapi.com/cache=expiry:max/resize=width:200/FYYq9KL6TnqtOT6TuQ3g",
   "dob": "1980-12-25T00:00:00",
   "bIsContractor": false,
   "managerID": null,
   "phoneNumber": "044 123 4567",
   "bWheelchairAccess": false,
   "startDate": "2020-02-17T00:00:00",
   "updateTime": "2019-10-18T00:00:00",
   "updatedBy": "mike"
},

Looks pretty standard, no? But, out of the box, you'll immediately hit issues with agGrid:

  • agGrid doesn't provide a way to display dates in a friendly "dd/MMM/yyyy" format. You have to write your own date formatter, plus a control to let your users pick a new date. (Seriously ?!)
  • when editing a row of data, agGrid treats each value as a string. So, rather than seeing a checkbox control for my boolean values, you'll get a textbox with the string "true" or "false" in it.
  • in my record (above), jobRol_ID is actually a foreign key value, linked to a reference data like this:
    {
        "id": 1000,
        "name": "Accountant"
    },
    {
        "id": 1001,
        "name": "Manager"
    },

    For this cell, I want to display the grid to show the reference data text value for this id. When I edit it, I would like a popup to appear of reference data strings. When a user chooses a string, I want my record to be updated with the id value of it.

So how does the agGrid documentation approach these problems? It avoids them. In their demos, date values are (always?) already pre-formated as "dd/mm/yyyy" strings, they avoid mentioning checkboxes (except using them to select an entire row), and for drop down lists, they just use strings... never a reference data "id".

My experiences with agGrid & Angular have been hugely painful and frustrating, and this is the article which I wish I had when I started out.

All of the source code is provided in the attached .zip file, but I strongly recommend you create your own Angular project, and cut'n'paste the files from the article as you read it.

Let's Get Started!

Let's start by having a look at what we're going to create.

I have setup a basic WebAPI in Azure, with a few endpoints. We're only going to use the two GET endpoints in this tutorial:

You can see the Swagger page here:

To keep things really simple, all we are going to do in this sample web application is display a list of our employees, let you edit them. Using a modern up-to-date grid library for Angular, this should be simple, no?

Here's the database schema. As I said before, this will be enough to demonstrate the problems you'll encounter when writing a full-blown enterprise app.

Image 2

You'll notice that I'm including the full text of my source files here. Nope, I'm not trying to pad out this article. I strongly recommend that you cut'n'paste from here, rather than downloading the full source code (which is included, at the top of the article). agGrid and Angular change so often that it'll be the safest way to make sure this all works in the future.

I also recommend that you go through this tutorial step-by-step, and check it's working as you're going along. Angular has a nasty habit of being regularly updated, and subtly breaking existing code.

1. Create the Angular Application

This isn't meant to be an Angular tutorial, so I'm going to rush through this bit. Hold tight!

In your favourite command prompt, create a new Angular app, and open it in Visual Studio Code.

ng new Employees --routing=true --style=scss  
cd Employees
code .

Now, in Visual Studio Code, click on Terminal \ New Terminal (if a Terminal window is not already open), and let's install agGrid, Bootstrap, and rxjs:

npm install --save ngx-bootstrap bootstrap rxjs-compat
npm install --save ag-grid-community ag-grid-angular 
npm install --save ag-grid-enterprise

Okay. Our frameworks are installed, let's write some code...

2. Add the "Models"

In Visual Studio Code, go into the src\app folder, and create a new folder, models. In here, we'll create two files, to contain classes representing our two database tables. First, employee.ts:

export class Employee {
    id: number;
    dept_id: number;
    jobRol_ID: number;
    firstName: string;
    lastName: string;
    imageURL: string;
    dob: Date;
    bIsContractor: boolean;
    managerID: number;
    phoneNumber: string;
    bWheelchairAccess: boolean;
    startDate: Date;
    xpos: number;
    ypos: number;
    updateTime: Date;
    updatedBy: string;
}

Next, create a file, jobRole.ts:

export class JobRole {
    id: number;
    name: string;
    imageURL: string;
}

As I said, you can click on the following two URLs, to see the JSON which we'll be downloading from our webservice, and these correspond with the fields defined in these classes.

3. Add the "Service"

When writing Angular code, it's always tempting to directly load the data within your Component. But it makes a lot more sense to keep this code in a separate service, which we can inject into the components that need it.

So, in your src\app folder, let's create a new folder called services. In this folder, add a new file called app.services.ts:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Employee } from '../models/employee';
import { JobRole } from '../models/jobRole';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';

@Injectable()
export class EmployeesService {
    readonly rootURL = 'https://mikesbank20200427060622.azurewebsites.net';

    constructor(private http: HttpClient) {
    }

    loadEmployees(): Observable<Employee[]> {
        var URL = this.rootURL + '/api/Employees';
        return this.http.get<Employee[]>(URL)
            .catch(this.defaultErrorHandler());
    }

    loadJobRoles(): Observable<JobRole[]> {
        var URL = this.rootURL + '/api/JobRoles';
        return this.http.get<JobRole[]>(URL)
            .catch(this.defaultErrorHandler());
    } 

    private defaultErrorHandler() {
        return (error:any) => Observable.throw(error.json().error || 'Server error');
    }
}

4. Include our Dependencies

Next, let's hop across to the app.module.ts file in the app folder. Notice how we're telling it that we're using agGrid, Http, and also our EmployeesService.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { EmployeesService } from './services/app.services';
import { AgGridModule } from 'ag-grid-angular';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    AgGridModule.withComponents([])
  ],
  providers: [EmployeesService],
  bootstrap: [AppComponent]
})
export class AppModule { }

5. Add Some Styling

Simply replace the contents of the styles.scss file with this:

@import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
@import "../node_modules/ag-grid-community/src/styles/ag-grid.scss";
@import 
"../node_modules/ag-grid-community/src/styles/ag-theme-alpine/sass/ag-theme-alpine-mixin.scss";

.ag-theme-alpine {
    @include ag-theme-alpine();
}

body {
    background-color:#ccc;
}
h3 {
    margin: 16px 0px;
}

6. A Little Bit of HTML...

Remove all the HTML in the app.component.html file, and replace it with this:

<div class="row">
  <div class="col-md-10 offset-md-1">
    <h3>
      Employees
    </h3>
    <ag-grid-angular 
      style="height:450px"
      class="ag-theme-alpine" 
      [columnDefs]="columnDefs"
      [rowData]='rowData'
      [defaultColDef]='defaultColDef'
    >
    </ag-grid-angular>
  </div>
</div>

7. And the TypeScript for the HTML

Now, we need to change our app.component.ts file to look like this:

import { Component } from '@angular/core';
import { EmployeesService } from './services/app.services';
import { JobRole } from './models/jobRole';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'Employees';
  rowData: any;
  columnDefs: any;
  defaultColDef: any;

  constructor(private service: EmployeesService) {

    this.service.loadJobRoles().subscribe(
      data => {
          this.createColumnDefs(data);
      });

      this.service.loadEmployees().subscribe(data => {
        this.rowData = data;
      });
  }

  createColumnDefs(jobRoles: JobRole[]) {
    this.columnDefs = [
      { headerName:"ID", field:"id", width:80 },
      { headerName:"First name", field:"firstName", width:120 },
      { headerName:"Last name", field:"lastName", width:120 },
      { headerName:"Job role", field:"jobRol_ID", width:180 },
      { headerName:"DOB", field:"dob", width:160 },
      { headerName:"Contractor?", field:"bIsContractor", width:120 },
      { headerName:"Phone", field:"phoneNumber", width:150 },
      { headerName:"Wheelchair?", field:"bWheelchairAccess", width:120 },
      { headerName:"Start date", field:"startDate", width:180 },
      { headerName:"Last update", field:"updateTime", width:180 }
    ];
    this.defaultColDef = {
      sortable: true,
      resizable: true,
      filter: true,
      editable: true
    }
  }
}

Phew. Now, in your terminate window, we can start the application:

ng serve 

If all goes well, after all that cutting'n'pasting, you'll be able to open up a browser, head over to http://localhost:4200 and end up with an agGrid with data from our service:

Image 3

What Just Happened ?

Now, lots of things just happened in that code, which you need to pay attention to.

Notice particularly that we load in our JobRoles data before we attempt to define our column definitions for our agGrid. If we attempted to just draw the agGrid before this data is ready, the grid will display, but we won't be able to create a drop-down-list of JobRole options. We'll get to this later...

We're also getting our service to load a list of Employee records, storing them in a rowData variable, which is the data we've asked the agGrid to display.

Finally, we've also defined a few defaults for the grid, such as allowing any of the fields to be editable, filterable and sortable. So, right now, you can see an agGrid in action - you add drag columns to reorder them, click on a header to sort, and add filtering. It's pretty cool.

Displaying Dates in a Friendly Format

I was pretty shocked when, after proudly getting this far, I suddenly found that agGrid doesn't give you a simple way to display dates in a friendly format.

I was even more shocked to find that (at the time of writing), I couldn't find anyone who'd posted an article showing how to implement this, in a reuseable way, for Angular. Displaying dates is a basic requirement for using any type of grid, and this really should've been included in the Getting Started guide somewhere.

The cleanest way to implement this functionality is to:

  • create your own CellRenderer for displaying dates in a format like "dd/MMM/yyyy"
  • create your own CellEditor to make a popup calendar appear when you edit the value

Strap yourself in... I told you this wasn't going to be pretty.

First, let's create a "cellRenderers" folder in our app, and create a file called DateTimeRenderer.ts in this folder:

import { Component, LOCALE_ID, Inject } from '@angular/core';
import { ICellRendererAngularComp } from 'ag-grid-angular';
import { ICellRendererParams } from 'ag-grid-community';
import { formatDate } from '@angular/common';

@Component({
    selector: 'datetime-cell',
    template: `<span>{{ formatTheDate() }}</span>`
})
export class DateTimeRenderer implements ICellRendererAngularComp {

    params: ICellRendererParams; 
    selectedDate: Date;

    constructor(@Inject(LOCALE_ID) public locale: string) { }

    agInit(params: ICellRendererParams): void {
        this.params = params;
        this.selectedDate = params.value;
    }

    formatTheDate() {
        //  Convert our selected Date into a readable format
        if (this.selectedDate == null)
            return "";

        return formatDate(this.selectedDate, 'd MMM yyyy', this.locale);
    }

    public onChange(event) {
        this.params.data[this.params.colDef.field] = event.currentTarget.checked;
    }

    refresh(params: ICellRendererParams): boolean {
        this.selectedDate = params.value;
        return true;
    }
}

Next, head over to app.module.ts, and add it to the declarations:

import { DateTimeRenderer } from './cellRenderers/DateTimeRenderer';

And then tell our @NgModule about it, in the declarations and imports sections:

@NgModule({
  declarations: [
    AppComponent,
    DateTimeRenderer
  ],
  imports: [
    BrowserModule,
    AgGridModule.withComponents([DateTimeRenderer])
  ],

With this in place, we can go back to our app.component.ts file. First, let's include this new component:

import { DateTimeRenderer } from './cellRenderers/DateTimeRenderer';

We can now add this renderer to our three date fields:

this.columnDefs = [
  . . .
  { headerName:"DOB", field:"dob", width:160, cellRenderer: 'dateTimeRenderer' },
  . . .
  { headerName:"Start date", field:"startDate",
    width:180, cellRenderer: 'dateTimeRenderer' },
  { headerName:"Last update", field:"updateTime",
    width:180, cellRenderer: 'dateTimeRenderer' }
];

Two more changes. We also need to tell our agGrid that we're using a homemade cell renderer. To do this, we need to add a new variable:

export class AppComponent {
   . . .
   frameworkComponents = {
     dateTimeRenderer: DateTimeRenderer
   }

   constructor(private service: EmployeesService) {
   . . .
}

And, in the app.component.html file, we need to tell it to use this variable:

<ag-grid-angular
  [frameworkComponents]='frameworkComponents'
  . . .

With all this in place, we finally have the dates in our three date columns in a readable format.

Image 4

Adding Cell Parameters

This looks really nice, but it would be much better if we could somehow make the format more generic. Perhaps, our American users want to see the dates shown as "mm/dd/yyyy".

To do this, we can add a CellRendererParams value to our columnDef records:

this.columnDefs = [
    . . .
    { headerName:"DOB", field:"dob", width:160, cellRenderer: 'dateTimeRenderer' ,
         cellRendererParams: 'dd MMM yyyy' },
    { headerName:"Start date", field:"startDate", width:180, cellRenderer: 'dateTimeRenderer',
         cellRendererParams: 'MMM dd, yyyy  HH:mm' },
    { headerName:"Last update", field:"updateTime", width:140, cellRenderer: 'dateTimeRenderer',
         cellRendererParams: 'dd/MM/yyyy' }

Now, we just need to make our CellRenderer use these parameters, when they exist. Back in the DateTimeRenderer.ts file, we will add a dateFormat string with a default value, and when initializing, we'll see if we specified a parameter to use:

export class DateTimeRenderer implements ICellRendererAngularComp {

    params: ICellRendererParams; 
    selectedDate: Date;
    dateFormat = 'd MMM yyyy';

    agInit(params: ICellRendererParams): void {
        this.params = params;
        this.selectedDate = params.value;

        if (typeof params.colDef.cellRendererParams != 'undefined') {
            this.dateFormat = params.colDef.cellRendererParams;
        }
    }

Now, we just need to use that in our formatTheDate function:

formatTheDate() {
    //  Convert a date like "2020-01-16T13:50:06.26" into a readable format
    if (this.selectedDate == null)
        return "";

    return formatDate(this.selectedDate, this.dateFormat, this.locale);
}

And look, with very little effort, we've created a reuseable date-time renderer, which our developers can easily implement, and choose their own date formats:

Image 5

Isn't this cool? Well, it is, until our pesky users attempt to edit the date.

We'll tackle that next.

Binding a Date to an Angular Materials DatePicker

The solution above is fine for displaying dates, but if we try editing one of these dates, we're back to having a text box. Not a great user experience.

Image 6

To improve our user experience, let's add Angular Materials to our project, and show how to get Material's DatePicker control to appear when we're editing a date.

First, we need to add Angular Materials to our project:

npm install --save @angular/material @angular/cdk @angular/animations hammerjs

Next, in the styles.scss file, add a couple of imports:

@import "../node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css";
@import 'https://fonts.googleapis.com/icon?family=Material+Icons';

... and we also need to add a few extra styles....

.mat-calendar-body-active div {
    border: 2px solid #444 !important;
    border-radius: 50% !important;
}

.mat-calendar-header {
    padding: 0px 8px 0px 8px !important;
}

Next, we need to tell our app.module.ts file that we're going to be using the DatePicker.

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from "@angular/material/core";
import { MatInputModule } from '@angular/material/input';

We need to add the Angular Materials libaries into their own module...

@NgModule({
  imports: [
    MatDatepickerModule,
    MatNativeDateModule,
    MatInputModule
  ],
  exports: [
    MatDatepickerModule,
    MatNativeDateModule,
    MatInputModule
  ]
})
export class MaterialModule { }

And then import this new MaterialModule into our app's module:

imports: [
  BrowserAnimationsModule,
  MaterialModule,
  . . .

I'm always a little nervous after adding new libraries into my application, so at this point, I'd recommend heading over to the app.component.html file, and add a few lines of HTML after our agGrid, just to test that the DatePicker is working okay.

<input matInput [matDatepicker]="picker">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>

Assuming this is working okay for you, let's go and add the DatePicker's <mat-calendar> control into a new CellRenderer. In the cellRenderers folder, create a new file called DatePickerRenderer.ts:

import { Component, LOCALE_ID, Inject, ViewChild } from '@angular/core';
import { ICellRendererAngularComp } from 'ag-grid-angular';
import { ICellRendererParams } from 'ag-grid-community';
import { formatDate } from '@angular/common';
import { MatDatepickerModule, MatDatepicker } from '@angular/material/datepicker';

@Component({
    selector: 'datetime-cell',
    template: `<mat-calendar [startAt]="thisDate" (selectedChange)="onSelectDate($event)">
               </mat-calendar>`
})
export class DatePickerRenderer implements ICellRendererAngularComp {
    params: ICellRendererParams;
    thisDate: Date;

    agInit(params: ICellRendererParams): void {
        this.params = params;

        var originalDateTime = this.params.data[this.params.colDef.field];
        if (originalDateTime == null)
            this.thisDate = new Date();     //  Default value is today
        else
            this.thisDate = new Date(originalDateTime);
    }

    getValue() {
        //  This gets called by agGrid when it closes the DatePicker control.
        //  agGrid uses it to get the final selected value.
        var result = new Date(this.thisDate).toISOString();
        return result;
    }

    isPopup() {
        //  We MUST tell agGrid that this is a popup control, to make it display properly.
        return true;
    }

    public onSelectDate(newValue) {
        // When we select a date, we'll store this, 
        // then get agGrid to close the Calendar control.
        this.thisDate = newValue;
        this.params.api.stopEditing();
    }

    refresh(params: ICellRendererParams): boolean {
        return true;
    }
}

This is all fairly simple. We create a <mat-calendar> object with an initial date of thisDate. When we select a different date, we update our thisDate value and get agGrid to close our popup.

Timezones

It's worth mentioning that if you choose to use a different DatePicker library, check that the value which is selected doesn't include a timezone section. With primeNg, for example, I found that I would click on December 25 1980, but it would actually return a value of:

1980-25-12T01:00:00

Ah. In this case, I needed to add some special code to take my chosen value, get the timezone of my machine, and offset the selected date by this timezone.

createDateString(dateStr) {
    //  You will ONLY need this, if you DatePicker returns a date, with a timezone section.
    //
    //  This converts a date in this format:
    //      Tue Mar 12 1985 00:00:00 GMT+0100 (Central European Standard Time)
    //  into a string in this format:
    //      "1985-03-12T01:00:00"
    // 
    var tzoffset = (new Date()).getTimezoneOffset() * 60000;
    var currentDate = new Date(dateStr);
    var withTimezone = new Date(currentDate.getTime() - tzoffset);
    var localISOTime = withTimezone.toISOString().slice(0, 19).replace("Z", "");
    return localISOTime;
}

In this example though, we don't need this function, as the <mat-calendar> does return a Date object of midnight at our selected Date.

Back to our code.

Now we need to tell our app about our new DatePickerRenderer component. Go into the app.module.ts file, and import it:

import { DatePickerRenderer } from './cellRenderers/DatePickerRenderer';

Then add it to the declarations:

declarations: [
  DatePickerRenderer,
  . . .
],

And into our imports:

imports: [
  BrowserModule,
  . . .
  AgGridModule.withComponents([DateTimeRenderer, DatePickerRenderer])
],

Now, it's ready to be used in our components.

So let's go into app.component.ts, and import it there:

import { DatePickerRenderer } from './cellRenderers/DatePickerRenderer';

...add it to our list of frameworkComponents...

frameworkComponents = {
  dateTimeRenderer: DateTimeRenderer,
  datePickerRenderer: DatePickerRenderer
}

...and now, we can finally add a cellEditor attribute to each of our three date columns....

{ headerName:"DOB", field:"dob", width:160,
     cellRenderer: 'dateTimeRenderer', cellRendererParams: 'dd/MMM/yyyy  HH:mm',
     cellEditor: 'datePickerRenderer' },

Phew. That's one hell of a lot of work, just to add a basic date picker to a grid control.

Image 7

But, of course, once you've done it in one place in your application, you just need to repeat those final three steps whenever you want to reuse this component in your other grids.

Do check that this is all working, before continuing. Double-click on a date, make sure calendar appears, select a date, and check that the chosen date is now shown in your grid.

Binding a Boolean Field to a Checkbox

Our next problem is that agGrid displays boolean values as a "true" or "false" string. And when you edit them, it just shows a textbox:

Image 8

Yeah, that really sucks.

Sadly, to turn this into a checkbox, we have to write another cell renderer.

In our project, let's create a new folder, CellRenderers, and in this folder, we will add a new file, CheckboxRenderer.ts:

import { Component } from '@angular/core';
import { ICellRendererAngularComp } from 'ag-grid-angular';
import { ICellRendererParams } from 'ag-grid-community';

@Component({
    selector: 'checkbox-cell',
    template: `<input type="checkbox" [checked]="params.value" (change)="onChange($event)">`
})
export class CheckboxRenderer implements ICellRendererAngularComp {

    public params: ICellRendererParams; 

    constructor() { }

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

    public onChange(event) {
        this.params.data[this.params.colDef.field] = event.currentTarget.checked;
    }

    refresh(params: ICellRendererParams): boolean {
        return true;
    }
}

As before, because we've created a new CellRenderer, we need to:

  • tell our NgModule about it, in the app.module.ts file
  • tell any of our Components which use this CellRenderer about it

So, in app.module.ts, we need to add an "include"...

import { CheckboxRenderer } from './cellRenderers/CheckboxRenderer';

...and add it to our declarations and imports...

@NgModule({
  declarations: [
     CheckboxRenderer,
     . . .
  ],
  imports: [
     . . .
     AgGridModule.withComponents([DateTimeRenderer, DatePickerRenderer, CheckboxRenderer])
  ],

Now, we need to tell our component about it.
Let's go into the app.component.ts file, and include it there...

import { CheckboxRenderer } from './cellRenderers/CheckboxRenderer';

Then add it to our frameworkComponents section...

frameworkComponents = {
    dateTimeRenderer: DateTimeRenderer,
    datePickerRenderer: DatePickerRenderer,
    checkboxRenderer: CheckboxRenderer
  }

And, with all this in place, we can add this renderer to our two boolean fields:

this.columnDefs = [
  . . .
  { headerName:"Contractor?", field:"bIsContractor",
    width:120, cellRenderer: 'checkboxRenderer' },
  . . .
  { headerName:"Wheelchair?", field:"bWheelchairAccess",
    width:120, cellRenderer: 'checkboxRenderer' },
  . . .
];

And once again, with all the pieces in place, we finally have checkboxes that are bound to our data.

Image 9

Actually, one thing you may notice is that if you double-click on the checkbox, it's replaced with a textbox, with either "true" or "false" in it. You can get around this by making:

this.columnDefs = [ 
    . . . 
    { headerName:"Contractor?", field:"bIsContractor", width:120, 
        cellRenderer: 'checkboxRenderer', editable: false }, 
    . . . 
    { headerName:"Wheelchair?", field:"bWheelchairAccess", width:120, 
        cellRenderer: 'checkboxRenderer', editable: false }, 
    . . . 
];

Yeah, it's a bit dumb. But you can still tick/untick the checkboxes, but this prevents that nasty textbox from appearing.

Foreign Keys

The other obvious thing that the agGrid authors carefully avoided documenting is how to bind a foreign key to a drop down list of { id, name } reference data in the grid.

Now, the Employee records I receive from my web service don't actually contain the Job Role string for each user. I doubt your REST service data does either. My Employee records contain a jobRol_ID value, which refers to a particular JobRole record.

Image 10

So, obviously, we want to get our agGrid to display the string "Manager", rather than the value "1001". And if the user edits this value, we want to see a drop down list of JobRole "name" values, but when they make a selection, obviously, the jobRol_ID value should be updated with a new id value, rather than the JobRole name string.

Now, during development, what I'm going to do is replace my one "Job Role" column definition with two column definitions, so I can see the raw JobRol_ID value, and the drop down list next to it.

{ headerName:"Job role ID", field:"jobRol_ID", width:130 },
{ 
  headerName:"Job role", field:"jobRol_ID", width:180, 
  cellEditor: 'agSelectCellEditor', 
  cellEditorParams: {
    cellHeight:30,
    values: jobRoles.map(s => s.name)
  },
  valueGetter: (params) => jobRoles.find(refData => refData.id == params.data.jobRol_ID)?.name,
  valueSetter: (params) => {
    params.data.jobRol_ID = jobRoles.find(refData => refData.name == params.newValue)?.id
  }
},

(I'm quite serious, you have no idea how many hours it took me, to create this tiny piece of code.... I couldn't find an example like this anywhere.)

Image 11

But it works! When I edit an item in the Job Role column, it correctly shows me a list of (text) options, and when I select one, I can see in the Job Role ID column that it has updated my record with the id of my chosen selection.

If you want a slightly better looking drop down list, you can install the Enterprise version of agGrid using:

npm install --save ag-grid-enterprise

You then just need to include it in app.module.ts:

import 'ag-grid-enterprise';

Then you just need to change the cellEditor to use "agRichSelectCellEditor" :

{ headerName:"Job role", field:"jobRol_ID", width:180, 
    cellEditor: 'agRichSelectCellEditor',

And with that in place, you'll have a nicer looking drop down list:

Image 12

Don't forget to remove that "Job role ID" column when you're happy that it's all working though.

Drop Down Lists - Plan B

I was never particularly happy with this implementation of the drop down lists. Do I really want to repeat the following lines of code each time I have a reference data item in my row? And, as you can see, all that really changes is the field I'm binding to, jobRol_ID, in this case, and the name of the array of data containing my reference data records, jobRoles.

{ 
    headerName:"Job role", field:"jobRol_ID", width:180, 
    cellEditor: 'agRichSelectCellEditor', 
    cellEditorParams: {
      cellHeight:30,
      values: jobRoles.map(s => s.name)
    },
    valueGetter: (params) => jobRoles.find
    (refData => refData.id == params.data.jobRol_ID)?.name,
    valueSetter: (params) => {
      params.data.jobRol_ID = jobRoles.find(refData => refData.name == params.newValue)?.id
    }
},

The ideal solution would have been to take the agRichSelectCellEditor code and modify it, to simply take an array name, and leave it to handle everything.... but nope, they won't let us do that.

//  This would've been the ideal solution... but, we can't do this...
{ 
    headerName:"Job role", field:"jobRol_ID", width:180, 
    cellEditor: 'agRichSelectCellEditor', referenceDataArray: "jobRoles" 
}

Also, from a UI point of view, the Enterprise drop down list agRichSelectCellEditor is a little strange.

Image 13

  1. It always shows the selected item at the top of the popup list, even though we can see it's always directly below the cell we've just clicked on, which already shows that value.
    And it's in the same font/style as all of the other items... it's easy to mistake it for one of the options which you can click on. It just looks odd. In the example above, do we really want to see "Team Leader" twice in the popup?
  2. When it first appears, the popup shows the "currently selected item" with a light-blue background... but as soon as you hover over a different item, the light-blue disappears, and your "hovered" item is now light-blue. Hang on... does "light-blue" mean it's my selected option, or my "hovered" option ?

In my drop down list control, I'll fix these problems:

  • I won't show the selected item at the top of the popup.
  • I will highlight the current selection in light-blue, and it will stay selected in that colour. I'll use a different colour to show which item you're hovering over. Trust me, when you use it, it feels more natural.

Below is a (combined) image showing my custom drop down list, next to the agRichSelectCellEditor list.

Image 14

 

To add a custom drop down list

In the Terminal window in Visual Studio Code, use the following command to define a new component, and register it with our NgModule:

ng g component cellRenderers\DropDownListRenderer --module=app.module.ts --skipTests=true 

This creates a new folder in our cellRenderers folder called "DropDownListRenderer", containing a TypeScript file, HTML and CSS file. It also registers the component in our NgModule - but - you will still need to go into app.module.ts to add DropDownListRendererComponent to the end of this line:

AgGridModule.withComponents([DateTimeRenderer, DatePickerRenderer,
   CheckboxRenderer, DropDownListRendererComponent])

The HTML for our drop down list is really simple. In the cellRenderers/DropDownListRenderer folder, you need to replace the contents of the .html file with this:

<div class="dropDownList">
    <div class="dropDownListItem" *ngFor="let item of items" (click)="selectItem(item.id)"

        [ngClass]="{'dropDownListSelectedItem': item.id == selectedItemID}" >
        {{ item.name }}
    </div>
</div>

We need a small bit of CSS in the drop-down-list-renderer.component.scss file:

.dropDownList {
    max-height:300px;
    min-width:220px;
    min-height:200px;
    overflow-y: auto;
}
.dropDownListItem {
    padding: 8px 10px;
}
.dropDownListItem:hover {
    cursor:pointer;
    background-color: rgba(33, 150, 243, 0.9);
}
.dropDownListSelectedItem {
    /* Whichever is our selected item, highlight it, and KEEP IT highlighted ! */
    background-color: rgba(33, 150, 243, 0.3) !important;
}

And the drop-down-list-renderer.component.ts file is quite straightforward. Notice how the agInit is checking if we've passed an array of reference data records to it, in the cellEditorParams attribute. Our drop down list will display the name values in this array's records, and we'll bind to the id values.

import { Component, OnInit } from '@angular/core';
import { ICellRendererParams } from 'ag-grid-community';
import { ICellRendererAngularComp } from 'ag-grid-angular';

@Component({
  selector: 'app-drop-down-list-renderer',
  templateUrl: './drop-down-list-renderer.component.html',
  styleUrls: ['./drop-down-list-renderer.component.scss']
})
export class DropDownListRendererComponent implements ICellRendererAngularComp {

  params: ICellRendererParams;
  items: any;
  selectedItemID: any;

  agInit(params: ICellRendererParams): void {
      this.params = params;
      this.selectedItemID = this.params.data[this.params.colDef.field];
      
      if (typeof params.colDef.cellEditorParams != 'undefined') {
          this.items = params.colDef.cellEditorParams;
      }
  }

  public selectItem(id) {
    //  When the user selects an item in our drop down list, 
    //  we'll store their selection, and ask
    //  agGrid to stop editing (so our drop down list disappears)
    this.selectedItemID = id;
    this.params.api.stopEditing();
  }

  getValue() {
    //  This gets called by agGrid when it closes the DatePicker control.
    //  agGrid uses it to get the final selected value.
    return this.selectedItemID;
  } 

  isPopup() {
    //  We MUST tell agGrid that this is a popup control, to make it display properly.
    return true;
  }
}

To use this renderer, we need to go into our app.component.ts file, and include it:

import { DropDownListRendererComponent } 
from './cellRenderers/drop-down-list-renderer/drop-down-list-renderer.component';

... and add it to our list of frameworkComponents:

frameworkComponents = {
  dateTimeRenderer: DateTimeRenderer,
  datePickerRenderer: DatePickerRenderer,
  checkboxRenderer: CheckboxRenderer,
  dropDownListRendererComponent: DropDownListRendererComponent
}

We can now use this in our column definitions:

{
   headerName:"Job role (custom)", field:"jobRol_ID", width:180,
   valueGetter: (params) => jobRoles.find
   (refData => refData.id == params.data.jobRol_ID)?.name,
   cellEditor: 'dropDownListRendererComponent', cellEditorParams: jobRoles
},

It would've been really nice to have gotten rid of that valueGetter line, but annoyingly, in agGrid, you cannot define one component which looks after displaying a cell's value in both "view" and "edit" mode. Instead, you have to define separate cellRenderer and cellEditor components.

Of course, we could have defined a cellRenderer to do this for us and pass it a cellRendererParams containing our jobRoles array.

But, this will do for now. We have a nicer looking drop down list, with much better usability.

Setting the Row Height

Okay, agGrid was never supposed to be a replacement for Excel, but it does specifically allow you to cope with huge numbers of rows, and often you'll want the row height to be less than the default of 40 pixels, so you can see more on the screen.

The good news is that agGrid gives us a simple rowHeight property.

<ag-grid-angular [rowHeight]=20

The bad news is that you're not actually expected to have row heights smaller than 40 pixels. This is what my grid looks like with a row height of 20:

Image 15

Of course, yes, you could (and probably will) now go off and write a load of CSS to make it look correct...

.ag-cell-not-inline-editing {
    line-height: 18px !important;
}

...which is a big improvement....

Image 16

But even then, don't try to edit the cells, as agGrid is still using 40-pixel high controls to edit your data. Notice how the textbox (shown above) overlaps into two rows. So, you'll need to do more CSS overriding on these controls as well. Seriously, I don't understand why they've provided a rowHeight setting, if half of their library ignores it.

So, how does agGrid's documentation these problems? Simple. Their examples all have rowHeights above 40 pixels, and they turn off editing on most of their cells.

Problem solved. (Depressed sigh.)

My agGrid Christmas List

My time with agGrid has been a real struggle. If the agGrid authors are reading this, I have a few requests:

  • Create some proper, real-world examples on your website. For example, all of us developers will need to display & edit dates in our grid... this would've been a perfect example of how to use CellRenderer and CellEditor, and demonstrate why CellRendererParams and CellEditorParams can be useful to make the controls more generic.
  • On your website, if you have a webpage describing, say, how to use column groups, add a Disqus section at the bottom of the page, so developers can ask questions, make comments and give each other suggestions about it. Yes, I know that there are GitHub pages, but it makes more sense to have comments specific to a particular agGrid subject on that particular page.
  • Give us the ability to create a single control for both viewing and editing a cell's data. Adding a checkbox to a grid is an obvious example, as it uses HTML and logic which doesn't change when you're viewing or editing the value.
  • The "rowHeight" functionality... either make it work throughout the grid (particularly when we're editing that cell's value), or get rid of it.

Summing Up

I really didn't want to write this article. It has taken me a huge amount of time just to get this far with agGrid, and I didn't want to spend even more hours documenting it.

But it really seems like no one else has written a sensible getting started guide for agGrid with Angular. This is a really long article, yet all we've done is introduce basic viewing and editing of a JSON record.

As you've seen, my web service does have POST/PUT endpoints, if you want to take this further, and try automatically saving changes back to the database.

Please do leave a comment if you've found this article useful.

History

  • 1st May, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Michael Gledhill
Software Developer
Switzerland Switzerland
I'm a C# developer, working in finance in Zurich, Switzerland.

Author of the PartnerReSearch iPad app, which was a winner of a "Business Insurance Innovation Award" in 2013, and a TechAward2014 "Innovation of the year" award in 2014.

Objective-C is the 2nd hardest language I've ever learned, after German... Wink | ;-)

Comments and Discussions

 
QuestionI have a question please Pin
Member 1486330114-Jun-20 22:30
MemberMember 1486330114-Jun-20 22:30 
QuestionDisagree Pin
Member 81965343-May-20 9:40
MemberMember 81965343-May-20 9:40 
AnswerRe: Disagree Pin
Michael Gledhill3-May-20 20:15
MemberMichael Gledhill3-May-20 20:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 1 May 2020

Tagged as

Stats

4K views
74 downloads
2 bookmarked