Building a CRUD-enabled Angular Application with Ignite UI For Angular and CDATA API Server

Hristo Anastasov / Tuesday, May 12, 2020

We at Infragistics always strived to deliver to you best of breed UI components to help you build modern UI and meet best UX practices. In addition to that, we provide some tooling to support you all the way from design to implementation and speed up development time.

While we tried to support this with rich documentation and samples, it was always a trouble to replicate a real-world scenario where the variety of databases and backend technologies is enormous.

Here we are going to review how to build an Angular application with Ignite UI for Angular, implementing full CRUD (Create, Read, Update, Delete) operations against an OData RESTful API, created with API Server.

To support this, we will use API Server from CData. API Server is the perfect tooling to expose your data as a professional API. Hook up any SQL or NoSQL database and the API Server instantly generates flexible and fully documented APIs. 

This article aims to:

  • Go through the API Server installation and setup.
  • Build a REST API from the local SQL instance of Northwind database
  • Consume and visualize the data in an Angular application built with Ignite UI for Angular
  • Demonstrate how to add full CRUD capabilities to the igxGrid to consume the API

Setting Up the API Server and connecting it to SQL server

Download the API Server. Once installed, go to API Server/Settings/Connections and add SQL Server connection for the Northwind database. Set the database connection credentials:

If you don’t have the Northwind database, you can create it by running this script).

After setting up the connection, go to Resources and choose Add resource. API Server will open a wizard, allowing you to choose tables from your connections. We choose Products, Orders and Order details tables from the Northwind connection. These are the tables that we are going to use in the demo.

Authentication

Next go to API Server Settings/User, and add new user. Copy the created auththoken and use it to set the value of the `authToken` variable inside `remoteData.service.ts`

Finally, we need to enable CORS (Cross-origin resource sharing).

Go to API Server Settings/Server and click the checkbox to "Enable cross-origin resource sharing (CORS)" and the checkbox to "Allow all domains without '*'" (or define the domains that are allowed to connect in Access-Control-Allow-Origin).

Set Access-Control-Allow-Methods to "GET, PUT, POST, DELETE".

Finally, you can go to API to see the Resources you have. Click any of it and API Server will list the URLs for the corresponding methods, along with description of the request and response body objects and code snippets.

Fine tuning

API Server allows for fine tuning of the API created. For example, you can define a relationship between tables, which tells API Server to build a SQL Join query and deliver the result in a single response (check the Status tab of the API Server to see what is happening behind the API Server). Quite helpful, isn’t it?

Let’s create such a relation between the Order and Order_details tables from Northwind. Click on the Edit icon for the Northwind_dbo_Order resource, and on the primary ID line (OrderID), add the ‘relationships’ attribute:

You can then retrieve data from both endpoints by using the OData $expand keyword:

api.rsc/Nortwind_dbo_Order(10248)?$expand=Details

Follow API Server documentation for more examples on everything you would need to create an enterprise ready API, including in-memory caching, users, rate limits, and etc.

Consume and visualize the data in an Angular application built with Ignite UI for Angular

Data Service

In our Angular application, let’s first create an injectable data service. This data service acts as the middleware between the view and the backend. It uses the HttpClient class in Angular to talk to the server over the HTTP protocol. It reads the data, does some normalizing (if needed) and returns it to the view of our application as an Observable stream.

We create four methods to implement the GET, POST, PUT and DELETE:

/**
 * Retrieves data from specific table.
 * Optionally pass `fields` argument to build the OData $select query
 * Optionally pass `expandRel` argument to build the OData $expand query, which joins a related table to the response.
 * Optionally pass `id` and 'primaryKey' arguments to build $filter query
 */
public getData(tableKey: string,
    fields?: string[],
    id?: number,
    primaryKey?: string,
    expandRel?: string): Observable<IProduct[] | IOrderDetails[]> {

    return this._http.get(this.buildDataUrl(tableFullName, fields, id, primaryKey, expandRel), HTTP_OPTIONS)
  );
}

public editData(tableKey: string, body: IProduct): any {
    return this._http.put(`${BASE_URL}/${tableFullName}(${id})`, body, HTTP_OPTIONS)
  );
}

public addData(tableKey: string, body: IProduct): any {
    return this._http.post(`${BASE_URL}/${tableFullName}`, body, HTTP_OPTIONS)
   );
}

public deleteData(tableKey: string, id: string): any {
    return this._http.delete(`${BASE_URL}/${tableFullName}/${id}`, HTTP_OPTIONS)
  );
}

A good practice is to implement the methods in the data service as generic as possible. The above methods take the SQL database table name as input and the record to be added/edited, or the id of the item to be deleted.

For the GET method, one can optionally pass fields and other arguments to create complex OData queries (select the fields to retrieve, filter, sort, etc.). See the builtDataUrl method for implementation details. If not passed, the getData method from above will return all data from the corresponding table.

To visualize the data returned, our main component calls the getData method. Initially it fetches data from the Products table to populate the products grid on the page:

When a row in the products grid is selected by the user, a request to retrieve data from the Orders and Order_details table is made:

this._ordersRequest$ = this._remoteService.getData(Tables.ORDERS, fields, pid, this.pkey, 'Details').subscribe(data => {
    this.populateOrdersGrid(data as IOrderDetails[]);
    this.populatePieChart(data);
    this.populateTimelineChart(data);
});

Note the additional arguments passed. First, we do not need all data fields to be returned, so we pass an array with the field names that we need. Also, `pid` and `pkey` are passed, which will build a $filter query to return results only for records, where ProductID equals the value of `pid`. By OData specification, the request is $filter=(ProductID eq 32).

The ‘Details’ argument is the value of a relation that the Orders table has (Remember when we set this up in the Fine tuning ?). Knowing that the Orders table has a relation to the Orders_details table, API Server will construct a join SQL query, and will return results from both tables in a single response. Making our life so much easier here!

Visualization

Once we have the data we need, we may show it to the user in various ways to help him get information from it. Adding one more grid on the page, a timeline chart and a pie chart and we have a full working dashboard now!

The orders grid on the page shows all orders featuring the product selected in the first grid. The timeline chart visualizes the occurring of orders in time, helping you understand if Product sales have been going up or down. The Pie Chart aggregates the data per country and display slices based on Quantities. Just one look at the pie chart and your business users know where they ship most.

The configuration of the charts is simple. For the timeline chart we used иgxCategoryChart, which needs data source and can do the rest for you! For the иgxPieChart, pass a dataSource and the valueMemberPath, which tells the chart which value from the data source to use:

<!-- Timeline Chart -->
<igx-category-chart [dataSource]="ordersTimelineData" chartType="line">
</igx-category-chart>

<!-- Pie Chart -->
<igx-pie-chart [dataSource]="ordersDetailsData" valueMemberPath="Freight" labelMemberPath="Quantity">
</igx-pie-chart>

This is how it looks, once we are done with the above:

Demonstrate how to add full CRUD capabilities to the igxGrid to consume the API

In the Angular component, inject the data service using DI. Now we are ready to use the service to do full CRUD operations against our data backend. Note that IgxGrid does not provide its own UI for adding and deleting rows. For that reason, once the response from the CRUD operations for CREATE and DELETE is retrieved, just manually update the corresponding operation in the grid:

// get data for the corresponding table
this.dataService.getData(Tables.PRODUCTS).subscribe(data => {
    this.populateProductsGrid(data as IProduct[]);
});

// edit the record object
this.dataService.editData(Tables.PRODUCTS, editedRecord).subscribe({
});

// delete the record with corresponding ID
this.dataService.deleteData(Tables.ORDERS_DETAILS, ID).subscribe(response => {
    this.grid.deleteRowById(ID);
});

//  Ads new record
this.dataService.addData(Tables.PRODUCTS, newRecord).subscribe(response => {
    this.grid.addRow(newRecord);
});

For readability purposes, you see only the important part in the methods. Wonder where the new record or edited data come from? That’s right, you allow your user to edit data directly in the igxGrid, and then leverage the IgxGrid API to get everything you need:

<igx-grid #productsGrid(onRowEdit)="onRowEdit($event)".../>

public onRowEdit(event: IGridEditEventArgs) {
  const record = event.newValue;
  this.dataService.edit(Tables.PRODUCTS, record).subscribe(response => {
    });
}

In the above example, we attach an event handler for the grid onRowEdit event. Reading the event arguments, we get the edited record and use it the request body object in the PUT method. By specification, the server will respond with the updated object, indicating that request have been successful. Of course, it may happen that a record with the corresponding ID (read from the body object) is not found, or other error occurs.

In this case, instead of using the syntax sugar above (defining a callback only for the next notification, sent by the Observable), you may want to pass handlers for the error and complete notifications too:

this.dataService.editData(Tables.PRODUCTS, editedRecord).subscribe({
    next: (metadata: any) => {
    },
    error: err => {
      console.log(err);
    },
    complete: () => {
      console.log('Complete notification')
     }
});

As a best practice, we recommend adding error and complete handlers as above for each operation, and then implementing appropriate UI alerts for the errors (prompt to retry, or load offline content meanwhile, etc.). This will allow you to keep better control and will protect your users just in case, if, for some reason, the server responds with an error.

We hope that you found this blog to be helpful as you look to implement full CRUD operations against an OData API in your own apps. As you can see, Ignite UI for Angular components expose a rich public API, which makes it easier to implement full CRUD, while also offering advanced capabilities for visualizing the data.

Please note: the code provided is fully functional, and will provide a great base that you can take and start building on. Just fork the sample repository and step on our code base. For example, start by adding more and more operators, leveraging the OData querying flexibility. Have fun with it and do not hesitate to contact us or even push your own code, we will be happy to review it!