Building a CRUD Application with ag-Grid — Part 4

Sean Landsman
AG Grid
Published in
19 min readDec 5, 2017

--

In Part 4 of this Series hook up the front end all the way down to the database, including support for all CRUD operations (create, read, update and deletion).

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, Reads, Updates and Deletion)

Introduction

While writing this part of the blog I uncovered two bugs in work done so far.

If you’ve been following this series so far please take a quick look at both parts 1 and 2 to see the changes made (look for Updated!).

The lesson here? Write tests before you write code, and write them often!

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

In this part we’ll be building on the simple grid we have so far and get our simple CRUD operations hooked up to the backend.

Simple Inline Editing

Let’s start off with the simplest of the CRUD operations — Updates. Specifically, we’ll start off with inline editing of “simple” values — values that contain a single value (i.e. aren’t complex objects).

Of the values we currently display, only name and country fall into this category, so let's start off with these.

In the case of the country column we only want to allow selection of a value from a predefined list, so we'll need to retrieve all possible values up front so that we can offer the user a choice at the right time.

Later, we’ll need to do the same for sport values, so let's create a new service that will retrieve static data (data that won't be changed by the user) for us.

First we need to create a new Spring Controller in the middle tier that we can use to access the static data. I won't go into this service too much as we've already covered using Controllers in a previous part of this series:

@CrossOrigin(origins = "http://localhost:4200")
@RestController
public class StaticDataController {
private CountryRepository countryRepository;
private SportRepository sportRepository;
public StaticDataController(CountryRepository countryRepository,
SportRepository sportRepository) {
this.countryRepository = countryRepository;
this.sportRepository = sportRepository;
}
@GetMapping("/countries")
public Iterable<Country> getCountries() {
return countryRepository.findAll();
}
@GetMapping("/sports")
public Iterable<Sport> getSports() {
return sportRepository.findAll();
}
}

Here we’re simply retrieving all possible values for country and sport respectively.

Next, let’s create an Angular Service that will call this rest end point:

@Injectable()
export class StaticDataService {
private apiRootUrl = 'http://localhost:8090';
private countriesUrl = this.apiRootUrl + '/countries';
private sportsUrl = this.apiRootUrl + '/sports';
static alphabeticalSort() {
return (a: StaticData, b: StaticData) => a.name.localeCompare(b.name);
}
constructor(private http: Http) {
}
countries(): Observable<Country[]> {
return this.http.get(this.countriesUrl)
.map((response: Response) => response.json())
.catch(this.defaultErrorHandler());
}
sports(): Observable<Sport[]> {
return this.http.get(this.sportsUrl)
.map((response: Response) => response.json())
.catch(this.defaultErrorHandler());
}
private defaultErrorHandler() {
return (error: any) => Observable.throw(error.json().error || 'Server error');
}
}

Again we won’t go into this as we’ve already covered accessing data from the REST endpoint in an earlier part. We have added a static method here (alphabeticalSort) that we'll use later, but this is just a utility method used for display purposes.

Now that we have this in place we can start making changes to your Grid — first, we need to make these two columns editable. As we want users to select from a predefined list in the case of country so we'll make use of the ag-Grid supplied richSelect editor here (you could also use the select editor if you're using the free version of ag-Grid).

richSelect requires it's values up front, so let's access all countries in the contructor - once we have them we can define our column definitions with these values provided:

// inject the athleteService & staticDataService
constructor(private athleteService: AthleteService,
staticDataService: StaticDataService) {
staticDataService.countries().subscribe(
countries => this.columnDefs = this.createColumnDefs(countries),
error => console.log(error)
);
}

With our new StaticDataService injected, we subscribe to the countries Observable, and once complete call this.createColumnDefs(countries) with the values we've just retrieved.

// create some simple column definitions
private createColumnDefs(countries: Country[]) {
return [
{
field: 'name',
editable: true
},
{
field: 'country',
cellRenderer: (params) => params.data.country.name,
editable: true,
cellEditor: 'richSelect',
cellEditorParams: {
values: countries,
cellRenderer: (params) => params.value.name
}
},
{
field: 'results',
valueGetter: (params) => params.data.results.length
}
]
}

Let’s break this method down:

editable: true

This makes the column editable.

cellRenderer: (params) => params.data.country.name,

As the country data we'll get back from the StaticDataService is a complex value (it has both id and a nameproperties) we need to let ag-Grid know how to display this.

There are a variety of options here, but a simple solution is to provide a cellRenderer as we've done here.

The cellRenderer will be provided with the current row data (along with other useful information - see the docs for further information) so we can access the row data (params.data), then the country column (params.data.country) and finally the country value: params.data.country.name.

cellEditor: 'richSelect'

We’re going to make use of the ag-Grid richSelect select editor - this allows for customisation of values in the dropdown. We're not doing this in this first pass, but will in a later section.

cellEditorParams: {
values: countries,
cellRenderer: (params) => params.value.name
}

At a minimum the richSelect requires the values to display, which is what values does here.

As above though, the list of country are complex objects, so we need to let ag-Grid know what value to actually display.

This time the params contains only the values in the richSelect, but the idea is the same - access the countryname value to be displayed.

With this in place we can now edit both the name and country columns:

Persisting Our Edits

So far we’re not actually doing anything with our edits — let’s hook into the changes and save them down to the Database.

<ag-grid-angular style="width: 100%; height: 500px;"
class="ag-fresh"
[columnDefs]="columnDefs"
[rowData]="rowData"
suppressHorizontalScroll (gridReady)="onGridReady($event)"
(cellValueChanged)="onCellValueChanged($event)">
</ag-grid-angular>

Here we hook into the cellValueChanged - we'll use this hook to save changed values.

onCellValueChanged(params: any) {
this.athleteService.save(params.data)
.subscribe(
savedAthlete => {
console.log('Athlete Saved');
this.setAthleteRowData();
},
error => console.log(error)
)
}

Once a cell value has been changed, this method will be called. We call the AthleteService to save the row data (params.data contains the row data), and on successful save update the row data once again.

What’s great about this is that the save method requires an Athlete and as our row data consists of an array of Athlete so this mapping will happy for free.

On both success and error we output a message in the console — of course in a real world application we’d provide a proper message of some sort to the users, but for this pass of the work a console message will be fine.

You can test your changes by doing a force refresh — you should find the new values are in the Grid — the old values have been overwritten.

Note: As we’re using an in memory database, changes will not be permanently persisted. If you stop and restart the Java application the original values will be displayed once more.

We can’t edit the Results column yet - this is simply a sum of underlying data. We'll get to this later.

Note we’re refreshing the entire grid data each time a cell value changes — this is very inefficient. We’ll look at making use of ag-Grid Update functionality so that we only redraw changed rows, not the entire grid.

Record Deletion

Next let’s take a look record deletion — this is probably the easiest of the CRUD operations to implement.

We’ll allow the users to select one or more records and then provide a button that when clicked will delete the selected records.

First, let’s enable rowSelection within the Grid:

<ag-grid-angular style="width: 100%; height: 500px;"
class="ag-fresh"
[columnDefs]="columnDefs"
[rowData]="rowData"
rowSelection="multiple" suppressRowClickSelection
suppressHorizontalScroll
(gridReady)="onGridReady($event)"
(cellValueChanged)="onCellValueChanged($event)">
</ag-grid-angular>

Here rowSelection="multiple" allows for one or more rows to be selected, and suppressRowClickSelectionprevents rows from being selected by clicking on a row.

What? Why would we want to prevent selection on row clicks? How will we select a row?

Well, how about we add a checkbox to each row, making this our row selection mechanism?

The benefits of this approach is that rows won’t be selected when a row is being edited. Separating the selection into a deliberate action (a user needs to click in the checkbox) makes the operation clearer (and safer!).

Adding a checkbox to a column is easy — all we need to do is add checkboxSelection: true to our column definition:

{
field: 'name',
editable: true,
checkboxSelection: true
}

Next, let’s add a button that a user can use to delete the selected rows — we’ll also add a utility method that will disable the delete button if no rows are selected:

<div>
<button (click)="deleteSelectedRows()" [disabled]="!rowsSelected()">
Delete Selected Row
</button>
</div>
<div>
<ag-grid-angular style="width: 100%; height: 500px;"
class="ag-fresh"
[columnDefs]="columnDefs"
[rowData]="rowData"
rowSelection="multiple" suppressRowClickSelection
suppressHorizontalScroll
(gridReady)="onGridReady($event)"
(cellValueChanged)="onCellValueChanged($event)">
</ag-grid-angular>
</div>

And the corresponding method implementations in our Grid component:

rowsSelected() {
return this.api && this.api.getSelectedRows().length > 0;
}
deleteSelectedRows() {
const selectRows = this.api.getSelectedRows();
// create an Observable for each row to delete
const deleteSubscriptions = selectRows.map((rowToDelete) => {
return this.athleteService.delete(rowToDelete);
});
// then subscribe to these and once all done, refresh the grid data
Observable.forkJoin(...deleteSubscriptions)
.subscribe(results => this.setAthleteRowData())
}

rowsSelected() will return true if the api is ready and if there are rows selected.

deleteSelectedRows() - we grab all the selected rows, then delete each row in turn. Finally, we call this.setAthleteRowData() to refresh the grid with the current data from the database.

This is a fairly naive and inefficient implementation — there are two obvious improvements to be made:

  • Batch the deletes — let the middle/backend do the work
  • Make use of ag-Grid Update functionality so that we only redraw changed/deleted rows, not the entire grid

We’ll cover these improvements in a later iteration.

Record Creation/Insertion

Ok, let’s look at something a bit more challenging — inserting new Athlete data. Adding new data to the Grid itself is easy, but in order to add a full Athlete, including Result information, requires a bit more work on the Angular side than we've used so far.

First, let’s create a new component that we’ll use for both new record creation, as well as record updates later on.

We’ll make use of the Angular CLI to do this for us:

ng g c athlete-edit-screen

g is shorthand for generate and c is shorthand for component.

Let’s replace the contents of the template () as follows:

<div>
<div style="display: inline-block">
<div style="float: left">
Name: <input [(ngModel)]="name"/>
</div>
<div style="float: left; padding-left: 10px">
Country:
<select [(ngModel)]="country">
<option disabled selected>Country...</option>
<option *ngFor="let country of countries" [ngValue]="country">
{{ country.name }}
</option>
</select>
</div>
</div>
<div>
<button (click)="insertNewResult()">Insert New Result</button>
<ag-grid-angular style="width: 100%; height: 200px;"
class="ag-fresh"
[columnDefs]="columnDefs"
[rowData]="rowData"
(gridReady)="onGridReady($event)"
(rowValueChanged)="onRowValueChanged($event)">
</ag-grid-angular>
</div>
<div>
<button (click)="saveAthlete()" [disabled]="!isValidAthlete()" style="float: right">
Save Athlete
</button>
</div>
</div>

It might look like there’s a lot going on here, but what we end up with will be something that looks like this:

In a nutshell this screen will show us the full details of an Athlete. In the top row we have the name and countryfields, and below this we'll list the various result's, if any.

The Grid that displays the results could have been extracted into it’s own component but in the interests of expediency I’ve opted to keep it a bit simpler for this demo.

The corresponding component class breaks down as follows:

constructor(staticDataService: StaticDataService) {
staticDataService.countries().subscribe(
countries => this.countries = countries.sort(StaticDataService.alphabeticalSort()),
error => console.log(error)
);
staticDataService.sports().subscribe(
sports => {
// store reference to sports, after sorting alphabetically
this.sports = sports.sort(StaticDataService.alphabeticalSort());
// create the column defs
this.columnDefs = this.createColumnDefs(this.sports)
},
error => console.log(error)
);
}

Here we retrieve the static sport and country data. The country data will be used in the corresponding dropdown on the edit screen, while the sport data is passed to the column definition code (in the same way we did in the grid component above), to be made available in a richSelect column.

insertNewResult() {
// insert a blank new row, providing the first sport as a default in the sport column
const updates = this.api.updateRowData(
{
add: [{
sport: this.sports[0]
}]
}
);
this.api.startEditingCell({
rowIndex: updates.add[0].rowIndex,
colKey: 'age'
});
}

This is what gets executed when a use want to select a new result. We create a new empty record (defaulting the sport richSelect to the first available sport) and ask the grid to create a new row for us using this.api.updateRowData.

The same mechanism can be used for updates as well as deletions — see the corresponding Update documentation for more information around this powerful piece of functionality that the Grid offers.

Once the new row has been inserted the Grid provides the new row information to us. Using this we can automatically start editing the new row (using this.api.startEditingCell) - in this case we start editing the first available cell: age.

@Output() onAthleteSaved = new EventEmitter<Athlete>();saveAthlete() {
const athlete = new Athlete();
athlete.name = this.name;
athlete.country = this.country;
athlete.results = [];
this.api.forEachNode((node) => {
const {data} = node;
athlete.results.push(<Result> {
id: data.id,
age: data.age,
year: data.year,
date: data.date,
bronze: data.bronze,
silver: data.silver,
gold: data.gold,
sport: data.sport
});
});
this.onAthleteSaved.emit(athlete);
}

This is probably the most important part of this class: when a user click on Save Athlete the saveAthlete method is invoked. We create an Athlete object and populate it with our forms details. We then let the parent component (GridComponent in this case) know that the save is complete, passing in the new Athlete.

Note: There are of course a few things missing in our implementation here — we’re performing only minimal validation, and the form could do with a cancel button too. As this is for illustrative purposes only, this will do, but in a real application you’d probably want to flesh this out further.

The full AthleteEditScreenComponent component isn't described here, primarily as the rest of it is either standard Angular functionality (i.e. simple property binding), or has been covered earlier in this part of the blog above.

To finish this section off, let’s take a look at how the parent GridComponent component process the save operation.

We’ve updated our template to include the new GridComponent, only showing it if we set the editInProgress flag:

<div>
<button (click)="insertNewRow()" [disabled]="editInProgress">Insert New Row</button>
<button (click)="deleteSelectedRows()" [disabled]="!rowsSelected()">Delete Selected Row</button>
</div>
<div>
<ag-grid-angular style="width: 100%; height: 500px;"
class="ag-fresh"
[columnDefs]="columnDefs"
[rowData]="rowData"
rowSelection="multiple" suppressRowClickSelection
suppressHorizontalScroll
(gridReady)="onGridReady($event)"
(cellValueChanged)="onCellValueChanged($event)">
</ag-grid-angular>
</div>
<ng-template [ngIf]="editInProgress">
<app-athlete-edit-screen (onAthleteSaved)="onAthleteSaved($event)"></app-athlete-edit-screen>
</ng-template>

Note that the GridComponent is listening for save completion here:

(onAthleteSaved)="onAthleteSaved($event)"

And in our GridComponent we process the save operation from AthleteEditScreenComponent as follows:

onAthleteSaved(savedAthlete: Athlete) {
this.athleteService.save(savedAthlete)
.subscribe(
success => {
console.log('Athlete saved');
this.setAthleteRowData();
},
error => console.log(error)
);
this.editInProgress = false;
}

Here we pass the new Athlete to our AthleteService to be persisted, and one saved reload the grid data.

As above, this refreshing of the entire grid data is very inefficient — again, we’ll look at improving this in a later iteration of this.

Finally, we hide the edit screen once more.

As above, in a real application some sort of validation would occur here, to ensure that valid data has been returned — again, we’re skipping this for legibility.

Record Update/Edit

Our last piece of the CRUD operation to completed is the Update part — here we’ll take an existing Athlete record and allow a user to edit it.

We’ll be making use of the AthleteEditScreenComponent we created above - this time however we'll pass in an existing record to edit. The rest of the functionality should remain largely unaltered.

First, we’ll update our GridComponent - we'll remove the cell by cell edit functionality we added earlier, and replace it with an entire record (row) based approach.

<div>
<button (click)="insertNewRow()" [disabled]="editInProgress">Insert New Row</button>
<button (click)="deleteSelectedRows()"
[disabled]="!rowsSelected() || editInProgress">
Delete Selected Row
</button>
</div>
<div>
<ag-grid-angular style="width: 100%; height: 500px;"
class="ag-fresh"
[columnDefs]="columnDefs"
[rowData]="rowData"
rowSelection="multiple" suppressRowClickSelection
suppressHorizontalScroll
suppressClickEdit
(gridReady)="onGridReady($event)"
(rowDoubleClicked)="onRowDoubleClicked($event)">
</ag-grid-angular>
</div>
<ng-template [ngIf]="editInProgress">
<app-athlete-edit-screen [athlete]="athleteBeingEdited"
(onAthleteSaved)="onAthleteSaved($event)">
</app-athlete-edit-screen>
</ng-template>

There are a number of changes above — let’s walk through them:

suppressClickEdit

This property prevents a cell from going into edit mode when double clicked on, which is the default behaviour for an editable cell. In our application however we want to make use of a separate component to do this for us.

(rowDoubleClicked)="onRowDoubleClicked($event)

We’ve removed the cell edit handler we had before and replaced it with a row based one. We’ll use this event to launch our AthleteEditScreenComponent.

<app-athlete-edit-screen [athlete]="athleteBeingEdited"

When we display our AthleteEditScreenComponent we'll also pass in an Athlete now.

When creating new a new Athlete this refence will be null, but when we edit an existing row we'll pass in the row (or Athlete, as each row of data is an Athlete) to be edited with our AthleteEditScreenComponent.

private athleteBeingEdited: Athlete = null;
onRowDoubleClicked(params: any) {
if (this.editInProgress) {
return;
}
this.athleteBeingEdited = <Athlete>params.data;
this.editInProgress = true;
}

This handler will be invoked when a user double clicks on a row. If an edit is already in progress we’ll ignore the request, but if not we’ll store the row double clicked on — this will be passed to the AthleteEditScreenComponent via property binding.

Finally, we set the editInProgress property which will cause the AthleteEditScreenComponent to be displayed.

onAthleteSaved(savedAthlete: Athlete) {
this.athleteService.save(savedAthlete)
.subscribe(
success => {
console.log('Athlete saved');
this.setAthleteRowData();
},
error => console.log(error)
);
this.athleteBeingEdited = null;
this.editInProgress = false;
}

Here we’ve made one small change to onAthleteSaved - we reset athleteBeingEdited to null once a save operation is complete, so that it's ready for the next insert or edit operation.

Switching our attention to AthleteEditScreenComponent next, we have a few small changes to make to take account of an existing Athlete being passed in to be edited:

@Input() athlete: Athlete = null;

Here we let Angular know that we’re expecting an Athlete to be passed in. Something will always be passed in here - either a valid Athlete in the case of an edit operation, or null in the case of an insert operation.

ngOnInit() {
if (this.athlete) {
this.name = this.athlete.name;
this.country = this.athlete.country;
this.rowData = this.athlete.results.slice(0);
}
}

When our component is initialised we check if an Athlete has been supplied - if it has, then we populate our components fields with the details passed in, ready for the user to edit.

saveAthlete() {
const athlete = new Athlete();
athlete.id = this.athlete ? this.athlete.id : null;

And finally, we check again if we’re editing an Athlete when the use clicks save. If we are, we also set the AthleteID so that when it reaches our REST service we'll do an update on the existing record, as opposed to inserting a new record.

And that’s it! If we now double click on a row we’ll be presented with a pre-populated AthleteEditScreenComponent, ready for us to edit.

With a little styling applied, we’ll end up with this:

Optimistic Locking

We’ve adopted an optimistic locking stragety here — we assume we can edit/delete something and that no one else has modified what we’re attempting to edit before us.

This is a fairly mechanism to use, but it has it’s downsides. It we attempt to edit/delete something that another user has edited before us, we’ll get an error. This again is normal and in a real application you would code for this — either let the user know that this has occurred, or do a check before the user attempts it (or both).

This optimistic locking is by an large handled for us by Spring JPA by the use of versioning. Each time a record is changes the version will be bumped up and the result stored in the DB. When a change is attemped Spring JPA will check the current version against the version in the DB — if they’re the the same the edit can continue, but if not an error will be raised.

This versioning is done by adding the following to the Entity's we want to version - in our case we only need to do this for Athlete and Result (Country and Sport cannot be edited).

@Entity
public class Athlete {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version()
private Long version = 0L;

Here the @Version annotation let's Spring JPA know that we want to version this class - the rest just happens automagically!

Section Break!

This will serve as a good place to do a quick check — we’ve done an awful lot of coding here, with lots to digest.

The code to this point can be found in the Part-4a branch in Github, but you can also see the main classes etc below.

Home Stretch — Final Improvements

We’re pretty much there now — we’ll make two more sets of improvements to what we have so far.

The first is around look & feel/usability, and the second will be around reducing unnecessary Grid redraws (and network calls).

Usability — Overlay Edit Component

At the moment the AthleteEditScreenComponent appears below the application (specifically below the grid). This works just fine, but visually it would look & feel better if it would appear over the Grid - make it feel as if the AthleteEditScreenComponent was part of the Grid itself.

In order to do this we need capture the Grid co-ordinates and supply them to AthleteEditScreenComponent when it's about to be displayed, so that AthleteEditScreenComponent can position itself accordingly.

First, let’s capture the Grid co-ordinates:

<ag-grid-angular style="width: 100%; height: 500px;"
class="ag-fresh"
#grid [columnDefs]="columnDefs"
[rowData]="rowData"

Here we grab a reference to the grid with #grid - we'll use it to grab the Grids dimensions and co-ordinates:

@ViewChild('grid', {read: ElementRef}) public grid;
private containerCoords: {} = null;
private updateContainerCoords() {
this.containerCoords = {
top: this.grid.nativeElement.offsetTop,
left: this.grid.nativeElement.offsetLeft,
height: this.grid.nativeElement.offsetHeight,
width: this.grid.nativeElement.offsetWidth
};
}

We store the Grid dimensions and co-ordinates here — we’ll update this information just before the AthleteEditScreenComponent is displayed (just before a new row is inserted, or when an existing row is double clicked/edited).

We pass this information to the AthleteEditScreenComponent:

<ng-template [ngIf]="editInProgress">
<app-athlete-edit-screen [athlete]="athleteBeingEdited"
[containerCoords]="containerCoords"
(onAthleteSaved)="onAthleteSaved($event)">
</app-athlete-edit-screen>
</ng-template>

Let’s now switch to the AthleteEditScreenComponent and see how we use this.

In our AthleteEditScreenComponent template we'll store a reference to the main div, and bind to the top & left co-ordinates:

<div class="input-panel" [style.width]="width" [style.top]="top" [style.left]="left" #panel>
<div style="display: inline-block">

Then in the AthleteEditScreenComponent component itself:

// to position this component relative to the containing component
@Input() containerCoords: any = null;
@ViewChild('panel', {read: ElementRef}) public panel;
private width: any;
private left: any;
private top: any;
ngOnInit() {
this.setPanelCoordinates();
... rest of the method
}
private setPanelCoordinates() {
// make our width 100pixels smaller than the container
this.width = (this.containerCoords.width - 100);
// set our left position to be the container left position plus half the difference in widths between this
// component and the container, minus the 15px padding
this.left = Math.floor(this.containerCoords.left + (this.containerCoords.width - this.width) / 2 - 15) + 'px';
// set our left position to be the container top position plus half the difference in height between this
// component and the container
this.top = Math.floor(this.containerCoords.top + (this.containerCoords.height - this.panel.nativeElement.offsetHeight) / 2) + 'px';
// add the px suffix back in (omitted above so that maths can work)
this.width = this.width + 'px'
}

With this in place our AthleteEditScreenComponent will position itself within the Grid, which makes for a better visual experience:

Performance Improvements

As we saw earlier, we retrieve the entire Grid data and redraw the entire Grid each time we make a change (on either create, update or delete). This is needlessly inefficient, and the Grid can help us do less network & Grid draws via use of it’s Update functionality.

First, we need to define a functional that will allow the Grid to uniquely identify each row in order to find data within it.

We do this by defining a getRowNodeId function and binding to it:

<ag-grid-angular style="width: 100%; height: 500px;"
class="ag-fresh"
...reset of grid definition [getRowNodeId]="getRowNodeId"

And in our Grid component itself:

getRowNodeId(params) {
return params.id;
}

Our data has an obvious attribute to use to uniquely identify each row — the ID attribute.

We can now use the Grids api.updateRowData functionality to only update/redraw changed rows - not the entire grid.

When we delete row(s):

deleteSelectedRows() {
const selectRows = this.api.getSelectedRows();
// create an Observable for each row to delete
const deleteSubscriptions = selectRows.map((rowToDelete) => {
return this.athleteService.delete(rowToDelete);
});
// then subscribe to these and once all done, update the grid
Observable.forkJoin(...deleteSubscriptions)
.subscribe(
results => {
// only redraw removed rows...
this.api.updateRowData(
{
remove: selectRows
}
);
}
);
}

Once all the rows have been successfully deleted we let the grid know which rows to remove. This results is a much faster user experience in that the Grid only redraws removed rows, and that we now no longer make another network call to retrieve the latest Grid data.

When we create or insert rows:

onAthleteSaved(athleteToSave: Athlete) {
this.athleteService.save(athleteToSave)
.subscribe(
savedAthlete => {
console.log('Athlete saved', savedAthlete.name);
const added = [];
const updated = [];
if (athleteToSave.id) {
updated.push(savedAthlete);
} else {
added.push(savedAthlete);
}
this.api.updateRowData(
{
add: added,
update: updated
}
);
},
error => console.log(error)
);
this.athleteBeingEdited = null;
this.editInProgress = false;
}

The onAthleteSaved method is called when we create a new record, or when we edit an existing one.

In order to tell the Grid if rows are being added or updated, we’ll check for the existence of the Athlete ID - if it's present we're doing an update, but if it's not we're performing an add.

With this information we can let the Grid know to either add or update the effected row:

this.api.updateRowData(
{
add: added,
update: updated
}
);

As with the delete improvement above this results in a much faster refresh and a far better user experience.

We’ve only scratched the surface on what we can do with the Grid here — I suggest you have a look at the Update documentation for more information on what’s possible.

Summary

Well this was a huge part of this series. We covered a lot of material and a lof ground, but we end up with a fair example of how you might write your own CRUD application using ag-Grid.

We’ve skipped a lot too though — there’s very little in the way of validation or error handling here, mostly to focus on the important ideas I’m trying to convey, but these are critical and shouldn’t be skipped your side!

I hope that this has been helpful — if you have any question please let me know below.

Learn more about AG Grid — high performance JavaScript Data Grid.

AG Grid supports multiple frameworks: Angular, Vue, React so you can pick the right framework for your needs. Concentrate on writing the best application you can, leave the data tables and grid interactions to AG Grid.

--

--

Sean Landsman
AG Grid

Lead Developer — Frameworks. Responsible for integrating all of ag-Grid’s supported frameworks (Angular, Vue, React etc)