projects/prestations-ng/src/foehn-input/foehn-input.component.ts
OnInit
OnDestroy
AfterViewInit
constructor()
|
autocapitalize | |
Type : string
|
|
The autocapitalize attribute never causes autocapitalization to be enabled for an element with a type attribute whose value is url, email, or password. https://developer.mozilla.org/fr/docs/Web/HTML/Global_attributes/autocapitalize |
autocomplete | |
Type : string
|
|
The attribute must take one of the following values: See https://developer.mozilla.org/fr/docs/Web/HTML/Attributes/autocomplete |
clearButton | |
Type : boolean
|
|
Default value : false
|
|
customErrors | |
Type : literal type
|
|
Default value : {}
|
|
disabled | |
Type : boolean
|
|
excludeFromErrorSummary | |
Type : boolean
|
|
Default value : false
|
|
Exclude the component from the error summary if the component has an error. |
helpModal | |
Type : HelpModal
|
|
helpText | |
Type : string
|
|
hideNotRequiredExtraLabel | |
Type : boolean
|
|
Default value : false
|
|
Hide the 'facultatif' extra label. |
id | |
Type : string
|
|
isErrorInherited | |
Type : boolean
|
|
Default value : false
|
|
isLabelSrOnly | |
Type : boolean
|
|
When TRUE, adds class 'visually-hidden' (only for screen readers) on tag |
label | |
Type : string
|
|
labelStyleModifier | |
Type : string
|
|
max | |
Type : number
|
|
maxlength | |
Type : number
|
|
min | |
Type : number
|
|
model | |
Type : T
|
|
name | |
Type : string
|
|
overrideGesdemMaxlength | |
Type : boolean
|
|
Default value : false
|
|
pattern | |
Type : string
|
|
preventPaste | |
Type : boolean
|
|
Default value : false
|
|
required | |
Type : boolean
|
|
showErrorWhenDisabled | |
Type : boolean
|
|
Default value : false
|
|
When set to TRUE: Shows error even if input is disabled |
standardHelpText | |
Type : string
|
|
updateModelWhenDisabled | |
Type : boolean
|
|
Default value : false
|
|
When set to TRUE: Allows model update to be emitted |
blur | |
Type : EventEmitter
|
|
focus | |
Type : EventEmitter
|
|
modelChange | |
Type : EventEmitter
|
|
Fired when the model changes for any reason |
userInput | |
Type : EventEmitter
|
|
Fired when the user changes a value, after the model is updated. |
attr.id |
Type : string
|
buildChildId | ||||||||
buildChildId(suffix: string)
|
||||||||
Parameters :
Returns :
string
|
buildChildName | ||||||||
buildChildName(suffix: string)
|
||||||||
Parameters :
Returns :
string
|
buildId | ||||||||
buildId(suffix: string)
|
||||||||
Parameters :
Returns :
string
|
displayClearButton |
displayClearButton()
|
Returns :
Observable<boolean>
|
focus |
focus()
|
Returns :
void
|
getAutoComplete |
getAutoComplete()
|
Returns :
string
|
getDescribedBy |
getDescribedBy()
|
Returns :
string
|
Protected getFirstInputComponentEnabled | ||||||
getFirstInputComponentEnabled(subComponents: QueryList<FoehnInputComponent<any>>)
|
||||||
Parameters :
Returns :
FoehnInputComponent<any>
|
getMaxLength |
getMaxLength()
|
Returns :
number
|
getMeAndSubComponents |
getMeAndSubComponents()
|
Returns :
FoehnInputComponent[]
|
getNativeInputList |
getNativeInputList()
|
Returns :
NgModel[]
|
handleChange | ||||||
handleChange(value: T)
|
||||||
Parameters :
Returns :
void
|
hasErrors |
hasErrors()
|
Returns :
boolean
|
hasErrorsToDisplay |
hasErrorsToDisplay()
|
Returns :
boolean
|
hasInheritErrorFromParent |
hasInheritErrorFromParent()
|
Returns :
boolean
|
Protected isEmpty | ||||||
isEmpty(data: any)
|
||||||
Parameters :
Returns :
boolean
|
Protected isEmptyWhenTrimmedIfString | ||||||
isEmptyWhenTrimmedIfString(data: any)
|
||||||
Parameters :
Returns :
boolean
|
isPristine |
isPristine()
|
Returns :
boolean
|
markAsDirty |
markAsDirty()
|
Returns :
void
|
markAsPristine |
markAsPristine()
|
Returns :
void
|
ngAfterViewInit |
ngAfterViewInit()
|
Returns :
void
|
ngOnDestroy |
ngOnDestroy()
|
Returns :
void
|
ngOnInit |
ngOnInit()
|
Returns :
void
|
onBlur | ||||||
onBlur(e: FocusEvent)
|
||||||
Parameters :
Returns :
void
|
onClear |
onClear()
|
Returns :
void
|
onFocus | ||||||
onFocus(e: FocusEvent)
|
||||||
Parameters :
Returns :
void
|
onKeydown | ||||||
onKeydown(key: KeyboardEvent)
|
||||||
Parameters :
Returns :
void
|
onModelChange | ||||||
onModelChange(value: T)
|
||||||
Parameters :
Returns :
void
|
onPaste | ||||||
onPaste(e: ClipboardEvent)
|
||||||
Parameters :
Returns :
void
|
refreshErrors | ||||||
refreshErrors(results: any[])
|
||||||
Parameters :
Returns :
void
|
reset |
reset()
|
Returns :
void
|
Protected triggerUserInput | ||||||
triggerUserInput(value: T)
|
||||||
Parameters :
Returns :
void
|
updateNgModel | ||||||
updateNgModel(value: T)
|
||||||
Parameters :
Returns :
void
|
Private _disabled |
Type : boolean
|
Private _errors |
Type : FormError[]
|
hostId |
Type : string
|
Decorators :
@HostBinding('attr.id')
|
inputElement |
Type : ElementRef
|
Decorators :
@ViewChild('entryComponent')
|
inputModelList |
Type : QueryList<NgModel>
|
Decorators :
@ViewChildren(NgModel)
|
model_ |
Type : T
|
Private registerNgModelService |
Type : RegisterNgModelService
|
Private showClearButton |
Type : Observable<boolean>
|
subComponents |
Type : QueryList<FoehnInputComponent<any>>
|
Decorators :
@ViewChildren(undefined)
|
type |
Type : string
|
Private userInputSubject |
Default value : new Subject<T>()
|
Private userInputSubscription |
Type : Subscription
|
Private validationErrorsSubjectSubscription |
Type : Subscription
|
validationHandlerService |
Type : ValidationHandlerService
|
errors |
geterrors()
|
model | ||||||
getmodel()
|
||||||
setmodel(value: T)
|
||||||
Parameters :
Returns :
void
|
disabled | ||||||
getdisabled()
|
||||||
setdisabled(value: boolean)
|
||||||
Parameters :
Returns :
void
|
import {
AfterViewInit,
Directive,
ElementRef,
EventEmitter,
forwardRef,
HostBinding,
Input,
OnDestroy,
OnInit,
Output,
QueryList,
ViewChild,
ViewChildren
} from '@angular/core';
import { NgModel } from '@angular/forms';
import { combineLatest, Observable, Subject, Subscription } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';
import { RegisterNgModelService } from '../foehn-form/register-ng-model.service';
import { HelpModal } from '../foehn-help-modal/foehn-help-modal.type';
import { FormError } from '../form-error';
import { ObjectHelper } from '../helpers/object.helper';
import { ServiceLocator } from '../service-locator';
// eslint-disable-next-line import/no-cycle
import { ValidationHandlerService } from '../validation/validation-handler.service';
export const GESDEM_MAX_DATA_LENGTH = 10000;
@Directive()
export abstract class FoehnInputComponent<T>
implements OnInit, OnDestroy, AfterViewInit
{
@Input()
id: string;
@Input()
required: boolean;
@Input()
label: string;
/**
* When TRUE, adds class 'visually-hidden' (only for screen readers) on tag <label> or <legend>
* depending on component extending this class
*/
@Input()
isLabelSrOnly: boolean;
@Input()
standardHelpText: string;
@Input()
helpText: string;
@Input()
name: string;
@Input()
pattern: string;
@Input()
labelStyleModifier: string;
@Input()
customErrors: { [key: string]: string } = {};
@Input()
maxlength: number;
@Input()
overrideGesdemMaxlength = false;
@Input()
min: number;
@Input()
max: number;
/**
* The attribute must take one of the following values:
* See https://developer.mozilla.org/fr/docs/Web/HTML/Attributes/autocomplete
*/
@Input()
autocomplete: string;
@Input()
preventPaste = false;
@Input()
clearButton = false;
/**
* Exclude the component from the error summary if the component has an error.
*/
@Input()
excludeFromErrorSummary = false;
/**
* Hide the 'facultatif' extra label.
*/
@Input()
hideNotRequiredExtraLabel = false;
@Input()
isErrorInherited = false;
/**
* When set to TRUE: Shows error even if input is disabled
*/
@Input()
showErrorWhenDisabled = false;
/**
* The autocapitalize attribute never causes autocapitalization to be enabled for
* an <input> element with a type attribute whose value is url, email, or password.
* https://developer.mozilla.org/fr/docs/Web/HTML/Global_attributes/autocapitalize
*/
@Input()
autocapitalize: string;
/**
* When set to TRUE: Allows model update to be emitted
*/
@Input()
updateModelWhenDisabled = false;
@Input()
helpModal: HelpModal;
@ViewChildren(NgModel)
inputModelList: QueryList<NgModel>;
@ViewChild('entryComponent')
inputElement: ElementRef;
@ViewChildren(forwardRef(() => FoehnInputComponent))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subComponents: QueryList<FoehnInputComponent<any>>;
/**
* Fired when the model changes for any reason
*/
@Output()
modelChange = new EventEmitter<T>();
/**
* Fired when the user changes a value, after the model is updated.
*/
@Output()
userInput = new EventEmitter<T>();
// eslint-disable-next-line @angular-eslint/no-output-native,@angular-eslint/no-output-rename
@Output('blur')
blurHandler = new EventEmitter<FocusEvent>();
// eslint-disable-next-line @angular-eslint/no-output-native,@angular-eslint/no-output-rename
@Output('focus')
focusHandler = new EventEmitter<FocusEvent>();
@HostBinding('attr.id')
hostId: string;
type: string;
validationHandlerService: ValidationHandlerService;
model_: T;
private _errors: FormError[];
private validationErrorsSubjectSubscription: Subscription;
private registerNgModelService: RegisterNgModelService;
private showClearButton: Observable<boolean>;
private userInputSubject = new Subject<T>();
private userInputSubscription: Subscription;
private _disabled: boolean;
constructor() {
this.validationHandlerService = ServiceLocator.injector.get(
ValidationHandlerService
);
this.registerNgModelService = ServiceLocator.injector.get(
RegisterNgModelService
);
this.required = false;
this.validationErrorsSubjectSubscription = combineLatest([
this.validationHandlerService.validationErrorsSubject,
this.validationHandlerService.validationDisplaySubject
]).subscribe(values => {
this.refreshErrors(values);
});
}
get errors(): FormError[] {
return this._errors?.sort((a, b) =>
ObjectHelper.stripHtml(a.message).localeCompare(
ObjectHelper.stripHtml(b.message)
)
);
}
get model(): T {
return this.model_;
}
@Input()
set model(value: T) {
this.onModelChange(value);
}
// eslint-disable-next-line @typescript-eslint/member-ordering
get disabled(): boolean {
return this._disabled;
}
@Input()
set disabled(value: boolean) {
this._disabled = value;
}
ngOnInit(): void {
// Ensures that the host has an Id for instrumentalization of the DOM by automated tests.
this.hostId = this.buildId();
this.showClearButton = this.modelChange
.asObservable()
.pipe(map(model => this.clearButton && !!model && !this._disabled));
this.userInputSubscription = this.userInputSubject
.pipe(
// This is used to ensure that we only catch the latest of multiple
// quick updates, and to make sure the userInput is fired when
// the component is done updating.
debounceTime(0)
)
.subscribe(value => {
this.userInput.emit(value);
});
}
ngOnDestroy(): void {
if (this.validationErrorsSubjectSubscription) {
this.validationErrorsSubjectSubscription.unsubscribe();
}
if (this.userInputSubscription) {
this.userInputSubscription.unsubscribe();
}
}
ngAfterViewInit(): void {
this.subComponents.changes.subscribe(() =>
this.registerNgModelService.updateRegisteredNgModels()
);
}
onModelChange(value: T): void {
this.updateNgModel(value);
}
handleChange(value: T): void {
this.triggerUserInput(value);
}
updateNgModel(value: T): void {
this.model_ = value;
if (!this._disabled || this.updateModelWhenDisabled) {
this.modelChange.emit(value);
}
}
hasErrorsToDisplay(): boolean {
return (
this.hasErrors() && (!this._disabled || this.showErrorWhenDisabled)
);
}
hasErrors(): boolean {
return this._errors && this._errors.length > 0 && this.isPristine();
}
hasInheritErrorFromParent(): boolean {
return this.isErrorInherited;
}
isPristine(): boolean {
if (!this.inputModelList && !this.subComponents) {
return true;
}
if (this.inputModelList && !this.subComponents) {
return this.inputModelList.filter(m => !m.pristine).length === 0;
}
if (!this.inputModelList && this.subComponents) {
return this.subComponents.filter(c => !c.isPristine()).length === 0;
}
return (
this.inputModelList.filter(m => !m.pristine).length === 0 &&
this.subComponents.filter(c => !c.isPristine()).length === 0
);
}
markAsDirty(): void {
this.inputModelList.forEach(ngModel => {
ngModel.control.markAsDirty();
});
this.subComponents.forEach(c => {
c.markAsDirty();
});
}
markAsPristine(): void {
this.inputModelList.forEach(ngModel => {
ngModel.control.markAsPristine();
});
this.subComponents.forEach(c => {
c.markAsPristine();
});
}
getNativeInputList(): NgModel[] {
let result: NgModel[] = this.inputModelList.toArray();
this.subComponents.forEach(c => {
result = result.concat(c.getNativeInputList());
});
return result;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getMeAndSubComponents(): FoehnInputComponent<any>[] {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let result: FoehnInputComponent<any>[] = [this];
if (this.subComponents) {
this.subComponents.forEach(c => {
result = result.concat(c.getMeAndSubComponents());
});
}
return result;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
refreshErrors(results: any[]): void {
const errors: FormError[] = results[0];
const shouldDisplayErrors: boolean = results[1];
if (shouldDisplayErrors) {
this._errors = errors.filter(e => e.name === this.name);
}
}
focus(): void {
let elementContainer: HTMLElement;
if (this.inputElement && this.inputElement.nativeElement) {
elementContainer = document.getElementById(
this.buildId('Container')
);
if (elementContainer) {
// Scroll in view should be done on input container
elementContainer.scrollIntoView();
}
// Focus should be on the input itself
this.inputElement.nativeElement.focus();
} else if (
this.subComponents &&
this.getFirstInputComponentEnabled(this.subComponents)
) {
const inputComponent = this.getFirstInputComponentEnabled(
this.subComponents
);
elementContainer = document.getElementById(
inputComponent.buildId('Container')
);
if (elementContainer) {
// Scroll in view should be done on input container
elementContainer.scrollIntoView();
}
// Focus should be on the input itself
inputComponent.inputElement.nativeElement.focus();
} else {
// Case for component who do not have any '#entryComponent' in their html template (i.e. foehn-radio, foehn-checkbox,...)
elementContainer = document.getElementById(
this.buildId('Container')
);
if (elementContainer) {
// Scroll in view should be done on input container
elementContainer.scrollIntoView();
}
const elementNodeListOf = elementContainer.querySelectorAll(
`input[id^="${this.name}"]`
);
if (elementNodeListOf && elementNodeListOf.length > 0) {
const inputElement =
this.findFirstHtmlInputElementEnabled(elementNodeListOf);
if (inputElement) {
// Focus should be on the input itself
inputElement.focus();
}
}
}
}
getMaxLength(): number {
if (!this.maxlength) {
return GESDEM_MAX_DATA_LENGTH;
}
if (
this.maxlength > GESDEM_MAX_DATA_LENGTH &&
!this.overrideGesdemMaxlength
) {
console.log(
`maxLength in ${this.name} cannot be more than ${GESDEM_MAX_DATA_LENGTH} !`
);
return GESDEM_MAX_DATA_LENGTH;
}
return this.maxlength;
}
buildId(suffix: string = ''): string {
// The baseId is either an id given by the user or the value of the name.
// For integration purposes, the container foehn-* must have an Id.
const baseId = this.id || this.name;
if (!baseId) {
return null;
}
return `${baseId}${suffix}`;
}
buildChildId(suffix: string = ''): string {
return this.buildId(`Child${suffix}`);
}
buildChildName(suffix: string = ''): string {
return `${this.name}Child${suffix}`;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onKeydown(key: KeyboardEvent): void {
// Do nothing, overriden by subcomponent
}
reset(): void {
this.updateNgModel(null);
}
onPaste(e: ClipboardEvent): void {
if (this.preventPaste === true) {
e.preventDefault();
}
}
onBlur(e: FocusEvent): void {
this.blurHandler.emit(e);
}
onFocus(e: FocusEvent): void {
this.focusHandler.emit(e);
}
onClear(): void {
this.updateNgModel(null);
this.handleChange(null);
this.focus();
this.markAsDirty();
}
displayClearButton(): Observable<boolean> {
// This has to be an observable to react to the change of model value.
return this.showClearButton;
}
getDescribedBy(): string {
const helpTextId = this.helpText ? `${this.buildChildId()}Help` : null;
const errorId = this.hasErrorsToDisplay()
? `${this.buildId()}ErrorsContainer`
: null;
// aria-describedby can be linked to multiple Ids separated by a comma.
return [helpTextId, errorId].filter(id => !!id).join(',') || null;
}
getAutoComplete(): string {
// retro compatibility (autocomplete used to be a boolean)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (this.autocomplete === false) {
return 'off';
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (this.autocomplete === true) {
return '';
}
return this.autocomplete;
}
protected getFirstInputComponentEnabled(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subComponents: QueryList<FoehnInputComponent<any>>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): FoehnInputComponent<any> {
const foehnInputComponents = subComponents.filter(
item =>
item.inputElement &&
item.inputElement.nativeElement &&
!item._disabled
);
if (!!foehnInputComponents && foehnInputComponents.length) {
return foehnInputComponents[0];
}
return null;
}
protected findFirstHtmlInputElementEnabled(
elementNodeListOf: NodeListOf<Element>
): HTMLInputElement {
for (let i = 0; i < elementNodeListOf.length; i++) {
const htmlInputElement = elementNodeListOf.item(
i
) as HTMLInputElement;
if (!htmlInputElement.disabled) {
return htmlInputElement;
}
}
return null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected isEmpty(data: any): boolean {
return data === null || data === undefined || data === '';
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected isEmptyWhenTrimmedIfString(data: any): boolean {
if (typeof data === 'string') {
return this.isEmpty(!!data ? data.trim() : data);
}
return this.isEmpty(data);
}
protected triggerUserInput(value: T): void {
if (
// If the callback is defined
!!this.userInput &&
// If the value is actually something (T or null)
typeof value !== 'undefined' &&
// If input isn't disabled
!this._disabled
) {
this.userInputSubject.next(value);
}
}
}