Une réécriture complète de ce bon vieil AngularJS
Pourquoi une réécriture complète ?
AngularJS ne suffisait-il donc pas ?
AngularJS n’est pas parfait,
concepts parfois
difficiles à cerner
Le web a bien évolué depuis
qu’AngularJS a été conçu
JavaScript a changé
De nouveaux
frameworks/libs sont
apparus avec de meilleures
idées et implémentations
Angular a été conçu pour
le web de demain :
avec ECMAScript 6
les Web Components
et le mobile en tête
(performances)
On ne dit plus Angular 2
Angular 3, 4, 5, etc.
It's just Angular
AngularJS vs Angular
JavaScript (JS) est une implémentation de ECMAScript
Une nouvelle version est apparue :
ECMASCRIPT 6, ES6, ou ECMASCRIPT 2015
ES6 est le nom le plus populaire
ES6 introduit deux nouveaux mot-clés
pour la déclaration de variables : let et const
let/const permettent de déclarer
des variables scopées
const ne produit des "vraies" constantes
qu'avec des primitives
function getMovieTitle(movie) {
if (movie.genre) {
let title = movie.title + ':' + movie.genre;
}
// title is not accessible here, error
console.log(title);
return movie.title;
}
const MOVIES_PER_PAGE = 20;
MOVIES_PER_PAGE = 15; // Error
const MOVIE = {};
MOVIE.title = 'Superman'; // OK
ES6 introduit les classes en JavaScript
class Movie extends Media {
constructor(title, genre = 'Action') {
super(title);
this.title = title;
this.genre = genre;
}
static defaultPopularity() {
return 10;
}
toString() {
return this.title + '(' + this.genre + ')';
}
}
let movie = new Movie('Titanic', 'Romance');
console.log(movie.toString()); // Titanic (Romance)
Ainsi que des vrais modules !
import defaultMember from 'module-name';
import * as name from 'module-name';
import { member } from 'module-name';
import { member as alias } from 'module-name';
import { member1 , member2 } from 'module-name';
import { member1 , member2 as alias2 , [...] } from 'module-name';
import defaultMember, { member [ , [...] ] } from 'module-name';
import defaultMember, * as name from 'module-name';
import 'module-name';
export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export let name1, name2, …, nameN; // also var
export let name1 = …, name2 = …, …, nameN; // also var, const
export expression;
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
Nouvelle syntaxe de fonctions utilisant
l’opérateur fat arrow : =>
Des template strings multilignes : backtick
Et bien plus encore : http://es6-features.org
// ES5
getUser(login)
.then(function (user) {
return getRights(user); // getRights returns a promise
})
.then(function (rights) {
return console.log('User rights: ' + rights.join(',') + '...' );
});
// ES6
getUser(login)
.then(user => getRights(user))
.then(rights => console.log(`User rights: ${rights.join(',')}...`))
Du typage dans JavaScript !
Des decorators
Sky is the limit...
interface MyFunction<T> {
(param1:T, param2?:string):void
}
enum MyEnum {
Trololo,
Trilili,
Tralala = 3
}
const myFunction:MyFunction<MyEnum> = function (param1, param2) {
console.log(param1, param2);
}
myFunction(MyEnum.Trololo); // 0, undefined
myFunction(MyEnum.Tralala, 'three'); // 3, 'three'
myFunction('hello'); // type error
bower install angular#1.6.4 angular-route#1.6.4 --save
angular.module('app-module', []);
<body ng-app="app-module">...</body>
npm install -g typescript && tsc --init
{
"compilerOptions": {
"target": "es5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"sourceMap": true,
"module": "commonjs",
"noImplicitAny": false,
"lib": ["es2016", "dom"],
"outDir": "dist",
"rootDir":"src"
}
}
tsc --watch
npm --init && npm install <PLEIN DE TRUCS ICI>
"dependencies": {
"@angular/common": "4.0.2",
"@angular/compiler": "4.0.2",
"@angular/core": "4.0.2",
"@angular/platform-browser": "4.0.2",
"@angular/platform-browser-dynamic": "4.0.2",
"@angular/http": "4.0.2",
"@angular/forms": "4.0.2",
"reflect-metadata": "0.1.10",
"rxjs": "5.3.0",
"zone.js": "0.8.5"
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
imports: [BrowserModule, HttpModule, FormsModule],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
function IntervalComponent($interval) {
var ctrl = this;
var interval = null;
ctrl.$onInit = function () {
interval = $interval(ctrl.onUpdate, ctrl.delay)
};
ctrl.$onDestroy = function () {
$interval.cancel(interval)
};
}
angular.module('app-module').component('intervalComponent', {
controller: ['$interval', IntervalComponent],
bindings: { delay: '=', onUpdate: '&' },
templateUrl: 'interval-component.html'
});
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'interval-component',
templateUrl: 'interval-component.html'
})
export class IntervalComponent implements OnInit, OnDestroy {
private interval:number = null;
ngOnInit():void {
this.interval = window.setInterval(() => this.onUpdate.emit(), this.delay);
}
ngOnDestroy():void {
window.clearInterval(this.interval);
}
@Input() delay:number = null;
@Output() onUpdate:EventEmitter<void> = new EventEmitter();
}
{{}} pour l’interpolation
Une grande liste de directives fournies par le framework à connaître (comme ng-class, ng-if, ng-styles, ng-switch, ng-click,
ng-submit, etc.)
{{}} pour l’interpolation
? pour le "fail safe"
[] pour le binding
de propriété
() pour le binding d’événement
# pour la déclaration
de variable locale
* pour les directives structurelles
<p>{{ user?.name }}</p>
<div [innerHTML]="unsafeProperty" [class.toto]="shouldAddTotoClass()"></div>
<button type="button" (click)="youClickedMe($event)">Click me</button>
<input type="text" #myInput>
<button type="button" (click)="myInput.focus()">Click me too</button>
<interval-component [delay]="2000" (onUpdate)="intervalUpdated()"></interval-component>
<ul *ngIf="movies.length">
<li *ngFor="let movie of movies; let i=index">{{ i }}-{{ movie }}</li>
</ul>
<ng-container [ngSwitch]="messageCount">
<p *ngSwitchCase="0">You have no message</p>
<p *ngSwitchCase="1">You have a message</p>
<p *ngSwitchDefault>You have some messages</p>
</ng-container>
angular.module('app-module').directive('textColor', function () {
return {
restrict: 'A',
scope: { textColor: '@' },
link: function (scope, element) {
element.css('color', scope.textColor);
element.on('mouseenter', function () {
element.css('color', null);
})
}
};
});
import { Directive, Input, HostBinding, HostListener } from '@angular/core';
@Directive({
selector: '[text-color]'
})
export class TextColorDirective {
@Input('text-color')
@HostBinding('style.color')
color:string = null;
@HostListener('mouseenter')
resetColor() {
this.color = null;
}
}
function ItemsService($http) {
var service = this;
function toData(response) {
return response.data;
}
service.fetchItems = function () {
var config = { params: { limit: 10 } };
return $http.get('api/items', config).then(toData);
};
}
angular.module('app-module').service('itemsService', ItemsService);
import { Injectable } from '@angular/core';
import { Http, Response, URLSearchParams } from '@angular/http';
import { Observable } from 'rxjs/Observable';
// Also add map, catch and throw for Observable
@Injectable()
export class ItemsService {
constructor(private http:Http) {}
fetchItems():Observable<any[]> {
let params = new URLSearchParams();
params.set('limit', '10');
return this.http.get('api/items', { params })
.map((response:Response) => response.json())
.catch(error => Observable.throw(error));
}
}
Le service HTTP d’Angular retourne des Observable
Il s’appuie sur RxJS
Les Observable sont des flux, ils ont donc une dimension temporelle et peuvent
être terminés
On doit également souscrire aux flux pour déclencher la récupération des données
On peut arrêter la
souscription à tout moment
Comme les Promise, les Observable peuvent être combinés, mais aussi transformés et chainés
La composition est donc très facile
Des Observable peuvent être créés
à partir de Promise et être
convertis en Promise
Il n’y a donc que des
avantages à s’en servir !
Sauf peut être importer (poids)
et apprendre (temps) RxJS
Exemple d’utilisation
d’RxJS : live search
Ne pas envoyer de requête
tant que l’utilisateur est
en train d’entrer des
caractères dans le champ
Ne pas envoyer de requête
si la query ne change pas
Gérer les retours
asynchrones dans l’ordre
Tout ceci est relativement
compliqué sans RxJS
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { SearchService } from './search.service';
@Component({
selector: 'search-cmp',
template: `
<h1>Search</h1>
<input #term (keyup)="search(term.value)">
<ul><li *ngFor="let result of results | async">{{ result }}</li></ul>
`
})
export class SearchComponent {
private searchStream = new Subject<string>();
results:Observable<string[]>;
constructor(private searchService:SearchService) {
this.items = this.searchStream
.debounceTime(300).distinctUntilChanged()
.switchMap((query:string) => this.searchService.search(query));
}
search(query:string) { this.searchStream.next(query); }
}
<p>{{ 12345 }}</p>
<!-- will display '12345' -->
<p>{{ 12345 | number }}</p>
<!-- will display '12,345' -->
<p>{{ 12345 | number:'6.' }}</p>
<!-- will display '012,345' -->
<p>{{ 12345 | number:'.2' }}</p>
<!-- will display '12,345.00' -->
<p>{{ 12345.13 | number:'.1-1' }}</p>
<!-- will display '12,345.1' -->
<p>{{ 12345.16 | number:'.1-1' }}</p>
<!-- will display '12,345.2' -->
import {
Pipe,
PipeTransform
} from '@angular/core';
@Pipe({ name: 'rating' })
export class CustomPipe
implements PipeTransform {
transform(value, args) {
let stars: string = '';
for (let i = 0; i < value; i++) {
stars += '☆';
}
return stars;
}
}
Angular introduit un pipe nommé async
Ce pipe fonctionne aussi bien sur
des Promise que des Observable
Il souscrit aux changements et marque automatiquement le composant
comme modifié
Le composant sera ensuite « re-rendu »
async gère aussi l’arrêt de la souscription
<ul><li *ngFor="let result of results | async">{{ result }}</li></ul>
Propose deux façons d’écrire les formulaires :
formulaire piloté par le template
formulaire piloté par le modèle (usage avancé)
Propose trois façons d'utiliser
la directive ngModel
Le two-way binding est déconseillé
<input type="text" name="userName" ngModel>
<input type="text" name="userName" [ngModel]="user.name">
<input type="text" name="userName" [(ngModel)]="user.name">
<input type="text" name="userName" [ngModel]="user.name" (ngModelChange)="user.name = $event">
<style>
input.ng-invalid {
border-left: 5px red solid;
}
input.ng-valid {
border-left: 5px green solid;
}
</style>
<h2>Sign up</h2>
<form (ngSubmit)="register(userForm.value)" #userForm="ngForm">
<div>
Username:
<input name="username" ngModel required maxlength="10" #username="ngModel">
<p *ngIf="username.control.dirty && !username.control.valid">Invalid username</p>
</div>
<div>
Password:
<input type="password" name="password" ngModel required minlength="6" #password="ngModel">
<p *ngIf="password.control.dirty && !password.control.valid">Invalid password</p>
</div>
<div>
Phone:
<input name="phone" ngModel required pattern="06[0-9]{8}" #phone="ngModel">
<p *ngIf="phone.control.dirty && !phone.control.valid">Invalid phone number</p>
</div>
<button type="submit" [disabled]="!userForm.valid">Register</button>
</form>
AngularJS fait du dirty checking
Souvent pointé du doigt pour
des soucis de performance
Cycles de digestion
potentiellement lourds
Solution « temporaire » :
one way binding (::)
Nécessité d’appeler
$scope.$apply dans les
callbacks de libs tierces
Dans tous les cas, les changements
dans le modèle sont simples à tracer
Soit une action utilisateur :
event (click, submit, etc.)
Soit le retour d’un appel backend :
XHR, Sockets
Soit le passage d’un timer :
setTimeout, setInterval
Changements toujours asynchrones
La détection de changements entièrement revue
La circulation des données aussi (prop + event)
Angular s’appuie sur Zone.js
« Portage » des zones de Dart
Les zones interceptent les appels asynchrones
Et offrent des hooks pour réagir en conséquence
La détection est donc transparente, rapide
et fonctionne sans $apply (librairies tierces)
Plusieurs gros changements…
TypeScript + Polyfills
Configuration et démarrage
plus complexe qu’AngularJS
Beaucoup d’outillage
(transpilation, packaging, etc.)
Framework complet
!== Library (cf React)
Mais code globalement
plaisant à écrire
Merci aussi à Youness Mourad
Sebastien
Pittion
Savinien
Richter