Pour cette section, vous allez créer une nouvelle application et appliquer des connaissances que vous avez apprises tout au long du cours Angular, ainsi que quelques fonctionnalités que vous n'avez pas encore rencontrées. Vous allez créer une application simple qui recense les livres que vous avez chez vous, dans votre bibliothèque. Vous pourrez ajouter une photo de chaque livre. L'utilisateur devra être authentifié pour utiliser l'application.
Pensez à la structure de l'application
Prenez le temps de réfléchir à la construction de l'application. Quels seront les components dont vous aurez besoin ? Les services ? Les modèles de données ?
L'application nécessite l'authentification. Il faudra donc un component pour la création d'un nouvel utilisateur, et un autre pour s'authentifier, avec un service gérant les interactions avec le backend.
Les livres pourront être consultés sous forme d'une liste complète, puis individuellement. Il faut également pouvoir ajouter et supprimer des livres. Il faudra donc un component pour la liste complète, un autre pour la vue individuelle et un dernier comportant un formulaire pour la création/modification. Il faudra un service pour gérer toutes les fonctionnalités liées à ces components, y compris les interactions avec le serveur.
Vous créerez également un component séparé pour la barre de navigation afin d'y intégrer une logique séparée.
Pour les modèles de données, il y aura un modèle pour les livres, comportant simplement le titre, le nom de l'auteur et la photo, qui sera facultative.
Il faudra également ajouter du routing à cette application, permettant l'accès aux différentes parties, avec une guard pour toutes les routes sauf l'authentification, empêchant les utilisateurs non authentifiés d'accéder à la bibliothèque.
Allez, c'est parti !
Structurez l'application
Pour cette application, je vous conseille d'utiliser le CLI pour la création des components. L'arborescence sera la suivante :
ng g c auth/signup ng g c auth/signin ng g c book-list ng g c book-list/single-book ng g c book-list/book-form ng g c header ng g s services/auth ng g s services/books ng g s services/auth-guard
Les services ainsi créés ne sont pas automatiquement mis dans l'array providers
d'AppModule
, donc ajoutez-les maintenant. Pendant que vous travaillez sur AppModule
, ajoutez également FormsModule
, ReactiveFormsModule
et HttpClientModule
:
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
HttpClientModule
],
providers: [AuthService, BooksService, AuthGuardService],
Intégrez dès maintenant le routing sans guard afin de pouvoir accéder à toutes les sections de l'application pendant le développement :
const appRoutes: Routes = [
{ path: 'auth/signup', component: SignupComponent },
{ path: 'auth/signin', component: SigninComponent },
{ path: 'books', component: BookListComponent },
{ path: 'books/new', component: BookFormComponent },
{ path: 'books/view/:id', component: SingleBookComponent }
];
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
HttpClientModule,
RouterModule.forRoot(appRoutes)
],
Générez également un dossier appelé models
et créez-y le fichier book.model.ts
:
export class Book {
photo: string;
synopsis: string;
constructor(public title: string, public author: string) {
}
}
Avant de lancer ng serve
, utilisez NPM pour ajouter Bootstrap à votre projet, et ajoutez-le à l'array styles de .angular-cli.json
:
npm install bootstrap@3.3.7 --save
"styles": [
"../node_modules/bootstrap/dist/css/bootstrap.css",
"styles.css"
],
Enfin, préparez HeaderComponent
avec un menu de navigation, avec les routerLink
et AppComponent qui l'intègre avec le router-outlet
:
class="navbar navbar-default"
class="container-fluid"
class="nav navbar-nav"
routerLinkActive="active"
routerLink="books"Livres
class="nav navbar-nav navbar-right"
routerLinkActive="active"
routerLink="auth/signup"Créer un compte
routerLinkActive="active"
routerLink="auth/signin"Connexion
class="container"
La structure globale de l'application est maintenant prête !
Intégrez Firebase à votre application
D'abord, installez Firebase avec NPM :
npm install firebase --save
Pour cette application, vous allez créer un nouveau projet sur Firebase. Une fois l'application créée, la console Firebase vous propose le choix suivant (sous la rubrique Overview) :
Choisissez "Ajouter Firebase à votre application Web" et copiez-collez la configuration dans le constructeur de votre AppComponent
(en ajoutant import * as firebase from 'firebase';
en haut du fichier, mettant à disposition la méthode initializeApp()
) :
import { Component } from '@angular/core';
import * as firebase from 'firebase';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor() {
const config = {
apiKey: 'AIzaSyCwfa_fKNCVrDMR1E88S79mpQP-6qertew4',
authDomain: 'bookshelves-3d570.firebaseapp.com',
databaseURL: 'https://bookshelves-3d570.firebaseio.com',
projectId: 'bookshelves-3d570',
storageBucket: 'bookshelves-3d570.appspot.com',
messagingSenderId: '6634573823'
};
firebase.initializeApp(config);
}
}
Votre application Angular est maintenant liée à votre projet Firebase, et vous pourrez maintenant intégrer tous les services dont vous aurez besoin.
Authentification
Votre application utilisera l'authentification par adresse mail et mot de passe proposée par Firebase. Pour cela, il faut d'abord l'activer dans la console Firebase :
Dans AuthService
, vous allez créer trois méthodes :
une méthode permettant de créer un nouvel utilisateur ;
une méthode permettant de connecter un utilisateur existant ;
une méthode permettant la déconnexion de l'utilisateur.
Puisque les opérations de création, de connexion et de déconnexion sont asynchrones, c'est-à-dire qu'elles n'ont pas un résultat instantané, les méthodes que vous allez créer pour les gérer retourneront des Promise, ce qui permettra également de gérer les situations d'erreur.
Importez Firebase dans AuthService
:
import { Injectable } from '@angular/core';
import * as firebase from 'firebase';
@Injectable()
export class AuthService {
Ensuite, créez la méthode createNewUser()
pour créer un nouvel utilisateur, qui prendra comme argument une adresse mail et un mot de passe, et qui retournera une Promise qui résoudra si la création réussit, et sera rejetée avec le message d'erreur si elle ne réussit pas :
createNewUser(email: string, password: string) {
return new Promise(
(resolve, reject) => {
firebase.auth().createUserWithEmailAndPassword(email, password).then(
() => {
resolve();
},
(error) => {
reject(error);
}
);
}
);
}
Créez également signInUser()
, méthode très similaire, qui s'occupera de connecter un utilisateur déjà existant :
signInUser(email: string, password: string) {
return new Promise(
(resolve, reject) => {
firebase.auth().signInWithEmailAndPassword(email, password).then(
() => {
resolve();
},
(error) => {
reject(error);
}
);
}
);
}
Créez une méthode simple signOutUser()
:
signOutUser() {
firebase.auth().signOut();
}
Ainsi, vous avez les trois fonctions dont vous avez besoin pour intégrer l'authentification dans l'application !
Vous pouvez ainsi créer SignupComponent
et SigninComponent
, intégrer l'authentification dans HeaderComponent
afin de montrer les bons liens, et implémenter AuthGuard
pour protéger la route /books
et toutes ses sous-routes.
Commencez par SignupComponent
afin de pouvoir enregistrer un utilisateur :
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService } from '../../services/auth.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-signup',
templateUrl: './signup.component.html',
styleUrls: ['./signup.component.css']
})
export class SignupComponent implements OnInit {
signupForm: FormGroup;
errorMessage: string;
constructor(private formBuilder: FormBuilder,
private authService: AuthService,
private router: Router) { }
ngOnInit() {
this.initForm();
}
initForm() {
this.signupForm = this.formBuilder.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.pattern(/[0-9a-zA-Z]{6,}/)]]
});
}
onSubmit() {
const email = this.signupForm.get('email').value;
const password = this.signupForm.get('password').value;
this.authService.createNewUser(email, password).then(
() => {
this.router.navigate(['/books']);
},
(error) => {
this.errorMessage = error;
}
);
}
}
Dans ce component :
vous générez le formulaire selon la méthode réactive
les deux champs,
email
etpassword
, sont requis — le champemail
utiliseValidators.email
pour obliger un string sous format d'adresse email ; le champpassword
emploieValidators.pattern
pour obliger au moins 6 caractères alphanumériques, ce qui correspond au minimum requis par Firebase ;
vous gérez la soumission du formulaire, envoyant les valeurs rentrées par l'utilisateur à la méthode
createNewUser()
si la création fonctionne, vous redirigez l'utilisateur vers
/books
;si elle ne fonctionne pas, vous affichez le message d'erreur renvoyé par Firebase.
Ci-dessous, vous trouverez le template correspondant :
class="row"
class="col-sm-8 col-sm-offset-2"
Créer un compte
[formGroup]="signupForm" (ngSubmit)="onSubmit()"
class="form-group"
for="email"Adresse mail
type="text"
id="email"
class="form-control"
formControlName="email"
class="form-group"
for="password"Mot de passe
type="password"
id="password"
class="form-control"
formControlName="password"
class="btn btn-primary"
type="submit"
[disabled]="signupForm.invalid"Créer mon compte
class="text-danger"{{ errorMessage }}
Il s'agit d'un formulaire selon la méthode réactive comme vous l'avez vu dans le chapitre correspondant. Il y a, en supplément, le paragraphe contenant l'éventuel message d'erreur rendu par Firebase.
Vous pouvez créer un template presque identique pour SignInComponent
pour la connexion d'un utilisateur déjà existant. Il vous suffit de renommer signupForm
en signinForm
et d'appeler la méthode signInUser()
plutôt que createNewUser()
.
Ensuite, vous allez modifier HeaderComponent
pour afficher de manière contextuelle les liens de connexion, de création d'utilisateur et de déconnexion :
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../services/auth.service';
import * as firebase from 'firebase';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {
isAuth: boolean;
constructor(private authService: AuthService) { }
ngOnInit() {
firebase.auth().onAuthStateChanged(
(user) => {
if(user) {
this.isAuth = true;
} else {
this.isAuth = false;
}
}
);
}
onSignOut() {
this.authService.signOutUser();
}
}
class="navbar navbar-default"
class="container-fluid"
class="nav navbar-nav"
routerLinkActive="active"
routerLink="books"Livres
class="nav navbar-nav navbar-right"
routerLinkActive="active" *ngIf="!isAuth"
routerLink="auth/signup"Créer un compte
routerLinkActive="active" *ngIf="!isAuth"
routerLink="auth/signin"Connexion
(click)="onSignOut()"
style="cursor:pointer"
*ngIf="isAuth"Déconnexion
Ici, vous utilisez onAuthStateChanged()
, qui permet d'observer l'état de l'authentification de l'utilisateur : à chaque changement d'état, la fonction que vous passez en argument est exécutée. Si l'utilisateur est bien authentifié, onAuthStateChanged()
reçoit l'objet de type firebase.User
correspondant à l'utilisateur. Vous pouvez ainsi baser la valeur de la variable locale isAuth
selon l'état d'authentification de l'utilisateur, et afficher les liens correspondant à cet état.
Il ne vous reste plus qu'à créer AuthGuardService
et l'appliquer aux routes concernées. Puisque la vérification de l'authentification est asynchrone, votre service retournera une Promise :
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import * as firebase from 'firebase';
@Injectable()
export class AuthGuardService implements CanActivate {
constructor(private router: Router) { }
canActivate(): Observable<boolean> | Promise<boolean> | boolean {
return new Promise(
(resolve, reject) => {
firebase.auth().onAuthStateChanged(
(user) => {
if(user) {
resolve(true);
} else {
this.router.navigate(['/auth', 'signin']);
resolve(false);
}
}
);
}
);
}
}
const appRoutes: Routes = [
{ path: 'auth/signup', component: SignupComponent },
{ path: 'auth/signin', component: SigninComponent },
{ path: 'books', canActivate: [AuthGuardService], component: BookListComponent },
{ path: 'books/new', canActivate: [AuthGuardService], component: BookFormComponent },
{ path: 'books/view/:id', canActivate: [AuthGuardService], component: SingleBookComponent }
];
Ah, mais qu'a-t-on oublié ? Le routing ne prend en compte ni le path vide, ni le path wildcard ! Ajoutez ces routes dès maintenant pour éviter toute erreur :
const appRoutes: Routes = [
{ path: 'auth/signup', component: SignupComponent },
{ path: 'auth/signin', component: SigninComponent },
{ path: 'books', canActivate: [AuthGuardService], component: BookListComponent },
{ path: 'books/new', canActivate: [AuthGuardService], component: BookFormComponent },
{ path: 'books/view/:id', canActivate: [AuthGuardService], component: SingleBookComponent },
{ path: '', redirectTo: 'books', pathMatch: 'full' },
{ path: '**', redirectTo: 'books' }
];
Ainsi, votre application comporte un système d'authentification complet, permettant l'inscription et la connexion/déconnexion des utilisateurs, et qui protège les routes concernées. Vous pouvez maintenant ajouter les fonctionnalités à votre application en sachant que les accès à la base de données et au stockage, qui nécessitent l'authentification, fonctionneront correctement.
Base de données
Dans ce chapitre, vous allez créer les fonctionnalités de l'application : la création, la visualisation et la suppression des livres, le tout lié directement à la base de données Firebase.
Pour créer BooksService
:
vous aurez un array local
books
et un Subject pour l'émettre ;vous aurez des méthodes :
pour enregistrer la liste des livres sur le serveur,
pour récupérer la liste des livres depuis le serveur,
pour récupérer un seul livre,
pour créer un nouveau livre,
pour supprimer un livre existant.
Pour la première étape, rien de nouveau (sans oublier d'importer Book et Subject) :
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Book } from '../models/book.model';
@Injectable()
export class BooksService {
books: Book[] = [];
booksSubject = new Subject<Book[]>();
emitBooks() {
this.booksSubject.next(this.books);
}
}
Ensuite, vous allez utiliser une méthode mise à disposition par Firebase pour enregistrer la liste sur un node de la base de données — la méthode set()
:
saveBooks() {
firebase.database().ref('/books').set(this.books);
}
La méthode ref()
retourne une référence au node demandé de la base de données, et set()
fonctionne plus ou moins comme put()
pour le HTTP : il écrit et remplace les données au node donné.
Maintenant que vous pouvez enregistrer la liste, vous allez créer les méthodes pour récupérer la liste entière des livres et pour récupérer un seul livre, en employant les deux fonctions proposées par Firebase :
getBooks() {
firebase.database().ref('/books')
.on('value', (data: DataSnapshot) => {
this.books = data.val() ? data.val() : [];
this.emitBooks();
}
);
}
getSingleBook(id: number) {
return new Promise(
(resolve, reject) => {
firebase.database().ref('/books/' + id).once('value').then(
(data: DataSnapshot) => {
resolve(data.val());
}, (error) => {
reject(error);
}
);
}
);
}
Pour getBooks()
, vous utilisez la méthode on()
. Le premier argument 'value'
demande à Firebase d'exécuter le callback à chaque modification de valeur enregistrée au endpoint choisi : cela veut dire que si vous modifiez quelque chose depuis un appareil, la liste sera automatiquement mise à jour sur tous les appareils connectés. Ajoutez un constructor au service pour appeler getBooks()
au démarrage de l'application :
constructor() {
this.getBooks();
}
Le deuxième argument est la fonction callback, qui reçoit ici une DataSnapshot
: un objet correspondant au node demandé, comportant plusieurs membres et méthodes (il faut importer DataSnapshot
depuis firebase.database.DataSnapshot
). La méthode qui vous intéresse ici est val()
, qui retourne la valeur des données, tout simplement. Votre callback prend également en compte le cas où le serveur ne retourne rien pour éviter les bugs potentiels.
La fonction getSingleBook()
récupère un livre selon son id, qui est simplement ici son index dans l'array enregistré. Vous utilisez once()
, qui ne fait qu'une seule requête de données. Du coup, elle ne prend pas une fonction callback en argument mais retourne une Promise, permettant l'utilisation de .then()
pour retourner les données reçues.
Pour BooksService, il ne reste plus qu'à créer les méthodes pour la création d'un nouveau livre et la suppression d'un livre existant :
createNewBook(newBook: Book) {
this.books.push(newBook);
this.saveBooks();
this.emitBooks();
}
removeBook(book: Book) {
const bookIndexToRemove = this.books.findIndex(
(bookEl) => {
if(bookEl === book) {
return true;
}
}
);
this.books.splice(bookIndexToRemove, 1);
this.saveBooks();
this.emitBooks();
}
Ensuite, vous allez créer BookListComponent
, qui :
souscrit au Subject du service et déclenche sa première émission ;
affiche la liste des livres, où chaque livre peut être cliqué pour en voir la page
SingleBookComponent
;permet de supprimer chaque livre en utilisant
removeBook()
;permet de naviguer vers
BookFormComponent
pour la création d'un nouveau livre.
import { Component, OnDestroy, OnInit } from '@angular/core';
import { BooksService } from '../services/books.service';
import { Book } from '../models/book.model';
import { Subscription } from 'rxjs/Subscription';
import { Router } from '@angular/router';
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.css']
})
export class BookListComponent implements OnInit, OnDestroy {
books: Book[];
booksSubscription: Subscription;
constructor(private booksService: BooksService, private router: Router) {}
ngOnInit() {
this.booksSubscription = this.booksService.booksSubject.subscribe(
(books: Book[]) => {
this.books = books;
}
);
this.booksService.emitBooks();
}
onNewBook() {
this.router.navigate(['/books', 'new']);
}
onDeleteBook(book: Book) {
this.booksService.removeBook(book);
}
onViewBook(id: number) {
this.router.navigate(['/books', 'view', id]);
}
ngOnDestroy() {
this.booksSubscription.unsubscribe();
}
}
class="row"
class="col-xs-12"
Vos livres
class="list-group"
class="list-group-item"
*ngFor="let book of books; let i = index"
(click)="onViewBook(i)"
class="list-group-item-heading"
{{ book.title }}
class="btn btn-default pull-right" (click)="onDeleteBook(book)"
class="glyphicon glyphicon-minus"
class="list-group-item-text"{{ book.author }}
class="btn btn-primary" (click)="onNewBook()"Nouveau livre
Il n'y a rien de nouveau ici, donc passez rapidement à SingleBookComponent
:
import { Component, OnInit } from '@angular/core';
import { Book } from '../../models/book.model';
import { ActivatedRoute, Router } from '@angular/router';
import { BooksService } from '../../services/books.service';
@Component({
selector: 'app-single-book',
templateUrl: './single-book.component.html',
styleUrls: ['./single-book.component.css']
})
export class SingleBookComponent implements OnInit {
book: Book;
constructor(private route: ActivatedRoute, private booksService: BooksService,
private router: Router) {}
ngOnInit() {
this.book = new Book('', '');
const id = this.route.snapshot.params['id'];
this.booksService.getSingleBook(+id).then(
(book: Book) => {
this.book = book;
}
);
}
onBack() {
this.router.navigate(['/books']);
}
}
Le component récupère le livre demandé par son id grâce à getSingleBook()
, et l'affiche dans le template suivant :
class="row"
class="col-xs-12"
{{ book.title }}
{{ book.author }}
{{ book.synopsis }}
class="btn btn-default" (click)="onBack()"Retour
Il ne reste plus qu'à créer BookFormComponent
, qui comprend un formulaire selon la méthode réactive et qui enregistre les données reçues grâce à createNewBook()
:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Book } from '../../models/book.model';
import { BooksService } from '../../services/books.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-book-form',
templateUrl: './book-form.component.html',
styleUrls: ['./book-form.component.css']
})
export class BookFormComponent implements OnInit {
bookForm: FormGroup;
constructor(private formBuilder: FormBuilder, private booksService: BooksService,
private router: Router) { }
ngOnInit() {
this.initForm();
}
initForm() {
this.bookForm = this.formBuilder.group({
title: ['', Validators.required],
author: ['', Validators.required],
synopsis: ''
});
}
onSaveBook() {
const title = this.bookForm.get('title').value;
const author = this.bookForm.get('author').value;
const synopsis = this.bookForm.get('synopsis').value;
const newBook = new Book(title, author);
newBook.synopsis = synopsis;
this.booksService.createNewBook(newBook);
this.router.navigate(['/books']);
}
}
class="row"
class="col-sm-8 col-sm-offset-2"
Enregistrer un nouveau livre
[formGroup]="bookForm" (ngSubmit)="onSaveBook()"
class="form-group"
for="title"Titre
type="text" id="title"
class="form-control" formControlName="title"
class="form-group"
for="author"Auteur
type="text" id="author"
class="form-control" formControlName="author"
class="form-group"
for="synopsis"Synopsis
id="synopsis"
class="form-control" formControlName="synopsis"
class="btn btn-success" [disabled]="bookForm.invalid"
type="submit"Enregistrer
Et ça y est, votre application fonctionne ! Elle enregistre et lit votre liste de livres sur votre backend Firebase, rendant ainsi son fonctionnement totalement dynamique !
Pour compléter cette application, vous allez ajouter la fonctionnalité qui permet d'enregistrer une photo de chaque livre grâce à l'API Firebase Storage.
Storage
Dans ce dernier chapitre, vous allez apprendre à utiliser l'API Firebase Storage afin de permettre à l'utilisateur d'ajouter une photo du livre, de l'afficher dans SingleBookComponent
et de la supprimer si on supprime le livre, afin de ne pas laisser des photos inutilisées sur le serveur.
Tout d'abord, vous allez ajouter une méthode dans BooksService
qui permet d'uploader une photo :
uploadFile(file: File) {
return new Promise(
(resolve, reject) => {
const almostUniqueFileName = Date.now().toString();
const upload = firebase.storage().ref()
.child('images/' + almostUniqueFileName + file.name).put(file);
upload.on(firebase.storage.TaskEvent.STATE_CHANGED,
() => {
console.log('Chargement…');
},
(error) => {
console.log('Erreur de chargement ! : ' + error);
reject();
},
() => {
resolve(upload.snapshot.ref.getDownloadURL());
}
);
}
);
}
Analysez cette méthode :
l'action de télécharger un fichier prend du temps, donc vous créez une méthode asynchrone qui retourne une Promise ;
la méthode prend comme argument un fichier de type File ;
afin de créer un nom unique pour le fichier (évitant ainsi d'écraser un fichier qui porterait le même nom que celui que l'utilisateur essaye de charger), vous créez un string à partir de
Date.now()
, qui donne le nombre de millisecondes passées depuis le 1er janvier 1970 ;vous créez ensuite une tâche de chargement
upload
:firebase.storage().ref()
vous retourne une référence à la racine de votre bucket Firebase,la méthode
child()
retourne une référence au sous-dossierimages
et à un nouveau fichier dont le nom est l'identifiant unique + le nom original du fichier (permettant de garder le format d'origine également),
vous utilisez ensuite la méthode
on()
de la tâcheupload
pour en suivre l'état, en y passant trois fonctions :la première est déclenchée à chaque fois que des données sont envoyées vers le serveur,
la deuxième est déclenchée si le serveur renvoie une erreur,
la troisième est déclenchée lorsque le chargement est terminé et permet de retourner l'URL unique du fichier chargé.
Maintenant que le service est prêt, vous allez ajouter les fonctionnalités nécessaires à BookFormComponent
.
Commencez par ajouter quelques membres supplémentaires au component :
bookForm: FormGroup;
fileIsUploading = false;
fileUrl: string;
fileUploaded = false;
Ensuite, créez la méthode qui déclenchera uploadFile()
et qui en récupérera l'URL retourné :
onUploadFile(file: File) {
this.fileIsUploading = true;
this.booksService.uploadFile(file).then(
(url: string) => {
this.fileUrl = url;
this.fileIsUploading = false;
this.fileUploaded = true;
}
);
}
Vous utiliserez fileIsUploading
pour désactiver le bouton submit
du template pendant le chargement du fichier afin d'éviter toute erreur — une fois l'upload terminé, le component enregistre l'URL retournée dans fileUrl
et modifie l'état du component pour dire que le chargement est terminé.
Il faut modifier légèrement onSaveBook()
pour prendre en compte l'URL de la photo si elle existe :
onSaveBook() {
const title = this.bookForm.get('title').value;
const author = this.bookForm.get('author').value;
const synopsis = this.bookForm.get('synopsis').value;
const newBook = new Book(title, author);
newBook.synopsis = synopsis;
if(this.fileUrl && this.fileUrl !== '') {
newBook.photo = this.fileUrl;
}
this.booksService.createNewBook(newBook);
this.router.navigate(['/books']);
}
Vous allez créer une méthode qui permettra de lier le <input type="file">
(que vous créerez par la suite) à la méthode onUploadFile()
:
detectFiles(event) {
this.onUploadFile(event.target.files[0]);
}
L'événement est envoyé à cette méthode depuis cette nouvelle section du template :
class="form-group"
Ajouter une photo
type="file" (change)="detectFiles($event)"
class="form-control" accept="image/*"
class="text-success" *ngIf="fileUploaded"Fichier chargé !
class="btn btn-success" [disabled]="bookForm.invalid || fileIsUploading"
type="submit"Enregistrer
Dès que l'utilisateur choisit un fichier, l'événement est déclenché et le fichier est uploadé. Le texte "Fichier chargé !" est affiché lorsque fileUploaded
est true
, et le bouton est désactivé quand le formulaire n'est pas valable ou quand fileIsUploading
est true
.
Il ne reste plus qu'à afficher l'image, si elle existe, dans SingleBookComponent
:
class="row"
class="col-xs-12"
style="max-width:400px;" *ngIf="book.photo" [src]="book.photo"
{{ book.title }}
{{ book.author }}
{{ book.synopsis }}
class="btn btn-default" (click)="onBack()"Retour
Il faut également prendre en compte que si un livre est supprimé, il faut également en supprimer la photo. La nouvelle méthode removeBook()
est la suivante :
removeBook(book: Book) {
if(book.photo) {
const storageRef = firebase.storage().refFromURL(book.photo);
storageRef.delete().then(
() => {
console.log('Photo removed!');
},
(error) => {
console.log('Could not remove photo! : ' + error);
}
);
}
const bookIndexToRemove = this.books.findIndex(
(bookEl) => {
if(bookEl === book) {
return true;
}
}
);
this.books.splice(bookIndexToRemove, 1);
this.saveBooks();
this.emitBooks();
}
Puisqu'il faut une référence pour supprimer un fichier avec la méthode delete()
, vous passez l'URL du fichier à refFromUrl()
pour en récupérer la référence.
Déployez votre application
Ça y est ! L'application est prête à être déployée. Si votre serveur de développement tourne encore, arrêtez-le et exécutez la commande suivante :
ng build --prod
Vous utilisez le CLI pour générer le package final de production de votre application dans le dossier dist
.
Le dossier dist
contient tous les fichiers à charger sur votre serveur de déploiement pour votre application.
Conclusion
Félicitations ! Vous avez utilisé vos nouvelles compétences Angular pour créer une application dynamique, comportant plusieurs components qui :
affichent des données dans le template avec le data binding ;
sont construits de manière dynamique avec les directives ;
communiquent ensemble grâce aux services ;
sont accessibles par un routing personnalisé ;
emploient des Observables pour gérer des flux de données ;
utilisent des formulaires pour exploiter des données fournies par l'utilisateur ;
fonctionnent avec un backend Firebase pour la gestion de l'authentification, des données et des fichiers.
Vous avez maintenant les connaissances nécessaires pour créer votre propre application Angular et pour trouver les informations dont vous avez besoin pour intégrer les fonctionnalités que vous ne connaissez pas encore. N'hésitez pas à aller plus loin, à chercher de nouvelles idées et de nouvelles méthodes, et vous créerez certainement des applications fabuleuses !