Skip to content
Snippets Groups Projects
Commit e90f1680 authored by mathias.chouet's avatar mathias.chouet
Browse files

Premier formulaire Angular Material : app-setup

formulaire "template-driven"
validation par le modèle JaLHyd (synchrone ou asynchrone)
2-way binding sur NgParam directement plutôt que BaseParamInputComponent
parent 7e476252
No related branches found
No related tags found
1 merge request!29Resolve "Remplacer mdbootstrap par angular-material"
Showing
with 320 additions and 104 deletions
......@@ -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>
......
......@@ -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
}
......@@ -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 ]
......
<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>
#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;
}
}
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() {
......
......@@ -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 {
......
......@@ -12,7 +12,7 @@ mat-card.compute-nodes-theme {
}
mat-card-header {
min-height: 85px;
min-height: 80px;
}
.mat-card-image-overlay-container {
......
......@@ -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
......
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;
}
}
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;
};
}
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));
}
}
......@@ -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",
......
......@@ -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%]",
......
/* 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;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment