Lab 7: Tying it all together with the Login Component
Hint: if you got lost, you can always check out the corresponding branch for the lab. If you want to start over, you can reset it by typing executing git reset --hard origin/lab-6
It's time for the final sprint! By now, we've successfully implemented quite a few components, added knobs to them to make them accessible to all team members, and even projected content through a container component. In this last lab, we'll work through mocking service calls and testing a composed component.
- Due to a Storybook bug in the version we are using, please adjust the
.storybook/tsconfig.json
file as follows:
{
"extends": "../tsconfig.app.json",
"compilerOptions": {
"types": [
"node"
],
+ "emitDecoratorMetadata": true
},
"exclude": [
"../src/test.ts",
"../src/**/*.spec.ts",
"../projects/**/*.spec.ts"
],
"include": [
"../src/**/*",
"../projects/**/*"
],
"files": [
"./typings.d.ts"
]
}
- adjust the input
Let’s add a possibility to read the text from our input boxes. Adjust the template (src/app/input/input.component.html
) as follows:
<label>{{ label }}</label>
+ <input [type]="type" [placeholder]="placeholder">
- <input #input [type]="type" [placeholder]="placeholder">
Then adjust the logic class (src/app/input/input.component.ts
) as follows:
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
@Component({
selector: 'app-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.scss']
})
export class InputComponent implements OnInit {
@Input() label: string;
@Input() placeholder: string;
@Input() type: 'text' | 'password';
constructor() { }
ngOnInit() {
}
+
+ @ViewChild('input', { static: false }) input: ElementRef<HTMLInputElement>;
+
+ get text(): string {
+ return this.input.nativeElement.value;
+ }
}
- generate the login module
ng g @ngx-storybook/schematics:c login
- add a login service
Run the following command to introduce a login service.
ng g service login/login
Adjust the implementation in src/app/login/login.service.ts
as follows:
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class LoginService {
login(username: string, password: string): Observable<boolean> {
return throwError('Not implemented.');
}
}
Currently, this implementation will always fail. This would be the right place to send an HTTP request.
- create a mock service
For our story, we don’t want to send actual HTTP requests to a login service. Instead, we’d like to provide a fake implementation. Let’s add this mock service next.
ng g service login/mocks/mock-login
Implement this service as follows (src/app/login/mocks/mock-login.service.ts
):
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class MockLoginService {
login(username: string, password: string): Observable<boolean> {
return of(!!username && username === password);
}
}
This very secure implementation of the LoginService: When a username is set and the username and password match, the login is successful. Great!
Hint: You could use an abstract class or an interface to ensure that both services have the same shape.
- build the login component
Let’s consume the LoginService
in the logic class of our login component (src/app/login/login.component.ts
) and provide a login
method for the template:
import { Component, OnInit } from '@angular/core';
import { LoginService } from './login.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
- constructor() {}
+ constructor(private readonly loginService: LoginService) {
+ }
+
+ login(username: string, password: string) {
+ this.loginService.login(username, password).subscribe(result => console.log(result));
+ }
ngOnInit() {
}
}
In the HTML template (src/app/login/login.component.html
), add the form group, one input for each username and password, and a confirm button.
<app-form-group [columns]="1" heading="Login">
<app-input #username label="Username" placeholder="jon.doe@gmail.com"></app-input>
<app-input #password label="Password" placeholder="*********" type="password"></app-input>
<app-button label="Submit" class="submit" (click)="login(username.text, password.text)"></app-button>
</app-form-group>
- include the dependencies for the login story
Next, implement the story. Instead of providing the actual implementation, pass the mock implementation to the component in the moduleMetadata
decorator.
import {moduleMetadata, storiesOf} from '@storybook/angular';
import {LoginComponent} from './login.component';
import {FormGroupModule} from '../form-group/form-group.module';
import {InputModule} from '../input/input.module';
import {ButtonModule} from '../button/button.module';
import {LoginService} from './login.service';
import {MockLoginService} from './mocks/mock-login.service';
storiesOf('Components | Login', module)
.addDecorator(moduleMetadata({
imports: [FormGroupModule, InputModule, ButtonModule],
providers: [{ provide: LoginService, useClass: MockLoginService }]
}))
.add('Default', () => ({
component: LoginComponent,
}));
Note: You may use the same mock implementation for unit tests.
- the login component should be visible in Storybook
- the login should fail if no username is given or username and password do not match (check the console in developer tools)
Hint: if you got lost, you can always check out the corresponding branch for the lab. need the solution, you can reset it by typing executing git reset --hard origin/lab-7