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