File

projects/prestations-ng/src/foehn-input/foehn-input.component.ts

Implements

OnInit OnDestroy AfterViewInit

Index

Properties
Methods
Inputs
Outputs
HostBindings
Accessors

Constructor

constructor()

Inputs

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

Outputs

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.

HostBindings

attr.id
Type : string

Methods

buildChildId
buildChildId(suffix: string)
Parameters :
Name Type Optional Default value
suffix string No ''
Returns : string
buildChildName
buildChildName(suffix: string)
Parameters :
Name Type Optional Default value
suffix string No ''
Returns : string
buildId
buildId(suffix: string)
Parameters :
Name Type Optional Default value
suffix string No ''
Returns : string
displayClearButton
displayClearButton()
Returns : Observable<boolean>
Protected findFirstHtmlInputElementEnabled
findFirstHtmlInputElementEnabled(elementNodeListOf: NodeListOf)
Parameters :
Name Type Optional
elementNodeListOf NodeListOf<Element> No
Returns : HTMLInputElement
focus
focus()
Returns : void
getAutoComplete
getAutoComplete()
Returns : string
getDescribedBy
getDescribedBy()
Returns : string
Protected getFirstInputComponentEnabled
getFirstInputComponentEnabled(subComponents: QueryList<FoehnInputComponent<any>>)
Parameters :
Name Type Optional
subComponents QueryList<FoehnInputComponent<any>> No
getMaxLength
getMaxLength()
Returns : number
getMeAndSubComponents
getMeAndSubComponents()
getNativeInputList
getNativeInputList()
Returns : NgModel[]
handleChange
handleChange(value: T)
Parameters :
Name Type Optional
value T No
Returns : void
hasErrors
hasErrors()
Returns : boolean
hasErrorsToDisplay
hasErrorsToDisplay()
Returns : boolean
hasInheritErrorFromParent
hasInheritErrorFromParent()
Returns : boolean
Protected isEmpty
isEmpty(data: any)
Parameters :
Name Type Optional
data any No
Returns : boolean
Protected isEmptyWhenTrimmedIfString
isEmptyWhenTrimmedIfString(data: any)
Parameters :
Name Type Optional
data any No
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 :
Name Type Optional
e FocusEvent No
Returns : void
onClear
onClear()
Returns : void
onFocus
onFocus(e: FocusEvent)
Parameters :
Name Type Optional
e FocusEvent No
Returns : void
onKeydown
onKeydown(key: KeyboardEvent)
Parameters :
Name Type Optional
key KeyboardEvent No
Returns : void
onModelChange
onModelChange(value: T)
Parameters :
Name Type Optional
value T No
Returns : void
onPaste
onPaste(e: ClipboardEvent)
Parameters :
Name Type Optional
e ClipboardEvent No
Returns : void
refreshErrors
refreshErrors(results: any[])
Parameters :
Name Type Optional
results any[] No
Returns : void
reset
reset()
Returns : void
Protected triggerUserInput
triggerUserInput(value: T)
Parameters :
Name Type Optional
value T No
Returns : void
updateNgModel
updateNgModel(value: T)
Parameters :
Name Type Optional
value T No
Returns : void

Properties

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

Accessors

errors
geterrors()
model
getmodel()
setmodel(value: T)
Parameters :
Name Type Optional
value T No
Returns : void
disabled
getdisabled()
setdisabled(value: boolean)
Parameters :
Name Type Optional
value boolean No
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);
        }
    }
}

results matching ""

    No results matching ""