import { Input, Output, EventEmitter, ChangeDetectorRef, OnChanges, ViewChild } from "@angular/core"; import { NgModel } from "@angular/forms"; import { BaseComponent } from "../base/base.component"; import { isNumeric, Structure, PabCloisons } from "jalhyd"; import { FormulaireDefinition } from "../../formulaire/definition/form-definition"; import { NgParameter } from "../../formulaire/ngparam"; import { I18nService } from "../../services/internationalisation/internationalisation.service"; /** * classe de gestion générique d'un champ de saisie avec titre, validation et message d'erreur * définitions : * - modèle : entité mémoire gérée, indépendamment de la façon dont elle est affichée. * A noter que si cette entité est une classe, on peut ne présenter à l'interface qu'un membre de cette classe, * cad que get model() et getModelValue() ne renverront pas la même chose. * Par ex : get model()-> instance_de_la_classe_Toto, getModelValue() -> Toto.unMembreNumerique * - valeur gérée : entité elle même si c'est un type simple (number, string, ...) ou une partie d'un classe * - UI : interface utilisateur, présentation de la valeur gérée */ export abstract class GenericInputComponent extends BaseComponent implements OnChanges { /** * entité mémoire gérée */ protected _model: NgParameter | FormulaireDefinition; // CalcName utilise un FormDefinition ici !? /** * flag de désactivation de l'input */ @Input() private _inputDisabled = false; /** * flag d'affichage du message d'erreur */ public showError = true; /** * id de l'input, utilisé notamment pour les tests */ public get inputId() { let id = "error-in-inputId"; if (this._model) { // unique input id based on parameter symbol if (this._model instanceof NgParameter) { const param = this._model as NgParameter; id = param.symbol; // if inside a nested Structure, prefix with Structure position // to disambiguate const nub = param.paramDefinition.parentNub; if (nub && (nub instanceof Structure || nub instanceof PabCloisons)) { id = nub.findPositionInParent() + "_" + id; } } } return id; } /** * chaîne affichée dans l'input quand aucune valeur n'est saisie */ @Input() public title: string; /** * événement signalant un changement : valeur du modèle, validité, ... */ @Output() protected change = new EventEmitter<any>(); /** * événement signalant un appui sur TAB ou SHIFT+TAB */ @Output() protected tabPressed = new EventEmitter<any>(); /** * valeur saisie. * Cette variable n'est modifiée que lorsqu'on affecte le modèle ou que l'utilisateur fait une saisie */ private _uiValue: string; /** * flag de validité de la saisie dans l'UI * par ex : est ce bien une valeur numérique ? n'y a-t-il que des minuscules ? etc... */ private _isValidUI = false; /** * flag de validité de la valeur du modèle * par ex : la valeur saisie fait elle bien partie d'un domaine de définition donné ? date inférieure à une limite ? etc... */ private _isValidModel = false; /** * message d'erreur UI */ private _errorMessageUI: string; /** * message d'erreur modèle */ private _errorMessageModel: string; @ViewChild("inputControl") inputField: NgModel; constructor(private cdRef: ChangeDetectorRef, protected intlService: I18nService) { super(); } public get isDisabled(): boolean { return this._inputDisabled; } /** * événement de changement de la validité de la saisie */ private emitValidChanged() { this.change.emit({ "action": "valid", "value": this.isValid }); } /** * détection des changements dans l'UI par le ChangeDetector du framework */ protected detectChanges() { if (this.cdRef) { // if (!this.cdRef['destroyed']) // pour éviter l'erreur "Attempt to use a destroyed view: detectChanges" // this.cdRef.detectChanges(); this.cdRef.markForCheck(); } } /** * calcul de la validité globale du composant (UI+modèle) */ public get isValid() { return this._isValidUI && this._isValidModel; } private setUIValid(b: boolean) { const old = this.isValid; this._isValidUI = b; if (this.isValid !== old) { this.emitValidChanged(); } } protected validateUI() { const { isValid, message } = this.validateUIValue(this._uiValue); this._errorMessageUI = message; this.detectChanges(); this.setUIValid(isValid); return isValid; } private setModelValid(b: boolean) { const old = this.isValid; this._isValidModel = b; if (this.isValid !== old) { this.emitValidChanged(); } // répercussion des erreurs sur le Form angular, pour faire apparaître/disparaître les mat-error if (b) { this.inputField.control.setErrors(null); } else { this.inputField.control.setErrors({ "incorrect": true }); } } private validateModel() { const { isValid, message } = this.validateModelValue(this.getModelValue()); this._errorMessageModel = message; this.detectChanges(); this.setModelValid(isValid); } public validate() { this.validateUI(); this.validateModel(); } /** * getter du message d'erreur affiché. * L'erreur de forme (UI) est prioritaire */ public get errorMessage() { if (this._errorMessageUI !== undefined) { return this._errorMessageUI; } return this._errorMessageModel; } public get model(): any { return this._model; } /** * événement de changement de la valeur du modèle */ private emitModelChanged() { this.change.emit({ "action": "model", "value": this.getModelValue() }); } protected setAndValidateModel(sender: any, v: any) { this.setModelValue(sender, v); this.emitModelChanged(); this.validateModel(); } public set model(v: any) { this.beforeSetModel(); this._model = v; this.afterSetModel(); this.updateAll(); this.detectChanges(); } /** * MAJ et validation de l'UI */ protected updateAndValidateUI() { this._uiValue = String(this.getModelValue()); this.validateUI(); } public get uiValue() { return this._uiValue; } /* * fonction appelée lorsque l'utilisateur fait une saisie * @param ui valeur dans le contrôle */ public set uiValue(ui: any) { this._uiValue = ui; this.updateModelFromUI(); } /** * met à jour le modèle d'après la saisie */ public updateModelFromUI() { if (this.validateUI()) { this.setAndValidateModel(this, +this._uiValue); // cast UI value to Number } } /** * Renvoie l'événement au composant du dessus */ public onTabPressed(event, shift: boolean) { this.tabPressed.emit({ originalEvent: event, shift: shift }); return false; // stops event propagation } private updateAll() { this.updateAndValidateUI(); this.validateModel(); } /** * appelé quand les @Input changent */ public ngOnChanges() { this.updateAll(); } /** * appelé avant le changement de modèle */ protected beforeSetModel() { } /** * appelé après le changement de modèle */ protected afterSetModel() { } /** * retourne la valeur du modèle */ protected abstract getModelValue(): any; /** * affecte la valeur du modèle */ protected abstract setModelValue(sender: any, v: any); /** * valide une valeur de modèle : est ce une valeur acceptable ? (par ex, nombre dans un intervalle, valeur dans une liste, ...) * @param v valeur à valider * @returns isValid : true si la valeur est valide, false sinon * @returns message : message d'erreur */ protected abstract validateModelValue(v: any): { isValid: boolean, message: string }; /** * valide une valeur saisie dans l'UI (forme de la saisie : est ce bien une date, un nombre, ...) * @param ui saisie à valider * @returns isValid : true si la valeur est valide, false sinon * @returns message : message d'erreur */ protected validateUIValue(ui: string): { isValid: boolean, message: string } { let valid = false; let msg: string; if (! isNumeric(ui)) { msg = this.intlService.localizeText("ERROR_PARAM_MUST_BE_A_NUMBER"); } else { valid = true; } return { isValid: valid, message: msg }; } }