From e90f1680d1872971bdb2a7149276cf4e647532fe Mon Sep 17 00:00:00 2001
From: "mathias.chouet" <mathias.chouet@irstea.fr>
Date: Wed, 30 Jan 2019 16:08:00 +0100
Subject: [PATCH] Premier formulaire Angular Material : app-setup
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

formulaire "template-driven"
validation par le modèle JaLHyd (synchrone ou asynchrone)
2-way binding sur NgParam directement plutôt que BaseParamInputComponent
---
 src/app/app.component.html                    |   9 +-
 src/app/app.component.scss                    |  13 +-
 src/app/app.module.ts                         |  15 ++-
 .../app-setup/app-setup.component.html        | 122 +++++++++++-------
 .../app-setup/app-setup.component.scss        |  50 +++++++
 .../app-setup/app-setup.component.ts          |  54 ++++----
 .../base-param-input.component.ts             |  49 ++++---
 .../calculator-list.component.scss            |   2 +-
 .../generic-input/generic-input.component.ts  |   2 +-
 ...jalhyd-async-model-validation.directive.ts |  37 ++++++
 .../jalhyd-model-validation.directive.ts      |  39 ++++++
 .../immediate-error-state-matcher.ts          |  16 +++
 src/locale/messages.en.json                   |   2 +
 src/locale/messages.fr.json                   |   2 +
 src/styles.scss                               |  12 +-
 15 files changed, 320 insertions(+), 104 deletions(-)
 create mode 100644 src/app/components/app-setup/app-setup.component.scss
 create mode 100644 src/app/directives/jalhyd-async-model-validation.directive.ts
 create mode 100644 src/app/directives/jalhyd-model-validation.directive.ts
 create mode 100644 src/app/formulaire/immediate-error-state-matcher.ts

diff --git a/src/app/app.component.html b/src/app/app.component.html
index 2df5abc73..21d15e808 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -3,14 +3,11 @@
 <header>
 
   <mat-toolbar #navbar id="main-toolbar" color="primary">
-    <span class="example-spacer"></span>
-  
+
     <span id="open-menu" style="font-size:30px;cursor:pointer;color:white" (click)="sidenav.toggle()">
       <mat-icon>menu</mat-icon>
     </span>
 
-    <a class="navbar-brand"></a>
-
     <!-- calculators list as a dropdown menu-->
     <div [hidden]="tabsFitInNavbar" id="dropdown-calc-container">
 
@@ -103,7 +100,9 @@
     </mat-sidenav>
 
     <mat-sidenav-content class="sidenav-content" fxFlexFill>
-      <router-outlet (activate)="onRouterOutletActivated($event)"></router-outlet>
+      <div id="app-content">
+        <router-outlet (activate)="onRouterOutletActivated($event)"></router-outlet>
+      </div>
     </mat-sidenav-content>
 
   </mat-sidenav-container>
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index f9ae93896..5cb8ef7a6 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -19,6 +19,10 @@ button:focus {
     z-index: 200;
 }
 
+#open-menu {
+    margin-right: 16px;
+}
+
 #open-menu mat-icon, #new-calculator mat-icon {
     transform: scale(1.6);
 }
@@ -155,6 +159,10 @@ button:focus {
         }
     }
 
+    a {
+        cursor: pointer;
+    }
+
     div.hyd_version {
         position: absolute;
         bottom: 0;
@@ -184,6 +192,9 @@ button:focus {
 
 .sidenav-content {
     margin-top: $navbar_height;
+}
+
+#app-content {
     padding: 1em;
     padding-bottom: 8em;
-}
\ No newline at end of file
+}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 0919fdfb2..974987c22 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -15,7 +15,8 @@ import {
   MatFormFieldModule,
   MatInputModule,
   MatListModule,
-  MatCardModule
+  MatCardModule,
+  ErrorStateMatcher
 } from "@angular/material";
 import { MaterialFileInputModule } from "ngx-material-file-input";
 
@@ -72,6 +73,10 @@ import { DialogConfirmCloseCalcComponent } from "./components/dialog-confirm-clo
 import { DialogLoadSessionComponent } from "./components/dialog-load-session/dialog-load-session.component";
 import { DialogSaveSessionComponent } from "./components/dialog-save-session/dialog-save-session.component";
 
+import { JalhydAsyncModelValidationDirective } from "./directives/jalhyd-async-model-validation.directive";
+import { JalhydModelValidationDirective } from "./directives/jalhyd-model-validation.directive";
+import { ImmediateErrorStateMatcher } from "./formulaire/immediate-error-state-matcher";
+
 const appRoutes: Routes = [
   { path: "list", component: CalculatorListComponent },
   { path: "calculator/:uid", component: GenericCalculatorComponent },
@@ -132,6 +137,8 @@ const appRoutes: Routes = [
     GenericCalculatorComponent,
     GraphTypeSelectComponent,
     HorizontalResultElementComponent,
+    JalhydAsyncModelValidationDirective,
+    JalhydModelValidationDirective,
     LogComponent,
     LogEntryComponent,
     NgParamInputComponent,
@@ -162,7 +169,11 @@ const appRoutes: Routes = [
     FormulaireService,
     HttpService,
     I18nService,
-    ParamService
+    ParamService,
+    {
+      provide: ErrorStateMatcher,
+      useClass: ImmediateErrorStateMatcher
+    }
   ],
   schemas: [ NO_ERRORS_SCHEMA ],
   bootstrap: [ AppComponent ]
diff --git a/src/app/components/app-setup/app-setup.component.html b/src/app/components/app-setup/app-setup.component.html
index 1f6040a14..469669f47 100644
--- a/src/app/components/app-setup/app-setup.component.html
+++ b/src/app/components/app-setup/app-setup.component.html
@@ -1,47 +1,75 @@
-<div class="container-fluid">
-    <div class="row">
-        <div class="col-4 mx-auto">
-            <h1>{{ uitextTitle }}</h1>
-            <br/>
-        </div>
-    </div>
-
-    <!-- précision d'affichage -->
-    <div class="row">
-        <div class="col-4 mx-auto">
-            <base-param-input #displayAccuracy title="{{uitextDisplayAccuracy}}"></base-param-input>
-        </div>
-    </div>
-
-    <!-- précision de calcul -->
-    <div class="row">
-        <div class="col-4 mx-auto">
-            <base-param-input #computeAccuracy title="{{uitextComputeAccuracy}}"></base-param-input>
-        </div>
-    </div>
-
-    <!-- nombre d'itérations max Newton -->
-    <div class="row">
-        <div class="col-4 mx-auto">
-            <base-param-input #newtonMaxIterations title="{{uitextNewtonMaxIteration}}"></base-param-input>
-        </div>
-    </div>
-
-    <!-- langue -->
-    <div class="row">
-        <div class="col-4 mx-auto">
-            <div class="btn-group" dropdown>
-                <button dropdownToggle mdbRippleRadius type="button" class="btn btn-primary dropdown-toggle">
-                    Language ({{ currentLanguageLabel }})
-                    <span class="caret"></span>
-                </button>
-                <ul *dropdownMenu class="dropdown-menu" role="menu">
-                    <li role="menuitem" *ngFor="let l of intlService.languages">
-                        <a class="dropdown-item" (click)="selectLang(l.code)">{{ l.label }}</a>
-                    </li>
-                </ul>
-            </div>
-        </div>
-    </div>
-
-</div>
\ No newline at end of file
+
+<div class="container" fxLayout="row" fxLayoutAlign="center space-evenly">
+  <mat-card id="app-setup">
+
+    <mat-card-header>
+      <mat-card-title>
+        <h1>{{ uitextTitle }}</h1>
+      </mat-card-title>
+    </mat-card-header>
+
+    <mat-card-content>
+      <form>
+
+        <!-- précision d'affichage -->
+        <mat-form-field>
+            <input matInput [placeholder]="uitextDisplayAccuracy" #dp="ngModel" name="dp" inputmode="numeric"
+                [ngModel]="displayPrec.value" (ngModelChange)="!dp.invalid ? displayPrec.setValue($event): null"
+                pattern="-?([0-9]+\.)?[0-9]+" required [appJalhydModelValidation]="displayPrec">
+
+            <mat-error *ngIf="dp.invalid && (dp.dirty || dp.touched)">
+                <div *ngIf="dp.errors.required || dp.errors.pattern">
+                    {{ uitextMustBeANumber }}
+                </div>
+                <div *ngIf="! dp.errors.required && dp.errors.jalhydModel">
+                    {{ dp.errors.jalhydModel.message }}
+                </div>
+            </mat-error>
+        </mat-form-field>
+
+        <!-- précision de calcul -->
+        <mat-form-field>
+            <input matInput [placeholder]="uitextComputeAccuracy" #cp="ngModel" name="cp" inputmode="numeric"
+                [ngModel]="computePrec.value" (ngModelChange)="!cp.invalid ? computePrec.setValue($event): null"
+                pattern="-?([0-9]+\.)?[0-9]+" required [appJalhydModelValidation]="computePrec">
+
+            <mat-error *ngIf="cp.invalid">
+                <div *ngIf="cp.errors.required || cp.errors.pattern">
+                    {{ uitextMustBeANumber }}
+                </div>
+                <div *ngIf="! cp.errors.required && cp.errors.jalhydModel">
+                    {{ cp.errors.jalhydModel.message }}
+                </div>
+            </mat-error>
+        </mat-form-field>
+
+        <!-- nombre d'itérations max Newton -->
+        <mat-form-field>
+            <input matInput [placeholder]="uitextNewtonMaxIteration" #nmi="ngModel" name="nmi" inputmode="numeric"
+                [ngModel]="newtonMaxIter.value" (ngModelChange)="!nmi.invalid ? newtonMaxIter.setValue($event): null"
+                pattern="-?([0-9]+\.)?[0-9]+" required [appJalhydModelValidation]="newtonMaxIter">
+
+            <mat-error *ngIf="nmi.invalid && (nmi.dirty || nmi.touched)">
+                <div *ngIf="nmi.errors.required || nmi.errors.pattern">
+                    {{ uitextMustBeANumber }}
+                </div>
+                <div *ngIf="! nmi.errors.required && nmi.errors.jalhydModel">
+                    {{ nmi.errors.jalhydModel.message }}
+                </div>
+            </mat-error>
+        </mat-form-field>
+
+        <!-- langue -->
+        <mat-form-field>
+            <mat-select placeholder="Language" [(value)]="currentLanguageCode">
+                <mat-option *ngFor="let l of availableLanguages" [value]="l.code">
+                    {{ l.label }}
+                </mat-option>
+            </mat-select>
+        </mat-form-field>
+
+        </form>
+    </mat-card-content>
+
+  </mat-card>
+</div>
diff --git a/src/app/components/app-setup/app-setup.component.scss b/src/app/components/app-setup/app-setup.component.scss
new file mode 100644
index 000000000..f21f04ee0
--- /dev/null
+++ b/src/app/components/app-setup/app-setup.component.scss
@@ -0,0 +1,50 @@
+#app-setup {
+    width: 360px;
+    padding: 3em;
+
+    mat-card-header {
+        margin-bottom: 2em;
+        min-height: 110px;
+
+        // @WARNING ::ng-deep est déprécié, mais y a rien d'autre pour
+        // l'instant (en attente de normalisation par le W3C)
+        ::ng-deep .mat-card-header-text {
+            margin: 0;
+        }
+    }
+
+    mat-form-field {
+        margin-top: 1.2em;
+        display: block;
+
+        ::ng-deep .mat-form-field-label {
+            font-size: 1.2em;
+            line-height: 1.4em;
+            margin-top: -2px;
+
+            &.mat-form-field-empty {
+                font-size: 1em;
+            }
+
+            .mat-form-field-required-marker {
+                display: none; // all fields are mandatory anyway
+            }
+        }
+    }
+
+    mat-select {
+
+        ::ng-deep .mat-select-value {
+            > span {
+                > span {
+                    line-height: 1.3em;
+                }
+            }
+        }
+    }
+
+    mat-error {
+        font-weight: 500;
+        font-size: 1.2em;
+    }
+}
diff --git a/src/app/components/app-setup/app-setup.component.ts b/src/app/components/app-setup/app-setup.component.ts
index 0cc435322..fc05bd484 100644
--- a/src/app/components/app-setup/app-setup.component.ts
+++ b/src/app/components/app-setup/app-setup.component.ts
@@ -1,40 +1,31 @@
-import { Component, ViewChild, OnInit } from "@angular/core";
+import { Component, OnInit } from "@angular/core";
 
 import { ParamDomainValue, Observer } from "jalhyd";
 
 import { ApplicationSetupService } from "../../services/app-setup/app-setup.service";
 import { I18nService, LanguageCode } from "../../services/internationalisation/internationalisation.service";
-import { NgBaseParam, BaseParamInputComponent } from "../base-param-input/base-param-input.component";
+import { NgBaseParam } from "../base-param-input/base-param-input.component";
 import { BaseComponent } from "../base/base.component";
+import { ErrorStateMatcher } from "@angular/material";
+
 
 @Component({
     selector: "setup",
-    templateUrl: "./app-setup.component.html"
+    templateUrl: "./app-setup.component.html",
+    styleUrls: ["./app-setup.component.scss"]
 })
 export class ApplicationSetupComponent extends BaseComponent implements Observer, OnInit {
-    /**
-     * précision d'affichage
-     */
-    private _displayPrec: NgBaseParam;
-
-    /**
-     * précision de calcul
-     */
-    private _computePrec: NgBaseParam;
 
-    /**
-     * nombre d'iterations max Newton
-     */
-    private _newtonMaxIter: NgBaseParam;
+    /** précision d'affichage */
+    public displayPrec: NgBaseParam;
 
-    @ViewChild("displayAccuracy")
-    private _displayAccuracyComponent: BaseParamInputComponent;
+    /** précision de calcul */
+    public computePrec: NgBaseParam;
 
-    @ViewChild("computeAccuracy")
-    private _computeAccuracyComponent: BaseParamInputComponent;
+    /** nombre d'iterations max Newton */
+    public newtonMaxIter: NgBaseParam;
 
-    @ViewChild("newtonMaxIterations")
-    private _newtonMaxIterComponent: BaseParamInputComponent;
+    public matcher: ErrorStateMatcher;
 
     constructor(
         private appSetupService: ApplicationSetupService,
@@ -74,21 +65,22 @@ export class ApplicationSetupComponent extends BaseComponent implements Observer
         return this.intlService.localizeText("INFO_SETUP_NEWTON_MAX_ITER");
     }
 
+    public get uitextMustBeANumber(): string {
+        return this.intlService.localizeText("ERROR_PARAM_MUST_BE_A_NUMBER");
+    }
+
     private init() {
         // modèle du composant BaseParamInputComponent de précision d'affichage
-        this._displayPrec = new NgBaseParam("dp", ParamDomainValue.POS, this.appSetupService.displayPrecision);
-        this._displayPrec.addObserver(this);
-        this._displayAccuracyComponent.model = this._displayPrec;
+        this.displayPrec = new NgBaseParam("dp", ParamDomainValue.POS, this.appSetupService.displayPrecision);
+        this.displayPrec.addObserver(this);
 
         // modèle du composant BaseParamInputComponent de précision de calcul
-        this._computePrec = new NgBaseParam("cp", ParamDomainValue.POS, this.appSetupService.computePrecision);
-        this._computePrec.addObserver(this);
-        this._computeAccuracyComponent.model = this._computePrec;
+        this.computePrec = new NgBaseParam("cp", ParamDomainValue.POS, this.appSetupService.computePrecision);
+        this.computePrec.addObserver(this);
 
         // modèle du composant BaseParamInputComponent du max d'itérations pour Newton
-        this._newtonMaxIter = new NgBaseParam("nmi", ParamDomainValue.POS, this.appSetupService.newtonMaxIterations);
-        this._newtonMaxIter.addObserver(this);
-        this._newtonMaxIterComponent.model = this._newtonMaxIter;
+        this.newtonMaxIter = new NgBaseParam("nmi", ParamDomainValue.POS, this.appSetupService.newtonMaxIterations);
+        this.newtonMaxIter.addObserver(this);
     }
 
     ngOnInit() {
diff --git a/src/app/components/base-param-input/base-param-input.component.ts b/src/app/components/base-param-input/base-param-input.component.ts
index b85fc7abe..a919e31a7 100644
--- a/src/app/components/base-param-input/base-param-input.component.ts
+++ b/src/app/components/base-param-input/base-param-input.component.ts
@@ -6,6 +6,7 @@ import { Message, ParamDefinition, ParamDomain, ParamDomainValue, Observable, is
 
 import { I18nService } from "../../services/internationalisation/internationalisation.service";
 import { GenericInputComponent } from "../generic-input/generic-input.component";
+import { ServiceFactory } from "../../services/service-factory";
 
 export class NgBaseParam extends Observable {
     private _param: ParamDefinition;
@@ -35,6 +36,38 @@ export class NgBaseParam extends Observable {
         this._param.setValue(val);
         this.notifyObservers(val);
     }
+
+    public get value() {
+        return this.getValue();
+    }
+
+    public set value(val: number) {
+        this.setValue(val);
+    }
+
+    public validateModelValue(v: any): { isValid: boolean, message: string } {
+        let msg: string;
+        let valid = false;
+
+        if (v === null || v === "") {
+            // NULL values are always invalid
+            msg = ServiceFactory.instance.i18nService.localizeText("ERROR_PARAM_NULL");
+        } else {
+            try {
+                this._param.checkValue(v);
+                valid = true;
+            } catch (e) {
+                if (e instanceof Message) {
+                    // @TODO ici au début le service de localisation n'a pas encore chargé ses messages…
+                    msg = ServiceFactory.instance.i18nService.localizeMessage(e);
+                } else {
+                    msg = "invalid value";
+                }
+            }
+        }
+
+        return { isValid: valid, message: msg };
+    }
 }
 
 @Component({
@@ -77,21 +110,7 @@ export class BaseParamInputComponent extends GenericInputComponent {
     }
 
     protected validateModelValue(v: any): { isValid: boolean, message: string } {
-        let msg;
-        let valid = false;
-
-        try {
-            this._paramDef.checkValue(v);
-            valid = true;
-        } catch (e) {
-            if (e instanceof Message) {
-                msg = this.intlService.localizeMessage(e);
-            } else {
-                msg = "invalid value";
-            }
-        }
-
-        return { isValid: valid, message: msg };
+        return this._model.validateModelValue(v);
     }
 
     protected modelToUI(v: any): string {
diff --git a/src/app/components/calculator-list/calculator-list.component.scss b/src/app/components/calculator-list/calculator-list.component.scss
index b7e81e487..460061c0f 100644
--- a/src/app/components/calculator-list/calculator-list.component.scss
+++ b/src/app/components/calculator-list/calculator-list.component.scss
@@ -12,7 +12,7 @@ mat-card.compute-nodes-theme {
     }
 
     mat-card-header {
-        min-height: 85px;
+        min-height: 80px;
     }
 
     .mat-card-image-overlay-container {
diff --git a/src/app/components/generic-input/generic-input.component.ts b/src/app/components/generic-input/generic-input.component.ts
index 90c2e7c03..19e797a40 100644
--- a/src/app/components/generic-input/generic-input.component.ts
+++ b/src/app/components/generic-input/generic-input.component.ts
@@ -27,7 +27,7 @@ export abstract class GenericInputComponent extends BaseComponent implements OnC
     /**
      * entité mémoire gérée
      */
-    protected _model: any;
+    protected _model: any; // NgBaseParam mais aussi FormDefinition parfois (!?)
 
     /**
      * flag de désactivation de l'input
diff --git a/src/app/directives/jalhyd-async-model-validation.directive.ts b/src/app/directives/jalhyd-async-model-validation.directive.ts
new file mode 100644
index 000000000..b8c7bebd0
--- /dev/null
+++ b/src/app/directives/jalhyd-async-model-validation.directive.ts
@@ -0,0 +1,37 @@
+import { Validator, AbstractControl, ValidationErrors, NG_ASYNC_VALIDATORS } from "@angular/forms";
+import { Directive, Input, forwardRef } from "@angular/core";
+import { NgBaseParam } from "../components/base-param-input/base-param-input.component";
+import { Observable } from "rxjs";
+
+/**
+ * Asynchronous validator for Ngparam, relying on JaLHyd ParamDefinition model
+ */
+@Directive({
+    selector: "[appAsyncJalhydModelValidation]",
+    providers: [ {
+        provide: NG_ASYNC_VALIDATORS,
+        useExisting: forwardRef(() => JalhydAsyncModelValidationDirective),
+        multi: true
+    } ]
+})
+export class JalhydAsyncModelValidationDirective implements Validator {
+    @Input("appAsyncJalhydModelValidation") ngBaseParam: NgBaseParam;
+
+    validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
+        let errorPromiseReturn = new Promise(res => {
+            res(null);
+        }); // no error, everything OK
+        const result = this.ngBaseParam.validateModelValue(control.value);
+        if (! result.isValid) {
+            errorPromiseReturn = new Promise<ValidationErrors>(res => {
+                res({
+                    "jalhydModel": {
+                        value: control.value,
+                        message: result.message
+                    }
+                });
+            });
+        }
+        return errorPromiseReturn;
+    }
+}
diff --git a/src/app/directives/jalhyd-model-validation.directive.ts b/src/app/directives/jalhyd-model-validation.directive.ts
new file mode 100644
index 000000000..06a9bf673
--- /dev/null
+++ b/src/app/directives/jalhyd-model-validation.directive.ts
@@ -0,0 +1,39 @@
+import { NG_VALIDATORS, Validator, AbstractControl, ValidatorFn } from "@angular/forms";
+import { Directive, Input } from "@angular/core";
+import { NgBaseParam } from "../components/base-param-input/base-param-input.component";
+
+/**
+ * Synchronous validator for Ngparam, relying on JaLHyd ParamDefinition model
+ */
+@Directive({
+    selector: "[appJalhydModelValidation]",
+    providers: [ {
+        provide: NG_VALIDATORS,
+        useExisting: JalhydModelValidationDirective,
+        multi: true
+    } ]
+})
+export class JalhydModelValidationDirective implements Validator {
+    @Input("appJalhydModelValidation") ngBaseParam: NgBaseParam;
+
+    validate(control: AbstractControl): { [key: string]: any } | null {
+        const mv = jalhydModelValidator(this.ngBaseParam)(control);
+        return mv;
+    }
+}
+
+export function jalhydModelValidator(ngBaseParam: NgBaseParam): ValidatorFn {
+    return (control: AbstractControl): { [key: string]: any } | null => {
+        let errorReturn = null; // no error, everything OK
+        const result = ngBaseParam.validateModelValue(control.value);
+        if (result && ! result.isValid) {
+            errorReturn = {
+                "jalhydModel": {
+                    value: control.value,
+                    message: result.message
+                }
+            };
+        }
+        return errorReturn;
+    };
+}
diff --git a/src/app/formulaire/immediate-error-state-matcher.ts b/src/app/formulaire/immediate-error-state-matcher.ts
new file mode 100644
index 000000000..e177f16ef
--- /dev/null
+++ b/src/app/formulaire/immediate-error-state-matcher.ts
@@ -0,0 +1,16 @@
+import { ErrorStateMatcher } from "@angular/material";
+import { FormControl, FormGroupDirective, NgForm } from "@angular/forms";
+
+/**
+ * An error state matcher for Angular Forms that displays errors immediately,
+ * instead of waiting for the focus to get out of the input at least once
+ */
+export class ImmediateErrorStateMatcher implements ErrorStateMatcher {
+
+    constructor() { }
+
+    isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
+        const isSubmitted = form && form.submitted;
+        return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
+    }
+}
diff --git a/src/locale/messages.en.json b/src/locale/messages.en.json
index 2fff7694e..3ba6ffc71 100644
--- a/src/locale/messages.en.json
+++ b/src/locale/messages.en.json
@@ -10,6 +10,8 @@
     "ERROR_INTERVAL_UNDEF": "Interval: invalid 'undefined' value",
     "ERROR_LANG_UNSUPPORTED": "internationalisation: unsupported '%locale%' locale",
     "ERROR_NEWTON_DERIVEE_NULLE": "Null function derivative in Newton computation",
+    "ERROR_PARAM_NULL": "Parameter value must not be NULL",
+    "ERROR_PARAM_MUST_BE_A_NUMBER": "Please type a numeric value",
     "ERROR_PARAMDEF_CALC_UNDEFINED": "calculability of '%symbol%' parameter is undefined",
     "ERROR_PARAMDEF_VALUE_FIXED": "value of '%symbol%' parameter cannot be changed",
     "ERROR_PARAMDEF_VALUE_INTERVAL": "parameter '%symbol%': value %value% is out of [%minValue%, %maxValue%] interval",
diff --git a/src/locale/messages.fr.json b/src/locale/messages.fr.json
index f673c64ef..e5ad681f1 100644
--- a/src/locale/messages.fr.json
+++ b/src/locale/messages.fr.json
@@ -10,6 +10,8 @@
     "ERROR_INTERVAL_UNDEF": "Interval&nbsp;: valeur 'undefined' incorrecte",
     "ERROR_LANG_UNSUPPORTED": "Internationalisation&nbsp;: locale '%locale%' non prise en charge",
     "ERROR_NEWTON_DERIVEE_NULLE": "Dérivée nulle dans un calcul par la méthode de Newton",
+    "ERROR_PARAM_NULL": "La valeur du paramètre ne peut pas être NULL",
+    "ERROR_PARAM_MUST_BE_A_NUMBER": "Veuillez entrer une valeur numérique",
     "ERROR_PARAMDEF_CALC_UNDEFINED": "La calculabilité du paramètre %symbol% n'est pas définie",
     "ERROR_PARAMDEF_VALUE_FIXED": "La valeur du paramètre %symbol% ne peut pas être changée",
     "ERROR_PARAMDEF_VALUE_INTERVAL": "Paramètre '%symbol%'&nbsp;: la valeur %value% est en dehors de l'intervalle [%minValue%, %maxValue%]",
diff --git a/src/styles.scss b/src/styles.scss
index 8643972d9..b78e18772 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -1,5 +1,5 @@
 /* You can add global styles to this file, and also import other style files */
-@import "~@angular/material/prebuilt-themes/indigo-pink.css";
+// @import "~@angular/material/prebuilt-themes/indigo-pink.css";
 // @import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
 // @import "~@angular/material/prebuilt-themes/pink-bluegrey.css";
 // @import "~@angular/material/prebuilt-themes/purple-green.css";
@@ -23,3 +23,13 @@ mat-dialog-container {
 .eight-em-bottom-padding {
     padding-bottom: 8em;
 }
+
+// bootstrap inspired styles
+
+h1 {
+    margin-top: 0;
+    margin-bottom: .5rem;
+    font-size: 2.5rem;
+    font-weight: 300;
+    line-height: 1.2;
+}
-- 
GitLab