Outils pour utilisateurs

Outils du site


lang:angular:projet

Ceci est une ancienne révision du document !


Création

Documentation

Création d'un projet de base : voir le tutorial de Angular Archive du v9.1.6 le 08/05/2020

Convertir un projet Angular en PWA : voir la page PWA de Angular Archive du v9.1.6 le 08/05/2020

Tutorial débutant mais clair : Comment créer une application web avec Angular Archive v11 21/03/21

Formation complète Le guide Angular - Marmicode Archive du 2019 le 28/07/2021 wget --recursive --no-clobber --page-requisites --html-extension --convert-links --restrict-file-names=windows --span-hosts --no-parent --content-disposition --domains=guide-angular.wishtack.io,gblobscdn.gitbook.com https://guide-angular.wishtack.io/

Projet unique

ng new angular

angular est le nom du dossier du nouveau projet

  • Erreurs possibles
...
CREATE angular/src/app/app.component.ts (211 bytes)
CREATE angular/src/app/app.component.css (0 bytes)
⠧ Installing packages (npm)...npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree 
npm ERR!
npm ERR! While resolving: angular@0.0.0
npm ERR! Found: jasmine-core@3.7.1
npm ERR! node_modules/jasmine-core
npm ERR!   dev jasmine-core@"~3.7.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer jasmine-core@">=3.8" from karma-jasmine-html-reporter@1.7.0
npm ERR! node_modules/karma-jasmine-html-reporter
npm ERR!   dev karma-jasmine-html-reporter@"^1.5.0" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR! See C:\Users\legar\AppData\Local\npm-cache\eresolve-report.txt for a full report. 

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\legar\AppData\Local\npm-cache\_logs\2021-07-14T20_17_00_602Z-debug.log
✖ Package install failed, see above.
The Schematic workflow failed. See above.

Cela peut arriver si la version d'Angular est un peu ancienne et que le fichier package.json.template est obsolète.

Supprimer le dossier créé et relancer :

ng new --skip-install [project]

Ici, on voit qu'il faut remplacer karma-jasmine-html-reporter@^1.5.0 par karma-jasmine-html-reporter@1.7.0 et jasmine-core@~3.7.0 par jasmine-core@>=3.8.

et lancer

npm install

unable to resolve dependency tree error for creating new angular project Archive du 07/05/2021 le 14/07/2021

Workspace avec une librairie et une application

ng new workspace --create-application false
cd workspace
ng generate library my-lib
ng generate application my-app

How to Create Library in Angular Tutorial Archive du 01/07/2020 le 25/07/2021

Le tutoriel explique ça très bien.

Par contre, l'utilisation de ng serve sur l'application ne détecte pas les modifications de la librairie. Pour résoudre ce problème, il faut faire une build watch de la librairie :

ng build library --watch &
ng serve application

Ne pas oublier le & pour laisser tourner la tâche en fond ou ne pas le mettre et utiliser deux terminaux différents.

  • Exemple d'architecture des fichiers
.
├── CMakeLists.txt
├── README.md
├── angular.json
├── dist
│   ├── app-main
│   │   ├── 3rdpartylicenses.txt
│   │   ├── assets
│   │   │   └── ...
│   │   ├── favicon.ico
│   │   ├── index.html
│   │   ├── ...
│   └── lib-jessica
│       ├── README.md
│       ├── esm2015
│       │   ├── ...
│       ├── fesm2015
│       │   ├── ...
│       ├── lib
│       │   ├── ...
│       └── ...
├── node_modules
│   └── ...
├── package-lock.json
├── package.json
├── projects
│   ├── app-main
│   │   ├── karma.conf.js
│   │   ├── src
│   │   │   ├── app
│   │   │   │   ├── ...
│   │   │   ├── assets
│   │   │   ├── environments
│   │   │   │   ├── environment.prod.ts
│   │   │   │   └── environment.ts
│   │   │   ├── favicon.ico
│   │   │   ├── index.html
│   │   │   ├── main.ts
│   │   │   ├── polyfills.ts
│   │   │   ├── styles.css
│   │   │   └── test.ts
│   │   ├── tsconfig.app.json
│   │   └── tsconfig.spec.json
│   └── lib-jessica
│       ├── README.md
│       ├── assets
│       ├── karma.conf.js
│       ├── ng-package.json
│       ├── package.json
│       ├── src
│       │   ├── assets
│       │   │   └── ...
│       │   ├── lib
│       │   │   └── ...
│       │   ├── public-api.ts
│       │   ├── test.ts
│       ├── tsconfig.lib.json
│       ├── tsconfig.lib.prod.json
│       └── tsconfig.spec.json
├── tree.log
└── tsconfig.json

Workspace and project file structure Archive du v12.1.5 le 31/07/2021

Nouvelle vue

  • Exemple
cd my-app/src/app
ng generate component ui/main

Cela devrait mettre à jour le fichier my-app/src/app/app.module.ts en ajoutant :

import { MainComponent } from './ui/main/main.component';

et en mettant à jour declarations :

declarations: [AppComponent, MainComponent],

Une fois le composant activé, si le routing est activé, il reste à lui fournir un path d'accès. Modifier le fichier my-app/src/app/app-routing.module.ts :

+import { MainComponent } from './ui/main/main.component';
+
-const routes: Routes = [];
+const routes: Routes = [
+  { path: '', component: MainComponent }
+];
  • Erreurs possibles
Could not find an NgModule. Use the skip-import option to skip importing in NgModule.

Il faut lancer la commande ng generate component ui/main depuis le dossier cd my-app/src/app.

Formulaire

Simple

Le code ci-dessous est la méthode réactive. La méthode template est déconseillée.

  • Code HTML

Le code spécifique à Angular est entre [] ou ().

<form [formGroup]="meyerhorForm">
  <label for="width">Width: </label>
  <input id="width" type="text" formControlName="width" />
  <br />
  <label for="load">Load: </label>
  <input id="load" type="text" formControlName="load" />
  <br />
  <label for="eccentric">Eccentric: </label>
  <input id="eccentric" type="text" formControlName="eccentric" />
  <br />
  <button [disabled]="!meyerhorForm.valid" type="submit" (click)="submit()">
    SUBMIT
  </button>
</form>
  • Code Angular
file.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import * as Module from './jessica-web';
import { validNumber } from '../util/validator/valid-number.validator';
import { Subscription } from 'rxjs';
import { debounceTime, filter, switchMap } from 'rxjs/operators';
 
@Component({
  selector: 'lib-lib-jessica',
  templateUrl: './lib-jessica.component.html',
  styles: [],
})
export class LibJessicaComponent implements OnInit, OnDestroy {
  private logger?: any;
 
  // Les données accessibles depuis le fichier html doivent être publiques.
  meyerhorForm: FormGroup;
 
  // Par convention, les variables observables ont pour suffixe $.
  private obs$!: Subscription;
 
  submit() {
    console.log(Number(this.meyerhorForm.value.width));
    console.log(Number(this.meyerhorForm.value.load));
    console.log(Number(this.meyerhorForm.value.eccentric));
  }
 
  private instance?: any;
 
  constructor() {
    this.meyerhorForm = new FormGroup({
      // Chaque champ doit être complété et être des nombres.
      width: new FormControl(null, [Validators.required, validNumber]),
      load: new FormControl(null, [Validators.required, validNumber]),
      eccentric: new FormControl(null, [Validators.required, validNumber]),
    });
    Module.default().then(async (instance: any) => {
      this.instance = instance;
      this.logger = new this.instance.SpdlogStdoutMt('log');
    });
  }
 
  ngOnInit(): void {
    // On s'inscrit dans ngOnInit.
    this.obs$ = this.meyerhorForm.valueChanges
      // Limite à une requête toutes les 200ms.
      .pipe(debounceTime(200))
      .pipe(
        // On n'applique l'action que si le formulaire est valide.
        filter(
          () => this.meyerhorForm.valid && this.instance !== undefined
        ),
        switchMap((/*data*/) => {
          ...
 
          return newvalue;
        })
      )
      // Applique la fonction subscribe pour chaque valeur dans switchMap.
      .subscribe((newvalue) => console.log(newvalue));
  }
 
  ngOnDestroy(): void {
    // On se désinscrit dans ngOnDestroy pour éviter les fuites mémoires.
    this.obs$!.unsubscribe();
  }
}

The RxJS library Archive du 12.1.4 le 28/07/2021

  • Notes

Si on veut que switchMap renvoie plusieurs valeurs, il faut return [{ a: valeur1, b: valeur2 }]; si la fonction est synchrone. Mais si la fonction est asynchrone, il faut return { a: valeur1, b: valeur2 };. switchMap est similaire à un map en programmation fonctionnelle. Mais puisque les events sont asynchrones, si un switchMap n'est pas terminé quand le suivant arrive, le plus ancien est abandonné : la fonction définie par subscribe n'est pas appelée mais la totalité de la fonction définie par le switchMap est exécutée.

Ne pas utiliser @Input() meyerhorForm?: FormGroup; sans le définir dans le constructeur. C'est la méthode template.

  • Validateur
valid-number.validator.ts
import { ValidatorFn } from '@angular/forms';
 
export const validNumber: ValidatorFn = (control) => {
  // value ne doit pas être vide. isNan renvoie false si "" ou null.
  if (isNaN(Number(control.value))) {
    return {
      validNumber: {
        reason: 'invalid number',
        value: control.value,
      },
    };
  }
 
  return null;
};

Imbriqué

Dividing a form into multiple components with validation (model driven) Archive du 07/04/2017 le 01/08/2021 Archive du projet

Angular Custom Form Controls with Reactive Forms and NgModel Archive du 19/10/2016 le 01/08/2021 Archive du projet

Angular - Reactive forms Archive du 12.1.5 le 01/08/2021 Archive du projet

Angular Forms Guide - Template Driven and Reactive Forms Archive du 26/01/2021 le 01/08/2021

Validating form input Archive du 12.1.5 le 01/08/2021

Angular: Nested Reactive Forms Using ControlValueAccessors(CVAs) Archive du 09/01/2019 le 01/08/2021 Archive du projet

Never again be confused when implementing ControlValueAccessor in Angular forms Archive du 14/09/2017 le 01/08/2021

C'est le formulaire enfant qui sera inclus dans le parent. L'enfant doit implémenter les interfaces ControlValueAccessor, Validator et ajouter les providers. Le parent doit implémenter le Subscriber.

Dans le cas de formulaire à plus de 2 niveaux : le niveau le plus haut implémente uniquement Subscriber, les niveaux les plus bas implémentent uniquement ControlValueAccessor, Validator et providers, les niveaux intermédiaires doivent tout implémenter.

  • Formulaire de base de l'enfant
foundation-strip-form.component.html
<!-- Le nom de formGroup doit être le même que la variable dans le fichier TypeScript -->
<div [formGroup]="foundation">
  <!-- html5 : le for va avec le id -->
  <!-- le formControlName doit avoir le même nom que le nom dans le fichier TypeScript -->
  <label for="width">Width: </label>
  <input id="width" type="text" formControlName="width" />
</div>

L'interface sera pratique pour exploiter les données depuis la form parent.

foundation-strip.ts
export interface FoundationStrip {
  width: number;
}
foundation-strip-form.component.ts
/* eslint-disable max-len */
import { Component, forwardRef } from '@angular/core';
import {
  ControlValueAccessor,
  FormGroup,
  Validators,
  NG_VALUE_ACCESSOR,
  NG_VALIDATORS,
  ValidationErrors,
  FormBuilder,
  Validator
} from '@angular/forms';
import { PartialObserver } from 'rxjs';
import { validNumber } from '../../../util/validator/valid-number.validator';
/* eslint-enable max-len */
 
@Component({
  selector: 'lib-foundation-strip-form',
  templateUrl: './foundation-strip-form.component.html',
  styleUrls: ['./foundation-strip-form.component.css'],
  providers: [
    {
      // Pour faire fonctionner la fonction writeValue.
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => FoundationStripFormComponent),
      multi: true
    },
    {
      // Pour forcer l'implémentation du validator.
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => FoundationStripFormComponent),
      multi: true
    }
  ]
})
export class FoundationStripFormComponent
  implements ControlValueAccessor, Validator
{
  foundation: FormGroup;
 
  // FormBuilder est injecté. Son utilisation est moins verbeuse que "new FormGroup".
  constructor(private fromBuilder: FormBuilder) {
    this.foundation = this.fromBuilder.group({
      // Validators.required : doit être non vide
      // validNumber : validateur perso pour valider un nombre.
      width: [null, [Validators.required, validNumber]]
    });
  }
 
  // ControlValueAccessor
  public onTouched!: () => void;
 
  // val est l'interface contenant les données
  writeValue(val: FoundationStrip): void {
    val && this.foundation.setValue(val, { emitEvent: false });
  }
  registerOnChange(
    fn?: PartialObserver<{ [key: string]: string | undefined }>
  ): void {
    this.foundation.valueChanges.subscribe(fn);
  }
  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }
  setDisabledState?(isDisabled: boolean): void {
    isDisabled ? this.foundation.disable() : this.foundation.enable();
  }
 
  validate(): ValidationErrors | null 
    // Si la form foundation ne contient que les sous-formulaires
    // dont les validateurs ne sont pas déclarées l'instanciation
    // this.foundation = this.fromBuilder.group, il faudra faire :
    // return this.form.controls.subform1.valid && this.form.controls.subform2.valid ?
    return this.foundation.valid
      ? null
      : {
          invalidForm: {
            valid: false,
            message: 'basicInfoForm fields are invalid'
          }
        };
  }
}
  • Formulaire de base du parent
meyerhof-form.component.html
<h3>Nested FormGroup</h3>
 
<form [formGroup]="form">
  <!-- formControlName est bien le nom dans le fichier TypeScript parent et non enfant -->
  <lib-foundation-strip-form
    formControlName="foundation"
  ></lib-foundation-strip-form>
  <!-- Divers autre formulaire -->
  <lib-vertical-eccentric-form
    formControlName="load"
  ></lib-vertical-eccentric-form>
  <button type="submit" (click)="submit()">Submit</button>
</form>
 
<!-- Le texte sera mise à jour en temps réel -->
<pre>Form Values: {{ form.valid }} {{ form.value | json }}</pre>
meyerhof-form.ts
/* eslint-disable max-len */
import { FoundationStrip } from '../foundation-strip/foundation-strip';
import { VerticalEccentric } from '../../load/vertical-eccentric/vertical-eccentric';
/* eslint-enable max-len */
 
export interface MeyerhofForm {
  foundation: FoundationStrip;
  load: VerticalEccentric;
}
meyerhof-form.component.ts
import {
  Component,
  EventEmitter,
  OnDestroy,
  OnInit,
  Output
} from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Subscription } from 'rxjs';
import { debounceTime, filter } from 'rxjs/operators';
 
import { MeyerhofForm } from './meyerhof-form';
 
@Component({
  selector: 'lib-meyerhof-form',
  templateUrl: './meyerhof-form.component.html',
  styleUrls: ['./meyerhof-form.component.css']
})
// On alloue l'observateur réactif dans OnInit et on le libère dans OnDestroy.
export class MeyerhofFormComponent implements OnInit, OnDestroy {
  form: FormGroup;
  private obs$!: Subscription;
 
  constructor(private fromBuilder: FormBuilder) {
    this.form = fromBuilder.group({
      foundation: [''],
      load: ['']
    });
  }
 
  submit(): void {
    console.log('MeyerhofFormComponent');
    console.log(JSON.stringify(this.form.controls.foundation.value['width']));
    console.log(JSON.stringify(this.form.controls.load.value['load']));
    console.log(JSON.stringify(this.form.controls.load.value['eccentric']));
  }
 
  ngOnInit(): void {
    this.obs$ = this.form.valueChanges
      .pipe(debounceTime(200))
      .pipe(filter(() => this.form.valid))
      // On force l'interface de l'objet a.
      .subscribe((a: MeyerhofForm) => {
        console.log('123/' + JSON.stringify(a.foundation.width) + '/456');
      });
  }
 
  ngOnDestroy(): void {
    this.obs$?.unsubscribe();
  }
}

Transfert de données par évènement

Sharing data between child and parent directives and components Archive du 12.1.5 le 01/08/2021 Archive du projet

Angular 8: Send Form Data from Child Output to Parent with Reactive Forms Archive du 15/11/2019 le 01/08/2021

Le présent paragraphe utilise l'annotation @Output ce qui est un template-driven form. Pour une form réactive, il suffit d'utiliser la méthode du paragraphe précédent en gardant le schéma : l'enfant implémente ControlValueAccessor et Validator, le parent implémente la Subscription.

Dans la form contenant les données, il faut ajouter un attribut d'évènements avec le décorateur @Output.

child.component.ts
export class MeyerhofFormComponent implements OnInit {
 
  @Output() changeEvent = new EventEmitter<MeyerhofForm>();
 
  ngOnInit(): void {
    this.obs$ = this.form.valueChanges
      .pipe(filter(() => this.form.valid))
      .pipe(debounceTime(200))
      .subscribe((a: MeyerhofForm) => {
        // Plutôt que de traiter les données, elles sont envoyés brutes aux parents.
        this.changeEvent.emit(a);
      });
  }
}
parent.component.html
<lib-meyerhof-form (changeEvent)="compute($event)"></lib-meyerhof-form>
parent.component.ts
export class MeyerhofCalcComponent {
 
  compute(newItem: MeyerhofForm): void {
    ...
  }
}

Ajout de fonctionnalités

Ajouter d'une dépendance javascript

Si le code Javascript n'est pas utilisé dans un code TypeScript

Télécharger bootstrap et copier les dossiers css et js dans projet\ui\assets\bootstrap.

Ajouter la dépendance au fichier package.json :

npm install --save bootstrap@4.5.0

et ajouter les fichiers .css et .js dans tous les .html. Pour cela ajouter aux rubriques build-angular:browser et build-angular:karma (ci-besoin pour les tests) :

            "styles": [
              "src/styles.css",
              "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
              "node_modules/bootstrap/dist/css/bootstrap.min.css",
              "src/assets/params/css/index.css"
            ],
            "scripts": [
              "node_modules/jquery/dist/jquery.min.js",
              "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js",
              "src/assets/params/js/index.js"
            ]
lang/angular/projet.1627854433.txt.gz · Dernière modification : 2021/08/01 23:47 de root