facebook
Bob at Genuitec
Virtual evangelist at large. The face of Genuitec often appearing in graphics. Pen-name for our more shy writers of content.
Posted on Sep 8th 2017 In this Angular tutorial, you will learn how to create an app to compare bus fares. We will be using Angular IDE to create our application, which will fetch bus fare data using goibibo’s web services.

Creating a New Angular Project

In the new Angular project wizard, enter `BusFareCompare` as the project name. We used Angular CLI version 1.1.0, Node.js version 6.11.2, and NPM version 3.10.10 in our tutorial, but you are free to use more recent versions. bus-fare-select-versions Click Finish to complete the creation of a new Angular 4 project. In the Terminal+ view, you can see commands being executed by Angular CLI. After the installation of all the modules is complete, we are now ready to continue with development.

Adding Angular Material

To style our application, we will be using Angular Material, which is a set of high-quality UI components developed by the Angular team, based on the Google Material design specification. These components help us to build applications with an attractive, yet familiar UI, giving users a consistent experience across devices. To install HammerJS, Angular Material and Angular CDK, type and execute the following commands in Terminal+.
npm install --save hammerjs @angular/material @angular/cdk
bus-fare-npm-install Modern Angular projects already include the Angular animations module (@angular/animations), but if you happen to be following along with an older version of the CLI or Angular, please install that as well. Next you add the indigo-pink Angular material theme by adding the following line to your `/src/styles.css file`
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
For more on Angular Material, please see our previous post, Building Applications with Angular Material.

Creating the Search Card Component

bus-fare-search-card-component To create a new Angular component in Angular IDE, we use the menu File > New > Component. In the New Angular CLI Component wizard, enter `search-card` as element name. Under the advanced section, make sure `Do not create code in its own directory(–flat)` is checked, as we want to have a flat directory for this project. Now click Finish to complete creation of the search card component. Four new files are created in the `src/app` folder – `search-card.component.css`, `search-card.component.html`, `search-card.component.spec.ts` and `search-card.component.ts` were created, and the content of `app.module.ts` is updated. Let us take a look at some of the things to note when we create a new component via `Angular CLI` using Angular IDE.

Important File Changes

The file named `src/app/search-card.component.ts` is the component file for our newly created Search Card component, and its content is below:
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-search-card',
  templateUrl: './search-card.component.html',
  styleUrls: ['./search-card.component.css']
})
export class SearchCardComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}
At the top of the file, `Component` and `OnInit` are imported from `@angular/core` (each Angular library name begins with the @angular prefix). `Component` is a decorator function that we use to notify Angular that a class is an Angular component, and `OnInit` is a trait that our component must inherit in order to respond to the `ngOnInit` lifecycle hook. You can read more about Angular lifecycles here. Looking at the metadata passed to the `@Component` decorator in our component, we see that in order to use this component in an Angular template, the html tagname will be `app-search-card`, as specified by the `selector` metadata attribute. Also, this component will use the content of `src/app/search-card.component.html` as its Angular template and be styled with `src/app/search-card.component.css`. We can now use the search card component in our app component, because `app.module.ts` has been updated as follows: `SearchCardComponent` is imported, and added to the `declarations` property of the `@NgModule` metadata. So update the `content of src/app/app.component.html` to be
<md-toolbar color="primary">
  <span>BusFare</span>
</md-toolbar>
<app-search-card></app-search-card>

Storing user search input

In order to search for bus rates, we need the user to input the following information:
  • Trip type (One way or return trip)
  • Origin
  • Destination
  • Departure date
  • Return date (if trip type is return)
Let us create a search card interface to be used to type-check the data from the templates in accordance with our requirements above. Use File > New > Interface and type `SearchCardInterface` into the `Name` field, and `search-card.interface.ts` as the `File name`. Ensure that the source folder is set to `/BusFareCompare/src/app`. Click Finish and update the content of the newly created file as follows:
export interface SearchCardInterface {
  tripType: string;
  origin: string;
  destination: string;
  departureDate: Date;
  arrivalDate: Date;
}
 

Imports

Next we update `src/app/app.module.ts` to import a bunch of modules.  The file changes are shown in the code below:
...
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MdButtonModule, MdCardModule, MdDatepickerModule, MdInputModule, MdNativeDateModule, MdRadioModule,
   MdToolbarModule } from '@angular/material';
import { FormsModule } from '@angular/forms';

...
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FormsModule,
    MdButtonModule,
    MdCardModule,
    MdDatepickerModule,
    MdInputModule,
    MdNativeDateModule,
    MdRadioModule,
    MdToolbarModule
  ],
...

Creating the Search Service

Let us create a search service, which uses the data collected from user input to make an API request for bus tickets available. As we mentioned earlier, we will be using the API provided by goibibo, so head over to the developer portal to create an account so you can follow along. You will need to obtain an App Id and App key that you will use later in the search service. We create the search service, by using the menu File > New > Service. In the new service wizard, enter the `search-card` element name and click Finish. bus-fare-new-service You should import `SearchCardInterface` in your search-card service, and also create a `getSearchResults` method in it, which takes the user input, bound to `model: SearchCardInterface`, and gets search results from the goibibo API. The code snippet for the `getSearchResults` stub is below: `src/app/search-card.service.ts`
import { Injectable } from '@angular/core';

import { SearchCardInterface } from './search-card.interface';

@Injectable()
export class SearchService {

  constructor() { }

  getSearchResults(model: SearchCardInterface) {
 }

}
Let us now connect this stub method to the `onSubmit` method of our search-card component. The content of `src/app/search-card.component.ts` should be
import { Component, OnInit } from '@angular/core';
import { SearchCardInterface } from './search-card.interface';

import {SearchService} from './search-card.service';

@Component({
  selector: 'app-search-card',
  templateUrl: './search-card.component.html',
  styleUrls: ['./search-card.component.css']
})
export class SearchCardComponent implements OnInit {
  model: SearchCardInterface;

  constructor(private service: SearchService) { }

  ngOnInit() {
    this.model = {tripType: '0', origin: 'Bangalore', destination: 'Hyderabad', departureDate: new Date(),
    arrivalDate: new Date()};
  }

  onSubmit() {
    this.service.getSearchResults(this.model);
  }
}
Notice that we did not add SearchService to the provider of the search-card component. We will add it to `src/app/app.component.ts` because we want all the children of app component to use the same instance of search service. The content of `src/app/app.component.ts` should be updated as follows:
import { Component } from '@angular/core';
import {SearchService} from './search-card.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [SearchService]
})
export class AppComponent {
  title = 'app';
}

Template

Let us enter the following code in `src/app/search-card.component.html`
<md-card class="bus-ticket-card">
 <md-card-title>Book bus tickets online</md-card-title>
 <md-card-content>
 <form (ngSubmit)="onSubmit()" #searchForm="ngForm">
 <div>
 <md-radio-group name="tripType" [(ngModel)]="model.tripType" required>
 <md-radio-button value="0">One way</md-radio-button>
 <md-radio-button value="1">Round trip</md-radio-button>
 </md-radio-group>
 </div>
 <div>
 <md-input-container>
 <input mdInput name="origin" placeholder="Origin" [(ngModel)]="model.origin" required>
 </md-input-container>
 <md-input-container>
 <input mdInput name="destination" placeholder="Destination" [(ngModel)]="model.destination" required>
 </md-input-container>
 </div>
 <div>
 <md-input-container>
 <input mdInput name="departureDate" [(ngModel)]="model.departureDate" [mdDatepicker]="departPicker" placeholder="Depart:" required>
 <md-datepicker-toggle mdSuffix [for]="departPicker"></md-datepicker-toggle>
 </md-input-container>
 <md-datepicker [startAt]="model.departureDate" #departPicker></md-datepicker>
<md-input-container>
<input mdInput name="arrivalDate" [(ngModel)]="model.arrivalDate" [mdDatepicker]="returnPicker" placeholder="Return:" required>
 <md-datepicker-toggle mdSuffix [for]="returnPicker"></md-datepicker-toggle>
 </md-input-container>
 <md-datepicker [startAt]="model.arrivalDate" #returnPicker></md-datepicker>
 </div>
 <button md-raised-button color="accent" [disabled]="!searchForm.form.valid">Let's Go</button>
 </form>
 </md-card-content>

</md-card>
Add some style in `src/app/search-card.component.css`
.bus-ticket-card{
    width: 400px;
    margin: 24px auto;
}

Updating getSearchResults stub

We are going to user the user input to make an HTTP request to the API, and for this, we will need the Angular HttpClient. Let us include `HttpClientModule` among the imports in `src/app/app.module.ts`
import {HttpClientModule} from '@angular/common/http';
...

  imports: [
    ...
    HttpClientModule,
    ...
  ]
Next we inject `HttpClient` in our search service, and also develop the code for the `getSearchResults` method in `src/app/search-card.service.ts`. Remember to replace the APPID and APPKEY text in the snippet below with the Id and Key you obtain from the developer portal!
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { HttpClient } from '@angular/common/http';

import { SearchCardInterface } from './search-card.interface';

@Injectable()
export class SearchService {

// Observable source
 private resultsSource = new Subject<any>();

// Observable stream
 results$ = this.resultsSource.asObservable();
constructor(private http: HttpClient) { }
getSearchResults(model: SearchCardInterface) {
 const baseUrl = 'http://developer.goibibo.com';
 const endpoint = '/api/bus/search/';
 const params = {
 app_id: 'APPID',
 app_key: 'APPKEY',
 format: 'json',
 source: model.origin,
 destination: model.destination,
 dateofdeparture: this.formatDate(model.departureDate),
 dateofarrival: this.formatDate(model.arrivalDate)
 }
 const url = `${baseUrl}${endpoint}?${this.objToUrlParams(params)}`;
 this.http.get(url).subscribe(data => {
 this.resultsSource.next(data['data']);
 });
 }
// This is an util method to return YYYYMMDD date string
 formatDate(date: Date): string {
 const mm = date.getMonth() + 1; // getMonth is zero indexed
 const dd = date.getDate();
 return `${date.getFullYear()}${ mm > 9 ? mm : '0' + mm}${dd > 9 ? dd : '0' + dd}`;
 }

// This is a method to convert a dictionary to url param string
 objToUrlParams(params): string {
 let toret = '';
 for (const key in params) {
 if (params.hasOwnProperty(key)) {
 toret += `${key}=${encodeURIComponent(params[key])}&`;
 }
 }
 return toret;
 }

};

Creating the Search Result Component

Create a new component, SearchResultComponent with the element name `search-result` and ensure that in the Advanced section of the New Angular CLI Component wizard, you have the `–flat` option checked. First, we include our newly created component in our app component by updating `src/app/app.component.html`
<md-toolbar color="primary">
  <span>BusFare</span>
</md-toolbar>
<app-search-card></app-search-card>
<app-search-result></app-search-result>
Next, we will add search result component to subscribe to the search service in order to get search results. Let us do this by updating `src/app/search-result-component.ts`
import { Component, OnInit } from '@angular/core';
import {SearchService} from './search-card.service';
import { Subscription } from 'rxjs/Subscription';

@Component({
  selector: 'app-search-result',
  templateUrl: './search-result.component.html',
  styleUrls: ['./search-result.component.css']
})
export class SearchResultComponent implements OnInit {
  subscription: Subscription;
  results: object;

  constructor(private service: SearchService) {
    this.subscription = service.results$.subscribe(results => {
      this.results = results;
    })
  }

  ngOnInit() {

  }
}
We will display some part of the API response in a simple table, so let us update the template of our search result component, `src/app/search-result.component.html`
<section *ngIf="results">
<h3>Search results</h3>
<table >
<thead>
<tr>
<th>origin</th>
<th>destination</th>
<th>seat</th>
<th>fare.totalfare</th>
<th>duration</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of results.onwardflights">
<td>{{item.origin}}</td>
<td>{{item.destination}}</td>
<td>{{item.seat}}</td>
<td>{{item.fare.totalfare}}</td>
<td>{{item.duration}}</td>
</tr>
</tbody>
</table>
</section>
Note: At the time of this writing, due to a bug in Angular IDE, you may see incorrect validation errors reported against this file, for the items within the ngFor loop. This is a known issue that will be fixed shortly.

Styling

We style the search result table, by including our style rules in `src/app/search-result.component.css`
table {
  border: 1px solid #ccc;
  border-collapse: collapse;
  margin: 0;
  padding: 0;
  width: 100%;
  table-layout: fixed;
}
table caption {
  font-size: 1.5em;
  margin: .5em 0 .75em;
}
table tr {
  background: #f8f8f8;
  border: 1px solid #ddd;
  padding: .35em;
}
table th,
table td {
  padding: .625em;
  text-align: center;
}
table th {
  font-size: .85em;
  letter-spacing: .1em;
  text-transform: uppercase;
}
@media screen and (max-width: 600px) {
  table {
    border: 0;
  }
  table caption {
    font-size: 1.3em;
  }
  table thead {
    border: none;
    clip: rect(0 0 0 0);
    height: 1px;
    margin: -1px;
    overflow: hidden;
    padding: 0;
    position: absolute;
    width: 1px;
  }
  table tr {
    border-bottom: 3px solid #ddd;
    display: block;
    margin-bottom: .625em;
  }
  table td {
    border-bottom: 1px solid #ddd;
    display: block;
    font-size: .8em;
    text-align: right;
  }
  table td:before {
    /*
    * aria-label has no advantage, it won't be read inside a table
    content: attr(aria-label);
    */
    content: attr(data-label);
    float: left;
    font-weight: bold;
    text-transform: uppercase;
  }
  table td:last-child {
    border-bottom: 0;
  }
}

Running the app

Let us run our Angular app.  There is a server view in Angular IDE, and in order to open it, select Window > Show View > Servers. Right click on `BusFareCompare` under the Angular CLI node and click `Start Server`. Angular IDE serves the application on localhost port 4200 by default, so open up `http://127.0.0.1:4200` in your browser to see the running app. bus-fare-run-the-app Leave “one way trip” selected, enter Bangalore in the origin field and Hyderabad in the destination field, set an appropriate departure date, and click the Let’s Go button. bus-fare-app The search results are then displayed as below: bus-fare-search-results

Conclusion

There are a few obvious improvements that can be made to this app:
  • Storing the API key on the client side is probably not acceptable in most scenarios.
  • Loading animations can be added during search.
  • Return date field should be disabled if “One way” is selected, no results will be displayed if the return date is earlier than the departure date.
  • Using the Angular Material table component would improve the results display significantly.
These, and other small enhancements, are left as an exercise for the reader. We hope you enjoyed developing a real-world Angular application with Angular IDE!