8000 There is no FormControl instance attached to form control element with name: xxx · Issue #14057 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

There is no FormControl instance attached to form control element with name: xxx #14057

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
charlieamer opened this issue Jan 22, 2017 · 39 comments
Labels
area: forms freq2: medium P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent state: confirmed type: bug/fix
Milestone

Comments

@charlieamer
Copy link

I'm submitting a ... (check one with "x")

[x] bug report => search github for a similar issue or PR before submitting
[ ] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

When using ReactiveForms and we override old form group with the new one (using for example this.form = new FormGroup(...)) an error occurs (the error is mentioned in the title).
(The error can be seen in console).

Expected behavior

No error happens.

Minimal reproduction of the problem with instructions

http://plnkr.co/jVMbtQGKeYhPsCkrzQN8
The error can be seen after 3 seconds (I used set timeout in the example). Error should be visible in console.

What is the motivation / use case for changing the behavior?

I understand that this is not the best way to use form groups, i should check if they are already created, and create it only if necessary. However, this bug indicates (I concluded this from stacktrace) that control set-up doesn't work after it's been cleaned up. This should not be the case. Problem occurs because Angular's cleanUpControl function sets up change callback to its own handler (the handler just throws error), and then in setUpControl function, Angular calls write value BEFORE it registers callback handler. It means that the last handler remembered by the custom control was cleanUpControl's handler which only throws an error. Functions setUpControl and cleanUpControl can be found in @angular/forms/src/directives/shared.js

Please tell us about your environment:

Project is set-up using angular-cli, but it doesn't really affect the problem if I change the setup.

  • Angular version: 2.0.X

2.4.4

  • Browser: [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ]

I tested this on Chrome and Firefox, they both reproduce the problem

  • Language: [all | TypeScript X.X | ES6/7 | ES5]

Typescript

  • Node (for AoT issues): node --version =
@DzmitryShylovich
Copy link
Contributor

You have to use FormGroup.addControl/removeControl. How angular can know that you created a new FromGroup?

@charlieamer
Copy link
Author

Well as I said, I understand that I am not using the form in the correct way. However, I am concerned if this will potentially create bugs in some other use-cases. In my opinion, if control set-up is called after control clean-up, there should be no error, or maybe I am missing something here ?

@Snesi
Copy link
Snesi commented May 4, 2017

Hi I'm having the same problem, but I tried to reproduce it using one of Angular's examples and it doesn't happen.

I can replace and modify the FormGroup without seeing the error. I don't understand why the error isn't showing up.

https://plnkr.co/edit/FSiL2g?p=preview

@skshirsagar
Copy link

@charlieamer, could you find solution to your problem? I am experiencing the exact issue. As @Snesi mentioned, this problem does not happen with Angular's example code. I have tried to compare every single aspect between these apps and can't seem to find why it works in one case and not in other...

@skshirsagar
Copy link

@charlieamer BTW, I have tried to do removeControl and addControl instead of creating new FormGroup and that does not help either.

@charlieamer
Copy link
Author

Sorry for being off for a long time, I've been away @Snesi. I was trying to find how I solved the problem through my commits, but I couldn't find the solution. Can you please post some of the screenshots of your code @skshirsagar ?

@skshirsagar
Copy link

@charlieamer it is the exact same code that you have in your plunker snippet.

@skshirsagar
Copy link

Here is a plunker forked from yours where I am doing removeControl and addControl instead of creating new FormGroup. It has the exact same as your original one...
http://plnkr.co/edit/KA8QnMsnLksTlU8xYIok?p=preview

@charlieamer
Copy link
Author

I think I solved the problem by using FormArray, let me just remember how exactly I solved it

@charlieamer
Copy link
Author

Using FormArray I solved my problem. However I get array as value, which may or may not be what you want. Example output { arr: [ 3, 4] }

@Component({
  selector: 'my-app',
  template: `
  <form [formGroup]='form.controls.arr'>
    <test formControlName='0'>
    </test>
  </form>
  Form value: {{form.value | json}}
  `
})
export class AppComponent implements OnInit {
  form = new FormGroup({
    arr: new FormArray([new FormControl(3)])
  }<
8000
/span>);
  ngOnInit() {
    setTimeout(() => {
      this.form.controls['arr'].push(new FormControl(4));
    }, 3000);
  }
}

@Snesi
Copy link
Snesi commented May 15, 2017

Hi, I solved my problem too, but I'm not sure how. I ended up refactoring the whole thing and added this little bit of code that might have something to do with it:

if(!!this.nTextForm.get(name)){
  this.nTextForm.setControl(name, newFormControl)
} else {
  this.nTextForm.addControl(name, newFormControl)
}

So nTextForm is my FormGroup with n form controls. I get the input field names from a backend bot, so the names might repeat themselves. If the name is already being used, I found that addControl is ignored, so I have to use setControl instead.

@marinho
Copy link
marinho commented May 31, 2017

I have the same issue. Using setControl/addControl doesn't make difference, as I have a totally new form instance, like this:

initializeForm() {
    this.form = this.formBuilder.group({
        field1: null,
        field2: this.formBuilder.group({ subField1: null, subField2: null })
    });
}

So, initializeForm is called when route.queryParams emits a new value.

The FormArray might work, but it's definitely an ugly solution.

I tried @Snesi's trick of having a wrapper FormGroup, but it still fails in the same way. Is there another solution?

@leandroz
Copy link
leandroz commented Jun 10, 2017

To add more information, in my case the error only happens with nested FormGroups.
I create an entire new form each time the user changes a field, some controls are the same and other change. There is one that is a FormGroup where the name does not change, but the FromControls inside do, and there is when the error happens.

I was able to go around the issue by creating a FormArray instead of a FormGroup,

Something more to add, this wasn't happening in 2.x versions.

@nikita-yaroshevich
Copy link

I have the similar issue. But I was able to solve it using
[formControl]="form.controls['mycontrol']"
instead of
formControlName="mycontrol"
Maybe it will usefull for someone else)

@mariohmol
Copy link

@nikita-yaroshevich nice catch! In my case it shows the error message on the console but everything seems to work fine.

I'm using ionic3 and it works for every component.. the only one that shows that error is the radio, that can be usefull to try to reproduce the error.

Maybe (at least for my case) can be something relative to be a formArray, as it may handle radio as a list.

@maximkott
Copy link

@nikita-yaroshevich Thank you so much!

Any ideas what causes this issue? Also, any plans on fixing this?

@raysuelzer
Copy link

I suspect this has to do with the rendering life cycle. In my case, I am removing an item from the form array... But it looks like there might be a render before this is actually completed.. perhaps because i am using lodash and it might be outside of the zone.

@raysuelzer
Copy link
raysuelzer commented Nov 30, 2017

So, it looks like this has to do with an onTouched event listener on custom controls and is probably a bug.

When angular removes a control it registers runs this code

function cleanUpControl(control, dir) {
    /** @type {?} */ ((dir.valueAccessor)).registerOnChange(function () { return _noControlError(dir); }); /** @type {?} */
    ((dir.valueAccessor)).registerOnTouched(function () { return _noControlError(dir); });

However, a custom control will mark itself as 'touched' or 'changed' after the formControl has been removed, which will cause an error. I'll try and make a minimum reproduction at some point.

@jiahang-ma
Copy link

When I use the 0.6.2 version,This BUG will not appear

@MichaelBrimer
Copy link

I had this error “There is no FormControl instance attached to form control element with name”
But calling disable on the formgroup before removing the items solved the problem.
this.formGroup.disable();
DeleteComponents ………………..

@robvaneck
Copy link

Im also trying to delete a formGroup from my large Reactive FormArray[]
using code below.. and sometimes I get a

There is no FormControl instance attached to form control element with path: 'rides -> 1 -> distance'
  // deletes a single ride
  public deleteRide(ride: FormGroup, i: number): void{
    this.unsubscribeChanges(ride);
    this.myRides.removeAt(i);
    
    // fire the mileage validators again.. 
    // not toon soon
    setTimeout(()=>{
      this.revalidate();
    }, 0);
  }

  // validate the form again
  // we need this because the validation is only fired on input value changes
  // not position / array changes
  private revalidate(): void{

    // remove 'new', so validation class shows
    this.ridesForm.get('rides')['controls'].forEach((ride)=>{
      for (const prop in ride.controls) {
        ride.controls[prop].updateValueAndValidity({ onlySelf: false, emitEvent: false });
      }
      ride.new = false;
    });

    // mark everything as touched, so errors pop-up
    Utils.touchAllFormFields(this.ridesForm);
  }

@ngbot ngbot bot added this to the Backlog milestone Jan 23, 2018
@Tarnadas
Copy link

I also encounter this problem when using AbstractControl#setControl().
I want to patch my form with values received from the backend. There is a FormArray I need to patch, but since I cannot dynamically change its size, I have to create a new one and reassign it to the form.

  const emptyControl = {
    someControl1: [ '', [ Validators.required ] ],
    someControl2: [ '', [ Validators.required ] ],
    someControl3: [ 0, [ Validators.required ] ],
  };

  function patchValue(value) {
    this.form.reset();
    const newFormArray = this.formBuilder.array([]);
    value.someKey.forEach(() => {
      newFormArray.push(this.formBuilder.group(emptyControl));
    });
    this.form.setControl('someKey', newFormArray );
    setTimeout(() => {
      this.form.setValue(value);
      this.changeDetectorRef.markForCheck();
    });
  }

The workaround posted by @nikita-yaroshevich resolved my issue.

@m-radzikowski
Copy link

I've the same problem using custom form field component (implementing ControlValueAccessor):

<app-my-custom-form-control formControlName="myField"></app-my-custom-form-control>

From what can I see, when I call

this.form = buildForm();

when I want to have fresh instance of it on some conditions change, there is registered function

ƒ () { return _noControlError(dir); }

as onChange() callback. Then writeValue is called with new, fresh value, which makes my custom component emit event on onChange callback. And since error function is registered there, the error is thrown.

It's also solved by @nikita-yaroshevich workaround with using formControl directive:

<app-my-custom-form-control [formControl]="form.controls['myField']"></app-my-custom-form-control>

@peturh
Copy link
peturh commented Sep 6, 2018

This bug is really bad!

The point of having interfaces are that they should guarantee to work with the components and modules that uses them.

It got solved for me using the same as everyone else, except I use the getter on my form.

<app-my-custom-form-control [formControl]="form.get('myField')"></app-my-custom-form-control>

@MariMax
Copy link
MariMax commented Sep 7, 2018

as a workaround instead of recreating the form group it is possible to reset it's value

    this.quoteForm = this.fb.group({
      price: [0],
    });
...
    this.quoteForm.setValue({
      price: newPrice,
    })

@huskerona
Copy link

Solution by @nikita-yaroshevich is great, but this really needs to be fixed. Referencing a control this way is not practical at all. Almost all the custom components, in my application, that implement ControlValueAccessor seem to be affected.

@m-radzikowski
Copy link

I've managed to solve this error for custom form control component. The problem was emitting change event when form was re-created and new value was assigned to component.

Small example - I hope it will help some of you, cause the problem may be the same for few people here:

@Component({
  selector: 'app-custom-form-control',
  templateUrl: './custom-form-control.component.html',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomFormControlComponent),
      multi: true
    }
  ]
})
export class CustomFormControlComponent implements OnInit, ControlValueAccessor {

  form = this.fb.group({
    firstname: [''],
    lastname: [''],
  });

  private onChange: (value: any) => void = () => null;

  constructor(private fb: FormBuilder) {
  }

  ngOnInit() {
    this.emitValueChanges();
  }

  private emitValueChanges() {
    this.form.valueChanges.subscribe(data => { // TODO Unsubscribe in ngOnDestroy
      this.onChange(data);
    });
  }

  writeValue(value: any): void {
    this.form.setValue(value, {
      emitEvent: false, // THIS WAS THE MISSING PART
    });
  }

  registerOnChange(fn: (value: any) => void): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    // TODO Implement
  }

  setDisabledState(isDisabled: boolean): void {
    isDisabled ? this.form.disable() : this.form.enable();
  }
}

Component usage:

<form [formGroup]="myForm" (ngSubmit)="myFormSubmit()">
  <app-custom-form-control formControlName="name"></app-custom-form-control>
  <button type="submit" [disabled]="myForm.invalid">Submit</button>
</form>

Recreating myForm in submit callback does no longer throw exception.

@dgroh
Copy link
dgroh commented Oct 23, 2018

same here when using setControl

  private setCoordinatesControl() {
    this.attributesForm.removeControl('coordinatesFormArray');

    const control = new FormArray([]);

    this.coordinates.forEach(coordinate => {
      control.push(
        this.fb.group({
          x: coordinate.x,
          y: coordinate.y
        })
      );
    });

    this.attributesForm.setControl('coordinatesFormArray', control);

    return this.attributesForm.get('coordinatesFormArray');
  }

@cbaddeley
Copy link
cbaddeley commented Feb 20, 2019

We just ran in to this same issue. The template changes didn't work for us so we went back to our custom form controls and found out we were implementing them incorrectly since it only happened with three of our seven custom form controls. The changes made to our three form controls that were affected were all different, but it came down to the problem of calling the onChange in the value setter which is initially set in the writeValue of the component.

So I don't know if this is a bug with Angular if it can be fixed by restructuring a custom form control.

I made a stackblitz that duplicates the error with some comments in the custom form control so you can see how we went about fixing it. Thank you everyone for the tips it helped a lot figuring this thing out.

https://stackblitz.com/edit/angular-cyuhs5

In case the stackblitz ever messes up here's the custom form control component

const CUSTOM_CONTROL_VALUE_ACCESSOR: Provider = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => CustomFormControl),
  multi: true
};

@Component({
  selector: 'app-custom-form-control',
  template: '<button (click)="updateValue(1)">1</button><button (click)="updateValue(2)">2</button>',
  providers: [CUSTOM_CONTROL_VALUE_ACCESSOR]
})

// This is currently broken. 
export class CustomFormControl {

  private _value: number;
  public get value() {
    return this._value;
  }
  public set value(value: number) {
    this._value = value;
    this.onChange(this._value); // Comment out this line
  }

  updateValue(value: string) {
    this.value = parseFloat(this.value.toString() + value);
    // this.onChange(this.value); //Uncomment this line
  }
  private onTouched = () => { };
  private onChange: any = () => { };

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  writeValue(value: number): void {
    this.value = value || 0;
  }
}

and here's the form component

@Component({
  selector: 'app-example-form',
  template: `<button (click)="toggleFormGroup()">Toggle Form Groups</button>
            <br>
            <br>
            <form [formGroup]="exampleForm">
              <div [formGroup]="targetFormGroup">
                <app-custom-form-control formControlName="value"></app-custom-form-control>
              </div>
            </form>
            <p>Value at index {{ arrayIndex }} : {{ targetFormGroup.get('value').value }}</p>`
})
export class ExampleForm {

  private _toggle = false;
  public arrayIndex = 0;

  public exampleForm = new FormGroup({
    exampleFormArray: new FormArray([
      new FormGroup({
        value: new FormControl
      }),
      new FormGroup({
        value: new FormControl()
      })
    ])
  });

  public targetFormGroup: FormGroup = this.exampleForm.get(['exampleFormArray', 0]) as FormGroup;

  public toggleFormGroup() {
    this._toggle = !this._toggle;
    if (this._toggle) {
      this.targetFormGroup = this.exampleForm.get(['exampleFormArray', 1]) as FormGroup;
      this.arrayIndex = 1;
    } else {
      this.targetFormGroup = this.exampleForm.get(['exampleFormArray', 0]) as FormGroup;
      this.arrayIndex = 0;
    }
  }

}

@sangecz
Copy link
sangecz commented Jun 6, 2019

is there any better alternative to angular reactive forms? Could not count times trying to figure out what's wrong in my code (especially impl. control value accessor), finding out that I'm using not right syntax for particular case although [formControl] and [formControlName] syntaxes counter-intuitively seems dual. thx

@JDouven
Copy link
JDouven commented Aug 19, 2019

To add to this comment from @m-radzikowski:

In my case I was using:

writeValue(value: any): void {
  this.form.controls['button'].setValue(value, {
    emitEvent: false,
  });
}

Using emitEvent: false here did not suppress updates from this.form.valueChanges, therefore my onChange was still being called. I solved it by subscribing to each individual control's valueChanges instead:

private emitValueChanges() {
  this.form.controls['button'].valueChanges.subscribe(() => this.onChange());
}

@jek-bao-choo
Copy link
jek-bao-choo commented Apr 8, 2020

I've managed to solve this error for custom form control component. The problem was emitting change event when form was re-created and new value was assigned to component.

Small example - I hope it will help some of you, cause the problem may be the same for few people here:

@Component({
  selector: 'app-custom-form-control',
  templateUrl: './custom-form-control.component.html',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomFormControlComponent),
      multi: true
    }
  ]
})
export class CustomFormControlComponent implements OnInit, ControlValueAccessor {

  form = this.fb.group({
    firstname: [''],
    lastname: [''],
  });

  private onChange: (value: any) => void = () => null;

  constructor(private fb: FormBuilder) {
  }

  ngOnInit() {
    this.emitValueChanges();
  }

  private emitValueChanges() {
    this.form.valueChanges.subscribe(data => { // TODO Unsubscribe in ngOnDestroy
      this.onChange(data);
    });
  }

  writeValue(value: any): void {
    this.form.setValue(value, {
      emitEvent: false, // THIS WAS THE MISSING PART
    });
  }

  registerOnChange(fn: (value: any) => void): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    // TODO Implement
  }

  setDisabledState(isDisabled: boolean): void {
    isDisabled ? this.form.disable() : this.form.enable();
  }
}

Component usage:

<form [formGroup]="myForm" (ngSubmit)="myFormSubmit()">
  <app-custom-form-control formControlName="name"></app-custom-form-control>
  <button type="submit" [disabled]="myForm.invalid">Submit</button>
</form>

Recreating myForm in submit callback does no longer throw exception.

@m-radzikowski solution works. Thanks for sharing your solution. After playing around a bit, I fixed mine by checking for falsy in writeValue() method. See below reference.


import {ChangeDetectionStrategy, Component, forwardRef, Input} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR} from '@angular/forms';
import {MatDatepickerInputEvent} from '@angular/material/datepicker';
import * as moment from 'moment';

@Component({
  selector: 'app-cva-date',
  templateUrl: './cva-date.component.html',
  styleUrls: ['./cva-date.component.css'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CvaDateComponent),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => CvaDateComponent),
      multi: true
    }
  ],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CvaDateComponent implements ControlValueAccessor {

  @Input()
  _dateValue; // notice the '_'

  minDate: any = new Date(1900, 0, 1);
  maxDate: any = new Date();

  get dateValue() {
    return moment(this._dateValue, 'LL');
  }

  set dateValue(val) {
    this._dateValue = moment(val).format('LL');
    this.propagateChange(this._dateValue);
  }

  addEvent(type: string, event: MatDatepickerInputEvent<Date>) {
    this.dateValue = moment(event.value, 'LL');
  }

  writeValue(obj: any): void {
    if (obj) {
      this.dateValue = moment(obj, 'LL');
    }
  }

  // Function to call when changed
  propagateChange: any = (_: any) => { };

  /* this propagates the value */
  // Allows Angular to register a function to call when the model changes.
  // Save the function as a property to call later here.
  registerOnChange(fn: any) {
    this.propagateChange = fn;
  }

  /* IMPORTANT - Can not propagateChange on registerOnTouched. It will not work in registering in formControl */
  registerOnTouched(fn: any) { }

  // Function to call for validation ControlValueAccessor
  validateFn: any = () => { };

  validate(c: FormControl) {
    return this.validateFn(c);
  }

}

<div [formGroup]="form">
 <p><label [attr.for]="question?.key">{{question?.label}}</label></p>
      <app-cva-date [formControlName]="question?.key" [id]="question?.key"></app-cva-date>
</div>

@jelbourn jelbourn added P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent and removed severity3: broken labels Oct 1, 2020
@Krysztal
Copy link
Krysztal commented Nov 4, 2021

5 years after issue creation and still forms cause this bug. Are you going to fix it?

@Hypenate
Copy link
Hypenate commented Jul 5, 2022

Same issue, we create the form when the entity changes (using rxjs).

Throws the error:

            <sg-switch  formControlName="enableArchiving">
              ...
            </sg-switch>

Works correctly:

            <sg-switch [formControl]="$any(form.get('enableArchiving'))">
              ...
            </sg-switch>

@billypopper7
Copy link

This is still an issue with Angular 14. This comment solved it for me.

@maxenge-l
Copy link

I've another solution that worked for me. I emit the form as an Observable like this

form$ = combineLatest([this.x$, this.y$]).pipe(
  map(([x, y]) => {
    return this.fb.group({
      x: this.fb.control(x),
      y: this.fb.control(y),
    });
  }),
  mergeMap((val) => {
    return from([null, val]);
  }),
  shareReplay(1),
);

The interesting part is:

 mergeMap((val) => {
    return from([null, val]);
  })

I basically interlace null values between each form. Like [Form, null, Form, null] and so on. This seems to fix the cleanup process / state problems inside angular. The template looks like the following:

<form [formGroup]="form" *ngIf="form$ | async as form">
    <wiz-text-input  formControlName="x"></wiz-text-input>
</form>

@thanh-nguyen-codeleap
Copy link

It's been 7 years and Angular team still decide bug like this is not worth fixing?

@kudak3
Copy link
kudak3 commented Oct 28, 2024

We experienced a similar issue using Angular 18. We were removing a control, performing some logic, and adding the control again, and the error occurred interminantly.

We solved it by detecting changes before adding the control.
i.e adding the line this.cdr.detectChanges()

this.formGroup.removeControl(field, {
  emitEvent: false,
});
 ...
 //some logic here
 ...
this.cdr.detectChanges()
this.formGroup.addControl(field, {
  emitEvent: false,
});

@Will-at-FreedomDev
Copy link

It's been 7 years and Angular team still decide bug like this is not worth fixing?

8 years now :) Glad we could change our code to initialize the form group exactly one time (it's a Create/Edit form). Definitely sunk too much time into chasing this issue down though.

Kind of a low hanging enhancement - maybe update the error message to suggest initializing the form once? Or even just linking to this GitHub issue? I remember there was an initiative in angular to make error messages more helpful. If this bug is not going to be addressed, at least update the error message to offer some workarounds or guidance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: forms freq2: medium P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent state: confirmed type: bug/fix
Projects
None yet
Development

No branches or pull requests

0