# Consuming the REST API
With our Angular application up and running, let’s now extend the application to consume the REST controller API.[1]
# Track model
Since our Angular application will fetch
Track
entities, implement a simple domain model with TypeScript.ng generate interface track
Angular CLI will generate an empty
track.ts
. Populate fields to theTrack
class.export interface Track { title: string; uri: string; artists: string[]; }
# Spotify Service
Implement a service class that performs GET requests to the /api/playlists/tracks
endpoint.
This will allow us to encapsulate access to the REST controller in a single class, which we can reuse throughout the entire application.
Open a console terminal and issue the following command.
ng generate service spotify
Open the
spotify.service.ts
file that Angular CLI just created and add some functionality:import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {Observable} from 'rxjs'; import {Track} from './track'; @Injectable({ providedIn: 'root' }) export class SpotifyService { private apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = '/api'; } public getTracksForPlaylist(): Observable<Track[]> { return this.http.get<Track[]>(`${this.apiUrl}/playlists/tracks`); } }
Add the api call to
spotify.service.spec.ts
.import {TestBed} from '@angular/core/testing'; import {SpotifyService} from './spotify.service'; import {Track} from './track'; import {of} from 'rxjs'; import {HttpClient} from '@angular/common/http'; describe('SpotifyService', () => { let httpClientSpy: { get: jasmine.Spy }; let spotifyService: SpotifyService; beforeEach(() => { httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']); TestBed.configureTestingModule({ providers: [ {provide: HttpClient, useValue: httpClientSpy} ] }); spotifyService = new SpotifyService(httpClientSpy as any); }); it('should be created', () => { const service: SpotifyService = TestBed.get(SpotifyService); expect(service).toBeTruthy(); }); it('should return expected tracks (HttpClient called once)', () => { const expectedTracks: Track[] = [ {uri: 'uriA', title: 'titleA', artists: ['artistA']}, {uri: 'uriB', title: 'titleB', artists: ['artistB', 'artistC']} ]; httpClientSpy.get.and.returnValue(of(expectedTracks)); spotifyService.getTracksForPlaylist().subscribe( tracks => expect(tracks).toEqual(expectedTracks, 'expected tracks'), fail ); expect(httpClientSpy.get.calls.count()).toBe(1, 'one call'); }); });
Run tests.
ng test
# Angular Material
Install Angular Material, Angular CDK and Angular Animations.
npm install @angular/material @angular/cdk @angular/animations
Import BrowserAnimationsModule into your application to enable animations support in
app.module.ts
.import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; @NgModule({ ... imports: [ BrowserAnimationsModule, ... ], ... }) export class AppModule { }
Import the NgModule for each component you want to use in
app.module.ts
.import {MatTableModule} from '@angular/material'; @NgModule({ ... imports: [ MatTableModule .. ], ... }) export class AppModule { }
To get started with a prebuilt theme, include one of Angular Material's prebuilt themes globally in your application. You can add this to your
styles.css
.@import "~@angular/material/prebuilt-themes/indigo-pink.css";
# Playlist Component
Open a terminal console and generate a playlist component:
ng generate component playlist
Implement the class so that it can take a
SpotifyService
instance in the constructor and uses theSpotifyService getTracksForPlaylist()
method to fetch all tracks.import {Component, OnInit} from '@angular/core'; import {Track} from '../track'; import {SpotifyService} from '../spotify.service'; @Component({ selector: 'app-playlist', templateUrl: './playlist.component.html', styleUrls: ['./playlist.component.css'] }) export class PlaylistComponent implements OnInit { tracks: Track[]; displayedColumns: string[] = ['title', 'artists']; constructor(private spotifyService: SpotifyService) { } ngOnInit() { this.spotifyService.getTracksForPlaylist().subscribe(tracks => { this.tracks = tracks; }); } }
Additionally, we need to edit the component’s HTML file,
playlist.component.html
, to create the table that displays the list of entities.<h1 class="mat-h1">Playlist</h1> <table mat-table [dataSource]="tracks"> <ng-container matColumnDef="title"> <th mat-header-cell *matHeaderCellDef>Title</th> <td mat-cell *matCellDef="let element">{{element.title}}</td> </ng-container> <ng-container matColumnDef="artists"> <th mat-header-cell *matHeaderCellDef>Artists</th> <td mat-cell *matCellDef="let element">{{element.artists}}</td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> </table>
Add Tests to
playlist.component.spec.ts
.import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {PlaylistComponent} from './playlist.component'; import {Observable, of} from 'rxjs'; import {Track} from '../track'; import {SpotifyService} from '../spotify.service'; import {MatTableModule} from '@angular/material'; describe('PlaylistComponent', () => { let component: PlaylistComponent; let fixture: ComponentFixture<PlaylistComponent>; let spotifyService: SpotifyService; class MockSpotifyService { public getTracksForPlaylist(): Observable<Track[]> { return of([ {uri: 'uriA', title: 'titleA', artists: ['artistA']}, {uri: 'uriB', title: 'titleB', artists: ['artistB', 'artistC']} ]); } } beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [PlaylistComponent], imports: [MatTableModule], providers: [ PlaylistComponent, {provide: SpotifyService, useClass: MockSpotifyService} ] }).compileComponents(); fixture = TestBed.createComponent(PlaylistComponent); component = fixture.componentInstance; spotifyService = TestBed.get(SpotifyService); })); it('should create', () => { expect(component).toBeTruthy(); }); it('should not have tracks message after construction', () => { expect(component.tracks).toBeUndefined(); }); it('should have tracks after Angular calls ngOnInit', () => { component.ngOnInit(); spotifyService.getTracksForPlaylist().subscribe( tracks => expect(component.tracks).toEqual(tracks), fail ); }); it('should render title in a h1 tag', () => { fixture.detectChanges(); const compiled = fixture.debugElement.nativeElement; expect(compiled.querySelector('h1').textContent).toContain('Playlist'); }); it('should render title and artist', () => { fixture.detectChanges(); const compiled = fixture.debugElement.nativeElement; let trs = compiled.querySelectorAll('tbody tr'); expect(trs.length).toBe(2); expect(trs[0].children[0].textContent).toBe('titleA'); expect(trs[0].children[1].textContent).toBe('artistA'); expect(trs[1].children[0].textContent).toBe('titleB'); expect(trs[1].children[1].textContent).toBe('artistB,artistC'); }); });
Run tests.
ng test
# Application Component HTML
The app.component.html
file allows us to define the root component’s HTML template. Replace content with:
<app-playlist></app-playlist>
# Application Module
Next, we need to edit the
app.module.ts
file, so Angular can import all the required modules, components, and services.import {BrowserModule} from '@angular/platform-browser'; import {NgModule} from '@angular/core'; import {MatTableModule} from '@angular/material'; import {AppComponent} from './app.component'; import {PlaylistComponent} from './playlist/playlist.component'; import {HttpClientModule} from '@angular/common/http'; @NgModule({ declarations: [ AppComponent, PlaylistComponent ], imports: [ BrowserModule, HttpClientModule, MatTableModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Add PlaylistComponent and HttpClientTestingModule to
app.component.spec.ts
.beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent, PlaylistComponent ], imports: [ HttpClientTestingModule, MatTableModule ] }).compileComponents(); }));
Run tests.
ng test
# Proxy Configuration
Using the proxying support in webpack's dev server we can redirect the /api
calls for local development.
We create a file next to our project's
package.json
calledproxy.conf.json
with the content.{ "/api/*": { "target": "http://localhost:8080", "secure": false, "logLevel": "debug", "changeOrigin": true } }
We then add the
proxyConfig
option to the serve target inangular.json
."serve": { ... "options": { "browserTarget": "ui:build", "proxyConfig": "./proxy.conf.json" },
EXERCISE
- Push your changes to Heroku and test the application.
- Create new component for header.
- Create new component in order to search tracks and to add them to the playlist.
- Angular User Inputs: https://angular.io/guide/user-input (opens new window)
- Angular Material: https://material.angular.io/ (opens new window)
WARNING
- In order to login you have to call
/api/login
manually in the browser.