/* * © 2017 Stratio Big Data Inc., Sucursal en España. * * This software is licensed under the Apache License, Version 2.0. * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the terms of the License for more details. * * SPDX-License-Identifier: Apache-2.0. */ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, forwardRef, HostBinding, Input, OnChanges, OnDestroy, OnInit, Output, ViewChildren } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subscription } from 'rxjs'; import { StInputError } from './st-input.error.model'; import { StDropDownMenuGroup, StDropDownMenuItem } from '..'; /** * @description {Component} Input * * This component specifies an input field where the user can enter data. * * @model * * [Error messages] {./st-input.error.model.ts#StInputError} * * @example * * {html} * * ``` * * * ``` */ @Component({ selector: 'st-input', templateUrl: './st-input.component.html', styleUrls: ['./st-input.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StInputComponent), multi: true }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => StInputComponent), multi: true } ], changeDetection: ChangeDetectionStrategy.OnPush }) export class StInputComponent implements ControlValueAccessor, OnChanges, OnInit, OnDestroy { /** @input {string} [placeholder=null] The text that appears as placeholder of the input. It is empty by default */ @Input() placeholder: string = ''; /** @input {string} [name=''] Input name */ @Input() name: string = ''; /** @input {string} [label=''] Label to show over the input. It is empty by default */ @Input() label: string = ''; /** @input {'text' | 'number' | 'password'} [fieldType='text'] Input type */ @Input() fieldType: 'text' | 'number' | 'password' = 'text'; /** @input {StInputError} [errors=] Customized error messages */ @Input() errors: StInputError; /** @Input {string} [qaTag=''] Id value for qa test */ @Input() qaTag: string; /** @Input {boolean} [forceValidations=false] If you specify it to 'true', the input checks the errors before being modified by user */ @Input() forceValidations: boolean = false; /** @Input {string} [contextualHelp=''] It will be displayed when user clicks on the info button */ @Input() contextualHelp: string; /** @Input {string} [maxLength=''] Define a max-length for input field */ @Input() maxLength: number; /** @Input {string} [min=''] Define a minimum number for number inputs */ @Input() min: number; /** @Input {string} [max=''] Define a maximum number for number inputs */ @Input() max: number; /** @Input {boolean} [isFocused=false] If true, the input will be focused on view init. */ @Input() isFocused: boolean = false; /** @Input {boolean} [readonly=''] This parameter disables the input and it can not be modified by the user */ @Input() readonly: boolean = false; /** @Input {string} [step=''] It specifies the interval between legal numbers in the input field */ @Input() step: string; /** @Input {string} [default=] Default value of input */ @Input() default: any; /** @input {(StDropDownMenuItem | StDropDownMenuGroup)[]} [autocompleteList=Array()] List to be used for autocomplete feature. It is empty by default */ @Input() autocompleteList: (StDropDownMenuItem | StDropDownMenuGroup)[] = []; /** @input {number} [charsToShowAutocompleteList=1] Number of characters before displaying autocomplete list. By default is 1 */ @Input() charsToShowAutocompleteList: number = 1; /** @Input {any} [value=''] Value of the input */ @Input() get value(): any { return this._value; } set value(value: any) { this._value = value; } /** @Output {} [blur] Notify when user leaves a field */ @Output() blur: EventEmitter = new EventEmitter(); @ViewChildren('input') vc: any; public disabled: boolean = false; // To check disable public displayResetButtonValue: boolean = false; public focus: boolean = false; public internalControl: FormControl; public errorMessage: string = undefined; public showErrorValue: boolean = false; public expandedMenu: boolean = false; private sub: Subscription; private _value: any; private valueChangeSub: Subscription; private internalInputModel: any; constructor(private _cd: ChangeDetectorRef) { } onChange = (_: any) => { } onTouched = () => { } @HostBinding('class.st-input--autocomplete') get showAutocompleteList(): boolean { return this.expandedMenu && this.autocompleteList && this.autocompleteList.length > 0; } validate(control: FormControl): any { if (this.sub) { this.sub.unsubscribe(); } this.sub = control.statusChanges.subscribe(() => this.checkErrors(control)); } ngOnChanges(change: any): void { if (this.forceValidations && this.internalControl) { this.writeValue(this.internalControl.value); } } ngOnInit(): void { this.internalControl = new FormControl(this.internalInputModel); this.valueChangeSub = this.internalControl.valueChanges.subscribe((value) => { this.onChange(this.getTypedValue(value)); this.showAutocompleteMenu(); this.showErrorValue = this.showError(); this.displayResetButtonValue = this.displayResetButton(); }); } ngAfterViewInit(): void { if (this.isFocused) { this.focus = true; this.vc.first.nativeElement.focus(); } } ngOnDestroy(): void { if (this.valueChangeSub) { this.valueChangeSub.unsubscribe(); } if (this.sub) { this.sub.unsubscribe(); } } // When value is received from outside writeValue(value: any): void { this.internalInputModel = value; this._value = value; this.internalControl.setValue(this.getTypedValue(value)); } // Registry the change function to propagate internal model changes registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } // Registry the touch function to propagate internal touch events TODO: make this function. registerOnTouched(fn: () => void): void { this.onTouched = fn; } setDisabledState(disable: boolean): void { this.disabled = disable; if (this.disabled && this.internalControl && this.internalControl.enabled) { this.internalControl.disable(); } else if (!this.disabled && this.internalControl && this.internalControl.disabled) { this.internalControl.enable(); } this._cd.markForCheck(); } showError(): boolean { return this.errorMessage !== undefined && (!this.internalControl.pristine || this.forceValidations) && !this.focus && !this.disabled; } get labelQaTag(): string { return (this.qaTag || this.name) + '-label'; } displayResetButton(): boolean { return this.default !== undefined && this.internalControl.dirty && this.internalControl.value !== this.default; } resetToDefault(): void { this.writeValue(this.default); this._cd.markForCheck(); } /** Autocomplete list actions */ onListSelect(data: StDropDownMenuItem): void { if (data && data.value && data.value.length) { this.writeValue(data.value); } else { this.writeValue(''); } this.expandedMenu = false; this._cd.markForCheck(); } onClickOutside(event: Event): void { if (this.expandedMenu) { this.expandedMenu = false; } } /** Style functions */ onFocus(event: Event): void { this.focus = true; this.showErrorValue = this.showError(); this.showAutocompleteMenu(); } onFocusOut(event: Event, emitEvent: boolean): void { this.focus = false; if (emitEvent) { this.blur.emit(); } this.showErrorValue = this.showError(); } // When status change call this function to check if have errors private checkErrors(control: FormControl): void { let errors: { [key: string]: any } = control.errors; this.errorMessage = this.getErrorMessage(errors); this._cd.markForCheck(); } // Get error message in function of error list. private getErrorMessage(errors: { [key: string]: any }): string { if (!errors) { return undefined; } if (!this.errors) { return ''; } if (errors.hasOwnProperty('required')) { return this.errors.required || this.errors.generic || ''; } if (errors.hasOwnProperty('fieldType')) { return this.errors.type || this.errors.generic || ''; } if (errors.hasOwnProperty('minlength')) { return this.errors.minLength || this.errors.generic || ''; } if (errors.hasOwnProperty('maxlength')) { return this.errors.maxLength || this.errors.generic || ''; } if (errors.hasOwnProperty('pattern')) { return this.errors.pattern || this.errors.generic || ''; } if (errors.hasOwnProperty('min')) { return this.errors.min || this.errors.generic || ''; } if (errors.hasOwnProperty('max')) { return this.errors.max || this.errors.generic || ''; } return ''; } private getTypedValue(value: string): any { switch (this.fieldType) { case 'number': if (!value || isNaN(Number(value))) { return value; } else { return Number(value); } default: return value; } } private showAutocompleteMenu(): void { this.expandedMenu = this.focus && this.internalControl && this.charsToShowAutocompleteList <= (this.internalControl.value || '').length; this._cd.markForCheck(); } }