juin 10 2019

Angular 8 - Tutoriel et exemple sur l'enregistrement et l'authentification des utilisateurs

Tutoriel créé avec Angular 8.0.0 et Webpack 4.33

Dans ce tutoriel nous allons voir comment créer une application Angular simple qui prend en charge l'enregistrement et l'authentification des utilisateurs.

Le projet est disponible sur GitHub à https://github.com/cornflourblue/angular-8-registration-login-example

Le voici en action: (Le voir sur StackBlitz a https://stackblitz.com/edit/angular-8-registration-login-example)


Démarrer l'exemple de tutoriel Angular 8 sur un serveur web local

L'exemple de tutoriel utilise Webpack 4 pour transpiler le TypeScript et regrouper les modules Angular 8 ensemble. Le webpack-dev-server est utilisé pour le serveur web local, pour en savoir plus sur l'utilisation de Webpack avec TypeScript, vous pouvez consulter la documentation Webpack.

  1. Installez NodeJS et NPM à partir de https://nodejs.org/en/download/.
  2. Téléchargez ou clonez le code source du tutoriel à partir de https://github.com/cornflourblue/angular-8-registration-login-example.
  3. Installez les packages npm en exécutant la commande npm install dans le répertoire racine du projet où se trouve le package.json.
  4. Démarrez l'application en exécutant la commande npm start dans le répertoire racine du projet.
  5. Votre navigateur devrait s'ouvrir automatiquement à l'adresse http://localhost:8080 avec la page de connexion affichée.


Démarrer l'exemple de tutoriel avec une vraie API

Le tutoriel utilise une fausse API qui stocke les utilisateurs dans le stockage local HTML5, pour utiliser une vraie API supprimez simplement fakeBackendProvider du fichier app.module.ts situé sous le commentaire // provider used to create fake backend.

Vous pouvez créer votre propre API backend ou commencer avec l'une des options ci-dessous (ces tutoriels sont en anglais):


Structure du projet Angular 8

Le structure du projet et le style de code suit principalement les recommandations du guide de style Angular officiel (en anglais), avec quelques petits changements ici et là.

Le projet a un dossier pour chaque fonctionnalité (home, login et register), le code réutilisable et partagé est placé dans des dossiers précédés du caractère de soulignement "_" afin de différencier facilement le code partagé du code spécifique à une fonctionnalité.

Les fichiers index.ts dans chaque dossier regroupent les modules exportés d'un dossier ensemble afin qu'ils peuvent être importés avec le chemin de dossier au lieu du chemin de module complet et pour permettre l'importation de plusieurs modules en une seule importation. (e.g. import { AlertService, UserService, AuthenticationService } from '@/_services').

Un alias de chemin '@' a été configuré dans les fichiers tsconfig.json et webpack.config.js qui correspond au répertoire "/src/app". Cela permet aux importations d'être relatives au dossier '/src/app' en préfixant le chemin d'importation par '@', éliminant ainsi la nécessité d'utiliser des chemins relatifs longs comme import MyComponent from '../../../MyComponent'.

Voici la structure du projet du tutoriel:

 

Modèle de composant d'alerte

Path: /src/app/_components/alert.component.html

Le modèle de composant d'alerte contient le code HTML permettant d'afficher les messages d'alerte en haut de la page.

<div *ngIf="message" [ngClass]="message.cssClass">{{message.text}}</div>
 

Composant d'alerte

Path: /src/app/_components/alert.component.ts

Le composant d'alerte transmet les messages d'alerte au modèle chaque fois qu'un message est reçu du service d'alerte. Pour ce faire, il s'abonne à la méthode getMessage() du service d'alerte, qui renvoie un Observable.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';

import { AlertService } from '@/_services';

@Component({ selector: 'alert', templateUrl: 'alert.component.html' })
export class AlertComponent implements OnInit, OnDestroy {
    private subscription: Subscription;
    message: any;

    constructor(private alertService: AlertService) { }

    ngOnInit() {
        this.subscription = this.alertService.getAlert()
            .subscribe(message => {
                switch (message && message.type) {
                    case 'success':
                        message.cssClass = 'alert alert-success';
                        break;
                    case 'error':
                        message.cssClass = 'alert alert-danger';
                        break;
                }

                this.message = message;
            });
    }

    ngOnDestroy() {
        this.subscription.unsubscribe();
    }
}
 

Styles LESS/CSS globaux

Path: /src/app/_content/app.less

Styles LESS/CSS globaux pour styliser n'importe quelle section de l'application.

// global application styles
a {
    cursor: pointer;
}
 

Garde d'authentification

Path: /src/app/_helpers/auth.guard.ts

La garde d'authentification est utilisée pour empêcher les utilisateurs non authentifiés d'accéder aux itinéraires restreints. Dans cet exemple, elle est utilisée dans app.routing.ts pour protéger l'itinéraire de la page d'accueil.

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

import { AuthenticationService } from '@/_services';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
    constructor(
        private router: Router,
        private authenticationService: AuthenticationService
    ) {}

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const currentUser = this.authenticationService.currentUserValue;
        if (currentUser) {
            // authorised so return true
            return true;
        }

        // not logged in so redirect to login page with the return url
        this.router.navigate(['/login'], { queryParams: { returnUrl: state.url }});
        return false;
    }
}
 

Intercepteur d'erreur HTTP

Path: /src/app/_helpers/error.interceptor.ts

L'intercepteur d'erreur intercepte les réponses http de l'API pour vérifier s'il y a eu des erreurs. S'il y a une réponse 401 non autorisée, l'utilisateur est automatiquement déconnecté de l'application, toutes les autres erreurs sont renvoyées au service appelant afin qu'une alerte puisse être affichée à l'utilisateur.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { AuthenticationService } from '@/_services';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    constructor(private authenticationService: AuthenticationService) {}

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request).pipe(catchError(err => {
            if (err.status === 401) {
                // auto logout if 401 response returned from api
                this.authenticationService.logout();
                location.reload(true);
            }
            
            const error = err.error.message || err.statusText;
            return throwError(error);
        }))
    }
}
 

Faux fournisseur d'API backend

Path: /src/app/_helpers/fake-backend.ts

Le faux fournisseur d’API backend permet à l’exemple de s’exécuter sans backend / backendless, il utilise le stockage local HTML5 pour stocker les données d’utilisateur enregistrées et fournit de fausses implémentations pour les méthodes d’authentification et CRUD; celles-ci seraient gérées par une véritable API et une base de données dans une application de production.

Il est implémenté à l'aide de la classe HttpInterceptor introduite dans Angular 4.3 avec le nouveau HttpClientModule. En étendant la classe HttpInterceptor, vous pouvez créer un intercepteur personnalisé pour modifier les demandes http avant qu'elles ne soient envoyées au serveur. Dans ce cas, FakeBackendInterceptor intercepte certaines demandes en fonction de leur URL et fournit une fausse réponse au lieu d'aller au serveur.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators';

// array in local storage for registered users
let users = JSON.parse(localStorage.getItem('users')) || [];

@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const { url, method, headers, body } = request;

        // wrap in delayed observable to simulate server api call
        return of(null)
            .pipe(mergeMap(handleRoute))
            .pipe(materialize()) // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648)
            .pipe(delay(500))
            .pipe(dematerialize());

        function handleRoute() {
            switch (true) {
                case url.endsWith('/users/authenticate') && method === 'POST':
                    return authenticate();
                case url.endsWith('/users/register') && method === 'POST':
                    return register();
                case url.endsWith('/users') && method === 'GET':
                    return getUsers();
                case url.match(/\/users\/\d+$/) && method === 'DELETE':
                    return deleteUser();
                default:
                    // pass through any requests not handled above
                    return next.handle(request);
            }    
        }

        // route functions

        function authenticate() {
            const { username, password } = body;
            const user = users.find(x => x.username === username && x.password === password);
            if (!user) return error('Username or password is incorrect');
            return ok({
                id: user.id,
                username: user.username,
                firstName: user.firstName,
                lastName: user.lastName,
                token: 'fake-jwt-token'
            })
        }

        function register() {
            const user = body

            if (users.find(x => x.username === user.username)) {
                return error('Username "' + user.username + '" is already taken')
            }

            user.id = users.length ? Math.max(...users.map(x => x.id)) + 1 : 1;
            users.push(user);
            localStorage.setItem('users', JSON.stringify(users));

            return ok();
        }

        function getUsers() {
            if (!isLoggedIn()) return unauthorized();
            return ok(users);
        }

        function deleteUser() {
            if (!isLoggedIn()) return unauthorized();

            users = users.filter(x => x.id !== idFromUrl());
            localStorage.setItem('users', JSON.stringify(users));
            return ok();
        }

        // helper functions

        function ok(body?) {
            return of(new HttpResponse({ status: 200, body }))
        }

        function error(message) {
            return throwError({ error: { message } });
        }

        function unauthorized() {
            return throwError({ status: 401, error: { message: 'Unauthorised' } });
        }

        function isLoggedIn() {
            return headers.get('Authorization') === 'Bearer fake-jwt-token';
        }

        function idFromUrl() {
            const urlParts = url.split('/');
            return parseInt(urlParts[urlParts.length - 1]);
        }
    }
}

export const fakeBackendProvider = {
    // use fake backend in place of Http service for backend-less development
    provide: HTTP_INTERCEPTORS,
    useClass: FakeBackendInterceptor,
    multi: true
};
 

JWT Interceptor

Path: /src/app/_helpers/jwt.interceptor.ts

L'intercepteur JWT intercepte les demandes http de l'application pour ajouter un jeton d'authentification JWT à l'en-tête Authorization si l'utilisateur est connecté.

Il est implémenté à l'aide de la classe HttpInterceptor introduite dans Angular 4.3 avec le nouveau HttpClientModule. En étendant la classe HttpInterceptor, vous pouvez créer un intercepteur personnalisé pour modifier les demandes http avant qu'elles ne soient envoyées au serveur. Dans ce cas, FakeBackendInterceptor intercepte certaines demandes en fonction de leur URL et fournit une fausse réponse au lieu d'aller au serveur.

Les intercepteurs HTTP sont ajoutés au pipeline de demandes dans la section fournisseurs (providers) du fichier app.module.ts.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';

import { AuthenticationService } from '@/_services';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
    constructor(private authenticationService: AuthenticationService) {}

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // add authorization header with jwt token if available
        let currentUser = this.authenticationService.currentUserValue;
        if (currentUser && currentUser.token) {
            request = request.clone({
                setHeaders: { 
                    Authorization: `Bearer ${currentUser.token}`
                }
            });
        }

        return next.handle(request);
    }
}
 

Modèle d'utilisateur

Path: /src/app/_models/user.ts

Le modèle utilisateur est une petite classe qui définit les propriétés d'un utilisateur. La propriété token est utilisée pour contenir le jeton JWT renvoyé par l'API en cas d'authentification réussie.

export class User {
    id: number;
    username: string;
    password: string;
    firstName: string;
    lastName: string;
    token: string;
}
 

Service d'alerte

Path: /src/app/_services/alert.service.ts

Le service d'alerte permet à n'importe quel composant de l'application d'afficher des messages d'alerte en haut de la page via le composant d'alerte.

Il a des méthodes pour afficher les messages d'erreur et de succès, et a d'une méthode getMessage() qui renvoie un Observable utilisé par le composant d'alerte pour s'abonner aux notifications chaque fois qu'un message doit être affiché.

import { Injectable } from '@angular/core';
import { Router, NavigationStart } from '@angular/router';
import { Observable, Subject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class AlertService {
    private subject = new Subject<any>();
    private keepAfterRouteChange = false;

    constructor(private router: Router) {
        // clear alert messages on route change unless 'keepAfterRouteChange' flag is true
        this.router.events.subscribe(event => {
            if (event instanceof NavigationStart) {
                if (this.keepAfterRouteChange) {
                    // only keep for a single route change
                    this.keepAfterRouteChange = false;
                } else {
                    // clear alert message
                    this.clear();
                }
            }
        });
    }

    getAlert(): Observable<any> {
        return this.subject.asObservable();
    }

    success(message: string, keepAfterRouteChange = false) {
        this.keepAfterRouteChange = keepAfterRouteChange;
        this.subject.next({ type: 'success', text: message });
    }

    error(message: string, keepAfterRouteChange = false) {
        this.keepAfterRouteChange = keepAfterRouteChange;
        this.subject.next({ type: 'error', text: message });
    }

    clear() {
        // clear by calling subject.next() without parameters
        this.subject.next();
    }
}
 

Service d'authentification

Path: /src/app/_services/authentication.service.ts

Le service d'authentification est utilisé pour se connecter et se déconnecter de l'application, pour se connecter, il affiche les informations d'identification de l'utilisateur sur l'API et vérifie la réponse pour un jeton JWT. S'il en existe un, cela signifie que l'authentification a réussi et que les détails de l'utilisateur, y compris le jeton, sont ajoutés. au stockage local.

Les détails de l'utilisateur connecté sont stockés dans la mémoire de stockage locale. L'utilisateur reste donc connecté s'il actualise le navigateur et entre les sessions du navigateur jusqu'à sa déconnexion. Si vous ne souhaitez pas que l'utilisateur reste connecté entre les actualisations ou les sessions, son comportement peut facilement être modifié en stockant les détails de l'utilisateur dans un endroit moins persistant, tel que le stockage de session qui persisterait entre les actualisations mais pas dans les sessions du navigateur, ou dans une variable privée dans le répertoire. service d'authentification qui serait effacé lors de l'actualisation du navigateur.

Le service d'authentification présente deux propriétés permettant d'accéder à l'utilisateur actuellement connecté. L'observable currentUser peut être utilisé lorsque vous souhaitez qu'un composant se mette à jour de manière réactive lorsqu'un utilisateur se connecte ou se déconnecte, par exemple dans le fichier app.component.ts, afin qu'il puisse afficher / masquer la barre de navigation principale lorsque l'utilisateur se connecte / déconnecte. La propriété currentUserValue peut être utilisée lorsque vous souhaitez simplement obtenir la valeur actuelle de l'utilisateur connecté, mais que vous n'avez pas besoin de mettre à jour de manière réactive lorsqu'elle change, par exemple dans auth.guard.ts, qui restreint l'accès aux itinéraires en vérifiant si l'utilisateur est actuellement connecté.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { User } from '@/_models';

@Injectable({ providedIn: 'root' })
export class AuthenticationService {
    private currentUserSubject: BehaviorSubject<User>;
    public currentUser: Observable<User>;

    constructor(private http: HttpClient) {
        this.currentUserSubject = new BehaviorSubject<User>(JSON.parse(localStorage.getItem('currentUser')));
        this.currentUser = this.currentUserSubject.asObservable();
    }

    public get currentUserValue(): User {
        return this.currentUserSubject.value;
    }

    login(username, password) {
        return this.http.post<any>(`${config.apiUrl}/users/authenticate`, { username, password })
            .pipe(map(user => {
                // store user details and jwt token in local storage to keep user logged in between page refreshes
                localStorage.setItem('currentUser', JSON.stringify(user));
                this.currentUserSubject.next(user);
                return user;
            }));
    }

    logout() {
        // remove user from local storage and set current user to null
        localStorage.removeItem('currentUser');
        this.currentUserSubject.next(null);
    }
}
 

Service utilisateur

Path: /src/app/_services/user.service.ts

Le service utilisateur contient un ensemble standard de méthodes CRUD pour la gestion des utilisateurs. Il sert d'interface entre l'application Angular et l'API backend.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { User } from '@/_models';

@Injectable({ providedIn: 'root' })
export class UserService {
    constructor(private http: HttpClient) { }

    getAll() {
        return this.http.get<User[]>(`${config.apiUrl}/users`);
    }

    register(user: User) {
        return this.http.post(`${config.apiUrl}/users/register`, user);
    }

    delete(id: number) {
        return this.http.delete(`${config.apiUrl}/users/${id}`);
    }
}
 

Modèle de composant d'accueil

Path: /src/app/home/home.component.html

Le modèle de composant d'accueil contient le HTML et le syntax Angular 8 permettant d'afficher un message de bienvenue simple et une liste d'utilisateurs avec un lien permettant de supprimer tout utilisateur.

<h1>Hi {{currentUser.firstName}}!</h1>
<p>You're logged in with Angular 8!!</p>
<h3>All registered users:</h3>
<ul>
    <li *ngFor="let user of users">
        {{user.username}} ({{user.firstName}} {{user.lastName}})
        - <a (click)="deleteUser(user.id)" class="text-danger">Delete</a>
    </li>
</ul>
 

Composant d'accueil

Path: /src/app/home/home.component.ts

Le composant home contient la logique permettant d'afficher l'utilisateur actuel, d'afficher une liste de tous les utilisateurs et de supprimer des utilisateurs.

import { Component, OnInit } from '@angular/core';
import { first } from 'rxjs/operators';

import { User } from '@/_models';
import { UserService, AuthenticationService } from '@/_services';

@Component({ templateUrl: 'home.component.html' })
export class HomeComponent implements OnInit {
    currentUser: User;
    users = [];

    constructor(
        private authenticationService: AuthenticationService,
        private userService: UserService
    ) {
        this.currentUser = this.authenticationService.currentUserValue;
    }

    ngOnInit() {
        this.loadAllUsers();
    }

    deleteUser(id: number) {
        this.userService.delete(id)
            .pipe(first())
            .subscribe(() => this.loadAllUsers());
    }

    private loadAllUsers() {
        this.userService.getAll()
            .pipe(first())
            .subscribe(users => this.users = users);
    }
}
 

Modèle de composant de connexion

Path: /src/app/login/login.component.html

Le modèle de composant de connexion contient un formulaire de connexion avec les champs nom d'utilisateur et mot de passe. Il affiche des messages de validation pour les champs non valides lorsque le bouton de soumission est cliqué. L'événement submit submit est lié à la méthode onSubmit() du composant de connexion.

<h2>Login</h2>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
    <div class="form-group">
        <label for="username">Username</label>
        <input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.username.errors }" />
        <div *ngIf="submitted && f.username.errors" class="invalid-feedback">
            <div *ngIf="f.username.errors.required">Username is required</div>
        </div>
    </div>
    <div class="form-group">
        <label for="password">Password</label>
        <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
        <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
            <div *ngIf="f.password.errors.required">Password is required</div>
        </div>
    </div>
    <div class="form-group">
        <button [disabled]="loading" class="btn btn-primary">
            <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
            Login
        </button>
        <a routerLink="/register" class="btn btn-link">Register</a>
    </div>
</form>
 

Composant de connexion

Path: /src/app/login/login.component.ts

Le composant de connexion utilise le service d'authentification pour se connecter à l'application. Si l'utilisateur est déjà connecté, il est automatiquement redirigé vers la page d'accueil.

L'objet loginForm: FormGroup définit les contrôles et les validateurs du formulaire et est utilisé pour accéder aux données entrées dans le formulaire. FormGroup fait partie du module Angular Reactive Forms et est lié au modèle de connexion ci-dessus avec la directive [formGroup]="loginForm".

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';

import { AlertService, AuthenticationService } from '@/_services';

@Component({ templateUrl: 'login.component.html' })
export class LoginComponent implements OnInit {
    loginForm: FormGroup;
    loading = false;
    submitted = false;
    returnUrl: string;

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private authenticationService: AuthenticationService,
        private alertService: AlertService
    ) {
        // redirect to home if already logged in
        if (this.authenticationService.currentUserValue) {
            this.router.navigate(['/']);
        }
    }

    ngOnInit() {
        this.loginForm = this.formBuilder.group({
            username: ['', Validators.required],
            password: ['', Validators.required]
        });

        // get return url from route parameters or default to '/'
        this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
    }

    // convenience getter for easy access to form fields
    get f() { return this.loginForm.controls; }

    onSubmit() {
        this.submitted = true;

        // reset alerts on submit
        this.alertService.clear();

        // stop here if form is invalid
        if (this.loginForm.invalid) {
            return;
        }

        this.loading = true;
        this.authenticationService.login(this.f.username.value, this.f.password.value)
            .pipe(first())
            .subscribe(
                data => {
                    this.router.navigate([this.returnUrl]);
                },
                error => {
                    this.alertService.error(error);
                    this.loading = false;
                });
    }
}
 

Modèle de composant d'enregistrement

Path: /src/app/register/register.component.html

Le modèle de composant d'enregistrement contient un formulaire d'inscription simple avec des champs pour le nom, le nom, le nom d'utilisateur et le mot de passe. Il affiche des messages de validation pour les champs non valides lorsque le bouton de soumission est cliqué. L'événement submit du formulaire est lié à la méthode onSubmit() du composant register.

<h2>Register</h2>
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
    <div class="form-group">
        <label for="firstName">First Name</label>
        <input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.firstName.errors }" />
        <div *ngIf="submitted && f.firstName.errors" class="invalid-feedback">
            <div *ngIf="f.firstName.errors.required">First Name is required</div>
        </div>
    </div>
    <div class="form-group">
        <label for="lastName">Last Name</label>
        <input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.lastName.errors }" />
        <div *ngIf="submitted && f.lastName.errors" class="invalid-feedback">
            <div *ngIf="f.lastName.errors.required">Last Name is required</div>
        </div>
    </div>
    <div class="form-group">
        <label for="username">Username</label>
        <input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.username.errors }" />
        <div *ngIf="submitted && f.username.errors" class="invalid-feedback">
            <div *ngIf="f.username.errors.required">Username is required</div>
        </div>
    </div>
    <div class="form-group">
        <label for="password">Password</label>
        <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
        <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
            <div *ngIf="f.password.errors.required">Password is required</div>
            <div *ngIf="f.password.errors.minlength">Password must be at least 6 characters</div>
        </div>
    </div>
    <div class="form-group">
        <button [disabled]="loading" class="btn btn-primary">
            <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
            Register
        </button>
        <a routerLink="/login" class="btn btn-link">Cancel</a>
    </div>
</form>
 

Composant d'enregistrement

Path: /src/app/register/register.component.ts

Le composant d'enregistrement crée un nouvel utilisateur avec le service utilisateur lors de l'envoi du formulaire de registre. Si l'utilisateur est déjà connecté, il est automatiquement redirigé vers la page d'accueil.

L'objet registerForm: FormGroup définit les contrôles de formulaire et les validateurs. Il est utilisé pour accéder aux données entrées dans le formulaire. FormGroup fait partie du module Angular Reactive Forms et est lié au modèle de connexion ci-dessus avec la directive [formGroup]="registerForm".

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';

import { AlertService, UserService, AuthenticationService } from '@/_services';

@Component({ templateUrl: 'register.component.html' })
export class RegisterComponent implements OnInit {
    registerForm: FormGroup;
    loading = false;
    submitted = false;

    constructor(
        private formBuilder: FormBuilder,
        private router: Router,
        private authenticationService: AuthenticationService,
        private userService: UserService,
        private alertService: AlertService
    ) {
        // redirect to home if already logged in
        if (this.authenticationService.currentUserValue) {
            this.router.navigate(['/']);
        }
    }

    ngOnInit() {
        this.registerForm = this.formBuilder.group({
            firstName: ['', Validators.required],
            lastName: ['', Validators.required],
            username: ['', Validators.required],
            password: ['', [Validators.required, Validators.minLength(6)]]
        });
    }

    // convenience getter for easy access to form fields
    get f() { return this.registerForm.controls; }

    onSubmit() {
        this.submitted = true;

        // reset alerts on submit
        this.alertService.clear();

        // stop here if form is invalid
        if (this.registerForm.invalid) {
            return;
        }

        this.loading = true;
        this.userService.register(this.registerForm.value)
            .pipe(first())
            .subscribe(
                data => {
                    this.alertService.success('Registration successful', true);
                    this.router.navigate(['/login']);
                },
                error => {
                    this.alertService.error(error);
                    this.loading = false;
                });
    }
}
 

Modèle de composant d'application

Path: /src/app/app.component.html

Le modèle de composant d'application est le modèle de composant racine de l'application. Il contient la barre de navigation principale qui s'affiche uniquement pour les utilisateurs authentifiés, une directive router-outlet pour afficher le contenu de chaque vue en fonction de l'itinéraire actuel et un composant d'alerte pour l'affichage des messages d'alerte de n'importe où dans l'application.

<!-- nav -->
<nav class="navbar navbar-expand navbar-dark bg-dark" *ngIf="currentUser">
    <div class="navbar-nav">
        <a class="nav-item nav-link" routerLink="/">Home</a>
        <a class="nav-item nav-link" (click)="logout()">Logout</a>
    </div>
</nav>

<!-- main app container -->
<div class="jumbotron">
    <div class="container">
        <div class="row">
            <div class="col-sm-6 offset-sm-3">
                <alert></alert>
                <router-outlet></router-outlet>
            </div>
        </div>
    </div>
</div>
 

Composant d'application

Path: /src/app/app.component.ts

Le composant app est le composant racine de l'application, il définit la balise racine de l'application comme <app></app> avec la propriété selector du décorateur @Component.

Il s'abonne à la variable currentUser observable dans le service d'authentification afin de pouvoir afficher / masquer de manière réactive la barre de navigation principale lorsque l'utilisateur se connecte / déconnecte de l'application.

Le composant d'application contient une méthode logout() qui est appelée à partir du lien de déconnexion situé dans la barre de navigation principale ci-dessus pour déconnecter l'utilisateur et le rediriger vers la page de connexion.

import { Component } from '@angular/core';
import { Router } from '@angular/router';

import { AuthenticationService } from './_services';
import { User } from './_models';

import './_content/app.less';

@Component({ selector: 'app', templateUrl: 'app.component.html' })
export class AppComponent {
    currentUser: User;

    constructor(
        private router: Router,
        private authenticationService: AuthenticationService
    ) {
        this.authenticationService.currentUser.subscribe(x => this.currentUser = x);
    }

    logout() {
        this.authenticationService.logout();
        this.router.navigate(['/login']);
    }
}
 

Module d'application

Path: /src/app/app.module.ts

Le module d'application définit le module racine de l'application ainsi que les métadonnées relatives au module. Pour plus d'informations sur les modules angulaires, consultez cette page sur le site officiel de la documentation.

C’est là que le faux fournisseur d’API backend est ajouté à l’application. Pour passer à une véritable API, supprimez simplement les fournisseurs situés en dessous du commentaire // providers used to create fake backend.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

// used to create fake backend
import { fakeBackendProvider } from './_helpers';

import { appRoutingModule } from './app.routing';
import { JwtInterceptor, ErrorInterceptor } from './_helpers';
import { AppComponent } from './app.component';
import { HomeComponent } from './home';
import { LoginComponent } from './login';
import { RegisterComponent } from './register';
import { AlertComponent } from './_components';

@NgModule({
    imports: [
        BrowserModule,
        ReactiveFormsModule,
        HttpClientModule,
        appRoutingModule
    ],
    declarations: [
        AppComponent,
        HomeComponent,
        LoginComponent,
        RegisterComponent,
        AlertComponent
    ],
    providers: [
        { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
        { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },

        // provider used to create fake backend
        fakeBackendProvider
    ],
    bootstrap: [AppComponent]
})
export class AppModule { };
 

Module de routage des applications

Path: /src/app/app.routing.ts

Le routage pour l'application Angular est configuré comme un tableau de Routes, chaque composant est mappé sur un chemin afin que le routeur angulaire sache quel composant afficher en fonction de l'URL dans la barre d'adresse du navigateur. La route de la page d'accueil est sécurisée en passant AuthGuard à la propriété canActivate de la route.

Le tableau Routes est transmis à la méthode RouterModule.forRoot() qui crée un module de routage avec toutes les routes d'application configurées et inclut également tous les fournisseurs de routeurs angulaires et les directives telles que <router-outlet></router-outlet> directive. Pour plus d'informations sur le routage et la navigation angulaires, voir https://angular.io/guide/router.

import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home';
import { LoginComponent } from './login';
import { RegisterComponent } from './register';
import { AuthGuard } from './_helpers';

const routes: Routes = [
    { path: '', component: HomeComponent, canActivate: [AuthGuard] },
    { path: 'login', component: LoginComponent },
    { path: 'register', component: RegisterComponent },

    // otherwise redirect to home
    { path: '**', redirectTo: '' }
];

export const appRoutingModule = RouterModule.forRoot(routes);
 

Fichier index.html principal

Path: /src/index.html

Le fichier index.html principal est la page initiale chargée par le navigateur qui lance tout. Webpack regroupe tous les fichiers javascript et les insère dans le corps de la page index.html afin que les scripts soient chargés et exécutés par le navigateur.

<!DOCTYPE html>
<html>
<head>
    <base href="/" />
    <title>Angular 8 User Registration and Login Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- bootstrap css -->
    <link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <app>Loading...</app>
</body>
</html>
 

Fichier main.ts

Path: /src/main.ts

Le fichier main.ts est le point d'entrée utilisé par angular pour lancer et initialiser (bootstrap) l'application.

import './polyfills';

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule);
 

Polyfills

Path: /src/polyfills.ts

Certaines fonctionnalités utilisées par Angular 8 ne sont pas encore prises en charge de manière native par tous les principaux navigateurs. Les polyfills sont utilisées pour ajouter la prise en charge de ces fonctionnalités afin que votre application Angular 8 fonctionne avec tous les principaux navigateurs.

import 'core-js/features/reflect';
import 'zone.js/dist/zone';
 

Fichier typings personnalisé

Path: /src/typings.d.ts

Un fichier typings personnalisé est utilisé pour déclarer les types créés en dehors de votre application Angular afin que le compilateur TypeScript ne génère pas d'erreurs sur les types inconnus. Ce fichier typings contient une déclaration pour l'objet de config global créé par webpack (voir webpack.config.js ci-dessous).

// so the typescript compiler doesn't complain about the global config object
declare var config: any;
 

npm package.json

Path: /package.json

Le fichier package.json contient des informations sur la configuration du projet, y compris les dépendances installées lors de l'exécution de npm install. La documentation complète est disponible sur le site web de npm docs.

{
    "name": "angular-8-registration-login-example",
    "version": "1.0.0",
    "repository": {
        "type": "git",
        "url": "https://github.com/cornflourblue/angular-8-registration-login-example.git"
    },
    "scripts": {
        "build": "webpack --mode production",
        "start": "webpack-dev-server --mode development --open"
    },
    "license": "MIT",
    "dependencies": {
        "@angular/common": "^8.0.0",
        "@angular/compiler": "^8.0.0",
        "@angular/core": "^8.0.0",
        "@angular/forms": "^8.0.0",
        "@angular/platform-browser": "^8.0.0",
        "@angular/platform-browser-dynamic": "^8.0.0",
        "@angular/router": "^8.0.0",
        "core-js": "^3.1.3",
        "rxjs": "^6.3.3",
        "zone.js": "^0.9.1"
    },
    "devDependencies": {
        "@types/node": "^12.0.7",
        "angular2-template-loader": "^0.6.2",
        "css-loader": "^2.1.1",
        "html-loader": "^0.5.5",
        "html-webpack-plugin": "^3.2.0",
        "less": "^3.0.4",
        "less-loader": "^5.0.0",
        "style-loader": "^0.23.1",
        "ts-loader": "^6.0.1",
        "typescript": "^3.1.3",
        "webpack": "^4.32.2",
        "webpack-cli": "^3.1.2",
        "webpack-dev-server": "^3.7.0"
    }
}
 

TypeScript tsconfig.json

Path: /tsconfig.json

Le fichier tsconfig.json configure la manière dont le compilateur TypeScript convertit TypeScript en JavaScript afin qu'il puisse s'exécuter dans le navigateur. Plus d'informations sont disponibles sur le TypeScript docs.

{
    "compilerOptions": {
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "target": "ES5",
        "baseUrl": "src",
        "paths": {
            "@/*": [
                "app/*"
            ]
        }
    }
}
 

Webpack 4 Config

Path: /webpack.config.js

Webpack 4 est utilisé pour compiler et regrouper tous les fichiers du projet afin qu'ils soient prêts à être chargés dans un navigateur. Pour ce faire, il utilise des chargeurs et des plug-ins configurés dans le fichier webpack.config.js. Pour plus d'informations sur Webpack, consultez la documentation Webpack.

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

module.exports = {
    entry: './src/main.ts',
    resolve: {
        extensions: ['.ts', '.js'],
        alias: {
            '@': path.resolve(__dirname, 'src/app/'),
        }
    },
    module: {
        rules: [
            {
                test: /\.ts$/,
                use: ['ts-loader', 'angular2-template-loader']
            },
            {
                test: /\.html$/,
                use: 'html-loader'
            },
            {
                test: /\.less$/,
                use: ['style-loader', 'css-loader', 'less-loader']
            },

            // workaround for warning: System.import() is deprecated and will be removed soon. Use import() instead.
            {
                test: /[\/\\]@angular[\/\\].+\.js$/,
                parser: { system: true }
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({ template: './src/index.html' }),
        new webpack.DefinePlugin({
            // global app config object
            config: JSON.stringify({
                apiUrl: 'http://localhost:4000'
            })
        }),

        // workaround for warning: Critical dependency: the request of a dependency is an expression
        new webpack.ContextReplacementPlugin(
            /\@angular(\\|\/)core(\\|\/)fesm5/,
            path.resolve(__dirname, 'src')
        )
    ],
    optimization: {
        splitChunks: {
            chunks: 'all',
        },
        runtimeChunk: true
    },
    devServer: {
        historyApiFallback: true
    }
}

 

Abonnez-vous ou suivez-moi pour les mises à jour

Abonnez-vous à ma chaîne YouTube ou suivez-moi sur Twitter ou sur GitHub pour savoir quand je publie un nouveau contenu.

 


Supporté par