Craftsmanship JS
Ce n'est pas nécessaire de travailler à la maison
0 si le projet ne démarre pas
- Outillage
- L'utilisation de l'ensemble des notions vu en cours
- La qualité du code (TypeScript, Angular, HTML, CSS)
- La qualité des tests
- Méthodologie
- Fun (bonus)
Formation TypeScript
Git
NPM
TypeScript
WebPack
Jasmine + Karma
Test E2E
TSLint
...
$ npm install -g @angular/cli
$ ng new Application
$ ng serve
$ ng build
Création de l'application :
Démarrage de l'application (pour développer) :
Construction de l'application (pour production) :
Démarrage des tests (unitaire) :
$ ng test
Formez des groupes de 3
Initialisez le projet Angular grâce à Angular-cli
Regardez la structure de fichier puis changez le titre de la page
Migrez les tests Jasmine + karma vers Jest
Lancez les tests Jasmine + Karma
Observez la différence
@Component({
selector: 'css-selector',
template: `<div>Hello World</div>`,
styles: [`
div { color: red; }
`]
})
export class AppComponent { }
Composant
Vue
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
}
Composant
Vue
@Component({
selector: 'like-button',
template: `<button>Like</button>`,
})
export class LikeButton { }
Composant
Vue
<div>
<h1>A tweet</h1>
<like-button></like-button>
</div>
<!-- attention, <like-button/> ne fonctionne pas -->
@Component({
selector: 'like-button',
template: `<button>{{nbLikes}}</button>`,
})
export class LikeButton {
nbLikes = 0;
}
Composant
Vue
État
@Component({
selector: 'like-button',
template: `<button>{{nbLikes++}}</button>`,
})
export class LikeButton {
nbLikes = 0;
}
Composant
Vue
État
Ne jamais modifier l'état dans une expression !
@Component({
selector: 'like-button',
template: `<button type="button" [disabled]="isLiked">{{nbLikes}}</button>`,
})
export class LikeButton {
nbLikes = 0;
isLiked = true;
}
Composant
Vue
État
[PROPERTY-NAME]="VALUE"
Propriétés
@Component({
selector: 'like-button',
template: `
<button
type="button"
[disabled]="isLiked"
[class.liked]="isLiked"
[style.color]="isLiked ? 'red' : 'grey'"
>
{{nbLikes}}
</button>`,
})
export class LikeButton {
nbLikes = 0;
isLiked = true;
}
Composant
Vue
État
Propriétés
@Component({
selector: 'cell-table',
template: `<td [colspan]="Math.floor(Math.random() * 10)">help</td>`,
})
export class CellTable {
}
Composant
Vue
État
Attention, DOM !== attributs HTML
Propriétés
@Component({
selector: 'cell-table',
template: `<td [attr.colspan]="Math.floor(Math.random() * 10)">help</td>`,
})
export class CellTable {
}
Composant
Vue
État
Attributs HTML accessible via [attr.atributHTML]
Propriétés
@Component({
selector: 'tweet',
template: `
<div>
<p>{{tweet.message?.text}}</p>
<like-button [nbLikes]="tweet.likes"></like-button>
</div>`,
})
export class TweetComponent {
tweet: any = {
likes: 10
};
}
Composant
Vue
État
[PROPERTY-NAME]="VALUE"
Propriétés
@Component({
selector: 'like-button',
template: `<button type="button" [disabled]="isLiked">{{nbLikes}}</button>`,
})
export class LikeButton {
@Input() nbLikes = 0;
isLiked = true;
}
Composant
Vue
État
[PROPERTY-NAME]="VALUE"
Propriétés
@Component({
selector: 'like-button',
template: `
<button
type="button"
[disabled]="isLiked"
[class.liked]="isLiked"
[style.color]="isLiked ? 'red' : 'grey'"
>
{{likes}}
</button>`,
})
export class LikeButton {
@Input('nbLikes') likes = 0;
isLiked = true;
}
Composant
Vue
État
Surcharge des noms de propriétés
Propriétés
import { Input, Component } from "@angular/core";
Composant
Vue
État
import viennent de @angular/core
Propriétés
Là où se passe l'inversion de contrôle
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { LikeButtonComponent } from './like-button/like-button.component';
@NgModule({
declarations: [
AppComponent,
LikeButtonComponent
],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Création d'un composant avec la commande
$ ng generate component my-component-name
Création d'un composant dans un dossier avec la commande
$ ng generate component folder/my-component-name
Composant
Vue
État
Propriétés
Paramètre d'une fonction
Fonction
Retour d'une fonction
Live coding !
@Component({
selector: 'like-button',
template: `
<button type="button" (click)="handleClick()>
{{likes}}
</button>
`,
})
export class LikeButton {
@Input('nbLikes') likes = 0;
handleClick() {
this.likes++;
}
}
Composant
Vue
État
Propriétés
@Component({
selector: 'like-button',
template: `
<button type="button" (click)="handleClick()>
{{likes}}
</button>
`,
})
export class LikeButton {
@Input('nbLikes') likes = 0;
handleClick() {
this.likes++;
}
}
Composant
Vue
État
Propriétés
@Component({
selector: 'like-button',
template: `
<button type="button" (click)="handleClick()>
{{likes}}
</button>
`,
})
export class LikeButton {
@Input('nbLikes') likes = 0;
@Output() onLike = new EventEmitter<undefined>();
handleClick() {
this.onLike.emit();
}
}
Composant
Vue
État
Propriétés
@Component({
selector: 'tweet',
template: `
<div>
<p>{{tweet.message?.text}}</p>
<like-button [nbLikes]="tweet.likes" (onLike)="handleLike()"></like-button>
</div>`,
})
export class TweetComponent {
tweet: any = {
likes: 10
};
handleLike():void {
this.tweet.likes++;
}
}
Composant
Vue
État
Propriétés
@Component({
selector: 'like-button',
template: `
<button type="button" (click)="handleClick()>
{{likes}}
</button>
`,
})
export class LikeButton {
@Input('nbLikes') likes = 0;
@Output('like') onLike = new EventEmitter<undefined>();
handleClick() {
this.onLike.emit();
}
}
Composant
Vue
État
Propriétés
On peut renommer les événements
@Component({
selector: 'like-button',
template: `
<button type="button" (click)="handleClick()>
{{likes}}
</button>
`,
})
export class LikeButton {
@Input('nbLikes') likes = 0;
@Output('like') onLike = new EventEmitter<number>();
handleClick() {
this.onLike.emit(this.likes);
}
}
Composant
Vue
État
Propriétés
On peut passer des informations
@Component({
selector: 'tweet',
template: `
<div>
<p>{{tweet.message?.text}}</p>
<like-button [nbLikes]="tweet.likes" (onLike)="handleLike($event)"></like-button>
</div>`,
})
export class TweetComponent {
tweet: any = {
likes: 10
};
handleLike(initialLike: number):void {
this.tweet.likes = initialLike + 1;
}
}
Composant
Vue
État
Propriétés
<input
[value]="currentHero.firstName"
(input)="currentHero.firstName = $event.target.value"
/>
Composant
Vue
État
Propriétés
Composant
Vue
État
Propriétés
Le test des événements sert à observer le comportement du composant lors de modification de l'état du composant
import { Component, OnInit } from '@angular/core';
@Component({ selector: 'user', /* ... */ })
export class UserComponent implements OnInit {
@Input() data: User;
products: Product[];
ngOnInit(): void {
this.products = this.getProducts(this.data.id);
}
getProducts(id){ ... }
}
@Component({
selector: "app-post",
template: `
<article>
<ng-content></ng-content>
</article>
`
})
export class PostComponent {}
<!-- HTML Du Composant Parent -->
<app-post>
<h2>Title</h2>
<p>Content</p>
</app-post>
Directive = Composant sans template
Ajoute un comportement
import { Directive, ElementRef, Renderer2 } from '@angular/core';
@Directive({
selector: '[myHighlight]'
})
export class HighlightDirective {
constructor(element: ElementRef, renderer: Renderer2) {
//element.nativeElement.style.backgroundColor = 'yellow';
renderer.setStyle(element.nativeElement, 'backgroundColor', 'yellow');
}
}
<p myHighlight>
Highlight me!
</p>
import { Directive, HostListener, HostBinding } from '@angular/core';
@Directive({ selector: '[myHighlight]' })
export class HighlightDirective {
@HostBinding('style.backgroundColor') color = 'red';
constructor() { ... }
@HostListener('mouseenter') onMouseEnter() { this.color = 'blue'; }
@HostListener('mouseleave') onMouseLeave() { this.color = 'red'; }
}
<p myHighlight>
Highlight me!
</p>
import { Directive } from '@angular/core';
@Directive({
selector: '[myHighlight]',
host: {
'[style.backgroundColor]': 'color',
'(mouseenter)': 'highlight()',
'(mouseleave)': 'restoreColor()',
}
})
export class HighlightDirective {
color = ''
highlight() { this.color = 'yellow'; }
restoreColor() { this.color = ''; }
}
<p myHighlight>
Highlight me!
</p>
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HighlightDirective } from './highlight.directive';
@NgModule({
declarations: [
HighlightDirective
],
imports: [
BrowserModule
]
})
export class AppModule {}
Même chose que pour les composant
Il faut déclarer la directive dans le module
Ensemble de directives utilitaire
import { Component } from '@angular/core';
@Component({
selector: 'ngStyle-example',
template: `
<h1 [ngStyle]="{'font-size': size}">
Title
</h1>
<label>Size:
<input type="text" [value]="size" (input)="size = $event.target.value">
</label>
`
})
export class NgStyleExample {
size = '20px';
}
import { Component } from '@angular/core';
@Component({
selector: 'toggle-button',
template: `
<div [ngClass]="{'highlight': isHighlighted}"></div>
<button (click)="toggle(!isHighlighted)">Click me!</button>
`,
styles: [
`.highlight { ... }`
]
})
class ToggleButton {
isHighlighted = false;
toggle(newState) {
this.isHighlighted = newState;
}
}
D'autres syntaxes :
[ngClass]="'class class1'"
[ngClass]="['class', 'class1']"
[ngClass]="{'class': isClass, 'class1': isClass1}"
<ng-template ngFor [ngForOf]="items" let-item>
<li> {{ item.label }} </li>
</ng-template>
<ng-template ngFor [ngForOf]="items" let-item let-i="index" let-isOdd="odd" let-first="first">
<li [ngClass]="{odd: isOdd, first}"> {{i}} : {{ item.label }}</li>
</ng-template>
D'autres variables sont disponible : index, first, last, even et odd
<li *ngFor="let item of items; let i = index">
{{i}} : {{ item.label }}
</li>
Impossible de mettre la classe comme avant avec cette syntaxe
<li *ngFor="let item of items; let i = index; let first = first;" [ngClass]="{first}" >
{{i}} : {{ item.label }}
</li>
<div *ngIf="condition">...</div>
<ng-template [ngIf]="condition">
<div>...</div>
</ng-template>
NG ELSE
<div *ngIf="condition; else elseBlock">...</div>
<ng-template #elseBlock>No data</ng-template>
Hidden
<div [hidden]="condition">...</div>
<div [ngSwitch]="value">
<p *ngSwitchCase="0">0, increment</p>
<p *ngSwitchCase="1">1, stop incrementing</p>
<p *ngSwitchDefault>> 1, STOP!</p>
</div>
C
C
C
C
C
C
C
State
State
State
State
State
State
State
MVC à la rescousse
C
C
C
C
C
C
C
State
Service
Dans angular, les services sont des singletons
import { UserService } from './user.service'
@Component({
/* ... */
})
export class AppComponent {
constructor(private userService: UserService) {
console.log(userService.getUser());
}
}
import { Injectable } from '@angular/core';
import { Logger } from './logger-service';
@Injectable()
export class UserService {
constructor(private logger: Logger) { }
getUsers(): Promise<User> {
this.logger.log('getUsers called!');
// ...
}
}
import { AppComponent } from './application.components';
import { UserService } from './user.service';
@NgModule({
declarations: [ AppComponent ],
providers: [ UserService ]
})
export class AppModule { }
// fichier application.component.ts
import { UserService } from './user.service'
@Component({
providers: [ UserService ]
})
export class AppComponent {
constructor(private userService: UserService) {
console.log(userService.getUser());
}
}
export function serverConfigFactory(appService: AppService){
return appService.getConfig();
}
@NgModule({
providers: [
UserService, // Le plus simple et le plus courant : une classe
{
provide: LoginService, // Pour un élément de ce type
useClass: LoginServiceImpl // Utiliser cette classe (ou implémentation)
},
{
provide: ServerConfig, // Pour un élément de ce type
useFactory: serverConfigFactory, // Utiliser une fonction factory
deps: [ AppService ] // La factory peut elle même avoir des injections
}
]
})
export class AppModule { }
Les différentes manières de déclarer un services
// Fichier app.module.ts
const apiUrl: string = 'api.heroes.com';
const env: string = 'dev';
@NgModule({
declareations: [ AppComponent ],
providers: [
{ provide: 'apiUrl', useValue: apiUrl },
{ provide: 'env', useValue: env }
]
})
export class AppModule { }
// Fichier app.component.ts
@Component({/* ... */})
class AppComponent {
constructor( @Inject('apiUrl') private api: string ) { ... }
}
Les différentes manières de déclarer un services
En réalité, les services sont des singleton au niveau de leur injecteur
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor() { }
}
Angular 6, une syntaxe raccourci pour ne pas avoir à injecter manuellement un service
import {TestBed, async} from '@angular/core/testing';
import {UserService} from './user.service';
describe('UserService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
UserService,
{ provide: LoggerService, useValue: { log: jasmine.createSpy() } }
]
});
});
it('should return 1 user', async(() => {
const service = TestBed.get(UserService);
service.getUsers().then(users => {
expect(users.length).toBe(1);
});
}));
});
Ça se test !
Déplacez la logique de vos composants (app.component.ts) vers vos services
Idem que les Pipes Unix
Sert à transformer des données
S'utilise côté template
{{ variable | pipeName | pipe2Name:pipeArg1:pipeArg2 }}
{{ new Date() | date }}
// Friday, april 15, 1988
{{ 42 | currency:'EUR':'symbol' | upperCase }}
// 42,00 €
import { isString, isBlank } from '@angular/core/src/facade/lang';
import { PipeTransform, Pipe } from '@angular/core';
@Pipe({ name: 'randomCasePipe' })
export class RandomCasePipe implements PipeTransform {
transform(value: any, param1:string, param2:string): string {
if (isBlank(value)) {
return value;
}
if (!isString(value)) {
throw new Error('MyLowerCasePipe value should be a string');
}
return value
.split('')
.map(letter => Math.random() > 0.5 ? letter.toLowerCase() : letter.toUpperCase())
.join('');
}
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { randomCasePipe } from './randomcase.pipe';
@NgModule({
declarations: [
randomCasePipe
],
imports: [
BrowserModule
]
})
export class AppModule {}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { randomCasePipe } from './randomcase.pipe';
@NgModule({
declarations: [
randomCasePipe
],
providers: [
randomCasePipe
],
imports: [
BrowserModule
]
})
export class AppModule {}
import { Component } from '@angular/core';
import { randomCasePipe } from './randomCasePipe';
@Component({
selector: 'app',
})
class App {
name: string;
constructor(randomCase: randomCasePipe) {
this.name = randomCase.transform('Hello Angular');
}
}
Une fonction pure et une fonction impure ?
Une pipe pure et une pipe impure ?
@Pipe({
name: 'myImpurePipe',
pure: false
})
export class MyImpurePipe implements PipeTransform {
transform(value: any): any {
value.piped = true;
return value;
}
}
@Component({
selector: 'pipes',
template: '{{ promise | async }}'
})
class PipesAppComponent {
promise: Promise;
constructor() {
this.promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Hey, this is the result of the promise");
}, 2000);
});
}
}
Ça vous parle ?
Ça vous parle ?
Du coup, ajoutons aux streams les mêmes fonctions qu'il y a pour les tableaux !
Les observables
import { Observable } from "rxjs";
new Observable(subscriber => {
subscriber.next('Rxjs'); // string
subscriber.next(2018); // number
subscriber.next('training'); // string
});
Les observables, le raccourci
import { from } from 'rxjs';
const myArray = [1, 2, 3]
const myObservableArray = from(myArray)
const myPromise = new Promise(resolve => resolve('Hello RxJS!'))
const myObservablePromise = from(myPromise)
Souscription
import { Observable } from "rxjs";
const myObservable = new Observable(subscriber => {
subscriber.next('Hello');
subscriber.next('Observable');
subscriber.next(2);
});
myObservable.subscribe(response => {
console.log(response)
})
// => 'Hello'
// 'Observable'
// 2
Souscription
import { Observable } from "rxjs";
const myObservable = new Observable(subscriber => {
subscriber.next('Hello');
subscriber.next('Observable');
subscriber.next(2);
});
myObservable.subscribe({
next: next => console.log('onNext: %s', next),
error: error => console.error('onError: %s', error),
complete: () => console.log('onCompleted')
})
unsubscribe
import { Component, OnDestroy } from "@angular/core";
import { Observable, Subscriber } from "rxjs";
@Component({ ... })
export class AppComponent implements OnDestroy {
private subscriber: Subscriber;
constructor() {
const source = new Observable(observer => {
const interval = setInterval(() => observer.next('TICK'), 1000);
return () => {
observer.complete();
clearInterval(interval);
};
});
this.subscriber = source.subscribe(value => console.log(value));
}
ngOnDestroy() { this.subscriber.unsubscribe(); }
}
Les opérateurs - map
import { of } from "rxjs";
import { map } from "rxjs/operators";
const myObservable = of(1, 2, 3);
myObservable.pipe(map(x => x * 10)).subscribe(console.log);
Les opérateurs - filter
import { from } from "rxjs";
import { filter } from "rxjs/operators";
const myArray = [1, 2, 3, 4, 5];
from(myArray)
.pipe(filter(element => element > 3))
.subscribe(console.log)
Les opérateurs - error
import { interval, of } from "rxjs";
import { map, catchError } from "rxjs/operators";
const source = from([1, 2, 3, 4, 5, 6]).pipe(
map(value => {
if (value > 5) {
throw new Error("Error detected!");
}
return value;
}),
catchError(error => of(5))
);
source.subscribe({
next: value => console.log(value),
error: err => console.error(err.message),
complete: () => console.log(`We're done here!`)
});
Un exemple complet
function getDataFromNetwork(): Observable<SomeClass> {
/* ... */
}
function getDataFromAnotherRequest(arg: SomeClass): Observable<SomeOtherClass> {
/* ... */
}
getDataFromNetwork()
.pipe(
debounce(300),
filter((rep1) => rep1 !== null),
mergeMap((rep1) => {
return getDataFromAnotherRequest(rep1);
}),
map((rep2) => rep2.data)
)
.subscribe((value) => console.log(`next => ${value}`));
Avec Angular
Ça se test
function race(cars: string[], numberOfLap: number): Observable<string> {
return interval(1000)
.pipe(
map(lap => {
if(lap < numberOfLap) {
return 'race still running';
}
return `${cars[0]} won`;
})
)
}
it('should return winner when battle is over', (done) => {
const subscriber = race(['mario', 'luigi'], 0)
.subscribe(
(raceLog) => {
expect(racelog).toBe('mario won')
subscriber.unsubscribe();
done();
}
)
});
Transformez vos setInterval ou setTimeout vers un Observable
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { LikeButtonComponent } from './like-button/like-button.component';
@NgModule({
declarations: [
AppComponent,
LikeButtonComponent
],
imports: [BrowserModule, HttpClientModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Avant de commencer
Avant de commencer
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Person } from './model/person';
@Injectable()
export class ContactService {
constructor(private http: HttpClient){ }
getContacts(): Observable<Person[]> {
return this.http.get<Person[]>('people.json');
}
}
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Contact } from './model/contact';
@Injectable()
export class ContactService {
constructor(private http: HttpClient) { }
save(contact: Contact): Observable<Contact> {
const headers = new HttpHeaders();
headers.set('Authorization', 'xxxxxxx');
const requestOptions: RequestOptionsArgs = {
headers
};
return this.http.put(`rest/contacts/${contact.id}`, contact, requestOptions);
}
}
import {Component} from '@angular/core';
import {ContactService} from './contact.service';
@Component({
selector: 'app',
template: '{{ displayedData | json }}'
})
export class AppComponent {
displayedData: Array<Contact>;
constructor(private contactService: ContactService) {
contactService.getContacts().subscribe(contacts => {
this.displayedData = contacts;
});
}
}
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class HeaderInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>{
const clone = req.clone({ setHeaders: {'Authorization': `token ${TOKEN}`} });
return next.handle(clone);
}
}
Les intercepteurs
import { NgModule } from '@angular/core';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { HeaderInterceptor } from './header.interceptor';
@NgModule({
providers: [{
provide: HTTP_INTERCEPTORS,
useClass: HeaderInterceptor,
multi: true,
}],
})
export class AppModule {}
Les intercepteurs
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed, async } from '@angular/core/testing';
describe('UserService', () => {
beforeEach(() => TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
}));
it('should return 1 user', async(() => {
const userService = TestBed.get(UserService);
const http = TestBed.get(HttpTestingController);
const mockedUsers = [{ name: 'Florent' }];
userService.getUsers().subscribe((users: User[]) => {
expect(users.length).toBe(1);
});
http.expectOne('/api/users').flush(mockedUsers);
});
});
Les tests
Récupérez les information sur vos pokemons depuis une API (seulement 2 pokemons)
Une forte adaptation des données est nécessaire, utilisez le pattern adapter si vous en sentez le besoin
Qu'est ce qu'une SPA ?
Pourquoi faire une SPA ?
Quels sont les limites de cette architecture applicative ?
Client
Page web
Serveur
html + css + js + Data
Client
APP Angular
Serveur
Statique
Ngnix, GH-page, surge.sh, ...
html + css + js
API
Java, Node, ...
data (Json)
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent, ContactsComponent, ContactComponent } from './pages';
const routes: Routes = [
{ path: '', component: HomeComponent }, // path: '/'
{ path: 'contacts', component: ContactsComponent },
{ path: 'contact/:id', component: ContactComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(routes)
]
})
export class AppModule { }
Où s'affichera le composant HomeComponent ?
import { Component } from '@angular/core';
@Component({
template: `
<header><h1>Title</h1></header>
<router-outlet></router-outlet>
`
})
export class AppComponent { }
Dans le composant Router Outlet !
@Component({
template: `
<nav>
<ul>
<li><a routerLink="contacts">Link 1</a></li>
<li><a [routerLink]="['contact', 1]">Link 2</a></li>
<li><a [routerLink]="['contact', id]">Link 3</a></li>
</ul>
</nav>
<router-outlet></router-outlet>
`
})
export class AppComponent {
id = 2;
}
Les liens ne doivent plus être des href !
import { RouterModule, Routes } from '@angular/router';
import { ContactComponent, EditComponent, ViewComponent } from './pages';
const routes: Routes = [
{
path: 'contact/:id', component: ContactComponent, children: [
{path: 'edit', component: EditCmp},
{path: 'view', component: ViewCmp}
]
}
];
const routing = RouterModule.forRoot(routes);
Hiérarchie des routes
@Component({
template: `
<nav>
<ul>
<button type="button" (click)="handleClick()">Link</button>
</ul>
</nav>
<router-outlet></router-outlet>
`
})
export class AppComponent {
constructor(public router: Router) {}
handleClick() {
this.router.navigate(['/contact/edit/1']);
}
}
Navigation côté TypeScript
Quel est la mauvaise pratique dans cette exemple ?
import { HashLocationStrategy, LocationStrategy } from '@angular/common';
@NgModule({
providers: [{ provide: LocationStrategy, useClass: HashLocationStrategy }]
})
export class AppModule { }
HashLocationStrategy
// Inclure <base href="/"> dans la balise head de votre html
// Ou alors :
import { Component } from '@angular/core';
import { APP_BASE_HREF } from '@angular/common';
@NgModule({
providers: [{ provide: APP_BASE_HREF, useValue: '/' }],
})
export class AppModule { }
PathLocationStrategy
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
@Component({
template: "userid : {{id}}"
})
export class ProductComponent implements OnInit {
id: string = "Musique d'ascenseur";
constructor(private route: ActivatedRoute) { }
ngOnInit() {
this.route.params
.subscribe((params: Params): void => {
this.id = Number(params.id);
});
}
}
Récupérer les paramètres d'URL
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
@Component({
template: "userid : {{id}}"
})
export class ProductComponent implements OnInit {
id: string = "Musique d'ascenseur";
constructor(private route: ActivatedRoute) { }
ngOnInit() {
const snapshot: ActivatedRouteSnapshot = this.route.snapshot;
this.id = Number(snapshot.params.id);
}
}
Récupérer les paramètres d'URL
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, UrlTree} from '@angular/router';
import { AuthService } from './auth.service';
import { AdminComponent } from './admin.component';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) { }
canActivate(route: ActivatedRouteSnapshot): boolean | UrlTree {
if(this.authService.isLoggedIn()) return true;
return this.router.parseUrl( '/login' );
}
}
Guards
import { CanActivate, Router, Routes } from '@angular/router';
const routes: Routes = [
{ path: 'admin', component: AdminComponent, canActivate: [ AuthGuard ] }
];
Les templates en .html sont en réalité du JS
Just In Time -> Compilation directement chez le client (dev)
Ahead Of Time -> pré compilation au build (prod)
La force d'angular
2 manières différentes :
<input
[value]="currentHero.firstName"
(input)="currentHero.firstName = $event.target.value"
>
<input
[ngModel]="currentHero.firstName"
(ngModelChange)="currentHero.firstName=$event"
>
<input
[(ngModel)]="currentHero.firstName"
>
Banana In the Box
@Component({
selector: 'contact-form',
template: `
<form (submit)="saveForm()">
<label>
Name:
<input type="text" [(ngModel)]="contact.name" name="name">
</label>
<button type="submit">Save</button>
</form>
`
})
export class ContactFormComponent implements OnInit {
contact: Contact = new Contact;
constructor(private contactService: ContactService) { }
ngOnInit(): void {
this.contactService.load().subscribe(contact => this.contact = contact);
}
saveForm(): void {
this.contactService.save(this.contact).subscribe();
}
}
<form (submit)="saveForm()" novalidate>
<label>
Name:
<input type="text" [(ngModel)]="contact.name" name="name">
</label>
<button type="submit">Save</button>
</form>
Désactive la validation du navigateur
<form (submit)="saveForm()" novalidate>
<label>
Name:
<input
type="text"
[(ngModel)]="contact.name"
name="name"
#nameInput="ngModel"
>
</label>
<span [hidden]="nameInput.valid">Error</span>
<button type="submit">Save</button>
</form>
Validation des composants
<form (submit)="saveForm()" novalidate>
<label>
Name:
<input
type="text"
[(ngModel)]="contact.name"
name="name"
required
#nameInput="ngModel"
>
</label>
<span [hidden]="nameInput.pristine && nameInput.valid">Error</span>
<button type="submit">Save</button>
</form>
L'état d'un champ
<form (submit)="saveForm()" novalidate>
<label>
Name:
<input
type="text"
[(ngModel)]="contact.name"
name="name"
required
#nameInput="ngModel"
>
</label>
<span [hidden]="!nameInput.errors?.required">Name is required</span>
<button type="submit">Save</button>
</form>
Un message différent par erreur
<form (submit)="saveForm()" #contactForm="ngForm" novalidate>
<label>
Name:
<input
type="text"
[(ngModel)]="contact.name"
name="name"
required
#nameInput="ngModel"
>
</label>
<span [hidden]="!nameInput.errors?.required">Name is required</span>
<button type="submit" [disabled]="contactForm.invalid" >Save</button>
</form>
Validation du formulaire
@Directive({
selector: '[pattern][ngModel]',
providers: [
{ provide: NG_VALIDATORS, useExisting: PatternValidator, multi: true }
]
})
export class PatternValidator implements Validator {
@Input('pattern') pattern: string;
validate(control: AbstractControl): { [key: string]: any } {
if (control.value && control.value.match(new RegExp(this.pattern))) {
return null;
}
return { pattern: true };
}
}
Custom Validator
Data driven form
import {NgModule} from "@angular/core";
import {ReactiveFormsModule} from "@angular/forms";
@NgModule({
/* ... */
imports: [ReactiveFormsModule],
})
export class AppModule {}
@Component({ /* ... */ })
export class SignupReactiveComponent implements OnInit {
signupForm: FormGroup;
user: User;
ngOnInit() {
this.signupForm = new FormGroup({
username: new FormControl('', Validators.required)
});
}
save() {
if (this.signupForm.valid) {
/* save user */
}
}
}
<form novalidate [formGroup]="signupForm" (submit)="save()">
<div
[ngClass]="{
'has-error': (
signupForm.get('username').touched ||
signupForm.get('username').dirty)
&& signupForm.get('username').invalid
}"
>
<label>Username *</label>
<div>
<input type="text" name="username" formControlName="username"/>
<span *ngIf="(
signupForm.get('username').touched ||
signupForm.get('username').dirty) &&
signupForm.get('username').errors"
>
<span *ngIf="signupForm.get('username').errors?.required">Username is required</span>
</span>
</div>
</div>
<div>
<button [disabled]="signupForm.invalid">Submit</button>
</div>
</form>
<form (ngSubmit)="onSubmit()" [formGroup]="form">
<div *ngFor="let question of questions">
<label [attr.for]="question.key">{{question.label}}</label>
<div [ngSwitch]="question.controlType">
<input
*ngSwitchCase="'textbox'"
[formControlName]="question.key"
[id]="question.key"
[type]="question.type"
>
<!-- other types -->
</div>
<div *ngIf="!isValid">{{question.label}} is required</div>
<div>
<button type="submit" [disabled]="!form.valid">Save</button>
</div>
</form>
Ajouter un formulaire pour créer et faire combattre votre Pokemon
Framework JS et le syndrome de la page blanche
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>AngularBdd</title>
<base href="/">
<meta
name="viewport"
content="width=device-width, initial-scale=1"
>
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>
Client
APP Angular
Serveur
html + css + js
API
Java, Node, ...
data (JSON)
+ initial data
data (JSON)
Statique
Client
APP Angular
Serveur
Statique
Ngnix, GH-page, surge.sh, ...
html + css + js + initial data
API
Java, Node, ...
data (Json)
Pensez fonctionnel et pas "uniquement technique"
Ne pas faire des dossiers par composants technique, mais par composants fonctionnels
Travaillez avec vos collègues des différents métier !
Choisissez le bon !
@berthel350
FBerthelot @Github
florent@berthelot.io
https://berthelot.io