Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
353 lines (267 sloc) 12.4 KB

+++ draft = false date = "2017-03-08T15:07:37-06:00" title = "Building an Angular & Material App Part I" tags = ["angular", "material", "angular-cli"] author = "Ryan Jordan"

+++

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: .9375rem auto;
}
p {
  margin: 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 .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.