feat: add reusable input

This commit is contained in:
2025-12-11 21:05:40 -03:00
parent bd48ea25b7
commit 515b762862
4 changed files with 150 additions and 0 deletions

View 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>

View 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;
}

View 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);
});
});

View 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;
}
}