Installation des outils
> npm install -g typescript
> npm install -g @angular/cliGit
NodeJs
Un IDE
Depuis l'invite de commandes
> npm uninstall -g @angular/cli
> npm cache clean
> npm install -g @angular/cliOU
Répertoire d'installation en local : M:\Env formation Angular
Github de la formation
> git clone -b step-00 https://github.com/Jmadih/angularX
> cd angularX
> npm install
> ng serve
●une branche d’exercice : step-00
●une branche de solution : step-00-solution
Un concept => une branche
Les languages supporter par Angular :
@Component({
selector: 'my-app',
template: '<h1 id="output">My First Angular 2 App</h1>'
})
class AppComponent { }
var AppComponent = ng.core.Component({
selector: 'my-app',
template: '<h1 id="output">My First Angular 2 App</h1>'
})
.Class({
constructor: function () { }
});
Arrow functions
Classes
Collections
get / set transparents
let / const
ES6 modules
Decorators
Classes
Interfaces
Typings
Function return
Optional params
Angular 10
Angular 9
Configuration
ES5 est désactivé par défaut, pour ajouter un navigateur => .browserlistrc
CLI
Génère l’arborescence de l’application
Initialise un repo Git + 1er commit
Installe les deps NPM + Typings
> ng new my-best-appNouveau projet :
> ng g component home
create src/app/home/home.component.html (23 bytes)
create src/app/home/home.component.spec.ts (614 bytes)
create src/app/home/home.component.ts (262 bytes)
create src/app/home/home.component.scss (0 bytes)
update src/app/app.module.ts (390 bytes)
Nouveau composant :
Component : ng g component [name] --flat
Directive : ng g directive [name]
Pipe : ng g pipe [name]
Service : ng g service [name]
Class : ng g class [name]
Interface : ng g interface [name]
Enum : ng g enum [name]
Build : ng build -prod
Lint : ng lint
Directives : ng doc directives
Toutes les commandes :
(Advanced: Angular elements, Ivy, Schematics ...)
Une classe avec un décorateur @NgModule
Paramètres : declarations, imports, exports, providers, bootstrap.
Bootstrapping
git checkout -f step-01
Créez un composant Home (HomeComponent)
Intégrez le dans le template du composant parent (Ici AppComponent)
Créez un module Home (HomeModule)
Déclarez le composant Home dans le module Home
3 catégories de bindings
3 catégories de bindings
<!-- Interpolation -->
<h1>{{introduction.firstname}}</h1>
<!-- One way binding -->
<h1 [innerText]="introduction.firstname"></h1>
<span [style.backgroundColor]="introduction.favouriteColor"></span>
<!-- Two way binding -->
<input [(ngModel)]="introduction.firstname"/>
<!-- Event binding -->
<button (click)="sendForm()">Send</button>Exemples
attributs vs propriétés
Les attributs c'est du HTML, les propriétés c'est du DOM
Les attributs servent à initialiser les propriétés mais pas vice versa !
attributs vs propriétés
<input type="text" value="foo"> <!-- attributs value et type -->
Le navigateur interprète cette balise et crée un objet node en mémoire :
input:
...
value: "foo" // <-- propriétés value et type
type: "text"
...git checkout -f step-02
Dans le Home Component : Remplissez un model de type Introduction
Affichez le model dans le template du Home Component
Ajoutez un événement qui déclenche"alert('edit')"sur le bouton Edit
Ajoutez la propriété disable sur le bouton Edit, cette propriété s'active quand on clique sur le bouton
Rendu
Modifie le DOM en ajoutant ou en supprimant des éléments.
<!-- *ngIf -->
<div *ngIf="hero; else notHero" class="name">{{hero.name}}</div>
<ng-template #notHero >
I'm not a Hero bro
</ng-template>
<!-- *ngFor -->
<ul>
<li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>
<!-- *ngSwitch -->
<div [ngSwitch]="hero?.emotion">
<app-happy-hero *ngSwitchCase="'happy'" [hero]="hero"></app-happy-hero>
<app-sad-hero *ngSwitchCase="'sad'" [hero]="hero"></app-sad-hero>
<app-confused-hero *ngSwitchCase="'app-confused'" [hero]="hero"></app-confused-hero>
<app-unknown-hero *ngSwitchDefault [hero]="hero"></app-unknown-hero>
</div>Change l'apparence ou le comportement d'un élement
<div [style.width.px]="size"></div>
<td [attr.colspan]="5*5">cell</td>
<div [class.isSpecial]="isSpecial">Special class</div>
<div [style.color]="isSpecial ? 'red' : 'green'">Special class</div>
CUSTUM
import {Directive, ElementRef, Input, OnInit, Renderer2} from '@angular/core';
@Directive({
selector: '[custumDirective]'
})
export class custumDirective implements OnInit {
constructor(private el: ElementRef, private renderer: Renderer2) {
}
ngOnInit() {
this.renderer.addClass(this.el.nativeElement, 'wild');
}
}
Juste une directive avec un template
<app-component>
Hello world...
</app-component>git checkout -f step-03
Rendu
import {ModuleWithProviders} from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {IntroductionComponent} from "./introduction/introduction.component";
import {SkillsComponent} from "./skills/skills.component";
import {AboutComponent} from "./about/about.component";
const routes: Routes = [
{path: '', redirectTo: 'introduction', pathMatch: 'full'},
{path : 'introduction', component: IntroductionComponent},
{path : 'skills', component: SkillsComponent },
{path : 'about', component : AboutComponent},
{path : '**', component: IntroductionComponent}
];
export const AppRouting: ModuleWithProviders = RouterModule.forRoot(routes, {useHash: true});Déclaration
<!-- app.component.html -->
<router-outlet></router-outlet>Déclaration de l’emplacement dans lequel le composant doit s’afficher
<button mat-raised-button color="primary" [routerLink]="['/page1']">Page1</button>
<button mat-raised-button color="primary" [routerLink]="['/page2']">Page2</button>
<button mat-raised-button color="primary" [routerLink]="['/page3']">Page3</button>Redirection
import {ModuleWithProviders} from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {IntroductionComponent} from "./introduction/introduction.component";
import {SkillsComponent} from "./skills/skills.component";
import {SkillsResolve} from "./skills/skills.resolve";
import {AboutComponent} from "./about/about.component";
const routes: Routes = [
{path: '', redirectTo: 'introduction', pathMatch: 'full'},
{path : 'introduction', component: IntroductionComponent},
{path : 'skills', component: SkillsComponent, resolve: {skills: SkillsResolve} },
{path : 'about', component : AboutComponent},
{path : '**', component: IntroductionComponent}
];
export const AppRouting: ModuleWithProviders = RouterModule.forRoot(routes, {useHash: true});Utilisation d'un resolver
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router';
@Injectable()
export class SkillsResolver implements Resolve<any> {
constructor() {}
resolve(activatedRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> {
const elem: number = activatedRoute.params['param'];
}
}
Déclaration d'un resolver
@Component({
selector: 'app-my',
templateUrl: './my.component.html',
styleUrls: ['./my.component.scss']
})
export class MyComponent implements OnInit {
infos: any;
constructor(private route: ActivatedRoute) {
}
ngOnInit() {
this.route.data.subscribe(data => {
this.infos= data['skills'];
});
}Récupérer les données du resolver
export const appRoutes: Routes = [
{path: 'info/:id', component: InfoComponent, resolve: { info: InfoResolverService}},
{path: 'about', component: AboutComponent, canActivate: [MyGuard]},
{path: '**', component: NotFoundComponent},
{path: '404', component: NotFoundComponent}
];
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class ConnectedGuardGuard implements CanActivate {
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return true;
}
}
git checkout -f step-04
Créez un fichier "app.routes.ts" puis ajoutez les deux routes home et skills
Ajoutez le routerOutlet dans le app.component.html
Ajoutez dans le menubar les deux routerLink (home et skills)
Ajoutez le routerLink dans le fichier not-found.component.html pour rediriger vers home.
git checkout -f step-05
Déplacez le tableau introductions dans le resolver
Récupérez la données dans le composant HomeComponent à l'aide de la classe ActivatedRoute
Injection de dépendance
import { Engine, Tires, Doors } from './shared';
class Car {
constructor(
private _engine: Engine,
private _tires: Tires,
private _doors: Doors
) {
}
}
Typescript
Injection de dépendance
Injection de dépendance
import { Injectable } from '@angular/core';
@Injectable()
export class MyService {
name: string;
constructor() {
this.name = "Angular";
}
getName(): string {
return this.name;
}
}Déclaration
import { Component, OnInit } from '@angular/core';
import {MyService} from './my.service';
@Component({
selector: 'app-my',
templateUrl: './my.component.html',
styleUrls: ['./my.component.scss'],
providers: [MyService] //Ou {provide: MyService, {useClass: MyService}}
})
export class MyComponent implements OnInit {
constructor(private myService: MyService) {
}
ngOnInit() {}
}Injection locale
import { NgModule } from '@angular/core';
import {SharedModule} from '../shared/shared.module';
import {MyResolverService} from './my-resolver.service';
import {MyService} from './my.service';
@NgModule({
imports: [
SharedModule
],
declarations: [],
exports: [],
providers: [MyResolverService, MyService] //Ou {provide: MyService, {useClass: MyService}}
})
export class MyModule { }
Injection globale
import { NgModule } from '@angular/core';
import {HttpClientModule} from '@angular/common/http';
@NgModule({
imports: [HttpClientModule],
...
});
import { Injectable } from '@angular/core';
import {HttpBackend, HttpClient} from '@angular/common/http';
@Injectable()
export class MyService {
constructor(private http: HttpClient) { }
getSkills(id: number): Observable<Skill>{
const headers = { 'Authorization': 'secret' };
const params = { 'page': '1' };
return this.http.get<Skill>('/api/skills', { headers, params });
}
}
var observer = rx.Observer.create(
function onNext(result){
console.log(result);
},
function onError(err){
console.log(err);
},
function onCompleted(){
console.log('Completed');
}
);
observable.subscribe(observer);Lecture d'un marble diagram
import { ajax } from "rxjs/observable/dom/ajax";
import { of } from "rxjs/observable/of";
import { catchError, map } from "rxjs/operators";
const name = ajax
.getJSON<{ name: string }>("/api/employees/alice")
.pipe(
map(employee => employee.name),
catchError(error => of(null))
);import { Injectable } from '@angular/core';
import {HttpBackend, HttpClient} from '@angular/common/http';
@Injectable()
export class MyService {
constructor(private http: HttpClient) { }
getSkills(id: number): Observable<string>{
const headers = { 'Authorization': 'secret' };
const params = { 'page': '1' };
return this.http
.get<Skill>('/api/skills', { headers, params })
.pipe(
map(skill => skill.name),
catchError(err => of(null))
);
}
}
Jasmine est un Framework de test JavaScript orienté Behavior-Driven Development, ou BDD.
Karma est un outil qui nous permet d'exécuter des navigateurs et d'exécuter des tests Jasmine
//helloWorld.ts
function helloWorld() {
return 'Hello world!';
}
//helloWorld.spec.ts
describe('Hello world', () => { (1)
it('says hello', () => { (2)
expect(helloWorld()) (3)
.toEqual('Hello world!'); (4)
});
});Exemple:
1 - describe(string, function): définit une série de tests
2 - it(string, function): définit un test individuel
3- expect(actual): expectation, ça décrit le comportement attendu
matcher(expected): la valeur attendu, une comparaison est faite
4 - expected: le test casse si le "actual" n'égale pas le "expected"
describe('Hello world', () => { (1)
it('says hello', () => { (2)
expect(helloWorld()) (3)
.toEqual('Hello world!'); (4)
});
});git checkout -f step-06
Créez un service : HomeService
Refactoring: Déplacez le code de récupération des introductions dans ce service
git checkout step-07
/**
* Introductions
*/
app.get('/api/intros', api.getIntros);
app.get('/api/intros/:id', api.getIntro);
app.put('/api/intros/:id', api.updateIntro);
/**
* Skills
*/
app.get('/api/skills', api.getSkills);
app.put('/api/skills/:id', api.updateSkill);
app.post('/api/skills', api.addSkill);
app.delete('/api/skills/:id', api.deleteSkill);git checkout step-07
# cd _server
# npm start
✔Express server listening on http://localhost:9000/
Démarrer le serveur
#npm install#npm startimport {Component, OnInit, Output, Input, EventEmitter} from '@angular/core';
import {FormGroup, FormControl} from "@angular/forms";
@Component({
selector: 'app-intro-form',
templateUrl: 'form.component.html',
styleUrls: ['form.component.scss']
})
export class IntroFormComponent implements OnInit {
@Input() model:any = {};
@Output() submit;
constructor() {
this.submit = new EventEmitter();
}
ngOnInit() {
console.log(model);
}
submit(model){
this.submit.emit(model);
}// parent.component.html
<app-intro-form [model]="model" (submit)="save($event)" > </app-intro-form>git checkout -f step-08
La gestion des formulaires a toujours été un des points forts d'Angular
Template Driven Forms Pilotée par les templates (simple) Model Driven Forms
Pilotée par le code (plus verbeux mais plus puissant)
valid : si le champ est valide avec les validations et contraintes qui lui sont appliquées errors : objet contenant les erreurs du champ dirty : false jusqu'à ce que l'utilisateur modifie le champ pristine : opposé de dirty touched : false jusqu'à ce que l'utilisateur soit entré et sorti du champ untouched : opposé de touched value : la valeur du champ valueChanges : un observable emettant la valeur du champ à chaque changement
const username = new FormControl('', {
validators: Validators.required,
updateOn: blur}); // Update only when remove focus
console.log(username.dirty);
console.log(username.hasError('required')); //trueNew
valid : si le formulaire est valide errors : objet contenant les erreurs du formulaire dirty : false jusqu'à ce que l'utilisateur modifie un des champs du formulaire pristine : opposé de dirty touched : false jusqu'à ce que l'utilisateur soit entré et sorti d'un des champs du formulaire untouched : opposé de touched value : la valeur du formulaire sous forme de clé/valeur avec le nom du champ en clé valueChanges : un observable emettant la valeur du formulaire à chaque changement sur un des champs
const form = new FormGroup({
username: new FormControl(),
password: new FormControl()
});
console.log(form.dirty);
console.log(form.value);
console.log(form.get('username'));
@Component({
selector: 'app-react-form',
templateUrl: './react-form.component.html'
})
export class ReactFormComponent {
loginForm: FormGroup;
constructor(formBuilder: FormBuilder) {
this.loginForm = formBuilder.group({username: '', password: ''});
}
register() {
console.log(this.loginForm.value);
}
}
<form (ngSubmit)="register(loginForm.value)" [formGroup]="loginForm">
<div class="form-group">
<label for="username">Nom d'utilisateur</label>
<input type="text" class="form-control"
id="username" formControlName="username"
placeholder="Nom d'utilisateur">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control"
id="password" formControlName="password"
placeholder="Mot de passe">
</div>
<button type="submit" class="btn btn-success">Se connecter</button>
</form>@Component({
selector: 'app-react-form',
templateUrl: './react-form.component.html'
})
export class ReactFormComponent {
loginForm: FormGroup;
constructor(formBuilder: FormBuilder) {
this.loginForm = formBuilder.group({username: '', password: ''});
this.loginForm.valueChanges.subscribe(form => console.log(form));
this.loginForm.get('username').valueChanges
.subscribe(username => console.log(username));
}
register() {
console.log(this.loginForm.value);
}
}
Validateurs: Retourne une map d'erreurs ou 'null' si aucune erreur est détecté
Quelques validateurs:
required: permet de vérifier que le champ n'est pas vide
minLength: permet de vérifier la longueur minimum
maxLength: permet de vérifier la longueur maximum
pattern: permet de vérfier le respect d'une regex
@Component({
selector: 'app-react-form',
templateUrl: './react-form.component.html'
})
export class ReactFormComponent {
loginForm: FormGroup;
constructor(formBuilder: FormBuilder) {
this.loginForm = formBuilder.group({
username: formBuilder.control('', [Validators.required,Validators.minLength(8)]),
password: formBuilder.control('', [Validators.required,Validators.pattern("[^ @]*@[^ @]*")])
});
}
}
<form (ngSubmit)="register(loginForm.value)" [formGroup]="loginForm">
<div class="form-group">
<label for="username">Nom d'utilisateur</label>
<mat-input-container
[ngClass]="{required: loginForm.get('username').hasError('required')}">
<input type="text" class="form-control"
id="username" formControlName="username"
placeholder="Nom d'utilisateur">
<mat-hint align="start" *ngIf="loginForm.get('username').hasError('required')">
La saisie du nom d'utilisateur est obligatoire
</mat-hint>
</mat-input-container>
</div>
<button type="submit" class="btn btn-success">Se connecter</button>
</form>export class ValidFormComponent {
loginForm: FormGroup;
constructor(formBuilder: FormBuilder) {
const isKnowUser= (control: FormControl) =>
['jmadih', 'rinfrayco']
.filter(v => v === control.value).length ? null : {unknownUser: true};
this.loginForm = formBuilder.group({
username: formBuilder.control('', [Validators.required,
Validators.minLength(3), isKnowUser]),
password: formBuilder.control('', Validators.required)
});
}
}Si la fonction renvoie un observable la validation deviendra asynchrone.
git checkout -f step-09
git checkout step-09
Restart Rest server
<p>{{ 'Formation Angular' | uppercase}}</p>
Affichera 'FORMATION ANGULAR'
<p>{{ 'Formation Angular' | lowercase}}</p>
Affichera 'formation angular' <p>{{users | slice:0:2 | json}}</p>
Affichera les 2 premiers éléments du tableau
<p>{{ 'Formation Angular' | slice:0:9}}</p> <!-- Formation -->
Affichera 'Formation'
<p>{{ 'Formation Angular' | slice:-7}}</p> <!-- Angular -->
Affichera 'Angular' <p>{{ 0.8 | percent }}</p> <!-- 80% -->
<p>{{ 0.8 | percent:'.3' }}</p> <!-- 80.000% --> <p>{{ 12345 | number }}</p> <!-- 12,345 -->
<p>{{ 12345 | number:'9.' }}</p> <!-- 000,012,345 -->
<p>{{ 12345 | number:'.2' }}</p> <!-- 12,345.00 -->
<p>{{ 12345.14 | number:'.1-1' }}</p> <!-- 12,345.1 -->
<p>{{ 12345.16 | number:'.1-1' }}</p> <!-- 12,345.2 --> <p>{{ users }}</p>
affichera [object Object].
Pour afficher la représentation JSON de l'objet :
<p>{{ users | json }}</p>
Équivalent de JSON.stringify
<p>{{ today | date:'dd/MM/yyyy'}}</p> <!-- 05/02/2017 -->
<p>{{ today | date:'longDate'}}</p> <!-- February 5, 2017 -->
<p>{{ today | date:'HH:mm:ss'}}</p> <!-- 13:48:43 -->
<p>{{ today | date:'shortTime'}}</p> <!-- 1:48 PM --><p>{{ 20.8 | currency:'EUR' }}</p> <!-- EUR20.80 -->
<p>{{ 20.8 | currency:'USD':true }}</p> <!-- $20.80 -->
<p>{{ 20.8 | currency:'USD':true:'.3' }}</p> <!-- $20.800 --> @Component({
selector: 'app-pipes',
template: `
<p> {{usersName$ | async}}</p>
`
})
export class MyComponent {
usersName$: Observable<string>;
} @Pipe({
name: 'fromNow'
})
export class FromNowPipe implements PipeTransform {
transform(value: Date): string {
return moment(value).fromNow();
}
}
usage
{{myDate | fromNow}}Créez un pipe UndefinedPipe qui permet d'afficher 'N/A' si aucune valeur n'est renseigner
Appliquez ce pipe sur le champs mail de l'introduction
WebComponents (Angular Element)
Et si on pouvait déployer mon widget Angular sur n'importe quel site ?
Ne serait-il pas formidable de pouvoir déployer une nouvelle version de notre widget indépendamment du cycle de publication des grandes applications?
HTTP
WebComponent A
WebComponent B
Application
main.js
main.js
import {BrowserModule} from '@angular/platform-browser';
import {DoBootstrap, Injector, NgModule} from '@angular/core';
import {AppComponent} from './app.component';
import {createCustomElement} from '@angular/elements';
import {NgCardElementComponent} from './ng-card-element/ng-card-element.component';
@NgModule({
declarations: [
AppComponent
],
entryComponents: [NgCardElementComponent],
imports: [
BrowserModule
]
})
export class AppModule implements DoBootstrap {
constructor(private injector: Injector) {}
ngDoBootstrap() {
const el = createCustomElement(NgCardElementComponent, {
injector: this.injector
});
customElements.define('times-up', el);
}
}
Déclaration du WebComponent
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Time's UP</title>
</head>
<body>
<times-up endtime="27/02/2020 18:00:00" title="Title"
subtitle="Subtitle" description="description">
</times-up>
</body>
<script src="URL/main.js"></script>
</html>
Utilisation du WebComponent