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.
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.
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.
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.
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.
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;
}
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
.
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.
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.
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.
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