Angular
Building RealWorld, Production-Quality Apps with Angular

How To: Services for Interacting & Authenticating with a Server

PRO

I finished! On to the next chapter

Outline

Now that we can console log our form data, lets wire up our AuthComponent form to the actual login and register endpoints on our server. First we'll need to create a service that handles making requests to the server. Both login and register endpoints require a POST request, so we'll start building that out.

Create the ApiService

src/app/shared/services/api.service.ts

import { Injectable } from '@angular/core';
import { environment } from '../../../environments/environment';
import { Headers, Http, Response, URLSearchParams } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';

@Injectable()
export class ApiService {
  constructor(
    private http: Http
  ) {}

  private setHeaders(): Headers {
    let headersConfig = {
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    };

    return new Headers(headersConfig);
  }

  private formatErrors(error: any) {
     return Observable.throw(error.json());
  }

  post(path: string, body: Object = {}): Observable<any> { 
    return this.http.post(`${environment.api_url}${path}`, JSON.stringify(body), { headers: this.setHeaders() })
        .catch(this.formatErrors)
        .map((res:Response) => res.json());
  }

}

When working with the API it's a good idea to have the RealWorld API spec open in another tab for reference as we are buidling our functions based on its requirements.

We first set our header information (required for all API calls) as a function that returns headersConfig. The benefit to using a function to set our header is so we can add an authentication header in the future.

In our seed file we set the variable api_url as an environment constant so that it can be used througout the app.

formatErrors() returns any errors from our server as JSON.

Export the ApiService

src/app/shared/services/index.ts

export * from './api.service';

src/app/shared/index.ts

 export * from './layout';
+export * from './services';
 export * from './shared.module';

Next we'll create the UserService that provides methods for logging in, registering, to check whether a user is logged in, as well as accessing the data of the logged in user.

Create the UserService

src/app/shared/services/user.service.ts

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';

import { ApiService } from './api.service';
import { User } from '../models';


@Injectable()
export class UserService {
  private currentUserSubject = new BehaviorSubject<User>(new User());
  public currentUser = this.currentUserSubject.asObservable().distinctUntilChanged();

  private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
  public isAuthenticated = this.isAuthenticatedSubject.asObservable();

  constructor (
    private apiService: ApiService,
    private http: Http
  ) {}

  setAuth(user: User) {
    // Set current user data into observable
    this.currentUserSubject.next(user);
    // Set isAuthenticated to true
    this.isAuthenticatedSubject.next(true);
  }

  attemptAuth(type, credentials): Observable<User> {
    let route = (type === 'login') ? '/login' : '';
    return this.apiService.post('/users' + route, {user: credentials})
    .map(
      data => {
        this.setAuth(data.user);
        return data;
      }
    );
  }

  getCurrentUser(): User {
    return this.currentUserSubject.value;
  }

}

Export the serivce

src/app/shared/services/index.ts

export * from './api.service';
+ export * from './user.service';

Our post() request accepts a URL path (the type from attemptAuth) and parameters (credentials). It then constructs our request URL and returns an Oberservable as JSON. We then .map the JSON and pass our user data (data.user) to setAuth and return the data.

The UserService sets a user as a BehaviorSubject and saves it as currentUserSubject. setAuth() sets this to our user data from attemptAuth() and sets the isAuthenticatedSubject as true. The private variables currentUserSubject and isAuthenticatedSubject are convereted to a public Observable for use throughout the app. Converting the Subject to an Observable prevents any changes from being made to the User.

Import ApiService and UserService into AppModule and declare as providers

src/app/app.module.ts



[...]

 import { AuthModule } from './auth/auth.module';
 import { HomeModule } from './home/home.module';
 import {
+  ApiService,
+  UserService,
   FooterComponent,
   HeaderComponent,
  SharedModule,
 } from './shared';

 const rootRouting: ModuleWithProviders = RouterModule.forRoot([], { useHash: true });


[...]

     rootRouting,
     SharedModule
   ],
+  providers: [
+    ApiService,
+    UserService
+  ],
   bootstrap: [AppComponent]
 })
 export class AppModule {}

The next thing we need to do is modify the AuthComponent to wire it up to the UserService. First we need to create a standard model for any errors we get back from the server (email is taken, password too short, etc) as well as model for the user object we expect to receive when the request is successful.

Create the Errors and User models

src/app/shared/models/errors.model.ts

export class Errors {
  errors: {[key:string]: string} = {};
}

src/app/shared/models/user.model.ts

export class User {
  email: string;
  token: string;
  username: string;
  bio: string;
  image: string;
}
Export our models

src/app/shared/models/index.ts

export * from './errors.model';
export * from './user.model';

src/app/shared/index.ts

 export * from './layout';
+export * from './models';
 export * from './services';
 export * from './shared.module';

Lets implement error handling and wire up our form to pass the user's input to attemptAuth.

Wire up the AuthComponent to the UserService

src/app/auth/auth.component.ts

 import { Component, OnInit } from '@angular/core';
 import { FormBuilder, FormGroup, FormControl } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { Errors, UserService } from '../shared';

 @Component({
   selector: 'auth-page',


[...]

 export class AuthComponent implements OnInit {
   authType: String = '';
   title: String = '';
+  errors: Errors = new Errors();
   isSubmitting: boolean = false;
   authForm: FormGroup;

   constructor(
     private route: ActivatedRoute,
+    private router: Router,
+    private userService: UserService,
     private fb: FormBuilder
   ) {
     // use FormBuilder to create a form group


[...]


   submitForm() {
     this.isSubmitting = true;
+    this.errors = new Errors();

     let credentials = this.authForm.value;
+    this.userService.attemptAuth(this.authType, credentials)
+    .subscribe(
+      data => this.router.navigateByUrl('/'),
+      err => {
+        this.errors = err;
+        this.isSubmitting = false;
+      }
+    );
   }
 }

Wouldn't it be great if we could display errors the same way across every single form on the site without having to copy and paste the same code over and over? This is what components are great for - reusable UI functionality. Lets create a component called ListErrors that will do precisely that.

Create the ListErrors component

src/app/shared/list-errors.component.html

<ul class="error-messages" *ngIf="errorList">
  <li *ngFor="let error of errorList">
    {{ error }}
  </li>
</ul>

src/app/shared/list-errors.component.ts

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

import { Errors } from './models';

@Component({
  selector: 'list-errors',
  templateUrl: './list-errors.component.html'
})
export class ListErrorsComponent {
  formattedErrors: Array<string> = [];

  @Input()
  set errors(errorList: Errors) {
    this.formattedErrors = [];

    if (errorList.errors) {
      for (let field in errorList.errors) {
        this.formattedErrors.push(`${field} ${errorList.errors[field]}`);
      }
    }
  };

  get errorList() { return this.formattedErrors; }


}

Keep in mind that we receive Errors as an Object with key-value pairs. Here we're looping through our object and saving the data as key (field) value (errorList.errors[field]) pairs in the formattedErrors array.

Export it...

src/app/shared/index.ts

 export * from './layout';
+export * from './list-errors.component';
 export * from './models';
 export * from './services';
 export * from './shared.module';

Since this component will be used across our entire app, we need to declare & export it in our SharedModule.

Declare & export the ListErrors component in the SharedModule

src/app/shared/shared.module.ts



[...]

 import { HttpModule } from '@angular/http';
 import { RouterModule } from '@angular/router';

+import { ListErrorsComponent } from './list-errors.component';

 @NgModule({
   imports: [
     CommonModule,


[...]

     HttpModule,
     RouterModule
   ],
+  declarations: [
+    ListErrorsComponent
+  ],
   exports: [
     CommonModule,
     FormsModule,
     ReactiveFormsModule,
     HttpModule,
+    ListErrorsComponent,
     RouterModule
   ]
 })


[...]

And now we can just place the list-errors component in the AuthComponent's template.

Place the list ListErrors component into the AuthComponent's template

src/app/auth/auth.component.html



[...]

           <a [routerLink]="['/login']" *ngIf="authType == 'register'">Have an account?</a>
           <a [routerLink]="['/register']" *ngIf="authType == 'login'">Need an account?</a>
         </p>
+        <list-errors [errors]="errors"></list-errors>
         <form [formGroup]="authForm" (ngSubmit)="submitForm()">
           <fieldset [disabled]="isSubmitting">
             <fieldset class="form-group">


[...]

...and voila, it works!

You can compare your code to the working code on Github or checkout the branch locally:

git checkout m-6