feat: add reusable input
This commit is contained in:
19
src/app/components/form-field/form-field.html
Normal file
19
src/app/components/form-field/form-field.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<div class="form-field">
|
||||
<label [for]="control.name">{{label()}}</label>
|
||||
<input
|
||||
(input)="onChange($event)"
|
||||
(blur)="onTouchedFn()"
|
||||
[disabled]="isDisabled()"
|
||||
[id]="control.name"
|
||||
[value]="control.value"
|
||||
placeholder="{{placeholder()}}"
|
||||
type="{{type()}}"
|
||||
>
|
||||
<div class="errors-container">
|
||||
@for(error of errorsDictionary()|keyvalue; track error.key) {
|
||||
@if(control.hasError(error.key) && isDirty){
|
||||
<p class="error-text">• {{error.value}}</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
31
src/app/components/form-field/form-field.scss
Normal file
31
src/app/components/form-field/form-field.scss
Normal file
@@ -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;
|
||||
}
|
||||
47
src/app/components/form-field/form-field.spec.ts
Normal file
47
src/app/components/form-field/form-field.spec.ts
Normal file
@@ -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: ` <form [formGroup]="form">
|
||||
<app-form-field formControlName="mock"/>
|
||||
</form>`,
|
||||
})
|
||||
class FormMock {
|
||||
formField = viewChild.required<FormField>(FormField);
|
||||
form = new FormGroup({
|
||||
mock: new FormControl(),
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
describe('FormField', () => {
|
||||
let component: FormMock;
|
||||
let fixture: ComponentFixture<FormMock>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
53
src/app/components/form-field/form-field.ts
Normal file
53
src/app/components/form-field/form-field.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user