Search code examples
javascriptangulartypescriptangular-reactive-forms

How can i create reusable input field in Angular?


I am trying to create a reusable field as we do in react. But I failed to do that. I need some suggestions or guidance to fix my issue. Actually, I created a component that holds the input field. Now I want to use that field everywhere in the Angular forms. Please help me to fix this.

Whenever i try to submit it always return undefined.

Any solution appreciated!

Login Form

<Modal [isOpen]="true" title="Login" actionLabel="Sign in" [onSubmit]="handleSubmit">
  <form [formGroup]="loginForm">
    <div class="flex flex-col gap-4">
      <app-input placeholder="Email" type="email" controlName="email"
        (formControlChange)="handleFormControl('email',$event)" />
      <app-input placeholder="Password" type="password" controlName="password"
        (formControlChange)="handleFormControl('password',$event)" />
    </div>
  </form>
</Modal>


export class LoginModalComponent implements OnInit {

  loginForm!: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit(): void {
    this.loginForm = this.fb.group({
      email: new FormControl(''),
      password: new FormControl('')
    })
  }

  handleFormControl(formControlName: string, formControl: FormControl) {
    this.loginForm.setControl(formControlName, formControl);
  }

  handleSubmit(): void {
    console.log(this.loginForm);
  }
}

Input Component

import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app-input',
  template: `
    <input
        [placeholder]="placeholder"
        [formControl]="formControl"
        class="
          w-full
          p-4
          text-lg
          bg-black
          border-2
          border-neutral-800
          rounded-md
          outline-none
          text-white
          focus:border-sky-500
          focus:border-2
          transition
          disabled:bg-neutral-900
          disabled:opacity-70
          disabled:cursor-not-allowed
        "
        [type]="type"
    >
  `,
  styles: [
  ]
})
export class InputComponent implements OnInit {
  @Input('placeholder') placeholder: string = '';
  @Input('controlName') controlName: string = '';
  @Input('type') type: string = 'text';
  @Input() required: boolean = false;
  @Input() email: boolean = false;

  @Output() formControlChange = new EventEmitter<FormControl>();
  formControl!: FormControl;

  ngOnInit(): void {
    this.initializeFormControl()
    this.subscribeToValueChange()
    // this.formControlChange.emit(this.formControl);
  }

  initializeFormControl(): void {
    const validators = [];
    if (this.required) {
      validators.push(Validators.required)
    }

    if (this.email) {
      validators.push(Validators.email);
    }

    this.formControl = new FormControl('', validators);
  }

  subscribeToValueChange(): void {
    this.formControl.valueChanges.subscribe((value) => {
      this.formControlChange.emit(this.formControl);
    })
  }
}


Solution

  • You can use NG_VALUE_ACCESSOR implementation for reusable custom form control components

    demo : https://stackblitz.com/edit/angular-module-4xcvpm?file=src%2Fapp%2Fmodel.compoennt.ts

    input.component.ts

    import { Component, Input, forwardRef, OnInit } from '@angular/core';
    import {
      ControlValueAccessor,
      NG_VALUE_ACCESSOR,
      Validators,
      FormControl,
      ValidatorFn,
    } from '@angular/forms';
    
    @Component({
      selector: 'app-input',
      template: `
        <input
            [placeholder]="placeholder"
            [formControl]="formControl"
            class="
              w-full
              p-4
              text-lg
              bg-black
              border-2
              border-neutral-800
              rounded-md
              outline-none
              text-white
              focus:border-sky-500
              focus:border-2
              transition
              disabled:bg-neutral-900
              disabled:opacity-70
              disabled:cursor-not-allowed
            "
            [type]="type"
        >`,
      providers: [
        {
          provide: NG_VALUE_ACCESSOR,
          useExisting: forwardRef(() => InputComponent),
          multi: true,
        },
      ],
      styles: [],
    })
    export class InputComponent implements OnInit, ControlValueAccessor {
      @Input('placeholder') placeholder: string = '';
      @Input('type') type: string = 'text';
      @Input() required: boolean = false;
      @Input() email: boolean = false;
    
      formControl!: FormControl;
      onTouched: any;
      onChange: any;
    
      ngOnInit(): void {
        const validators: ValidatorFn[] = [];
    
        if (this.required) {
          validators.push(Validators.required);
        }
    
        if (this.email) {
          validators.push(Validators.email);
        }
    
        this.formControl = new FormControl('', validators);
      }
    
      writeValue(value: any): void {
        this.formControl.setValue(value);
      }
    
      registerOnChange(fn: any): void {
        this.onChange = fn;
        this.formControl.valueChanges.subscribe(fn);
      }
    
      registerOnTouched(fn: any): void {
        this.onTouched = fn;
      }
    
      setDisabledState(isDisabled: boolean): void {
        isDisabled ? this.formControl.disable() : this.formControl.enable();
      }
    }
    

    login-modal.component.ts

    import { Component, OnInit } from '@angular/core';
    import { FormBuilder, FormGroup, Validators } from '@angular/forms';
    
    @Component({
      selector: 'app-login-modal',
      template: `
        <Modal [isOpen]="true" title="Login" actionLabel="Sign in" (submit)="handleSubmit()">
          <form [formGroup]="loginForm">
            <div class="flex flex-col gap-4">
              <app-input placeholder="Email" type="email" required="true" email="true" formControlName="email"></app-input>
              <app-input placeholder="Password" type="password" required="true" formControlName="password"></app-input>
            </div>
          </form>
        </Modal>
      `,
      styles: [],
    })
    export class LoginModalComponent implements OnInit {
      loginForm!: FormGroup;
    
      constructor(private fb: FormBuilder) {}
    
      ngOnInit(): void {
        this.loginForm = this.fb.group({
          email: ['', [Validators.required, Validators.email]],
          password: ['', Validators.required],
        });
    
        this.loginForm.get('email')!.valueChanges.subscribe((value) => {
          console.log('email value changed:', value);
        });
    
        this.loginForm.get('password')!.valueChanges.subscribe((value) => {
          console.log('password value changed:', value);
        });
      }
    
      handleSubmit(): void {
        console.log('heloo');
        console.log(this.loginForm.value);
      }
    }
    

    modal.component.ts

    import { Component, Input, Output, EventEmitter } from '@angular/core';
    
    @Component({
      selector: 'Modal',
      template: `
        <div *ngIf="isOpen" class="modal">
          <h2>{{ title }}</h2>
          <ng-content></ng-content>
          <button (click)="submit.emit()">{{ actionLabel }}</button>
        </div>
      `,
    })
    export class ModalComponent {
      @Input() isOpen: boolean = false;
      @Input() title: string = '';
      @Input() actionLabel: string = '';
      @Output() submit = new EventEmitter<void>();
    }