我有一个自定义 Angular 组件(Angular 12+),我对其进行了修改以使用表单重置方法。以前,组件在运行重置时仍然显示错误,但我已修复此问题,以便在重置时清除错误。但是,自定义组件上的现有测试在错误消息周围失败,我无法对测试进行正面或反面来理解原因。任何帮助深表感谢。
失败的测试 #1:“当用户输入无效输入时,AtlasInput Native 应更新无效输入失败的错误消息”错误:预期 0 大于 0。
失败的测试#2:“当用户输入无效输入时,AtlasInput Native 应在无效输入上设置错误失败”错误:预期“有效”与“无效”匹配。
input.component.spec.ts:
import { async, ComponentFixture, TestBed, fakeAsync, tick, discardPeriodicTasks, inject } from '@angular/core/testing';
import { FocusMonitor, FocusOrigin, A11yModule } from '@angular/cdk/a11y';
import { ReactiveFormsModule, FormsModule, FormControl } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DebugElement } from '@angular/core';
import { IMaskModule } from 'angular-imask';
import { AtlasFormFieldErrors } from '@wellsky/atlas-ui/core';
import { AtlasInputTestComponent } from './testing/atlas-input-test.component';
import { AtlasInputModule } from './input.module';
import { AtlasInput } from './input.component';
import { ObserversModule } from '@angular/cdk/observers';
import { UserValidators } from './testing/user-validator.service';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
/**
* Utility to dispatch any event on a Node.
* @docs-private
*/
function dispatchEvent<T extends Event>(node: Node | Window, event: T): T {
node.dispatchEvent(event);
return event;
}
/**
* Shorthand to dispatch a fake event on a specified node.
* @docs-private
*/
function dispatchFakeEvent(node: Node | Window, type: string, canBubble?: boolean): Event {
return dispatchEvent(node, createFakeEvent(type, canBubble));
}
/**
* Creates a fake event object with any desired event type.
* @docs-private
*/
function createFakeEvent(type: string, canBubble = false, cancelable = true) {
const event = document.createEvent('Event');
event.initEvent(type, canBubble, cancelable);
return event;
}
/**
* Patches an elements focus and blur methods to emit events consistently and predictably.
* This is necessary, because some browsers, like IE11, will call the focus handlers asynchronously,
* while others won't fire them at all if the browser window is not focused.
* @docs-private
*/
function patchElementFocus(element: HTMLElement) {
element.focus = () => dispatchFakeEvent(element, 'focus');
element.blur = () => dispatchFakeEvent(element, 'blur');
}
describe('AtlasInput', () => {
let testComponent: AtlasInputTestComponent;
let fixture: ComponentFixture<AtlasInputTestComponent>;
let focusMonitor: FocusMonitor;
let debugElementTest: DebugElement;
let inputElementOne: HTMLElement;
let changeHandler: (origin: FocusOrigin) => void;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AtlasInputTestComponent
],
imports: [
ReactiveFormsModule,
FormsModule,
BrowserAnimationsModule,
IMaskModule,
AtlasInputModule,
A11yModule,
ObserversModule
]
})
.compileComponents();
}));
beforeEach(inject([FocusMonitor], (fm: FocusMonitor) => {
fixture = TestBed.createComponent(AtlasInputTestComponent);
debugElementTest = fixture.debugElement;
testComponent = fixture.componentInstance;
fixture.detectChanges();
inputElementOne = debugElementTest.query(By.css('input')).nativeElement;
focusMonitor = fm;
changeHandler = jasmine.createSpy('focus origin change handler');
focusMonitor.monitor(inputElementOne).subscribe(changeHandler);
patchElementFocus(inputElementOne);
}));
it('should create component', () => {
expect(testComponent).toBeTruthy();
});
it('should set the id for input', () => {
const inputElements = debugElementTest.query(By.css('input'));
expect(inputElements.nativeElement.attributes.getNamedItem('id').value).toContain('atlas-input-');
// To set the id
const id = 'input-id';
testComponent.id = id;
fixture.detectChanges();
expect(inputElements.nativeElement.attributes.getNamedItem('id').value).toEqual(id);
});
it('should set the tabIndex for input', () => {
const inputElements = debugElementTest.query(By.css('input'));
expect(inputElements.nativeElement.getAttribute('tabIndex')).toBe('0');
const tabIndex = 1;
testComponent.tabIndex = tabIndex;
fixture.detectChanges();
expect(inputElements.nativeElement.getAttribute('tabIndex')).toBe('1');
// To set the disabled true
testComponent.disabled = true;
fixture.detectChanges();
expect(inputElements.nativeElement.getAttribute('tabIndex')).toBe('-1');
});
it('should disable the input (reactive and template driven)', () => {
// Assert
const inputElements = debugElementTest.queryAll(By.css('input'));
// Act (disabling both inputs)
testComponent.disabled = true;
fixture.detectChanges();
// Expect
expect(inputElements[0].nativeElement.attributes['disabled']).toBeDefined();
expect(inputElements[1].nativeElement.attributes['disabled']).toBeDefined();
});
it('should make the inputs (reactive and template driven) required', () => {
// Assert
const inputElements = debugElementTest.queryAll(By.css('input'));
// Act (make required) both inputs)
testComponent.required = true;
fixture.detectChanges();
// Expect
expect(inputElements[0].nativeElement.attributes['required']).toBeDefined();
expect(inputElements[1].nativeElement.attributes['required']).toBeDefined();
});
//Angular Material 12 Always promotes label to a placeholder when label is present
//and keeps placeholder value in the data-placeholder attribute on the control
//https://material.angular.io/components/form-field/overview#floating-label
it('should set the placeholder for inputs (reactive and template driven)', () => {
// Assert
const inputElements = debugElementTest.queryAll(By.css('input'));
const expectedPlaceholder = `I'm the new placeholder`;
// Act (make required) both inputs)
testComponent.placeholder = expectedPlaceholder;
fixture.detectChanges();
// Expect
expect(inputElements[0].nativeElement.attributes['data-placeholder']).toBeDefined();
expect(inputElements[0].nativeElement.attributes['data-placeholder'].value).toEqual(expectedPlaceholder);
expect(inputElements[1].nativeElement.attributes['data-placeholder']).toBeDefined();
expect(inputElements[1].nativeElement.attributes['data-placeholder'].value).toEqual(expectedPlaceholder);
});
it('should set the label for inputs (reactive and template driven)', () => {
// Assert
const inputElements = debugElementTest.queryAll(By.directive(AtlasInput));
const expectedLabel = `I'm the new label`;
// Act (make required) both inputs)
testComponent.label = expectedLabel;
fixture.detectChanges();
// Expect
expect(inputElements[0].nativeElement.attributes['ng-reflect-label']).toBeDefined();
expect(inputElements[0].nativeElement.attributes['ng-reflect-label'].value).toEqual(expectedLabel);
expect(inputElements[1].nativeElement.attributes['ng-reflect-label']).toBeDefined();
expect(inputElements[1].nativeElement.attributes['ng-reflect-label'].value).toEqual(expectedLabel);
});
it('should change a label in container', async(() => {
// Assert
const matLabelElement = debugElementTest.queryAll(By.css('mat-label'));
fixture.detectChanges();
// Expect
expect(matLabelElement[2].nativeElement.innerHTML).toEqual(testComponent.labelValue);
// Act - changes the label value
testComponent.changeLabel();
fixture.detectChanges();
// Expect
fixture.whenStable().then(() => {
expect(matLabelElement[2].nativeElement.innerHTML).toEqual(testComponent.labelValue);
});
}));
it('should set the maxLength', () => {
// Assert
const inputElements = debugElementTest.queryAll(By.css('input'));
const expectedMaxLength = 20;
// Act
testComponent.maxLength = expectedMaxLength;
fixture.detectChanges();
// Expect
expect(inputElements[0].nativeElement.getAttribute('maxlength')).toBe(expectedMaxLength.toString());
expect(inputElements[1].nativeElement.getAttribute('maxlength')).toBe(expectedMaxLength.toString());
});
it('should add clear icon on value update', fakeAsync(() => {
// Assert
const expectedOptions = {
clearButton: true,
charCounter: true
};
inputElementOne.focus();
fixture.detectChanges();
tick();
// Act
testComponent.options = expectedOptions;
testComponent.amountFormControl.setValue('123456');
fixture.detectChanges();
discardPeriodicTasks();
// Expect
expect(inputElementOne.classList.contains('cdk-focused'))
.toBe(true, 'input should have cdk-focused class');
expect(inputElementOne.classList.contains('cdk-program-focused'))
.toBe(true, 'input should have cdk-program-focused class');
// Act
focusMonitor.stopMonitoring(inputElementOne);
fixture.detectChanges();
}));
it('should set an error', () => {
// Assert
const inputValue = '1 2 3 4 5 6 7 8 9 10 11 12 13';
// Act
testComponent.customErrors = new AtlasFormFieldErrors();
testComponent.customErrors.setError('maxlength', 'Custom error message for maxLength');
fixture.detectChanges();
testComponent.amountFormControl.setValue(inputValue);
fixture.detectChanges();
// Expect
expect(testComponent.amountFormControl.value).toEqual(inputValue);
expect(testComponent.amountFormControl.getError('maxlength').actualLength).toBeGreaterThan(testComponent.amountFormControl.getError('maxlength').requiredLength);
expect(testComponent.amountFormControl.status).toBe('INVALID');
});
it('should able to clear the entered text', fakeAsync(() => {
// Assert
const inputValue = '1 2 3 4 5';
const expectedOptions = {
clearButton: true,
charCounter: true
};
inputElementOne.focus();
fixture.detectChanges();
tick();
// Act
testComponent.amountFormControl.setValue(inputValue);
fixture.detectChanges();
testComponent.options = expectedOptions;
fixture.detectChanges();
discardPeriodicTasks();
// Expect
expect(inputElementOne.classList.contains('cdk-focused'))
.toBe(true, 'input should have cdk-focused class');
expect(inputElementOne.classList.contains('cdk-program-focused'))
.toBe(true, 'input should have cdk-program-focused class');
expect(testComponent.atlasInput.showClearButton()).toBeTrue();
expect(testComponent.amountFormControl.value).toBeDefined();
// Assert to clear the text
const buttonDebugElement = debugElementTest.query(By.css('button'));
buttonDebugElement.nativeElement.click();
fixture.detectChanges();
tick(300);
// Expect
expect(testComponent.amountFormControl.value).toBeNull();
// To remove the focus
// Act
focusMonitor.stopMonitoring(inputElementOne);
fixture.detectChanges();
// Expect
expect(inputElementOne.classList.contains('cdk-focused'))
.toBe(false, 'input should not have cdk-focused class');
expect(inputElementOne.classList.contains('cdk-program-focused'))
.toBe(false, 'input should not have cdk-program-focused class');
}));
});
describe('AtlasInput Native', () => {
let inputComponent: AtlasInput;
let inputFixture: ComponentFixture<AtlasInput>;
let ngControlForm: FormControl;
let inputControlForm: FormControl;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AtlasInput
],
imports: [
ReactiveFormsModule,
FormsModule,
BrowserAnimationsModule,
MatIconModule,
MatFormFieldModule,
MatInputModule,
],
})
.compileComponents();
}));
beforeEach(() => {
// Native input
ngControlForm = new FormControl();
inputControlForm = new FormControl();
inputFixture = TestBed.createComponent(AtlasInput);
(inputFixture.componentInstance as any).ngControl = ngControlForm;
(inputFixture.componentInstance as any).inputControl = inputControlForm;
inputComponent = inputFixture.componentInstance;
inputFixture.detectChanges();
});
describe('when a user enters an invalid input', () => {
beforeEach(() => {
// induce status change
ngControlForm.markAsTouched();
ngControlForm.setErrors({});
inputFixture.detectChanges();
});
it('should update error message on invalid input', () => {
expect(inputComponent.errorMessage.length).toBeGreaterThan(0)
});
it('should set errors on invalid input', () => {
expect(inputComponent.inputControl.status).toMatch('INVALID');
});
});
describe('when a user enters a valid input', () => {
beforeEach(() => {
// induce status change
ngControlForm.setErrors(null);
inputFixture.detectChanges();
});
it('should remove any error messages', () => {
expect(inputComponent.errorMessage.length).toEqual(0);
});
it('should clear errors on valid input', () => {
expect(inputComponent.inputControl.status).toMatch('VALID');
});
});
describe('when a user enters input w/ character count', () => {
it('should update count with a non zero quantity', () => {
const inputValue = 'cats';
ngControlForm.setValue(inputValue);
expect(inputComponent.currentInputLength).toEqual(inputValue.length);
});
it('should update count with a zero quantity when empty', () => {
const inputValue = undefined;
ngControlForm.setValue(inputValue);
expect(inputComponent.currentInputLength).toEqual(0);
});
});
});
input.component.ts:
import {
Component,
OnInit,
Input,
Optional,
Self,
ChangeDetectorRef,
NgZone,
AfterViewInit,
OnDestroy,
ViewChild,
OnChanges,
SimpleChanges,
EventEmitter,
ElementRef,
Output,
} from '@angular/core';
import { FormControl, Validators, ControlValueAccessor, ValidationErrors, NgControl } from '@angular/forms';
import { debounceTime } from 'rxjs/operators';
import { FocusMonitor, FocusOrigin } from '@angular/cdk/a11y';
// Models
import { FloatLabelType, MatFormFieldAppearance } from '@angular/material/form-field';
import { ThemePalette } from '@angular/material/core';
import { AtlasFormFieldExtras, AtlasFormFieldErrors } from '@wellsky/atlas-ui/core';
import IMask from 'imask';
let nextUniqueId = 0;
@Component({
selector: 'atlas-input',
templateUrl: './input.component.html',
exportAs: 'atlasInput'
})
export class AtlasInput implements OnInit, ControlValueAccessor, AfterViewInit, OnChanges, OnDestroy {
private _errors: AtlasFormFieldErrors;
private _maxLength: number;
private _showClearButton: boolean;
private _maskedInput: any;
private _options: AtlasFormFieldExtras;
private _uid = `atlas-input-${nextUniqueId++}`;
private _id: string;
currentInputLength: number;
errorMessage: string = '';
inputControl: FormControl;
value: any;
maskedValue: any;
@ViewChild('matInput') matInput: ElementRef<HTMLElement>;
@ViewChild('contentLabel') contentLabel: ElementRef<HTMLElement>;
@ViewChild('labelContentData') labelContentData: ElementRef<HTMLElement>;
@Input()
debounceTime: number;
@Input()
get disabled(): boolean {
return this.inputControl.disabled;
}
set disabled(disabled: boolean) {
if (this.inputControl) {
if (disabled) {
this.inputControl.disable();
} else {
this.inputControl.enable();
}
}
}
@Input()
get errors(): AtlasFormFieldErrors {
return this._errors;
}
set errors(errors: AtlasFormFieldErrors) {
if (errors) {
this._errors = errors;
}
}
/**
* Input of checkbox component: id
* Gives ID to the <atlas-input> component
*/
@Input()
set id(value: string) {
// If id is passed from external component then assign or else assign id from within the component
this._id = value || this._uid;
}
get id(): string { return this._id; }
/**
* Set Tab index for Input
*/
@Input()
tabIndex: number;
@Input()
fieldAppearance: MatFormFieldAppearance;
@Input()
floatLabel: FloatLabelType;
@Input()
hideRequiredMarker: boolean;
@Input()
hintEnd: string;
@Input()
hintStart: string;
@Input()
label: string;
@Input()
readonly: boolean;
@Input()
get maxLength(): number {
return this._maxLength;
}
set maxLength(maxLength: number) {
this._maxLength = maxLength;
}
@Input()
placeholder: string;
@Input()
required: boolean;
@Input()
type: string;
autofocus: boolean;
@Input('autofocus') set setAutofocus(v: boolean | '') {
this.autofocus = v === '' || v;
}
@Input()
get options(): AtlasFormFieldExtras {
return this._options;
}
set options(options: AtlasFormFieldExtras) {
if (options) {
this._options = options;
}
}
/**
* @deprecated: This property is getting deprecated instead use options
*/
@Input()
get enable(): AtlasFormFieldExtras {
return this._options;
}
set enable(options: AtlasFormFieldExtras) {
console.warn(`Atlas.UI: 'enable' property is getting deprecated, please use 'options' instead.`);
if (options) {
this._options = options;
}
}
/**
* Input of input component: color
* Supports three values: 'primary' | 'accent' | 'warn' | undefined
*/
@Input()
color: ThemePalette;
@Output() blur = new EventEmitter<object>();
/**
* mask to be supplied from the user
*/
@Input()
maskConfig: any;
constructor(
@Optional() @Self() public ngControl: NgControl,
public elementRef: ElementRef,
private _focusMonitor: FocusMonitor,
private _cdr: ChangeDetectorRef,
private _ngZone: NgZone
) {
// Values: 'legacy' | 'standard' | 'fill' | 'outline'
this.fieldAppearance = 'fill';
// Values: 'auto' | 'always' | 'never'
this.floatLabel = 'auto';
// false: for hiding the required marker when required is enabled
this.hideRequiredMarker = false;
// Hint Start
this.hintStart = '';
// Hint End
this.hintEnd = '';
// Label
this.label = '';
// Values: color | date | datetime-local |email | month | number | password | search | tel | text | time | url | week
this.type = 'text';
// Extras
this.options = {
clearButton: false,
charCounter: false
};
// Max-length
this.maxLength = 256;
// Form Control
this.inputControl = new FormControl(
'',
[
Validators.maxLength(this.maxLength),
],
);
// Required
this.required = false;
// Debounce time in ms
this.debounceTime = 0;
// Errors
this.errors = new AtlasFormFieldErrors();
// Default char count
this.currentInputLength = 0;
// Replace the provider from above with this.
if (this.ngControl != null) {
// Setting the value accessor directly (instead of using
// the providers) to avoid running into a circular import.
this.ngControl.valueAccessor = this;
}
// Set default color to 'accent'
this.color = 'accent';
// Set value to ID
this.id = this.id;
// Set default value to tabIndex
this.tabIndex = 0;
// Set the mask as null as default
// Takes config from the imask library maskOptions
this.maskConfig = null;
// Set the value (eg: 0000000000) for the inputControl
this.value = null;
// Sets the masked value (eg: 000-000-0000) for the inputControl
this.maskedValue = null;
// Handles user input and emit value to CVA
this.inputControl.valueChanges.
pipe(
debounceTime(this.debounceTime)
).subscribe((inputValue) => {
// Sets the masked and normal value
this.onChange(this.setMaskedValue(inputValue));
});
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.maskConfig && this._maskedInput) {
this._maskedInput.updateOptions(changes.maskConfig.currentValue);
}
}
private setErrors(): void {
if (this.ngControl.invalid && this.ngControl.touched) {
this.inputControl.setErrors(this.ngControl.errors);
} else {
this.inputControl.setErrors(null);
}
}
private setInputDisabledState(): void {
if (this.ngControl.disabled !== this.inputControl.disabled) {
this.disabled = this.ngControl.disabled;
}
}
ngOnInit() {
// Get and update the errors on inputControl
if (this.ngControl === null) { return; }
this.setInputDisabledState();
this.ngControl.statusChanges.subscribe(
() => {
this.setInputDisabledState();
});
this.ngControl.valueChanges.subscribe((inputValue) => {
// Set errors
this.setErrors();
// Update errors
this.updateError(this.ngControl.invalid, this.ngControl.errors, this.errors);
this.updateCharCount(inputValue);
});
}
// Reads and add dynamic label from ng-content to mat-label using cdkObserveContent
labelContentChanged() {
// Removing the mat-label content, if present before adding the updated label from ng-content
if (this.labelContentData && this.labelContentData.nativeElement && this.labelContentData.nativeElement.childNodes.length) {
this.labelContentData.nativeElement.innerHTML = '';
}
// Adding the child to the mat-label when label is available in ng-content
if (this.contentLabel && this.contentLabel.nativeElement && this.contentLabel.nativeElement.children.length) {
this.labelContentData.nativeElement.insertAdjacentHTML('beforeend', this.contentLabel.nativeElement.children[0].innerHTML);
}
}
// Sets the masked and normal value
private setMaskedValue(inputValue) {
if (this.maskConfig && this.matInput) {
this._maskedInput = IMask(this.matInput.nativeElement, this.maskConfig);
}
if (!inputValue && this._maskedInput) {
this._maskedInput.value = '';
}
if (this._maskedInput) {
this.maskedValue = this._maskedInput.value;
this.value = this._maskedInput.unmaskedValue;
return this.value;
}
this.maskedValue = inputValue;
this.value = inputValue;
return this.value;
}
// Verifies and updates/adds error to a control
private verifyAndUpdateError() {
if (this.ngControl && this.ngControl.invalid && this.ngControl.touched) {
// Update errors
this.updateError(
this.inputControl.invalid,
this.inputControl.errors,
this.errors,
);
}
}
ngAfterViewInit() {
this._focusMonitor.monitor(this.elementRef.nativeElement, true)
.subscribe(origin => this._ngZone.run(() => {
this._showClearButton = this.formatOrigin(origin);
this._cdr.markForCheck();
}));
if (this.type === 'tel' && !this.maskConfig) {
this.maskConfig = {
mask: '000-000-0000'
};
}
if (this.maskConfig) {
this._maskedInput = IMask(this.matInput.nativeElement, this.maskConfig);
}
// Verifies and updates/adds error to a control
this.verifyAndUpdateError();
// Focus
if (this.autofocus) {
this.matInput.nativeElement.focus();
}
// Triggered an additional change detection to update error on a control
this._cdr.detectChanges();
this.labelContentChanged();
}
public fireBlur(e) {
this.blur.emit(e);
}
// Clears input value
public clear(event) {
// Prevents the datepicker popup to get opened
event.stopPropagation();
// Update char count
this.updateCharCount(null);
// Sets input value to null
this.inputControl.setValue(null);
this.showClearButton();
}
// To show the clear button
public showClearButton(): boolean {
if (this.options.clearButton && this.inputControl.valid && this.inputControl.value && this._showClearButton) {
return true;
} else {
return false;
}
}
private formatOrigin(origin: FocusOrigin): boolean {
return origin ? true : false;
}
// get error message
private getErrorMessage(
activatedErrors: ValidationErrors,
atlasErrors: AtlasFormFieldErrors,
): string {
const _activatedErrors = Object.keys(activatedErrors);
const errors: Map<string, string> = atlasErrors.getErrors();
const error = _activatedErrors.find((errorCode) => errors.has(errorCode));
if (error) {
return atlasErrors.getErrorMessage(error);
}
return 'Invalid Input';
}
// Updates error message according to the ValidationErrors
private updateError(isInputInvalid, activeErrors, errorSet): void {
// Update error message
if (isInputInvalid) {
this.errorMessage = this.getErrorMessage(activeErrors, errorSet);
} else {
this.errorMessage = '';
}
}
// Updates the char counter at Hint section
private updateCharCount(inputValue) {
this.currentInputLength = inputValue?.length || 0;
}
getTabIndex() {
if (this.inputControl.disabled) {
return -1;
} else {
return this.tabIndex || 0;
}
}
// -----------ControlValueAccessor-----------------
// Function to call when the change detects.
onChange = (input) => { };
// Function to call when the input is touched.
onTouched = () => { };
// Allows Angular to update the model.
writeValue(input: any): void {
this.value = input;
// Update the model and changes needed for the view here.
this.inputControl.setValue(this.value, { emitEvent: false });
// Sets the masked value when emitValue is false (default)
this.setMaskedValue(this.value);
// Verifies and updates/adds error to a control
this.verifyAndUpdateError();
}
// Allows Angular to register a function to call when the model changes.
registerOnChange(fn: (input: any) => void): void {
// Save the function as a property to call later here.
this.onChange = fn;
}
// Allows Angular to register a function to call when the input has been touched.
registerOnTouched(fn: any): void {
// Save the function as a property to call later here.
this.onTouched = fn;
}
ngOnDestroy() {
this._focusMonitor.stopMonitoring(this.elementRef.nativeElement);
if (this._maskedInput) {
this._maskedInput.destroy();
}
}
}
input.component.html:
<mat-form-field
class="w-100"
[appearance]="fieldAppearance"
[floatLabel]="floatLabel"
[hideRequiredMarker]="hideRequiredMarker" [color]="color">
<!-- label -->
<span #contentLabel [hidden]="true" (cdkObserveContent)="labelContentChanged()">
<ng-content select="[label]"></ng-content>
</span>
<mat-label *ngIf="label.length">
{{label}}
</mat-label>
<mat-label #labelContentData *ngIf="contentLabel.children.length"></mat-label>
<!-- input -->
<input
(blur)="fireBlur($event)"
[readonly]="readonly"
matInput
#matInput
[type]="type"
[formControl]="inputControl"
[required]="required"
[attr.maxLength]="maxLength"
[placeholder]="placeholder"
[id]="id"
[tabIndex]="getTabIndex()">
<!-- label prefix -->
<span matPrefix>
<ng-content select="[input-prefix]"></ng-content>
</span>
<!-- label suffix -->
<span matSuffix class="d-inline-flex">
<ng-content select="[input-suffix]"></ng-content>
<button
*ngIf="showClearButton()"
type="button"
mat-icon-button
(click)="clear($event)"
[attr.aria-label]="'Clear text'"
[attr.aria-pressed]="'clear'">
<mat-icon [color]="color">cancel</mat-icon>
</button>
</span>
<!-- hints -->
<mat-hint align="start">
<span *ngIf="hintStart.length">{{hintStart}}</span>
<ng-content *ngIf="!hintStart.length" select="[hint-start]"></ng-content>
</mat-hint>
<mat-hint align="end">
<span *ngIf="hintEnd.length">{{hintEnd}}</span>
<ng-content *ngIf="!hintEnd.length" select="[hint-end]"></ng-content>
<span
class="ms-1"
*ngIf="options['charCounter'] && !disabled">
{{currentInputLength}}/{{maxLength}}
</span>
</mat-hint>
<!-- errors -->
<mat-error>
<span>{{errorMessage}}</span>
<mat-icon class="float-right">warning</mat-icon>
</mat-error>
</mat-form-field>