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.
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.
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:
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() {
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.
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() {
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:
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.
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 import
s:
@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();
else
this.thisDate = new Date(originalDateTime);
}
getValue() {
var result = new Date(this.thisDate).toISOString();
return result;
}
isPopup() {
return true;
}
public onSelectDate(newValue) {
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) {
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.
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:
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.
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 checkbox
es, 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.
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.)
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:
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.
{
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.
- 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? - 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.
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 {
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) {
this.selectedItemID = id;
this.params.api.stopEditing();
}
getValue() {
return this.selectedItemID;
}
isPopup() {
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
:
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....
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