I thought it would be fun to write an app that uses the Punk API with the Angular CLI and Angular Material. My plan is to build this app over the span of a few blog posts. Part I will focus on the set up of Angular, Angular Material. At the end of this tutorial, we will have an app that displays a list of beers that we can page through. Or if you want to just view the finished product, you can view the repo or the live demo.
Prerequisites
I’m going to use the Angular CLI with Yarn to get up and running. So, if you don’t already have them installed you can do so by running:
npm install -g @angular/cli yarn
Versions
- Yarn:
^0.27.5
- Angular:
^4.0.0
- Angular-CLI:
1.2.0
- Angular Material:
2.0.0-beta.7
Getting Started
Let’s get to it.
ng set --global packageManager=yarn
ng new brewski-catalogue --style scss
This creates a new Angular project called brewski-catalogue
and then installs our dependencies using Yarn. Once complete, we can type ng serve
or yarn start
and point our browser to localhost:4200
. You should see “app works!” displayed on the page.
Suds Service
We will need a service to make an http request and retrieve the list of beers to use in our app. Let’s do that now by making a new directory called services
at src/app/services
and then use the Angular CLI to generate our service.
mkdir src/app/services
ng generate service services/suds
This creates two files for you in the src/app/services
directory and imports the service. Let’s take a look at the suds.service.ts
and add our http request.
import "rxjs/add/operator/map";
import { Injectable } from "@angular/core";
import { Http, Response } from "@angular/http";
@Injectable()
export class SudsService {
API_PATH: string = "https://api.punkapi.com/v2";
MAX_PER_PAGE: number = 10;
constructor(private http: Http) {}
getSuds() {
return this.http
.get(`${this.API_PATH}/beers?per_page=${this.MAX_PER_PAGE}`)
.map((res: Response) => res.json());
}
}
Notice that I’m limiting our response to 10 beers per page. Next, we will need to add our services to the src/app.module.ts
:
// All of our other imports
...
// Import the services here
import { SudsService } from './services';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule
],
providers: [
// Add your services here
SudsService
],
bootstrap: [AppComponent]
})
export class AppModule{ }
Beers List Component
Let’s add a component that will display our list of beers that we get back from the http request. Again, we will first have to make a directory for all of our components to live.
mkdir src/app/components
ng generate component components/beer-list
Let’s add our beer-list component to the app.component.html
so we can see it.
<h1>{{ title }}</h1>
<app-beer-list></app-beer-list>
Now you should see “app works!” along with “beer list works!” in the browser. So lets test out our beer service and display some beer! Inside our beer-list.component.ts
file lets use the beer service to spit out the json that gets returned back from our service. The beer-list.component.ts
should look like this.
import { Component, OnInit } from "@angular/core";
import { SudsService } from "../../services/suds.service";
@Component({
selector: "app-beer-list",
templateUrl: "./beer-list.component.html",
styleUrls: ["./beer-list.component.css"],
})
export class BeerListComponent implements OnInit {
suds: any[];
constructor(private sudsService: sudsService) {}
ngOnInit() {
this.sudsService.getSuds().subscribe(suds => (this.suds = suds));
}
}
And in our template (beer-list.component.html
) for now let’s just put {{ suds | json }}
. You should now have a huge json list of beers displayed in your browser.
Angular Material
At this point, I’m going to stop where we are and take some time to make our app more visually appealing. I’m going to add Angular Material to the project to help us do that. Let’s install it, yarn add @angular/material
. Next, we’ll add the material module to our app.module.ts
file.
// Other imports
...
import { MaterialModule } from '@angular/material';
@NgModule({
declarations: [...],
imports: [
...
HttpModule,
// Add Angular Material inside our imports
MaterialModule
],
providers: [
services
],
bootstrap: [AppComponent]
})
export class AppModule{ }
Don’t forget to add a comma to the import before our MaterialModule
. Then you will need to add @import ”~@angular/material/prebuilt-themes/indigo-pink.css”;
to the src/styles.scss
file. Once that’s complete, we can start building our beer-list component.
Lets remove the {{ beers | json }}
from our beer-list.component.html
and replace it with a material card layout:
<md-card *ngFor="let beer of beers">
<md-card-header>
<md-card-title>{{ beer.name }}</md-card-title>
<md-card-subtitle>{{ beer.tagline }}</md-card-subtitle>
</md-card-header>
<md-card-content>
<p>{{ beer.description }}</p>
</md-card-content>
<md-card-actions>
<button md-button color="primary">VIEW</button>
</md-card-actions>
</md-card>
And drop a few styles into the beer-list.component.scss
:
md-card {
max-width: 37.5rem;
margin: 0.9375rem auto;
}
p {
margin: 0 0.5rem;
}
This should give our component a bit of a makeover. I’ve limited the cards to 600px
and centered them. The VIEW
button will take us to a more detailed page that we will set up later.
Now lets go into our app.component.html
file and give it some pizzaz.
<md-toolbar color="primary">{{ title }}</md-toolbar>
<app-beer-list></app-beer-list>
We will also need to update the title in our app.component.ts
to Angular Brewski Catalogue
.
At this point we should have our beer app looking just peachy. Everything works as it should, but we only have a list of 10 beers. We need to figure out a way to paginate through our list of beers. So lets add a beer-list-controls
component.
ng g c components/beer-list-controls
Let’s add them to our beer-list.component.html
template:
<md-card *ngFor="let beer of beers>
...
</md-card>
<app-beer-list-controls></app-beer-list-controls>
Open up the src/app/components/beer-list-controls/beer-list-controls.ts
and add the following code:
import { Component, OnInit } from "@angular/core";
@Component({
selector: "app-beer-list-controls",
templateUrl: "./beer-list-controls.component.html",
styleUrls: ["./beer-list-controls.component.css"],
})
export class BeerListControlsComponent implements OnInit {
constructor() {}
ngOnInit() {}
nextPage() {}
prevPage() {}
}
Add two buttons to our component with Angular Material:
<button md-raised-button color="primary" (click)="prevPage()">Previous</button>
<button md-raised-button color="primary" (click)="nextPage()">Next</button>
And add some styles to make it look presentable:
:host {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
margin: 1.5625rem 0;
}
button {
margin: 0 0.625rem;
}
Our buttons look great, but they don’t actually do anything yet. Lets hook everything up. We will need a way to keep track with the current page number along with a way to increment and decrement that variable. So let’s add that to our src/app/components/beer-list-controls.component.ts
.
import { Component, OnInit } from "@angular/core";
@Component({
selector: "app-beer-list-controls",
templateUrl: "./beer-list-controls.component.html",
styleUrls: ["./beer-list-controls.component.scss"],
})
export class BeerListControlsComponent implements OnInit {
currentPage = 1;
constructor() {}
ngOnInit() {}
prevPage() {
this.currentPage--;
}
nextPage() {
this.currentPage++;
}
}
You can now console.log(this.currentPage)
inside the prevPage()
and nextPage()
methods and you will see the variable increment and decrement when we click on our buttons. Now we need a way to tell our Beer List Component that we want to update the page number. So lets add an event emitter, and an Output decorator. I’m also going to go ahead and hook the event emitter up to be used on a method called updatePage()
that we will create in a second.
import { Component, OnInit, EventEmitter, Output } from '@angular/core';
...
...
export class BeerListControlsComponent implements OnInit {
@Output() updatePage = new EventEmitter();
currentPage = 1;
...
prevPage() {
this.currentPage--;
this.updatePage.emit(this.currentPage);
}
nextPage() {
this.currentPage++;
this.updatePage.emit(this.currentPage);
}
}
Now when our previous or next buttons are clicked, our component methods will be called to decrement/increment our property and then emit an event to our BeerListComponent
. Now lets add our updatePage()
method to the BeerListComponent
.
// Change our OnInit method to call updatePage
ngOnInit() {
this.updatePage();
}
updatePage(page?: number) {
this.sudsService.getSuds(page)
.subscribe(suds => this.beers = suds);
}
Notice, I refactored our code so that the ngOnInit()
method calls the updatePage()
method. I have also added an optional parameter (note the ?
). Now lets edit our template to finish wiring up our event emitter.
<app-beer-list-controls (updatePage)="updatePage($event)"></app-beer-list-controls>
The last thing we need to do is fix our service to use a default parameter. In the src/app/services/suds.service.ts
, edit the getSuds()
method to take a parameter with a default value of 1 and add the parameter to our url:
getSuds(page: number = 1) {
return this.http.get(`${this.API_PATH}/beers?page=${page}&per_page=${this.MAX_PER_PAGE}`)
.map((res: Response) => res.json());
}
And there we have it, we should now be able to change the page of beers.
Part II will focus on fixing/adding our tests. You can view the code on GitHub or the live demo of the completed tutorial (currently only up to part I).
I hope this tutorial has been helpful. If you spot errors or would like to suggest an edit, please feel free to open an issue or submit a pull request to this article on GitHub.