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 @@
+
\ 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;
+ }
+}