diff --git a/src/app/components/form-field/form-field.html b/src/app/components/form-field/form-field.html new file mode 100644 index 0000000..7cb5fb2 --- /dev/null +++ b/src/app/components/form-field/form-field.html @@ -0,0 +1,19 @@ +
+ + +
+ @for(error of errorsDictionary()|keyvalue; track error.key) { + @if(control.hasError(error.key) && isDirty){ +

• {{error.value}}

+ } + } +
+
\ No newline at end of file diff --git a/src/app/components/form-field/form-field.scss b/src/app/components/form-field/form-field.scss new file mode 100644 index 0000000..207ea00 --- /dev/null +++ b/src/app/components/form-field/form-field.scss @@ -0,0 +1,31 @@ +@media (min-width: 768px) { + .form-field { + flex: 0 0 calc(33.33% - 1rem); + } +} + +.form-field { + padding: 0.5rem 0; + + input[type='text'], + input[type='tel'] { + width: 100%; + border: none; + padding: 0.5rem; + height: 3rem; + margin-top: 0.5rem; + } + + label { + font-size: 1.2rem; + } +} + +.errors-container { + margin-top: 0.4rem; +} + +.error-text { + margin: 0; + color: #000; +} diff --git a/src/app/components/form-field/form-field.spec.ts b/src/app/components/form-field/form-field.spec.ts new file mode 100644 index 0000000..36dd1fe --- /dev/null +++ b/src/app/components/form-field/form-field.spec.ts @@ -0,0 +1,47 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FormField } from './form-field'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { Component, viewChild } from '@angular/core'; +import { By } from '@angular/platform-browser'; + +@Component({ + selector: 'form-mock', + imports: [ReactiveFormsModule, FormField], + template: `
+ + `, +}) +class FormMock { + formField = viewChild.required(FormField); + form = new FormGroup({ + mock: new FormControl(), + }); + +} + +describe('FormField', () => { + let component: FormMock; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FormField, ReactiveFormsModule], + }).compileComponents(); + fixture = TestBed.createComponent(FormMock); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should trigger onChanges', () => { + const onChangeSpy = spyOn(component.formField(), 'onChange').and.callThrough(); + const input = fixture.debugElement.query(By.css('input')); + input.triggerEventHandler('input', {target: {value: 'a'}}); + fixture.detectChanges(); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/app/components/form-field/form-field.ts b/src/app/components/form-field/form-field.ts new file mode 100644 index 0000000..969457d --- /dev/null +++ b/src/app/components/form-field/form-field.ts @@ -0,0 +1,53 @@ +import { KeyValuePipe } from '@angular/common'; +import { Component, computed, input, Optional, Self, signal } from '@angular/core'; +import { ControlValueAccessor, NgControl, ReactiveFormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-form-field', + imports: [ReactiveFormsModule, KeyValuePipe], + templateUrl: './form-field.html', + styleUrl: './form-field.scss', +}) +export class FormField implements ControlValueAccessor { + readonly disabled = input(false); + readonly errorsDictionary = input<{ [key: string]: string }>({}); + readonly formDisabled = signal(false); + readonly isDisabled = computed(() => this.disabled() || this.formDisabled()); + readonly label = input(''); + readonly type = input('text'); + placeholder = input(''); + + onChangeFn: any = () => {}; + onTouchedFn: any = () => {}; + + constructor(@Self() @Optional() protected readonly control: NgControl) { + if (this.control === null) { + console.warn('Form control is null, did you forget to add the formGroup?'); + } + this.control.valueAccessor = this; + } + + writeValue(obj: any): void {} + + registerOnChange(fn: any): void { + this.onChangeFn = fn; + } + + registerOnTouched(fn: any): void { + this.onTouchedFn = fn; + } + + setDisabledState?(isDisabled: boolean): void { + this.formDisabled.set(isDisabled); + } + + onChange(event: Event): void { + const data = (event.target as HTMLInputElement).value; + this.onChangeFn(data); + this.onTouchedFn(); + } + + get isDirty() { + return this.control.dirty === null ? false : this.control.dirty; + } +}