Learning Ionic(Second Edition)
上QQ阅读APP看书,第一时间看更新

Building the Giphy app

Now that we have all the pieces to get started, we will start off by adding Twitter Bootstrap CSS to the app.

For this example, we will be using a Bootstrap theme from https://bootswatch.com/ named Cosmos. We can find the Cosmos CSS theme on the theme page: https://bootswatch.com/cosmo/, by clicking on the Cosmos dropdown and selecting the bootstrap.min.css option. Or alternatively, we can find it here: https://bootswatch.com/cosmo/bootstrap.min.css.

If you want, you can use any other theme or the vanilla Bootstrap CSS as well.

To add the theme file, navigate to giphy-app/src/styles.css and add the following line inside it:

@import "https://bootswatch.com/cosmo/bootstrap.min.css";

That is it, now our app is powered with Twitter Bootstrap CSS.

Next, we will start working on our app's main page. For that we will be leveraging an example template from Twitter Bootstrap named the Starter Template. The template can be found here: http://getbootstrap.com/examples/starter-template/.

The Starter template consists of a navigation bar and a body section where the content gets displayed.

For the Navbar section, we will be generating a new component named nav-bar and updating the relevant code in it.

To generate a new custom component using Angular CLI, navigate to the giphy-app folder and run the following:

ng generate component nav-bar

Note: You can either kill the current running command or spawn a new command prompt/terminal to run the preceding command.

And you should see something like this:

create src/app/nav-bar/nav-bar.component.css
create src/app/nav-bar/nav-bar.component.html
create src/app/nav-bar/nav-bar.component.spec.ts
create src/app/nav-bar/nav-bar.component.ts
update src/app/app.module.ts

Now open giphy-app/src/app/nav-bar/nav-bar.component.html and update it as follows:

<nav class="navbar navbar-inverse navbar-fixed-top"> 
<p class="container">
<p class="navbar-header">
<a class="navbar-brand" [routerLink]="['/']">Giphy App</a>
</p>
<p id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li [routerLinkActive]="['active']"><a [routerLink]="
['/trending']">Trending</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="
['/search']">Search</a></li>
</ul>
</p>
</p>
</nav>

All we are doing here is creating the header bar with two menu items and the app name, which acts as a link to the home page.

Next, we will update the giphy-app/src/app/app.component.html to load the nav-bar component. Replace the contents of that file with the following:

<nav-bar></nav-bar>

Next, we will start adding routes to the app. As discussed earlier, we are going to have three routes.

To add routing support to the current app, we need to do three things:

  1. Create the routes needed.
  2. Configure @NgModule.
  3. Tell Angular where to load the content of these routes.

At the time of writing, Angular CLI has disabled route generation. Hence we are going to create the same manually. Otherwise we could simply run ng generate route home to generate the home route.

So first, let's define all the routes. Create a new file named app.routes.ts inside the app folder. Update the file as follows:

import { HomeComponent } from './home/home.component'; 
import { TrendingComponent } from './trending/trending.component';
import { SearchComponent } from './search/search.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

export const ROUTES = [
{ path: '', component: HomeComponent },
{ path: 'trending', component: TrendingComponent },
{ path: 'search', component: SearchComponent },
{ path: '**', component: PageNotFoundComponent }
];

All we have done here is exported an array of routes. Do notice the path '**'. This is how we define the other section of the routes.

We will create the required components now. Run the following:

ng generate component home
ng generate component trending
ng generate component search
ng generate component pageNotFound

Next, we will configure the @NgModule. Open giphy-app/src/app/app.module.ts and add the following imports at the top:

import { RouterModule }   from '@angular/router'; 
import { ROUTES } from './app.routes';

Next, update the imports property of the @NgModule decorator as follows:

//.. snipp 
imports: [
BrowserModule,
FormsModule,
HttpModule,
RouterModule.forRoot(ROUTES)
],
//.. snipp

The completed page would look as follows:

import { BrowserModule } from '@angular/platform-browser'; 
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { NavBarComponent } from './nav-bar/nav-bar.component';
import { HomeComponent } from './home/home.component';
import { TrendingComponent } from './trending/trending.component';
import { SearchComponent } from './search/search.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

import { ROUTES } from './app.routes';

@NgModule({
declarations: [
AppComponent,
NavBarComponent,
HomeComponent,
TrendingComponent,
SearchComponent,
PageNotFoundComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
RouterModule.forRoot(ROUTES)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Now we will update the app component to show the Navbar as well as the current route content.

Update the giphy-app/src/app/app.component.html as follows:

<app-nav-bar></app-nav-bar> 
<router-outlet></router-outlet>

Using the router-outlet, we tell the router to load the current route content at that location.

If you want to know more about routing in Angular, you can check out: Routing in Eleven Dimensions with Component Router by Brian Ford: https://www.youtube.com/watch?v=z1NB-HG0ZH4.

Next, we will update the home component HTML and test the app so far.

Open giphy-app/src/app/home/home.component.html and update it as follows:

<p class="container"> 
<p class="starter-template">
<h1>Giphy App</h1>
<p class="lead">This app uses the JSON API provided by Giphy to Browse and Search Gifs.
<br> To know more checkout : <a href="https://github.com/Giphy/GiphyAPI#trending-gifs-endpoint">Giphy API</a> </p>
</p>
</p>

Once this is done, save the file and run the following:

ng  serve

And we should see the following page:

As we can see, the page looks broken. Let's fix this by adding a couple of styles. Open giphy-app/src/styles.css and add the following:

body {
padding-top: 50px;
padding-bottom: 20px;
}

.starter-template {
padding: 40px 15px;
text-align: center;
}

Now our page will look as expected:

Next, we will start by writing the service to talk to the Giphy API. We will be writing three methods, one to get a random gif, one to get the latest trends, and one to search the Gif API with a keyword.

To get started, we will generate a service. Run the following:

ng generate service giphy
WARNING Service is generated but not provided, it must be provided to be used

As shown in the warning, the service that has been generated has not been marked as a provider. So we need to do that manually.

Open giphy-app/src/app/app.module.ts and import the GiphyService:

import { GiphyService } from './giphy.service';

Next, add the GiphyService as a provider in the @NgModule decorator, providers property:

//.. snipp 
providers: [
GiphyService
],
//..snipp

The complete giphy-app/src/app/app.module.ts would look as follows:

import { BrowserModule } from '@angular/platform-browser'; 
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { NavBarComponent } from './nav-bar/nav-bar.component';
import { HomeComponent } from './home/home.component';
import { TrendingComponent } from './trending/trending.component';
import { SearchComponent } from './search/search.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

import { ROUTES } from './app.routes';

import { GiphyService } from './giphy.service';

@NgModule({
declarations: [
AppComponent,
NavBarComponent,
HomeComponent,
TrendingComponent,
SearchComponent,
PageNotFoundComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
RouterModule.forRoot(ROUTES)
],
providers: [
GiphyService
],
bootstrap: [AppComponent]
})
export class AppModule { }

Now we will update the giphy-app/src/app/giphy.service.ts with the three methods. Open giphy-app/src/app/giphy.service.ts and update it as follows:

import { Injectable } from '@angular/core'; 
import { Http, Response, Jsonp } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/Rx';

@Injectable()
export class GiphyService {
private giphyAPIBase = 'http://api.giphy.com/v1/gifs';
private APIKEY = 'dc6zaTOxFJmzC';

constructor(private http: Http) { }

getRandomGif(): Observable<Response> {
return this.http.get(this.giphyAPIBase +
'/random?api_key=' + this.APIKEY)
.map((res) => res.json());
}

getTrendingGifs(offset, limit): Observable<Response> {
return this.http.get(this.giphyAPIBase +
'/trending?api_key=' + this.APIKEY + '&offset=' + offset +
'&limit=' + limit)
.map((res) => res.json());
}

searchGifs(offset, limit, text): Observable<Response> {
return this.http.get(this.giphyAPIBase + '/search?api_key=' +
this.APIKEY + '&offset=' + offset +
'&limit=' + limit + '&q=' + text)
.map((res) => res.json());
}
}

All we are doing here is making an HTTP GET request to the corresponding Giphy API URLs and returning an Observable.

In RxJS (http://reactivex.io/rxjs/), an Observable is an entity, which can change over a period of time. This is the most basic building block of RxJS. An Observer subscribes to an Observable and reacts to its changes. This pattern is called a Reactive pattern.

Quoting from the documentation:

This pattern facilitates concurrent operations because it does not need to block while waiting for the Observable to emit objects, but instead it creates a sentry in the form of an observer that stands ready to react appropriately at whatever future time the Observable does so.

If you are new to Observables, you can start here: http://reactivex.io/documentation/observable.html followed by: Taking advantage of Observables in Angular: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html and Angular 2 HTTP requests with Observables: https://scotch.io/tutorials/angular-2-http-requests-with-observables.

Now that the service is completed, we will update the HomeComponent to get a random gif and display it on the home page.

Open giphy-app/src/app/home/home.component.ts and update it as follows:

import { Component, OnInit } from '@angular/core'; 
import { GiphyService } from '../giphy.service';

@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
public gif: string;
public result: any;
public isLoading: boolean = true;

constructor(private giphyService: GiphyService) {
this.getRandomGif();
}

ngOnInit() {
}

getRandomGif() {
this.giphyService.getRandomGif().subscribe(
(data) => {
this.result = data;
this.gif = this.result.data.image_url;
this.isLoading = false;
},
(err) => console.log('Oops!', err),
() => console.log('Response', this.result)
)
}
}

In the preceding code, first off, we have imported GiphyService and added it to the constructor. Next, we have written getRandomGif() and invoked getRandomGif() from the constructor. In getRandomGif(), we have invoked getRandomGif() on giphyService to get a random gif. We are then assigning the gif to a class variable named gif.

Just to see if everything is working fine, we will run the app by executing ng serve and opening developer tools. If everything goes well, we should see the response from the Giphy API:

Now that we have the response, we want to build a component that will display the gif. We want to build a separate component for this because we will be using the same component on other pages as well to display a gif where needed.

Let's go ahead and scaffold the component. Run the following:

ng generate component gif-viewr

Next, open giphy-app/src/app/gif-viewr/gif-viewr.component.html and update it as follows:

<p class="item"> 
<p class="well">
<img src="{{imgUrl}}">
</p>
</p>

Once this is done, we need to tell the component to expect the data from the parent component, as the home component will pass the imgUrl to the gif-viewer component.

Open giphy-app/src/app/gif-viewr/gif-viewr.component.ts. First, update the import statement by adding a reference to the Input decorator:

import { Component, OnInit, Input} from '@angular/core';

Next, add an Input decorator to the imgUrl variable:

@Input() imgUrl: string;

The updated giphy-app/src/app/gif-viewr/gif-viewr.component.ts would look as follows:

import { Component, OnInit, Input} from '@angular/core'; 

@Component({
selector: 'app-gif-viewr',
templateUrl: './gif-viewr.component.html',
styleUrls: ['./gif-viewr.component.css']
})
export class GifViewrComponent implements OnInit {
@Input() imgUrl: string;

constructor() { }

ngOnInit() {
}
}

Note: To define an input for a component, we use the @Input decorator. To know more about the @Input decorator, refer to the Attribute Directives section in Angular docs: https://angular.io/docs/ts/latest/guide/attribute-directives.html.

Save the file and open giphy-app/src/app/home/home.component.html. We will add the app-gif-viewr component inside this page:

<app-gif-viewr class="home" [imgUrl]="gif"></app-gif-viewr>

The complete file would look as follows:

<p class="container"> 
<p class="starter-template">
<h1>Giphy App</h1>
<p class="lead">This app uses the JSON API provided by Giphy to
Browse and Search Gifs.
<br> To know more checkout :
<a href=
"https://github.com/Giphy/GiphyAPI#trending-gifs-endpoint">
Giphy API</a> </p>
</p>

<app-gif-viewr class="home" [imgUrl]="gif"></app-gif-viewr>
</p>

Next, we will update CSS to beautify the page. Open giphy-app/src/styles.css and add the following CSS to the existing styles:

.home .well{ 
width: 70%;
margin: 0 auto;
}

img{
width: 100%;
}

If we go back to the browser and refresh, we should see the following:

And every time we refresh a page, we will see a new gif come up.

Next, we are going to work on the Trending page. This page will show gifs that are trending using the Pintrest layout (or Masonry layout). The Trending REST API supports pagination. We will be making use of this to load 12 gifs at a time. And then provide a Load More button to fetch the next 12 gifs.

First, let's get the data from the Giphy API. Open giphy-app/src/app/trending/trending.component.ts. We will first import the GiphyService:

import { GiphyService } from '../giphy.service';

Now, we will add the same to the constructor and update the constructor to invoke getTrendingGifs():

constructor(private giphyService: GiphyService) { } 
In ngOnInit(), we will call the getTrendingGifs() API:
ngOnInit() {
this.getTrendingGifs(this.offset, this.perPage);
}
Next, we will add the required class variables:
private offset = 0;
private perPage = 12;
public results: any;
public gifs: Array<any> = [];
public isLoading: boolean = true;

offset and perPage will be used to manage pagination.

results will be used to store the response from the server.

gifs is the array consisting of an array of trending gifs that we are exposing to the template.

isLoading is a boolean variable to keep track if a request is in progress or not. Using isLoading, we will show/hide the Load More button.

Next, we will add getTrendingGifs():

getTrendingGifs(offset, limit) { 
this.giphyService.getTrendingGifs(offset, limit).subscribe(
(data) => {
this.results = data;
this.gifs = this.gifs.concat(this.results.data);
this.isLoading = false;
},
(err) => console.log('Oops!', err),
() => console.log('Response', this.results)
)
}
And finally getMore(), which will be invoked by the Load More button:
getMore() {
this.isLoading = true;
this.offset = this.offset + this.perPage;
this.getTrendingGifs(this.offset, this.perPage);
}

To display the gifs retrieved, we will update the trending component template. Open giphy-app/src/app/trending/trending.component.html and update it as follows:

<p class="container"> 
<h1 class="text-center">Trending Gifs</h1>
<p class="wrapper">
<app-gif-viewr [imgUrl]="gif.images.original.url" *ngFor="let gif of gifs"></app-gif-viewr>
</p>
<input type="button" value="Load More" class="btn btn-primary btn-block" *ngIf="!isLoading" (click)="getMore()">
</p>

All we are doing here is setting up app-gif-viewr to take the gif URL by applying an *ngFor directive on it. And at the bottom, a Load More button, so a user can load more gifs.

And finally to achieve the Pintrest/Masonry layout, we will add a couple of CSS rules. Open giphy-app/src/styles.css and add the following styles:

*, *:before, *:after { 
box-sizing: border-box !important;
}

.wrapper {
column-width: 18em;
column-gap: 1em;
}

.item {
display: inline-block;
padding: .25rem;
width: 100%;
}

.well {
position: relative;
display: block;
}

Save all the files and head back to the browser. If we click on the trending menu item in the Navbar, we should see the following:

And if we scroll down completely, we should see a Load More button:

Clicking on the Load More button will load the next set of gifs:

I wasted about 15 minutes clicking Load More and watching the gifs. I think this is why APIs should have a rate limit.

Finally, we will implement searching gif. Open giphy-app/src/app/search/search.component.ts and import GiphyService:

import { GiphyService } from '../giphy.service';

Add giphyService as a class variable in the constructor:

constructor(private giphyService: GiphyService) { }

Next, we will add variables to manage pagination as well as the response:

  private offset = 0; 
private perPage = 12;
public results: any;
public query: string;
public gifs: Array<any> = [];
public isLoading: boolean = true;

Now we will invoke searchGifs, which makes a REST call to get the searched gifs, by passing in the query string:

searchGifs(offset, limit, query) { 
this.giphyService.searchGifs(offset, limit, query).subscribe(
(data) => {
this.results = data;
this.gifs = this.gifs.concat(this.results.data);
this.isLoading = false;
},
(err) => console.log('Oops!', err),
() => console.log('Response', this.results)
)
}

The following is a method to manage the search form submit button:

  search(query) { 
this.query = query;
this.isLoading = true;
this.searchGifs(this.offset, this.perPage, this.query);
}

And finally, getMore() to load more pages of the same query:

getMore() { 
this.isLoading = true;
this.offset = this.offset + this.perPage;
this.searchGifs(this.offset, this.perPage, this.query);
}

The updated giphy-app/src/app/search/search.component.ts would look as follows:

import { Component, OnInit } from '@angular/core'; 
import { GiphyService } from '../giphy.service';

@Component({
selector: 'app-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.css']
})
export class SearchComponent implements OnInit {
private offset = 0;
private perPage = 12;
public results: any;
public query: string;
public gifs: Array<any> = [];
public isLoading: boolean = true;

constructor(private giphyService: GiphyService) { }

ngOnInit() {
}

searchGifs(offset, limit, query) {
this.giphyService.searchGifs(offset, limit, query).subscribe(
(data) => {
this.results = data;
this.gifs = this.gifs.concat(this.results.data);
this.isLoading = false;
},
(err) => console.log('Oops!', err),
() => console.log('Response', this.results)
)
}

search(query) {
this.query = query;
this.isLoading = true;
this.searchGifs(this.offset, this.perPage, this.query);
}

getMore() {
this.isLoading = true;
this.offset = this.offset + this.perPage;
this.searchGifs(this.offset, this.perPage, this.query);
}
}

Now we will update the giphy-app/src/app/search/search.component.html. Open giphy-app/src/app/search/search.component.html and update it as follows:

<p class="container"> 
<h1 class="text-center">Search Giphy</h1>
<p class="row">
<input class="form-control" type="text" placeholder="Search
something.. Like.. LOL or Space or Wow" #searchText
(keyup.enter)="search(searchText.value)">
</p>
<br>
<p class="wrapper">
<app-gif-viewr [imgUrl]="gif.images.original.url" *ngFor="let
gif of gifs"></app-gif-viewr>
</p>
<input type="button" value="Load More" class="btn btn-primary btn-block" *ngIf="!isLoading" (click)="getMore()">
</p>

This view is the same as the Trending component, except there is a search textbox, which will allow the user to search by entering a string.

If we save all the files, go back to the browser, and navigate to the Search page, we should see an empty page with a search textbox. At this point, the load more button will not be shown. If we enter text and hit the return key, we should see results, as shown in the following screenshot:

With this we have completed the implementation of a Giphy API with an Angular app.

To bring this example to a closure, we will update giphy-app/src/app/page-not-found/page-not-found.component.html as follows:

<p class="container"> 
<p class="starter-template">
<h1>404 Not Found</h1>
<p class="lead">Looks Like We Were Not Able To Find What You Are Looking For.
<br>Back to : <a [routerLink]="['/']">Home</a>? </p>
</p>
</p>

And when we navigate to http://localhost:4200/nopage, we should see the following page: