Building a CRUD Application with ag-Grid - Part 3

Sean Landsman | 21st November 2017

Summary

In Part 3 of this Series we create the scaffolding for our Angular application and get our data displayed in our first, simple, grid.

Series Chapters

  • Part 1: Introduction & Initial Setup: Maven, Spring and JPA/Backend (Database)
  • Part 2: Middle Tier: Exposing our data with a REST Service
  • Part 3: Front End - Initial Implementation
  • Part 4: Front End - Grid Features & CRUD (Creation, Updates and Deletion)

Introduction

The completed code for this blog series can be found here (once the series is complete), with this particular section being under Part 3

In order for our data to be useful we need to make it available to users. So far we've exposed our data via REST service in the previous part, but now let's make it available to our users in the browser.

We'll be running an Angular application. One of the quickest way to spin up an Angular application is to make use of the Angular CLI, which we'll make use of here.

Scaffolding with Angular CLI

First things first, let's install Angular CLI. In our case we're going to install it globally as it's easier to use this way, but you can install it locally (i.e. local to your project) if you prefer.

npm install -g @angular/cli

Next we'll create a new Angular application in the root of the project:

ng new frontend

Angular CLI will create a scaffolded project all ready to go - we can test it as follows:

cd frontend ng serve

You can now navigate to http://localhost:4200/ and see the results of the scaffolding:

Angular CLI

Development

There are a number of ways you might structure your overall application - in this series we're going to keep the front and backends separate both in structure and in execution, at least when in development mode.

Doing so makes front end development easier and allows us to separate the two tiers (front and middle). We'll cover application packaging (into a single deployable artifact) later in the series.

We'll run the server and front end code separately in development mode:

// server mvn spring-boot:run // in a separate terminal/window, serve the front end code ng serve

First Call to the Server

We'll make use of a simple Application architecture in the frontend:

Front End Architecture

As a first pass let's attempt to retrieve all Olympic Data from the server. In order to do that we're going to break our front end application into further packages: one for our model and another for our services.

The model classes are pretty simple and are pretty much mirrors of their Java counterparts:

import {Country} from './country.model'; import {Result} from './result.model'; export class Athlete { id: number; name: string; country: Country; results: Result[]; }

We'll also create a AthleteService that will interact with our REST endpoint:

import {Injectable} from '@angular/core'; import {Athlete} from '../model/athlete.model'; import {Http, Response} from '@angular/http'; import 'rxjs/add/operator/map' import 'rxjs/add/operator/catch'; import {Observable} from 'rxjs/Observable'; @Injectable() export class AthleteService { private apiUrl = 'http://localhost:8080/athletes'; constructor(private http: Http) { } findAll(): Observable<Athlete[]> { return this.http.get(this.apiUrl) .map((res: Response) => res.json()) .catch((error: any) => Observable.throw(error.json().error || 'Server error')); } }

There's a fair bit going on here - we're creating a Service that will make use of Angular's Http service. In order to access it we use Angular Dependency Injection facility, so all we need to do is specify it in our constructor.

In the findAll method we're providing an Observable that will make a call to our REST endpoint, and on retrieval, map it to the model classes we created above. We actually get a lot of functionality from not too many lines of code here, which is great.

So far so good - let's plug this service into our application next:

export class AppComponent implements OnInit { private athletes: Athlete[]; constructor(private athleteService: AthleteService) { } ngOnInit() { this.athleteService.findAll().subscribe( athletes => { this.athletes = athletes }, error => { console.log(error); } ) } }

We'll create a quick template to output our results:

<div *ngFor="let athlete of athletes"> <span>{{athlete.id}}</span> <span>{{athlete.name}}</span> <span>{{athlete?.country?.name}}</span> <span>{{athlete?.results?.length}}</span> </div>

Ok, great - we should be good to go now right? Unfortunately not - if we run both the front and backend as it stands we'll get the following error:

Error

The problem here is that our Angular application is running in localhost:4200, but our backend application is running on localhost:8080. The browser will by default prevent this due to the risk of malicious indirection - you can read more about CORS here, but for now we have an easy solution to this.

Let's head back to our AthleteController.java controller and enable CORS:

@CrossOrigin(origins = "http://localhost:4200") @RestController public class AthleteController { ... rest of the class

With this one line we're good to go. Note that in a real application you'd probably want to only enable CORS for local development (perhaps with Spring profiles). You also want to be able to externalise the ports you run on via the use of properties.

Ok, let's try that again - let's start our applications and checkout the results:

// server mvn spring-boot:run // in a separate terminal/window, serve the front end code ng serve

Once both have started you can navigate to localhost:4200. You should see something like this:

localhost:4200

Great, good progress so far - we now know we can call the backend successfully!

ag-Grid

We're finally in a position to start hooking our data into ag-Grid!

First, let's install the ag-Grid dependencies - as we're going to be using a few of the Enterprise features that ag-Grid offers we'll install both the ag-grid and ag-grid-enterprise dependencies.

If you're not using any of the Enterprise features then you only need to install the ag-grid dependency.

npm install --save ag-grid-community ag-grid-enterprise ag-grid-angular

The ag-grid-angular is what allows us to talk to the grid and provides the rich Angular functionality we want.

We also need to let the Angular CLI know about the styles we want to use. In our demo we're going to use the Fresh Theme, but there are others available - please see the Themes Documentation for more details.

In order to let the CLI know about the styles we want to add we need to add them to the .angular-cli.json file. Look for the styles section and add the following CSS entries:

"styles": [ "styles.css", "../node_modules/ag-grid/dist/styles/ag-grid.css", "../node_modules/ag-grid/dist/styles/ag-theme-balham.css" ],

styles.css is a style file generated by Angular CLI - you can either keep it or remove it. We won't be using it in our demo here.

Next we need to add the AgGridModule to our application. We do this by adding it to our app.module.ts:

... other imports import {AgGridModule} from 'ag-grid-angular'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpModule, AgGridModule ...rest of module

We now have the ag-Grid dependencies all setup - our next step is to actually use ag-Grid to display some data.

Our Grid Component

Let's create a new component that will be responsible for displaying our data in ag-Grid:

ng generate component Grid

This will create a new Angular component for us and automatically register it in our Angular module.

installing component create src/app/grid/grid.component.css create src/app/grid/grid.component.html create src/app/grid/grid.component.spec.ts create src/app/grid/grid.component.ts update src/app/app.module.ts

Let's open up our new Component and inject our AthleteService as before. The AthleteService will be responsible for supplying data to the Grid. Later, it will also be responsible for updates & deletions too.

We also need two properties for our component: one for row data and one for column definitions - at a minimum the grid require what columns you want in the Grid, as well as what data to display.

Finally, we'll hook into the gridReady event from the Grid. We do this for two reasons: firstly, to access the GridApi and ColumnApi and secondly to auto resize the columns on initialisation.

export class GridComponent implements OnInit { // row data and column definitions private rowData: Athlete[]; private columnDefs: ColDef[]; // gridApi and columnApi private api: GridApi; private columnApi: ColumnApi; // inject the athleteService constructor(private athleteService: AthleteService) { this.columnDefs = this.createColumnDefs(); } // on init, subscribe to the athelete data ngOnInit() { this.athleteService.findAll().subscribe( athletes => { this.rowData = athletes }, error => { console.log(error); } ) } // one grid initialisation, grap the APIs and auto resize the columns to fit the available space onGridReady(params): void { this.api = params.api; this.columnApi = params.columnApi; this.api.sizeColumnsToFit(); } // create some simple column definitions private createColumnDefs() { return [ {field: 'id'}, {field: 'name'}, {field: 'country'}, {field: 'results'} ] } }

And our view template looks like this:

<ag-grid-angular style="width: 100%; height: 800px;" class="ag-theme-balham" (gridReady)="onGridReady($event)" [columnDefs]="columnDefs" [rowData]="rowData"> </ag-grid-angular>

Notice that this is where we're binding to the row data and column definitions, as well as hooking into the gridReady event. There are other ways of doing this, but this is clearer and more idiomatic from an Angular perspective.

As we're now using the AthleteService in our Grid Component, we can remove it from app.component.ts.

Finally, we can hook our component into app.component.html:

<app-grid></app-grid>

With this in place we can now run our application...but we don't see quite what we're hoping for:

datagrid

The reason for this is pretty simple - both Country and Results are complex data. They don't have a simple key-value relationship unlike the rest of the data.

We can fix this easily by making use of a Value Getter which will convert the complex raw data into something more display friendly:

private createColumnDefs() { return [ {field: 'id'}, {field: 'name'}, {field: 'country', valueGetter: (params) => params.data.country.name}, {field: 'results', valueGetter: (params) => params.data.results.length} ] }

Here the valueGetter callback will be called for every row for country and results, where we return the country name and results length respectively.

grid second pass

Summary

That might have seemed like a fair bit of work, but it's worth noting that we only had to import a single module and then reference the grid in a single component to get a grid up and running.

With the addition of a few simple properties we can enable filtering, sorting and so on. We can also start working providing the rest of the CRUD operations (creation, updates and deletions).

We'll take a look at all that and more in the next part of the series.

See you next time!

If you liked this article then please share
   
Sean Landsman

Sean Landsman

Lead Developer - Frameworks

Sean was the first person that Niall asked to join the team. Sean ensures that we can keep the agnostic in ag-Grid... he is responsible for integrating with all of our supported frameworks. He has also recently given a number of talks at conferences where his calm manner belies his years of experience.