Toujours dans le cadre de mon auto-formation sur Angular, je documente içi les étapes de développement d'une application simple qui gère des appareils éléctriques. Elle vérifie l'état des appareils, s'ils sont allumés ou non. Des actions 'tout allumer' ou 'tout éteindre' ou autres sont possibles. Je rappel que je me suis basé sur le cours d'Openclassroom.
J'ai commencé par créer un projet que j'ai dénommé appareils-app-angular.
ng new mon-projet-angular --style=scss --skip-tests=true
N'oubliez pas d'intégrer Bootstrap à votre projet. Depuis le dossier appareils-app-angular , télécharger Bootstrap pour l'intégrer au package.json du projet :
npm install bootstrap@<version> --save
Ouvrez le fichier angular.json du dossier source de votre projet. Dans "architect/build/options", modifiez l'array styles comme suit :
Et pout términer, lancer le serveur :
ng serve
J'ai créé un component nommé appareil :
ng generate component appareil
- Vérifier bien dans
app.module.ts
que le CLI a ajouté le componentappareilComponent
dans l'arraydeclarations
et le statement Import en haut du fichier.
- Vérifier dans
appareil.component.ts
, le CLI a créé un selecteurapp-appareil
, nous l'insérerons dans notre code pour utiliser ce component.
- Ouvrez
appareil.component.html
(dans le nouveau dossier appareil créé par le CLI), supprimez le contenu, et entrez le code ci-dessous :
- Insérer le selector dans le fichier
app.component.html
dans une balise HTML comme suit :
Angular permet une manipulation dynamique des éléments du DOM (Document Object Model : éléments HTML affiché par le navigateur) grâce à la liaison des données.
Cette communication entre votre Typescript et le template HTML prend deux directions :
-
les informations venant de votre code (.ts) qui doivent être affichées dans le navigateur (.html), comme par exemple des informations que votre code (.ts) a calculé ou récupéré sur un serveur. Les deux principales méthodes pour cela sont le string interpolation et le property binding ;
-
les informations venant du template (.html) qui doivent être gérées par le code (.ts) : l'utilisateur a rempli un formulaire ou cliqué sur un bouton, et il faut réagir et gérer ces événements. On parlera de event binding pour cela.
-
on parlera aussi de two-way binding ou communication à double sens dans certains cas notamment les formulaires.
L'interpolation est la manière la plus basique d'émettre des données issues de votre code TypeScript.
Dans appareil.component.ts
, j'ai inséré le code suivant en haut de la déclaration de classe :
Insérez le code suivant dans appareil.component.html
:
La syntaxe pour l'interpolation : les doubles accolades {{ }}
exprime la variable appareilName
'(ou toutes expressions Typescript valable : méthodes, fonctions) qui est instanciée dans le code Typescript. Içi la méthode getStatus()
fonctionne de la même manière. Si on rafraîchit le navigateur, on obtient ceci :
Cette technique permet :
- d'afficher le contenu de notre variable
appareilName
- de modifier dynamiquement les propriétés d'un élément du DOM en fonction de données dans le TypeScript
Pour notre application, l'utilisateur authentifié aura la possibilité d'allumer tous les appareils. Pour ce faire, un bouton 'Tout allumer' sera désactivé par la propriété disabled
. Cette propriété sera liée à une variable dans le code Typescript. La syntaxe du property binding est le double crochet []
.
Pour simuler une authentification (valeur globale), il faut qu'on déclare une variable boolean dans AppComponent
. Sa valeur sera modifiée au bout de 4 secondes par une méthode constructor
dans laquelle un timeout est mis en place. Cette modification de valeur de notre variable impactera la proriété du bouton. Le bouton sera activé quand cet appel d'API imaginaire sera effectué.
Ajoutez maintenant un bouton au template global app.component.html
, en dessous de la liste d'appareils. Le point d'exclamation fait que le bouton est désactivé lorsque isAuth === false
.
La propriété de l'élément du DOM a été modifiée dynamiquement affectant ainsi son état. Le bouton est passé de l'état inactif à actif.
Jusque là, les données viennent du Typescript vers le Template. L'event binding vas dans le sens inverse. Les données en l'occurence des événements, viennent du Template HTML. On utilise les parenthèses ()
pour créer une liaison à un événement.
Revenons à notre application, notre bouton pour l'instant ne fait rien. Il s'active juste au bout de 4 secondes soit une simulation d'appel à un API d'authentification. Içi, je vais ajouter l'évenement click
en propriété à mon bouton. De même, je crée une méthode onAllumer()
qui n'existe pas encore app.component.ts
.
Ajoutons dans app.component.ts
la méthode onAllumer()
. Ici il sert juste à afficher le message dans la console:
Cette technique résulte de la combinaison de la property binding et event binding. Par conséquent, elle emploie le mélange des syntaxes : des crochets et des parenthèses [()]
.
Pour pouvoir utiliser le two-way binding, il faut importer FormsModule
depuis @angular/forms
dans votre application. Vous pouvez accomplir cela en l'ajoutant à l'array imports
de votre AppModule
(sans oublier d'ajouter le statement import
correspondant en haut du fichier) :
J'ai tésté sur l'application en insérant un <input>
dans appareil.component.html
. Ici, j'utilise une directive ngModel
pour le lier à appareilName
. Je précise qu'il y a une partie sur directives plus loin dans ce document.
Dans le navigateur, si vous modifiez le nom <input>
, le contenu du titre <h4>
change. Il est important de souligner que chaque instance du component AppareilComponent
est entièrement indépendante une fois créée : le fait d'en modifier une ne change rien aux autres.
L'intérêt de la création des propriétés personnalisées (ou événement) est de pouvoir transmettre des données depuis l'extérieur vers un component. Il faut utiliser le décorateur @Input()
et ne pas définir de valeur stricte à la variable lors de sa déclaration. Mais au préalable, il faut importé input
depuis @angular/core
dans appareil.component.ts
en haut du fichier.
Testons sur notre applications des appareils élétriques.
Vous pouvez également créer une propriété pour régler l'état de l'appareil.
Voilà ce qui se passe, @Input
a créé une propriété appareilName qu'on pourra fixer sur la balise <app-appareil>
:
C'est une première étape intéressante, mais ce serait encore plus dynamique de pouvoir passer des variables depuis AppComponent
pour nommer les appareils. On peut imaginer une autre partie de l'application qui récupérerait ces noms depuis un serveur, par exemple. Heureusement, vous savez déjà utiliser le property binding !
J'ouvre le fichier app.component.ts
et j'instancie trois variable avec les noms des appareils :
Maintenant, utilisez les crochets []
pour lier le contenu de ces variables à la propriété du component dans app.component.html
:
Ce sont des instructions intégrées dans le DOM. On peut en créer ou uitilisé celles fournies avec ANGULAR. Elles sont précédés d'un *
à l'utilisation.
Pour notre application, nous allons mettre place un témoin rouge qui ne s'affiche que si l'appareil est éteint. Pour cela il nous faut utiliser la directive structurelle *ngIf
. Un component auquel on ajoute la directive *ngIf="condition"
ne s'affichera que si la condition est "truthy" (elle retourne la valeur true où la variable mentionnée est définie et non-nulle), comme un statement if classique. Testons!
Dans le fichier appreil.component.html
, j'ai ajouté une <div>
(modifie la structure du document) avec style
CSS et ma directive comme ceci :
Supposons que nous récuperons un array
contenant les appareils et leurs états depuis un serveur. Pour l'instant, je vais créer cet array
directement dans app.component.ts
:
Chaque objet a une propriété name
et une propriété status
. J'utilise la deuxième directive structurelle *ngFor="let appareil of appareils"
. Elle affiche une itération de l'objet appareil
de l'array appareils
. Après cette directive, j'utilise l'objet appareil
, à l'intérieur d'une balise HTML. Les propriétés name
et status
de cet objet sont passés par le property binding dans les propriétés de cet balise HTML qui sont notamments appareilName
et appareilStatus
.
Ces directives modifient dynamiquement le comportement d'un objet existant. Je cite *ngModel
que j'ai utilisé en two way binding, ngStyle
, ngClass
.
Pour l'application, je veux utiliser ngStyle
pour changer la couleur du texte suivant l'état de l'appareil, rouge si 'éteint' et vert si 'allumé'. Cette directive prend un objet JS de type clé:valeur
. Le style est la clé à la quelle on donne une nouvelle valeur. Dans notre cas, je crée dans appareil.component.ts
une fonction getColor()
et la donne en valeur à mon style :
La fonction retourne green
si allumé, red
si éteint.
On peut aller plus loin, modifions maintenant la couleur de la balise <li>
pour lui donner les même couleurs que les textes selon l'état de l'appareil. J'utilise ngClass
pour ça. Elle prend des class
comme clé et une condition en valeur.
Les pipes prennent des données en input, les transforment, et puis affichent les données modifiées dans le DOM. Il y a des pipes fournis avec Angular, et vous pouvez également créer vos propres pipes si vous en avez besoin. Je vous propose de commencer avec les pipes fournis avec Angular. Pour ajouter un Pipe
on utilise |
.
Ce que je vais faire c'est ajouter à l'application la date de dernière mise à jour. J'utilise DatePipe
qui analyse l'objet JS de type Date
avec son encodage de base, le transforme et l'affiche sous la mise en forme que j'ai choisi.
J'ouvre mon fichier app.component.ts
, je crée une variable lastUpdate
. Je vais dans mon fichier app.component.html
, j'y affiche la variable que je vient de créer dans un paragraphe.
L'objet Date est crée, mettons le en forme avec DatePipe. Angular permet de paramétrer DatePipe avec un argument de formatage comme suit : | date : 'short'
:
On peut même utiliser une chaîne de Pipes. L'image en dessous montre que j'ai paramétrer DatePipe sous un autre argument de formatage et j'ai affiché la date en majuscule.
Je voulais parler de async
pour mettre en exergue sa grande utilité lors de gestion de données asynchrones, que l'application doit récupérer sur un serveur par exemples. Pour le moment nous ne communiquons pas avec un serveur, mais plus tard on le fera.
Je vais simuler cette communication avec une Promise
qui sera résolue au bout de 2 secondes.
Je met à jour la variable lastUpdate comme suit :
lastUpdate:any = new Promise((resolve, reject) => {
const date = new Date();
setTimeout(
() => {
resolve(date);
}, 2000);
});
Si on enregistre et qu'on ouvre notre console, une erreur apparaît. En effet, au moment de générer le DOM, lastUpdate
est encore une Promise
et n'a pas de valeur modifiable par les pipes.
Il nous faut ajouter async
en début de chaîne pour dire à Angular d'attendre l'arrivée des données avant d'exécuter les autres pipes.
Une petite parenthèse, je vous renvois au repository exercice-blog-angular pour voir l'application qui gère des posts de blog que j'ai réalisé suite à ce cours 😓.
Continuons avec les services
.
C'est quoi les services ? 😬 En gros, c'est un fichier qui contient des données (authentifications) ou du code (fonctions globales, etc) afin de les centraliser. Ce fichier que l'on nommera suivant le service qu'on souhaite mettre en place sera utilisé par tout ou partie de l'application. Les avantages : non répétitivité, maintenabilité, lisibilité, stabilité du code.
Comment utiliser un service? Un service doit être injecté. Il faut prêter une attention particulière au choix du niveau d'injection. Cela impactera sur son instanciation et son accéssibilité aux components et aux autres services.
Trois possibilités existes:
- Dans
app.module.ts
= une seule instance accéssible par tous components et autres services de l'application. - Dans
app.component.ts
= une instance accéssible à tous les components mais pas aux autres services. - Dans un autre component = instance accéssible uniquement au component lui même et ses enfants.
Pour mon application, je vais utiliser la première option, dans app.module.ts
. Je vais intégrer à mon application un service nommé AppareilService
qui contiendrait les données des appareils électriques, et également des fonctions globales liées aux appareils, comme "tout allumer" ou "tout éteindre".
Plus tard, j'intégrerais aussi deuxième service AuthService
qui s'occuperait de vérifier l'authentification de l'utilisateur, et qui pourrait également stocker des informations sur l'utilisateur actif comme son adresse mail et son pseudo.
Pour commencer, je crée un sous dossier services
dans mon dossier app
. Je crée le fichier appareil.services.ts
dans lequel je glisse le code ci-dessous.
export class AppareilServices {
}
J'injecte ce service dans app.module.ts
et j'ajoute un array providers
. L'import du service doit être fait en haut du fichier.
Maintenant que l'instance du service est créé, je vais l'intégrer dans app.component.ts
. Pour ce faire, on le déclare comme argument dans son constructeur sans oublier de l'importer en haut du fichier :
Je copie depuis app.component.ts
l'array appareils
. Je le colle dans appareil.service.ts
. Je retourne dans AppComponent et je déclare appareils
simplement comme un array de type any
.
A ce stade mon bouton 'Allumer tout!' ne fait qu'afficher le message 'On allume tout!' dans la console. Je vais maintenant ajouter une méthode switchOnAll()
dans AppareilService
pour activer cette fontionnalité. Ensuite je crée un autre bouton 'Eteindre tout!' et une autre méthode switchOfAll()
pour tout éteindre.
Je déclenche cette méthode dans app.component.ts
dans onAllume()
qui est relié au bouton par l'événement click
:
Je met en place un message de confirmation pou onEteindre()
:
Ce qui serait bien c'est d'ajouter une fonctionnalité qui permet d'allumer ou éteindre les appareils un à la fois. Pour y arriver, j'ai besoin de faire communiquer AppareilComponent
à AppareilService
.
Je commence par capturer l'index de chaque appareil membre de l'array de AppareilService
dans une propriété indexOfAppareil
que je crée grâce au property binding @Input()
dans appareils.component.ts
.
@Input()
appareilName:string | undefined;
@Input()
appareilStatus:string | undefined;
@Input()
indexOfAppareil: number | any;
Pour avoir l'index, il faut se rendre dans app.component.html
, dans la directive ngFor
, on donne à i
l'index de chaque membre du tableau. On lie la à propriété personnalisée indexOfAppareil
pour chaque itération du tableau son index.
<ul class="list-group">
<app-appareil *ngFor="let appareil of appareils; let i = index"
[appareilName]="appareil.name"
[appareilStatus]="appareil.status"
[indexOfAppareil]="i"> </app-appareil>
</ul>
Je crée maintenant deux méthodes dans AppareilService
, permettant d'allumer ou d'éteindre un seul appareil en fonction de son index.
switchOnOne(index:number){
this.appareils[index].status='allumé';
}
switchOffOne(index:number){
this.appareils[index].status='éteint';
}
Maintenant dans AppareilComponent
, je vais intégrer le service en le construisant. Ensuite, je crée les méthodes pour allumer ou éteindr l'appareil en fonction de son statut.
constructor(private appareilService: AppareilService) { }
onSwitchOn(){
this.appareilService.switchOnOne(this.indexOfAppareil);
}
onSwitchOff(){
this.appareilService.switchOffOne(this.indexOfAppareil);
}
Je termine par les boutons affichés sur chaque itération du tableau.
<li [ngClass]="{'list-group-item' :true,
'list-group-item-success': appareilStatus==='allumé',
'list-group-item-danger' : appareilStatus==='éteint'}">
<div
style="width:15px; height:15px;background-color: red;"
*ngIf="appareilStatus ==='éteint'"></div>
<h4 [ngStyle]="{color: getColor()}">Appreil {{ appareilName }} -- Statut {{ appareilStatus}}</h4>
<input type="text" class="text form-control" [(ngModel)]="appareilName">
<button class="btn btn-sm btn-success"
[disabled]="appareilStatus === 'allumé'"
(click)="onSwitchOn()">Allumer</button>
<button class="btn btn-sm btn-danger"
[disabled]="appareilStatus === 'éteint'"
(click)="onSwitchOff()">Eteindre</button>
</li>