From 655def7e4cf8e0fcf536bd0e6458b8cb8746e8b3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Thu, 13 Oct 2022 10:54:20 +0200
Subject: [PATCH 01/19] refactor: FieldSet class: rename getPropValue to
 getNubPropValue, setPropValue to setNubPropValue

refs #483
---
 .../fieldset-container.component.ts           |  4 ++--
 src/app/formulaire/elements/fieldset.ts       | 22 +++++++++++++------
 2 files changed, 17 insertions(+), 9 deletions(-)

diff --git a/src/app/components/fieldset-container/fieldset-container.component.ts b/src/app/components/fieldset-container/fieldset-container.component.ts
index aa9c6639d..f612f362a 100644
--- a/src/app/components/fieldset-container/fieldset-container.component.ts
+++ b/src/app/components/fieldset-container/fieldset-container.component.ts
@@ -89,8 +89,8 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit {
             const prms = after.backupParameters();
             // replace in-place to change properties (overkill)
             // @WTF why only those two ?
-            newFs.setPropValue("structureType", after.properties.getPropValue("structureType"));
-            newFs.setPropValue("loiDebit", after.properties.getPropValue("loiDebit"));
+            newFs.setNubPropValue("structureType", after.properties.getPropValue("structureType"));
+            newFs.setNubPropValue("loiDebit", after.properties.getPropValue("loiDebit"));
 
             // au cas où un des paramètres du fieldset source est en mode calcul,
             // on met le paramètre copié en mode fixé (nghyd#567)
diff --git a/src/app/formulaire/elements/fieldset.ts b/src/app/formulaire/elements/fieldset.ts
index b73ff379a..3b10ef25e 100644
--- a/src/app/formulaire/elements/fieldset.ts
+++ b/src/app/formulaire/elements/fieldset.ts
@@ -140,9 +140,11 @@ export class FieldSet extends FormulaireElement implements Observer {
     }
 
     /**
+     * get associated nub property value
+     * @param key property name
      * @param inSection if true, will look for the required property in the Nub's section (children[0])
      */
-    private getPropValue(key: string, inSection: boolean = false): any {
+    private getNubPropValue(key: string, inSection: boolean = false): any {
         if (inSection) {
             return this.sectionProperties.getPropValue(key);
         } else {
@@ -150,7 +152,13 @@ export class FieldSet extends FormulaireElement implements Observer {
         }
     }
 
-    public setPropValue(key: string, val: any): boolean {
+    /**
+     * assign associated nub property
+     * @param key nub property name
+     * @param val value to assign with
+     * @returns true if property value has changed
+     */
+    public setNubPropValue(key: string, val: any): boolean {
         return this.properties.setPropValue(key, val, this);
     }
 
@@ -259,7 +267,7 @@ export class FieldSet extends FormulaireElement implements Observer {
     private setSelectValueFromProperty(selectId: string, inSection: boolean = false) {
         const selectField: SelectField = this.getFormulaireNodeById(selectId) as SelectField;
         if (selectField) {
-            let propVal: any = this.getPropValue(selectField.associatedProperty, inSection);
+            let propVal: any = this.getNubPropValue(selectField.associatedProperty, inSection);
             if (propVal === undefined) {
                 propVal = ""; // clodo bullet-proof loading
             }
@@ -288,7 +296,7 @@ export class FieldSet extends FormulaireElement implements Observer {
         const ct: string = json["calcType"];
         const currentCt = this.properties.getPropValue("calcType");
         const calc_type: CalculatorType = currentCt ? currentCt : (ct ? CalculatorType[ct] : this.parentForm.calculatorType);
-        this.setPropValue("calcType", calc_type);
+        this.setNubPropValue("calcType", calc_type);
 
         // parse fields once, so that SelectField elements are present
         // when setting default properties below
@@ -313,7 +321,7 @@ export class FieldSet extends FormulaireElement implements Observer {
                         if (enumClass) {
                             formalValue = enumClass[defaultValue];
                         }
-                        this.setPropValue(prop, formalValue);
+                        this.setNubPropValue(prop, formalValue);
                     }
                 }
             }
@@ -401,9 +409,9 @@ export class FieldSet extends FormulaireElement implements Observer {
                                         const prop = (fe as SelectField).associatedProperty;
                                         // for multiple select
                                         if (Array.isArray(data.value)) {
-                                            this.setPropValue(prop, data.value.map((v: any) => v.value));
+                                            this.setNubPropValue(prop, data.value.map((v: any) => v.value));
                                         } else {
-                                            this.setPropValue(prop, data.value.value);
+                                            this.setNubPropValue(prop, data.value.value);
                                         }
                                     }
                                 }
-- 
GitLab


From 557c397820c5c0bfd7d7258b9ec0935f0d319525 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Thu, 13 Oct 2022 10:58:19 +0200
Subject: [PATCH 02/19] refactor: SelectField: rename getSelectedEntryFromValue
 to getEntryFromValue

refs #483
---
 src/app/formulaire/elements/fieldset.ts     | 2 +-
 src/app/formulaire/elements/select-field.ts | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/app/formulaire/elements/fieldset.ts b/src/app/formulaire/elements/fieldset.ts
index 3b10ef25e..65200665a 100644
--- a/src/app/formulaire/elements/fieldset.ts
+++ b/src/app/formulaire/elements/fieldset.ts
@@ -271,7 +271,7 @@ export class FieldSet extends FormulaireElement implements Observer {
             if (propVal === undefined) {
                 propVal = ""; // clodo bullet-proof loading
             }
-            const selectElement = selectField.getSelectedEntryFromValue(propVal);
+            const selectElement = selectField.getEntryFromValue(propVal);
             try {
                 selectField.setValue(selectElement);
             } catch (e) {
diff --git a/src/app/formulaire/elements/select-field.ts b/src/app/formulaire/elements/select-field.ts
index b0833fb61..ce08ddefd 100644
--- a/src/app/formulaire/elements/select-field.ts
+++ b/src/app/formulaire/elements/select-field.ts
@@ -98,7 +98,7 @@ export class SelectField extends Field {
      */
     protected afterParseConfig() { }
 
-    public getSelectedEntryFromValue(val: any): SelectEntry {
+    public getEntryFromValue(val: any): SelectEntry {
         for (const se of this._entries) {
             if (se.value === val) {
                 return se;
-- 
GitLab


From 4db193e86c5c9f6731768cc3ad86b3a9b35dbff9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Wed, 19 Oct 2022 10:05:04 +0200
Subject: [PATCH 03/19] update jalhyd_branch to
 328-fusionner-les-select-avec-source-et-les-select_custom

refs #483
---
 jalhyd_branch | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/jalhyd_branch b/jalhyd_branch
index 1f7391f92..d9cf99b69 100644
--- a/jalhyd_branch
+++ b/jalhyd_branch
@@ -1 +1 @@
-master
+328-fusionner-les-select-avec-source-et-les-select_custom
-- 
GitLab


From 566b9c54f85b1fa74cbe7e77ad0336841498d647 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Mon, 17 Oct 2022 13:12:19 +0200
Subject: [PATCH 04/19] refactor: merge SelectField and SelectFieldCustom
 classes, split select code into specific classes

refs #483
---
 src/app/calculators/bief/config.json          |   2 +-
 src/app/calculators/cloisons/config.json      |   6 +-
 src/app/calculators/courberemous/config.json  |   2 -
 src/app/calculators/dever/config.json         |   6 +-
 .../calculators/parallelstructure/config.json |   6 +-
 src/app/calculators/prebarrage/config.json    |  12 +-
 src/app/calculators/solveur/config.json       |  11 +-
 src/app/calculators/verificateur/config.json  |  10 +-
 .../field-set/field-set.component.ts          |   2 +-
 .../generic-select.component.html             |   2 +-
 .../select-field-line.component.ts            |   9 +-
 .../formulaire/definition/form-definition.ts  |   4 +-
 .../formulaire/definition/form-fixedvar.ts    |   7 +-
 .../definition/form-parallel-structures.ts    |   2 +-
 .../formulaire/definition/form-pb-cloison.ts  |   4 +-
 src/app/formulaire/definition/form-solveur.ts |   7 +-
 .../definition/form-verificateur.ts           |   5 +-
 src/app/formulaire/elements/fieldset.ts       |  27 +-
 .../elements/select-field-custom.ts           | 343 ------------------
 src/app/formulaire/elements/select-field.ts   | 244 -------------
 .../elements/{ => select}/select-entry.ts     |   0
 .../select/select-field-device-loi-debit.ts   |  42 +++
 .../select-field-device-structure-type.ts     |  32 ++
 .../select/select-field-downstream-basin.ts   |  53 +++
 .../elements/select/select-field-factory.ts   |  66 ++++
 .../elements/select/select-field-nub-prop.ts  |  38 ++
 .../select/select-field-remous-target.ts      |  34 ++
 .../select/select-field-searched-param.ts     |  63 ++++
 .../select-field-solver-targeted-result.ts    |  64 ++++
 .../select/select-field-solveur-target.ts     |  62 ++++
 .../select/select-field-species-list.ts       |  73 ++++
 .../select/select-field-target-pass.ts        |  62 ++++
 .../select/select-field-upstream-basin.ts     |  53 +++
 .../elements/select/select-field.ts           | 268 ++++++++++++++
 src/app/services/formulaire.service.ts        |   2 +-
 35 files changed, 947 insertions(+), 676 deletions(-)
 delete mode 100644 src/app/formulaire/elements/select-field-custom.ts
 delete mode 100644 src/app/formulaire/elements/select-field.ts
 rename src/app/formulaire/elements/{ => select}/select-entry.ts (100%)
 create mode 100644 src/app/formulaire/elements/select/select-field-device-loi-debit.ts
 create mode 100644 src/app/formulaire/elements/select/select-field-device-structure-type.ts
 create mode 100644 src/app/formulaire/elements/select/select-field-downstream-basin.ts
 create mode 100644 src/app/formulaire/elements/select/select-field-factory.ts
 create mode 100644 src/app/formulaire/elements/select/select-field-nub-prop.ts
 create mode 100644 src/app/formulaire/elements/select/select-field-remous-target.ts
 create mode 100644 src/app/formulaire/elements/select/select-field-searched-param.ts
 create mode 100644 src/app/formulaire/elements/select/select-field-solver-targeted-result.ts
 create mode 100644 src/app/formulaire/elements/select/select-field-solveur-target.ts
 create mode 100644 src/app/formulaire/elements/select/select-field-species-list.ts
 create mode 100644 src/app/formulaire/elements/select/select-field-target-pass.ts
 create mode 100644 src/app/formulaire/elements/select/select-field-upstream-basin.ts
 create mode 100644 src/app/formulaire/elements/select/select-field.ts

diff --git a/src/app/calculators/bief/config.json b/src/app/calculators/bief/config.json
index 95f2ca549..7434f2abc 100644
--- a/src/app/calculators/bief/config.json
+++ b/src/app/calculators/bief/config.json
@@ -72,4 +72,4 @@
         "selectIds": [ "select_section", "select_regime" ],
         "help": "hsl/cote_amont_aval.html"
     }
-]
\ No newline at end of file
+]
diff --git a/src/app/calculators/cloisons/config.json b/src/app/calculators/cloisons/config.json
index 82ccf331e..2423ef4bc 100644
--- a/src/app/calculators/cloisons/config.json
+++ b/src/app/calculators/cloisons/config.json
@@ -21,15 +21,11 @@
         "fields": [
             {
                 "id": "select_structure",
-                "type": "select",
-                "property": "structureType",
-                "source": "device_structure_type"
+                "type": "select"
             },
             {
                 "id": "select_loidebit",
                 "type": "select",
-                "property": "loiDebit",
-                "source": "device_loi_debit",
                 "help": {
                     "OrificeSubmerged": "structures/orifice_noye.html",
                     "WeirSubmergedLarinier": "structures/fente_noyee.html",
diff --git a/src/app/calculators/courberemous/config.json b/src/app/calculators/courberemous/config.json
index 272de8687..b66d709bb 100644
--- a/src/app/calculators/courberemous/config.json
+++ b/src/app/calculators/courberemous/config.json
@@ -72,8 +72,6 @@
             {
                 "id": "select_target",
                 "type": "select",
-                "property": "varCalc",
-                "source": "remous_target",
                 "help": {
                     "B": "hsl/section_parametree.html#largeur-au-miroir-surface-et-perimetre-mouille",
                     "P": "hsl/section_parametree.html#largeur-au-miroir-surface-et-perimetre-mouille",
diff --git a/src/app/calculators/dever/config.json b/src/app/calculators/dever/config.json
index ce6bea142..6b502f27a 100644
--- a/src/app/calculators/dever/config.json
+++ b/src/app/calculators/dever/config.json
@@ -19,15 +19,11 @@
         "fields": [
             {
                 "id": "select_structure",
-                "type": "select",
-                "property": "structureType",
-                "source": "device_structure_type"
+                "type": "select"
             },
             {
                 "id": "select_loidebit",
                 "type": "select",
-                "property": "loiDebit",
-                "source": "device_loi_debit",
                 "help": {
                     "WeirFree": "structures/seuil_denoye.html",
                     "TriangularWeirFree": "structures/dever_triang.html",
diff --git a/src/app/calculators/parallelstructure/config.json b/src/app/calculators/parallelstructure/config.json
index 26c3cf9a4..d265d9a66 100644
--- a/src/app/calculators/parallelstructure/config.json
+++ b/src/app/calculators/parallelstructure/config.json
@@ -18,15 +18,11 @@
         "fields": [
             {
                 "id": "select_structure",
-                "type": "select",
-                "property": "structureType",
-                "source": "device_structure_type"
+                "type": "select"
             },
             {
                 "id": "select_loidebit",
                 "type": "select",
-                "property": "loiDebit",
-                "source": "device_loi_debit",
                 "help": {
                     "KIVI": "structures/kivi.html",
                     "WeirVillemonte": "structures/kivi.html",
diff --git a/src/app/calculators/prebarrage/config.json b/src/app/calculators/prebarrage/config.json
index 7dada6df9..1b0431549 100644
--- a/src/app/calculators/prebarrage/config.json
+++ b/src/app/calculators/prebarrage/config.json
@@ -61,15 +61,11 @@
                 "fields": [
                     {
                         "id": "select_structure",
-                        "type": "select",
-                        "property": "structureType",
-                        "source": "device_structure_type"
+                        "type": "select"
                     },
                     {
                         "id": "select_loidebit",
                         "type": "select",
-                        "property": "loiDebit",
-                        "source": "device_loi_debit",
                         "help": {
                             "KIVI": "structures/kivi.html",
                             "WeirVillemonte": "structures/kivi.html",
@@ -171,13 +167,11 @@
                 "fields": [
                     {
                         "id": "select_upstream_basin",
-                        "type": "select_custom",
-                        "source": "upstream_basin"
+                        "type": "select"
                     },
                     {
                         "id": "select_downstream_basin",
-                        "type": "select_custom",
-                        "source": "downstream_basin"
+                        "type": "select"
                     }
                 ]
             },
diff --git a/src/app/calculators/solveur/config.json b/src/app/calculators/solveur/config.json
index 71a23177b..99d215264 100644
--- a/src/app/calculators/solveur/config.json
+++ b/src/app/calculators/solveur/config.json
@@ -5,15 +5,11 @@
         "fields": [
             {
                 "id": "select_target_nub",
-                "type": "select_custom",
-                "source": "solveur_target"
+                "type": "select"
             },
             {
                 "id": "select_target_result",
-                "type": "select",
-                "property": "targettedResult",
-                "source": "solveur_targetted_result",
-                "default": ""
+                "type": "select"
             },
             "Ytarget"
         ]
@@ -24,8 +20,7 @@
         "fields": [
             {
                 "id": "select_searched_param",
-                "type": "select_custom",
-                "source": "solveur_searched"
+                "type": "select"
             },
             "Xinit"
         ]
diff --git a/src/app/calculators/verificateur/config.json b/src/app/calculators/verificateur/config.json
index 438d2bf32..07f6f672c 100644
--- a/src/app/calculators/verificateur/config.json
+++ b/src/app/calculators/verificateur/config.json
@@ -5,9 +5,7 @@
         "fields": [
             {
                 "id": "select_target_pass",
-                "type": "select_custom",
-                "source": "verificateur_target",
-                "messageWhenEmpty": "INFO_VERIF_CREATE_PASS_FRIST"
+                "type": "select"
             }
         ]
     },
@@ -16,10 +14,8 @@
         "type": "fieldset",
         "fields": [
             {
-                "type": "select_custom",
-                "id": "select_species_list",
-                "source": "verificateur_species",
-                "multiple": true
+                "type": "select",
+                "id": "select_species_list"
             }
         ]
     },
diff --git a/src/app/components/field-set/field-set.component.ts b/src/app/components/field-set/field-set.component.ts
index 293fce6e2..448d40a7d 100644
--- a/src/app/components/field-set/field-set.component.ts
+++ b/src/app/components/field-set/field-set.component.ts
@@ -5,7 +5,7 @@ import { FieldSet } from "../../formulaire/elements/fieldset";
 import { ParamFieldLineComponent } from "../param-field-line/param-field-line.component";
 import { Field } from "../../formulaire/elements/field";
 import { InputField } from "../../formulaire/elements/input-field";
-import { SelectField } from "../../formulaire/elements/select-field";
+import { SelectField } from "../../formulaire/elements/select/select-field";
 import { FormulairePab } from "../../formulaire/definition/form-pab";
 import { SelectFieldLineComponent } from "../select-field-line/select-field-line.component";
 import { FieldsetContainer } from "../../formulaire/elements/fieldset-container";
diff --git a/src/app/components/generic-select/generic-select.component.html b/src/app/components/generic-select/generic-select.component.html
index 9c6880e13..7ce918d76 100644
--- a/src/app/components/generic-select/generic-select.component.html
+++ b/src/app/components/generic-select/generic-select.component.html
@@ -23,4 +23,4 @@
 </mat-form-field>
 <div *ngIf="messageWhenEmpty" class="message-when-empty">
     {{ messageWhenEmpty }}
-</div>
\ No newline at end of file
+</div>
diff --git a/src/app/components/select-field-line/select-field-line.component.ts b/src/app/components/select-field-line/select-field-line.component.ts
index eccc37a99..2b8cb3a03 100644
--- a/src/app/components/select-field-line/select-field-line.component.ts
+++ b/src/app/components/select-field-line/select-field-line.component.ts
@@ -1,10 +1,9 @@
 import { Component, Input, OnInit } from "@angular/core";
 
-import { SelectField } from "../../formulaire/elements/select-field";
-import { SelectEntry } from "../../formulaire/elements/select-entry";
+import { SelectField } from "../../formulaire/elements/select/select-field";
+import { SelectEntry } from "../../formulaire/elements/select/select-entry";
 import { I18nService } from "../../services/internationalisation.service";
 import { ApplicationSetupService } from "../../services/app-setup.service";
-import { SelectFieldCustom } from "../../formulaire/elements/select-field-custom";
 import { decodeHtml } from "../../util";
 
 @Component({
@@ -124,8 +123,6 @@ export class SelectFieldLineComponent implements OnInit {
 
     // called every time we navigate to the module
     ngOnInit(): void {
-        if (this._select instanceof SelectFieldCustom) {
-            this._select.updateEntries();
-        }
+        this._select.updateEntries();
     }
 }
diff --git a/src/app/formulaire/definition/form-definition.ts b/src/app/formulaire/definition/form-definition.ts
index 814db2e70..cc5837883 100644
--- a/src/app/formulaire/definition/form-definition.ts
+++ b/src/app/formulaire/definition/form-definition.ts
@@ -24,8 +24,8 @@ import { TopFormulaireElementIterator } from "../form-iterator/top-element-itera
 import { CalculatorResults } from "../../results/calculator-results";
 import { ServiceFactory } from "../../services/service-factory";
 import { PabTable } from "../elements/pab-table";
-import { SelectEntry } from "../elements/select-entry";
-import { SelectField } from "../elements/select-field";
+import { SelectEntry } from "../elements/select/select-entry";
+import { SelectField } from "../elements/select/select-field";
 
 /**
  * classe de base pour tous les formulaires
diff --git a/src/app/formulaire/definition/form-fixedvar.ts b/src/app/formulaire/definition/form-fixedvar.ts
index 575f7cb10..9289094a7 100644
--- a/src/app/formulaire/definition/form-fixedvar.ts
+++ b/src/app/formulaire/definition/form-fixedvar.ts
@@ -2,12 +2,11 @@ import { FormulaireDefinition } from "./form-definition";
 import { FixedResults } from "../../results/fixed-results";
 import { VarResults } from "../../results/var-results";
 import { ChartType } from "../../results/chart-type";
-import { CalculatorResults } from "../../results/calculator-results";
-import { ParamRadioConfig, NgParameter } from "../elements/ngparam";
+import { NgParameter } from "../elements/ngparam";
 import { FieldSet } from "../elements/fieldset";
 import { FormulaireNode } from "../elements/formulaire-node";
 
-import { SelectFieldCustom } from "../elements/select-field-custom";
+import { SelectField } from "../elements/select/select-field";
 import { Nub, IObservable, VariatedDetails } from "jalhyd";
 
 export class FormulaireFixedVar extends FormulaireDefinition {
@@ -95,7 +94,7 @@ export class FormulaireFixedVar extends FormulaireDefinition {
                 sel.addObserver(this);
                 if (firstNotif) {
                     // force 1st observation
-                    (sel as SelectFieldCustom).notifyValueChanged();
+                    (sel as SelectField).notifyValueChanged();
                 }
             }
         }
diff --git a/src/app/formulaire/definition/form-parallel-structures.ts b/src/app/formulaire/definition/form-parallel-structures.ts
index 9e4dd86d6..d25b4df33 100644
--- a/src/app/formulaire/definition/form-parallel-structures.ts
+++ b/src/app/formulaire/definition/form-parallel-structures.ts
@@ -2,7 +2,7 @@ import { Structure, Nub, ParallelStructure, StructureProperties, Props, Session,
 
 import { FieldsetContainer } from "../elements/fieldset-container";
 import { FieldSet } from "../elements/fieldset";
-import { SelectField } from "../elements/select-field";
+import { SelectField } from "../elements/select/select-field";
 import { NgParameter } from "../elements/ngparam";
 import { FieldsetTemplate } from "../elements/fieldset-template";
 import { FormulaireNode } from "../elements/formulaire-node";
diff --git a/src/app/formulaire/definition/form-pb-cloison.ts b/src/app/formulaire/definition/form-pb-cloison.ts
index 02934c401..25910c87d 100644
--- a/src/app/formulaire/definition/form-pb-cloison.ts
+++ b/src/app/formulaire/definition/form-pb-cloison.ts
@@ -5,7 +5,7 @@ import { FieldSet } from "../elements/fieldset";
 import { FormulaireNode } from "../elements/formulaire-node";
 import { FieldsetContainer } from "../elements/fieldset-container";
 import { FormulairePrebarrage } from "./form-prebarrage";
-import { SelectFieldCustom } from "../elements/select-field-custom";
+import { SelectField } from "../elements/select/select-field";
 import { ServiceFactory } from "app/services/service-factory";
 
 export class FormulairePbCloison extends FormulaireParallelStructure {
@@ -41,7 +41,7 @@ export class FormulairePbCloison extends FormulaireParallelStructure {
             }
         }
 
-        if (sender instanceof SelectFieldCustom) {
+        if (sender instanceof SelectField) {
             const nub = this._currentNub as PbCloison;
             const pb = nub.parent;
             const emptyFields: boolean = ServiceFactory.applicationSetupService.enableEmptyFieldsOnFormInit;
diff --git a/src/app/formulaire/definition/form-solveur.ts b/src/app/formulaire/definition/form-solveur.ts
index 481ea5f6d..f658c98ee 100644
--- a/src/app/formulaire/definition/form-solveur.ts
+++ b/src/app/formulaire/definition/form-solveur.ts
@@ -1,9 +1,8 @@
 import { IObservable, ParamDefinition, Nub } from "jalhyd";
 
-import { SelectFieldCustom } from "../elements/select-field-custom";
 import { NgParameter } from "../elements/ngparam";
 import { FormulaireFixedVar } from "./form-fixedvar";
-import { SelectField } from "../elements/select-field";
+import { SelectField } from "../elements/select/select-field";
 import { FieldSet } from "../elements/fieldset";
 
 /**
@@ -52,7 +51,7 @@ export class FormulaireSolveur extends FormulaireFixedVar {
             this.reset();
         }
 
-        if (sender instanceof SelectFieldCustom) {
+        if (sender instanceof SelectField) {
             if (sender.id === "select_target_nub" && data.action === "select") {
                 // update Solveur property: Nub to calculate
                 try {
@@ -95,7 +94,7 @@ export class FormulaireSolveur extends FormulaireFixedVar {
      * Re-populate searched parameter selector with fresh entries
      */
     private refreshParameterEntries() {
-        const pSel = this.getFormulaireNodeById("select_searched_param") as SelectFieldCustom;
+        const pSel = this.getFormulaireNodeById("select_searched_param") as SelectField;
         if (pSel) {
             pSel.updateEntries();
             // reflect changes in GUI
diff --git a/src/app/formulaire/definition/form-verificateur.ts b/src/app/formulaire/definition/form-verificateur.ts
index 3bcd463a6..893754665 100644
--- a/src/app/formulaire/definition/form-verificateur.ts
+++ b/src/app/formulaire/definition/form-verificateur.ts
@@ -1,9 +1,8 @@
 import { IObservable, Nub, Verificateur, Result, VariatedDetails } from "jalhyd";
 
-import { SelectFieldCustom } from "../elements/select-field-custom";
+import { SelectField } from "../elements/select/select-field";
 import { FormulaireFixedVar } from "./form-fixedvar";
 import { VerificateurResults } from "../../results/verificateur-results";
-import { CalculatorResults } from "../../results/calculator-results";
 import { ServiceFactory } from "../../services/service-factory";
 
 /**
@@ -94,7 +93,7 @@ export class FormulaireVerificateur extends FormulaireFixedVar {
             this.reset();
         }
 
-        if (sender instanceof SelectFieldCustom) {
+        if (sender instanceof SelectField) {
             this.reset(); // reset results
             if (sender.id === "select_target_pass" && data.action === "select") {
                 // update Verificateur property: Pass to check
diff --git a/src/app/formulaire/elements/fieldset.ts b/src/app/formulaire/elements/fieldset.ts
index 65200665a..4a67910e4 100644
--- a/src/app/formulaire/elements/fieldset.ts
+++ b/src/app/formulaire/elements/fieldset.ts
@@ -9,12 +9,12 @@ import {
 
 import { FormulaireElement } from "./formulaire-element";
 import { Field } from "./field";
-import { SelectField } from "./select-field";
+import { SelectField } from "./select/select-field";
 import { NgParameter, ParamRadioConfig } from "./ngparam";
 import { FieldsetContainer } from "./fieldset-container";
-import { SelectFieldCustom } from "./select-field-custom";
+import { SelectFieldFactory } from "./select/select-field-factory";
 import { FormulaireFixedVar } from "../definition/form-fixedvar";
-import { SelectEntry } from "./select-entry";
+import { SelectEntry } from "./select/select-entry";
 import { FormulaireNode } from "./formulaire-node";
 import { ServiceFactory } from "app/services/service-factory";
 
@@ -109,18 +109,7 @@ export class FieldSet extends FormulaireElement implements Observer {
     }
 
     private parse_select(json: {}): SelectField {
-        const res: SelectField = new SelectField(this);
-        res.parseConfig(json);
-        res.addObserver(this);
-        return res;
-    }
-
-    private parse_select_custom(json: {}): SelectField {
-        const source = json["source"];
-        if (source === undefined || source === "") {
-            throw new Error(`Fieldset.parse_select_custom(): "source" must not be empty`);
-        }
-        const res: SelectField = new SelectFieldCustom(this);
+        const res: SelectField = SelectFieldFactory.newSelectField(json, this);
         res.parseConfig(json);
         res.addObserver(this);
         return res;
@@ -225,12 +214,6 @@ export class FieldSet extends FormulaireElement implements Observer {
                     param = this.parse_select(field);
                     this.addField(param);
                     break;
-
-                case "select_custom":
-                    param = this.parse_select_custom(field);
-                    this.addField(param);
-                    break;
-
             }
         }
     }
@@ -311,7 +294,7 @@ export class FieldSet extends FormulaireElement implements Observer {
                 const fe = this.getFormulaireNodeById(sId);
                 if (fe) {
                     const prop = (fe as SelectField).associatedProperty;
-                    const defaultValue = (fe as SelectField).defaultValue;
+                    const defaultValue = (fe as SelectField).configDefaultValue;
                     // Sets Nub default property, unless this property is already set
                     const currentValue = this.properties.getPropValue(prop);
                     if (defaultValue !== undefined && currentValue === undefined) {
diff --git a/src/app/formulaire/elements/select-field-custom.ts b/src/app/formulaire/elements/select-field-custom.ts
deleted file mode 100644
index 5c395281e..000000000
--- a/src/app/formulaire/elements/select-field-custom.ts
+++ /dev/null
@@ -1,343 +0,0 @@
-import { SelectEntry } from "./select-entry";
-import { ServiceFactory } from "../../services/service-factory";
-import { SelectField } from "./select-field";
-import { decodeHtml, arraysAreEqual } from "../../util";
-
-import { FishSpecies, Session, Solveur, FishPass, CalculatorType, Verificateur, Nub, PbCloison, PreBarrage, acSection } from "jalhyd";
-
-import { sprintf } from "sprintf-js";
-
-/**
- * A select field that populates itself with custom stuff (ex: references to Nubs, Parameters…)
- */
-export class SelectFieldCustom extends SelectField {
-
-    /**
-     * Loads UI with the value held by the model
-     */
-    protected initSelectedValue() {
-        const nub = this.parentForm.currentNub;
-
-        switch (this.source) {
-
-            case "solveur_target": // Solveur, module cible (à calculer)
-                const ntc = (nub as Solveur).nubToCalculate;
-                if (ntc !== undefined) {
-                    this.setValueFromId(this._entriesBaseId + ntc.uid);
-                }
-                break;
-
-            case "solveur_searched": // Solveur, paramètre recherché (à faire varier)
-                const sp = (nub as Solveur).searchedParameter;
-                if (sp !== undefined) {
-                    let parentNubUid;
-                    const parentNub = sp.getParentComputeNode(false);
-                    if (parentNub instanceof acSection) {
-                        parentNubUid = parentNub.uid;
-                    }
-                    else {
-                        parentNubUid = sp.nubUid
-                    }
-                    this.setValueFromId(this._entriesBaseId + parentNubUid + "_" + sp.symbol);
-                }
-                break;
-
-            case "verificateur_target": // Vérificateur, passe cible (à vérifier)
-                const ntv = (nub as Verificateur).nubToVerify;
-                if (ntv !== undefined) {
-                    this.setValueFromId(this._entriesBaseId + ntv.uid);
-                }
-                break;
-
-            case "verificateur_species": // Vérificateur, liste d'espèces (choix multiple)
-                const sl = (nub as Verificateur).speciesList;
-                if (sl !== undefined) {
-                    this.setValueFromId(sl.map((s) => {
-                        const spgId = s.substring(s.lastIndexOf("_") + 1);
-                        return this._entriesBaseId + spgId;
-                    }));
-                }
-                break;
-
-            case "upstream_basin": // PbCloisons, bassin amont
-                const ub = (nub as PbCloison).bassinAmont;
-                // console.log("-- load UB", ub, this._entriesBaseId + ub?.uid);
-                this.setValueFromId(this._entriesBaseId + (ub ? ub.uid : "none"));
-                break;
-
-            case "downstream_basin": // PbCloisons, bassin aval
-                const db = (nub as PbCloison).bassinAval;
-                // console.log("-- load DB", db, this._entriesBaseId + db?.uid);
-                this.setValueFromId(this._entriesBaseId + (db ? db.uid : "none"));
-                break;
-        }
-    }
-
-    /**
-     * Populates entries with available options
-     */
-    protected populate() {
-        const fs = ServiceFactory.formulaireService;
-        let candidateNubs: any[];
-        switch (this.source) {
-
-            case "solveur_target": // Solveur, module cible (à calculer)
-                // find all Nubs having at least one link to another Nub's result
-                candidateNubs =
-                    Session.getInstance().getDownstreamNubs().concat(
-                        Session.getInstance().getUpstreamNubsHavingExtraResults()
-                    ).filter(
-                        (element, index, self) => self.findIndex((e) => e.uid === element.uid) === index
-                    );
-                for (const cn of candidateNubs) {
-                    const nub = fs.getFormulaireFromId(cn.uid);
-                    if (nub) {
-                        const calc = nub.calculatorName;
-                        let label = calc;
-                        // calculated param
-                        if (cn.calculatedParam !== undefined) {
-                            const varName = fs.expandVariableName(cn.calcType, cn.calculatedParam.symbol);
-                            label += ` / ${varName} (${cn.calculatedParam.symbol})`;
-                        }
-                        this.addEntry(new SelectEntry(this._entriesBaseId + cn.uid, cn.uid, decodeHtml(label)));
-                    } else {
-                        // silent fail, this Solveur nub was probably loaded before all the candidate nubs are done loading
-                    }
-                }
-                break;
-
-            case "solveur_searched": // Solveur, paramètre recherché (à faire varier)
-                // find all non-calculated, non-linked parameters of all Nubs that
-                // the current "target" Nub depends on (if any)
-                const solv = this.parentForm.currentNub as Solveur;
-                const ntc: Nub = solv.nubToCalculate;
-                const searchableParams = Solveur.getDependingNubsSearchableParams(
-                    ntc,
-                    solv.targettedResult !== undefined && solv.targettedResult !== ""
-                );
-                for (const p of searchableParams) {
-                    if (p.visible) {
-                        const calc = fs.getFormulaireFromId(p.originNub.uid).calculatorName;
-                        const varName = fs.expandVariableName(p.originNub.calcType, p.symbol);
-                        const label = `${p.symbol} - ${varName} (${calc})`;
-                        this.addEntry(new SelectEntry(this._entriesBaseId + p.getParentComputeNode(false).uid + "_" + p.symbol, p, decodeHtml(label)));
-                    }
-                }
-                break;
-
-            case "verificateur_target": // Vérificateur, passe cible (à vérifier)
-                // find all Nubs of type FishPass
-                candidateNubs = Session.getInstance().getAllNubs().filter((element) => {
-                    return (
-                        (element instanceof FishPass)
-                        && element.calcType !== CalculatorType.Par // ParSimulation extends Par @TODO find something better
-                    );
-                });
-                for (const cn of candidateNubs) {
-                    const nub = fs.getFormulaireFromId(cn.uid);
-                    if (nub) {
-                        const label = nub.calculatorName + " (" + fs.getLocalisedTitleFromCalculatorType(nub.calculatorType) + ")";
-                        this.addEntry(new SelectEntry(this._entriesBaseId + cn.uid, cn.uid, decodeHtml(label)));
-                    } else {
-                        // silent fail, this Verificateur nub was probably loaded before all the candidate nubs are done loading
-                    }
-                }
-                break;
-
-            case "verificateur_species":
-                // add UIDs of all Espece type Nubs in the session
-                const especeNubs = Session.getInstance().getAllNubs().filter((element) => element.calcType === CalculatorType.Espece);
-                for (const en of especeNubs) {
-                    const form = ServiceFactory.formulaireService.getFormulaireFromNubId(en.uid);
-                    this.addEntry(
-                        new SelectEntry(
-                            this._entriesBaseId + en.uid,
-                            en.uid,
-                            sprintf(
-                                ServiceFactory.i18nService.localizeText("INFO_VERIFICATEUR_CUSTOM_SPECIES"),
-                                form ? form.calculatorName : "*** form not loaded yet ***"
-                            )
-                        )
-                    );
-                }
-                // add all FishSpecies
-                for (let j = 1; j < Object.keys(FishSpecies).length / 2; j++) { // exclude "0" (SPECIES_CUSTOM)
-                    const spgId = FishSpecies[j].substring(FishSpecies[j].lastIndexOf("_") + 1);
-                    this.addEntry(
-                        new SelectEntry(
-                            this._entriesBaseId + spgId,
-                            FishSpecies[j],
-                            ServiceFactory.i18nService.localizeText("INFO_ENUM_" + FishSpecies[j])
-                        )
-                    );
-                }
-                break;
-
-            case "upstream_basin": // PbCloisons, bassin amont
-                const pbWallU = this.parentForm.currentNub as PbCloison;
-                const preBarrageU = pbWallU.parent as PreBarrage;
-                const posDb = pbWallU.bassinAval?.findPositionInParent();
-                // river upstream
-                this.addEntry(
-                    new SelectEntry(
-                        this._entriesBaseId + "none",
-                        undefined,
-                        ServiceFactory.i18nService.localizeText("INFO_LIB_AMONT")
-                    )
-                );
-                // all available basins, depending on current downstream basin
-                for (const b of preBarrageU.bassins) {
-                    const pos = b.findPositionInParent();
-                    if (posDb === undefined || pos < posDb) {
-                        this.addEntry(
-                            new SelectEntry(
-                                this._entriesBaseId + b.uid,
-                                b.uid,
-                                ServiceFactory.i18nService.localizeMessage(b.description)
-                            )
-                        );
-                    }
-                }
-                break;
-
-            case "downstream_basin": // PbCloisons, bassin aval
-                const pbWallD = this.parentForm.currentNub as PbCloison;
-                const preBarrageD = pbWallD.parent as PreBarrage;
-                const posUb = pbWallD.bassinAmont?.findPositionInParent();
-                // all available basins, depending on current upstream basin
-                for (const b of preBarrageD.bassins) {
-                    const pos = b.findPositionInParent();
-                    if (posUb === undefined || pos > posUb) {
-                        this.addEntry(
-                            new SelectEntry(
-                                this._entriesBaseId + b.uid,
-                                b.uid,
-                                ServiceFactory.i18nService.localizeMessage(b.description)
-                            )
-                        );
-                    }
-                }
-                // river downstream
-                this.addEntry(
-                    new SelectEntry(
-                        this._entriesBaseId + "none",
-                        undefined,
-                        ServiceFactory.i18nService.localizeText("INFO_LIB_AVAL")
-                    )
-                );
-                break;
-        }
-    }
-
-    protected setDefaultValue() {
-        // default to first available entry if any
-        if (this._entries.length > 0) {
-            if (this._multiple) {
-                this.setValue([this._entries[0]]);
-            } else {
-                this.setValue(this._entries[0]);
-            }
-        } else {
-            // notify observers that no value is selected anymore
-            this.notifyValueChanged();
-        }
-    }
-
-    /**
-     * Once config is parsed, init original value from model
-     * (needs config, for this._entriesBaseId to be set)
-     */
-    protected afterParseConfig() {
-        this.populate();
-        this.initSelectedValue();
-    }
-
-    /**
-     * Reloads available entries, trying to keep the current selected
-     * value; does not notify observers if value did not change
-     */
-    public updateEntries() {
-        // store previous selected entry
-        const pse = this._selectedEntry;
-        // empty
-        this.clearEntries();
-        // populate
-        this.populate();
-        // if no entry is available anymore, unset value
-        if (this.entries.length === 0) {
-            if (this.multiple) {
-                super.setValue([]);
-            } else {
-                super.setValue(undefined);
-            }
-        } else {
-            // keep previously selected entry(ies) if possible
-            if (pse) {
-                if (!Array.isArray(pse) && pse.id) {
-                    this.setValueFromId(pse.id);
-                } else if (Array.isArray(pse) && pse.length > 0) {
-                    this.setValueFromId(pse.map((e) => e.id));
-                } else {
-                    this.setDefaultValue();
-                }
-            } else {
-                this.setDefaultValue();
-            }
-        }
-    }
-
-    /**
-     * Updates selectedValue; notifies observers only if
-     * value.id has changed
-     */
-    public setValue(v: SelectEntry | SelectEntry[]) {
-        const previousSelectedEntry = this._selectedEntry;
-        this._selectedEntry = v;
-        // if value changed
-        const valueChanged = (
-            !previousSelectedEntry
-            || (
-                !Array.isArray(previousSelectedEntry)
-                && !Array.isArray(v)
-                && previousSelectedEntry.id !== v.id
-            )
-            || (
-                Array.isArray(previousSelectedEntry)
-                && Array.isArray(v)
-                && !arraysAreEqual(previousSelectedEntry, v, "id", true)
-            )
-        );
-        if (valueChanged) {
-            this.notifyValueChanged();
-        }
-    }
-
-    /**
-     * Sets value from given ID(s); if it was not found, sets the
-     * first available entry as selectedValue
-     */
-    public setValueFromId(id: string | string[]) {
-        let found = false;
-        const entries = [];
-        if (this._multiple && Array.isArray(id)) {
-            for (const e of this._entries) {
-                if (id.includes(e.id)) {
-                    entries.push(e);
-                    found = true;
-                }
-            }
-            this.setValue(entries);
-        } else {
-            for (const e of this._entries) {
-                if (e.id === id) {
-                    found = true;
-                    this.setValue(e);
-                }
-            }
-        }
-        if (!found) {
-            this.setDefaultValue();
-        }
-    }
-
-}
diff --git a/src/app/formulaire/elements/select-field.ts b/src/app/formulaire/elements/select-field.ts
deleted file mode 100644
index ce08ddefd..000000000
--- a/src/app/formulaire/elements/select-field.ts
+++ /dev/null
@@ -1,244 +0,0 @@
-import {
-    CourbeRemous,
-    Nub,
-    ParallelStructure,
-    StructureType,
-    LoiDebit,
-    Session,
-    Solveur,
-    StructureProperties
- } from "jalhyd";
-
-import { Field } from "./field";
-import { SelectEntry } from "./select-entry";
-import { FormulaireNode } from "./formulaire-node";
-import { FormulaireDefinition } from "../definition/form-definition";
-import { ServiceFactory } from "../../services/service-factory";
-
-export class SelectField extends Field {
-
-    /** string to build the select entries IDs from */
-    protected _entriesBaseId: string;
-
-    protected _entries: SelectEntry[];
-
-    protected _selectedEntry: SelectEntry | SelectEntry[];
-
-    /** name of the Nub property associated to this field, if any */
-    protected _associatedProperty: string;
-
-    /** default value for this field */
-    protected _defaultValue: string;
-
-    /** if true, user can select multiple values */
-    protected _multiple = false;
-
-    /** if true, select box is grayed out */
-    public disabled = false;
-
-    /** source identifier for populating with available values */
-    protected source: string;
-
-    /**
-     * message to display below the select field when no option is selected,
-     * if this message is defined and not empty
-     */
-    protected _messageWhenEmpty: string;
-
-    constructor(parent: FormulaireNode) {
-        super(parent);
-        this.clearEntries();
-    }
-
-    public get entriesBaseId(): string {
-        return this._entriesBaseId;
-    }
-
-    public get entries() {
-        return this._entries;
-    }
-
-    public get associatedProperty(): string {
-        return this._associatedProperty;
-    }
-
-    public get defaultValue(): string {
-        return this._defaultValue;
-    }
-
-    public get multiple(): boolean {
-        return this._multiple;
-    }
-
-    public get messageWhenEmpty(): string {
-        let msg: string;
-        if (this._selectedEntry === undefined && this._messageWhenEmpty) {
-            msg = ServiceFactory.i18nService.localizeText(this._messageWhenEmpty);
-        }
-        return msg;
-    }
-
-    public clearEntries() {
-        this._entries = [];
-    }
-
-    public addEntry(e: SelectEntry) {
-        this._entries.push(e);
-        if (! this._selectedEntry) {
-            if (this._multiple) {
-                this.setValue([ e ]);
-            } else {
-                this.setValue(e);
-            }
-        }
-    }
-
-    /**
-     * Triggered at the end of parseConfig()
-     */
-    protected afterParseConfig() { }
-
-    public getEntryFromValue(val: any): SelectEntry {
-        for (const se of this._entries) {
-            if (se.value === val) {
-                return se;
-            }
-        }
-    }
-
-    public getValue(): SelectEntry | SelectEntry[] {
-        return this._selectedEntry;
-    }
-
-    public setValue(v: SelectEntry | SelectEntry[]) {
-        if (this._selectedEntry !== v) {
-            this._selectedEntry = v;
-            this.notifyValueChanged();
-        }
-    }
-
-    public notifyValueChanged() {
-        this.notifyObservers({
-            "action": "select",
-            "value": this._selectedEntry
-        }, this);
-    }
-
-    public updateLocalisation() {
-        super.updateLocalisation();
-        for (const e of this._entries) {
-            if (this.source === "solveur_targetted_result") {
-                // @WARNING clodo hack for Solveur
-                // 1. calculated param
-                const nub: Nub = (this.parentForm as FormulaireDefinition).currentNub;
-                const ntc = (nub as Solveur).nubToCalculate;
-                if (e.value !== undefined && ntc !== undefined) {
-                    if (e.value === "" && ntc.calculatedParam !== undefined) {
-                        const varName = ServiceFactory.formulaireService.expandVariableName(ntc.calcType, ntc.calculatedParam.symbol);
-                        e.label = `${varName} (${ntc.calculatedParam.symbol})`;
-                    } else {
-                        // 2. extra results
-                        const varName = ServiceFactory.formulaireService.expandVariableName(ntc.calcType, e.value);
-                        e.label = `${varName} (${e.value})`;
-                    }
-                }
-            } else {
-                // general case
-                const aId = e.id.split("_");
-                const trad = ServiceFactory.formulaireService.localizeText(
-                    `${aId[1].toUpperCase()}_${aId[2]}`,
-                    this.parentForm.currentNub.calcType
-                );
-                e.label = trad;
-            }
-        }
-    }
-
-    public parseConfig(field: {}, data?: {}) {
-        this._confId = field["id"];
-        this._entriesBaseId = this._confId + "_";
-        this._helpLink = field["help"];
-        this._associatedProperty = field["property"];
-        this._defaultValue = field["default"];
-        this._messageWhenEmpty = field["messageWhenEmpty"];
-        if (field["multiple"] !== undefined) {
-            this._multiple = field["multiple"];
-        }
-        this.source = field["source"];
-
-        this.loadEntriesFromSource();
-
-        this.afterParseConfig();
-    }
-
-    /**
-     * Adds available entries to the selector, depending on the "source" identifier
-     */
-    protected loadEntriesFromSource() {
-        const nub: Nub = (this.parentForm as FormulaireDefinition).currentNub;
-        // ad-hoc cases
-        switch (this.source) {
-            // driven by string[], not enum (easier for variable names)
-            case "remous_target":
-                this.addEntry(new SelectEntry(this._entriesBaseId + "none", ""));
-                for (const at of CourbeRemous.availableTargets) {
-                    const e: SelectEntry = new SelectEntry(this._entriesBaseId + at, at);
-                    this.addEntry(e);
-                }
-                break;
-
-            // driven by string[], not enum
-            case "solveur_targetted_result":
-                // @WARNING for localisation, @see hack in this.updateLocalisation()
-                // 1. calculated param
-                const ntc = (nub as Solveur).nubToCalculate;
-                if (ntc?.calculatedParam !== undefined) { // some nubs have no calculatedParam, for ex. SectionParam
-                    this.addEntry(new SelectEntry(this._entriesBaseId + "none", ""));
-                }
-                // 2. extra results
-                if (ntc?.resultsFamilies !== undefined) {
-                    for (const er of Object.keys(ntc.resultsFamilies)) {
-                        const e: SelectEntry = new SelectEntry(this._entriesBaseId + er, er);
-                        this.addEntry(e);
-                    }
-                }
-                break;
-
-            // possible values depend on CalcType
-            case "device_structure_type":
-                for (const st in (nub as ParallelStructure).getLoisAdmissibles()) {
-                    const e: SelectEntry = new SelectEntry(this._entriesBaseId + st, StructureType[st]);
-                    this.addEntry(e);
-                }
-                break;
-
-            // possible values depend on CalcType
-            case "device_loi_debit":
-                // get current structure type from appropriate Nub child
-                const child = nub.getChildren()[this.parent.indexAsKid()];
-                const la = (nub as ParallelStructure).getLoisAdmissibles();
-                const loiDebit = child.properties.getPropValue("loiDebit");
-                const stCode = StructureProperties.findCompatibleStructure(loiDebit, nub as ParallelStructure);
-                const stName = StructureType[stCode];
-                if (la[stName] !== undefined) {
-                    for (const ld of la[stName]) {
-                        const e: SelectEntry = new SelectEntry(this._entriesBaseId + LoiDebit[ld], ld);
-                        this.addEntry(e);
-                    }
-                }
-                break;
-
-            // general case : property values taken from an enum
-            default:
-                // find enum associated to property
-                const enumClass = Session.enumFromProperty[this._associatedProperty];
-                if (enumClass !== undefined) {
-                    // add one select entry per enum entry, in the enum order
-                    for (let j = 0; j < Object.keys(enumClass).length / 2; j++) {
-                        this.addEntry(new SelectEntry(this._entriesBaseId + j, j));
-                    }
-                }
-        }
-    }
-
-}
diff --git a/src/app/formulaire/elements/select-entry.ts b/src/app/formulaire/elements/select/select-entry.ts
similarity index 100%
rename from src/app/formulaire/elements/select-entry.ts
rename to src/app/formulaire/elements/select/select-entry.ts
diff --git a/src/app/formulaire/elements/select/select-field-device-loi-debit.ts b/src/app/formulaire/elements/select/select-field-device-loi-debit.ts
new file mode 100644
index 000000000..15194174f
--- /dev/null
+++ b/src/app/formulaire/elements/select/select-field-device-loi-debit.ts
@@ -0,0 +1,42 @@
+import { LoiDebit, ParallelStructure, StructureProperties, StructureType } from "jalhyd";
+import { SelectField } from "./select-field";
+import { SelectEntry } from "./select-entry";
+
+/*
+    "id": "select_loidebit",
+    "type": "select",
+    "property": "loiDebit",
+    "source": "device_loi_debit",
+    "help": {
+*/
+
+export class SelectFieldDeviceLoiDebit extends SelectField {
+    protected populate() {
+        // possible values depend on CalcType
+
+        // get current structure type from appropriate Nub child
+        const child = this.nub.getChildren()[this.parent.indexAsKid()];
+        const la = (this.nub as ParallelStructure).getLoisAdmissibles();
+        const loiDebit = child.properties.getPropValue("loiDebit");
+        const stCode = StructureProperties.findCompatibleStructure(loiDebit, this.nub as ParallelStructure);
+        const stName = StructureType[stCode];
+        if (la[stName] !== undefined) {
+            for (const ld of la[stName]) {
+                const e: SelectEntry = new SelectEntry(this._entriesBaseId + LoiDebit[ld], ld);
+                this.addEntry(e);
+            }
+        }
+    }
+
+    protected initSelectedValue() {
+        this.findAndSetDefaultValue();
+    }
+
+    public get associatedProperty(): string {
+        return "loiDebit";
+    }
+
+    public get configDefaultValue(): string {
+        return undefined;
+    }
+}
\ No newline at end of file
diff --git a/src/app/formulaire/elements/select/select-field-device-structure-type.ts b/src/app/formulaire/elements/select/select-field-device-structure-type.ts
new file mode 100644
index 000000000..b818eedf5
--- /dev/null
+++ b/src/app/formulaire/elements/select/select-field-device-structure-type.ts
@@ -0,0 +1,32 @@
+import { ParallelStructure, StructureType } from "jalhyd";
+import { SelectField } from "./select-field";
+import { SelectEntry } from "./select-entry";
+
+/*
+    "id": "select_structure",
+    "type": "select",
+    "property": "structureType",
+    "source": "device_structure_type"
+*/
+
+export class SelectFieldDeviceStructureType extends SelectField {
+    protected populate() {
+        // possible values depend on CalcType
+        for (const st in (this.nub as ParallelStructure).getLoisAdmissibles()) {
+            const e: SelectEntry = new SelectEntry(this._entriesBaseId + st, StructureType[st]);
+            this.addEntry(e);
+        }
+    }
+
+    protected initSelectedValue() {
+        this.findAndSetDefaultValue();
+    }
+
+    public get associatedProperty(): string {
+        return "structureType";
+    }
+
+    public get configDefaultValue(): string {
+        return undefined;
+    }
+}
\ No newline at end of file
diff --git a/src/app/formulaire/elements/select/select-field-downstream-basin.ts b/src/app/formulaire/elements/select/select-field-downstream-basin.ts
new file mode 100644
index 000000000..77fcab4bd
--- /dev/null
+++ b/src/app/formulaire/elements/select/select-field-downstream-basin.ts
@@ -0,0 +1,53 @@
+import { ServiceFactory } from "app/services/service-factory";
+import { PbCloison, PreBarrage } from "jalhyd";
+import { SelectEntry } from "./select-entry";
+import { SelectField } from "./select-field";
+
+/*
+    "id": "select_downstream_basin",
+    "type": "select_custom",
+    "source": "downstream_basin"
+*/    
+
+// PbCloisons, bassin aval
+export class SelectFieldDownstreamBasin extends SelectField {
+    protected populate() {
+        const pbWallD = this.parentForm.currentNub as PbCloison;
+        const preBarrageD = pbWallD.parent as PreBarrage;
+        const posUb = pbWallD.bassinAmont?.findPositionInParent();
+        // all available basins, depending on current upstream basin
+        for (const b of preBarrageD.bassins) {
+            const pos = b.findPositionInParent();
+            if (posUb === undefined || pos > posUb) {
+                this.addEntry(
+                    new SelectEntry(
+                        this._entriesBaseId + b.uid,
+                        b.uid,
+                        ServiceFactory.i18nService.localizeMessage(b.description)
+                    )
+                );
+            }
+        }
+        // river downstream
+        this.addEntry(
+            new SelectEntry(
+                this._entriesBaseId + "none",
+                undefined,
+                ServiceFactory.i18nService.localizeText("INFO_LIB_AVAL")
+            )
+        );
+    }
+
+    protected initSelectedValue() {
+        const db = (this.nub as PbCloison).bassinAval;
+        this.setValueFromId(this._entriesBaseId + (db ? db.uid : "none"));
+    }
+
+    public get associatedProperty(): string {
+        return undefined;
+    }
+
+    public get configDefaultValue(): string {
+        return undefined;
+    }
+}
diff --git a/src/app/formulaire/elements/select/select-field-factory.ts b/src/app/formulaire/elements/select/select-field-factory.ts
new file mode 100644
index 000000000..38e8e7b49
--- /dev/null
+++ b/src/app/formulaire/elements/select/select-field-factory.ts
@@ -0,0 +1,66 @@
+import { FormulaireNode } from "../formulaire-node";
+import { SelectField } from "./select-field";
+import { SelectFieldDeviceLoiDebit } from "./select-field-device-loi-debit";
+import { SelectFieldDeviceStructureType } from "./select-field-device-structure-type";
+import { SelectFieldRemousTarget } from "./select-field-remous-target";
+import { SelectFieldSolverTargetedResult } from "./select-field-solver-targeted-result";
+import { SelectFieldUpstreamBasin } from "./select-field-upstream-basin";
+import { SelectFieldDownstreamBasin } from "./select-field-downstream-basin";
+import { SelectFieldSolverTarget } from "./select-field-solveur-target";
+import { SelectFieldSearchedParam } from "./select-field-searched-param";
+import { SelectFieldSpeciesList } from "./select-field-species-list";
+import { SelectFieldTargetPass } from "./select-field-target-pass";
+import { SelectFieldNubProperty } from "./select-field-nub-prop";
+
+export class SelectFieldFactory {
+    public static newSelectField(json: {}, parent: FormulaireNode): SelectField {
+        switch (json["id"]) {
+            case "select_downstream_basin":
+                return new SelectFieldDownstreamBasin(parent);
+
+            case "select_loidebit":
+                return new SelectFieldDeviceLoiDebit(parent);
+
+            case "select_searched_param":
+                return new SelectFieldSearchedParam(parent);
+
+            case "select_species_list":
+                return new SelectFieldSpeciesList(parent);
+
+            case "select_structure":
+                return new SelectFieldDeviceStructureType(parent);
+
+            case "select_target":
+                return new SelectFieldRemousTarget(parent);
+
+            case "select_target_nub":
+                return new SelectFieldSolverTarget(parent);
+
+            case "select_target_pass":
+                return new SelectFieldTargetPass(parent);
+
+            case "select_target_result":
+                return new SelectFieldSolverTargetedResult(parent);
+
+            case "select_upstream_basin":
+                return new SelectFieldUpstreamBasin(parent);
+
+            case "select_divingjetsupported":
+            case "select_gridprofile":
+            case "select_gridtype":
+            case "select_material":
+            case "select_operation":
+            case "select_partype":
+            case "select_passtype":
+            case "select_regime":
+            case "select_resolution":
+            case "select_section":
+            case "select_sppoperation":
+            case "select_unit":
+                return new SelectFieldNubProperty(parent, json);
+
+            default:
+                throw new Error("unknown select id ${id}");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/app/formulaire/elements/select/select-field-nub-prop.ts b/src/app/formulaire/elements/select/select-field-nub-prop.ts
new file mode 100644
index 000000000..eca0463b3
--- /dev/null
+++ b/src/app/formulaire/elements/select/select-field-nub-prop.ts
@@ -0,0 +1,38 @@
+import { Session } from "jalhyd";
+import { FormulaireNode } from "../formulaire-node";
+import { SelectEntry } from "./select-entry";
+import { SelectField } from "./select-field";
+
+/*
+  par exemple :
+    "id": "select_regime",
+    "type": "select",
+    "property": "regime",
+    "default": "Fluvial"
+
+*/
+
+// nub property values taken from an enum
+// "standard" select that normally does not require customisation
+export class SelectFieldNubProperty extends SelectField {
+    constructor(parent: FormulaireNode, json: {}) {
+        super(parent);
+        this._associatedProperty = json["property"];
+        this._configDefaultValue = json["default"];
+    }
+
+    protected populate() {
+        // find enum associated to property
+        const enumClass = Session.enumFromProperty[this._associatedProperty];
+        if (enumClass !== undefined) {
+            // add one select entry per enum entry, in the enum order
+            for (let j = 0; j < Object.keys(enumClass).length / 2; j++) {
+                this.addEntry(new SelectEntry(this._entriesBaseId + j, j));
+            }
+        }
+    }
+
+    protected initSelectedValue() {
+        this.findAndSetDefaultValue();
+    }
+}
\ No newline at end of file
diff --git a/src/app/formulaire/elements/select/select-field-remous-target.ts b/src/app/formulaire/elements/select/select-field-remous-target.ts
new file mode 100644
index 000000000..6d0f2c506
--- /dev/null
+++ b/src/app/formulaire/elements/select/select-field-remous-target.ts
@@ -0,0 +1,34 @@
+import { CourbeRemous } from "jalhyd";
+import { SelectField } from "./select-field";
+import { SelectEntry } from "./select-entry";
+
+/*
+    "id": "select_target",
+    "type": "select",
+    "property": "varCalc",
+    "source": "remous_target",
+    "help": {
+*/
+
+export class SelectFieldRemousTarget extends SelectField {
+    protected populate() {
+        // driven by string[], not enum (easier for variable names)
+        this.addEntry(new SelectEntry(this._entriesBaseId + "none", ""));
+        for (const at of CourbeRemous.availableTargets) {
+            const e: SelectEntry = new SelectEntry(this._entriesBaseId + at, at);
+            this.addEntry(e);
+        }
+    }
+
+    protected initSelectedValue() {
+        this.findAndSetDefaultValue();
+    }
+
+    public get associatedProperty(): string {
+        return "varCalc";
+    }
+
+    public get configDefaultValue(): string {
+        return undefined;
+    }
+}
\ No newline at end of file
diff --git a/src/app/formulaire/elements/select/select-field-searched-param.ts b/src/app/formulaire/elements/select/select-field-searched-param.ts
new file mode 100644
index 000000000..5380e1238
--- /dev/null
+++ b/src/app/formulaire/elements/select/select-field-searched-param.ts
@@ -0,0 +1,63 @@
+import { ServiceFactory } from "app/services/service-factory";
+import { decodeHtml } from "app/util";
+import { acSection, Nub, Solveur } from "jalhyd";
+import { SelectEntry } from "./select-entry";
+import { SelectField } from "./select-field";
+
+/*
+    "id": "select_searched_param",
+    "type": "select_custom",
+    "source": "solveur_searched"
+*/
+
+// Solveur, paramètre recherché (à faire varier)
+export class SelectFieldSearchedParam extends SelectField {
+    protected populate() {
+        const fs = ServiceFactory.formulaireService;
+
+        // find all non-calculated, non-linked parameters of all Nubs that
+        // the current "target" Nub depends on (if any)
+        const solv = this.parentForm.currentNub as Solveur;
+        const ntc: Nub = solv.nubToCalculate;
+        const searchableParams = Solveur.getDependingNubsSearchableParams(
+            ntc,
+            solv.targettedResult !== undefined && solv.targettedResult !== ""
+        );
+        for (const p of searchableParams) {
+            if (p.visible) {
+                const calc = fs.getFormulaireFromId(p.originNub.uid).calculatorName;
+                const varName = fs.expandVariableName(p.originNub.calcType, p.symbol);
+                const label = `${p.symbol} - ${varName} (${calc})`;
+                this.addEntry(new SelectEntry(this._entriesBaseId + p.getParentComputeNode(false).uid + "_" + p.symbol, p, decodeHtml(label)));
+            }
+        }
+    }
+
+    protected initSelectedValue() {
+        const sp = (this.nub as Solveur).searchedParameter;
+        if (sp !== undefined) {
+            let parentNubUid;
+            const parentNub = sp.getParentComputeNode(false);
+            if (parentNub instanceof acSection) {
+                parentNubUid = parentNub.uid;
+            }
+            else {
+                parentNubUid = sp.nubUid
+            }
+            this.setValueFromId(this._entriesBaseId + parentNubUid + "_" + sp.symbol);
+        }
+    }
+
+    public get associatedProperty(): string {
+        return undefined;
+    }
+
+    public get configDefaultValue(): string {
+        return undefined;
+    }
+
+    public updateLocalisation() {
+        // do not override localisation done in populate()
+        // ie. avoid what is done by SelectField.updateLocalisation()
+    }
+}
diff --git a/src/app/formulaire/elements/select/select-field-solver-targeted-result.ts b/src/app/formulaire/elements/select/select-field-solver-targeted-result.ts
new file mode 100644
index 000000000..2b10da827
--- /dev/null
+++ b/src/app/formulaire/elements/select/select-field-solver-targeted-result.ts
@@ -0,0 +1,64 @@
+import { FormulaireDefinition } from "app/formulaire/definition/form-definition";
+import { ServiceFactory } from "app/services/service-factory";
+import { Nub, Solveur } from "jalhyd";
+import { FormulaireElement } from "../formulaire-element";
+import { SelectField } from "./select-field";
+import { SelectEntry } from "./select-entry";
+
+/*
+    "id": "select_target_result",
+    "type": "select",
+    "property": "targettedResult",
+    "source": "solveur_targetted_result",
+    "default": ""
+*/
+export class SelectFieldSolverTargetedResult extends SelectField {
+
+    protected populate() {
+        // @WARNING for localisation, @see hack in this.updateLocalisation()
+        // 1. calculated param
+        const ntc = (this.nub as Solveur).nubToCalculate;
+        if (ntc?.calculatedParam !== undefined) { // some nubs have no calculatedParam, for ex. SectionParam
+            this.addEntry(new SelectEntry(this._entriesBaseId + "none", ""));
+        }
+        // 2. extra results
+        if (ntc?.resultsFamilies !== undefined) {
+            for (const er of Object.keys(ntc.resultsFamilies)) {
+                const e: SelectEntry = new SelectEntry(this._entriesBaseId + er, er);
+                this.addEntry(e);
+            }
+        }
+    }
+
+    protected initSelectedValue() {
+        this.findAndSetDefaultValue();
+    }
+
+    public get configDefaultValue(): string {
+        return "";
+    }
+
+    public get associatedProperty(): string {
+        return "targettedResult";
+    }
+
+    public updateLocalisation() {
+        FormulaireElement.prototype.updateLocalisation.call(this);
+        for (const e of this._entries) {
+            // @WARNING clodo hack for Solveur
+            // 1. calculated param
+            const nub: Nub = (this.parentForm as FormulaireDefinition).currentNub;
+            const ntc = (nub as Solveur).nubToCalculate;
+            if (e.value !== undefined && ntc !== undefined) {
+                if (e.value === "" && ntc.calculatedParam !== undefined) {
+                    const varName = ServiceFactory.formulaireService.expandVariableName(ntc.calcType, ntc.calculatedParam.symbol);
+                    e.label = `${varName} (${ntc.calculatedParam.symbol})`;
+                } else {
+                    // 2. extra results
+                    const varName = ServiceFactory.formulaireService.expandVariableName(ntc.calcType, e.value);
+                    e.label = `${varName} (${e.value})`;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/app/formulaire/elements/select/select-field-solveur-target.ts b/src/app/formulaire/elements/select/select-field-solveur-target.ts
new file mode 100644
index 000000000..64d37dc33
--- /dev/null
+++ b/src/app/formulaire/elements/select/select-field-solveur-target.ts
@@ -0,0 +1,62 @@
+/*
+    "id": "select_target_nub",
+    "type": "select_custom",
+    "source": "solveur_target"
+*/
+
+import { ServiceFactory } from "app/services/service-factory";
+import { decodeHtml } from "../../../util";
+import { Session, Solveur } from "jalhyd";
+import { SelectEntry } from "./select-entry";
+import { SelectField } from "./select-field";
+
+// Solveur, module cible (à calculer)
+export class SelectFieldSolverTarget extends SelectField {
+    protected populate() {
+
+        const fs = ServiceFactory.formulaireService;
+
+        // find all Nubs having at least one link to another Nub's result
+        const candidateNubs: any[] =
+            Session.getInstance().getDownstreamNubs().concat(
+                Session.getInstance().getUpstreamNubsHavingExtraResults()
+            ).filter(
+                (element, index, self) => self.findIndex((e) => e.uid === element.uid) === index
+            );
+        for (const cn of candidateNubs) {
+            const nub = fs.getFormulaireFromId(cn.uid);
+            if (nub) {
+                const calc = nub.calculatorName;
+                let label = calc;
+                // calculated param
+                if (cn.calculatedParam !== undefined) {
+                    const varName = fs.expandVariableName(cn.calcType, cn.calculatedParam.symbol);
+                    label += ` / ${varName} (${cn.calculatedParam.symbol})`;
+                }
+                this.addEntry(new SelectEntry(this._entriesBaseId + cn.uid, cn.uid, decodeHtml(label)));
+            } else {
+                // silent fail, this Solveur nub was probably loaded before all the candidate nubs are done loading
+            }
+        }
+    }
+
+    protected initSelectedValue() {
+        const ntc = (this.nub as Solveur).nubToCalculate;
+        if (ntc !== undefined) {
+            this.setValueFromId(this._entriesBaseId + ntc.uid);
+        }
+    }
+
+    public get associatedProperty(): string {
+        return undefined;
+    }
+
+    public get configDefaultValue(): string {
+        return undefined;
+    }
+
+    public updateLocalisation() {
+        // do not override localisation done in populate()
+        // ie. avoid what is done by SelectField.updateLocalisation()
+    }
+}
\ No newline at end of file
diff --git a/src/app/formulaire/elements/select/select-field-species-list.ts b/src/app/formulaire/elements/select/select-field-species-list.ts
new file mode 100644
index 000000000..bcdbbee79
--- /dev/null
+++ b/src/app/formulaire/elements/select/select-field-species-list.ts
@@ -0,0 +1,73 @@
+/*
+"type": "select_custom",
+"id": "select_species_list",
+"source": "verificateur_species",
+"multiple": true
+*/
+
+import { ServiceFactory } from "app/services/service-factory";
+import { CalculatorType, FishSpecies, Session, Verificateur } from "jalhyd";
+import { sprintf } from "sprintf-js";
+import { FormulaireNode } from "../formulaire-node";
+import { SelectEntry } from "./select-entry";
+import { SelectField } from "./select-field";
+
+// Vérificateur, liste d'espèces (choix multiple)
+export class SelectFieldSpeciesList extends SelectField {
+    constructor(parent: FormulaireNode) {
+        super(parent);
+        this._multiple = true;
+    }
+
+    protected populate() {
+        // add UIDs of all Espece type Nubs in the session
+        const especeNubs = Session.getInstance().getAllNubs().filter((element) => element.calcType === CalculatorType.Espece);
+        for (const en of especeNubs) {
+            const form = ServiceFactory.formulaireService.getFormulaireFromNubId(en.uid);
+            this.addEntry(
+                new SelectEntry(
+                    this._entriesBaseId + en.uid,
+                    en.uid,
+                    sprintf(
+                        ServiceFactory.i18nService.localizeText("INFO_VERIFICATEUR_CUSTOM_SPECIES"),
+                        form ? form.calculatorName : "*** form not loaded yet ***"
+                    )
+                )
+            );
+        }
+        // add all FishSpecies
+        for (let j = 1; j < Object.keys(FishSpecies).length / 2; j++) { // exclude "0" (SPECIES_CUSTOM)
+            const spgId = FishSpecies[j].substring(FishSpecies[j].lastIndexOf("_") + 1);
+            this.addEntry(
+                new SelectEntry(
+                    this._entriesBaseId + spgId,
+                    FishSpecies[j],
+                    ServiceFactory.i18nService.localizeText("INFO_ENUM_" + FishSpecies[j])
+                )
+            );
+        }
+    }
+
+    protected initSelectedValue() {
+        const sl = (this.nub as Verificateur).speciesList;
+        if (sl !== undefined) {
+            this.setValueFromId(sl.map((s) => {
+                const spgId = s.substring(s.lastIndexOf("_") + 1);
+                return this._entriesBaseId + spgId;
+            }));
+        }
+    }
+
+    public get associatedProperty(): string {
+        return undefined;
+    }
+
+    public get configDefaultValue(): string {
+        return undefined;
+    }
+
+    public updateLocalisation() {
+        // do not override localisation done in populate()
+        // ie. avoid what is done by SelectField.updateLocalisation()
+    }
+}
\ No newline at end of file
diff --git a/src/app/formulaire/elements/select/select-field-target-pass.ts b/src/app/formulaire/elements/select/select-field-target-pass.ts
new file mode 100644
index 000000000..36430d217
--- /dev/null
+++ b/src/app/formulaire/elements/select/select-field-target-pass.ts
@@ -0,0 +1,62 @@
+import { ServiceFactory } from "app/services/service-factory";
+import { decodeHtml } from "app/util";
+import { CalculatorType, FishPass, Session, Verificateur } from "jalhyd";
+import { FormulaireElement } from "../formulaire-element";
+import { FormulaireNode } from "../formulaire-node";
+import { SelectEntry } from "./select-entry";
+import { SelectField } from "./select-field";
+
+/*
+    "id": "select_target_pass",
+    "type": "select_custom",
+    "source": "verificateur_target",
+    "messageWhenEmpty": "INFO_VERIF_CREATE_PASS_FRIST"
+*/
+
+// Vérificateur, passe cible (à vérifier)
+export class SelectFieldTargetPass extends SelectField {
+    constructor(parent: FormulaireNode) {
+        super(parent);
+        this._messageWhenEmpty = "INFO_VERIF_CREATE_PASS_FRIST";
+    }
+
+    protected populate() {
+        const fs = ServiceFactory.formulaireService;
+        // find all Nubs of type FishPass
+        const candidateNubs: any[] = Session.getInstance().getAllNubs().filter((element) => {
+            return (
+                (element instanceof FishPass)
+                && element.calcType !== CalculatorType.Par // ParSimulation extends Par @TODO find something better
+            );
+        });
+        for (const cn of candidateNubs) {
+            const nub = fs.getFormulaireFromId(cn.uid);
+            if (nub) {
+                const label = nub.calculatorName + " (" + fs.getLocalisedTitleFromCalculatorType(nub.calculatorType) + ")";
+                this.addEntry(new SelectEntry(this._entriesBaseId + cn.uid, cn.uid, decodeHtml(label)));
+            } else {
+                // silent fail, this Verificateur nub was probably loaded before all the candidate nubs are done loading
+            }
+        }
+    }
+
+    protected initSelectedValue() {
+        const ntv = (this.nub as Verificateur).nubToVerify;
+        if (ntv !== undefined) {
+            this.setValueFromId(this._entriesBaseId + ntv.uid);
+        }
+    }
+
+    public get associatedProperty(): string {
+        return undefined;
+    }
+
+    public get configDefaultValue(): string {
+        return undefined;
+    }
+
+    public updateLocalisation() {
+        // do not override localisation done in populate()
+        // ie. avoid what is done by SelectField.updateLocalisation()
+    }
+}
\ No newline at end of file
diff --git a/src/app/formulaire/elements/select/select-field-upstream-basin.ts b/src/app/formulaire/elements/select/select-field-upstream-basin.ts
new file mode 100644
index 000000000..06e335f5c
--- /dev/null
+++ b/src/app/formulaire/elements/select/select-field-upstream-basin.ts
@@ -0,0 +1,53 @@
+import { ServiceFactory } from "app/services/service-factory";
+import { PbCloison, PreBarrage } from "jalhyd";
+import { SelectEntry } from "./select-entry";
+import { SelectField } from "./select-field";
+
+/*
+    "id": "select_upstream_basin",
+    "type": "select_custom",
+    "source": "upstream_basin"
+*/
+
+// PbCloisons, bassin amont
+export class SelectFieldUpstreamBasin extends SelectField {
+    protected populate() {
+        const pbWallU = this.parentForm.currentNub as PbCloison;
+        const preBarrageU = pbWallU.parent as PreBarrage;
+        const posDb = pbWallU.bassinAval?.findPositionInParent();
+        // river upstream
+        this.addEntry(
+            new SelectEntry(
+                this._entriesBaseId + "none",
+                undefined,
+                ServiceFactory.i18nService.localizeText("INFO_LIB_AMONT")
+            )
+        );
+        // all available basins, depending on current downstream basin
+        for (const b of preBarrageU.bassins) {
+            const pos = b.findPositionInParent();
+            if (posDb === undefined || pos < posDb) {
+                this.addEntry(
+                    new SelectEntry(
+                        this._entriesBaseId + b.uid,
+                        b.uid,
+                        ServiceFactory.i18nService.localizeMessage(b.description)
+                    )
+                );
+            }
+        }
+    }
+
+    protected initSelectedValue() {
+        const ub = (this.nub as PbCloison).bassinAmont;
+        this.setValueFromId(this._entriesBaseId + (ub ? ub.uid : "none"));
+    }
+
+    public get associatedProperty(): string {
+        return undefined;
+    }
+
+    public get configDefaultValue(): string {
+        return undefined;
+    }
+}
\ No newline at end of file
diff --git a/src/app/formulaire/elements/select/select-field.ts b/src/app/formulaire/elements/select/select-field.ts
new file mode 100644
index 000000000..c6e2024ed
--- /dev/null
+++ b/src/app/formulaire/elements/select/select-field.ts
@@ -0,0 +1,268 @@
+import { Field } from "../field";
+import { SelectEntry } from "./select-entry";
+import { arraysAreEqual } from "../../../util";
+import { FormulaireNode } from "../formulaire-node";
+import { ServiceFactory } from "app/services/service-factory";
+import { FormulaireDefinition } from "../../definition/form-definition";
+import { Nub } from "jalhyd";
+
+export abstract class SelectField extends Field {
+
+    /**
+     * select options
+     */
+    protected _entries: SelectEntry[];
+
+    /**
+     * currently selected option
+     */
+    protected _selectedEntry: SelectEntry | SelectEntry[];
+
+    /** if true, user can select multiple values */
+    protected _multiple = false;
+
+    /** if true, select box is grayed out */
+    public disabled = false;
+
+    /** string to build the select entries IDs from */
+    protected _entriesBaseId: string;
+
+    /** name of the Nub property associated to this field, if any */
+    protected _associatedProperty: string;
+
+    /** default value (from configuration) for this field */
+    protected _configDefaultValue: string;
+
+    /**
+     * message to display below the select field when no option is selected,
+     * if this message is defined and not empty
+     */
+    protected _messageWhenEmpty: string;
+
+    constructor(parent: FormulaireNode) {
+        super(parent);
+        this.clearEntries();
+    }
+
+    /**
+     * associated nub
+     */
+    protected get nub(): Nub {
+        return (this.parentForm as FormulaireDefinition).currentNub;
+    }
+
+    public getValue(): SelectEntry | SelectEntry[] {
+        return this._selectedEntry;
+    }
+
+    /**
+     * Updates _selectedEntry; notifies observers only if
+     * value.id has changed
+     */
+    public setValue(v: SelectEntry | SelectEntry[]) {
+        // if multiple is true, value must be an array
+        if (this._multiple && !Array.isArray(v)) {
+            throw new Error("select value is not an array");
+        }
+
+        const previousSelectedEntry = this._selectedEntry;
+        this._selectedEntry = v;
+
+        // if old and new values are not both undefined
+        if (!(previousSelectedEntry === undefined && v === undefined)) {
+            // if value changed
+            const valueChanged = (
+                (previousSelectedEntry === undefined && v !== undefined)
+                || (
+                    !Array.isArray(previousSelectedEntry)
+                    && !Array.isArray(v)
+                    && previousSelectedEntry.id !== v.id
+                )
+                || (
+                    Array.isArray(previousSelectedEntry)
+                    && Array.isArray(v)
+                    && !arraysAreEqual(previousSelectedEntry, v, "id", true)
+                )
+            );
+            if (valueChanged) {
+                this.notifyValueChanged();
+            }
+        }
+    }
+
+    public notifyValueChanged() {
+        this.notifyObservers({
+            "action": "select",
+            "value": this._selectedEntry
+        }, this);
+    }
+
+    /**
+     * Sets value from given entry id(s); if it was not found, sets the
+     * first available entry as selectedValue
+     */
+    protected setValueFromId(id: string | string[]) {
+        let found = false;
+        const entries = [];
+        if (Array.isArray(id)) {
+            for (const e of this._entries) {
+                if (id.includes(e.id)) {
+                    entries.push(e);
+                    found = true;
+                }
+            }
+            this.setValue(entries);
+        } else {
+            for (const e of this._entries) {
+                if (e.id === id) {
+                    found = true;
+                    this.setValue(e);
+                }
+            }
+        }
+        if (!found) {
+            this.findAndSetDefaultValue();
+        }
+    }
+
+    /**
+     * try to find a default value to select
+     */
+    protected findAndSetDefaultValue() {
+        // default to first available entry if any
+        if (this._entries.length > 0) {
+            if (this._multiple) {
+                this.setValue([this._entries[0]]);
+            } else {
+                this.setValue(this._entries[0]);
+            }
+        } else {
+            // notify observers that no value is selected anymore
+            this.notifyValueChanged();
+        }
+    }
+
+    public parseConfig(field: {}, data?: {}) {
+        this._confId = field["id"];
+        this._entriesBaseId = this._confId + "_";
+        this._helpLink = field["help"];
+        this.afterParseConfig();
+    }
+
+    /**
+     * Once config is parsed, init original value from model
+     * (needs config for this._entriesBaseId to be set).
+     * Triggered at the end of parseConfig()
+     */
+    protected afterParseConfig() {
+        this.populate();
+        this.initSelectedValue();
+    }
+
+    /**
+     * fill select with options
+     */
+    protected abstract populate();
+
+    /**
+     * initialise select (loads UI with the value held by the model)
+     */
+    protected abstract initSelectedValue();
+
+    /**
+     * associated nub property
+     */
+    public get associatedProperty(): string {
+        return this._associatedProperty;
+    }
+
+    /**
+     * default value from configuration
+     */
+    public get configDefaultValue(): string {
+        return this._configDefaultValue;
+    }
+
+    private clearEntries() {
+        this._entries = [];
+    }
+
+    public get entries() {
+        return this._entries;
+    }
+
+    protected addEntry(e: SelectEntry) {
+        this._entries.push(e);
+    }
+
+    public getEntryFromValue(val: any): SelectEntry {
+        for (const se of this._entries) {
+            if (se.value === val) {
+                return se;
+            }
+        }
+    }
+
+    /**
+    * Reloads available entries, trying to keep the current selected
+    * value; does not notify observers if value did not change
+    */
+    public updateEntries() {
+        // store previous selected entry
+        const pse = this._selectedEntry;
+        // empty
+        this.clearEntries();
+        // populate
+        this.populate();
+        this.updateLocalisation();
+        // if no entry is available anymore, unset value
+        if (this._entries.length === 0) {
+            if (this._multiple) {
+                this.setValue([]);
+            } else {
+                this.setValue(undefined);
+            }
+        } else {
+            // keep previously selected entry(ies) if possible
+            if (pse) {
+                if (!Array.isArray(pse) && pse.id) {
+                    this.setValueFromId(pse.id);
+                } else if (Array.isArray(pse) && pse.length > 0) {
+                    this.setValueFromId(pse.map((e) => e.id));
+                } else {
+                    this.findAndSetDefaultValue();
+                }
+            } else {
+                this.findAndSetDefaultValue();
+            }
+        }
+    }
+
+    public get entriesBaseId(): string {
+        return this._entriesBaseId;
+    }
+
+    public get messageWhenEmpty(): string {
+        let msg: string;
+        if (this._selectedEntry === undefined && this._messageWhenEmpty) {
+            msg = ServiceFactory.i18nService.localizeText(this._messageWhenEmpty);
+        }
+        return msg;
+    }
+
+    public get multiple(): boolean {
+        return this._multiple;
+    }
+
+    public updateLocalisation() {
+        super.updateLocalisation();
+        for (const e of this._entries) {
+            const aId = e.id.split("_");
+            const trad = ServiceFactory.formulaireService.localizeText(
+                `${aId[1].toUpperCase()}_${aId[2]}`,
+                this.parentForm.currentNub.calcType
+            );
+            e.label = trad;
+        }
+    }
+}
diff --git a/src/app/services/formulaire.service.ts b/src/app/services/formulaire.service.ts
index 4c9e42baa..83020d527 100644
--- a/src/app/services/formulaire.service.ts
+++ b/src/app/services/formulaire.service.ts
@@ -32,7 +32,7 @@ import { NotificationsService } from "./notifications.service";
 import { FormulaireDefinition } from "../formulaire/definition/form-definition";
 import { FormulaireElement } from "../formulaire/elements/formulaire-element";
 import { InputField } from "../formulaire/elements/input-field";
-import { SelectField } from "../formulaire/elements/select-field";
+import { SelectField } from "../formulaire/elements/select/select-field";
 import { FormulaireSectionParametree } from "../formulaire/definition/form-section-parametree";
 import { FormulaireCourbeRemous } from "../formulaire/definition/form-courbe-remous";
 import { FormulaireParallelStructure } from "../formulaire/definition/form-parallel-structures";
-- 
GitLab


From faa6e21cc9e3f6a310fee8759ed222e89b680f03 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Mon, 17 Oct 2022 14:14:43 +0200
Subject: [PATCH 05/19] refactor: select component: remove unused disabled flag

refs #483
---
 .../components/generic-select/generic-select.component.html   | 3 +--
 .../select-field-line/select-field-line.component.ts          | 4 ----
 src/app/formulaire/elements/select/select-field.ts            | 3 ---
 3 files changed, 1 insertion(+), 9 deletions(-)

diff --git a/src/app/components/generic-select/generic-select.component.html b/src/app/components/generic-select/generic-select.component.html
index 7ce918d76..0a75a714e 100644
--- a/src/app/components/generic-select/generic-select.component.html
+++ b/src/app/components/generic-select/generic-select.component.html
@@ -1,6 +1,5 @@
 <mat-form-field>
-    <mat-select [id]="selectId" [placeholder]="label" [(value)]="selectedValue" [multiple]="isMultiple"
-        [disabled]="isDisabled">
+    <mat-select [id]="selectId" [placeholder]="label" [(value)]="selectedValue" [multiple]="isMultiple">
         <mat-select-trigger *ngIf="isMultiple">
             {{ selectedValue && selectedValue[0] ? entryLabel(selectedValue[0]) : '' }}
             <span *ngIf="selectedValue?.length > 1" class="multiple-selection-label">
diff --git a/src/app/components/select-field-line/select-field-line.component.ts b/src/app/components/select-field-line/select-field-line.component.ts
index 2b8cb3a03..c6b2474a0 100644
--- a/src/app/components/select-field-line/select-field-line.component.ts
+++ b/src/app/components/select-field-line/select-field-line.component.ts
@@ -34,10 +34,6 @@ export class SelectFieldLineComponent implements OnInit {
         return this._select.multiple;
     }
 
-    public get isDisabled(): boolean {
-        return this._select.disabled;
-    }
-
     public get entries(): SelectEntry[] {
         if (! this._select) {
             return [];
diff --git a/src/app/formulaire/elements/select/select-field.ts b/src/app/formulaire/elements/select/select-field.ts
index c6e2024ed..e90449882 100644
--- a/src/app/formulaire/elements/select/select-field.ts
+++ b/src/app/formulaire/elements/select/select-field.ts
@@ -21,9 +21,6 @@ export abstract class SelectField extends Field {
     /** if true, user can select multiple values */
     protected _multiple = false;
 
-    /** if true, select box is grayed out */
-    public disabled = false;
-
     /** string to build the select entries IDs from */
     protected _entriesBaseId: string;
 
-- 
GitLab


From 9a6ca6cfaf569f4b054f8449aac170a2c66d1ab4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Mon, 17 Oct 2022 16:01:19 +0200
Subject: [PATCH 06/19] test(e2e): check that select default value from
 configuration is honored

refs #483
---
 e2e/select-default-value.e2e-spec.ts | 42 ++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)
 create mode 100644 e2e/select-default-value.e2e-spec.ts

diff --git a/e2e/select-default-value.e2e-spec.ts b/e2e/select-default-value.e2e-spec.ts
new file mode 100644
index 000000000..8796f19ac
--- /dev/null
+++ b/e2e/select-default-value.e2e-spec.ts
@@ -0,0 +1,42 @@
+import { browser } from "protractor";
+import { CalculatorPage } from "./calculator.po";
+import { ListPage } from "./list.po";
+import { Navbar } from "./navbar.po";
+import { PreferencesPage } from "./preferences.po";
+
+describe("check the select default value - ", () => {
+    let prefPage: PreferencesPage;
+    let listPage: ListPage;
+    let calcPage: CalculatorPage;
+    let navBar: Navbar;
+
+    beforeAll(() => {
+        prefPage = new PreferencesPage();
+        listPage = new ListPage();
+        calcPage = new CalculatorPage();
+        navBar = new Navbar();
+    });
+
+    beforeEach(async () => {
+        // disable evil option "empty fields on module creation"
+        await prefPage.navigateTo();
+        await prefPage.disableEvilEmptyFields();
+        await browser.sleep(200);
+
+        await prefPage.changeLanguage(1); // fr
+        await browser.sleep(200);
+    });
+
+    it("in the 'backwater curve' calculator", async () => {
+        // open backwater curve calculator
+        await navBar.clickNewCalculatorButton();
+        await listPage.clickMenuEntryForCalcType(4);
+        await browser.sleep(200);
+
+        // in the calculator configuration file, the default resolution method is 'Trapezes'.
+        // let's check this...
+        const sel_section = calcPage.getSelectById("select_resolution");
+        const val = await calcPage.getSelectValueText(sel_section);
+        expect(val).toBe("Intégration par trapèzes");
+    });
+});
-- 
GitLab


From bfdae695d130d2aecce8456da0597063a13f3d70 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Tue, 18 Oct 2022 09:51:01 +0200
Subject: [PATCH 07/19] fix: select default from configuration value not
 honored

refs #483
---
 src/app/formulaire/elements/fieldset.ts | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/app/formulaire/elements/fieldset.ts b/src/app/formulaire/elements/fieldset.ts
index 4a67910e4..1190098be 100644
--- a/src/app/formulaire/elements/fieldset.ts
+++ b/src/app/formulaire/elements/fieldset.ts
@@ -309,8 +309,6 @@ export class FieldSet extends FormulaireElement implements Observer {
                 }
             }
         }
-
-        this.updateFields();
     }
 
     public getNodeParameter(symbol: string): NgParameter {
-- 
GitLab


From f5eba4f51b651ce8f6ac61432b15a3ed9a91b51d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Mon, 17 Oct 2022 16:27:09 +0200
Subject: [PATCH 08/19] refactor: SelectFieldNubProperty: move configuration
 parsing out of constructor

refs #483
---
 src/app/formulaire/elements/fieldset.ts                   | 1 +
 .../formulaire/elements/select/select-field-factory.ts    | 4 ++--
 .../formulaire/elements/select/select-field-nub-prop.ts   | 8 ++++++--
 src/app/formulaire/elements/select/select-field.ts        | 4 ++--
 4 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/src/app/formulaire/elements/fieldset.ts b/src/app/formulaire/elements/fieldset.ts
index 1190098be..89a696bf0 100644
--- a/src/app/formulaire/elements/fieldset.ts
+++ b/src/app/formulaire/elements/fieldset.ts
@@ -111,6 +111,7 @@ export class FieldSet extends FormulaireElement implements Observer {
     private parse_select(json: {}): SelectField {
         const res: SelectField = SelectFieldFactory.newSelectField(json, this);
         res.parseConfig(json);
+        res.afterParseConfig();
         res.addObserver(this);
         return res;
     }
diff --git a/src/app/formulaire/elements/select/select-field-factory.ts b/src/app/formulaire/elements/select/select-field-factory.ts
index 38e8e7b49..c7ae60316 100644
--- a/src/app/formulaire/elements/select/select-field-factory.ts
+++ b/src/app/formulaire/elements/select/select-field-factory.ts
@@ -57,10 +57,10 @@ export class SelectFieldFactory {
             case "select_section":
             case "select_sppoperation":
             case "select_unit":
-                return new SelectFieldNubProperty(parent, json);
+                return new SelectFieldNubProperty(parent);
 
             default:
                 throw new Error("unknown select id ${id}");
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/app/formulaire/elements/select/select-field-nub-prop.ts b/src/app/formulaire/elements/select/select-field-nub-prop.ts
index eca0463b3..df62610a8 100644
--- a/src/app/formulaire/elements/select/select-field-nub-prop.ts
+++ b/src/app/formulaire/elements/select/select-field-nub-prop.ts
@@ -15,8 +15,12 @@ import { SelectField } from "./select-field";
 // nub property values taken from an enum
 // "standard" select that normally does not require customisation
 export class SelectFieldNubProperty extends SelectField {
-    constructor(parent: FormulaireNode, json: {}) {
+    constructor(parent: FormulaireNode) {
         super(parent);
+    }
+
+    public parseConfig(json: {}, data?: {}) {
+        super.parseConfig(json, data);
         this._associatedProperty = json["property"];
         this._configDefaultValue = json["default"];
     }
@@ -35,4 +39,4 @@ export class SelectFieldNubProperty extends SelectField {
     protected initSelectedValue() {
         this.findAndSetDefaultValue();
     }
-}
\ No newline at end of file
+}
diff --git a/src/app/formulaire/elements/select/select-field.ts b/src/app/formulaire/elements/select/select-field.ts
index e90449882..435d042f3 100644
--- a/src/app/formulaire/elements/select/select-field.ts
+++ b/src/app/formulaire/elements/select/select-field.ts
@@ -143,7 +143,7 @@ export abstract class SelectField extends Field {
         this._confId = field["id"];
         this._entriesBaseId = this._confId + "_";
         this._helpLink = field["help"];
-        this.afterParseConfig();
+        //this.afterParseConfig(); // done in FieldSet.parse_select()
     }
 
     /**
@@ -151,7 +151,7 @@ export abstract class SelectField extends Field {
      * (needs config for this._entriesBaseId to be set).
      * Triggered at the end of parseConfig()
      */
-    protected afterParseConfig() {
+    public afterParseConfig() {
         this.populate();
         this.initSelectedValue();
     }
-- 
GitLab


From 1930e4d61c838777bf245674f1d8bf69993eca4e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Tue, 18 Oct 2022 17:19:50 +0200
Subject: [PATCH 09/19] refactor: calculator JSON configuration: remove
 selectIds, customSelectIds

refs #483
---
 src/app/calculators/bief/config.json          |  1 -
 src/app/calculators/cloisons/config.json      |  1 -
 src/app/calculators/courberemous/config.json  |  1 -
 src/app/calculators/dever/config.json         |  1 -
 src/app/calculators/espece/config.json        |  1 -
 src/app/calculators/grille/config.json        |  1 -
 src/app/calculators/lechaptcalmon/config.json |  1 -
 .../calculators/macrorugocompound/config.json |  1 -
 src/app/calculators/par/config.json           |  1 -
 .../calculators/parallelstructure/config.json |  1 -
 src/app/calculators/parsimulation/config.json |  1 -
 src/app/calculators/prebarrage/config.json    | 14 -----
 .../calculators/regimeuniforme/config.json    |  1 -
 .../calculators/sectionparametree/config.json |  1 -
 src/app/calculators/solveur/config.json       |  2 -
 src/app/calculators/spp/config.json           |  1 -
 src/app/calculators/trigo/config.json         |  1 -
 src/app/calculators/verificateur/config.json  |  2 -
 .../formulaire/definition/form-definition.ts  |  8 +++
 .../formulaire/definition/form-fixedvar.ts    | 34 ++-----------
 src/app/formulaire/elements/fieldset.ts       | 51 +++++++++----------
 .../select/select-field-device-loi-debit.ts   | 16 +++---
 .../select-field-device-structure-type.ts     | 16 +++---
 .../select/select-field-downstream-basin.ts   |  8 ---
 .../select/select-field-remous-target.ts      | 16 +++---
 .../select/select-field-searched-param.ts     |  8 ---
 .../select-field-solver-targeted-result.ts    | 15 +++---
 .../select/select-field-solveur-target.ts     | 10 +---
 .../select/select-field-species-list.ts       | 10 +---
 .../select/select-field-target-pass.ts        | 10 +---
 .../select/select-field-upstream-basin.ts     | 10 +---
 .../elements/select/select-field.ts           |  7 +++
 .../deep-selectfield-iterator.ts              | 19 +++++++
 33 files changed, 91 insertions(+), 180 deletions(-)
 create mode 100644 src/app/formulaire/form-iterator/deep-selectfield-iterator.ts

diff --git a/src/app/calculators/bief/config.json b/src/app/calculators/bief/config.json
index 7434f2abc..584efa828 100644
--- a/src/app/calculators/bief/config.json
+++ b/src/app/calculators/bief/config.json
@@ -69,7 +69,6 @@
     {
         "type": "options",
         "defaultNodeType": "SectionRectangle",
-        "selectIds": [ "select_section", "select_regime" ],
         "help": "hsl/cote_amont_aval.html"
     }
 ]
diff --git a/src/app/calculators/cloisons/config.json b/src/app/calculators/cloisons/config.json
index 2423ef4bc..e3b2a662d 100644
--- a/src/app/calculators/cloisons/config.json
+++ b/src/app/calculators/cloisons/config.json
@@ -61,7 +61,6 @@
     },
     {
         "type": "options",
-        "selectIds": [ "select_structure", "select_loidebit" ],
         "help": "pab/cloisons.html"
     }
 ]
diff --git a/src/app/calculators/courberemous/config.json b/src/app/calculators/courberemous/config.json
index b66d709bb..30bab2879 100644
--- a/src/app/calculators/courberemous/config.json
+++ b/src/app/calculators/courberemous/config.json
@@ -96,7 +96,6 @@
     {
         "type": "options",
         "defaultNodeType": "SectionRectangle",
-        "selectIds": [ "select_resolution", "select_section", "select_target" ],
         "help": "hsl/courbe_remous.html",
         "resultsHelp": {
             "B": "hsl/section_parametree.html#largeur-au-miroir-surface-et-perimetre-mouille",
diff --git a/src/app/calculators/dever/config.json b/src/app/calculators/dever/config.json
index 6b502f27a..69188b972 100644
--- a/src/app/calculators/dever/config.json
+++ b/src/app/calculators/dever/config.json
@@ -51,7 +51,6 @@
     },
     {
         "type": "options",
-        "selectIds": [ "select_structure", "select_loidebit" ],
         "help": "structures/dever.html"
     }
 ]
\ No newline at end of file
diff --git a/src/app/calculators/espece/config.json b/src/app/calculators/espece/config.json
index 1b6185a5e..71a6f42f7 100644
--- a/src/app/calculators/espece/config.json
+++ b/src/app/calculators/espece/config.json
@@ -94,7 +94,6 @@
     {
         "type": "options",
         "help": "verif/especes_predefinies.html",
-        "selectIds": [ "select_divingjetsupported" ],
         "calculateDisabled": true
     }
 ]
diff --git a/src/app/calculators/grille/config.json b/src/app/calculators/grille/config.json
index e964fa9e3..b01502d19 100644
--- a/src/app/calculators/grille/config.json
+++ b/src/app/calculators/grille/config.json
@@ -96,7 +96,6 @@
     },
     {
         "type": "options",
-        "selectIds": [ "select_gridtype", "select_gridprofile" ],
         "help": "devalaison/grille.html",
         "resultsHelp": {
             "VAPDG": "devalaison/grille.html#vitesse-dapproche-moyenne-pour-le-debit-maximum-turbine-en-soustrayant-la-partie-superieure-eventuellement-obturee",
diff --git a/src/app/calculators/lechaptcalmon/config.json b/src/app/calculators/lechaptcalmon/config.json
index 431552de8..d655d5ca3 100644
--- a/src/app/calculators/lechaptcalmon/config.json
+++ b/src/app/calculators/lechaptcalmon/config.json
@@ -28,7 +28,6 @@
     },
     {
         "type": "options",
-        "selectIds": [ "select_material" ],
         "help": "hyd_en_charge/lechapt-calmon.html"
     }
 ]
\ No newline at end of file
diff --git a/src/app/calculators/macrorugocompound/config.json b/src/app/calculators/macrorugocompound/config.json
index 8df7adca3..79290dc66 100644
--- a/src/app/calculators/macrorugocompound/config.json
+++ b/src/app/calculators/macrorugocompound/config.json
@@ -78,7 +78,6 @@
     },
     {
         "type": "options",
-        "selectIds": [ "select_passtype" ],
         "help": "pam/macrorugo_complexe.html"
     }
 ]
diff --git a/src/app/calculators/par/config.json b/src/app/calculators/par/config.json
index dc6fcb41e..c7ba05c46 100644
--- a/src/app/calculators/par/config.json
+++ b/src/app/calculators/par/config.json
@@ -38,7 +38,6 @@
     },
     {
         "type": "options",
-        "selectIds": [ "select_partype" ],
         "help": "par/calage.html"
     }
 ]
diff --git a/src/app/calculators/parallelstructure/config.json b/src/app/calculators/parallelstructure/config.json
index d265d9a66..87ef99908 100644
--- a/src/app/calculators/parallelstructure/config.json
+++ b/src/app/calculators/parallelstructure/config.json
@@ -74,7 +74,6 @@
     },
     {
         "type": "options",
-        "selectIds": [ "select_structure", "select_loidebit" ],
         "help": "structures/lois_ouvrages.html",
         "resultsHelp": {
             "ENUM_StructureJetType": "structures/lois_ouvrages.html#type-de-jet"
diff --git a/src/app/calculators/parsimulation/config.json b/src/app/calculators/parsimulation/config.json
index a5cb2c9de..810625bf6 100644
--- a/src/app/calculators/parsimulation/config.json
+++ b/src/app/calculators/parsimulation/config.json
@@ -54,7 +54,6 @@
     },
     {
         "type": "options",
-        "selectIds": [ "select_partype" ],
         "help": "par/simulation.html"
     }
 ]
diff --git a/src/app/calculators/prebarrage/config.json b/src/app/calculators/prebarrage/config.json
index 1b0431549..406711a5e 100644
--- a/src/app/calculators/prebarrage/config.json
+++ b/src/app/calculators/prebarrage/config.json
@@ -15,10 +15,6 @@
                     "Z1",
                     "Z2"
                 ]
-            },
-            {
-                "type": "options",
-                "selectIds": [ ]
             }
         ]
     },
@@ -41,10 +37,6 @@
                 "templates": [
                     "fs_basin"
                 ]
-            },
-            {
-                "type": "options",
-                "selectIds": [ ]
             }
         ]
     },
@@ -181,17 +173,11 @@
                 "templates": [
                     "fs_ouvrage"
                 ]
-            },
-            {
-                "type": "options",
-                "selectIds": [ "select_structure", "select_loidebit" ],
-                "customSelectIds": [ "select_upstream_basin", "select_downstream_basin" ]
             }
         ]
     },
     {
         "type": "options",
-        "selectIds": [ ],
         "upstreamSelectId": "select_upstream",
         "downstreamSelectId": "select_downstream",
         "help": "pab/prebarrage.html"
diff --git a/src/app/calculators/regimeuniforme/config.json b/src/app/calculators/regimeuniforme/config.json
index 14a3239ef..171dc0139 100644
--- a/src/app/calculators/regimeuniforme/config.json
+++ b/src/app/calculators/regimeuniforme/config.json
@@ -50,7 +50,6 @@
     {
         "type": "options",
         "defaultNodeType": "SectionRectangle",
-        "selectIds": [ "select_section" ],
         "help": "hsl/regime_uniforme.html",
         "resultsHelp": {
             "V": "hsl/section_parametree.html#la-vitesse-moyenne-ms"
diff --git a/src/app/calculators/sectionparametree/config.json b/src/app/calculators/sectionparametree/config.json
index f24b88ca8..4e4ce5e28 100644
--- a/src/app/calculators/sectionparametree/config.json
+++ b/src/app/calculators/sectionparametree/config.json
@@ -50,7 +50,6 @@
     {
         "type": "options",
         "defaultNodeType": "SectionRectangle",
-        "selectIds": [ "select_section" ],
         "help": "hsl/section_parametree.html",
         "resultsHelp": {
             "B": "hsl/section_parametree.html#largeur-au-miroir-surface-et-perimetre-mouille",
diff --git a/src/app/calculators/solveur/config.json b/src/app/calculators/solveur/config.json
index 99d215264..9c0fd2b31 100644
--- a/src/app/calculators/solveur/config.json
+++ b/src/app/calculators/solveur/config.json
@@ -27,8 +27,6 @@
     },
     {
         "type": "options",
-        "selectIds": [ "select_target_result" ],
-        "customSelectIds": [ "select_target_nub", "select_searched_param" ],
         "targettedResultSelectId": "select_target_result",
         "help": "maths/solver.html"
     }
diff --git a/src/app/calculators/spp/config.json b/src/app/calculators/spp/config.json
index 8506d7c73..e28d38ff8 100644
--- a/src/app/calculators/spp/config.json
+++ b/src/app/calculators/spp/config.json
@@ -31,7 +31,6 @@
     },
     {
         "type": "options",
-        "selectIds": [ "select_sppoperation" ],
         "help": "maths/operators.html#somme-et-produit-de-puissances"
     }
 ]
\ No newline at end of file
diff --git a/src/app/calculators/trigo/config.json b/src/app/calculators/trigo/config.json
index 3b9f5d97a..264fe9299 100644
--- a/src/app/calculators/trigo/config.json
+++ b/src/app/calculators/trigo/config.json
@@ -24,7 +24,6 @@
     },
     {
         "type": "options",
-        "selectIds": [ "select_operation", "select_unit" ],
         "help": "maths/operators.html#fonction-trigonometrique"
     }
 ]
\ No newline at end of file
diff --git a/src/app/calculators/verificateur/config.json b/src/app/calculators/verificateur/config.json
index 07f6f672c..7faef08de 100644
--- a/src/app/calculators/verificateur/config.json
+++ b/src/app/calculators/verificateur/config.json
@@ -21,8 +21,6 @@
     },
     {
         "type": "options",
-        "selectIds": [ "select_pab_jet_type" ],
-        "customSelectIds": [ "select_target_pass", "select_species_list" ],
         "help": "verif/principe.html"
     }
 ]
diff --git a/src/app/formulaire/definition/form-definition.ts b/src/app/formulaire/definition/form-definition.ts
index cc5837883..bcf6eef68 100644
--- a/src/app/formulaire/definition/form-definition.ts
+++ b/src/app/formulaire/definition/form-definition.ts
@@ -26,6 +26,7 @@ import { ServiceFactory } from "../../services/service-factory";
 import { PabTable } from "../elements/pab-table";
 import { SelectEntry } from "../elements/select/select-entry";
 import { SelectField } from "../elements/select/select-field";
+import { DeepSelectFieldIterator } from "../form-iterator/deep-selectfield-iterator";
 
 /**
  * classe de base pour tous les formulaires
@@ -410,6 +411,13 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         return new TopFormulaireElementIterator(this);
     }
 
+    /**
+     * itère sur tous les SelectField
+     */
+    public get allSelectFields(): IterableIterator<SelectField> {
+        return new DeepSelectFieldIterator(this);
+    }
+
     //  interface Observer
 
     public update(sender: any, data: any) {
diff --git a/src/app/formulaire/definition/form-fixedvar.ts b/src/app/formulaire/definition/form-fixedvar.ts
index 9289094a7..0a329f489 100644
--- a/src/app/formulaire/definition/form-fixedvar.ts
+++ b/src/app/formulaire/definition/form-fixedvar.ts
@@ -14,12 +14,6 @@ export class FormulaireFixedVar extends FormulaireDefinition {
     protected _fixedResults: FixedResults;
     protected _varResults: VarResults;
 
-    /** ids of select fields */
-    private _selectIds: string[] = [];
-
-    /** ids of "custom" select fields */
-    private _customSelectIds: string[] = [];
-
     constructor(parent?: FormulaireNode) {
         super(parent);
         this._fixedResults = new FixedResults();
@@ -31,10 +25,6 @@ export class FormulaireFixedVar extends FormulaireDefinition {
         return this._fixedResults;
     }
 
-    public get selectids(): string[] {
-        return this._selectIds;
-    }
-
     public resetFormResults() {
         this._fixedResults.reset();
         this._varResults.reset();
@@ -73,24 +63,14 @@ export class FormulaireFixedVar extends FormulaireDefinition {
 
     public afterParseFieldset(fs: FieldSet) {
         // observe all Select fields @see this.update()
-        if (this._selectIds.length > 0) {
-            for (const sId of this._selectIds) {
-                const sel = fs.getFormulaireNodeById(sId);
-                if (sel) {
-                    // Formulaire is listening to FieldSet properties (@TODO why not directly Select ?)
-                    fs.properties.addObserver(this);
-                }
-            }
-        }
+        fs.properties.addObserver(this);
     }
 
     protected completeParse(firstNotif: boolean = true) {
         super.completeParse(firstNotif);
         // observe all CustomSelect fields @TODO move to afterParseFieldset ?
-        if (this._customSelectIds.length > 0) {
-            for (const csId of this._customSelectIds) {
-                const sel = this.getFormulaireNodeById(csId);
-                // Formulaire is listening to Select value
+        for (const sel of this.allSelectFields) {  // Formulaire is listening to Select value
+            if (!sel.hasAssociatedNubProperty) { // only to "custom" selects
                 sel.addObserver(this);
                 if (firstNotif) {
                     // force 1st observation
@@ -100,14 +80,6 @@ export class FormulaireFixedVar extends FormulaireDefinition {
         }
     }
 
-    protected parseOptions(json: {}) {
-        super.parseOptions(json);
-        // get ids of all select fields
-        this._selectIds = this.getOption(json, "selectIds") || [];
-        // get ids of all "custom" select fields
-        this._customSelectIds = this.getOption(json, "customSelectIds") || [];
-    }
-
     protected compute() {
         this.runNubCalc(this.currentNub);
         this.refreshFieldsets(); // important: before reaffectResultComponents() or it will break results components localization
diff --git a/src/app/formulaire/elements/fieldset.ts b/src/app/formulaire/elements/fieldset.ts
index 89a696bf0..e5fbca7d7 100644
--- a/src/app/formulaire/elements/fieldset.ts
+++ b/src/app/formulaire/elements/fieldset.ts
@@ -15,8 +15,6 @@ import { FieldsetContainer } from "./fieldset-container";
 import { SelectFieldFactory } from "./select/select-field-factory";
 import { FormulaireFixedVar } from "../definition/form-fixedvar";
 import { SelectEntry } from "./select/select-entry";
-import { FormulaireNode } from "./formulaire-node";
-import { ServiceFactory } from "app/services/service-factory";
 
 export class FieldSet extends FormulaireElement implements Observer {
 
@@ -237,9 +235,10 @@ export class FieldSet extends FormulaireElement implements Observer {
         // for all select fields known by the form, set selected value
         // from associated property
         if (this.parentForm instanceof FormulaireFixedVar) {
-            const selectIds = this.parentForm.selectids;
-            for (const sId of selectIds) {
-                this.setSelectValueFromProperty(sId, (this._confId === "fs_section"));
+            for (const sel of this.parentForm.allSelectFields) {
+                if (sel.hasAssociatedNubProperty) {  // ie. if select is a standard select
+                    this.setSelectValueFromProperty(sel.id, (this._confId === "fs_section"));
+                }
             }
         }
     }
@@ -289,13 +288,10 @@ export class FieldSet extends FormulaireElement implements Observer {
         // for all select fields known by the form, apply default value
         // to associated property, usually from associated enum
         if (this.parentForm instanceof FormulaireFixedVar) {
-            const selectIds = this.parentForm.selectids;
-            for (const sId of selectIds) {
-                // find select element in parent form
-                const fe = this.getFormulaireNodeById(sId);
-                if (fe) {
-                    const prop = (fe as SelectField).associatedProperty;
-                    const defaultValue = (fe as SelectField).configDefaultValue;
+            for (const sel of this.parentForm.allSelectFields) {
+                if (sel.hasAssociatedNubProperty) { // ie. if select is a standard select
+                    const prop = sel.associatedProperty;
+                    const defaultValue = sel.configDefaultValue;
                     // Sets Nub default property, unless this property is already set
                     const currentValue = this.properties.getPropValue(prop);
                     if (defaultValue !== undefined && currentValue === undefined) {
@@ -379,28 +375,27 @@ export class FieldSet extends FormulaireElement implements Observer {
                             });
                         }
                     } else {
-                        if (this.parentForm instanceof FormulaireFixedVar) {
-                            // for all select fields known by the form, apply received value
-                            // to associated property
-                            const selectIds = this.parentForm.selectids;
-                            for (const sId of selectIds) {
-                                if (senderId === sId) {
-                                    // find select element in parent form
-                                    const fe = this.parentForm.getFieldById(sId);
-                                    if (fe && data.value !== undefined) {
-                                        const prop = (fe as SelectField).associatedProperty;
-                                        // for multiple select
-                                        if (Array.isArray(data.value)) {
-                                            this.setNubPropValue(prop, data.value.map((v: any) => v.value));
-                                        } else {
-                                            this.setNubPropValue(prop, data.value.value);
+                        if (data.value !== undefined) {
+                            if (this.parentForm instanceof FormulaireFixedVar) {
+                                // for all select fields known by the form, apply received value
+                                // to associated property
+                                for (const sel of this.parentForm.allSelectFields) {
+                                    if (senderId === sel.id) {
+                                        // find select element in parent form
+                                        if (sel.hasAssociatedNubProperty) { // if select is a standard select
+                                            const prop = sel.associatedProperty;
+                                            // for multiple select
+                                            if (Array.isArray(data.value)) {
+                                                this.setNubPropValue(prop, data.value.map((v: any) => v.value));
+                                            } else {
+                                                this.setNubPropValue(prop, data.value.value);
+                                            }
                                         }
                                     }
                                 }
                             }
                         }
                     }
-
                     break; // switch (data.action)
             }
         }
diff --git a/src/app/formulaire/elements/select/select-field-device-loi-debit.ts b/src/app/formulaire/elements/select/select-field-device-loi-debit.ts
index 15194174f..6a9c86e7c 100644
--- a/src/app/formulaire/elements/select/select-field-device-loi-debit.ts
+++ b/src/app/formulaire/elements/select/select-field-device-loi-debit.ts
@@ -1,6 +1,7 @@
 import { LoiDebit, ParallelStructure, StructureProperties, StructureType } from "jalhyd";
 import { SelectField } from "./select-field";
 import { SelectEntry } from "./select-entry";
+import { FormulaireNode } from "../formulaire-node";
 
 /*
     "id": "select_loidebit",
@@ -11,6 +12,11 @@ import { SelectEntry } from "./select-entry";
 */
 
 export class SelectFieldDeviceLoiDebit extends SelectField {
+    constructor(parent: FormulaireNode) {
+        super(parent);
+        this._associatedProperty = "loiDebit";
+    }
+
     protected populate() {
         // possible values depend on CalcType
 
@@ -31,12 +37,4 @@ export class SelectFieldDeviceLoiDebit extends SelectField {
     protected initSelectedValue() {
         this.findAndSetDefaultValue();
     }
-
-    public get associatedProperty(): string {
-        return "loiDebit";
-    }
-
-    public get configDefaultValue(): string {
-        return undefined;
-    }
-}
\ No newline at end of file
+}
diff --git a/src/app/formulaire/elements/select/select-field-device-structure-type.ts b/src/app/formulaire/elements/select/select-field-device-structure-type.ts
index b818eedf5..0210eb1b7 100644
--- a/src/app/formulaire/elements/select/select-field-device-structure-type.ts
+++ b/src/app/formulaire/elements/select/select-field-device-structure-type.ts
@@ -1,6 +1,7 @@
 import { ParallelStructure, StructureType } from "jalhyd";
 import { SelectField } from "./select-field";
 import { SelectEntry } from "./select-entry";
+import { FormulaireNode } from "../formulaire-node";
 
 /*
     "id": "select_structure",
@@ -10,6 +11,11 @@ import { SelectEntry } from "./select-entry";
 */
 
 export class SelectFieldDeviceStructureType extends SelectField {
+    constructor(parent: FormulaireNode) {
+        super(parent);
+        this._associatedProperty = "structureType";
+    }
+
     protected populate() {
         // possible values depend on CalcType
         for (const st in (this.nub as ParallelStructure).getLoisAdmissibles()) {
@@ -21,12 +27,4 @@ export class SelectFieldDeviceStructureType extends SelectField {
     protected initSelectedValue() {
         this.findAndSetDefaultValue();
     }
-
-    public get associatedProperty(): string {
-        return "structureType";
-    }
-
-    public get configDefaultValue(): string {
-        return undefined;
-    }
-}
\ No newline at end of file
+}
diff --git a/src/app/formulaire/elements/select/select-field-downstream-basin.ts b/src/app/formulaire/elements/select/select-field-downstream-basin.ts
index 77fcab4bd..05675917d 100644
--- a/src/app/formulaire/elements/select/select-field-downstream-basin.ts
+++ b/src/app/formulaire/elements/select/select-field-downstream-basin.ts
@@ -42,12 +42,4 @@ export class SelectFieldDownstreamBasin extends SelectField {
         const db = (this.nub as PbCloison).bassinAval;
         this.setValueFromId(this._entriesBaseId + (db ? db.uid : "none"));
     }
-
-    public get associatedProperty(): string {
-        return undefined;
-    }
-
-    public get configDefaultValue(): string {
-        return undefined;
-    }
 }
diff --git a/src/app/formulaire/elements/select/select-field-remous-target.ts b/src/app/formulaire/elements/select/select-field-remous-target.ts
index 6d0f2c506..20b40cc47 100644
--- a/src/app/formulaire/elements/select/select-field-remous-target.ts
+++ b/src/app/formulaire/elements/select/select-field-remous-target.ts
@@ -1,6 +1,7 @@
 import { CourbeRemous } from "jalhyd";
 import { SelectField } from "./select-field";
 import { SelectEntry } from "./select-entry";
+import { FormulaireNode } from "../formulaire-node";
 
 /*
     "id": "select_target",
@@ -11,6 +12,11 @@ import { SelectEntry } from "./select-entry";
 */
 
 export class SelectFieldRemousTarget extends SelectField {
+    constructor(parent: FormulaireNode) {
+        super(parent);
+        this._associatedProperty = "varCalc";
+    }
+
     protected populate() {
         // driven by string[], not enum (easier for variable names)
         this.addEntry(new SelectEntry(this._entriesBaseId + "none", ""));
@@ -23,12 +29,4 @@ export class SelectFieldRemousTarget extends SelectField {
     protected initSelectedValue() {
         this.findAndSetDefaultValue();
     }
-
-    public get associatedProperty(): string {
-        return "varCalc";
-    }
-
-    public get configDefaultValue(): string {
-        return undefined;
-    }
-}
\ No newline at end of file
+}
diff --git a/src/app/formulaire/elements/select/select-field-searched-param.ts b/src/app/formulaire/elements/select/select-field-searched-param.ts
index 5380e1238..1342fb643 100644
--- a/src/app/formulaire/elements/select/select-field-searched-param.ts
+++ b/src/app/formulaire/elements/select/select-field-searched-param.ts
@@ -48,14 +48,6 @@ export class SelectFieldSearchedParam extends SelectField {
         }
     }
 
-    public get associatedProperty(): string {
-        return undefined;
-    }
-
-    public get configDefaultValue(): string {
-        return undefined;
-    }
-
     public updateLocalisation() {
         // do not override localisation done in populate()
         // ie. avoid what is done by SelectField.updateLocalisation()
diff --git a/src/app/formulaire/elements/select/select-field-solver-targeted-result.ts b/src/app/formulaire/elements/select/select-field-solver-targeted-result.ts
index 2b10da827..a547bde3c 100644
--- a/src/app/formulaire/elements/select/select-field-solver-targeted-result.ts
+++ b/src/app/formulaire/elements/select/select-field-solver-targeted-result.ts
@@ -4,6 +4,7 @@ import { Nub, Solveur } from "jalhyd";
 import { FormulaireElement } from "../formulaire-element";
 import { SelectField } from "./select-field";
 import { SelectEntry } from "./select-entry";
+import { FormulaireNode } from "../formulaire-node";
 
 /*
     "id": "select_target_result",
@@ -13,6 +14,10 @@ import { SelectEntry } from "./select-entry";
     "default": ""
 */
 export class SelectFieldSolverTargetedResult extends SelectField {
+    constructor(parent: FormulaireNode) {
+        super(parent);
+        this._associatedProperty = "targettedResult";
+    }
 
     protected populate() {
         // @WARNING for localisation, @see hack in this.updateLocalisation()
@@ -34,14 +39,6 @@ export class SelectFieldSolverTargetedResult extends SelectField {
         this.findAndSetDefaultValue();
     }
 
-    public get configDefaultValue(): string {
-        return "";
-    }
-
-    public get associatedProperty(): string {
-        return "targettedResult";
-    }
-
     public updateLocalisation() {
         FormulaireElement.prototype.updateLocalisation.call(this);
         for (const e of this._entries) {
@@ -61,4 +58,4 @@ export class SelectFieldSolverTargetedResult extends SelectField {
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/app/formulaire/elements/select/select-field-solveur-target.ts b/src/app/formulaire/elements/select/select-field-solveur-target.ts
index 64d37dc33..aecd238b8 100644
--- a/src/app/formulaire/elements/select/select-field-solveur-target.ts
+++ b/src/app/formulaire/elements/select/select-field-solveur-target.ts
@@ -47,16 +47,8 @@ export class SelectFieldSolverTarget extends SelectField {
         }
     }
 
-    public get associatedProperty(): string {
-        return undefined;
-    }
-
-    public get configDefaultValue(): string {
-        return undefined;
-    }
-
     public updateLocalisation() {
         // do not override localisation done in populate()
         // ie. avoid what is done by SelectField.updateLocalisation()
     }
-}
\ No newline at end of file
+}
diff --git a/src/app/formulaire/elements/select/select-field-species-list.ts b/src/app/formulaire/elements/select/select-field-species-list.ts
index bcdbbee79..602a56854 100644
--- a/src/app/formulaire/elements/select/select-field-species-list.ts
+++ b/src/app/formulaire/elements/select/select-field-species-list.ts
@@ -58,16 +58,8 @@ export class SelectFieldSpeciesList extends SelectField {
         }
     }
 
-    public get associatedProperty(): string {
-        return undefined;
-    }
-
-    public get configDefaultValue(): string {
-        return undefined;
-    }
-
     public updateLocalisation() {
         // do not override localisation done in populate()
         // ie. avoid what is done by SelectField.updateLocalisation()
     }
-}
\ No newline at end of file
+}
diff --git a/src/app/formulaire/elements/select/select-field-target-pass.ts b/src/app/formulaire/elements/select/select-field-target-pass.ts
index 36430d217..10cd639a1 100644
--- a/src/app/formulaire/elements/select/select-field-target-pass.ts
+++ b/src/app/formulaire/elements/select/select-field-target-pass.ts
@@ -47,16 +47,8 @@ export class SelectFieldTargetPass extends SelectField {
         }
     }
 
-    public get associatedProperty(): string {
-        return undefined;
-    }
-
-    public get configDefaultValue(): string {
-        return undefined;
-    }
-
     public updateLocalisation() {
         // do not override localisation done in populate()
         // ie. avoid what is done by SelectField.updateLocalisation()
     }
-}
\ No newline at end of file
+}
diff --git a/src/app/formulaire/elements/select/select-field-upstream-basin.ts b/src/app/formulaire/elements/select/select-field-upstream-basin.ts
index 06e335f5c..0eeb6b605 100644
--- a/src/app/formulaire/elements/select/select-field-upstream-basin.ts
+++ b/src/app/formulaire/elements/select/select-field-upstream-basin.ts
@@ -42,12 +42,4 @@ export class SelectFieldUpstreamBasin extends SelectField {
         const ub = (this.nub as PbCloison).bassinAmont;
         this.setValueFromId(this._entriesBaseId + (ub ? ub.uid : "none"));
     }
-
-    public get associatedProperty(): string {
-        return undefined;
-    }
-
-    public get configDefaultValue(): string {
-        return undefined;
-    }
-}
\ No newline at end of file
+}
diff --git a/src/app/formulaire/elements/select/select-field.ts b/src/app/formulaire/elements/select/select-field.ts
index 435d042f3..f67c07404 100644
--- a/src/app/formulaire/elements/select/select-field.ts
+++ b/src/app/formulaire/elements/select/select-field.ts
@@ -173,6 +173,13 @@ export abstract class SelectField extends Field {
         return this._associatedProperty;
     }
 
+    /**
+     * @returns true if select field is associated to a nub property
+     */
+    public get hasAssociatedNubProperty(): boolean {
+        return this._associatedProperty !== undefined;
+    }
+
     /**
      * default value from configuration
      */
diff --git a/src/app/formulaire/form-iterator/deep-selectfield-iterator.ts b/src/app/formulaire/form-iterator/deep-selectfield-iterator.ts
new file mode 100644
index 000000000..6c8547543
--- /dev/null
+++ b/src/app/formulaire/form-iterator/deep-selectfield-iterator.ts
@@ -0,0 +1,19 @@
+import { AbstractFormulaireNodeIterator } from "./abstract-node-iterator";
+import { FormulaireNode } from "../elements/formulaire-node";
+import { SelectField } from "../elements/select/select-field";
+
+/**
+ * itérateur qui extrait récursivement les SelectField dans un tableau de FormulaireElement
+ * (qui peut contenir des FieldsetContainer)
+ */
+export class DeepSelectFieldIterator extends AbstractFormulaireNodeIterator<SelectField> implements IterableIterator<SelectField> {
+    protected isIterable(fe: FormulaireNode) {
+        return fe instanceof SelectField;
+    }
+
+    // interface IterableIterator
+
+    [Symbol.iterator](): IterableIterator<SelectField> {
+        return this;
+    }
+}
-- 
GitLab


From 2ef065a418a1fbb812ff0faaaa8991edebb0e524 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Wed, 19 Oct 2022 17:50:32 +0200
Subject: [PATCH 10/19] refactor: use SelectField derived model for chart type
 component

refs #483
---
 .../results-chart/chart-type.component.ts     | 80 ++++++++-----------
 .../results-chart/results-chart.component.ts  |  9 ++-
 .../elements/select/select-field-charttype.ts | 53 ++++++++++++
 3 files changed, 91 insertions(+), 51 deletions(-)
 create mode 100644 src/app/formulaire/elements/select/select-field-charttype.ts

diff --git a/src/app/components/results-chart/chart-type.component.ts b/src/app/components/results-chart/chart-type.component.ts
index 6ba573a91..39839c3a9 100644
--- a/src/app/components/results-chart/chart-type.component.ts
+++ b/src/app/components/results-chart/chart-type.component.ts
@@ -1,7 +1,10 @@
 import { Component } from "@angular/core";
-import { Observable, IObservable, Observer } from "jalhyd";
+import { IObservable, Observer } from "jalhyd";
 import { I18nService } from "../../services/internationalisation.service";
 import { ChartType } from "../../results/chart-type";
+import { SelectFieldChartType } from "app/formulaire/elements/select/select-field-charttype";
+import { SelectEntry } from "app/formulaire/elements/select/select-entry";
+import { decodeHtml } from "../../util";
 
 @Component({
     selector: "chart-type",
@@ -11,53 +14,46 @@ import { ChartType } from "../../results/chart-type";
     ]
 })
 export class ChartTypeSelectComponent implements IObservable {
-    private _entries: ChartType[] = [ ChartType.Histogram, ChartType.Dots, ChartType.Scatter ];
-    private _entriesLabels: string[] = [];
-    private _selected: ChartType;
-
-    private _observable: Observable;
-
-    public get selectId() {
-        return "chart-type"; // for generic select component
-    }
+    private _select: SelectFieldChartType;
 
     constructor(private intlService: I18nService) {
-        this._observable = new Observable();
-        this._entriesLabels = [
-            this.intlService.localizeText("INFO_PARAMFIELD_CHART_TYPE_HISTOGRAM"),
-            this.intlService.localizeText("INFO_PARAMFIELD_CHART_TYPE_DOTS"),
-            "XY"
-        ];
+        this._select = new SelectFieldChartType(undefined, this.intlService);
+        this._select.afterParseConfig(); // fill entries, set default value
+        this._select.updateEntries();
     }
 
     public get entries(): ChartType[] {
-        return this._entries;
+        return this._select.entryValues;
     }
 
-    protected entryLabel(entry: ChartType): string {
-        const i = this._entries.indexOf(entry);
-        return this._entriesLabels[i];
+    public entryLabel(ct: ChartType): string {
+        const entry = this._select.getEntryFromValue(ct);
+        return decodeHtml(entry.label);
     }
 
     public get selectedValue(): ChartType {
-        return this._selected;
+        const val = this._select.getValue() as SelectEntry;
+        return val.value;
     }
 
-    public set selectedValue(v: ChartType) {
-        if (this._selected !== v) {
-            this._selected = v;
-            this.notifyObservers({
-                "action": "graphTypeChanged",
-                "value": v
-            }, this);
-        }
+    public set selectedValue(ct: ChartType) {
+        const entry = this._select.getEntryFromValue(ct);
+        this._select.setValue(entry);
     }
 
-    public get isMultiple(): boolean {
-        return false;
+    public get selectId() {
+        return this._select.id;
     }
 
-    public get isDisabled(): boolean {
+    public get label(): string {
+        return this._select.label;
+    }
+
+    public get messageWhenEmpty(): string {
+        return this._select.messageWhenEmpty;
+    }
+
+    public get enableHelpButton(): boolean {
         return false;
     }
 
@@ -65,8 +61,8 @@ export class ChartTypeSelectComponent implements IObservable {
         return false;
     }
 
-    public get label() {
-        return this.intlService.localizeText("INFO_PARAMFIELD_CHART_TYPE");
+    public get isMultiple(): boolean {
+        return this._select.multiple;
     }
 
     // interface IObservable
@@ -75,30 +71,20 @@ export class ChartTypeSelectComponent implements IObservable {
      * ajoute un observateur à la liste
      */
     addObserver(o: Observer) {
-        this._observable.addObserver(o);
+        this._select.addObserver(o);
     }
 
     /**
      * supprime un observateur de la liste
      */
     removeObserver(o: Observer) {
-        this._observable.removeObserver(o);
+        this._select.removeObserver(o);
     }
 
     /**
      * notifie un événement aux observateurs
      */
     notifyObservers(data: any, sender?: any) {
-        this._observable.notifyObservers(data, sender);
-    }
-
-    // implemented because of generic template
-    public get enableHelpButton() {
-        return false;
-    }
-
-    // implemented because of generic template
-    public get messageWhenEmpty(): string {
-        return undefined;
+        this._select.notifyObservers(data, sender);
     }
 }
diff --git a/src/app/components/results-chart/results-chart.component.ts b/src/app/components/results-chart/results-chart.component.ts
index bb0c86a01..0b33967f3 100644
--- a/src/app/components/results-chart/results-chart.component.ts
+++ b/src/app/components/results-chart/results-chart.component.ts
@@ -1,6 +1,6 @@
 import { Component, ViewChild, AfterContentInit, ChangeDetectorRef, Input, OnChanges } from "@angular/core";
 
-import { BaseChartDirective, NgChartsModule } from "ng2-charts";
+import { BaseChartDirective } from "ng2-charts";
 
 import { Observer, ParamFamily, Result } from "jalhyd";
 
@@ -16,6 +16,7 @@ import { AppComponent } from "../../app.component";
 
 import zoomPlugin from 'chartjs-plugin-zoom';
 import { Chart } from "chart.js";
+import { SelectFieldChartType } from "app/formulaire/elements/select/select-field-charttype";
 
 @Component({
     selector: "results-chart",
@@ -495,9 +496,9 @@ export class ResultsChartComponent extends ResultsComponentDirective implements
     // interface Observer
 
     update(sender: any, data: any) {
-        if (sender instanceof ChartTypeSelectComponent) {
-            if (data.action === "graphTypeChanged") {
-                this._results.chartType = data.value;
+        if (sender instanceof SelectFieldChartType) {
+            if (data.action === "select") {
+                this._results.chartType = data.value.value;
             }
         }
         this.drawChart();
diff --git a/src/app/formulaire/elements/select/select-field-charttype.ts b/src/app/formulaire/elements/select/select-field-charttype.ts
new file mode 100644
index 000000000..b6f35d3f9
--- /dev/null
+++ b/src/app/formulaire/elements/select/select-field-charttype.ts
@@ -0,0 +1,53 @@
+import { ChartType } from "app/results/chart-type";
+import { I18nService } from "app/services/internationalisation.service";
+import { FormulaireNode } from "../formulaire-node";
+import { SelectEntry } from "./select-entry";
+import { SelectField } from "./select-field";
+
+export class SelectFieldChartType extends SelectField {
+    private _entryValues: ChartType[] = [ChartType.Histogram, ChartType.Dots, ChartType.Scatter];
+
+    constructor(parent: FormulaireNode, private intlService: I18nService) {
+        super(parent);
+        this._confId = "chart-type";
+        this._multiple = false;
+    }
+
+    public get entryValues(): ChartType[] {
+        return this._entryValues;
+    }
+
+    protected populate() {
+        for (const v of this._entryValues) {
+            const id: string = ChartType[v];
+            this.addEntry(new SelectEntry(id, v));
+        }
+    }
+
+    protected initSelectedValue() {
+        this.findAndSetDefaultValue();
+    }
+
+    public updateLocalisation() {
+        for (const e of this._entries) {
+            switch (e.value) {
+                case ChartType.Histogram:
+                    e.label = this.intlService.localizeText("INFO_PARAMFIELD_CHART_TYPE_HISTOGRAM");
+                    break;
+
+                case ChartType.Dots:
+                    e.label = this.intlService.localizeText("INFO_PARAMFIELD_CHART_TYPE_DOTS");
+                    break;
+
+                case ChartType.Scatter:
+                    e.label = "XY";
+                    break;
+
+                default:
+                    throw new Error("SelectFieldChartType localisation not properly defined");
+            }
+        }
+
+        this._label = this.intlService.localizeText("INFO_PARAMFIELD_CHART_TYPE");
+    }
+}
\ No newline at end of file
-- 
GitLab


From cd000992a4b9d2e49393274042e3d7eb7348ea23 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Thu, 27 Oct 2022 10:58:59 +0200
Subject: [PATCH 11/19] refactor: calculator configuration: remove
 defaultNodeType property

refs #483
---
 src/app/calculators/bief/config.json          |  2 +-
 src/app/calculators/courberemous/config.json  |  2 +-
 .../calculators/regimeuniforme/config.json    |  2 +-
 .../calculators/sectionparametree/config.json |  2 +-
 .../formulaire/definition/config-parser.ts    | 41 +++++++++++++++++++
 .../formulaire/definition/form-definition.ts  | 32 ++++++++++++---
 .../definition/form-parallel-structures.ts    |  1 -
 .../formulaire/elements/fieldset-template.ts  |  5 ---
 8 files changed, 71 insertions(+), 16 deletions(-)
 create mode 100644 src/app/formulaire/definition/config-parser.ts

diff --git a/src/app/calculators/bief/config.json b/src/app/calculators/bief/config.json
index 584efa828..4c486031c 100644
--- a/src/app/calculators/bief/config.json
+++ b/src/app/calculators/bief/config.json
@@ -19,6 +19,7 @@
                 "id": "select_section",
                 "type": "select",
                 "property": "nodeType",
+                "default": "SectionRectangle",
                 "help": {
                     "1": "hsl/types_sections.html#section-rectangulaire",
                     "0": "hsl/types_sections.html#section-circulaire",
@@ -68,7 +69,6 @@
     },
     {
         "type": "options",
-        "defaultNodeType": "SectionRectangle",
         "help": "hsl/cote_amont_aval.html"
     }
 ]
diff --git a/src/app/calculators/courberemous/config.json b/src/app/calculators/courberemous/config.json
index 30bab2879..d1682af8b 100644
--- a/src/app/calculators/courberemous/config.json
+++ b/src/app/calculators/courberemous/config.json
@@ -7,6 +7,7 @@
                 "id": "select_section",
                 "type": "select",
                 "property": "nodeType",
+                "default": "SectionRectangle",
                 "help": {
                     "1": "hsl/types_sections.html#section-rectangulaire",
                     "0": "hsl/types_sections.html#section-circulaire",
@@ -95,7 +96,6 @@
     },
     {
         "type": "options",
-        "defaultNodeType": "SectionRectangle",
         "help": "hsl/courbe_remous.html",
         "resultsHelp": {
             "B": "hsl/section_parametree.html#largeur-au-miroir-surface-et-perimetre-mouille",
diff --git a/src/app/calculators/regimeuniforme/config.json b/src/app/calculators/regimeuniforme/config.json
index 171dc0139..c51679f69 100644
--- a/src/app/calculators/regimeuniforme/config.json
+++ b/src/app/calculators/regimeuniforme/config.json
@@ -7,6 +7,7 @@
                 "id": "select_section",
                 "type": "select",
                 "property": "nodeType",
+                "default": "SectionRectangle",
                 "help": {
                     "1": "hsl/types_sections.html#section-rectangulaire",
                     "0": "hsl/types_sections.html#section-circulaire",
@@ -49,7 +50,6 @@
     },
     {
         "type": "options",
-        "defaultNodeType": "SectionRectangle",
         "help": "hsl/regime_uniforme.html",
         "resultsHelp": {
             "V": "hsl/section_parametree.html#la-vitesse-moyenne-ms"
diff --git a/src/app/calculators/sectionparametree/config.json b/src/app/calculators/sectionparametree/config.json
index 4e4ce5e28..a54bed3df 100644
--- a/src/app/calculators/sectionparametree/config.json
+++ b/src/app/calculators/sectionparametree/config.json
@@ -7,6 +7,7 @@
                 "id": "select_section",
                 "type": "select",
                 "property": "nodeType",
+                "default": "SectionRectangle",
                 "help": {
                     "1": "hsl/types_sections.html#section-rectangulaire",
                     "0": "hsl/types_sections.html#section-circulaire",
@@ -49,7 +50,6 @@
     },
     {
         "type": "options",
-        "defaultNodeType": "SectionRectangle",
         "help": "hsl/section_parametree.html",
         "resultsHelp": {
             "B": "hsl/section_parametree.html#largeur-au-miroir-surface-et-perimetre-mouille",
diff --git a/src/app/formulaire/definition/config-parser.ts b/src/app/formulaire/definition/config-parser.ts
new file mode 100644
index 000000000..8deec7c96
--- /dev/null
+++ b/src/app/formulaire/definition/config-parser.ts
@@ -0,0 +1,41 @@
+export class ConfigIterator implements IterableIterator<Object> {
+    private _index = 0;
+
+    private _entries: [string, any][];
+
+    constructor(private _json: Object, private _key: string) {
+        this._entries = Object.entries(this._json);
+    }
+
+    public next(): IteratorResult<Object> {
+        let i = this._index;
+        while (i < this._entries.length) {
+            const ent = this._entries[i][1];
+            if (ent.type === this._key) {
+                this._index = i + 1;
+                return {
+                    done: false,
+                    value: ent
+                };
+            }
+            i++;
+        }
+        return {
+            done: true,
+            value: undefined
+        };
+    }
+
+    [Symbol.iterator](): IterableIterator<Object> {
+        return this;
+    }
+}
+
+export class ConfigParser {
+    constructor(private _json: {}) {
+    }
+
+    public forAll(key: string): ConfigIterator {
+        return new ConfigIterator(this._json, key);
+    }
+}
\ No newline at end of file
diff --git a/src/app/formulaire/definition/form-definition.ts b/src/app/formulaire/definition/form-definition.ts
index bcf6eef68..9c9b94889 100644
--- a/src/app/formulaire/definition/form-definition.ts
+++ b/src/app/formulaire/definition/form-definition.ts
@@ -27,6 +27,7 @@ import { PabTable } from "../elements/pab-table";
 import { SelectEntry } from "../elements/select/select-entry";
 import { SelectField } from "../elements/select/select-field";
 import { DeepSelectFieldIterator } from "../form-iterator/deep-selectfield-iterator";
+import { ConfigParser as ConfigParser } from "./config-parser";
 
 /**
  * classe de base pour tous les formulaires
@@ -43,7 +44,7 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
     protected _props = {};
 
     /** for SectionNubs only */
-    protected _defaultNodeType;
+    private _defaultSectionType;
 
     /** aide en ligne pour les résultats */
     protected _resultsHelpLinks: { [key: string]: string };
@@ -128,7 +129,7 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
             // add new Section as first child, from given nodeType
             const propsSection = new Props();
             propsSection.setPropValue("calcType", CalculatorType.Section);
-            propsSection.setPropValue("nodeType", this._defaultNodeType);
+            propsSection.setPropValue("nodeType", this._defaultSectionType);
             propsSection.setPropValue(Prop_NullParameters, ServiceFactory.applicationSetupService.enableEmptyFieldsOnFormInit);
             const section = Session.getInstance().createNub(propsSection);
             this.currentNub.setSection(section as acSection);
@@ -161,10 +162,8 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
     }
 
     protected parseOptions(json: {}) {
-        const dnt = json["defaultNodeType"];
-        if (dnt !== undefined) {
-            this._defaultNodeType = SectionType[dnt];
-        }
+        this.parseDefaultSectionType();
+
         this._helpLink = json["help"];
         this._resultsHelpLinks = json["resultsHelp"];
         if (json["calculateDisabled"] !== undefined) {
@@ -172,6 +171,25 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         }
     }
 
+    /**
+     * determine default section type from select configuration
+     */
+    private parseDefaultSectionType() {
+        if (this._defaultSectionType === undefined) {
+            const jp = new ConfigParser(this._jsonConfig);
+            for (const fs of jp.forAll("fieldset")) {
+                const fsp = new ConfigParser(fs["fields"]);
+                for (const sel of fsp.forAll("select")) {
+                    if (sel["id"] === "select_section") {
+                        const st = sel["default"];
+                        this._defaultSectionType = SectionType[st];
+                        return;
+                    }
+                }
+            }
+        }
+    }
+
     /** called at the end of parseConfig() */
     protected completeParse(firstNotif: boolean = true) {
         this.helpLinks = this._resultsHelpLinks;
@@ -238,6 +256,8 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
     public preparseConfig(json: {}) {
         this._jsonConfig = json;
 
+        this.parseDefaultSectionType();
+
         // analyse des options globales
         // il est utile de le faire avant le reste pour les modules de calcul utilisant
         // des sections (id des selects type de section/variable à calculer)
diff --git a/src/app/formulaire/definition/form-parallel-structures.ts b/src/app/formulaire/definition/form-parallel-structures.ts
index d25b4df33..2fbc67186 100644
--- a/src/app/formulaire/definition/form-parallel-structures.ts
+++ b/src/app/formulaire/definition/form-parallel-structures.ts
@@ -64,7 +64,6 @@ export class FormulaireParallelStructure extends FormulaireRepeatableFieldset {
 
         const params = {};
         params["calcType"] = templ.calcTypeFromConfig;
-        params["nodeType"] = templ.defaultNodeTypeFromConfig;
         params["structureType"] = templ.defaultStructTypeFromConfig;
         params["loiDebit"] = templ.defaultLoiDebitFromConfig;
         params[Prop_NullParameters] = ServiceFactory.applicationSetupService.enableEmptyFieldsOnFormInit;
diff --git a/src/app/formulaire/elements/fieldset-template.ts b/src/app/formulaire/elements/fieldset-template.ts
index b65b48874..ddad4008b 100644
--- a/src/app/formulaire/elements/fieldset-template.ts
+++ b/src/app/formulaire/elements/fieldset-template.ts
@@ -19,11 +19,6 @@ export class FieldsetTemplate {
         return CalculatorType[ct];
     }
 
-    public get defaultNodeTypeFromConfig(): SectionType {
-        const nt: string = this._jsonConfig["defaultNodeType"];
-        return SectionType[nt];
-    }
-
     public get defaultStructTypeFromConfig(): StructureType {
         const st: string = this._jsonConfig["defaultStructType"];
         return StructureType[st];
-- 
GitLab


From d816e82bcf7e3e515f4fa3cf491a827c13788ef1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Thu, 27 Oct 2022 13:40:47 +0200
Subject: [PATCH 12/19] doc: update developer documentation

refs #483
---
 DEVELOPERS.md | 195 ++++++++++++++------------------------------------
 1 file changed, 55 insertions(+), 140 deletions(-)

diff --git a/DEVELOPERS.md b/DEVELOPERS.md
index 4a6b1d4d7..e69e95255 100644
--- a/DEVELOPERS.md
+++ b/DEVELOPERS.md
@@ -177,8 +177,6 @@ Dans cet exemple, on définit un seul groupe de champs nommé arbitrairement "fs
 Le deuxième et dernier bloc contient les options pour ce module: ici uniquement le lien vers la page de documentation pour ce module (`help`).
 
 Les options peuvent également contenir :
- * `defaultNodeType` : le type de section par défaut du module de calcul, pour les modules contenant une section
- * `selectIds` : les identifiants des listes déroulantes
  * `resultsHelp` : l'aide associée aux résultats (voir ci-dessous)
  * `calculateDisabled` : pour masquer le bouton Calculer (ex: module Espèce)
 
@@ -279,11 +277,13 @@ case CalculatorType.MacroRugoCompound:
 
 Les listes déroulantes sont toujours associées à des **propriétés** du Nub.
 
-En général les valeurs autorisées sont tirées de l'**enum** correspondant, d'après le tableau `Session.enumFromProperty` de JaLHyd. Pour les autres cas, voir les paragraphes "source" et "liste déroulante personnalisée" ci-dessous.
+En général les valeurs autorisées sont tirées de l'**enum** correspondant, d'après le tableau `Session.enumFromProperty` de JaLHyd. Pour les autres cas, voir le paragraphe "liste déroulante personnalisée" ci-dessous.
 
 #### configuration
 
-Dans le fichier de configuration du module, ajouter la définition des listes déroulantes dans "fields", notamment la propriété associée et la valeur par défaut. Puis dans le bloc de configuration, déclarer les identifiants des listes dans "selectIds". Exemple dans `trigo/config.json`
+Dans le fichier de configuration du module, ajouter la définition des listes déroulantes dans "fields", notamment la propriété associée et la valeur par défaut; cette dernière est optionnelle et en son absence, la 1ère valeur de la liste sera sélectionnée.
+
+Exemple dans `trigo/config.json` (ici le 2ème champ ne spécifie pas de valeur par défaut) :
 
 ```json
 {
@@ -299,169 +299,108 @@ Dans le fichier de configuration du module, ajouter la définition des listes d
             "id": "select_unit",
             "type": "select",
             "property": "trigoUnit",
-            "default": "DEG"
         },
         …
     ]
 },
-…
-{
-    "type": "options",
-    "selectIds": [ "select_operation", "select_unit" ],
-    …
-}
-```
-
-**IMPORTANT** : les id doivent être de la forme `select_`_`unmotclesansespacenitirets`_
-
-#### source
- 
-Une liste déroulante telle que décrite ci-dessus peut être associée à une **source**, qui détermine quels sont les choix possibles. Ceci est utile lorsque les choix possibles ne proviennent pas d'un `enum`. Pour autant ce n'est pas équivalent à la méthode "liste déroulante personnalisée" décrite au chapitre suivant (ces deux techniques devraient être fusionnées).
-
-Pour ajouter une source, modifier la méthode `loadEntriesFromSource()` de la classe `SelectField`, dans le fichier `src/app/formulaire/elements/select-field.ts`.
-
-Exemple pour la source "remous_target" associée à la propriété "varCalc", dans le module CourbeRemous :
-
-```typescript
-switch (source) {
-    // driven by string[], not enum (easier for variable names)
-    case "remous_target":
-        this.addEntry(new SelectEntry(this._entriesBaseId + "none", ""));
-        for (const at of CourbeRemous.availableTargets) {
-            const e: SelectEntry = new SelectEntry(this._entriesBaseId + at, at);
-            this.addEntry(e);
-        }
-        break;
 ```
 
-Ici on ajoute des options de type `SelectEntry` à l'aide de la méthode `addEntry()` : une option vide au début, puis une pour chaque élément d'un tableau.
-
-Puis dans le fichier de configuration du module, déclarer la source :
-
-```json
-{
-    "id": "select_target",
-    "type": "select",
-    "property": "varCalc",
-    "source": "remous_target",
-    …
-```
+**IMPORTANT** : les ids doivent être de la forme `select_`_`unmotclesansespacenitirets`_
 
 #### liste déroulante personnalisée
 
-Il est possible de personnaliser complètement le comportement d'une liste déroulante pour des cas plus complexes, en utilisant l'élément de formulaire `SelectCustom`.
+Il est possible de personnaliser complètement le comportement d'une liste déroulante pour des cas plus complexes.
 
-De telles listes doivent être déclarées dans la configuration du module en utilisant le type `select_custom`, un identifiant de `source` (identique à celui vu au chapitre précédent, mais attention : il se comporte différemment) et l'identifiant de la liste doit être mentionné dans l'option `customSelectIds` (et non `selectIds`).
+De telles listes doivent être déclarées dans la configuration du module en utilisant uniquement le type `select` et un identifiant de liste. Dans ce cas, on ne fournit pas de champ `property` ni `default`.
 
 Exemple pour la cible du Solveur :
 ```json
 …
 "fields": [
     {
+        "type": "select",
         "id": "select_target_nub",
-        "type": "select_custom",
-        "source": "solveur_target"
     },
     …
 ]
 …
-{
-    "type": "options",
-    "customSelectIds": [ "select_target_nub", … ],
-    …
-}
 ```
 
-Dans le fichier `src/app/formulaire/elements/select-field-custom.ts`, remplir les méthodes `populate()` (définit les choix possibles) et `initSelectedValue()` (affecte la valeur actuellement définie dans le modèle).
-
+Il faut ensuite faire 2 choses :
+- créer une classe dérivée de `SelectField` et implémenter les méthodes `populate()` (remplissage de la liste) et `initSelectedValue()` (fixation de la valeur par défaut) :
 
-Exemple pour la cible du Solveur : `populate()`
 ```typescript
-case "solveur_target": // Solveur, module cible (à calculer)
-    // find all Nubs having at least one link to another Nub's result
-    candidateNubs =
-        Session.getInstance().getDownstreamNubs().concat(
-            Session.getInstance().getUpstreamNubsHavingExtraResults()
-        ).filter(
-            (element, index, self) => self.findIndex((e) => e.uid === element.uid) === index
-        );
-    for (const cn of candidateNubs) {
-        const nub = fs.getFormulaireFromId(cn.uid);
-        if (nub) {
-            const calc = nub.calculatorName;
-            let label = calc;
-            // calculated param
-            if (cn.calculatedParam !== undefined) {
-                const varName = fs.expandVariableName(cn.calcType, cn.calculatedParam.symbol);
-                label += ` / ${varName} (${cn.calculatedParam.symbol})`;
-            }
-            this.addEntry(new SelectEntry(this._entriesBaseId + cn.uid, cn.uid, decodeHtml(label)));
-        } else {
-            // silent fail, this Solveur nub was probably loaded before all the candidate nubs are done loading
-        }
+ export class SelectFieldSolverTarget extends SelectField {
+    protected populate() {
+        …
+        // une série de addEntry()
+        …
     }
-    break;
-```
 
-Comme au chapitre précédent, il s'agit d'ajouter des options de type `SelectEntry` à l'aide de la méthode `addEntry()`.
+    protected initSelectedValue() {
+        // utiliser setValueFromId() avec un id calculé
+        // ou findAndSetDefaultValue() pour utiliser la 1ère valeur de la liste
+    }
+}
+```
 
-Exemple pour la cible du Solveur : `initSelectedValue()`
+- modifier la méthode `newSelectField` de la classe `SelectFieldFactory` pour créer une instance de la classe dérivée en utilisant le champ `id` précisé dans le JSON de configuration :
 ```typescript
-case "solveur_target": // Solveur, module cible (à calculer)
-    const ntc = (nub as Solveur).nubToCalculate;
-    if (ntc !== undefined) {
-        this.setValueFromId(this._entriesBaseId + ntc.uid);
+    public static newSelectField(json: {}, parent: FormulaireNode): SelectField {
+        switch (json["id"]) {
+            …
+            case "select_target_nub":
+                return new SelectFieldSolverTarget(parent);
+            …
+        }
     }
-    break;
 ```
 
-Ici il s'agit de choisir la bonne option du sélecteur, en fonction de la valeur courante de la propriété dans le modèle.
-
-Enfin, ce type de liste déroulante nécessite une classe de formulaire personnalisée, dans laquelle la méthode `update()` doit être enrichie.
+Enfin, ce type de liste déroulante peut nécessiter une classe de formulaire personnalisée, dans laquelle la méthode `update()` doit être enrichie.
 
-Exemple dans `src/app/formulaire/definition/form-solveur.ts`, pour la cible du Solveur :
+Exemple dans `src/app/formulaire/definition/form-solveur.ts` pour la cible du Solveur :
 
 ```typescript
 public update(sender: IObservable, data: any) {
     …
-    if (sender instanceof SelectFieldCustom) {
-        if (sender.id === "select_target_nub" && data.action === "select") {
-            // update Solveur property: Nub to calculate
-            try {
-                // if searchedParam is set to a value that won't be available anymore
-                // once nubToCalculate is updated, setPropValue throws an error, but
-                // nubToCalculate is updated anyway; here, just inhibit the error
-                this._currentNub.properties.setPropValue("nubToCalculate", data.value.value);
-            } catch (e) { }
-            // refresh targetted result selector
-            const trSel = this.getFormulaireNodeById(this._targettedResultSelectId) as SelectField;
-            if (trSel) {
-                (trSel.parent as FieldSet).updateFields();
-                // trick to re-set observers
-                this.completeParse(false);
+    if (sender instanceof SelectField) {
+            if (sender.id === "select_target_nub" && data.action === "select") {
+                // update Solveur property: Nub to calculate
+                try {
+                    // if searchedParam is set to a value that won't be available anymore
+                    // once nubToCalculate is updated, setPropValue throws an error, but
+                    // nubToCalculate is updated anyway; here, just inhibit the error
+                    this._currentNub.properties.setPropValue("nubToCalculate", data.value.value);
+                } catch (e) { }
+                // refresh targetted result selector
+                const trSel = this.getFormulaireNodeById(this._targettedResultSelectId) as SelectField;
+                if (trSel) {
+                    (trSel.parent as FieldSet).updateFields();
+                    // trick to re-set observers
+                    this.completeParse(false);
+                }
+                // refresh parameters selector
+                this.refreshParameterEntries();
             }
-            // refresh parameters selector
-            this.refreshParameterEntries();
-        }
         …
     }
     …
 }
 ```
 
-Ici on écoute l'événement généré par l'objet `SelectFieldCustom`, et on agit en conséquence : affecter la propriété concernée, et rafraîchir un champ dépendant.
+Ici on écoute l'événement généré par l'objet dérivé de `SelectField`, et on agit en conséquence : affecter la propriété concernée et rafraîchir un champ dépendant.
 
 
 #### si l'affichage de certains champs dépend du choix dans la liste
 
 Les listes dont l'identifiant est déclaré dans le fichier de configuration du module déclencheront, lorsque leur valeur change, un effacement des résultats du module et une mise à jour de tous les "fieldset" du formulaire.
 
-Cette dernière opération permet de vérifier la visibilité de chaque champ du formulaire, et afficher / masquer ceux dont la visibilité a changé.
+Cette dernière opération permet de vérifier la visibilité de chaque champ du formulaire et afficher/masquer ceux dont la visibilité a changé.
 
 Ainsi, pour rendre la visibilité d'un champ dépendante du choix dans la liste, il faut, **dans le code du Nub dans JaLHyd** :
 
- * écouter le changement de propriété (méthode `update()`, action `propertyChange`)
- * selon la nouvelle valeur, ajuster la propriété `.visible` des paramètres concernés
+ * écouter le changement de propriété (méthode `update()`, action `propertyChange`),
+ * selon la nouvelle valeur, ajuster la propriété `visible` des paramètres concernés.
  
  Il n'y a rien à faire de particulier dans ngHyd.
 
@@ -469,7 +408,7 @@ Ainsi, pour rendre la visibilité d'un champ dépendante du choix dans la liste,
 
 Il faut utiliser ou étendre `FormulaireSection`.
 
-Dans la configuration du module, ajouter un sélecteur de section, associé à la propriété "nodeType" :
+Dans la configuration du module, ajouter un sélecteur de section associé à la propriété "nodeType" (type de section) :
 
 ```json
 {
@@ -484,17 +423,7 @@ Dans la configuration du module, ajouter un sélecteur de section, associé à l
     }
 }
 ```
-
-Puis dans les options, déclarer  ce sélecteur et ajouter "defaultNodeType" :
-
-```json
-{
-    "type": "options",
-    "defaultNodeType": "SectionRectangle",
-    "selectIds": [ "select_section" ],
-    …
-}
-```
+La section par défaut du formulaire sera celle du sélecteur, que celle ci soit ou non configurée explicitement par le champ `default`.
 
 ### si le module agrège des modules enfants
 
@@ -581,14 +510,10 @@ Dans la configuration du module, dans le "fieldset_template", ajouter un sélect
         {
             "id": "select_structure",
             "type": "select",
-            "property": "structureType",
-            "source": "device_structure_type"
         },
         {
             "id": "select_loidebit",
             "type": "select",
-            "property": "loiDebit",
-            "source": "device_loi_debit",
             "help": {
                 "Orifice_OrificeSubmerged": "structures/orifice_noye.html",
                 "SeuilRectangulaire_WeirVillemonte": "structures/kivi.html",
@@ -602,16 +527,6 @@ Dans la configuration du module, dans le "fieldset_template", ajouter un sélect
         }
 ```
 
-Dans les options, déclarer  les sélecteurs :
-
-```json
-{
-    "type": "options",
-    "selectIds": [ "select_structure", "select_loidebit" ],
-    …
-}
-```
-
 ### documentation
 
 Pour chaque langue, ajouter un fichier .md dans les dossiers `docs/*/calculators`, puis placer ce nouveau fichier dans la hiérarchie de la documentation, en ajoutant son chemin dans les fichiers `mkdocs-*.yml`.
-- 
GitLab


From e0403b11998037034d5db801e32f6c9119a5d236 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Thu, 27 Oct 2022 16:45:26 +0200
Subject: [PATCH 13/19] fix: standard select defaut value from calculator
 configuration not honored

refs #483
---
 src/app/formulaire/elements/fieldset.ts            | 10 ++--------
 src/app/formulaire/elements/select/select-field.ts | 12 +++++++++---
 2 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/src/app/formulaire/elements/fieldset.ts b/src/app/formulaire/elements/fieldset.ts
index e5fbca7d7..d0229afe7 100644
--- a/src/app/formulaire/elements/fieldset.ts
+++ b/src/app/formulaire/elements/fieldset.ts
@@ -4,7 +4,7 @@ import {
     Props,
     Observer,
     Nub,
-    Session,
+    enumValueFromString
 } from "jalhyd";
 
 import { FormulaireElement } from "./formulaire-element";
@@ -295,13 +295,7 @@ export class FieldSet extends FormulaireElement implements Observer {
                     // Sets Nub default property, unless this property is already set
                     const currentValue = this.properties.getPropValue(prop);
                     if (defaultValue !== undefined && currentValue === undefined) {
-                        let formalValue = defaultValue;
-                        // !! property names must be unique throughout JaLHyd !!
-                        const enumClass = Session.enumFromProperty[prop];
-                        if (enumClass) {
-                            formalValue = enumClass[defaultValue];
-                        }
-                        this.setNubPropValue(prop, formalValue);
+                        this.setNubPropValue(prop, enumValueFromString(prop, defaultValue));
                     }
                 }
             }
diff --git a/src/app/formulaire/elements/select/select-field.ts b/src/app/formulaire/elements/select/select-field.ts
index f67c07404..5a174b636 100644
--- a/src/app/formulaire/elements/select/select-field.ts
+++ b/src/app/formulaire/elements/select/select-field.ts
@@ -4,7 +4,7 @@ import { arraysAreEqual } from "../../../util";
 import { FormulaireNode } from "../formulaire-node";
 import { ServiceFactory } from "app/services/service-factory";
 import { FormulaireDefinition } from "../../definition/form-definition";
-import { Nub } from "jalhyd";
+import { enumValueFromString, Nub } from "jalhyd";
 
 export abstract class SelectField extends Field {
 
@@ -128,10 +128,16 @@ export abstract class SelectField extends Field {
     protected findAndSetDefaultValue() {
         // default to first available entry if any
         if (this._entries.length > 0) {
+            let val;
+            if (this._configDefaultValue === undefined) {
+                val = this._entries[0];
+            } else {
+                val = this.getEntryFromValue(enumValueFromString(this._associatedProperty, this._configDefaultValue));
+            }
             if (this._multiple) {
-                this.setValue([this._entries[0]]);
+                this.setValue([val]);
             } else {
-                this.setValue(this._entries[0]);
+                this.setValue(val);
             }
         } else {
             // notify observers that no value is selected anymore
-- 
GitLab


From 285aad671fe8108ac9d3fafcf66aa8704576214a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Fri, 28 Oct 2022 09:10:06 +0200
Subject: [PATCH 14/19] test(e2e): increase timeout for failing tests

refs #483
---
 e2e/calculate-all-params.e2e-spec.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/e2e/calculate-all-params.e2e-spec.ts b/e2e/calculate-all-params.e2e-spec.ts
index 30887028d..39129ac61 100644
--- a/e2e/calculate-all-params.e2e-spec.ts
+++ b/e2e/calculate-all-params.e2e-spec.ts
@@ -20,6 +20,9 @@ describe("ngHyd − calculate all parameters of all calculators", async () => {
         calcPage = new CalculatorPage();
         prefPage = new PreferencesPage();
         navBar = new Navbar();
+
+        // increase timeout to avoid "Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL" message
+        jasmine.DEFAULT_TIMEOUT_INTERVAL = 10 * 60 * 1000; // 10 min
     });
 
     // get calculators list (IDs) @TODO read it from config, but can't import jalhyd here :/
-- 
GitLab


From 131537183c908ffd7a722032886bc104cb43d3c2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Fri, 28 Oct 2022 09:10:29 +0200
Subject: [PATCH 15/19] test(e2e): check section select default value from
 calculator configuration is honored

refs #483
---
 e2e/select-default-value.e2e-spec.ts | 17 +++++++++++++++--
 1 file changed, 15 insertions(+), 2 deletions(-)

diff --git a/e2e/select-default-value.e2e-spec.ts b/e2e/select-default-value.e2e-spec.ts
index 8796f19ac..9ec1205cd 100644
--- a/e2e/select-default-value.e2e-spec.ts
+++ b/e2e/select-default-value.e2e-spec.ts
@@ -35,8 +35,21 @@ describe("check the select default value - ", () => {
 
         // in the calculator configuration file, the default resolution method is 'Trapezes'.
         // let's check this...
-        const sel_section = calcPage.getSelectById("select_resolution");
-        const val = await calcPage.getSelectValueText(sel_section);
+        const sel = calcPage.getSelectById("select_resolution");
+        const val = await calcPage.getSelectValueText(sel);
         expect(val).toBe("Intégration par trapèzes");
     });
+
+    it("in the 'up/downstream elevations of a reach' calculator", async () => {
+        // open "up/downstream elevations of a reach" calculator
+        await navBar.clickNewCalculatorButton();
+        await listPage.clickMenuEntryForCalcType(21);
+        await browser.sleep(200);
+
+        // in the calculator configuration file, the default section method is 'Rectangulaire'.
+        // let's check this...
+        const sel = calcPage.getSelectById("select_section");
+        const val = await calcPage.getSelectValueText(sel);
+        expect(val).toBe("Rectangulaire");
+    });
 });
-- 
GitLab


From 4d93072a9fd19bc028323d318f0f44f93ea73ee9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Mon, 7 Nov 2022 15:07:30 +0100
Subject: [PATCH 16/19] fix: select field: check default value

---
 src/app/formulaire/elements/select/select-field.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/app/formulaire/elements/select/select-field.ts b/src/app/formulaire/elements/select/select-field.ts
index 5a174b636..f76690de4 100644
--- a/src/app/formulaire/elements/select/select-field.ts
+++ b/src/app/formulaire/elements/select/select-field.ts
@@ -133,6 +133,9 @@ export abstract class SelectField extends Field {
                 val = this._entries[0];
             } else {
                 val = this.getEntryFromValue(enumValueFromString(this._associatedProperty, this._configDefaultValue));
+                if (val === undefined) {
+                    throw Error("invalid select default value " + this._configDefaultValue + " for " + this._associatedProperty + " property");
+                }
             }
             if (this._multiple) {
                 this.setValue([val]);
-- 
GitLab


From f443a1d6e6dabc7491fcd6ca549cd4c0f42d2e6a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Mon, 7 Nov 2022 15:27:07 +0100
Subject: [PATCH 17/19] fix: calculator JSON configuration: remove unused
 defaultMaterial in Lechapt-Calmon

---
 src/app/calculators/lechaptcalmon/config.json | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/app/calculators/lechaptcalmon/config.json b/src/app/calculators/lechaptcalmon/config.json
index d655d5ca3..1b440a6ae 100644
--- a/src/app/calculators/lechaptcalmon/config.json
+++ b/src/app/calculators/lechaptcalmon/config.json
@@ -2,7 +2,6 @@
     {
         "id": "fs_materiau",
         "type": "fieldset",
-        "defaultMaterial": "UnlinedCastIronCoarseConcrete",
         "fields": [
             {
                 "id": "select_material",
-- 
GitLab


From 47c2d30b922c3d081440bfa4209d067aecf871a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Tue, 8 Nov 2022 08:36:10 +0100
Subject: [PATCH 18/19] refactor: JSON calculator config: replace
 defaultLoiDebit/defaultStructType by select default property

---
 src/app/calculators/cloisons/config.json                    | 6 +++---
 src/app/calculators/dever/config.json                       | 6 +++---
 src/app/calculators/parallelstructure/config.json           | 6 +++---
 src/app/calculators/prebarrage/config.json                  | 6 +++---
 .../elements/select/select-field-device-loi-debit.ts        | 5 +++++
 .../elements/select/select-field-device-structure-type.ts   | 5 +++++
 6 files changed, 22 insertions(+), 12 deletions(-)

diff --git a/src/app/calculators/cloisons/config.json b/src/app/calculators/cloisons/config.json
index e3b2a662d..116afc893 100644
--- a/src/app/calculators/cloisons/config.json
+++ b/src/app/calculators/cloisons/config.json
@@ -16,16 +16,16 @@
         "id": "fs_ouvrage",
         "type": "fieldset_template",
         "calcType": "Structure",
-        "defaultStructType": "SeuilRectangulaire",
-        "defaultLoiDebit": "WeirSubmergedLarinier",
         "fields": [
             {
                 "id": "select_structure",
-                "type": "select"
+                "type": "select",
+                "default": "SeuilRectangulaire"
             },
             {
                 "id": "select_loidebit",
                 "type": "select",
+                "default": "WeirSubmergedLarinier",
                 "help": {
                     "OrificeSubmerged": "structures/orifice_noye.html",
                     "WeirSubmergedLarinier": "structures/fente_noyee.html",
diff --git a/src/app/calculators/dever/config.json b/src/app/calculators/dever/config.json
index 69188b972..9055348a8 100644
--- a/src/app/calculators/dever/config.json
+++ b/src/app/calculators/dever/config.json
@@ -14,16 +14,16 @@
         "id": "fs_ouvrage",
         "type": "fieldset_template",
         "calcType": "Structure",
-        "defaultStructType": "SeuilRectangulaire",
-        "defaultLoiDebit": "WeirFree",
         "fields": [
             {
                 "id": "select_structure",
-                "type": "select"
+                "type": "select",
+                "default": "SeuilRectangulaire"
             },
             {
                 "id": "select_loidebit",
                 "type": "select",
+                "default": "WeirFree",
                 "help": {
                     "WeirFree": "structures/seuil_denoye.html",
                     "TriangularWeirFree": "structures/dever_triang.html",
diff --git a/src/app/calculators/parallelstructure/config.json b/src/app/calculators/parallelstructure/config.json
index 87ef99908..873f9a699 100644
--- a/src/app/calculators/parallelstructure/config.json
+++ b/src/app/calculators/parallelstructure/config.json
@@ -13,16 +13,16 @@
         "id": "fs_ouvrage",
         "type": "fieldset_template",
         "calcType": "Structure",
-        "defaultStructType": "SeuilOrificeRectangulaire",
-        "defaultLoiDebit": "GateCem88v",
         "fields": [
             {
                 "id": "select_structure",
-                "type": "select"
+                "type": "select",
+                "default": "SeuilOrificeRectangulaire"
             },
             {
                 "id": "select_loidebit",
                 "type": "select",
+                "default": "GateCem88v",
                 "help": {
                     "KIVI": "structures/kivi.html",
                     "WeirVillemonte": "structures/kivi.html",
diff --git a/src/app/calculators/prebarrage/config.json b/src/app/calculators/prebarrage/config.json
index 406711a5e..1aff6ed82 100644
--- a/src/app/calculators/prebarrage/config.json
+++ b/src/app/calculators/prebarrage/config.json
@@ -48,16 +48,16 @@
                 "id": "fs_ouvrage",
                 "type": "fieldset_template",
                 "calcType": "Structure",
-                "defaultStructType": "SeuilRectangulaire",
-                "defaultLoiDebit": "WeirSubmergedLarinier",
                 "fields": [
                     {
                         "id": "select_structure",
-                        "type": "select"
+                        "type": "select",
+                        "default": "SeuilRectangulaire"
                     },
                     {
                         "id": "select_loidebit",
                         "type": "select",
+                        "default": "WeirSubmergedLarinier",
                         "help": {
                             "KIVI": "structures/kivi.html",
                             "WeirVillemonte": "structures/kivi.html",
diff --git a/src/app/formulaire/elements/select/select-field-device-loi-debit.ts b/src/app/formulaire/elements/select/select-field-device-loi-debit.ts
index 6a9c86e7c..8acbca178 100644
--- a/src/app/formulaire/elements/select/select-field-device-loi-debit.ts
+++ b/src/app/formulaire/elements/select/select-field-device-loi-debit.ts
@@ -17,6 +17,11 @@ export class SelectFieldDeviceLoiDebit extends SelectField {
         this._associatedProperty = "loiDebit";
     }
 
+    public parseConfig(json: {}, data?: {}) {
+        super.parseConfig(json, data);
+        this._configDefaultValue = json["default"];
+    }
+
     protected populate() {
         // possible values depend on CalcType
 
diff --git a/src/app/formulaire/elements/select/select-field-device-structure-type.ts b/src/app/formulaire/elements/select/select-field-device-structure-type.ts
index 0210eb1b7..aee929760 100644
--- a/src/app/formulaire/elements/select/select-field-device-structure-type.ts
+++ b/src/app/formulaire/elements/select/select-field-device-structure-type.ts
@@ -16,6 +16,11 @@ export class SelectFieldDeviceStructureType extends SelectField {
         this._associatedProperty = "structureType";
     }
 
+    public parseConfig(json: {}, data?: {}) {
+        super.parseConfig(json, data);
+        this._configDefaultValue = json["default"];
+    }
+
     protected populate() {
         // possible values depend on CalcType
         for (const st in (this.nub as ParallelStructure).getLoisAdmissibles()) {
-- 
GitLab


From 90ea29b9635d8ea154ac7dab5771a9854dc5f265 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr>
Date: Tue, 8 Nov 2022 13:32:46 +0100
Subject: [PATCH 19/19] fix: wrong default value for discharge law select

refs #483
---
 DEVELOPERS.md                                 |  6 +--
 .../definition/form-parallel-structures.ts    |  5 --
 .../formulaire/elements/fieldset-container.ts |  6 ++-
 .../formulaire/elements/fieldset-template.ts  | 46 +++++++++++++++----
 .../select/select-field-device-loi-debit.ts   | 23 ++++++++--
 .../elements/select/select-field-factory.ts   | 16 ++++++-
 .../elements/select/select-field.ts           |  8 ++--
 7 files changed, 82 insertions(+), 28 deletions(-)

diff --git a/DEVELOPERS.md b/DEVELOPERS.md
index e69e95255..908e88488 100644
--- a/DEVELOPERS.md
+++ b/DEVELOPERS.md
@@ -497,23 +497,23 @@ Dans chaque fichier de langue du dossier `src/locale`, ajouter les traductions p
 
 Il faut utiliser ou étendre `FormulaireParallelStructure` (ex: Cloisons, Dever…).
 
-Dans la configuration du module, dans le "fieldset_template", ajouter un sélecteur de structure associé à la propriété "structureType" et un sélecteur de loi de débit associé à la propriété  "loiDebit", noter les propriétés "calcType" (toujours "Structure" dans ce cas), "defaultStructType" et "defaultLoiDebit" :
+Dans la configuration du module, dans le "fieldset_template", ajouter un sélecteur de structure et un sélecteur de loi de débit, ajouter les propriétés "calcType" (toujours "Structure" dans ce cas) et les valeurs par défaut avec "default" dans la définition du select. A noter que les ids des select sont fixés et doivent obligatoirement être "select_structure" et "select_loidebit" :
 
 ```json
 {
     "id": "fs_ouvrage",
     "type": "fieldset_template",
     "calcType": "Structure",
-    "defaultStructType": "VanneFondRectangulaire",
-    "defaultLoiDebit": "GateCem88v",
     "fields": [
         {
             "id": "select_structure",
             "type": "select",
+            "default": "VanneFondRectangulaire",
         },
         {
             "id": "select_loidebit",
             "type": "select",
+            "default": "GateCem88v",
             "help": {
                 "Orifice_OrificeSubmerged": "structures/orifice_noye.html",
                 "SeuilRectangulaire_WeirVillemonte": "structures/kivi.html",
diff --git a/src/app/formulaire/definition/form-parallel-structures.ts b/src/app/formulaire/definition/form-parallel-structures.ts
index 2fbc67186..b47c4808b 100644
--- a/src/app/formulaire/definition/form-parallel-structures.ts
+++ b/src/app/formulaire/definition/form-parallel-structures.ts
@@ -57,11 +57,6 @@ export class FormulaireParallelStructure extends FormulaireRepeatableFieldset {
     }
 
     protected createChildNub(templ: FieldsetTemplate): Nub {
-        // !!! attention !!!
-        // Il doit y avoir cohérence dans le fichier de conf entre les valeurs defaultXXX et les valeurs possibles pour les select
-        // cad valeur par défaut du 1er select (type d'ouvrage), du 2ème (loi de débit).
-        // A terme, il faudrait analyser le fichier de conf (dépendances d'existence) pour déterminer automatiquement ces valeurs
-
         const params = {};
         params["calcType"] = templ.calcTypeFromConfig;
         params["structureType"] = templ.defaultStructTypeFromConfig;
diff --git a/src/app/formulaire/elements/fieldset-container.ts b/src/app/formulaire/elements/fieldset-container.ts
index 23b548895..0f57d21fe 100644
--- a/src/app/formulaire/elements/fieldset-container.ts
+++ b/src/app/formulaire/elements/fieldset-container.ts
@@ -21,7 +21,7 @@ export class FieldsetContainer extends FormulaireElement {
         if (this._template !== undefined) {
             throw new Error("template already set!");
         }
-        this._template = new FieldsetTemplate(fst);
+        this._template = fst;
     }
 
     public addFieldset(fs: FieldSet) {
@@ -105,7 +105,9 @@ export class FieldsetContainer extends FormulaireElement {
         for (const t of templateNames) {
             for (const d of templates) {
                 if (d.id === t) {
-                    this.template = d;
+                    const tmpl = new FieldsetTemplate();
+                    tmpl.parseConfig(d);
+                    this.template = tmpl;
                 }
             }
         }
diff --git a/src/app/formulaire/elements/fieldset-template.ts b/src/app/formulaire/elements/fieldset-template.ts
index ddad4008b..9147d2cbb 100644
--- a/src/app/formulaire/elements/fieldset-template.ts
+++ b/src/app/formulaire/elements/fieldset-template.ts
@@ -2,13 +2,16 @@ import { FieldSet } from "./fieldset";
 import { CalculatorType, LoiDebit, Nub, StructureType, SectionType } from "jalhyd";
 import { FormulaireDefinition } from "../definition/form-definition";
 import { FieldsetContainer } from "./fieldset-container";
+import { SelectFieldFactory } from "./select/select-field-factory";
 
 export class FieldsetTemplate {
     private _jsonConfig: {};
 
-    constructor(config: {}) {
-        this._jsonConfig = config;
-    }
+    // parsed default structure type from JSON configuration file (from select_structure field)
+    private _defaultStructureType: StructureType;
+
+    // parsed default stage discharge law from JSON configuration file (from select_loidebit field)
+    private _defaultLoiDebit: LoiDebit;
 
     public get config() {
         return this._jsonConfig;
@@ -20,16 +23,41 @@ export class FieldsetTemplate {
     }
 
     public get defaultStructTypeFromConfig(): StructureType {
-        const st: string = this._jsonConfig["defaultStructType"];
-        return StructureType[st];
+        return this._defaultStructureType;
     }
 
     public get defaultLoiDebitFromConfig(): LoiDebit {
-        const ld: string = this._jsonConfig["defaultLoiDebit"];
-        if (LoiDebit[ld] === undefined) {
-            throw new Error(`FieldsetTemplate.defaultLoiDebitFromConfig: La loi de débit ${ld} n'est pas définie`);
+        return this._defaultLoiDebit;
+    }
+
+    private findSelectField(id: string): any {
+        for (const f of this._jsonConfig["fields"]) {
+            if (f["type"] === "select" && f["id"] === id) {
+                return f;
+            }
+        }
+    }
+
+    public parseConfig(config: {}) {
+        this._jsonConfig = config;
+
+        if (this._jsonConfig["calcType"] === "Structure") {
+            // parse default stage discharge law
+
+            const ld: string = this.findSelectField(SelectFieldFactory.SelectId_LoiDebit)["default"];
+            if (LoiDebit[ld] === undefined) {
+                throw new Error(`FieldsetTemplate.defaultLoiDebitFromConfig : la loi de débit ${ld} n'est pas définie`);
+            }
+            this._defaultLoiDebit = LoiDebit[ld];
+
+            // parse default structure type
+
+            const st: string = this.findSelectField(SelectFieldFactory.SelectId_StructureType)["default"];
+            if (StructureType[st] === undefined) {
+                throw new Error(`FieldsetTemplate.defaultStructTypeFromConfig : le type de structure ${st} n'est pas défini`);
+            }
+            this._defaultStructureType = StructureType[st];
         }
-        return LoiDebit[ld];
     }
 
     /**
diff --git a/src/app/formulaire/elements/select/select-field-device-loi-debit.ts b/src/app/formulaire/elements/select/select-field-device-loi-debit.ts
index 8acbca178..2c556ff0a 100644
--- a/src/app/formulaire/elements/select/select-field-device-loi-debit.ts
+++ b/src/app/formulaire/elements/select/select-field-device-loi-debit.ts
@@ -22,13 +22,27 @@ export class SelectFieldDeviceLoiDebit extends SelectField {
         this._configDefaultValue = json["default"];
     }
 
+    /**
+     * get this select's nub
+     */
+    protected get nub(): ParallelStructure {
+        return super.nub as ParallelStructure;
+    }
+
+    /**
+     * get discharge law linked to this select's nub
+     */
+    private get loiDebit(): LoiDebit {
+        const child = this.nub.getChildren()[this.parent.indexAsKid()];
+        return child.properties.getPropValue("loiDebit");
+    }
+
     protected populate() {
         // possible values depend on CalcType
 
         // get current structure type from appropriate Nub child
-        const child = this.nub.getChildren()[this.parent.indexAsKid()];
-        const la = (this.nub as ParallelStructure).getLoisAdmissibles();
-        const loiDebit = child.properties.getPropValue("loiDebit");
+        const la = this.nub.getLoisAdmissibles();
+        const loiDebit: LoiDebit = this.loiDebit;
         const stCode = StructureProperties.findCompatibleStructure(loiDebit, this.nub as ParallelStructure);
         const stName = StructureType[stCode];
         if (la[stName] !== undefined) {
@@ -40,6 +54,7 @@ export class SelectFieldDeviceLoiDebit extends SelectField {
     }
 
     protected initSelectedValue() {
-        this.findAndSetDefaultValue();
+        // current discharge law from linked nub
+        this.findAndSetDefaultValue(LoiDebit[this.loiDebit]);
     }
 }
diff --git a/src/app/formulaire/elements/select/select-field-factory.ts b/src/app/formulaire/elements/select/select-field-factory.ts
index c7ae60316..49fed0c91 100644
--- a/src/app/formulaire/elements/select/select-field-factory.ts
+++ b/src/app/formulaire/elements/select/select-field-factory.ts
@@ -13,12 +13,24 @@ import { SelectFieldTargetPass } from "./select-field-target-pass";
 import { SelectFieldNubProperty } from "./select-field-nub-prop";
 
 export class SelectFieldFactory {
+    /**
+      * Id of the select linked to structure type (JSON calculator configuration).
+      * Also used in fieldset template parsing.
+      */
+    public static readonly SelectId_StructureType: string ="select_structure";
+
+    /**
+     * Id of the select linked to stage discharge law (JSON calculator configuration).
+     * Also used in fieldset template parsing.
+     */
+    public static readonly SelectId_LoiDebit: string ="select_loidebit";
+
     public static newSelectField(json: {}, parent: FormulaireNode): SelectField {
         switch (json["id"]) {
             case "select_downstream_basin":
                 return new SelectFieldDownstreamBasin(parent);
 
-            case "select_loidebit":
+            case SelectFieldFactory.SelectId_LoiDebit:
                 return new SelectFieldDeviceLoiDebit(parent);
 
             case "select_searched_param":
@@ -27,7 +39,7 @@ export class SelectFieldFactory {
             case "select_species_list":
                 return new SelectFieldSpeciesList(parent);
 
-            case "select_structure":
+            case SelectFieldFactory.SelectId_StructureType:
                 return new SelectFieldDeviceStructureType(parent);
 
             case "select_target":
diff --git a/src/app/formulaire/elements/select/select-field.ts b/src/app/formulaire/elements/select/select-field.ts
index f76690de4..5e490c754 100644
--- a/src/app/formulaire/elements/select/select-field.ts
+++ b/src/app/formulaire/elements/select/select-field.ts
@@ -125,11 +125,13 @@ export abstract class SelectField extends Field {
     /**
      * try to find a default value to select
      */
-    protected findAndSetDefaultValue() {
+    protected findAndSetDefaultValue(value?: string) {
         // default to first available entry if any
         if (this._entries.length > 0) {
-            let val;
-            if (this._configDefaultValue === undefined) {
+            let val: SelectEntry;
+            if (value !== undefined) {
+                val = this.getEntryFromValue(enumValueFromString(this._associatedProperty, value));
+            } else if (this._configDefaultValue === undefined) {
                 val = this._entries[0];
             } else {
                 val = this.getEntryFromValue(enumValueFromString(this._associatedProperty, this._configDefaultValue));
-- 
GitLab