Skip to content

Latest commit

 

History

History
1194 lines (842 loc) · 22.7 KB

slides.md

File metadata and controls

1194 lines (842 loc) · 22.7 KB
theme background class highlighter lineNumbers info drawings title
./theme
text-center
shiki
false
## Lucca Angular for integrator training slides
persist
Angular Basics

Angular Basics

Formation Angular pour les intégrateurs


layout: section

Component 1/2

Création d'un component

Par convention, on utilise UN fichier contenant UNE classe pour UN Component.

1 component = 1 classe dans 1 fichier

Convention de nommage : something.component.ts

Angular expose ses classes, méthodes et outils dans le namespace @angular. Par exemple @angular/core.

L'api complète exposée est présentée ici : API Angular

Pour créer un Component il faut exposer une classe décorée par @Component. En passant les options suivantes :

  • selector : string nom de l'élément html créé permettant d'utiliser le component.
  • template : string code html du Component

layout: section

Component 2/2

Utilisation d'un Component

Pour utiliser un Component dans un autre, il faut le déclarer dans le Module.

declarations: any[] liste des Components utilisés

@NgModule({
  ...
  declarations: [ MyComponent ],
  ...
})

Le nouveau Component pourra alors être utilisé dans l'ensemble du Module.

Ne pas oublier d'importer le Component (ici PlanetsComponents) avec le chemin relatif et sans l'extension '.ts'.


layout: section

Template

Afficher les propriétés d'un Component

Dans la classe, on ajoute un attribut à la classe.

export class PlanetsComponent {
    title = 'La liste des planètes'
}

Dans le template, on affiche une propriété de l'objet Component en utilisant les 'doubles moustaches' {{ title }}

<h2>{{ title }}</h2>

layout: section

Property Binding 1/3

Syntaxe

[prop]="value"

One way data binding

Les changements sur les propriétés faites dans le component sont actualisés sur le DOM, mais pas l'inverse.


layout: section

Property Binding 2/3

Différence attribut - propriété

<a href='foo.html' class='test one' name='fooAnchor' id='fooAnchor'>Hi</a>
+-------------------------------------------+
| a                                         |
+-------------------------------------------+
| href:       "http://example.com/foo.html" |
| name:       "fooAnchor"                   |
| id:         "fooAnchor"                   |
| className:  "test one"                    |
| attributes:                               |
|    href:  "foo.html"                      |
|    name:  "fooAnchor"                     |
|    id:    "fooAnchor"                     |
|    class: "test one"                      |
+-------------------------------------------+
const link = document.getElementById('fooAnchor');
alert(link.href);                 // alerts "http://example.com/foo.html"
alert(link.getAttribute("href")); // alerts "foo.html"

layout: section

Property Binding 3/3

Attribute binding

Il peut arriver que l'on veuille adresser un attribut et non pas une propriété.

<table>
    <tr>
        <td>un</td>
        <td>deux</td>
    </tr>
    <tr>
        <td [attr.colspan]="1 + 1">trois</td>
    </tr>
</table>

layout: section

Class Binding

Ajout de classes css dynamquement

<span class="un deux" [class]="myClass">Test</span>

Attention la classe est écrasée

<span [class.un]="true" [class.deux]="false">Test</span>
<span [ngClass]="{un: true, deux: false}"></span>

layout: section

Style Binding

Modification dynamique de styles inline

<p [style.color]="pinkColor">
    [style.color]="pinkColor"
</p>

<p [style.fontSize.em]="1.5">
    [style.fontSize.em]="1.5"
</p>

layout: section

Event Binding

Déclenchement d'actions sur des événements

<button (click)="doSomething()">un</button>
<button on-click="doSomething()">deux</button>

doSomething est une méthode du component.

Depuis le template, on peut passer l'objet Event $event à la méthode utilisée.

<button (click)="doSomething($event)">un</button>

Il est alors possible d'utiliser. (Par exemple $event.stopPropagation).


layout: section

Input Properties 1/2

Déclaration via Decorator

Importer le Decorator Input

import { Input } from '@angular/core';

Ajouter Input() devant la propriété

@Input() name = '';

layout: section

Input Properties 2/2

Utilisation

<my-component [name]="'John Doe'"></my-component>

layout: section

Output Properties 1/4

Mise en place de l'event

Importer EventEmitter

import { EventEmitter } from '@angular/core';

Déclarer l'event dans la classe

change = new EventEmitter();

layout: section

Output Properties 2/4

Déclaration via Decorator

Importer le Decorator Output

import { Output } from '@angular/core';

Ajouter Output() devant l'event

@Output() change = new EventEmitter();

layout: section

Output Properties 3/4

Déclenchement de l'event

Où désiré dans le code, il suffit d'appeler :

this.change.emit(data);

Avec data l'information à transmettre.


layout: section

Output Properties 4/4

Utilisation

Comme un event natif :

<my-component (change)="doSomething($event)">

et on récupère les informations dans $event


layout: section

Built-in Directives 1/3

ngIf

<div *ngIf="errorCount > 0" class="error">
  {{errorCount}} errors detected
</div>

layout: section

Built-in Directives 2/3

ngSwitch

<div [ngSwitch]="value">
    <p *ngSwitchCase="'init'">increment to start</p>
    <p *ngSwitchCase="0">0, increment again</p>
    <p *ngSwitchCase="1">1, increment again</p>
    <p *ngSwitchCase="2">2, stop incrementing</p>
    <p *ngSwitchDefault>&gt; 2, STOP!</p>
</div>

layout: section

Built-in Directives 3/3

ngFor (ngForOf)

<li *ngFor="let planet of planets; let i = index">
    {{ i }} : {{ planet }}
</li>

En plus de index, on a : first, last, even, odd


layout: section

Pipes

Transformation de données à l'affichage

{{ valeur_à_formater | nom_du_pipe }}
{{ valeur_à_formater | nom_du_pipe:argument_1:argument_2 }}
{{ valeur_à_formater | nom_du_pipe_1 | nom_du_pipe_2 }}

Quelques pipes fournies par Angular

AsyncPipe, CurrencyPipe, DatePipe, DecimalPipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, PercentPipe, SlicePipe, TitleCasePipe, UpperCasePipe


layout: section

Custom Pipes

Écrire ses propres Pipes

something.pipe.ts

import { Pipe, PipeTransform } from	'@angular/core'

@Pipe({ name: 'summary' })
export class SomethingPipe implements PipeTransform {
  transform (value: string, param1) {
    // code
  }
}

Déclaration dans le module

@NgModule({
    ...
    declarations: [SomethingPipe]
    ...
})

layout: section

Elvis Operator

Safe navigation operator

permet d'éviter les erreurs dûes à la lecture de propriétés sur les objets null ou undefined.

Provoque une erreur si user n'existe pas
{{ user.firstName }}

Sans erreur (n'affiche rien) si user n'existe pas
{{ user?.firstName }}

layout: section

Transclusion

Transporter le contenu d'un Component

Le système de transclusion de Angular. Il suffit d'utiliser le component <ng-content> pour récupérer le contenu passé.

Il est possible de récupérer un sous contenu en précisant la cible via l'attribut select de ce component. Le ciblage se fait alors avec un sélecteur css.


layout: section

Forms

Model Driven Forms vs Template Driven Forms

Une approche entièrement déclarative, reposant sur des directives. C'est l'approche Template Driven.

Une approche plus programatique, dans laquelle il est de la responsabilité du développeur de créer le modèle. C'est l'approche Model Driven


layout: section

ReactiveForms 1/13

Préparation

import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [
    ReactiveFormsModule
  ]
});

layout: section

ReactiveForms 2/13

FormGroup

Pour représenter et manipuler le formulaire, nous allons créer un objet FormGroup.

import { FormGroup } from '@angular/forms';

export class UserFormComponent implements OnInit {

  form: FormGroup;

  ngOnInit() {
    this.form = new FormGroup({});
  }

}

layout: section

ReactiveForms 3/13

FormControl

Un formulaire n'est intéressant que s'il contient des champs.

import { FormGroup, FormControl } from '@angular/forms';

export class UserFormComponent implements OnInit {

  form: FormGroup;

  ngOnInit() {
    this.form = new FormGroup({
      name: new FormControl()
    });
  }

}

layout: section

ReactiveForms 4/13

Sous groupes

Il est possible de déclarer un FormGroup dans un FormGroup.

import { FormGroup, FormControl } from '@angular/forms';

export class UserFormComponent implements OnInit {

  form: FormGroup;

  ngOnInit() {
    this.form = new FormGroup({
      address: new FormGroup({
          street: new FormControl()
      })
    });
  }

}

layout: section

ReactiveForms 5/13

Bound to Html

<form [formGroup]="form">
  <input formControlName="name" type="text">
  <fieldset formGroupName="address">
      <input formControlName="street" type="text">
  </fieldset>
</form>

layout: section

ReactiveForms 6/13

Validators

Un FormControl peut être construit avec :

  1. une valeur par défaut,
  2. un validateur ou un tableau de validateurs.
import { Validators } from '@angular/forms';

export class UserFormComponent implements OnInit {
  ngOnInit() {

    this.form = new FormGroup({
      name: new FormControl('', Validators.required),
      bio: new FormControl('', [
        Validators.required,
        Validators.minLength(10)
      ]),
    });

  }

}

layout: section

ReactiveForms 7/13

Builtin validators

  • Validators.required
  • Validators.minLength(n: number)
  • Validators.maxLength(n: number)
  • Validators.pattern(r: Regexp)
  • Validators.min(n: number)
  • Validators.max(n: number)
  • Validators.email
  • Validators.requiredTrue

layout: section

ReactiveForms 8/13

State

Un objet FormControl (et donc FormGroup) porte un état.

this.form = new FormGroup({
    name: new FormControl('', Validators.required),
});


this.form.touched; // true / false
this.form.get('name').touched; // true / false

Les propriétés d'états :

  • valid / invalid
  • dirty / pristine
  • touched / untouched
  • value

layout: section

ReactiveForms 9/13

Error message & error class

<label for="name" class="six columns-md" [class.text-danger]="form.get('name').touched && form.get('name').invalid">
    Name
    <input 
        formControlName="name" 
        id="name" 
        type="text" 
        [class.has-error]="form.get('name').touched && form.get('name').invalid" 
        class="u-full-width">
    <p *ngIf="form.get('name').touched && form.get('name').invalid" class="text-danger">Required</p>
</label>

layout: section

ReactiveForms 10/13

Errors messages : Shortcuts

  form = new FormGroup({
    one: new FormControl(undefined, [Validators.required]),
    two: new FormGroup({
      three: new FormControl(),
    }),
  });

  oneControl = this.form.get('one') as FormControl;
  threeControl = this.form.get('two.three') as FormControl;
<label for="name" class="six columns-md" [class.text-danger]="oneControl.touched && oneControl.invalid">
    One
    <input [class.has-error]="oneControl.touched && oneControl.invalid"
      formControlName="one" id="one" type="text" class="u-full-width">
    <p *ngIf="oneControl.touched && oneControl.invalid" class="text-danger">Required</p>
</label>

layout: section

ReactiveForms 11/13

Erreurs spécifiques

La propriété errors d'un FormControl contient des informations sur les erreurs.

<div class="row">
    <label for="bio" class="twelve columns-md" [class.text-danger]="bio.touched && bio.invalid">
        Bio
        <textarea [class.has-error]="bio.touched && bio.invalid" 
          formControlName="bio" id="bio" class="u-full-width"></textarea>
        <p *ngIf="bio.touched && bio.invalid && bio.errors['required']" class="text-danger">Required</p>
        <p *ngIf="bio.touched && bio.invalid && bio.errors['minlength']" class="text-danger">
            Minlength : {{ bio.errors['minlength'].actualLength }} / {{ bio.errors['minlength'].requiredLength }}
        </p>
    </label>
</div>

layout: section

ReactiveForms 12/13

Valeurs initiales d'un formulaire

// remplit les champs en commun
this.form.patchValue({ name: 'John' });
// remplit tout (mais ne support pas les manques)
this.form.setValue({
  name: 'John',
  email: 'john@doe.fr',
});

layout: section

ReactiveForms 13/13

Soumettre le formulaire

Pour soumettre le formulaire on utilise l'event submit.

<form [formGroup]="form" novalidate (submit)="saveUser()">
...
</form>

layout: section

Template Drive Forms 1/8

Déclaration

NgForm est un directive dont le selecteur est <form>. De plus l'instance de la directive est exposée sous le nom 'ngForm'.

<form #form="ngForm">
</form>

Un NgForm contient une propriété form de type FormGroup.

Extrait des sources de Angular.

this.form = new FormGroup({}, composeValidators(validators), composeAsyncValidators(asyncValidators));

layout: section

Template Driven Forms 2/8

Links to controls

ngModel

La directive ngForm ne créé pas automatiquement les contrôles en lien avec les input. Il faut créer ce contrôle. C'est le rôle de la directive ngModel. Qui agit comme ngForm mais au niveau d'un input.

Name

La directive ngModel utilisée comme enfant de la directive ngForm a besoin d'être liée à celle-ci. Ce lien se fait via l'attribut name.

<form #form="ngForm">
    <input ngModel name="username" type="text">
</form>

layout: section

Template Driven Forms 3/8

Links to groups of controls

Il est possible de définir un sous formulaire (fieldset), grâce à la directive ngModelGroup.

<form #form="ngForm">

    <input ngModel name="username" type="text">

    <div ngModelGroup="profile">
        <input ngModel name="firstname" type="text">
        <input ngModel name="lastname" type="text">
    </div>

</form>

layout: section

Template Driven Forms 4/8

Valeur initiales

Pour fournir une valeur initiale à un contrôle, on ne peut pas utiliser l'attribut value. Celui-ci est écrasé par la directive ngModel. La solution consiste à passer une valeur à cette directive.

export class UserFormComponent {
  username: string = 't8g';
}
<input [ngModel]="username" name="username" id="username" type="text" class="u-full-width">

Par contre si on observe la propriété username, on s'aperçoit que celle-ci n'est pas mise à jour lorsque la valeur du contrôle change.


layout: section

Template Driven Forms 5/8

2way data binding : "banana box"

<input [(ngModel)]="username" name="username" id="username" type="text" class="u-full-width">

Cette syntaxe est en fait un raccourci pour

<input [ngModel]="username" (ngModelChange)="username = $event" 
  name="username" id="username" type="text" class="u-full-width">

layout: section

Template Driven Forms 6/8

Validation

Dans l'approche Template Driven, la validation se définit grâce à des directives.

<!-- email -->
<input type="email" name="email" ngModel email>
<input type="email" name="email" ngModel email="true">
<input type="email" name="email" ngModel [email]="true">

<!-- minlength / maxlength -->
<textarea name="bio" ngModel maxlength="100">
<textarea name="bio" ngModel [minlength]="min"><!-- min propriété du component -->

<!-- pattern -->
<input type="text" name="username" ngModel pattern="[a-zA-Z ]*">

<!-- required -->
<input type="text" name="username" ngModel required>

layout: section

Template Driven Forms 7/8

Afficher les erreurs

Pour accèder à un FormControl en particulier et ainsi à son état, on utilise form.controls['controlName']. Mais attention, à l'initialisation du template, celui-ci est nul. Donc penser à utiliser un elvis operator.

<form #form="ngForm">

    <div class="row">
      
      <label for="name" class="six columns-md" 
        [class.text-danger]="form.controls['name']?.touched && form.controls['name']?.invalid">
        Name
        <input ngModel name="name" required 
          [class.has-error]="form.controls['name']?.touched && form.controls['name']?.invalid" 
          id="name" type="text" class="u-full-width">
        <p *ngIf="form.controls['name']?.touched && form.controls['name']?.invalid" class="text-danger">Required</p>
      </label>

    </div>

</form>

layout: section

Template Driven Forms 8/8

Afficher les erreurs : raccourci

Pour alléger un peu le template, il est possible de récupérer une variable locale au template contenant l'instance du NgModel et donc l'état du NgControl associé.

<form #form="ngForm">

    <div class="row">
      
      <label for="name" class="six columns-md" 
        [class.text-danger]="name.touched && name.invalid">
        Name
        <input ngModel name="name" required #name="ngModel"
          [class.has-error]="name.touched && name.invalid" 
          id="name" type="text" class="u-full-width">
        <p *ngIf="name.touched && name.invalid" class="text-danger">Required</p>
      </label>

    </div>

</form>

layout: section

Routing 1/4

Configuration : app.routing.ts

import { Routes, RouterModule }   from '@angular/router';
import { FirstComponent } from './first.component';
import { SecondComponent } from './second.component';

// Configuration des routes
const appRoutes: Routes = [
  {
    path: 'first', // url (sans / initial)
    component: FirstComponent // composant à charger pour cette route
  },
  {
    path: 'second',
    component: SecondComponent
  }
];

// Export du module de routing configuré
export const routing = RouterModule.forRoot(appRoutes);

layout: section

Routing 2/4

Intégration dans le module

import { routing } from './app.routing';
import { AppComponent }  from './app.component';

@NgModule({
  imports: [
    BrowserModule,
    routing
  ],
  declarations: [
    AppComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

layout: section

Routing 3/4

Layout

Enfin dans le template du component principal, nous plaçons le component de route : Router Outlet

<router-outlet></router-outlet>

layout: section

Routing 4/4

Navigation

Pour naviguer d'une vue à une autre, on utilise une directive routerLink qui va créer le lien pour nous.

<a routerLink="/first">First</a>

Bonus : classe active

Il est possible de définir une classe css lorsque la route est active.

<a routerLink="/first" routerLinkActive="active">First</a>

layout: section

JS : données 1/4

forEach : une action pour chaque item

const innerPlanets = [
  { name: 'Mercure', satellites: [] },
  { name: 'Venus', satellites: [] },
  { name: 'Earth', satellites: [{ name: 'Lune' }] },
  { name: 'Mars', satellites: [{ name: 'Phobos' }, { name: 'Déimos' }]},
];

innerPlanets.forEach((planet) => {
  console.log('palnet name', planet.name);
});

layout: section

JS : données 2/4

map : nouveau tableau de données transformées

const planetsNames = innerPlanets.map((planet) => {
  return planet.name;
});
// ou
const planetsNames = innerPlanets.map((planet) => planet.name);
// ou
const planetsNames = innerPlanets.map(({ name }) => name);

layout: section

JS : données 3/4

filter : portion de tableau

const planetsWithSatellites = innerPlanets.filter((planet) => {
  return planet.satellites.length > 0; 
});
// ou
const planetsWithSatellites = innerPlanets.map((planet) => planet.satellites.length > 0);
// ou
const planetsWithSatellites = innerPlanets.map(({ satellites }) => satellites.length > 0);

layout: section

JS : données 4/4

reduce : transformation

const totalNumberOfSatellites = innerPlanets.reduce(
  (acc, planet) => {
    return acc + planet.satellites.length; 
  },
  0
);

layout: section

Reactive Programming

Définition

La programmation réactive est un paradigme de programmation visant à conserver une cohérence d'ensemble en propageant les modifications d'une source réactive (modification d'une variable, entrée utilisateur, etc.) aux éléments dépendants de cette source. - wikipédia

Beaucoup d'outils pour manipuler des données (.map, .filter, .reduce). Mais pour des données qui s'accumulent avec le temps ?

Nouvelle définition

"Reactive programming is programming with asynchronous data streams." - André Staltz


layout: section

Flux

Définitions

Un flux est simplement une collection qui s'étoffe avec le temps

Un Observable est un objet encapsulant un flux et qui peut être manipulé comme une collection.


layout: section

RxJS 1/2

Subscribe & Unsubscribe

someObservable$.subscribe((value) => {
  console.log('value', value);
});

const observer = {
  next: (value) => console.log('new value', value),
  error: (error) => console.log('error', error),
  complete: () => console.log('end'),
};

someObservable$.subscribe(observer);

const subscription = someObservable$.subscribe(x => console.log(x));
// Plus tard
subscription.unsubscribe();

layout: section

RxJS 2/2

Operators

// timer$ est un Observable qui compte les secondes 1, 2, 3, ...

timer$.pipe(
  filter((t) => t % 2 === 0),
  map((t) => `Il s'est passé ${t} secondes`),
);