Skip to content

Lab 7: Tying it all together with the Login Component

Christian Liebel edited this page Jul 31, 2019 · 8 revisions

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.

Labs

  • 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.

Self-check

  • 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