theme | background | class | highlighter | lineNumbers | info | drawings | title | |
---|---|---|---|---|---|---|---|---|
./theme |
text-center |
shiki |
false |
## Lucca Angular for integrator training slides
|
|
Angular Basics |
Formation Angular pour les intégrateurs
Création d'un component
Par convention, on utilise UN fichier contenant UNE classe pour UN Component.
Convention de nommage : something.component.ts
Angular expose ses classes, méthodes et outils dans le namespace @angular
. Par exemple @angular/core
.
L'api complète exposée est présentée ici : API Angular
Pour créer un Component il faut exposer une classe décorée par @Component
. En passant les options suivantes :
selector
: string nom de l'élément html créé permettant d'utiliser le component.template
: string code html du Component
Utilisation d'un Component
Pour utiliser un Component dans un autre, il faut le déclarer dans le Module.
declarations
: any[] liste des Components utilisés
@NgModule({
...
declarations: [ MyComponent ],
...
})
Le nouveau Component pourra alors être utilisé dans l'ensemble du Module.
Ne pas oublier d'importer le Component (ici PlanetsComponents
) avec le chemin relatif et sans l'extension '.ts'.
Afficher les propriétés d'un Component
Dans la classe, on ajoute un attribut à la classe.
export class PlanetsComponent {
title = 'La liste des planètes'
}
Dans le template, on affiche une propriété de l'objet Component en utilisant les 'doubles moustaches' {{ title }}
<h2>{{ title }}</h2>
Syntaxe
[prop]="value"
One way data binding
Les changements sur les propriétés faites dans le component sont actualisés sur le DOM, mais pas l'inverse.
Différence attribut - propriété
<a href='foo.html' class='test one' name='fooAnchor' id='fooAnchor'>Hi</a>
+-------------------------------------------+
| a |
+-------------------------------------------+
| href: "http://example.com/foo.html" |
| name: "fooAnchor" |
| id: "fooAnchor" |
| className: "test one" |
| attributes: |
| href: "foo.html" |
| name: "fooAnchor" |
| id: "fooAnchor" |
| class: "test one" |
+-------------------------------------------+
const link = document.getElementById('fooAnchor');
alert(link.href); // alerts "http://example.com/foo.html"
alert(link.getAttribute("href")); // alerts "foo.html"
Attribute binding
Il peut arriver que l'on veuille adresser un attribut et non pas une propriété.
<table>
<tr>
<td>un</td>
<td>deux</td>
</tr>
<tr>
<td [attr.colspan]="1 + 1">trois</td>
</tr>
</table>
Ajout de classes css dynamquement
<span class="un deux" [class]="myClass">Test</span>
Attention la classe est écrasée
<span [class.un]="true" [class.deux]="false">Test</span>
<span [ngClass]="{un: true, deux: false}"></span>
Modification dynamique de styles inline
<p [style.color]="pinkColor">
[style.color]="pinkColor"
</p>
<p [style.fontSize.em]="1.5">
[style.fontSize.em]="1.5"
</p>
Déclenchement d'actions sur des événements
<button (click)="doSomething()">un</button>
<button on-click="doSomething()">deux</button>
Où doSomething
est une méthode du component.
Depuis le template, on peut passer l'objet Event $event
à la méthode utilisée.
<button (click)="doSomething($event)">un</button>
Il est alors possible d'utiliser. (Par exemple $event.stopPropagation
).
Déclaration via Decorator
Importer le Decorator Input
import { Input } from '@angular/core';
Ajouter Input()
devant la propriété
@Input() name = '';
Utilisation
<my-component [name]="'John Doe'"></my-component>
Mise en place de l'event
Importer EventEmitter
import { EventEmitter } from '@angular/core';
Déclarer l'event dans la classe
change = new EventEmitter();
Déclaration via Decorator
Importer le Decorator Output
import { Output } from '@angular/core';
Ajouter Output()
devant l'event
@Output() change = new EventEmitter();
Déclenchement de l'event
Où désiré dans le code, il suffit d'appeler :
this.change.emit(data);
Avec data l'information à transmettre.
Utilisation
Comme un event natif :
<my-component (change)="doSomething($event)">
et on récupère les informations dans $event
ngIf
<div *ngIf="errorCount > 0" class="error">
{{errorCount}} errors detected
</div>
ngSwitch
<div [ngSwitch]="value">
<p *ngSwitchCase="'init'">increment to start</p>
<p *ngSwitchCase="0">0, increment again</p>
<p *ngSwitchCase="1">1, increment again</p>
<p *ngSwitchCase="2">2, stop incrementing</p>
<p *ngSwitchDefault>> 2, STOP!</p>
</div>
ngFor (ngForOf)
<li *ngFor="let planet of planets; let i = index">
{{ i }} : {{ planet }}
</li>
En plus de index
, on a : first
, last
, even
, odd
Transformation de données à l'affichage
{{ valeur_à_formater | nom_du_pipe }}
{{ valeur_à_formater | nom_du_pipe:argument_1:argument_2 }}
{{ valeur_à_formater | nom_du_pipe_1 | nom_du_pipe_2 }}
Quelques pipes fournies par Angular
AsyncPipe, CurrencyPipe, DatePipe, DecimalPipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, PercentPipe, SlicePipe, TitleCasePipe, UpperCasePipe
Écrire ses propres Pipes
something.pipe.ts
import { Pipe, PipeTransform } from '@angular/core'
@Pipe({ name: 'summary' })
export class SomethingPipe implements PipeTransform {
transform (value: string, param1) {
// code
}
}
Déclaration dans le module
@NgModule({
...
declarations: [SomethingPipe]
...
})
Safe navigation operator
permet d'éviter les erreurs dûes à la lecture de propriétés sur les objets null ou undefined.
Provoque une erreur si user n'existe pas
{{ user.firstName }}
Sans erreur (n'affiche rien) si user n'existe pas
{{ user?.firstName }}
Transporter le contenu d'un Component
Le système de transclusion de Angular. Il suffit d'utiliser le component <ng-content>
pour récupérer le contenu passé.
Il est possible de récupérer un sous contenu en précisant la cible via l'attribut select
de ce component. Le ciblage se fait alors avec un sélecteur css.
Model Driven Forms vs Template Driven Forms
Une approche entièrement déclarative, reposant sur des directives. C'est l'approche Template Driven.
Une approche plus programatique, dans laquelle il est de la responsabilité du développeur de créer le modèle. C'est l'approche Model Driven
Préparation
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [
ReactiveFormsModule
]
});
FormGroup
Pour représenter et manipuler le formulaire, nous allons créer un objet FormGroup
.
import { FormGroup } from '@angular/forms';
export class UserFormComponent implements OnInit {
form: FormGroup;
ngOnInit() {
this.form = new FormGroup({});
}
}
FormControl
Un formulaire n'est intéressant que s'il contient des champs.
import { FormGroup, FormControl } from '@angular/forms';
export class UserFormComponent implements OnInit {
form: FormGroup;
ngOnInit() {
this.form = new FormGroup({
name: new FormControl()
});
}
}
Sous groupes
Il est possible de déclarer un FormGroup
dans un FormGroup
.
import { FormGroup, FormControl } from '@angular/forms';
export class UserFormComponent implements OnInit {
form: FormGroup;
ngOnInit() {
this.form = new FormGroup({
address: new FormGroup({
street: new FormControl()
})
});
}
}
Bound to Html
<form [formGroup]="form">
<input formControlName="name" type="text">
<fieldset formGroupName="address">
<input formControlName="street" type="text">
</fieldset>
</form>
Validators
Un FormControl
peut être construit avec :
- une valeur par défaut,
- un validateur ou un tableau de validateurs.
import { Validators } from '@angular/forms';
export class UserFormComponent implements OnInit {
ngOnInit() {
this.form = new FormGroup({
name: new FormControl('', Validators.required),
bio: new FormControl('', [
Validators.required,
Validators.minLength(10)
]),
});
}
}
Builtin validators
- Validators.required
- Validators.minLength(n: number)
- Validators.maxLength(n: number)
- Validators.pattern(r: Regexp)
- Validators.min(n: number)
- Validators.max(n: number)
- Validators.email
- Validators.requiredTrue
State
Un objet FormControl
(et donc FormGroup
) porte un état.
this.form = new FormGroup({
name: new FormControl('', Validators.required),
});
this.form.touched; // true / false
this.form.get('name').touched; // true / false
Les propriétés d'états :
- valid / invalid
- dirty / pristine
- touched / untouched
- value
Error message & error class
<label for="name" class="six columns-md" [class.text-danger]="form.get('name').touched && form.get('name').invalid">
Name
<input
formControlName="name"
id="name"
type="text"
[class.has-error]="form.get('name').touched && form.get('name').invalid"
class="u-full-width">
<p *ngIf="form.get('name').touched && form.get('name').invalid" class="text-danger">Required</p>
</label>
Errors messages : Shortcuts
form = new FormGroup({
one: new FormControl(undefined, [Validators.required]),
two: new FormGroup({
three: new FormControl(),
}),
});
oneControl = this.form.get('one') as FormControl;
threeControl = this.form.get('two.three') as FormControl;
<label for="name" class="six columns-md" [class.text-danger]="oneControl.touched && oneControl.invalid">
One
<input [class.has-error]="oneControl.touched && oneControl.invalid"
formControlName="one" id="one" type="text" class="u-full-width">
<p *ngIf="oneControl.touched && oneControl.invalid" class="text-danger">Required</p>
</label>
Erreurs spécifiques
La propriété errors
d'un FormControl
contient des informations sur les erreurs.
<div class="row">
<label for="bio" class="twelve columns-md" [class.text-danger]="bio.touched && bio.invalid">
Bio
<textarea [class.has-error]="bio.touched && bio.invalid"
formControlName="bio" id="bio" class="u-full-width"></textarea>
<p *ngIf="bio.touched && bio.invalid && bio.errors['required']" class="text-danger">Required</p>
<p *ngIf="bio.touched && bio.invalid && bio.errors['minlength']" class="text-danger">
Minlength : {{ bio.errors['minlength'].actualLength }} / {{ bio.errors['minlength'].requiredLength }}
</p>
</label>
</div>
Valeurs initiales d'un formulaire
// remplit les champs en commun
this.form.patchValue({ name: 'John' });
// remplit tout (mais ne support pas les manques)
this.form.setValue({
name: 'John',
email: '[email protected]',
});
Soumettre le formulaire
Pour soumettre le formulaire on utilise l'event submit
.
<form [formGroup]="form" novalidate (submit)="saveUser()">
...
</form>
Déclaration
NgForm
est un directive dont le selecteur est <form>
. De plus l'instance de la directive est exposée sous le nom 'ngForm'.
<form #form="ngForm">
</form>
Un NgForm
contient une propriété form
de type FormGroup
.
Extrait des sources de Angular.
this.form = new FormGroup({}, composeValidators(validators), composeAsyncValidators(asyncValidators));
Links to controls
La directive ngForm
ne créé pas automatiquement les contrôles en lien avec les input. Il faut créer ce contrôle. C'est le rôle de la directive ngModel
. Qui agit comme ngForm
mais au niveau d'un input.
La directive ngModel
utilisée comme enfant de la directive ngForm
a besoin d'être liée à celle-ci. Ce lien se fait via l'attribut name.
<form #form="ngForm">
<input ngModel name="username" type="text">
</form>
Links to groups of controls
Il est possible de définir un sous formulaire (fieldset), grâce à la directive ngModelGroup
.
<form #form="ngForm">
<input ngModel name="username" type="text">
<div ngModelGroup="profile">
<input ngModel name="firstname" type="text">
<input ngModel name="lastname" type="text">
</div>
</form>
Valeur initiales
Pour fournir une valeur initiale à un contrôle, on ne peut pas utiliser l'attribut value
. Celui-ci est écrasé par la directive ngModel
.
La solution consiste à passer une valeur à cette directive.
export class UserFormComponent {
username: string = 't8g';
}
<input [ngModel]="username" name="username" id="username" type="text" class="u-full-width">
Par contre si on observe la propriété username
, on s'aperçoit que celle-ci n'est pas mise à jour lorsque la valeur du contrôle change.
2way data binding : "banana box"
<input [(ngModel)]="username" name="username" id="username" type="text" class="u-full-width">
Cette syntaxe est en fait un raccourci pour
<input [ngModel]="username" (ngModelChange)="username = $event"
name="username" id="username" type="text" class="u-full-width">
Validation
Dans l'approche Template Driven, la validation se définit grâce à des directives.
<!-- email -->
<input type="email" name="email" ngModel email>
<input type="email" name="email" ngModel email="true">
<input type="email" name="email" ngModel [email]="true">
<!-- minlength / maxlength -->
<textarea name="bio" ngModel maxlength="100">
<textarea name="bio" ngModel [minlength]="min"><!-- min propriété du component -->
<!-- pattern -->
<input type="text" name="username" ngModel pattern="[a-zA-Z ]*">
<!-- required -->
<input type="text" name="username" ngModel required>
Afficher les erreurs
Pour accèder à un FormControl
en particulier et ainsi à son état, on utilise form.controls['controlName']
.
Mais attention, à l'initialisation du template, celui-ci est nul. Donc penser à utiliser un elvis operator.
<form #form="ngForm">
<div class="row">
<label for="name" class="six columns-md"
[class.text-danger]="form.controls['name']?.touched && form.controls['name']?.invalid">
Name
<input ngModel name="name" required
[class.has-error]="form.controls['name']?.touched && form.controls['name']?.invalid"
id="name" type="text" class="u-full-width">
<p *ngIf="form.controls['name']?.touched && form.controls['name']?.invalid" class="text-danger">Required</p>
</label>
</div>
</form>
Afficher les erreurs : raccourci
Pour alléger un peu le template, il est possible de récupérer une variable locale au template contenant l'instance du NgModel
et donc l'état du NgControl
associé.
<form #form="ngForm">
<div class="row">
<label for="name" class="six columns-md"
[class.text-danger]="name.touched && name.invalid">
Name
<input ngModel name="name" required #name="ngModel"
[class.has-error]="name.touched && name.invalid"
id="name" type="text" class="u-full-width">
<p *ngIf="name.touched && name.invalid" class="text-danger">Required</p>
</label>
</div>
</form>
Configuration : app.routing.ts
import { Routes, RouterModule } from '@angular/router';
import { FirstComponent } from './first.component';
import { SecondComponent } from './second.component';
// Configuration des routes
const appRoutes: Routes = [
{
path: 'first', // url (sans / initial)
component: FirstComponent // composant à charger pour cette route
},
{
path: 'second',
component: SecondComponent
}
];
// Export du module de routing configuré
export const routing = RouterModule.forRoot(appRoutes);
Intégration dans le module
import { routing } from './app.routing';
import { AppComponent } from './app.component';
@NgModule({
imports: [
BrowserModule,
routing
],
declarations: [
AppComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
Layout
Enfin dans le template du component principal, nous plaçons le component de route : Router Outlet
<router-outlet></router-outlet>
Navigation
Pour naviguer d'une vue à une autre, on utilise une directive routerLink
qui va créer le lien pour nous.
<a routerLink="/first">First</a>
Bonus : classe active
Il est possible de définir une classe css lorsque la route est active.
<a routerLink="/first" routerLinkActive="active">First</a>
forEach : une action pour chaque item
const innerPlanets = [
{ name: 'Mercure', satellites: [] },
{ name: 'Venus', satellites: [] },
{ name: 'Earth', satellites: [{ name: 'Lune' }] },
{ name: 'Mars', satellites: [{ name: 'Phobos' }, { name: 'Déimos' }]},
];
innerPlanets.forEach((planet) => {
console.log('palnet name', planet.name);
});
map : nouveau tableau de données transformées
const planetsNames = innerPlanets.map((planet) => {
return planet.name;
});
// ou
const planetsNames = innerPlanets.map((planet) => planet.name);
// ou
const planetsNames = innerPlanets.map(({ name }) => name);
filter : portion de tableau
const planetsWithSatellites = innerPlanets.filter((planet) => {
return planet.satellites.length > 0;
});
// ou
const planetsWithSatellites = innerPlanets.map((planet) => planet.satellites.length > 0);
// ou
const planetsWithSatellites = innerPlanets.map(({ satellites }) => satellites.length > 0);
reduce : transformation
const totalNumberOfSatellites = innerPlanets.reduce(
(acc, planet) => {
return acc + planet.satellites.length;
},
0
);
Définition
La programmation réactive est un paradigme de programmation visant à conserver une cohérence d'ensemble en propageant les modifications d'une source réactive (modification d'une variable, entrée utilisateur, etc.) aux éléments dépendants de cette source. - wikipédia
Beaucoup d'outils pour manipuler des données (.map, .filter, .reduce). Mais pour des données qui s'accumulent avec le temps ?
Nouvelle définition
"Reactive programming is programming with asynchronous data streams." - André Staltz
Définitions
Un flux est simplement une collection qui s'étoffe avec le temps
Un Observable est un objet encapsulant un flux et qui peut être manipulé comme une collection.
Subscribe & Unsubscribe
someObservable$.subscribe((value) => {
console.log('value', value);
});
const observer = {
next: (value) => console.log('new value', value),
error: (error) => console.log('error', error),
complete: () => console.log('end'),
};
someObservable$.subscribe(observer);
const subscription = someObservable$.subscribe(x => console.log(x));
// Plus tard
subscription.unsubscribe();
Operators
// timer$ est un Observable qui compte les secondes 1, 2, 3, ...
timer$.pipe(
filter((t) => t % 2 === 0),
map((t) => `Il s'est passé ${t} secondes`),
);