Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Real-World Angular Series, Part 3b: Fetching and Displaying API Data

DZone's Guide to

Real-World Angular Series, Part 3b: Fetching and Displaying API Data

In this part of our series, we go over everything you'll need to know about data to get your application running, including fetching it from an API and manipulating it.

· Web Dev Zone
Free Resource

Get deep insight into Node.js applications with real-time metrics, CPU profiling, and heap snapshots with N|Solid from NodeSource. Learn more.

Welcome back! If you missed the first part of this article, check it out here.

Angular: Create a Utility Service

Before we start building out our components, let's make a utility service that we can build on throughout development.

Run the following command to generate the boilerplate:

$ ng g service core/utils

We'll begin using this service to add an isLoaded() utility. Then we'll create methods to manage the display of event dates. Each event has a start datetime and an end datetime. Start and end dates for a single event may be different days or the same day. We want a way to collapse same-day events into one date when displaying them in the UI. We also don't need to show times on the main listings, only on detail pages. Finally, we'll want a way to determine if an event already happened and is now in the past.

We'll import and take advantage of Angular's built-in DatePipe to help us craft some helper methods:

// src/app/core/utils.service.ts
import { Injectable } from '@angular/core';
import { DatePipe } from '@angular/common';

@Injectable()
export class UtilsService {

  constructor(private datePipe: DatePipe) { }

  isLoaded(loading: boolean): boolean {
    return loading === false;
  }

  eventDates(start, end): string {
    // Display single-day events as "Jan 7, 2018"
    // Display multi-day events as "Aug 12, 2017 - Aug 13, 2017"
    const startDate = this.datePipe.transform(start, 'mediumDate');
    const endDate = this.datePipe.transform(end, 'mediumDate');

    if (startDate === endDate) {
      return startDate;
    } else {
      return `${startDate} - ${endDate}`;
    }
  }

  eventDatesTimes(start, end): string {
    // Display single-day events as "1/7/2018, 5:30 PM - 7:30 PM"
    // Display multi-day events as "8/12/2017, 8:00 PM - 8/13/2017, 10:00 AM"
    const startDate = this.datePipe.transform(start, 'shortDate');
    const startTime = this.datePipe.transform(start, 'shortTime');
    const endDate = this.datePipe.transform(end, 'shortDate');
    const endTime = this.datePipe.transform(end, 'shortTime');

    if (startDate === endDate) {
      return `${startDate}, ${startTime} - ${endTime}`;
    } else {
      return `${startDate}, ${startTime} - ${endDate}, ${endTime}`;
    }
  }

  eventPast(eventEnd): boolean {
    // Check if event has already ended
    const now = new Date();
    const then = new Date(eventEnd.toString());
    return now >= then;
  }

}

First, we need to import the DatePipe from @angular/common. We'll add it to the constructor function's parameters.

Note: We also need to provideDatePipe in our app.module.ts if we want to be able to use it here. We'll add it when we provide our UtilsService.

The isLoaded() method uses an expression to check if the loading argument strictly evaluates to false. We'll be using a loading property in each component to track the state of API calls. This lets us know if we've received some kind of response from the API endpoint since loading would be undefined otherwise. This helps ensure that we don't reveal the wrong UI state in our templates.

The eventDates() method accepts start and end dates, then uses the date pipe to transform the dates into user-friendly strings. If the start and end dates are the same, only one date is returned. If they're different, the dates are returned as a range.

The eventDatesTimes() method does something very similar, but with times as well.

Lastly, the eventPast() method accepts an eventEnd parameter and compares it to the current datetime, outputting a boolean that informs us if the event has already ended.

Provide Date Pipe and Utility Service in the App Module

We can now import and provide the date pipe and our utility service in the app module. Open the app.module.ts file and make the following updates:

// src/app/app.module.ts
...
import { DatePipe } from '@angular/common';
import { UtilsService } from './core/utils.service';
...
@NgModule({
  ...,
  providers: [
    ...,
    DatePipe,
    UtilsService
  ],
  ...
})
...

We're now ready to use our new utilities in components. We'll add more methods to this handy service as we need them throughout development.

Angular: Create a Filter/Sort Service

Our app is going to need several different ways to organize arrays of data. In AngularJS, we would have used built-in filters for this, such as filter and orderBy. Angular (v2+) uses pipes to transform data, but no longer provides out-of-the-box pipes for filtering or sorting for reasons cited here.

Due to the performance and minification impacts of this choice, we will not create custom pipes to implement filtering or sorting functionality. This would simply re-introduce the same problems the Angular team was attempting to solve by removing these filters. Instead, the appropriate approach is to use services.

Throughout the development of our app, we'll add additional methods for searching, filtering, and sorting. For now, we'll start with three: searching (search() and noSearchResults()) and sorting by date (orderByDate()).

Run the following command to generate a service with the Angular CLI:

$ ng g service core/filter-sort

Open the new filter-sort.service.ts file and add:

// src/app/core/filter-sort.service.ts
import { Injectable } from '@angular/core';
import { DatePipe } from '@angular/common';

@Injectable()
export class FilterSortService {

  constructor(private datePipe: DatePipe) { }

  private _objArrayCheck(array: any[]): boolean {
    // Checks if the first item in the array is an object
    // (assumes same-shape for all array items)
    // Necessary because some arrays passed in may have
    // models that don't match {[key: string]: any}[]
    // This check prevents uncaught reference errors
    const item0 = array[0];
    const check = !!(array.length && item0 !== null && Object.prototype.toString.call(item0) === '[object Object]');
    return check;
  }

  search(array: any[], query: string, excludeProps?: string|string[], dateFormat?: string) {
    // Match query to strings and Date objects / ISO UTC strings
    // Optionally exclude properties from being searched
    // If matching dates, can optionally pass in date format string
    if (!query || !this._objArrayCheck(array)) {
      return array;
    }
    const lQuery = query.toLowerCase();
    const isoDateRegex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; // ISO UTC
    const dateF = dateFormat ? dateFormat : 'medium';
    const filteredArray = array.filter(item => {
      for (const key in item) {
        if (item.hasOwnProperty(key)) {
          if (!excludeProps || excludeProps.indexOf(key) === -1) {
            const thisVal = item[key];
            if (
              // Value is a string and NOT a UTC date
              typeof thisVal === 'string' &&
              !thisVal.match(isoDateRegex) &&
              thisVal.toLowerCase().indexOf(lQuery) !== -1
            ) {
              return true;
            } else if (
              // Value is a Date object or UTC string
              (thisVal instanceof Date || thisVal.toString().match(isoDateRegex)) &&
              // https://angular.io/api/common/DatePipe
              // Matching date format string passed in as param (or default to 'medium')
              this.datePipe.transform(thisVal, dateF).toLowerCase().indexOf(lQuery) !== -1
            ) {
              return true;
            }
          }
        }
      }
    });
    return filteredArray;
  }

  noSearchResults(arr: any[], query: string): boolean {
    // Check if array searched by query returned any results
    return !!(!arr.length && query);
  }

  orderByDate(array: any[], prop: string, reverse?: boolean) {
    // Order an array of objects by a date property
    // Default: ascending (1992->2017 | Jan->Dec)
    if (!prop || !this._objArrayCheck(array)) {
      return array;
    }
    const sortedArray = array.sort((a, b) => {
      const dateA = new Date(a[prop]).getTime();
      const dateB = new Date(b[prop]).getTime();
      return !reverse ? dateA - dateB : dateB - dateA;
    });
    return sortedArray;
  }

}

First we need to import the DatePipe from @angular/common. We'll add it to the constructor function's parameters.

Note: We already providedDatePipe in our app.module.ts when we implemented our UtilsService.

Then we'll create a private _objArrayCheck() method to ensure that the array we're trying to search or sort contains objects. If it doesn't, uncaught reference errors will be produced, so we'd like a way to prevent this.

The search() method accepts the array of objects to be filtered, a query to search for, any optional properties we want to exclude from searching (either a single property string or an array of properties), and optionally, a date format string.

The dateFormat should be one of the formats from the Angular DatePipe. This allows users to search for dates that are much less readable in the raw data. The developer can determine which format they want to be able to query. For example, if UTC date strings or JavaScript Date objects are transformed, the user can query for Jan and receive results with a value that is actually 2017-01-07T15:00:00.000Z in the data.

Note: It's advisable to match the dateFormat to any date pipe used in the display of the data being searched. That way, users can see the way dates are displayed in your listing and this can inform the way they structure their query.

If the query is falsey, we'll return the unfiltered array. Otherwise, we'll set the query to lowercase, since our search should be case-insensitive (we'll do the same to the values we're querying). Since UTC dates are recognized as strings and not Dates in JavaScript, we'll use a regular expression to differentiate them from other strings. If no dateFormat parameter is passed in, we'll default to medium(ie., Sep 3, 2010, 12:05:08 PM for the US).

Next, we'll filter the array using the filter() array m0ethod. We'll iterate over each property in each object in the array, first making sure that the object contains the property with the hasOwnProperty() method. If the key doesn't match anything passed in excludeProps, we'll check the value for matches to the query.

This is done differently for various value types. The search handles strings, JavaScript Date objects, and UTC strings. If we want to ensure that the search doesn't query certain properties, we'll make sure to pass them in as excludedProps when calling the method in our components.

Note: We won't search properties with values that are any other types because our RSVP app doesn't need this. If you'd like to see a more robust implementation that handles strings, numbers, booleans, and dates, please check out this filter-sort service Gist on GitHub.

The noSearchResults() method simply accepts an array and a query and returns true if the array is empty and a query is present.

Our orderByDate() method accepts an array of objects, the property containing the date value we want to sort by, and an optional reverseargument to change the sort order from ascending to descending.

If no property is passed, the array is returned unsorted.

Note: As written, this method expects an array of objects because this is how our data is structured in the RSVP app. An array of dates will not be sorted. You can easily update this method to support more robust array sorting if needed for other apps.

We can then use the sort() array method to re-order the array by date timestamp.

Provide FilterSort Service in the App Module

Now we'll import and provide the FilterSortService in the app module. Open the app.module.ts file and make the following updates:

// src/app/app.module.ts
...
import { FilterSortService } from './core/filter-sort.service';
...
@NgModule({
  ...,
  providers: [
    ...,
    FilterSortService
  ],
  ...
})
...

We can now search as well as sort our event arrays by date in our components.

Angular: Home Component Event List

Our components should get and display lists of events. We've already created the API endpoints to return this data and implemented an API service to fetch it. Now we need to subscribe to and display it in our components.

Show Public Events in the Home Component

Let's update our Home component to show the public upcoming events list. Open home.component.ts:

// src/app/pages/home/home.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ApiService } from './../../core/api.service';
import { UtilsService } from './../../core/utils.service';
import { FilterSortService } from './../../core/filter-sort.service';
import { Subscription } from 'rxjs/Subscription';
import { EventModel } from './../../core/models/event.model';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit, OnDestroy {
  pageTitle = 'Events';
  eventListSub: Subscription;
  eventList: EventModel[];
  filteredEvents: EventModel[];
  loading: boolean;
  error: boolean;
  query: '';

  constructor(
    private title: Title,
    public utils: UtilsService,
    private api: ApiService,
    public fs: FilterSortService) { }

  ngOnInit() {
    this.title.setTitle(this.pageTitle);
    this._getEventList();
  }

  private _getEventList() {
    this.loading = true;
    // Get future, public events
    this.eventListSub = this.api
      .getEvents$()
      .subscribe(
        res => {
          this.eventList = res;
          this.filteredEvents = res;
          this.loading = false;
        },
        err => {
          console.error(err);
          this.loading = false;
          this.error = true;
        }
      );
  }

  searchEvents() {
    this.filteredEvents = this.fs.search(this.eventList, this.query, '_id', 'mediumDate');
  }

  resetQuery() {
    this.query = '';
    this.filteredEvents = this.eventList;
  }

  ngOnDestroy() {
    this.eventListSub.unsubscribe();
  }

}

When we use subscriptions in our components, we should always take care to unsubscribe from them in the OnDestroy lifecycle hook, so we'll import OnDestroy and ensure that our HomeComponent class implements it and also has an ngOnDestroy() method.

We'll also import our API, utilities, and filter/sort services, as well as Subscription and the EventModel. We'll change the pageTitle to Eventsand declare properties for the event list API call subscription, event list results, and filtered events (for search results), which should have a type matching an array of EventModels. In order to handle loading state and errors, we'll need a loading property and an error property to inform our UI what to display. Finally, we need a member to store our search query.

The eventListSub subscription is called in our ngOnInit() lifecycle hook using a private _getEventList() method. In this method, we'll set loading to true while we make the API call. Then we'll subscribe to the api.getEvents$() observable from our API service. When we receive items in the stream, we'll use that response to set the local eventList property and the initial filteredEvents. We'll also set loading to false and handle errors.

The searchEvents() method calls the search() method from our filter/sort service and passes the appropriate parameters to it. We don't want the search to check the event _id so we'll pass that as excludeProps. Our events are going to be displayed in the template with the mediumDate date format, so we'll also pass that to the search() method.

We want to be able to reset the query with a button in the template, so the resetQuery() method sets the query property to an empty string and resets the filteredEvents array back to the initial eventList acquired in the API call.

Finally, we'll unsubscribe from eventListSub in the ngOnDestroy() lifecycle method.

Home Component Template

Now open the home.component.html template:

<!-- src/app/pages/home/home.component.html -->
<h1 class="text-center"></h1>
<app-loading *ngIf="loading"></app-loading>

<ng-template [ngIf]="utils.isLoaded(loading)">
  <ng-template [ngIf]="eventList">
    <ng-template [ngIf]="eventList.length">

      <!-- Search events -->
      <section class="search input-group mb-3">
        <label class="input-group-addon" for="search">Search</label>
        <input
          id="search"
          type="text"
          class="form-control"
          [(ngModel)]="query"
          (keyup)="searchEvents()" />
        <span class="input-group-btn">
          <button
            class="btn btn-danger"
            (click)="resetQuery()"
            [disabled]="!query">&times;</button>
        </span>
      </section>

      <!-- No search results -->
      <p *ngIf="fs.noSearchResults(filteredEvents, query)" class="alert alert-warning">
        No events found for <em class="text-danger"></em>, sorry!
      </p>

      <!-- Events listing -->
      <section class="list-group">
        <a
          *ngFor="let event of fs.orderByDate(filteredEvents, 'startDatetime')"
          [routerLink]="['/event', event._id]"
          class="list-group-item list-group-item-action flex-column align-items-start">
          <div class="d-flex w-100 justify-content-between">
            <h5 class="mb-1" [innerHTML]="event.title"></h5>
            <small></small>
          </div>
        </a>
      </section>
    </ng-template>

    <!-- No upcoming public events available -->
    <p *ngIf="!eventList.length" class="alert alert-info">
      No upcoming public events available.
    </p>
  </ng-template>

  <!-- Error loading events -->
  <p *ngIf="error" class="alert alert-danger">
    <strong>Oops!</strong> There was an error retrieving event data. 
  </p>

</ng-template>

In the template, we'll use the structural directive NgIf to dynamically load only the parts of the UI that should be revealed at a particular state of the component. The <app-loading> component shows if the loading property is true, and the event list or error should show if loading is false (but not undefined).

Note: Sometimes we're using a custom element template directive <ng-template [ngIf]> and sometimes we're using the *ngIfattribute directive. The <ng-template> approach does not create a container element in our markup, so we'll use this approach when we simply want if logic but no extra elements cluttering up our template. If we already have an element anyway, then we'll use the *ngIf attribute.

If event data was successfully retrieved and there are events present in the event array, we want to show a search form and the event list, which can be filtered on the fly. The search input is two-way bound to the query using the [(ngModel)] directive. This means that changes to the query will be kept in sync whether they came from the UI or the class. We then have a button that can be used to resetQuery().

If the user searches for a term that produces no results in the filtered array, we'll show a warning. We can check for search results by passing the filteredEvents and query to our filter/sort service's noSearchResults()method.

We'll use our sortByDate() method from the filter/sort service to display events ordered by their startDatetime. We'll iterate over the filteredEventsarray with the NgFor directive to display the event title, location, and dates. We'll also link to each event's detail page using the RouterLink directive and the event's _id (we'll create this detail page a little later).

If event data was retrieved, but no events were returned in the array from the API, we'll show a message saying that there are no upcoming, public events available.

Last, if there was an error retrieving data from the API, we'll show a message. The console should also log the error message.

Our public events homepage should look something like this now:

Angular RSVP homepage with events

If we type in a search query that returns no matches, we'll see the following:

Angular RSVP homepage with no search results

If there are no events available in the database, the homepage should show this message:

Angular RSVP homepage with no events

When an error occurs while fetching the events' data, the homepage should look like this:

Angular RSVP homepage with events

Note: We can easily test API errors by stopping the Node API server in the terminal and reloading the Angular application in the browser. Without the API accessible, it will show an error.

Summary

In Part 3 of our Real-World Angular Series, we've covered fetching data from the database with a Node API and manipulating and displaying the data in Angular. In the next part of the tutorial series, we'll tackle access management, displaying the admin events list, and developing an event details page with tabbed child components.

Node.js application metrics sent directly to any statsd-compliant system. Get N|Solid

Topics:
web dev ,angular ,web application development

Published at DZone with permission of Kim Maida, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}