Search code examples
javascriptangularangular-reactive-formsangular2-changedetection

Why Cannot find control with name: 'username' only randomly?


I have an Angular reactive form with works most of the time. But sometimes, randomly, I get an error

ERROR Error: Cannot find control with name: 'username'
    at _throwError (forms.js:2337)
    at setUpControl (forms.js:2245)
    at FormGroupDirective.push../node_modules/@angular/forms/fesm5/forms.js.FormGroupDirective.addControl (forms.js:5471)
    at FormControlName.push../node_modules/@angular/forms/fesm5/forms.js.FormControlName._setUpControl (forms.js:6072)
    at FormControlName.push../node_modules/@angular/forms/fesm5/forms.js.FormControlName.ngOnChanges (forms.js:5993)
    at checkAndUpdateDirectiveInline (core.js:21093)
    at checkAndUpdateNodeInline (core.js:29495)
    at checkAndUpdateNode (core.js:29457)
    at debugCheckAndUpdateNode (core.js:30091)
    at debugCheckDirectivesFn (core.js:30051)

The component is a simple login page, HTML is

<div id="loginSection" class="middle-dashboard-box">
  <div class="login-title content-inner-title" i18n="logintitle|@@loginheader">Login</div>
  <form class="login-form" [formGroup]="loginFormGroup" (ngSubmit)="login($event)">
    <mymat-form-error></mymat-form-error>
    <div>
      <mat-form-field class="login-input input-control-width">
        <input matInput formControlName="username" i18n-placeholder="Username|@@username" placeholder="Username">
      </mat-form-field>
    </div>
    <div>
      <mat-form-field class="login-input input-control-width"> 
        <input matInput formControlName="password" type="password" i18n-placeholder="Password|@@password" placeholder="Password">
      </mat-form-field>
    </div>
    <div class="forgot-pass">
      <a href="#public/forgotpassword" i18n="forgot password|@@forgotPasswordLink">Forgot password?</a>
    </div>
    <button mat-raised-button color="primary" type="submit" class="main-btn login-btn"
      i18n="Login|Login button action@@login">Login</button>
  </form>
</div>

and TS is

@Component( {
    templateUrl: './login.component.html',
    styleUrls: ['./login.component.scss']
} )
export class LoginComponent implements OnInit {

    loginFormGroup: FormGroup;

    constructor( private router: Router,
        private formBuilder: FormBuilder,
        private loginService: LoginService ) { }

    ngOnInit() {
        this.loginFormGroup = this.formBuilder.group( {
            username: ['', Validators.required],
            password: ['', Validators.required]
        } );
        this.loginFormGroup.controls['username'].setValue( this.loginService.latestUsername );
    }

    login( event ) {
        // do login stuff, not relevant here
    }
}

this.loginService.latestUsername points to a getter in the LoginService:

get latestUsername(): string {
    return localStorage.getItem( LoginService.LATESTUSERNAME_KEY ) || ''
}

The FormControlName.ngOnChanges in the stacktrace makes me think ng is running a change detection before the control is properly initialized, but as there is no subscription or similar around, I don't see what could be triggering it. I'm observing the error during Selenium tests (there reliably on some tests, intermittently on others), but have had it once or twice while clicking around manually. The effect only occurs since upgrading to Angular 8.2.14 (from 7)


Solution

  • Looking at the source code of FormControlName and thinking about when ngOnChanges() can run, I reckon ngOnChanges() is running before ngOnInit() has finished building the form.

    To diagnose this, I would litter both ngOnChanges() and ngOnInit() with console.log() to trace what's going on.

    To get around the problem (if this is the cause), it should be as simple as guarding the form with *ngIf to make sure the directives aren't prematurely triggering change detection. Ideally you could also use OnPush change detection strategy.