A tester !
Bon bah apparemment on peut... ma foi... sigh...
Unit testing routes configuration with angular.
Enfin une explication claire sur le double data-binding angular
Exemple upload côté client :
fichier html
<input type="file" #package (change)="prepareFile(package.files)" class="is-small fileupload"/>
<button type="button" class="button is-small" (click)="importPackage()" [disabled]="!this.validPackage">Valider</button>
fichier ts :
selectedPackage: any;
prepareFile(files: any) {
this.selectedPackage = files;
//exemple pour bloquer l'upload si on a pas affaire à un zip :
if(files != null){
let extension = files[0].name.split('.').pop();
if(extension == "zip" || extension == "ZIP"){
this.validPackage = true;
}else{
this.validPackage= false;
}
}
}
importPackage(): void {
let file: File = this.selectedPackage[0];
this.packageService.uploadFile(file).subscribe(
result => this.onFileUploaded(result)
);
}
onFileUploaded(result: any): void {
this.selectedPackage = null;
}
packageService.ts
uploadFile(file: File): Observable<Entity[]> {
let formData: FormData = new FormData();
formData.append('file', file, file.name);
const url = `${this.getUrl()}/upload`;
return this.http.post(url, formData)
.map(res => this.extractListFromResponse(res))
.catch(res => this.handleError(res))
}
Dans notre cas, le retour de la requete est une liste d'item. A vous de voir de votre côté.
Controller côté java:
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public ResponseEntity uploadFile(@RequestParam("file") MultipartFile file) {
if (!file.isEmpty()) {
try {
return new ResponseEntity<>(this.packageService.uploadPackage(file.getBytes()), HttpStatus.OK);
} catch (GenericBELException | IOException e) {
return super.forwardException("Impossible de récupérer le package " + file.getOriginalFilename(), e);
}
} else {
return this.createStructuredResponse("Impossible de récupérer le package " + file.getOriginalFilename() + ". Il était vide à la réception.", HttpStatus.NO_CONTENT);
}
}
A partir du file, vous pouvez extraire le contenu avec des BufferReader.
Je rajoute aussi ce header, qui permet de garder la connexion vivante derrière un proxy :
Angular :
headers.append('Proxy-Connection', 'Keep-alive');
http.service.ts
import {Injectable} from '@angular/core';
import {Http, XHRBackend, RequestOptions, Request, RequestOptionsArgs, Response, Headers} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
@Injectable()
export class HttpService extends Http {
constructor (backend: XHRBackend, options: RequestOptions) {
let token = localStorage.getItem('auth_token'); // your custom token getter function here
options.headers.set('Authorization', `Bearer ${token}`);
super(backend, options);
}
request(url: string|Request, options?: RequestOptionsArgs): Observable<Response> {
let token = localStorage.getItem('auth_token');
if (typeof url === 'string') { // meaning we have to add the token to the options, not in url
if (!options) {
// let's make option object
options = {headers: new Headers()};
}
options.headers.set('Authorization', `Bearer ${token}`);
} else {
// we have to add the token to the url object
url.headers.set('Authorization', `Bearer ${token}`);
}
return super.request(url, options).catch(this.catchAuthError(this));
}
private catchAuthError (self: HttpService) {
// we have to pass HttpService's own instance here as `self`
return (res: Response) => {
console.log(res);
if (res.status === 401 || res.status === 403) {
// if not authenticated
console.log(res);
}
return Observable.throw(res);
};
}
}
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
/**
* The authentication entry point is called when the client makes a call to a resource without proper authentication. In
* other words, the client has not logged in.
* <p>
* If this class is not present in the code, then Spring security opens a default pop-up. We want a 401 error.
*/
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
LOGGER.info("intercepted request : 401 error code.");
response.getWriter().append("Access Denied : " + authException.getMessage());
response.setStatus(401);
}
}
Cette classe est incluse dans la couche sécurité via la configuration du http :
//Override the default login pop-up with a 401 response.
http.exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint);
Ne reste plus qu'à configurer la gestion des erreurs 401 avec Angular :
import { Injectable } from '@angular/core';
import { Request, XHRBackend, RequestOptions, Response, Http, RequestOptionsArgs, Headers } from '@angular/http';
import { Router, NavigationEnd, Event } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
/**
* This class handles generically the error on authentication.
*/
@Injectable()
export class AuthenticatedHttpService extends Http {
private router: Router;
constructor(backend: XHRBackend, defaultOptions: RequestOptions, router: Router) {
super(backend, defaultOptions);
this.router = router;
}
request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
return super.request(url, options).catch((error: Response) => {
if ((error.status === 401 || error.status === 403) && (window.location.href.match(/\?/g) || []).length < 2) {
if(window.location.href != '/login') {
this.router.navigate(['/login']);
}
else {
return Observable.throw("authentication met an exception");
}
}
return Observable.throw("request authentication");
});
}
}
Cettte technique géniale permet de distinguer deux applications autonomes au sein d'un seul et même repo angular2 (ou 4).
Super option.
Comment gérer le découpage de votre projet en sous-projets via la gestion des modules.
Gérer la récupération des fichiers avec Angular4 peut être un vrai casse-tête : entre les problèmes de compatibilité lié au navigateur et l'API de service qui ne répond pas facilement à ce besoin, j'ai trouvé cette petite librairie bien pratique.
On l'appelle de cette manière :
import { Component } from '@angular/core';
import { ZipService} from '../services/ZipService';
import { saveAs as importedSaveAs } from "file-saver";
@Component({
selector: 'zip-zip',
templateUrl: './zip-component.html'
})
export class ZipComponent {
private zipService: ZipService;
constructor(zipService: ZipService) {
this.zipService= zipService;
}
public getZipFile(): any {
this.zipService.downloadZipFile().subscribe(
(data: any) => {
importedSaveAs(new Blob([data], {type: 'application/zip'}), 'filename.zip');
},
error => this.doSomething(),
() => this.doSomething()
);
}
}
Angular est un framework orienté Aspect qui n'empêche pas... (attention, roulement de tambour...) : D'écrire des objets !
Du coup, on peut avoir besoin de mettre en place de la programmation orientée aspect.
De mémoire, j'avais essayé ce framework : https://www.npmjs.com/package/aspect.js-angular
Mes petites options de build pour la prod : ng build --prod --env=prod --aot
La compilation AOT d'angular permet d'améliorer la vitesse d'exécution de l'application en prod.
Faudra que je creuse le sujet, mais c'est en place chez moi et c'est efficace (sauf lors du build, là c'est hyper long).
Bon, personnellement, je ne suis pas fan du ng-cli, mais reconstruire le build angular à la main, c'est juste la croix et la bannière.
Heureusement, il y a le .angular-cli.json, qui permet de configurer à minima le tin-touin. C'est notamment utile lorsque l'on souhaite changer l'icône du site ou rajouter des fichiers css perso, voire des fonts.
Exemple :
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "Mon application"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"logo-brand.png" <-------- nouvelle icone qui apparaitra dans l'onglet navigateur (à configurer ensuite dans le index.html)
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test/test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app", <------ On peut changer le nom du composant de base
"styles": [
"assets/css/bulma.css", <------ On peut rajouter ici des css perso
"assets/css/fontawesome-all.min.css",
"styles.css"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"lint": [
{
"project": "src/tsconfig.app.json"
},
{
"project": "src/tsconfig.spec.json"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "css",
"component": {}
}
}
Je recommande l'usage de ce navigateur sans GUI pour les tests angular. Il est léger, rapide et ne bugge pas.
On l'installe via le package.json :
$ npm install karma-slimerjs-launcher --save-dev
Puis, on le rajoute dans la conf karma :
karma.conf.js
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular/cli'],
plugins: [
require('karma-jasmine'),
require('karma-slimerjs-launcher'),
require('karma-coverage-istanbul-reporter'),
require('@angular/cli/plugins/karma')
],
...
browsers: ['SlimerJS'],
...
Les différents binding de key event définis dans Angular4.
Angular propose une petite feature bien pratique pour le format des données affichées à l'écran : la définition des pipes.
On définit un pipe comme ça :
import {DomSanitizer} from '@angular/platform-browser';
import {PipeTransform, Pipe} from "@angular/core";
@Pipe({ name: 'safeHtml'}) <------ C'est avec ce nom que vous appelerez votre pipeTransformer dans les vues
export class SafeHtmlPipe implements PipeTransform {
constructor(private sanitized: DomSanitizer) {}
transform(value: any) {
return this.sanitized.bypassSecurityTrustHtml(value);
}
}
On déclare ensuite le Pipe dans le NgModule via les déclarations :
import { SafeHtmlPipe } from './pipe/SafeHtmlPipe';
@NgModule({
declarations: [ SafeHtmlPipe ]
})
Et on peut s'en servir dans le modèle :
import { DatePipe } from '@angular/common';
@Component({
...,
providers: [DatePipe]
})
export class MyComponent {
private datePipe: DatePipe;
constructor(datePipe: DatePipe) {
this.datePipe = datePipe;
}
doSomething() {
let filename = 'filename'+this.datePipe.transform(new Date(), 'yyyyMMyy-hhmm')+'.jpg';
}
}
Ou dans les vues :
<label>Date</label>
<label>The hero's birthday is {{ birthday | date }}</label>
Le DatePipe a pour nom "date" : https://github.com/angular/angular/blob/5.2.3/packages/common/src/pipes/date_pipe.ts#L15-L174
Certains codes de retour HTTP sont gérés directement par le framework Angular et ne peuvent pas être récupérés au niveau de la requête. C'est notamment le cas pour le code 401, très souvent utilisé puisqu'il informe que l'utilisateur n'est pas loggué en session.
Une manip existante consiste à surcharger le comportement natif du composant HTTP en spécifiant le comportement sur le code erreur de votre choix.
Voici comment rajouter la gestion des codes 401 à une application Angular 4 :
app.module.ts
import { HttpModule, Http } from '@angular/http';
import { AppComponent } from './app.component';
import { AuthenticatedHttpService } from './services/AuthenticatedHttpService';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
HttpModule
],
providers: [ { provide: Http, useClass: AuthenticatedHttpService } ],
bootstrap: [ AppComponent ]
})
authenticatedHttpService.ts
import { Injectable } from '@angular/core';
import { Request, XHRBackend, RequestOptions, Response, Http, RequestOptionsArgs, Headers } from '@angular/http';
import { Router, NavigationEnd, Event } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
/**
* This class handles generically the error on authentication (code 401).
*/
@Injectable()
export class AuthenticatedHttpService extends Http {
private router: Router;
constructor(backend: XHRBackend, defaultOptions: RequestOptions, router: Router) {
super(backend, defaultOptions);
this.router = router;
}
request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
return super.request(url, options).catch((error: Response) => {
if ((error.status === 401 || error.status === 403) && (window.location.href.match(/\?/g) || []).length < 2) {
//DO SOMETHING HERE
//exemple :
if(window.location.href != '/login') {
this.router.navigate(['/login']);
}
}
return Observable.throw("request authentication");
});
}
}
Le routage sous Angular4 se fait avec des liens complets plutôt qu'avec des ancres (fonctionnement Aurelia), ce qui pose des problèmes avec les serveurs comme SpringBoot (redirection compliquée). Toutefois, il est possible de forcer l'usage des ancres sous Angular4 :
fichier app.module.ts
import { RouterModule, Routes } from '@angular/router';
import { HashLocationStrategy, LocationStrategy } from '@angular/common';
import { AppComponent } from './app.component';
import { ComponentOne } from './one/one.component';
const appRoutes: Routes = [
{ path: 'home', component: Home },
{ path: 'login', component: Login },
{ path: 'one', component: ComponentOne},
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: '**', redirectTo: '/home' }
];
@NgModule({
declarations: [
Home, Login, ComponentOne
],
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
HttpModule,
RouterModule.forRoot(appRoutes) <----- L'ajout de la configuration de routing au module
],
providers: [ { provide: LocationStrategy, useClass: HashLocationStrategy } ], <----- Impose l'usage des ancres pour la navigation
bootstrap: [ AppComponent ]
})
export class AppModule { }