Nous allons voir comment installer et configurer une application angular avec ngrx et bootstrap.
Configuration
Création du workspace angular
ng new workspace –create-application=false
Ajout de l’application
Dans le workspace, ajouter une application :
ng g app back-office.
Installation de ngrx
ng add @ngrx/store
Installation de ngrx
Ajout de bootstrap
ng add ngx-bootstrap
Création des différentes classes
Ajouter du reducer principal
import { ActionReducerMap } from "@ngrx/store";
export interface ApplicationState {
}
export const reducers: ActionReducerMap<ApplicationState> = {
}
Installation de ngrx effect
ng add @ngrx/effects
Puis ajoute le dans le module parent
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
StoreModule.forRoot({
}, {}),
BrowserAnimationsModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Création du module dédié à l’authentification
ng g m features/authentication
Ajout du composant de login
ng g c features/authentification/login
Puis en ajoutant le ReactiveFormModule dans le module d’authentification
@NgModule({
declarations: [
LoginComponent
],
imports: [
CommonModule,
ReactiveFormsModule,
]
})
export class AuthenticationModule { }
Configure le formulaire pour avoir un formGroup
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent {
loginForm = inject(FormBuilder).group({
username: ['', Validators.required],
password: ['', Validators.required]
});
}
Configuration des routes
Création du guard d’authentification, avec redirection
Ajoute un guard qui va contrôler l’authentification
ng g guard features/authentification/is-authenticated
Ajout de la route de login
Dans le module de routing pour l’authentication, ajoute la route pour le login
const routes: Routes = [{
path: 'login',
component: LoginComponent
}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthenticationRoutingModule { }
@NgModule({
declarations: [
LoginComponent
],
imports: [
CommonModule,
ReactiveFormsModule,
AuthenticationRoutingModule
]
})
export class AuthenticationModule { }
Ajout des routes enfants protégées par un guard
Dans le AppRoutingModule, ajoute les routes de protection avec le guard
const routes: Routes = [
{ path: '', redirectTo: 'games', pathMatch: 'full' },
{
path: '',
canActivate: [IsAuthenticatedGuard],
children: [
// on mettra les routes enfants protégées par login ici
]
}];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Paramétrage
Création du service d’authentication
Le guard va appeler notre service d’authentification
ng g s features/authentification/auth
Ajout des actions de ngrx
Pour que tout
Ajoutons un fichier dédié.
Mais avant ça, créons les interfaces nécessaires
export type ApiAuthUser = {
access_token: string,
}
export type ApiError = {
message: string,
statusCode: number
}
export interface ToLogUser {
username: string,
password: string
}
export interface WithToken {
token: string
}
export interface WithIsLogged {
isLogged: boolean
}
export interface AuthenticatedUser extends ToLogUser, WithIsLogged {
}
export interface AuthenticateStateWithToken extends AuthenticatedUser, WithToken {
}
export interface ToLogUserWithToken extends ToLogUser, WithToken {
}
Création du fichier des actions
import { createAction, props } from "@ngrx/store";
import { ApiError, AuthenticateStateWithToken, ToLogUser } from "../../models";
export const isLogginAction = createAction('[Auth] Is Loggin', props<{ user: ToLogUser }>());
export const isLogginSuccessAction = createAction('[Effect] Is Loggin', props<{ user: AuthenticateStateWithToken }>());
export const isLogginFailureAction = createAction('[Effect] Is Loggin failed', props<{ error: ApiError }>());
export const isLogoutAction = createAction('[Auth] Is Logout');
Paramétrage du reducer d’authentication
Dans notre reducer, nous allons gérer la réussite d’authentification
et les erreurs d’authentification
import { isLogginFailureAction } from './actions/index';
import { createReducer, on } from '@ngrx/store';
import { ApiError, AuthenticatedUser, AuthenticateStateWithToken } from '../models';
import { isLogginAction, isLogginSuccessAction } from './actions';
export const authenticationFeatureKey = 'authentication';
export interface AuthenticationState {
user?: AuthenticatedUser,
error?: ApiError
}
export const initialState: AuthenticationState = {
user: undefined
};
export const authenticationReducer = createReducer(
initialState,
on(isLogginSuccessAction, (state, { user }) => ({ ...state, user: { ...user, isLogged: true } })),
on(isLogginFailureAction, (state, { error }) => ({ ...state, user: { username: '', password: '', isLogged: false }, error }))
);
Mise à jour du Guard d’authentification
Prenons le soin de créer un service dédié d’appel à notre api (sans map, ou autre operator).
ng g s features/authentication/services/auth-layer
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiAuthUser, ToLogUser } from '../models';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class AuthLayerService {
private readonly httpClient = inject(HttpClient);
authenticate(user: ToLogUser): Observable<ApiAuthUser> {
return this.httpClient.post<ApiAuthUser>('http://localhost:3000/auth/login', {
username: user.username,
password: user.password
})
}
}
Pour utiliser notre Guard, nous allons mettre à jour notre service d’authentification
import { inject, Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { map, Observable } from 'rxjs';
import { ToLogUser, ToLogUserWithToken } from '../models';
import { isLogginAction } from '../store/actions';
import { selectUserAuthWithFailure, selectUserIsLogged } from '../store/selectors';
import { AuthLayerService } from './auth-layer.service';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private readonly store = inject(Store);
private readonly layer = inject(AuthLayerService);
selectAuthError(): Observable<any | undefined> {
return this.store.select(selectUserAuthWithFailure);
}
login(user: ToLogUser): void {
this.store.dispatch(isLogginAction({ user }));
}
authenticate(user: ToLogUser): Observable<ToLogUserWithToken> {
return this.layer.authenticate({
username: user.username,
password: user.password
}).pipe(
map(apiUser => ({
token: apiUser.access_token,
username: user.username,
password: user.password
})
));
}
get isLogged(): Observable<boolean | undefined> {
return this.store.select(selectUserIsLogged);
}
}
Il va être le proxy avec le store, et permettre l’appel à notre service d’appel à notre api d’authentication.
Nous pouvons appeler notre service dans notre Guard
import { inject, Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { map, Observable, take, tap } from 'rxjs';
import { AuthService } from '../services/auth.service';
@Injectable({
providedIn: 'root'
})
export class IsAuthenticatedGuard implements CanActivate {
private readonly authService = inject(AuthService);
private router = inject(Router);
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.authService.isLogged.pipe(
take(1),
map(isLogged => isLogged ? true : false),
tap(isLogged => !isLogged && this.router.navigate(['/login']))
);
}
}
Notons ici la redirection si nous ne sommes pas connectés.
Ajout d’un effect pour se connecter à notre api et gérer la redirection vers la page d’accueil
import { inject, Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { concatMap, map, tap, catchError } from 'rxjs/operators';
import { AuthService } from '../../services/auth.service';
import { isLogginAction, isLogginFailureAction, isLogginSuccessAction } from '../actions';
@Injectable()
export class AuthEffect {
private readonly authService = inject(AuthService);
private readonly actions$ = inject(Actions);
private readonly router = inject(Router);
onLoggedIn$ = createEffect(() => this.actions$.pipe(
ofType(isLogginAction),
concatMap(action => this.authService.authenticate(action.user)),
map(user => isLogginSuccessAction({ user: { ...user, isLogged: true } })),
catchError(error => {
return of(isLogginFailureAction({ error }));
})
));
onLoggedInSuccess$ = createEffect(() => this.actions$.pipe(
ofType(isLogginSuccessAction),
tap(action => this.router.navigate(['/'])
)),
{ dispatch: false }
);
}
Notons le dispatch à false dans le cas du deuxième effect, car nous ne renvoyons aucune action dans le pipe des actions.
Pour que l’effect soit pris en compte, pensez bien à l’ajouter dans le module d’authentication
import { ReactiveFormsModule } from '@angular/forms';
import { EffectsModule } from '@ngrx/effects';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StoreModule } from '@ngrx/store';
import { authenticationFeatureKey, authenticationReducer } from './store/store.reducer';
import { LoginComponent } from './login/login.component';
import { AuthenticationRoutingModule } from './authentication-routing.module';
import { AuthEffect } from './store/effects';
import { AlertModule } from 'ngx-bootstrap/alert';
@NgModule({
declarations: [
LoginComponent
],
imports: [
CommonModule,
ReactiveFormsModule,
StoreModule.forFeature(authenticationFeatureKey, authenticationReducer),
EffectsModule.forFeature([AuthEffect]),
AuthenticationRoutingModule,
AlertModule.forRoot()
]
})
export class AuthenticationModule { }
Ici, on peut aussi voir l’appel à l’AlertModule de bootstrap.
Paramétrage du composant de login
Pour se faire, nous allons avoir besoin de selectors, basés sur un selector feature (vu qu’on a pensé notre système sur du Feature Module / Feature Store).
import { createFeatureSelector, createSelector } from "@ngrx/store";
import { authenticationFeatureKey, AuthenticationState } from "../store.reducer";
export const selectAuthUserSelector = createFeatureSelector<AuthenticationState>(authenticationFeatureKey);
export const selectUserIsLogged = createSelector(selectAuthUserSelector, (state: AuthenticationState) => state.user?.isLogged);
export const selectUserAuthWithFailure = createSelector(selectAuthUserSelector, (state: AuthenticationState) => state.error);
Notons le dernier selector qui va afficher un message d’erreur quand l’effect va nous dispatcher l’action vers le reducer.
Côté template, nous avons un formGroup et une alert de type bootstrap qui va s’afficher si une erreur est survenue lors de l’authentification
<alert type="danger" *ngIf="errorOnLogin$ | async as error">
<strong>Oh snap!</strong> {{error.message}}
</alert>
<div [formGroup]="loginForm" id="login" class="text-center">
<form class="form-signin">
<img class="mb-4" src="https://getbootstrap.com/docs/4.0/assets/brand/bootstrap-solid.svg" alt="" width="72"
height="72">
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<label for="inputEmail" class="sr-only">Email address</label>
<input type="email" id="inputEmail" class="form-control" placeholder="Email address" formControlName="username"
autofocus>
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" id="inputPassword" class="form-control" placeholder="Password" formControlName="password">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"> Remember me
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="button" (click)="clickToLogin()">Sign in</button>
<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
</form>
</div>
Notons ici que nous utilisons les pipe async.
Côté component, nous récupérons les observables, sans passer par OnInit, ou bien le constructor, directement via la méthode inject.
import { HttpClient } from '@angular/common/http';
import { Component, inject } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { ToLogUser } from '../models';
import { AuthService } from '../services/auth.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent {
private readonly authService = inject(AuthService);
private readonly router = inject(Router);
loginForm = inject(FormBuilder).group({
username: ['', Validators.required],
password: ['', Validators.required]
});
errorOnLogin$ = this.authService.selectAuthError();
private client = inject(HttpClient);
clickToLogin(): void {
this.authService.login(this.loginForm.value as ToLogUser);
}
}
Et voila ! 🙂
Ca a éveillé ta curiosité ? ça te dit d’aller plus loin avec une formation Angular avancé ?