=====Création=====
====Documentation====
Création d'un projet de base : voir le [[https://angular.io/tutorial|tutorial de Angular]] {{ :lang:angular:projet:angular9-tutorial.zip |Archive du v9.1.6 le 08/05/2020}}
Convertir un projet Angular en PWA : voir la page [[https://angular.io/guide/service-worker-getting-started|PWA de Angular]] {{ :lang:angular:projet:angular_-_getting_started_with_service_workers_2020-05-08_7_14_25_pm_.html |Archive du v9.1.6 le 08/05/2020}}
Tutorial débutant mais clair : [[https://www.ganatan.com/|Comment créer une application web avec Angular]] {{ :lang:angular:projet:ganatan-v11-210321.7z |Archive v11 21/03/21}}
Formation complète [[https://guide-angular.wishtack.io/angular/|Le guide Angular - Marmicode]] {{ :lang:angular:projet:marmicode.tar.xz |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 [[https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/workspace/files/package.json.template|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
[[https://stackoverflow.com/questions/67433893/unable-to-resolve-dependency-tree-error-for-creating-new-angular-project|unable to resolve dependency tree error for creating new angular project]] {{ :lang:angular:projet:web_-_unable_to_resolve_dependency_tree_error_for_creating_new_angular_project_-_stack_overflow_2021-07-14_22_55_22_.html |Archive du 07/05/2021 le 14/07/2021}}
====Configuration====
Elle se fait avec le fichier ''tsconfig.json''. ([[https://www.typescriptlang.org/tsconfig|TSConfig Reference]] {{ :lang:angular:projet:typescript_tsconfig_reference_-_docs_on_every_tsconfig_option_14_01_2024_23_58_59_.html |Archive v5.0 le 15/01/2024}})
* Rubrique ''compilerOptions''
Cette rubrique contient les options pour le compilateur tsc. [[https://www.typescriptlang.org/docs/handbook/compiler-options.html|tsc CLI Options]] {{ :lang:angular:projet:typescript_documentation_-_tsc_cli_options_10_01_2024_05_36_09_.html |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"''.
====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
[[https://www.samarpaninfotech.com/blog/how-to-create-library-in-angular-tutorial/|How to Create Library in Angular Tutorial]] {{ :lang:angular:projet:how_to_create_library_using_angular_9_step-by-step_tutorial_2021-07-25_15_08_45_.html |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
[[https://angular.io/guide/file-structure|Workspace and project file structure]] {{ :lang:angular:projet:angular_-_workspace_and_project_file_structure_2021-07-31_09_06_47_.html |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''.
* ''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.
[[https://stackoverflow.com/questions/60121962/this-class-is-visible-to-consumers-via-somemodule-somecomponent-but-is-not-e|This class is visible to consumers via SomeModule -> SomeComponent, but is not exported from the top-level library entrypoint]] {{ :lang:angular:projet:angular_-_this_class_is_visible_to_consumers_via_somemodule_-_somecomponent_but_is_not_exported_from_the_top-level_library_entrypoint_-_stack_overflow_2021-08-03_18_38_59_.html |Archive du 07/02/2020 le 03/08/2021}}
====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 ''()''.
* Code Angular
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();
}
}
[[https://angular.io/guide/rx-library|The RxJS library]] {{ :lang:angular:projet:angular_-_the_rxjs_library_2021-07-28_21_31_12_.html |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
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é===
[[https://stackoverflow.com/questions/43270564/dividing-a-form-into-multiple-components-with-validation|Dividing a form into multiple components with validation]] (model driven) {{ :lang:angular:projet:angular_-_dividing_a_form_into_multiple_components_with_validation_-_stack_overflow_2021-08-01_14_32_37_.html |Archive du 07/04/2017 le 01/08/2021}} {{ :lang:angular:projet:angular-hzq5vy.zip |Archive du projet}}
[[https://coryrylan.com/blog/angular-custom-form-controls-with-reactive-forms-and-ngmodel|Angular Custom Form Controls with Reactive Forms and NgModel]] {{ :lang:angular:projet:angular_custom_form_controls_with_reactive_forms_and_ngmodel_-_angular_12_11_2021-08-01_14_37_40_.html |Archive du 19/10/2016 le 01/08/2021}} {{ :lang:angular:projet:angular-szsw3k.zip |Archive du projet}}
[[https://angular.io/guide/reactive-forms|Angular - Reactive forms]] {{ :lang:angular:projet:angular_-_reactive_forms_2021-08-01_14_50_54_.html |Archive du 12.1.5 le 01/08/2021}} {{ :lang:angular:projet:anovqbvjjgv.angular.zip |Archive du projet}}
[[https://blog.angular-university.io/introduction-to-angular-2-forms-template-driven-vs-model-driven/|Angular Forms Guide - Template Driven and Reactive Forms]] {{ :lang:angular:projet:angular_forms_guide_template_driven_and_reactive_forms_2021-08-01_14_52_40_.html |Archive du 26/01/2021 le 01/08/2021}}
[[https://angular.io/guide/form-validation|Validating form input]] {{ :lang:angular:projet:angular_-_validating_form_input_2021-08-01_14_54_43_.html |Archive du 12.1.5 le 01/08/2021}}
[[https://indepth.dev/posts/1245/angular-nested-reactive-forms-using-controlvalueaccessors-cvas|Angular: Nested Reactive Forms Using ControlValueAccessors(CVAs)]] {{ :lang:angular:projet:angular_nested_reactive_forms_using_controlvalueaccessors_cvas_-_angular_indepth_2021-08-01_14_55_22_.html |Archive du 09/01/2019 le 01/08/2021}} {{ :lang:angular:projet:angular-nested-forms-cva.zip |Archive du projet}}
[[https://indepth.dev/posts/1055/never-again-be-confused-when-implementing-controlvalueaccessor-in-angular-forms|Never again be confused when implementing ControlValueAccessor in Angular forms]] {{ :lang:angular:projet:never_again_be_confused_when_implementing_controlvalueaccessor_in_angular_forms_-_angular_indepth_2021-08-01_23_38_14_.html |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
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'
}
};
}
}
* Formulaire de base du parent
Nested FormGroup
Form Values: {{ form.valid }} {{ form.value | json }}
/* 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();
}
}
===Transfert de données par évènement===
[[https://angular.io/guide/inputs-outputs|Sharing data between child and parent directives and components]] {{ :lang:angular:projet:angular_-_sharing_data_between_child_and_parent_directives_and_components_2021-08-01_20_45_06_.html |Archive du 12.1.5 le 01/08/2021}} {{ :lang:angular:projet:xerqnqgmgdm.angular.zip |Archive du projet}}
[[https://stackoverflow.com/questions/58883905/angular-8-send-form-data-from-child-output-to-parent-with-reactive-forms|Angular 8: Send Form Data from Child Output to Parent with Reactive Forms]] {{ :lang:angular:projet:typescript_-_angular_8_send_form_data_from_child_output_to_parent_with_reactive_forms_-_stack_overflow_2021-08-01_20_47_59_.html |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();
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);
});
}
}
export class MeyerhofCalcComponent {
compute(newItem: MeyerhofForm): void {
...
}
}
===Erreurs===
* ''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 ...
[[https://stackoverflow.com/questions/39152071/cant-bind-to-formgroup-since-it-isnt-a-known-property-of-form/39152110|Can't bind to 'formGroup' since it isn't a known property of 'form']] {{ :lang:angular:projet:angular_-_can_t_bind_to_formgroup_since_it_isn_t_a_known_property_of_form_-_stack_overflow_2021-08-03_18_52_09_.html |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 : [[https://github.com/angular/angular/blob/f0c5ba08f63c60f7542dfd3592c4cfd42bd579bc/packages/compiler-cli/src/ngtsc/scope/src/local.ts#L604|Produce a `ts.Diagnostic` for an exported directive or pipe which was not declared or imported by the NgModule in question.]] {{ :lang:angular:projet:angular_local.ts_at_f0c5ba08f63c60f7542dfd3592c4cfd42bd579bc_angular_angular_2021-08-03_19_10_25_.html |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 ''