RESSOURCES


DÉVELOPPER VOTRE PREMIÈRE APPLICATION ANGULAR.

Les ressources de la formation « Angular : Développez votre première application avec TypeScript » se trouvent bien sur cette page. Vous trouverez, comme promis :

Aux utilisateurs d’Udemy : Angular évolue constamment, comme beaucoup de technologies dans le monde du développement Web (une nouvelle version majeure tout les 6 mois!). Le code de la formation est stabilisé pour la version 9 d’Angular, mais les vidéos de la formation ont été tournées pour la version 5. Si ce que vous voyez en vidéo ne correspond pas aux extraits de code fournis, pas de panique, c’est toujours le code qui est la source de vérité.  😇

Application de démonstration

L’application de démonstration que vous réaliserez pendant la formation est accessible en ligne : https://ng8-pokemons-app.firebaseapp.com.

La Correction du code

L’ensemble du code développé de l’application de Pokémons développé pendant la formation peut être téléchargé en cliquant sur le bouton ci-dessous :

Les Extraits de code

Chapitre 1 : Premiers pas avec Angular

app.component.ts

import { Component } from '@angular/core';
 
@Component({
  selector: 'my-app',
  template: `<h1>Hello {{name}}</h1>`,
})
export class AppComponent  { name = 'Angular'; }

app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
 
import { AppComponent }  from './app.component';
 
@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

bs-config.json

{
  "server": {
    "baseDir": "src",
    "routes": {
      "/node_modules": "node_modules"
    }
  }
}

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Angular QuickStart</title>
    <base href="/">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
 
    <!-- Polyfill(s) for older browsers -->
    <script src="https://unpkg.com/core-js@2.5.4/client/shim.min.js"></script>
    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>
 
    <script src="systemjs.config.js"></script>
    <script>
      System.import('main.js').catch(function(err){ console.error(err); });
    </script>
  </head>
 
  <body>
    <my-app>Loading AppComponent content here ...</my-app>
  </body>
</html>

main.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
 
import { AppModule } from './app/app.module';
 
platformBrowserDynamic().bootstrapModule(AppModule).catch(err => console.log(err));

package.json (Angular v8)

{
  "name": "ng-pokemons-app",
  "version": "1.0.0",
  "description": "An awesome application to handle some pokemons.",
  "scripts": {
    "build": "tsc -p src/",
    "build:watch": "tsc -p src/ -w",
    "serve": "lite-server -c=bs-config.json",
    "prestart": "npm run build",
    "start": "concurrently \"npm run build:watch\" \"npm run serve\""
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "~8.0.0",
    "@angular/common": "~8.0.0",
    "@angular/compiler": "~8.0.0",
    "@angular/core": "~8.0.0",
    "@angular/forms": "~8.0.0",
    "@angular/platform-browser": "~8.0.0",
    "@angular/platform-browser-dynamic": "~8.0.0",
    "@angular/router": "~8.0.0",
    "core-js": "~2.5.4",
    "rxjs": "~6.4.0",
    "rxjs-compat": "~6.4.0",
    "systemjs": "~0.19.40",
    "zone.js": "~0.9.1"
  },
  "devDependencies": {
    "angular-in-memory-web-api": "~0.8.0",
    "concurrently": "~3.6.0",
    "firebase-tools": "~7.0.2",
    "lite-server": "~2.4.0",
    "tslint": "~5.9.1",
    "typescript": "~3.4.0"
  }
}

package.json (Angular v9)

{
  "name": "ng-pokemons-app",
  "version": "1.0.0",
  "description": "An awesome application to handle some pokemons.",
  "scripts": {
    "build": "tsc -p src/",
    "build:watch": "tsc -p src/ -w",
    "serve": "lite-server -c=bs-config.json",
    "prestart": "npm run build",
    "start": "concurrently \"npm run build:watch\" \"npm run serve\""
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "~9.0.5",
    "@angular/common": "~9.0.5",
    "@angular/compiler": "~9.0.5",
    "@angular/core": "~9.0.5",
    "@angular/forms": "~9.0.5",
    "@angular/platform-browser": "~9.0.5",
    "@angular/platform-browser-dynamic": "~9.0.5",
    "@angular/router": "~9.0.5",
    "core-js": "~3.6.4",
    "rxjs": "~6.5.4",
    "rxjs-compat": "~6.5.4",
    "systemjs": "~0.19.47",
    "zone.js": "~0.10.2"
  },
  "devDependencies": {
    "angular-in-memory-web-api": "~0.9.0",
    "concurrently": "~5.1.0",
    "firebase-tools": "~7.14.0",
    "lite-server": "~2.5.4",
    "tslint": "~6.0.0",
    "typescript": "~3.8.3"
  }
}

systemjs.config.js

(function (global) {
  System.config({
    paths: {
      // paths serve as alias
      'npm:': 'node_modules/'
    },
    // map tells the System loader where to look for things
    map: {
      // our app is within the dist/app folder
      'app': 'app',
 
      // angular bundles
      '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
      '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
      '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
      '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
      '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
      '@angular/common/http': 'npm:@angular/common/bundles/common-http.umd.js',
      '@angular/http': 'npm:@angular/common/bundles/common-http.umd.js',
      '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
      '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
 
      // other libraries
      'tslib': 'npm:tslib/tslib.js',
      'rxjs': 'npm:rxjs',
      'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
    },
    // packages tells the System loader how to load when no filename and/or no extension
    packages: {
      app: {
        defaultExtension: 'js',
      },
      rxjs: {
          main: 'index.js',
          defaultExtension: 'js'
      },
      'rxjs/operators': {
          main: 'index.js',
          defaultExtension: 'js'
      }
    }
  });
})(this);

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": [ "es2015", "dom" ],
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true,
    "typeRoots" : ["node_modules/@types"]
  }
}

Chapitre 2 : Les Composants

app.component.ts

import { Component, OnInit } from '@angular/core';
import { Pokemon } from './pokemon';
import { POKEMONS } from './mock-pokemons';
 
@Component({
  selector: 'pokemon-app',
  template: '<h1>Pokémons</h1>'
})
export class AppComponent implements OnInit {
 
  pokemons: Pokemon[] = null;
 
  ngOnInit() {
    this.pokemons = POKEMONS;
  }
 
  selectPokemon(pokemon: Pokemon) {
    console.log('Vous avez selectionné ' + pokemon.name);
  }
   
}

mock-pokemons.ts

import { Pokemon } from './pokemon';
 
export const POKEMONS: Pokemon[] = [
    {
        id: 1,
        name: "Bulbizarre",
        hp: 25,
        cp: 5,
        picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/001.png",
        types: ["Plante", "Poison"],
        created: new Date()
    },
    {
        id: 2,
        name: "Salamèche",
        hp: 28,
        cp: 6,
        picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/004.png",
        types: ["Feu"],
        created: new Date()
    },
    {
        id: 3,
        name: "Carapuce",
        hp: 21,
        cp: 4,
        picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/007.png",
        types: ["Eau"],
        created: new Date()
    },
    {
        id: 4,
        name: "Aspicot",
        hp: 16,
        cp: 2,
        picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/013.png",
        types: ["Insecte", "Poison"],
        created: new Date()
    },
    {
        id: 5,
        name: "Roucool",
        hp: 30,
        cp: 7,
        picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/016.png",
        types: ["Normal", "Vol"],
        created: new Date()
    },
    {
        id: 6,
        name: "Rattata",
        hp: 18,
        cp: 6,
        picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/019.png",
        types: ["Normal"],
        created: new Date()
    },
    {
        id: 7,
        name: "Piafabec",
        hp: 14,
        cp: 5,
        picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/021.png",
        types: ["Normal", "Vol"],
        created: new Date()
    },
    {
        id: 8,
        name: "Abo",
        hp: 16,
        cp: 4,
        picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/023.png",
        types: ["Poison"],
        created: new Date()
    },
    {
        id: 9,
        name: "Pikachu",
        hp: 21,
        cp: 7,
        picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/025.png",
        types: ["Electrik"],
        created: new Date()
    },
    {
        id: 10,
        name: "Sabelette",
        hp: 19,
        cp: 3,
        picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/027.png",
        types: ["Normal"],
        created: new Date()
    },
    {
        id: 11,
        name: "Mélofée",
        hp: 25,
        cp: 5,
        picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/035.png",
        types: ["Fée"],
        created: new Date()
    },
    {
        id: 12,
        name: "Groupix",
        hp: 17,
        cp: 8,
        picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/037.png",
        types: ["Feu"],
        created: new Date()
    }
];

pokemon.ts

export class Pokemon {
  id: number;
  hp: number;
  cp: number;
  name: string;
  picture: string;
  types: Array<string>;
  created: Date;
}

Chapitre 3 : Les Templates

app.component.html

<h1 class='center'>Pokémons</h1>
<div class='container'>
<div class="row">
<div *ngFor='let pokemon of pokemons' class="col s6 m4">
  <div class="card horizontal" (click)="selectPokemon(pokemon)">
    <div class="card-image">
      <img [src]="pokemon.picture">
    </div>
    <div class="card-stacked">
      <div class="card-content">
        <p>{{ pokemon.name }}</p>
        <p><small>{{ pokemon.created }}</small></p>
      </div>
    </div>
  </div>
</div>
</div>
</div>

Chapitre 4 : Les Directives

border-card.directive.ts

import { Directive, ElementRef } from '@angular/core';
 
@Directive({
  selector: '[pkmnBorderCard]'
})
export class BorderCardDirective {
    constructor(private el: ElementRef) {
        this.setBorder('#f5f5f5');
        this.setHeight(180);
    }
 
    private setBorder(color: string) {
        let border = 'solid 4px ' + color;
        this.el.nativeElement.style.border = border;
    }
 
    private setHeight(height: number) {
        this.el.nativeElement.style.height = height + 'px';
    }
}

Chapitre 5 : Les Pipes

app.component.html

<h1 class='center'>Pokémons</h1>
<div class='container'>
<div class="row">
<div *ngFor='let pokemon of pokemons' class="col s6 m4">
  <div class="card horizontal" (click)="selectPokemon(pokemon)" pkmn-border-card>
    <div class="card-image">
      <img [src]="pokemon.picture">
    </div>
    <div class="card-stacked">
      <div class="card-content">
        <p>{{ pokemon.name }}</p>
        <!-- on ajoute le pipe pour les dates, avec le bon format : -->
        <p><small>{{ pokemon.created | date:"dd/MM/yyyy" }}</small></p>
        <!-- on utilise la directive ngFor pour afficher tous les types
        d'un pokémon donnée -->
        <span *ngFor='let type of pokemon.types' class="{{ type | pokemonTypeColor }}">{{ type }}</span>
      </div>
    </div>
  </div>
</div>
</div>
</div>

pokemon-type-color.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';
 
/*
 * Affiche la couleur correspondant au type du pokémon.
 * Prend en argument le type du pokémon.
 * Exemple d'utilisation:
 *   {{ pokemon.type | pokemonTypeColor }}
*/
@Pipe({name: 'pokemonTypeColor'})
export class PokemonTypeColorPipe implements PipeTransform {
  transform(type: string): string {
 
    let color: string;
 
    switch (type) {
      case 'Feu':
        color = 'red lighten-1';
        break;
      case 'Eau':
        color = 'blue lighten-1';
        break;
      case 'Plante':
        color = 'green lighten-1';
        break;
      case 'Insecte':
        color = 'brown lighten-1';
        break;
      case 'Normal':
        color = 'grey lighten-3';
        break;
      case 'Vol':
        color = 'blue lighten-3';
        break;
      case 'Poison':
        color = 'deep-purple accent-1';
        break;
      case 'Fée':
        color = 'pink lighten-4';
        break;
      case 'Psy':
        color = 'deep-purple darken-2';
        break;
      case 'Electrik':
        color = 'lime accent-1';
        break;
      case 'Combat':
        color = 'deep-orange';
        break;
      default:
        color = 'grey';
        break;
    }
 
    return 'chip ' + color;
 
  }
}

Chapitre 6 : Les Routes

app-routing.module.ts

import { NgModule }              from '@angular/core';
import { RouterModule, Routes }  from '@angular/router';
import { ListPokemonComponent } from './list-pokemon.component';
import { DetailPokemonComponent } from './detail-pokemon.component';
 
// routes
const appRoutes: Routes = [
    { path: 'pokemons', component: ListPokemonComponent },
    { path: 'pokemon/:id', component: DetailPokemonComponent },
    { path: '', redirectTo: 'pokemons', pathMatch: 'full' }
];
 
@NgModule({
    imports: [
        RouterModule.forRoot(appRoutes)
    ],
    exports: [
        RouterModule
    ]
})
export class AppRoutingModule { }

app.component.html

<nav>
  <div class="nav-wrapper teal">
    <a href="#" class="brand-logo center">pokemon-app</a>
  </div>
</nav>
 
<router-outlet></router-outlet>

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
 
import { AppComponent } from './app.component';
import { ListPokemonComponent } from './list-pokemon.component';
import { DetailPokemonComponent } from './detail-pokemon.component';
 
import { BorderCardDirective } from './border-card.directive';
import { PokemonTypeColorPipe } from './pokemon-type-color.pipe';
 
@NgModule({
    imports: [
      BrowserModule, 
      AppRoutingModule
    ],
    declarations: [
        AppComponent,
        BorderCardDirective,
        PokemonTypeColorPipe,
        ListPokemonComponent,
        DetailPokemonComponent
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

detail-pokemon.component.html

<div *ngIf="pokemon" class="row">
<div class="col s12 m8 offset-m2">
<h2 class="header center">{{ pokemon.name }}</h2>
<div class="card horizontal hoverable">
  <div class="card-image">
    <img [src]="pokemon.picture">
  </div>
  <div class="card-stacked">
    <div class="card-content">
      <table class="bordered striped">
        <tbody>
          <tr>
            <td>Nom</td>
            <td><strong>{{ pokemon.name }}</strong></td>
          </tr>
          <tr>
            <td>Points de vie</td>
            <td><strong>{{ pokemon.hp }}</strong></td>
          </tr>
          <tr>
            <td>Dégâts</td>
            <td><strong>{{ pokemon.cp }}</strong></td>
          </tr>
          <tr>
            <td>Types</td>
            <td>
              <span *ngFor="let type of pokemon.types" class="{{ type | pokemonTypeColor }}">{{ type }}</span>
            </td>
          </tr>
          <tr>
            <td>Date de création</td>
            <td><em>{{ pokemon.created | date:"dd/MM/yyyy" }}</em></td>
          </tr>
        </tbody>
      </table>
    </div>
    <div class="card-action">
      <a (click)="goBack()">Retour</a>
    </div>
  </div>
</div>
</div>
</div>
<h4 *ngIf='!pokemon' class="center">Aucun pokémon à afficher !</h4>

detail-pokemon.component.ts

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router, Params } from '@angular/router';
import { Pokemon } from './pokemon';
import { POKEMONS } from './mock-pokemons';
 
@Component({
    selector: 'detail-pokemon',
    templateUrl: './app/detail-pokemon.component.html'
})
export class DetailPokemonComponent implements OnInit {
 
    pokemons: Pokemon[] = null;
    pokemon: Pokemon = null;
 
    constructor(private route: ActivatedRoute, private router: Router) {}
 
    ngOnInit(): void {
        this.pokemons = POKEMONS;
 
        let id = +this.route.snapshot.paramMap.get('id');
        for (let i = 0; i < this.pokemons.length; i++) {
            if (this.pokemons[i].id == id) {
                this.pokemon = this.pokemons[i];
            }
        }
    }
 
    goBack(): void {
        this.router.navigate(['/pokemons']);
    }
 
}

list-pokemon.component.html

<h1 class='center'>Pokémons</h1>
  <div class='container'>
    <div class="row">
    <div *ngFor='let pokemon of pokemons' class="col s6 m4">
      <div class="card horizontal" (click)="selectPokemon(pokemon)" pkmn-border-card>
        <div class="card-image">
          <img [src]="pokemon.picture">
        </div>
        <div class="card-stacked">
          <div class="card-content">
            <p>{{ pokemon.name }}</p>
            <p><small>{{ pokemon.created | date:"dd/MM/yyyy" }}</small></p>
            <span *ngFor='let type of pokemon.types' class="{{ type | pokemonTypeColor }}">{{ type }}</span>
          </div>
        </div>
      </div>
    </div>
    </div>
  </div>

list-pokemon.component.ts

import { Component, OnInit } from '@angular/core';
import { Pokemon } from './pokemon';
import { POKEMONS } from './mock-pokemons';
import { Router } from '@angular/router';
 
@Component({
    selector: 'list-pokemon',
    templateUrl: './app/list-pokemon.component.html'
})
export class ListPokemonComponent implements OnInit {
 
    pokemons: Pokemon[] = null;
 
    constructor(private router: Router) { }
 
    ngOnInit(): void {
        this.pokemons = POKEMONS;
    }
 
    selectPokemon(pokemon: Pokemon): void {
        console.log('Vous avez selectionné ' + pokemon.name);
        let link = ['/pokemon', pokemon.id];
        this.router.navigate(link);
    }
 
}

page-not-found.component.ts

import { Component } from '@angular/core';
 
@Component({
    selector: 'page-404',
    template: `
    <div class='center'>
      <img src="http://assets.pokemon.com/assets/cms2/img/pokedex/full/035.png"/>
      <h1>Hey, cette page n'existe pas !</h1>
      <a routerLink="/pokemons" class="waves-effect waves-teal btn-flat">
        Retourner à l' accueil
      </a>
    </div>
  `
})
export class PageNotFoundComponent { }

Chapitre 7 : Les Modules

app-routing.module.ts

import { NgModule }              from '@angular/core';
import { RouterModule, Routes }  from '@angular/router';
import { PageNotFoundComponent } from './page-not-found.component';
 
const appRoutes: Routes = [
    { path: '', redirectTo: 'pokemons', pathMatch: 'full' },
    { path: '**', component: PageNotFoundComponent }
];
 
@NgModule({
    imports: [
        RouterModule.forRoot(appRoutes)
    ],
    exports: [
        RouterModule
    ]
})
export class AppRoutingModule { }

app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { PokemonsModule } from './pokemons/pokemons.module';
 
import { AppComponent }  from './app.component';
import { PageNotFoundComponent } from './page-not-found.component';
 
@NgModule({
    imports: [
        BrowserModule,
        PokemonsModule, // L'odre de chargement des modules est très important
        AppRoutingModule // pour l'ordre de déclaration des routes !
    ],
    declarations: [
        AppComponent,
        PageNotFoundComponent
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

pokemons-routing.module.ts

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
 
import { ListPokemonComponent }    from './list-pokemon.component';
import { DetailPokemonComponent }  from './detail-pokemon.component';
 
// les routes du module Pokémon
const pokemonsRoutes: Routes = [
    { path: 'pokemons', component: ListPokemonComponent },
    { path: 'pokemon/:id', component: DetailPokemonComponent }
];
 
@NgModule({
    imports: [
        RouterModule.forChild(pokemonsRoutes)
    ],
    exports: [
        RouterModule
    ]
})
export class PokemonRoutingModule { }

pokemons.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
 
import { ListPokemonComponent } from './list-pokemon.component';
import { DetailPokemonComponent } from './detail-pokemon.component';
import { BorderCardDirective } from './border-card.directive';
import { PokemonTypeColorPipe } from './pokemon-type-color.pipe';
 
@NgModule({
    imports: [
        CommonModule
    ],
    declarations: [
        ListPokemonComponent,
        DetailPokemonComponent,
        BorderCardDirective,
        PokemonTypeColorPipe
    ],
    providers: []
})
export class PokemonsModule { }

Chapitre 8 : Les Services

pokemons.service.ts

import { Injectable } from '@angular/core';
import { Pokemon } from './pokemon';
import { POKEMONS } from './mock-pokemons';
 
@Injectable()
export class PokemonsService {
 
    // Retourne tous les pokémons
    getPokemons(): Pokemon[] {
      return POKEMONS;
    }
     
    // Retourne le pokémon avec l'identifiant passé en paramètre
    getPokemon(id: number): Pokemon {
      let pokemons = this.getPokemons();
     
      for(let index = 0; index < pokemons.length; index++) {
        if(id === pokemons[index].id) {
          return pokemons[index];
        }
      }
    }
}

Chapitre 9 : Les Formulaires

edit-pokemon.component.ts

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Pokemon } from './pokemon';
import { PokemonsService } from './pokemons.service';
 
@Component({
  selector: 'edit-pokemon',
  template: `
    <h2 class="header center">Editer {{ pokemon?.name }}</h2>
        <p class="center">
            <img *ngIf="pokemon" [src]="pokemon.picture"/>
        </p>
    <pokemon-form [pokemon]="pokemon"></pokemon-form>
  `,
})
export class EditPokemonComponent implements OnInit {
 
  pokemon: Pokemon = null;
 
  constructor(
    private route: ActivatedRoute,
    private pokemonsService: PokemonsService) {}
 
  ngOnInit(): void {
    let id = +this.route.snapshot.params['id'];
    this.pokemon = this.pokemonsService.getPokemon(id);
  }
 
}

pokemon-form.component.css

.ng-valid[required], .ng-valid.required {
  border-left: 5px solid #42A948; /* bordure verte */
}
 
.ng-invalid:not(form) {
  border-left: 5px solid #a94442; /* bordure rouge */
}

pokemon-form.component.html

<form *ngIf="pokemon" (ngSubmit)="onSubmit()" #pokemonForm="ngForm">
  <div class="row">
    <div class="col s8 offset-s2">
      <div class="card-panel">
 
        <!-- Pokemon name -->
        <div class="form-group">
          <label for="name">Nom</label>
          <input type="text" class="form-control" id="name"
                  required
                  pattern="^[a-zA-Z0-9àéèç]{1,25}$"
                 [(ngModel)]="pokemon.name" name="name"
                 #name="ngModel">
 
          <div [hidden]="name.valid || name.pristine"
                class="card-panel red accent-1">
                Le nom du pokémon est requis (1-25).
          </div>
        </div>
 
        <!-- Pokemon hp -->
        <div class="form-group">
          <label for="hp">Point de vie</label>
          <input type="number" class="form-control" id="hp"
                  required
                  pattern="^[0-9]{1,3}$"
                 [(ngModel)]="pokemon.hp" name="hp"
                 #hp="ngModel">
           <div [hidden]="hp.valid || hp.pristine"
                 class="card-panel red accent-1">
                 Les points de vie du pokémon sont compris entre 0 et 999.
           </div>
        </div>
 
        <!-- Pokemon cp -->
        <div class="form-group">
          <label for="cp">Dégâts</label>
          <input type="number" class="form-control" id="cp"
                  required
                  pattern="^[0-9]{1,2}$"
                 [(ngModel)]="pokemon.cp" name="cp"
                 #cp="ngModel">
           <div [hidden]="cp.valid || cp.pristine"
                 class="card-panel red accent-1">
                 Les dégâts du pokémon sont compris entre 0 et 99.
           </div>
        </div>
 
        <!-- Pokemon types -->
        <form class="form-group">
          <label for="types">Types</label>
          <p *ngFor="let type of types">
            <label>
              <input type="checkbox"
                class="filled-in"
                id="{{ type }}"
                [value]="type"
                [checked]="hasType(type)"
                [disabled]="!isTypesValid(type)"
                (change)="selectType($event, type)"/>
              <span [attr.for]="type">
                <div class="{{ type | pokemonTypeColor }}">
                  {{ type }}
                </div>
              </span>
            </label>
          </p>
        </form>
 
        <!-- Submit button -->
        <div class="divider"></div>
        <div class="section center">
          <button type="submit"
            class="waves-effect waves-light btn"
            [disabled]="!pokemonForm.form.valid">
            Valider</button>
        </div>
 
      </div>
    </div>
  </div>
</form>
<h3 *ngIf="!pokemon" class="center">Aucun pokémon à éditer...</h3>

pokemon-form.component.ts

import { Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { PokemonsService } from './pokemons.service';
import { Pokemon } from './pokemon';
 
@Component({
    selector: 'pokemon-form',
    templateUrl: './app/pokemons/pokemon-form.component.html',
    styleUrls: ['./app/pokemons/pokemon-form.component.css']
})
export class PokemonFormComponent implements OnInit {
 
    @Input() pokemon: Pokemon; // propriété d'entrée du composant
    types: Array<string>; // types disponibles pour un pokémon : 'Eau', 'Feu', etc ...
    constructor(
        private pokemonsService: PokemonsService,
        private router: Router) { }
 
    ngOnInit() {
        // Initialisation de la propriété types
        this.types = this.pokemonsService.getPokemonTypes();
    }
 
    // Détermine si le type passé en paramètres appartient ou non au pokémon en cours d'édition.
    hasType(type: string): boolean {
        let index = this.pokemon.types.indexOf(type);
        if (index > -1) return true;
        return false;
    }
 
    // Méthode appelée lorsque l'utilisateur ajoute ou retire un type au pokémon en cours d'édition.
    selectType($event: any, type: string): void {
        let checked = $event.target.checked;
        if (checked) {
            this.pokemon.types.push(type);
        } else {
            let index = this.pokemon.types.indexOf(type);
            if (index > -1) {
                this.pokemon.types.splice(index, 1);
            }
        }
    }
 
    // Valide le nombre de types pour chaque pokémon
    isTypesValid(type: string): boolean {
        if (this.pokemon.types.length === 1 && this.hasType(type)) {
            return false;
        }
        if (this.pokemon.types.length >= 3 && !this.hasType(type)) {
            return false;
        }
 
        return true;
    }
 
    // La méthode appelée lorsque le formulaire est soumis.
    onSubmit(): void {
        console.log("Submit form !");
        let link = ['/pokemon', this.pokemon.id];
        this.router.navigate(link);
    }
 
}

Chapitre 10 : Effectuer des requêtes HTTP standards

add-pokemon.component.html

<h2 class="header center">Ajouter un Pokémon</h2>
<pokemon-form [pokemon]="pokemon"></pokemon-form>

add-pokemon.component.ts

import { Component, OnInit } from '@angular/core';
import { Pokemon } from './pokemon';
 
@Component({
    selector: 'add-pokemon',
    templateUrl: './app/pokemons/add-pokemon.component.html'
})
export class AddPokemonComponent implements OnInit {
 
    pokemon: Pokemon = null;
 
    constructor() { }
 
    ngOnInit(): void {
        this.pokemon = new Pokemon();
    }
 
}

in-memory-data.service.ts

import { InMemoryDbService } from 'angular-in-memory-web-api';
import { POKEMONS } from './pokemons/mock-pokemons';
 
export class InMemoryDataService implements InMemoryDbService {
    createDb() {
        let pokemons = POKEMONS;
        return { pokemons };
    }
}

loader.component.ts

import { Component } from '@angular/core';
 
@Component({
  selector: 'pkmn-loader',
  template: `
    <div class="preloader-wrapper big active">
      <div class="spinner-layer spinner-blue">
        <div class="circle-clipper left">
          <div class="circle"></div>
        </div><div class="gap-patch">
          <div class="circle"></div>
        </div><div class="circle-clipper right">
          <div class="circle"></div>
        </div>
      </div>
    </div>
  `
})
export class LoaderComponent {}

search-pokemon.component.html

<div class="row">
  <div class="col s12 m6 offset-m3">
    <div class="card">
      <div class="card-content">
        <div class="input-field">
          <input #searchBox (keyup)="search(searchBox.value)"
            placeholder="Rechercher un pokémon"/>
        </div>
 
        <div class="collection">
          <a *ngFor="let pokemon of pokemons$ | async"
            (click)="gotoDetail(pokemon)" class="collection-item">
            {{ pokemon.name }}
          </a>
        </div>
      </div>
    </div>
  </div>
</div>

search-pokemon.component.ts

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
 
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { Observable, Subject, of } from 'rxjs';
 
import { PokemonsService } from './pokemons.service';
import { Pokemon } from './pokemon';
  
@Component({
    selector: 'pokemon-search',
    templateUrl: './app/pokemons/search-pokemon.component.html'
})
export class PokemonSearchComponent implements OnInit {
  
    private searchTerms = new Subject<string>();
    pokemons$: Observable<Pokemon[]>;
  
    constructor(
        private pokemonsService: PokemonsService,
        private router: Router) { }
  
    // Ajoute un terme de recherche dans le flux de l'Observable 'searchTerms'
    search(term: string): void {
        this.searchTerms.next(term);
    }
  
    ngOnInit(): void {
        this.pokemons$ = this.searchTerms.pipe(
            // attendre 300ms de pause entre chaque requête
            debounceTime(300),
            // ignorer la recherche en cours si c'est la même que la précédente
            distinctUntilChanged(),
            // on retourne la liste des résultats correpsondant aux termes de la recherche
            switchMap((term: string) => this.pokemonsService.searchPokemons(term)),
        );
    }
  
    gotoDetail(pokemon: Pokemon): void {
        let link = ['/pokemon', pokemon.id];
        this.router.navigate(link);
    }
}

Chapitre 11 : Effectuer des traitements asynchrones avec RxJS

loader.component.ts

import { Component } from '@angular/core';
 
@Component({
  selector: 'pkmn-loader',
  template: `
    <div class="preloader-wrapper big active">
      <div class="spinner-layer spinner-blue">
        <div class="circle-clipper left">
          <div class="circle"></div>
        </div><div class="gap-patch">
          <div class="circle"></div>
        </div><div class="circle-clipper right">
          <div class="circle"></div>
        </div>
      </div>
    </div>
  `
})
export class LoaderComponent {}

search-pokemon.component.html

<div class="row">
  <div class="col s12 m6 offset-m3">
    <div class="card">
      <div class="card-content">
        <div class="input-field">
          <input #searchBox (keyup)="search(searchBox.value)"
            placeholder="Rechercher un pokémon"/>
        </div>
 
        <div class="collection">
          <a *ngFor="let pokemon of pokemons$ | async"
            (click)="gotoDetail(pokemon)" class="collection-item">
            {{ pokemon.name }}
          </a>
        </div>
      </div>
    </div>
  </div>
</div>

search-pokemon.component.ts

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
 
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { Observable, Subject, of } from 'rxjs';
 
import { PokemonsService } from './pokemons.service';
import { Pokemon } from './pokemon';
  
@Component({
    selector: 'pokemon-search',
    templateUrl: './app/pokemons/search-pokemon.component.html'
})
export class PokemonSearchComponent implements OnInit {
  
    private searchTerms = new Subject<string>();
    pokemons$: Observable<Pokemon[]>;
  
    constructor(
        private pokemonsService: PokemonsService,
        private router: Router) { }
  
    // Ajoute un terme de recherche dans le flux de l'Observable 'searchTerms'
    search(term: string): void {
        this.searchTerms.next(term);
    }
  
    ngOnInit(): void {
        this.pokemons$ = this.searchTerms.pipe(
            // attendre 300ms de pause entre chaque requête
            debounceTime(300),
            // ignorer la recherche en cours si c'est la même que la précédente
            distinctUntilChanged(),
            // on retourne la liste des résultats correpsondant aux termes de la recherche
            switchMap((term: string) => this.pokemonsService.searchPokemons(term)),
        );
    }
  
    gotoDetail(pokemon: Pokemon): void {
        let link = ['/pokemon', pokemon.id];
        this.router.navigate(link);
    }
}

Chapitre 12 : Authentification

auth-guard.service.ts

import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot }
    from '@angular/router';
import { AuthService } from './auth.service';
 
@Injectable()
export class AuthGuard implements CanActivate {
 
    constructor(private authService: AuthService, private router: Router) { }
 
    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
        let url: string = state.url;
        return this.checkLogin(url);
    }
 
    checkLogin(url: string): boolean {
        if (this.authService.isLoggedIn) { return true; }
        this.authService.redirectUrl = url;
        this.router.navigate(['/login']);
 
        return false;
    }
}

auth.service.ts

import { Injectable } from '@angular/core';
// RxJS 6
import { Observable, of } from 'rxjs';
import { tap, delay } from 'rxjs/operators';
 
@Injectable()
export class AuthService {
    isLoggedIn: boolean = false; // L'utilisateur est-il connecté ?
    redirectUrl: string; // où rediriger l'utilisateur après l'authentification ?
    // Une méthode de connexion
    login(name: string, password: string): Observable<boolean> {
        // Faites votre appel à un service d'authentification...
        let isLoggedIn = (name === 'pikachu' && password === 'pikachu');
 
        return of(true).pipe(
            delay(1000),
            tap(val => this.isLoggedIn = isLoggedIn)
        );
    }
 
    // Une méthode de déconnexion
    logout(): void {
        this.isLoggedIn = false;
    }
}

login-routing.module.ts

import { NgModule }       from '@angular/core';
import { RouterModule }   from '@angular/router';
import { AuthService }    from './auth.service';
import { LoginComponent } from './login.component';
 
@NgModule({
  imports: [
    RouterModule.forChild([
      { path: 'login', component: LoginComponent }
    ])
  ],
  exports: [
    RouterModule
  ],
  providers: [
    AuthService
  ]
})
export class LoginRoutingModule {}

login.component.ts

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';
 
@Component({
    selector: 'login',
    template: `
    <div class='row'>
    <div class="col s12 m4 offset-m4">
    <div class="card hoverable">
      <div class="card-content center">
        <span class="card-title">Page de connexion</span>
        <p><em>{{message}}</em></p>
      </div>
            <form #loginForm="ngForm">
          <div>
                    <label for="name">Name</label>
            <input type="text" id="name" [(ngModel)]="name" name="name" required>
          </div>
          <div>
            <label for="password">Password</label>
            <input type="password" id="password" [(ngModel)]="password" name="password" required>
          </div>
        </form>
      <div class="card-action center">
        <a (click)="login()" class="waves-effect waves-light btn"  *ngIf="!authService.isLoggedIn">Se connecter</a>
        <a (click)="logout()" *ngIf="authService.isLoggedIn">Se déconnecter</a>
      </div>
    </div>
    </div>
    </div>
  `
})
export class LoginComponent {
    message: string = 'Vous êtes déconnecté. (pikachu/pikachu)';
    private name: string;
    private password: string;
 
    constructor(private authService: AuthService, private router: Router) { }
 
    // Informe l'utilisateur sur son authentfication.
    setMessage() {
        this.message = this.authService.isLoggedIn ?
            'Vous êtes connecté.' : 'Identifiant ou mot de passe incorrect.';
    }
 
    // Connecte l'utilisateur auprès du Guard
    login() {
        this.message = 'Tentative de connexion en cours ...';
        this.authService.login(this.name, this.password).subscribe(() => {
            this.setMessage();
            if (this.authService.isLoggedIn) {
                // Récupère l'URL de redirection depuis le service d'authentification
                // Si aucune redirection n'a été définis, redirige l'utilisateur vers la liste des pokemons.
                let redirect = this.authService.redirectUrl ? this.authService.redirectUrl : '/pokemon/all';
                // Redirige l'utilisateur
                this.router.navigate([redirect]);
            } else {
                this.password = '';
            }
        });
    }
 
    // Déconnecte l'utilisateur
    logout() {
        this.authService.logout();
        this.setMessage();
    }
}

Chapitre 13 : Déployer votre application

systemjs.config.server.js

/**
 * Version de SystemJS dédié à la production.
 * Les paquets sont chargés depuis le web.
 */
(function (global) {
  System.config({
    paths: {
      'npm:': 'https://unpkg.com/' // Le nouvel alias
    },
 
    map: {
      app: 'dist',
      '@angular/core': 'npm:@angular/core@8.0.0/bundles/core.umd.min.js',
      '@angular/common': 'npm:@angular/common@8.0.0/bundles/common.umd.min.js',
      '@angular/compiler': 'npm:@angular/compiler@8.0.0/bundles/compiler.umd.min.js',
      '@angular/platform-browser': 'npm:@angular/platform-browser@8.0.0/bundles/platform-browser.umd.min.js',
      '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic@8.0.0/bundles/platform-browser-dynamic.umd.min.js',
      '@angular/common/http': 'npm:@angular/common@8.0.0/bundles/common-http.umd.min.js',
      '@angular/router': 'npm:@angular/router@8.0.0/bundles/router.umd.min.js',
      '@angular/forms': 'npm:@angular/forms@8.0.0/bundles/forms.umd.min.js',
      'rxjs': 'npm:rxjs@6.2.0',
      'angular-in-memory-web-api': 'npm:angular-in-memory-web-api@0.8.0/bundles/in-memory-web-api.umd.js',
      'tslib': 'npm:tslib/tslib.js'
    },
    packages: {
      app: {
        main: './main.js',
        defaultExtension: 'js'
      },
      rxjs: {
        main: 'index.js',
        defaultExtension: 'js'
      },
      'rxjs/operators': {
        main: 'index.js',
        defaultExtension: 'js'
      }
    }
  });
  })(this);

Cadeau : Votre Livre « Apprendre Angular » OFFERT

Le seul livre Angular en couleurs… Plus de 250 pages de contenu pour développer facilement votre première application Angular. Vous recevrez ce livre directement dans votre boîte aux lettres !

Un Magnifique Livre de plus 260 pages, 5 parties, et en couleurs !