angulartypescriptangular-reactive-formsangular-formsangular-signals

NG01203: No value accessor for form control name: 'name'


This is a repost of the deleted question from JOYBOY with title NG01203: No value accessor for form control name: 'name'


I've created Angular 18 application. I'm using both combination of standalone components and module wise components.

For re-usability, created input component. But I'm getting error

ERROR Error: NG01203: No value accessor for form control name: 'name'. Find more at https://angular.dev/errors/NG01203
    at _throwMissingValueAccessorError (forms.mjs:3420:9)
    at setUpControl (forms.mjs:3202:29)
    at _FormGroupDirective.addControl (forms.mjs:5320:5)
    at _FormControlName._setUpControl (forms.mjs:6011:39)
    at _FormControlName.ngOnChanges (forms.mjs:5960:28)
    at _FormControlName.rememberChangeHistoryAndInvokeOnChangesHook (core.mjs:3975:10)
    at callHookInternal (core.mjs:5004:10)
    at callHook (core.mjs:5031:5)
    at callHooks (core.mjs:4988:9)
    at executeInitAndCheckHooks (core.mjs:4943:5)
type InputProps = {
  formGroup: FormGroup;
  type: 'text' | 'password' | 'number';
  value: string;
  label: string;
  formControlName: string;
  placeholder: string;
  checkValidation: boolean;
}

@Component({
  selector: 'app-input',
  standalone: true,
  imports: [
    FormsModule,
    ReactiveFormsModule,
    NgClass
  ],
  templateUrl: './input.component.html',
})
export class InputComponent {

  public readonly formGroup = input.required<FormGroup>();
  public readonly label = input.required<InputProps['label']>();
  public readonly formControlName = input.required<InputProps['formControlName']>();
  public readonly checkValidation = input<InputProps['checkValidation']>(false);
  public readonly placeholder = input<InputProps['placeholder']>();

  public type = input<InputProps['type']>('text');
  public value = input<InputProps['value']>('');
}
input.component.html
<input
      [type]="type()"
      [id]="label()"
      [formControlName]="formControlName()"
      [ngClass]="inputBaseClasses"
      [value]="value()"
    />
    <label [for]="label()" [ngClass]="labelBaseClasses">
      {{ label() }}
    </label>
  </div>

I'm using this in my AuthModule.ts

@NgModule({
  declarations: [
    LoginComponent,
    RegisterComponent,
    AuthComponent
  ],
  imports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,
    AuthRoutingModule,
    AngularSvgIconModule.forRoot(),
    ButtonComponent,
    InputComponent
  ],
  exports: [
    FormsModule,
    ReactiveFormsModule
  ]
})
export class AuthModule { }

app.route.ts


export const routes: Routes = [
    {
        path: '',
        pathMatch: 'full',
        component: AppComponent
    },
    {
        path: 'auth',
        loadChildren: () => import('@app/core/auth/auth.module')
            .then(m => m.AuthModule)
    }
];

I'm using app-input component.

<app-input 
      label="Name"
      formControlName="name" 
      [formGroup]="signUpForm" 
    ></app-input>

I did try export FormsModule & ReactiveFormsModule from AuthModule.ts but still gives me no value accessor for form control : 'name'.

I am out of ideas right now.


Solution

  • When creating a custom form control, you generally do not need to feed in the formControlName it exists on the component definition.

    The custom component you create extends ControlValueAccessor which takes care of the binding to form control.

    Tutorial for create custom form control

    Example Stackblitz for reference

    The changes made to the code are as follows.

    First we wrap the custom component inside the form with the formGroup binded. Also notice that we have added the form control name on the custom component.

    <form [formGroup]="signUpForm">
      <app-input 
        label="Name"
        formControlName="name" 
      ></app-input>
    </form>
    

    Then we create the custom component control, by referring the above links.

    I removed the form control name from the component and just added an input event. The valueChanged, will store the updated value in the internal property _value.

    valueChanged(val: string) {
      this.onChange(val);
    }
    

    When a change happens it's captured by input and triggers the onChange event of the control, which updates the actual form control.

    When focus happens, I trigger the markAsTouched event.

    I bind the disabled flag to the HTML, through attributes.

    We can use writeValue to bind the value from form control into the html.

    writeValue(value: number) {
      this._value = value;
    }
    

    Full Code:

    import { Component, input } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import {
      ReactiveFormsModule,
      FormGroup,
      FormsModule,
      FormControl,
      ControlValueAccessor,
      NG_VALUE_ACCESSOR,
      NgControl,
    } from '@angular/forms';
    import { CommonModule, NgClass } from '@angular/common';
    
    type InputProps = {
      formGroup: FormGroup;
      type: 'text' | 'password' | 'number';
      value: string;
      label: string;
      formControlName: string;
      placeholder: string;
      checkValidation: boolean;
    };
    
    @Component({
      selector: 'app-input',
      standalone: true,
      imports: [FormsModule, ReactiveFormsModule, NgClass],
      template: `
      <input
          [type]="type()"
          #val
          (focus)="markAsTouched()"
          (input)="valueChanged(val.value)"
          [disabled]="disabled"
          [value]="_value"
        />
        <label [for]="label()">
          {{ label() }}
        </label>
      `,
      providers: [
        {
          provide: NG_VALUE_ACCESSOR,
          multi: true,
          useExisting: InputComponent,
        },
      ],
    })
    export class InputComponent implements ControlValueAccessor {
      public readonly label = input.required<InputProps['label']>();
      public readonly checkValidation = input<InputProps['checkValidation']>(false);
      public readonly placeholder = input<InputProps['placeholder']>();
    
      public type = input<InputProps['type']>('text');
      onChange = (value: any) => {};
      _value!: any;
      onTouched = () => {};
    
      touched = false;
    
      disabled = false;
    
        writeValue(value: number) {
          this._value = value;
        }
    
        valueChanged(val: string) {
          this.onChange(val);
        }
    
      registerOnChange(onChange: any) {
        this.onChange = onChange;
      }
    
      registerOnTouched(onTouched: any) {
        this.onTouched = onTouched;
      }
    
      markAsTouched() {
        if (!this.touched) {
          this.onTouched();
          this.touched = true;
        }
      }
    
      setDisabledState(disabled: boolean) {
        this.disabled = disabled;
      }
    }
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [ReactiveFormsModule, InputComponent, CommonModule],
      template: `
        <form [formGroup]="signUpForm">
          <app-input 
            label="Name"
            formControlName="name" 
          ></app-input>
        </form>
    
        {{signUpForm.value | json}}
      `,
    })
    export class App {
      signUpForm = new FormGroup({
        name: new FormControl('test'),
      });
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo