diff --git a/e2e/calculate-button-validation.e2e-spec.ts b/e2e/calculate-button-validation.e2e-spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..137071d830e0addd09d5f2f0df79d50930c58b91
--- /dev/null
+++ b/e2e/calculate-button-validation.e2e-spec.ts
@@ -0,0 +1,145 @@
+import { ListPage } from "./list.po";
+import { CalculatorPage } from "./calculator.po";
+import { Navbar } from "./navbar.po";
+import { browser, by, element } from "protractor";
+import { PreferencesPage } from "./preferences.po";
+
+describe("Calculate button - ", () => {
+    let listPage: ListPage;
+    let calcPage: CalculatorPage;
+    let navBar: Navbar;
+    let prefPage: PreferencesPage;
+
+    beforeAll(async () => {
+        prefPage = new PreferencesPage();
+        listPage = new ListPage();
+        calcPage = new CalculatorPage();
+        navBar = new Navbar();
+    });
+
+    beforeEach(async () => {
+        await prefPage.navigateTo();
+        // disable evil option "empty fields on module creation"
+        await prefPage.disableEvilEmptyFields();
+        await browser.sleep(200);
+    });
+
+    it("check button status only depends on calculator (no link between calculators)", async () => {
+        // start page
+        await navBar.clickNewCalculatorButton();
+        await browser.sleep(200);
+
+        // open PAB: chute calculator
+        await listPage.clickMenuEntryForCalcType(12);
+        await browser.sleep(200);
+
+        // start page
+        await navBar.clickNewCalculatorButton();
+        await browser.sleep(200);
+
+        // open PAB: dimensions
+        await listPage.clickMenuEntryForCalcType(5);
+        await browser.sleep(200);
+
+        // fill width field with invalid data
+        const inputW = calcPage.getInputById("W");
+        await inputW.clear();
+        await browser.sleep(20);
+        await inputW.sendKeys("-1");
+        await browser.sleep(200);
+        debugger
+        // check that "compute" button is inactive
+        let calcButtonClone = calcPage.getCalculateButton();
+        let disabledStateClone = await calcButtonClone.getAttribute("disabled");
+        expect(disabledStateClone).toBe("true");
+
+        // back to PAB: chute
+        await navBar.clickCalculatorTab(0);
+        await browser.sleep(200);
+
+        // check that "compute" button is active
+        calcButtonClone = calcPage.getCalculateButton();
+        disabledStateClone = await calcButtonClone.getAttribute("disabled");
+        expect(disabledStateClone).not.toBe("true");
+
+        // back to PAB: dimensions
+        await navBar.clickCalculatorTab(1);
+        await browser.sleep(200);
+
+        // check that "compute" button is inactive
+        calcButtonClone = calcPage.getCalculateButton();
+        disabledStateClone = await calcButtonClone.getAttribute("disabled");
+        expect(disabledStateClone).toBe("true");
+    });
+
+    describe("check button status in prébarrages - ", () => {
+        it("invalid data in Q input", async () => {
+            // start page
+            await navBar.clickNewCalculatorButton();
+            await browser.sleep(200);
+
+            // open prébarrages calculator
+            await listPage.clickMenuEntryForCalcType(30);
+            await browser.sleep(200);
+
+            // Q input
+            const inputQ = element(by.id("Q"));
+            await inputQ.clear();
+            await browser.sleep(200);
+            await inputQ.sendKeys("-1");
+            await browser.sleep(200);
+
+            calcPage.checkCalcButtonEnabled(false);
+
+            // upstream item
+            const upstream = element(by.id("amont"));
+            // should be displayed in error
+            expect(await upstream.getAttribute('class')).toContain("node-error");
+        });
+
+        it("add basin, invalid data in Q input", async () => {
+            // start page
+            await navBar.clickNewCalculatorButton();
+            await browser.sleep(200);
+
+            // open prébarrages calculator
+            await listPage.clickMenuEntryForCalcType(30);
+            await browser.sleep(200);
+
+            // "add basin" button
+            const addBasinBtn = element(by.id("add-basin"));
+            await addBasinBtn.click();
+            await browser.sleep(200);
+
+            // upstream item
+            const upstream = element(by.id("amont"));
+            await upstream.click();
+            await browser.sleep(200);
+
+            // invalid data in Q input
+            const inputQ = element(by.id("Q"));
+            await inputQ.clear();
+            await browser.sleep(200);
+            await inputQ.sendKeys("-1");
+            await browser.sleep(200);
+
+            // calculate button disabled ?
+            calcPage.checkCalcButtonEnabled(false);
+
+            // upstream item displayed in error ?
+            expect(await upstream.getAttribute('class')).toContain("node-error");
+
+            // valid data in Q input
+            await inputQ.clear();
+            await browser.sleep(200);
+            await inputQ.sendKeys("1");
+            await browser.sleep(200);
+
+            // calculate button still disabled ? (the basin is not connected to anything)
+            calcPage.checkCalcButtonEnabled(false);
+
+            // upstream item displayed not in error ?
+            expect(await upstream.getAttribute('class')).not.toContain("node-error");
+        });
+    });
+});
diff --git a/e2e/calculator.po.ts b/e2e/calculator.po.ts
index 4be6378862b7589d86a2314e73a9f1a00ffa8b34..757a2fa707aec856605435c2f14dd7cd7dda971a 100644
--- a/e2e/calculator.po.ts
+++ b/e2e/calculator.po.ts
@@ -221,6 +221,13 @@ export class CalculatorPage {
         return await cloneButton.click();
     }
 
+    // check that "compute" button is in given enabled/disabled state
+    checkCalcButtonEnabled(enabled: boolean) {
+        const calcButton = this.getCalculateButton();
+        expect(calcButton.isEnabled()).toBe(enabled);
+        return calcButton;
+    }
+
     async changeSelectValue(elt: ElementFinder, index: number) {
         await elt.click();
         const optionId = ".cdk-overlay-container mat-option:nth-of-type(" + (index + 1) + ")";
diff --git a/jalhyd_branch b/jalhyd_branch
index 554b541df35b8453451cb15ae370893ed9d6d49c..d64531f1305e091791eac674c3a36d86b9e17ddd 100644
--- a/jalhyd_branch
+++ b/jalhyd_branch
@@ -1 +1 @@
-308-log-ameliorer-la-synthese-de-journal
\ No newline at end of file
+devel
diff --git a/src/app/components/field-set/field-set.component.ts b/src/app/components/field-set/field-set.component.ts
index d4a614ab8aa982499eed43ba410e4f0b49900be4..1bde22b4165daa0a0064181ee0804a36359a9e5d 100644
--- a/src/app/components/field-set/field-set.component.ts
+++ b/src/app/components/field-set/field-set.component.ts
@@ -16,6 +16,7 @@ import { I18nService } from "../../services/internationalisation.service";
 import { sprintf } from "sprintf-js";
 
 import { capitalize } from "jalhyd";
+import { DefinedBoolean } from "app/definedvalue/definedboolean";
 
 @Component({
     selector: "field-set",
@@ -51,7 +52,7 @@ export class FieldSetComponent implements DoCheck {
     }
 
     public get isValid() {
-        return this._isValid;
+        return this._isValid.value;
     }
 
     /** flag d'affichage des boutons ajouter, supprimer, monter, descendre */
@@ -133,7 +134,7 @@ export class FieldSetComponent implements DoCheck {
     /**
      * flag de validité de la saisie
      */
-    private _isValid = false;
+    private _isValid: DefinedBoolean;
 
     /**
      * événement de changement d'état d'un radio
@@ -145,11 +146,18 @@ export class FieldSetComponent implements DoCheck {
     @Output()
     protected tabPressed = new EventEmitter<any>();
 
+    /**
+     * nombre d'appels à DoCheck
+     */
+    private _DoCheckCount: number = 0;
+
     public constructor(
         private notifService: NotificationsService,
         private i18nService: I18nService,
         private appSetupService: ApplicationSetupService
-    ) { }
+    ) {
+        this._isValid = new DefinedBoolean();
+    }
 
     public hasRadioFix(): boolean {
         if (this._fieldSet.hasInputs) {
@@ -223,8 +231,7 @@ export class FieldSetComponent implements DoCheck {
      * calcul de la validité de tous les ParamFieldLineComponent et tous les
      * SelectFieldLineComponent de la vue
      */
-    private updateValidity() {
-        this._isValid = false;
+    private computeValidity(): boolean {
         let paramsAreValid = true;
         let selectAreValid = true;
 
@@ -261,9 +268,15 @@ export class FieldSetComponent implements DoCheck {
         }
 
         // global validity
-        this._isValid = (paramsAreValid && selectAreValid);
+        return (paramsAreValid && selectAreValid);
+    }
 
-        this.validChange.emit();
+    private updateValidity(forceEmit: boolean = false) {
+        // global validity
+        this._isValid.value = this.computeValidity();
+        if (forceEmit || this._isValid.changed) {
+            this.validChange.emit();
+        }
     }
 
     /**
@@ -274,7 +287,12 @@ export class FieldSetComponent implements DoCheck {
     }
 
     public ngDoCheck() {
-        this.updateValidity();
+        this._DoCheckCount++;
+        // à priori, DoCheck n'est plus utile après quelques cycles de détection de changement
+        // puisque la validité du fieldset est déterminée par les saisies dans les inputs
+        if (this._DoCheckCount < 3) {
+            this.updateValidity(true);
+        }
     }
 
     /**
diff --git a/src/app/components/fieldset-container/fieldset-container.component.ts b/src/app/components/fieldset-container/fieldset-container.component.ts
index 630da2d19d881be096cadcd8f9e349be0281b85c..c98450cdcb53236feb992f69304ac6e7f21b405b 100644
--- a/src/app/components/fieldset-container/fieldset-container.component.ts
+++ b/src/app/components/fieldset-container/fieldset-container.component.ts
@@ -6,6 +6,7 @@ import { FieldSet } from "../../formulaire/elements/fieldset";
 import { FormulaireDefinition } from "../../formulaire/definition/form-definition";
 import { I18nService } from "../../services/internationalisation.service";
 import { ApplicationSetupService } from "../../services/app-setup.service";
+import { DefinedBoolean } from "app/definedvalue/definedboolean";
 
 @Component({
     selector: "fieldset-container",
@@ -27,7 +28,7 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit {
     }
 
     public get isValid() {
-        return this._isValid;
+        return this._isValid.value;
     }
     @Input()
     private _container: FieldsetContainer;
@@ -41,7 +42,7 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit {
     /**
      * flag de validité des FieldSet enfants
      */
-    private _isValid = false;
+    private _isValid: DefinedBoolean;
 
     /**
      * événément de changement d'état d'un radio
@@ -65,10 +66,17 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit {
     @Output()
     protected tabPressed = new EventEmitter<any>();
 
+    /**
+     * nombre d'appels à DoCheck
+     */
+    private _DoCheckCount: number = 0;
+
     public constructor(
         private i18nService: I18nService,
         private appSetupService: ApplicationSetupService
-    ) {}
+    ) {
+        this._isValid = new DefinedBoolean();
+    }
 
     /**
      * Ajoute un nouveau sous-nub (Structure, PabCloisons, YAXN… selon le cas)
@@ -105,17 +113,22 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit {
     }
 
     public ngDoCheck() {
-        this.updateValidity();
+        this._DoCheckCount++;
+        // à priori, DoCheck n'est plus utile après quelques cycles de détection de changement
+        // puisque la validité du fieldset container est déterminée par les saisies dans les inputs
+        if (this._DoCheckCount < 3) {
+            this.updateValidity(true);
+        }
     }
 
     /**
     * calcul de la validité de tous les FieldSet de la vue
     */
-    private updateValidity() {
-        this._isValid = false;
+    private computeValidity(): boolean {
+        let res = false;
 
         if (this._fieldsetComponents?.length > 0) {
-            this._isValid = this._fieldsetComponents.reduce(
+            res = this._fieldsetComponents.reduce(
                 // callback
                 (
                     // accumulator (valeur précédente du résultat)
@@ -133,10 +146,18 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit {
                 , this._fieldsetComponents.length > 0);
         } else {
             // empty / hidden container ? everything OK.
-            this._isValid = true;
+            res = true;
         }
 
-        this.validChange.emit();
+        return res;
+    }
+
+    private updateValidity(forceEmit: boolean = false) {
+        // global validity
+        this._isValid.value = this.computeValidity();
+        if (forceEmit || this._isValid.changed) {
+            this.validChange.emit();
+        }
     }
 
     /**
diff --git a/src/app/components/generic-calculator/calculator.component.ts b/src/app/components/generic-calculator/calculator.component.ts
index 9650543c6a0add5f27c5b2c5bb7aa61ea0e44f64..8b7f21b2a9b07e7fb2abf7554afe3f4929ef100b 100644
--- a/src/app/components/generic-calculator/calculator.component.ts
+++ b/src/app/components/generic-calculator/calculator.component.ts
@@ -61,6 +61,7 @@ import { sprintf } from "sprintf-js";
 
 import * as XLSX from "xlsx";
 import { ServiceFactory } from "app/services/service-factory";
+import { DefinedBoolean } from "app/definedvalue/definedboolean";
 
 @Component({
     selector: "hydrocalc",
@@ -110,7 +111,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
      * La validité de l'UI comprend la forme (pas de chaîne alpha dans les champs numériques, etc..).
      * La validité formulaire comprend le domaine de définition des valeurs saisies.
      */
-    private _isUIValid = false;
+    private _isUIValid: DefinedBoolean;
 
     /**
      * flag disabled du bouton "calculer"
@@ -158,6 +159,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
         private formulaireService: FormulaireService,
         private matomoTracker: MatomoTracker
     ) {
+        this._isUIValid = new DefinedBoolean();
         // hotkeys listeners
         this.hotkeysService.add(new Hotkey("alt+w", AppComponent.onHotkey(this.closeCalculator, this)));
         this.hotkeysService.add(new Hotkey("alt+d", AppComponent.onHotkey(this.cloneCalculator, this)));
@@ -331,7 +333,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
      * the UI validity state)
      */
     ngDoCheck() {
-        this.isCalculateDisabled = !this._isUIValid;
+        this.isCalculateDisabled = !this._isUIValid.value;
     }
 
     ngOnDestroy() {
@@ -473,12 +475,12 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
      * calcul de la validité globale de la vue
      */
     private updateUIValidity() {
-        this._isUIValid = false;
+        let res = false;
         if (!this._formulaire.calculateDisabled) {
             // all fieldsets must be valid
-            this._isUIValid = true;
+            res = true;
             if (this._fieldsetComponents !== undefined) {
-                this._isUIValid = this._isUIValid && this._fieldsetComponents.reduce(
+                res = res && this._fieldsetComponents.reduce(
                     // callback
                     (
                         // accumulator (valeur précédente du résultat)
@@ -497,7 +499,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
             }
             // all fieldset containers must be valid
             if (this._fieldsetContainerComponents !== undefined) {
-                this._isUIValid = this._isUIValid && this._fieldsetContainerComponents.reduce<boolean>(
+                res = res && this._fieldsetContainerComponents.reduce<boolean>(
                     // callback
                     (
                         // accumulator (valeur précédente du résultat)
@@ -516,16 +518,21 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
             }
             // special components must be valid
             if (this._pabTableComponent !== undefined) {
-                this._isUIValid = this._isUIValid && this._pabTableComponent.isValid;
+                res = res && this._pabTableComponent.isValid;
             }
             if (this._pbSchemaComponent !== undefined) {
-                this._isUIValid = this._isUIValid && this._pbSchemaComponent.isValid;
+                res = res && this._pbSchemaComponent.isValid;
             }
             if (this._formulaire.currentNub.calcType === CalculatorType.PreBarrage) {
                 const form: FormulairePrebarrage = this._formulaire as FormulairePrebarrage;
-                this._isUIValid = this._isUIValid && form.checkParameters().length === 0;
+                res = res && form.checkParameters().length === 0;
             }
         }
+
+        this._isUIValid.value = res;
+
+        // update prébarrage schema validity
+        this._pbSchemaComponent?.updateItemsValidity();
     }
 
     public getElementStyleDisplay(id: string) {
diff --git a/src/app/components/generic-input/generic-input.component.ts b/src/app/components/generic-input/generic-input.component.ts
index c30e69123c3fdd550ca1706144355a280100c1a9..3154a308fbdf1b9a83750ca2668e046f5bf5f578 100644
--- a/src/app/components/generic-input/generic-input.component.ts
+++ b/src/app/components/generic-input/generic-input.component.ts
@@ -5,6 +5,7 @@ import { FormulaireDefinition } from "../../formulaire/definition/form-definitio
 import { NgParameter } from "../../formulaire/elements/ngparam";
 import { I18nService } from "../../services/internationalisation.service";
 import { ApplicationSetupService } from "../../services/app-setup.service";
+import { DefinedBoolean } from "app/definedvalue/definedboolean";
 
 /**
  * classe de gestion générique d'un champ de saisie avec titre, validation et message d'erreur
@@ -79,6 +80,11 @@ export abstract class GenericInputComponentDirective implements OnChanges {
      */
     private _isValidModel = false;
 
+    /**
+     * flag de validité globale
+     */
+    private _isValid: DefinedBoolean;
+
     /**
      * message d'erreur UI
      */
@@ -96,7 +102,9 @@ export abstract class GenericInputComponentDirective implements OnChanges {
         private cdRef: ChangeDetectorRef,
         protected intlService: I18nService,
         protected appSetupService: ApplicationSetupService
-    ) { }
+    ) {
+        this._isValid = new DefinedBoolean();
+    }
 
     public get isDisabled(): boolean {
         if (this._model instanceof NgParameter) {
@@ -107,10 +115,13 @@ export abstract class GenericInputComponentDirective implements OnChanges {
     }
 
     /**
-     * événement de changement de la validité de la saisie
+     * modification et émission d'un événement de changement de la validité
      */
-    private emitValidChanged() {
-        this.change.emit({ "action": "valid", "value": this.isValid });
+    private setAndEmitValid() {
+        this._isValid.value = this._isValidUI && this._isValidModel;
+        if (this._isValid.changed) {
+            this.change.emit({ "action": "valid", "value": this._isValid.value });
+        }
     }
 
     /**
@@ -128,15 +139,12 @@ export abstract class GenericInputComponentDirective implements OnChanges {
      * calcul de la validité globale du composant (UI+modèle)
      */
     public get isValid() {
-        return this._isValidUI && this._isValidModel;
+        return this._isValid.value;
     }
 
-    private setUIValid(b: boolean) {
-        const old = this.isValid;
+    protected setUIValid(b: boolean) {
         this._isValidUI = b;
-        if (this.isValid !== old) {
-            this.emitValidChanged();
-        }
+        this.setAndEmitValid();
     }
 
     protected validateUI() {
@@ -147,12 +155,10 @@ export abstract class GenericInputComponentDirective implements OnChanges {
         return isValid;
     }
 
-    private setModelValid(b: boolean) {
-        const old = this.isValid;
+    protected setModelValid(b: boolean) {
         this._isValidModel = b;
-        if (this.isValid !== old) {
-            this.emitValidChanged();
-        }
+        this.setAndEmitValid();
+
         // répercussion des erreurs sur le Form angular, pour faire apparaître/disparaître les mat-error
         // setTimeout(() => {  // en cas de pb, décommenter le timeout
         if (b) {
diff --git a/src/app/components/ngparam-input/ngparam-input.component.ts b/src/app/components/ngparam-input/ngparam-input.component.ts
index 781e29b15614cb04192292ed2c465c22c6939f95..c5d6d9809a4437af1efac6caff16b238337c984b 100644
--- a/src/app/components/ngparam-input/ngparam-input.component.ts
+++ b/src/app/components/ngparam-input/ngparam-input.component.ts
@@ -116,6 +116,26 @@ export class NgParamInputComponent extends GenericInputComponentDirective implem
         return { isValid: valid, message: msg };
     }
 
+    private undefineModel() {
+        if (this.getModelValue() !== undefined) {
+            this.setModelValue(this, undefined);
+        }
+    }
+
+    protected setModelValid(b: boolean) {
+        if (!b) {
+            this.undefineModel();
+        }
+        super.setModelValid(b);
+    }
+
+    protected setUIValid(b: boolean) {
+        if (!b) {
+            this.undefineModel();
+        }
+        super.setUIValid(b);
+    }
+
     public update(sender: any, data: any): void {
         switch (data["action"]) {
             case "ngparamAfterValue":
@@ -151,9 +171,7 @@ export class NgParamInputComponent extends GenericInputComponentDirective implem
     }
 
     public ngOnDestroy() {
-        if (!this.isValid && this.getModelValue() !== undefined) {
-            this.setModelValue(this, undefined);
-        }
+        // résoudre le conflit en supprimant le code ajouté cad ne conserver que removeObserver()
         this._paramDef.removeObserver(this);
     }
 }
diff --git a/src/app/components/pab-table/pab-table.component.ts b/src/app/components/pab-table/pab-table.component.ts
index 486789521a32d7efb0cf3e782f7f1be51bbf30ac..7d95042fbe040b74b0ce78be4079036ccdc2478e 100644
--- a/src/app/components/pab-table/pab-table.component.ts
+++ b/src/app/components/pab-table/pab-table.component.ts
@@ -28,6 +28,7 @@ import { PabTable } from "../../formulaire/elements/pab-table";
 import { DialogEditPabComponent } from "../dialog-edit-pab/dialog-edit-pab.component";
 import { AppComponent } from "../../app.component";
 import { NgParameter, ParamRadioConfig } from "../../formulaire/elements/ngparam";
+import { DefinedBoolean } from "app/definedvalue/definedboolean";
 
 /**
  * The big editable data grid for calculator type "Pab" (component)
@@ -45,7 +46,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni
     private pabTable: PabTable;
 
     /** flag de validité des FieldSet enfants */
-    private _isValid = false;
+    private _isValid: DefinedBoolean;
 
     /** événément de changement de validité */
     @Output()
@@ -84,6 +85,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni
         private notifService: NotificationsService
     ) {
         this.selectedItems = [];
+        this._isValid = new DefinedBoolean();
     }
 
     /** update vary value from pab fish ladder and unable compute Button */
@@ -98,7 +100,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni
 
     /** Global Pab validity */
     public get isValid() {
-        return this._isValid;
+        return this._isValid.value;
     }
 
     /** returns true if the cell has an underlying model (ie. is editable) */
@@ -1440,14 +1442,22 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni
     /**
      * Computes the global Pab validity : validity of every cell of every row
      */
-    private updateValidity() {
-        this._isValid = true;
+    private computeValidity(): boolean {
+        let res = true;
         for (const r of this.rows) {
             for (const c of r.cells) {
-                this._isValid = this._isValid && ! this.isInvalid(c);
+                res = res && !this.isInvalid(c);
             }
         }
-        this.validChange.emit();
+
+        return res;
+    }
+
+    private updateValidity() {
+        this._isValid.value = this.computeValidity();
+        if (this._isValid.changed) {
+            this.validChange.emit();
+        }
     }
 
     public get uitextEditPabTable() {
diff --git a/src/app/components/param-field-line/param-field-line.component.html b/src/app/components/param-field-line/param-field-line.component.html
index daad0ba4b73516eb51066cd77ed629066571ffe4..a9114f5f2974450548c16151f381f711af3adb29 100644
--- a/src/app/components/param-field-line/param-field-line.component.html
+++ b/src/app/components/param-field-line/param-field-line.component.html
@@ -12,13 +12,11 @@
         </param-computed>
 
         <!-- composant pour gérer le cas "paramètre à varier" (min-max/liste de valeurs) -->
-        <param-values *ngIf="isRadioVarChecked" [title]="title" [param]="param" (change)="onInputChange($event)"
-            (valid)=onParamValuesValid($event)>
+        <param-values *ngIf="isRadioVarChecked" [title]="title" [param]="param" (change)="onInputChange($event)">
         </param-values>
 
         <!-- composant pour gérer le cas "paramètre lié" -->
-        <param-link *ngIf="isRadioLinkChecked" [title]="title" [param]="param" (change)="onInputChange($event)"
-            (valid)=onParamValuesValid($event)>
+        <param-link *ngIf="isRadioLinkChecked" [title]="title" [param]="param" (change)="onInputChange($event)">
         </param-link>
     </div>
 
diff --git a/src/app/components/param-field-line/param-field-line.component.ts b/src/app/components/param-field-line/param-field-line.component.ts
index 071aa1fd9f9f6a03e8b668b4708efe7529b800ed..0c68a61120b4a2a02c1c523896277eeba9fe8083 100644
--- a/src/app/components/param-field-line/param-field-line.component.ts
+++ b/src/app/components/param-field-line/param-field-line.component.ts
@@ -316,14 +316,6 @@ export class ParamFieldLineComponent implements OnChanges {
         }
     }
 
-    /**
-     * réception d'un événement de validité de ParamValuesComponent
-     */
-    public onParamValuesValid(event: boolean) {
-        this._isRangeValid = event;
-        this.emitValidity();
-    }
-
     public ngOnChanges() {
         this._ngParamInputComponent.model = this.param;
         this._ngParamInputComponent.showError = this.isRadioFixChecked;
diff --git a/src/app/components/param-link/param-link.component.ts b/src/app/components/param-link/param-link.component.ts
index 89255ef13580199816bf9f40f4a85a5ce31ea0e9..2571d9ec7cd0fab288fc147345c7e5ced4352a59 100644
--- a/src/app/components/param-link/param-link.component.ts
+++ b/src/app/components/param-link/param-link.component.ts
@@ -22,9 +22,6 @@ export class ParamLinkComponent implements OnChanges, Observer, OnDestroy {
     @Input()
     public title: string;
 
-    @Output()
-    public valid: EventEmitter<boolean>;
-
     /**
      * événement signalant un changement de valeur du modèle
      * @TODO l'utiliser aussi pour le changement de validité à
@@ -83,7 +80,6 @@ export class ParamLinkComponent implements OnChanges, Observer, OnDestroy {
         private intlService: I18nService,
         private formService: FormulaireService
     ) {
-        this.valid = new EventEmitter();
         this.formService.addObserver(this);
     }
 
diff --git a/src/app/components/pb-schema/pb-schema.component.ts b/src/app/components/pb-schema/pb-schema.component.ts
index 7ea4ba81d47b6bbaede7cb478cbf84bef38b4b36..b2f00304cfc517b08766edf36ff7afa9808d46ec 100644
--- a/src/app/components/pb-schema/pb-schema.component.ts
+++ b/src/app/components/pb-schema/pb-schema.component.ts
@@ -22,6 +22,7 @@ import { AppComponent } from "../../app.component";
 import { fv } from "app/util";
 import { FormulaireNode } from "app/formulaire/elements/formulaire-node";
 import { ServiceFactory } from "app/services/service-factory";
+import { DefinedBoolean } from "app/definedvalue/definedboolean";
 
 /**
  * The interactive schema for calculator type "PreBarrage" (component)
@@ -45,7 +46,7 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni
     private nativeElement: any;
 
     /** flag de validité du composant */
-    private _isValid = false;
+    private _isValid: DefinedBoolean;
 
     private upstreamId = "amont";
 
@@ -75,6 +76,7 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni
         private newPbCloisonDialog: MatDialog
     ) {
         this.hotkeysService.add(new Hotkey("del", AppComponent.onHotkey(this.removeOnHotkey, this)));
+        this._isValid = new DefinedBoolean();
     }
 
     /** tracks the fullscreen state */
@@ -303,6 +305,9 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni
         return (sommeA <= sommeB ? -1 : 1);
     }
 
+    /**
+     * @param item DOM element
+     */
     private selectNode(item: any) {
         // console.debug(`PbSchemaComponent.selectNode(${item?.id})`);
         // highlight clicked element
@@ -334,7 +339,7 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni
 
     /** Global Pb validity */
     public get isValid() {
-        return this._isValid;
+        return this._isValid.value;
     }
 
     /** used for a cosmetics CSS trick only (mat-card-header right margin) */
@@ -643,11 +648,18 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni
     private updateValidity() {
         // check that at least 1 basin is present and a route from river
         // upstream to river downstream exists (2nd check includes 1st)
-        this._isValid = (
-            this.model.hasUpDownConnection()
-            && ! this.model.hasBasinNotConnected()
-        );
-        this.validChange.emit();
+        this._isValid.value = this.model.hasUpDownConnection() && !this.model.hasBasinNotConnected();
+
+        if (this._isValid.changed) {
+            this.validChange.emit();
+        }
+    }
+
+    /**
+     * update all items validity rendering
+     */
+    public updateItemsValidity() {
+        this.highlightErrorItems(this._selectedItem?.uid);
     }
 
     private clearHighlightedItems() {
@@ -662,21 +674,24 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni
             item.classList.remove("node-highlighted-error");
         });
         const invalidUids: string[] = this.pbSchema.form.checkParameters();
-        this.nativeElement.querySelectorAll("g.node").forEach(item => {
-            let itemId: string;
-            if ([this.upstreamId, this.downstreamId].includes(item.id)) {
-                itemId = this.model.uid;
-            } else {
-                itemId = item.id
-            }
-            if (invalidUids.includes(itemId)) {
-                if (item.id === selectedUid) {
-                    item.classList.add("node-highlighted-error");
+        if (invalidUids.length > 0) {
+            this.nativeElement.querySelectorAll("g.node").forEach(item => {
+                // in this case, item is a HTML node of the SVG schema which id is a nud uid
+                let itemId: string;
+                if ([this.upstreamId, this.downstreamId].includes(item.id)) {
+                    itemId = this.model.uid;
                 } else {
-                    item.classList.add("node-error");
+                    itemId = item.id
                 }
-            }
-        });
+                if (invalidUids.includes(itemId)) {
+                    if (item.id === selectedUid) {
+                        item.classList.add("node-highlighted-error");
+                    } else {
+                        item.classList.add("node-error");
+                    }
+                }
+            });
+        }
     }
 
     private unselect() {
diff --git a/src/app/definedvalue/definedboolean.ts b/src/app/definedvalue/definedboolean.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2d8d9a3b836c908b44a62f5e485af428b8f81a60
--- /dev/null
+++ b/src/app/definedvalue/definedboolean.ts
@@ -0,0 +1,7 @@
+import { DefinedValue } from "./definedvalue";
+
+/**
+ * boolean value with initialised, changed, defined states
+ */
+export class DefinedBoolean extends DefinedValue<boolean> {
+}
diff --git a/src/app/definedvalue/definedvalue.ts b/src/app/definedvalue/definedvalue.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1e72754b0a76aa759ac722746da4e9c0e2678aba
--- /dev/null
+++ b/src/app/definedvalue/definedvalue.ts
@@ -0,0 +1,46 @@
+/**
+ * value management with initialised, changed and defined states
+ */
+export abstract class DefinedValue<T> {
+    private _initialised: boolean;
+
+    private _value: T;
+
+    private _changed: boolean;
+
+    constructor() {
+        this._initialised = false;
+        this._changed = false;
+    }
+
+    /**
+     * @returns true if setter has been called at least once
+     */
+    public get initialised(): boolean {
+        return this._initialised;
+    }
+
+    /**
+     * @returns true if value is not undefined
+     */
+    public get defined(): boolean {
+        return this._value !== undefined;
+    }
+
+    /**
+     * @returns true if value has been modified by last call to setter
+     */
+    public get changed(): boolean {
+        return this._changed;
+    }
+
+    public get value(): T {
+        return this._value;
+    }
+
+    public set value(v: T) {
+        this._changed = this._value !== v;
+        this._initialised = true;
+        this._value = v;
+    }
+}