loading...

Angular - NGRX-Data - AG Grid - Pt. 1

lysofdev profile image Esteban Hernández ・5 min read

Introduction

I've been building a Dashboard-style monitoring application for a client for the last few weeks. The application requires CRUD functionality across a series of resources. Although there is a lot of shared functionality across these different resources, each one has a set of business rules when it comes to creating, updating and destroying these resources. When starting out, I had to select a few libraries that would help me avoid having to write all of the common CRUD functionality while allowing me to easily insert the business logic at any point.

After some research, I settled on NGRX-Data for state management and AG Grid for the resource views. You might've heard some criticism around NGRX on how much boilerplate it requires but I want to clarify that NGRX Data is an additional layer of abstraction on top of the basic NGRX Store library which helps the developer avoid the common boilerplate code. In fact, I found myself writting very little code beyond configuration to get the majority of the necessary functionality going.

As for the UI, I chose AG Grid since it comes with tons of functionality out of the box and is very easy to extend. It comes with sensible defaults while also offering tons of extension points. I have yet to find any significant limitation on this library and I definetly recommend it's use for an application requiring anything beyond a trivial data table.

Finally, we'll be leveraging the Angular 2+ web application framework and the RxJs library. Be sure to understand both of these tools to follow along although this post will be more focused on NGRX Data and AG Grid.

Demo Data

I'll be using data from JSON Placeholder which is a free-to-use, mock API. It doesn't belong to me so much gratitude to Typicode for making this awesome tool available.

Installation

Creating an Angular project

Let's get our application setup. First, start a new Angular 2+ project. If you don't already have the @angular/cli installed, run the following:

npm i -g @angular/cli

Be sure to include routing and SCSS in the Angular application prompts.

ng new ngrx-data-ag-grid-demo
cd ngrx-data-ag-grid-demo

Install AG Grid:

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

We need to add some styles for AG Grid to our styles.scss file.

@import "~ag-grid-community/dist/styles/ag-grid.css";
@import "~ag-grid-community/dist/styles/ag-theme-balham.css";

Install NGRX Data

npm i --save @ngrx/data @ngrx/store @ngrx/entity @ngrx/effects

NGRX Data still requires NGRX Store, Effects and Entities. It does however add a lot of the functionality for CRUD actions freeing up developers to focus on the business domain. Create an app-store.module.ts file and add the following:

import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { StoreModule } from "@ngrx/store";
import { EffectsModule } from "@ngrx/effects";
import { EntityDataModule, DefaultDataServiceConfig } from "@ngrx/data";

import { PostCollectionService } from "./posts/post-collection.service";

import * as fromPosts from "./posts";

const NGRX_STORE_CONFIGURATION = {};

const REGISTERED_EFFECTS = [];

const ENTITY_METADATA = {};

const ENTITY_PLURAL_NAMES = {};

const NGRX_DATA_SERVICE_CONFIGURATION = {};

@NgModule({
  imports: [
    CommonModule,
    StoreModule.forRoot(NGRX_STORE_CONFIGURATION),
    EffectsModule.forRoot(REGISTERED_EFFECTS),
    EntityDataModule.forRoot({
      entityMetadata: ENTITY_METADATA,
      pluralNames: ENTITY_PLURAL_NAMES
    })
  ],
  providers: [
    {
      provide: DefaultDataServiceConfig,
      useValue: NGRX_DATA_SERVICE_CONFIGURATION
    },
    PostCollectionService
  ]
})
export class AppStoreModule {}

Configuring the API endpoint

Configure the API address by providing a DefaultDataServiceConfig object. Add the following to app-store.module.ts:

...
const NGRX_DATA_SERVICE_CONFIGURATION = {
  root: "https://jsonplaceholder.typicode.com/"
};
...

Add the Store to the App

Import the AppStoreModule within the app.module.ts:

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";

import { AppRoutingModule } from "./app-routing.module";
import { AppStoreModule } from "./app-store.module";
import { AppComponent } from "./app.component";

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppStoreModule, AppRoutingModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Configuring the first Entity Collection

NGRX Data is focused on entities which are just collections of JS objects. It'll handle the synchronization of a local cache and a remote endpoint by default with pessimistic strategies. It can however be configured to use optimistic strategies, multiple endpoints, etc. All defaults can be overriden.

Define the Entity state and configuration

The first entity will be the Post entity. Start by creating a posts directory and a state.ts file and an index.ts file. Add the following to state.ts:

export const entityCollectionName = "Post";

export const pluralizedEntityName = "posts";

export const entityCollectionEndpoint = pluralizedEntityName;

export interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}

Export the Entity state and configuration

And, index.ts:

export * from "./state";

Configure the Entity in the Store

The app-store.module.ts needs to be updated with the Post entity collection configuration:

...
import * as fromPosts from './posts';
...
const ENTITY_METADATA = {
  [fromPosts.entityCollectionName]: {}
};
...
const ENTITY_PLURAL_NAMES = {
  [fromPosts.entityCollectionName]: fromPosts.pluralizedEntityName
};
...

NGRX Data has a default pluralization feature based on the collection name but we found it to be very unreliable. We decided to always provide the pluralNames from configuration instead. This also makes mapping application routes to API calls more reliable.

Creating the Entity Collection Service

NGRX Data provides the EntityCollectionServiceBase class which provides the high-level implementation for the observable state and actions of the Entity store. Each Entity will have a dedicated service that extends this class.

Create a file within the posts directory named post-collection.service.ts and add the following:

import { Injectable } from "@angular/core";
import { EntityCollectionServiceBase } from "@ngrx/data";
import { EntityCollectionServiceElementsFactory } from "@ngrx/data";

import * as fromPosts from "./";

@Injectable()
export class PostCollectionService extends EntityCollectionServiceBase<
  fromPosts.Post
> {
  constructor(
    readonly elementsFactory: EntityCollectionServiceElementsFactory
  ) {
    super(fromPosts.entityCollectionName, elementsFactory);
  }
}

Display the data with AG Grid

Create a directory within the posts directory named posts-list and add a posts-list.component.ts file. Add the following:

import { Component } from "@angular/core";
import { concat } from "rxjs";
import { startWith } from "rxjs/operators";
import { FirstDataRenderedEvent } from "ag-grid-community";

import { PostCollectionService } from "../post-collection.service";

@Component({
  selector: "app-posts-list",
  template: `
    <h1>Posts</h1>
    <hr />
    <ag-grid-angular
      class="ag-theme-balham grid"
      [columnDefs]="columns"
      [rowData]="rows$ | async"
      [pagination]="true"
      [paginationAutoPageSize]="true"
      (firstDataRendered)="onFirstDataRendered($event)"
    ></ag-grid-angular>
  `,
  styles: [
    `
      :host {
        display: flex;
        flex-direction: column;
        justify-content: center;
        padding-left: 5vw;
      }

      .grid {
        height: 80vh;
        width: 90vw;
      }
    `
  ]
})
export class PostListComponent {
  private columnDefaults = {
    resizable: true,
    sortable: true,
    filter: true
  };

  readonly columns = [
    {
      ...this.columnDefaults,
      headerName: "ID",
      field: "id",
      resizable: false
    },
    {
      ...this.columnDefaults,
      headerName: "Title",
      field: "title"
    },
    {
      ...this.columnDefaults,
      headerName: "Body",
      field: "body"
    }
  ];

  readonly rows$ = concat(
    this.postCollectionService.getAll(),
    this.postCollectionService.entities$
  ).pipe(startWith(null));

  constructor(private postCollectionService: PostCollectionService) {}

  onFirstDataRendered({ columnApi }: FirstDataRenderedEvent): void {
    columnApi.autoSizeAllColumns();
  }
}

Setup lazy loading for Feature modules

This is a great opportunity to setup lazy-loading of each feature module. We'll load the proper presentation components based on the current route.

First, create a posts-routing.module.ts and add the following:

import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { PostListComponent } from "./posts-list/posts-list.component";

const routes: Routes = [
  {
    path: "",
    component: PostListComponent
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class PostsRoutingModule {}

Second, create a posts.module.ts and add the following:

import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { AgGridModule } from "ag-grid-angular";

import { PostsRoutingModule } from "./posts-routing.module";
import { PostListComponent } from "./posts-list/posts-list.component";

const AG_GRID_CUSTOM_COMPONENTS = [];

@NgModule({
  imports: [
    CommonModule,
    AgGridModule.withComponents(AG_GRID_CUSTOM_COMPONENTS),
    PostsRoutingModule
  ],
  declarations: [PostListComponent]
})
export class PostsModule {}

Next, add the router outlet to the app.component.html file:

<router-outlet></router-outlet>

Finally, add the first route to the app-routing.module.ts:

...
import * as fromPosts from './posts';
...
const routes: Routes = [
  {
    path: fromPosts.entityCollectionEndpoint,
    loadChildren: () => import("./posts/posts.module").then(m => m.PostsModule)
  }
];
...

We should now be able to navigate in our browser to http://localhost:4200/posts and see a grid populated with data from JSONPlaceholder. Not bad for how little code we've had to write.

Conclusion

For Part 2, we'll be adding the User entity and interpolating the author's name into each of the Post entries in the AG Grid.

Posted on by:

lysofdev profile

Esteban Hernández

@lysofdev

Software Engineer specializing on performant web applications.

Discussion

markdown guide
 

Thank you for the post!

It would be great to see in part 2 the implementation of a pagination considering this response

{
  data: [{id: 1, ...},{id: 2, ...}],
  page: 2,
  ...,
}

See reqres.in API for more info.

 

Get post thanks. think you left out
import { HttpClientModule } from '@angular/common/http';
and add HttpClientModule to the imports section of app.module.ts

There's not many sample apps around using ngrx-data so thanks for that. Would be interested in seeing post #2 on this topic!

Thanks and cheers

 

Hi Esteban, thanks for this first part. What about the second? 🤣