From 04a7f46b7244661d2dd9962b73490baa0b98a9f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Fri, 20 May 2022 08:14:08 +0200
Subject: [PATCH] refactor: use DefinedBoolean to manage form validity flag

refs #544
---
 .../field-set/field-set.component.ts          | 15 +++---
 .../fieldset-container.component.ts           | 15 +++---
 .../calculator.component.ts                   | 24 ++++++----
 .../generic-input/generic-input.component.ts  | 32 +++++++------
 .../pab-table/pab-table.component.ts          | 11 +++--
 .../pb-schema/pb-schema.component.ts          | 15 +++---
 src/app/definedvalue/definedboolean.ts        |  7 +++
 src/app/definedvalue/definedvalue.ts          | 46 +++++++++++++++++++
 8 files changed, 115 insertions(+), 50 deletions(-)
 create mode 100644 src/app/definedvalue/definedboolean.ts
 create mode 100644 src/app/definedvalue/definedvalue.ts

diff --git a/src/app/components/field-set/field-set.component.ts b/src/app/components/field-set/field-set.component.ts
index 584f42efa..36d2701bf 100644
--- a/src/app/components/field-set/field-set.component.ts
+++ b/src/app/components/field-set/field-set.component.ts
@@ -16,6 +16,7 @@ import { I18nService } from "../../services/internationalisation.service";
 import { sprintf } from "sprintf-js";
 
 import { capitalize } from "jalhyd";
+import { DefinedBoolean } from "app/definedvalue/definedboolean";
 
 @Component({
     selector: "field-set",
@@ -51,7 +52,7 @@ export class FieldSetComponent implements DoCheck {
     }
 
     public get isValid() {
-        return this._isValid;
+        return this._isValid.value;
     }
 
     /** flag d'affichage des boutons ajouter, supprimer, monter, descendre */
@@ -133,7 +134,7 @@ export class FieldSetComponent implements DoCheck {
     /**
      * flag de validité de la saisie
      */
-    private _isValid = false;
+    private _isValid: DefinedBoolean;
 
     /**
      * événement de changement d'état d'un radio
@@ -149,7 +150,9 @@ export class FieldSetComponent implements DoCheck {
         private notifService: NotificationsService,
         private i18nService: I18nService,
         private appSetupService: ApplicationSetupService
-    ) { }
+    ) {
+        this._isValid = new DefinedBoolean();
+    }
 
     public hasRadioFix(): boolean {
         if (this._fieldSet.hasInputs) {
@@ -264,11 +267,9 @@ export class FieldSetComponent implements DoCheck {
     }
 
     private updateValidity() {
-        const oldValidity = this._isValid;
-
         // global validity
-        this._isValid = this.computeValidity();
-        if (this._isValid !== oldValidity) {
+        this._isValid.value = this.computeValidity();
+        if (this._isValid.changed) {
             this.validChange.emit();
         }
     }
diff --git a/src/app/components/fieldset-container/fieldset-container.component.ts b/src/app/components/fieldset-container/fieldset-container.component.ts
index 4d60303d1..74fac2031 100644
--- a/src/app/components/fieldset-container/fieldset-container.component.ts
+++ b/src/app/components/fieldset-container/fieldset-container.component.ts
@@ -6,6 +6,7 @@ import { FieldSet } from "../../formulaire/elements/fieldset";
 import { FormulaireDefinition } from "../../formulaire/definition/form-definition";
 import { I18nService } from "../../services/internationalisation.service";
 import { ApplicationSetupService } from "../../services/app-setup.service";
+import { DefinedBoolean } from "app/definedvalue/definedboolean";
 
 @Component({
     selector: "fieldset-container",
@@ -27,7 +28,7 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit {
     }
 
     public get isValid() {
-        return this._isValid;
+        return this._isValid.value;
     }
     @Input()
     private _container: FieldsetContainer;
@@ -41,7 +42,7 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit {
     /**
      * flag de validité des FieldSet enfants
      */
-    private _isValid = false;
+    private _isValid: DefinedBoolean;
 
     /**
      * événément de changement d'état d'un radio
@@ -68,7 +69,9 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit {
     public constructor(
         private i18nService: I18nService,
         private appSetupService: ApplicationSetupService
-    ) {}
+    ) {
+        this._isValid = new DefinedBoolean();
+    }
 
     /**
      * Ajoute un nouveau sous-nub (Structure, PabCloisons, YAXN… selon le cas)
@@ -140,11 +143,9 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit {
     }
 
     private updateValidity() {
-        const oldValidity = this._isValid;
-
         // global validity
-        this._isValid = this.computeValidity();
-        if (this._isValid !== oldValidity) {
+        this._isValid.value = this.computeValidity();
+        if (this._isValid.changed) {
             this.validChange.emit();
         }
     }
diff --git a/src/app/components/generic-calculator/calculator.component.ts b/src/app/components/generic-calculator/calculator.component.ts
index 859463c39..31aa8ae8e 100644
--- a/src/app/components/generic-calculator/calculator.component.ts
+++ b/src/app/components/generic-calculator/calculator.component.ts
@@ -61,6 +61,7 @@ import { sprintf } from "sprintf-js";
 
 import * as XLSX from "xlsx";
 import { ServiceFactory } from "app/services/service-factory";
+import { DefinedBoolean } from "app/definedvalue/definedboolean";
 
 @Component({
     selector: "hydrocalc",
@@ -110,7 +111,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
      * La validité de l'UI comprend la forme (pas de chaîne alpha dans les champs numériques, etc..).
      * La validité formulaire comprend le domaine de définition des valeurs saisies.
      */
-    private _isUIValid = false;
+    private _isUIValid: DefinedBoolean;
 
     /**
      * flag disabled du bouton "calculer"
@@ -158,6 +159,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
         private formulaireService: FormulaireService,
         private matomoTracker: MatomoTracker
     ) {
+        this._isUIValid = new DefinedBoolean();
         // hotkeys listeners
         this.hotkeysService.add(new Hotkey("alt+w", AppComponent.onHotkey(this.closeCalculator, this)));
         this.hotkeysService.add(new Hotkey("alt+d", AppComponent.onHotkey(this.cloneCalculator, this)));
@@ -331,7 +333,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
      * the UI validity state)
      */
     ngDoCheck() {
-        this.isCalculateDisabled = !this._isUIValid;
+        this.isCalculateDisabled = !this._isUIValid.value;
     }
 
     ngOnDestroy() {
@@ -473,12 +475,12 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
      * calcul de la validité globale de la vue
      */
     private updateUIValidity() {
-        this._isUIValid = false;
+        let res = false;
         if (!this._formulaire.calculateDisabled) {
             // all fieldsets must be valid
-            this._isUIValid = true;
+            res = true;
             if (this._fieldsetComponents !== undefined) {
-                this._isUIValid = this._isUIValid && this._fieldsetComponents.reduce(
+                res = res && this._fieldsetComponents.reduce(
                     // callback
                     (
                         // accumulator (valeur précédente du résultat)
@@ -497,7 +499,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
             }
             // all fieldset containers must be valid
             if (this._fieldsetContainerComponents !== undefined) {
-                this._isUIValid = this._isUIValid && this._fieldsetContainerComponents.reduce<boolean>(
+                res = res && this._fieldsetContainerComponents.reduce<boolean>(
                     // callback
                     (
                         // accumulator (valeur précédente du résultat)
@@ -516,19 +518,23 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
             }
             // special components must be valid
             if (this._pabTableComponent !== undefined) {
-                this._isUIValid = this._isUIValid && this._pabTableComponent.isValid;
+                res = res && this._pabTableComponent.isValid;
             }
             if (this._pbSchemaComponent !== undefined) {
-                this._isUIValid = this._isUIValid && this._pbSchemaComponent.isValid;
+                res = res && this._pbSchemaComponent.isValid;
             }
             if (this._formulaire.currentNub.calcType === CalculatorType.PreBarrage) {
                 const form: FormulairePrebarrage = this._formulaire as FormulairePrebarrage;
-                this._isUIValid = this._isUIValid && form.checkParameters().length === 0;
+                res = res && form.checkParameters().length === 0;
             }
         }
 
+        this._isUIValid.value = res;
+
         // update prébarrage schema validity
+        if (this._isUIValid.changed) {
             this._pbSchemaComponent.updateItemsValidity();
+        }
     }
 
     public getElementStyleDisplay(id: string) {
diff --git a/src/app/components/generic-input/generic-input.component.ts b/src/app/components/generic-input/generic-input.component.ts
index 444127f7f..97d1f801d 100644
--- a/src/app/components/generic-input/generic-input.component.ts
+++ b/src/app/components/generic-input/generic-input.component.ts
@@ -5,6 +5,7 @@ import { FormulaireDefinition } from "../../formulaire/definition/form-definitio
 import { NgParameter } from "../../formulaire/elements/ngparam";
 import { I18nService } from "../../services/internationalisation.service";
 import { ApplicationSetupService } from "../../services/app-setup.service";
+import { DefinedBoolean } from "app/definedvalue/definedboolean";
 
 /**
  * classe de gestion générique d'un champ de saisie avec titre, validation et message d'erreur
@@ -79,6 +80,11 @@ export abstract class GenericInputComponentDirective implements OnChanges {
      */
     private _isValidModel = false;
 
+    /**
+     * flag de validité globale
+     */
+    private _isValid: DefinedBoolean;
+
     /**
      * message d'erreur UI
      */
@@ -96,7 +102,9 @@ export abstract class GenericInputComponentDirective implements OnChanges {
         private cdRef: ChangeDetectorRef,
         protected intlService: I18nService,
         protected appSetupService: ApplicationSetupService
-    ) { }
+    ) {
+        this._isValid = new DefinedBoolean();
+    }
 
     public get isDisabled(): boolean {
         if (this._model instanceof NgParameter) {
@@ -107,10 +115,13 @@ export abstract class GenericInputComponentDirective implements OnChanges {
     }
 
     /**
-     * événement de changement de la validité de la saisie
+     * modification et émission d'un événement de changement de la validité
      */
-    private emitValidChanged() {
-        this.change.emit({ "action": "valid", "value": this.isValid });
+    private setAndEmitValid() {
+        this._isValid.value = this._isValidUI && this._isValidModel;
+        if (this._isValid.changed) {
+            this.change.emit({ "action": "valid", "value": this._isValid.value });
+        }
     }
 
     /**
@@ -128,15 +139,12 @@ export abstract class GenericInputComponentDirective implements OnChanges {
      * calcul de la validité globale du composant (UI+modèle)
      */
     public get isValid() {
-        return this._isValidUI && this._isValidModel;
+        return this._isValid.value;
     }
 
     protected setUIValid(b: boolean) {
-        const old = this.isValid;
         this._isValidUI = b;
-        if (this.isValid !== old) {
-            this.emitValidChanged();
-        }
+        this.setAndEmitValid();
     }
 
     protected validateUI() {
@@ -148,11 +156,9 @@ export abstract class GenericInputComponentDirective implements OnChanges {
     }
 
     protected setModelValid(b: boolean) {
-        const old = this.isValid;
         this._isValidModel = b;
-        if (this.isValid !== old) {
-            this.emitValidChanged();
-        }
+        this.setAndEmitValid();
+
         // répercussion des erreurs sur le Form angular, pour faire apparaître/disparaître les mat-error
         if (b) {
             this.inputField.control.setErrors(null);
diff --git a/src/app/components/pab-table/pab-table.component.ts b/src/app/components/pab-table/pab-table.component.ts
index 597d28f3a..7d95042fb 100644
--- a/src/app/components/pab-table/pab-table.component.ts
+++ b/src/app/components/pab-table/pab-table.component.ts
@@ -28,6 +28,7 @@ import { PabTable } from "../../formulaire/elements/pab-table";
 import { DialogEditPabComponent } from "../dialog-edit-pab/dialog-edit-pab.component";
 import { AppComponent } from "../../app.component";
 import { NgParameter, ParamRadioConfig } from "../../formulaire/elements/ngparam";
+import { DefinedBoolean } from "app/definedvalue/definedboolean";
 
 /**
  * The big editable data grid for calculator type "Pab" (component)
@@ -45,7 +46,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni
     private pabTable: PabTable;
 
     /** flag de validité des FieldSet enfants */
-    private _isValid = false;
+    private _isValid: DefinedBoolean;
 
     /** événément de changement de validité */
     @Output()
@@ -84,6 +85,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni
         private notifService: NotificationsService
     ) {
         this.selectedItems = [];
+        this._isValid = new DefinedBoolean();
     }
 
     /** update vary value from pab fish ladder and unable compute Button */
@@ -98,7 +100,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni
 
     /** Global Pab validity */
     public get isValid() {
-        return this._isValid;
+        return this._isValid.value;
     }
 
     /** returns true if the cell has an underlying model (ie. is editable) */
@@ -1452,9 +1454,8 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni
     }
 
     private updateValidity() {
-        const oldValidity = this._isValid;
-        this._isValid = this.computeValidity();
-        if (this._isValid !== oldValidity) {
+        this._isValid.value = this.computeValidity();
+        if (this._isValid.changed) {
             this.validChange.emit();
         }
     }
diff --git a/src/app/components/pb-schema/pb-schema.component.ts b/src/app/components/pb-schema/pb-schema.component.ts
index 425b1ba88..ae157c56f 100644
--- a/src/app/components/pb-schema/pb-schema.component.ts
+++ b/src/app/components/pb-schema/pb-schema.component.ts
@@ -22,6 +22,7 @@ import { AppComponent } from "../../app.component";
 import { fv } from "app/util";
 import { FormulaireNode } from "app/formulaire/elements/formulaire-node";
 import { ServiceFactory } from "app/services/service-factory";
+import { DefinedBoolean } from "app/definedvalue/definedboolean";
 
 /**
  * The interactive schema for calculator type "PreBarrage" (component)
@@ -45,7 +46,7 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni
     private nativeElement: any;
 
     /** flag de validité du composant */
-    private _isValid = false;
+    private _isValid: DefinedBoolean;
 
     private upstreamId = "amont";
 
@@ -75,6 +76,7 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni
         private newPbCloisonDialog: MatDialog
     ) {
         this.hotkeysService.add(new Hotkey("del", AppComponent.onHotkey(this.removeOnHotkey, this)));
+        this._isValid = new DefinedBoolean();
     }
 
     /** tracks the fullscreen state */
@@ -334,7 +336,7 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni
 
     /** Global Pb validity */
     public get isValid() {
-        return this._isValid;
+        return this._isValid.value;
     }
 
     /** used for a cosmetics CSS trick only (mat-card-header right margin) */
@@ -641,16 +643,11 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni
      * Computes the global Pab validity : validity of every cell of every row
      */
     private updateValidity() {
-        const oldValidity = this._isValid;
-
         // check that at least 1 basin is present and a route from river
         // upstream to river downstream exists (2nd check includes 1st)
-        this._isValid = (
-            this.model.hasUpDownConnection()
-            && ! this.model.hasBasinNotConnected()
-        );
+        this._isValid.value = this.model.hasUpDownConnection() && !this.model.hasBasinNotConnected();
 
-        if (this._isValid !== oldValidity) {
+        if (this._isValid.changed) {
             this.validChange.emit();
         }
     }
diff --git a/src/app/definedvalue/definedboolean.ts b/src/app/definedvalue/definedboolean.ts
new file mode 100644
index 000000000..2d8d9a3b8
--- /dev/null
+++ b/src/app/definedvalue/definedboolean.ts
@@ -0,0 +1,7 @@
+import { DefinedValue } from "./definedvalue";
+
+/**
+ * boolean value with initialised, changed, defined states
+ */
+export class DefinedBoolean extends DefinedValue<boolean> {
+}
diff --git a/src/app/definedvalue/definedvalue.ts b/src/app/definedvalue/definedvalue.ts
new file mode 100644
index 000000000..1e72754b0
--- /dev/null
+++ b/src/app/definedvalue/definedvalue.ts
@@ -0,0 +1,46 @@
+/**
+ * value management with initialised, changed and defined states
+ */
+export abstract class DefinedValue<T> {
+    private _initialised: boolean;
+
+    private _value: T;
+
+    private _changed: boolean;
+
+    constructor() {
+        this._initialised = false;
+        this._changed = false;
+    }
+
+    /**
+     * @returns true if setter has been called at least once
+     */
+    public get initialised(): boolean {
+        return this._initialised;
+    }
+
+    /**
+     * @returns true if value is not undefined
+     */
+    public get defined(): boolean {
+        return this._value !== undefined;
+    }
+
+    /**
+     * @returns true if value has been modified by last call to setter
+     */
+    public get changed(): boolean {
+        return this._changed;
+    }
+
+    public get value(): T {
+        return this._value;
+    }
+
+    public set value(v: T) {
+        this._changed = this._value !== v;
+        this._initialised = true;
+        this._value = v;
+    }
+}
-- 
GitLab