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/
ng new angular
angular
est le nom du dossier du nouveau projet
... 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
Elle se fait avec le fichier tsconfig.json
. (TSConfig Reference Archive v5.0 le 15/01/2024)
compilerOptions
Cette rubrique contient les options pour le compilateur tsc. tsc CLI Options Archive du 04/01/2024 le 10/01/2024
Pour utiliser les dernières fonctionnalités javascript, utiliser esnext
.
{ "compilerOptions": { ..., "target": "esnext", "module": "esnext", "lib": ["esnext", "dom"], ... } }
Il est possible d'ajouter :
"noImplicitOverride": true,
Et mettre à jour moduleResolution
à node16
(node
/ node10
est déprécié) et propager les autres modifications :
"moduleResolution": "node16", "module": "node16",
et dans package.json
: “type”: “module”
.
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.
. ├── 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
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 } +];
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
.
error NG3001: Unsupported private class xxx. This class is visible to consumers via yyy → xxx, but is not exported from the top-level library entrypoint.
La création d'un composant depuis une librairie ne le met pas automatiquement à disposition des librairies / applications externes.
This class is visible to consumers via SomeModule -> SomeComponent, but is not exported from the top-level library entrypoint Archive du 07/02/2020 le 03/08/2021
Le code ci-dessous est la méthode réactive. La méthode template est déconseillée.
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>
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
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.
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; };
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.
<!-- 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.
export interface FoundationStrip { width: number; }
/* 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' } }; } }
<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>
/* 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; }
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(); } }
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
.
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); }); } }
<lib-meyerhof-form (changeEvent)="compute($event)"></lib-meyerhof-form>
export class MeyerhofCalcComponent { compute(newItem: MeyerhofForm): void { ... } }
error NG8002: Can't bind to 'formGroup' since it isn't a known property of 'div'
Dans xxx.module.ts
, il faut ajouter l'import en tête du fichier et l'ajouter également dans l'annotation NgModule
:
import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ imports: [..., ReactiveFormsModule] }) export class ...
Can't bind to 'formGroup' since it isn't a known property of 'form' Archive du 25/08/2016 le 03/08/2021
error NG6004: Present in the NgModule.exports of xxx but neither declared nor imported
Dans xxx.module.ts
, une classe dans la rubrique exports
doit être aussi dans la rubrique declarations
ou imports
.
import { XXXComponent } from '...'; @NgModule({ declarations: [XXXComponent], exports: [XXXComponent] }) export class ...
Source : Produce a `ts.Diagnostic` for an exported directive or pipe which was not declared or imported by the NgModule in question. Archive du 15/02/2021 le 03/08/2021
Autre message d'erreur possible:
error NG8001: 'xxx' is not a known element: 1. If 'xxx' is an Angular component, then verify that it is part of this module. 2. If 'xxx' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.
Il faut respecter les mêmes contraintes que ci-dessus et aussi ajouter la déclaration dans le fichier public-api.ts
.
export * from '...';
TypeError: control.registerOnChange is not a function
Dans le code HTML, les balises contenant formControlName
doivent être encapsulées dans des balises <form [formGroup]="...">
. Par exemple:
<form [formGroup]="meyerhorForm"> <label for="width">Width: </label> <input id="width" type="text" formControlName="width" /> </form>
What causes the "control.registerOnChange is not a function" error Archive du 04/01/2017 le 03/08/2021
NullInjectorError: No provider for FormBuilder!
Cela se produit lors des tests.
Il faut ajouter ReactiveFormsModule
dans les imports de xxx.spec.ts
.
beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ReactiveFormsModule], declarations: [...] }).compileComponents(); });
Nullinjectorerror: no provider for formbuilder (I'm importing ReactiveFormsModule) Archive du 04/03/2019 le 05/08/2021
No value accessor for form control with name
Ou encore ERROR Error: No value accessor for form control with unspecified name attribute
.
Peut se déclencher lors des tests et pas en production.
Dans la page HTML, il faut ajouter ngDefaultControl
aux éléments possédant un formControlName
et n'étant pas de type input
(par exemple, un composant Angular affichant un autre formulaire).
What is ngDefaultControl in Angular? Archive du 28/09/2017 le 06/08/2021
Les options sont dans les fichiers tsconfig.json
, dans le champ compilerOptions
.
{ "compilerOptions": { ... }, }
Intro to the TSConfig Reference Archive du v4.1 le 09/10/2021
Pour autoriser l'import depuis des fichiers json, il faut ajouter :
"resolveJsonModule": true
Pour autoriser l'import depuis des fichiers javascript, il faut ajouter :
"allowJs": true
Pour convertir des imports javascript en des modules, il faut ajouter :
"allowSyntheticDefaultImports": true
Pas besoin d'utiliser esModuleInterop
si ce n'est pas nécessaire.
Après de nombreux tests, je n'ai pas réussi à avoir une application avec un module et de la compiler, en même temps, en version de développement et de production. C'est possible de compiler les deux mais chaque compilation va remplacer la précédent.
Les peerDependencies
sont les dépendances des librairies.
Si le projet final utilise les mêmes dépendances, il va y avoir conflit sur la localisation des node_modules
à utiliser.
ERROR: src/lib/data/geotechnical/meyerhof/meyerhof-form.component.ts:49:13 - error TS2345: Argument of type 'MonoTypeOperatorFunction<any>' is not assignable to parameter of type 'OperatorFunction<any, any>'. Types of parameters 'source' and 'source' are incompatible. Type 'import("G:/Github/jessica/src/angular/node_modules/rxjs/internal/Observable").Observable<any>' is not assignable to type 'import("G:/Github/jessica/src/angular/projects/jessica/node_modules/rxjs/dist/types/internal/Observable").Observable<any>'. The types of 'operator.call' are incompatible between these types. Type '(subscriber: import("G:/Github/jessica/src/angular/node_modules/rxjs/internal/Subscriber").Subscriber<any>, source: any) => import("G:/Github/jessica/src/angular/node_modules/rxjs/internal/types").TeardownLogic' is not assignable to type '(subscriber: import("G:/Github/jessica/src/angular/projects/jessica/node_modules/rxjs/dist/types/internal/Subscriber").Subscriber<any>, source: any) => import("G:/Github/jessica/src/angular/projects/jessica/node_modules/rxjs/dist/types/internal/types").TeardownLogic'. Types of parameters 'subscriber' and 'subscriber' are incompatible. Type 'Subscriber<any>' is missing the following properties from type 'Subscriber<any>': syncErrorValue, syncErrorThrown, syncErrorThrowable, _unsubscribeAndRecycle, and 2 more.
et Angular détecter une incompatibilité qui n'en ai pas une, même si les deux modules ont la même version.
Il faut donc configurer le fichier tsconfig.app.json
.
{ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [], "paths": { // Dépendances du projet et des librairies "@angular/*": ["./projects/app-main/node_modules/@angular/*"], "@ngx-translate/*": ["./projects/app-main/node_modules/@ngx-translate/*"], "cldr": ["./projects/app-main/node_modules/cldrjs/dist/cldr"], "cldr/*": ["./projects/app-main/node_modules/cldrjs/dist/cldr/*"], "globalize/*": [ "./projects/app-main/node_modules/globalize/dist/globalize/*" ], // Librairies "jessica": ["./projects/jessica/dist"], "toolbox": ["./projects/toolbox/dist"] } }, ... }
Il doit bien y avoir un moyen d'éviter ça mais je n'ai pas trouvé.
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" ]