'Angular 2 and browser autofill

I'm implementing login page with Angular reactive forms. Button "login" is disabled if form is invalid.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
    selector: 'signin',
    templateUrl: './signin.component.html'
})
export class SignInComponent implements OnInit {
    private signInForm: FormGroup;

    constructor(private formBuilder: FormBuilder) { }

    ngOnInit() {
        this.buildForm();
    }

    private buildForm(): void {
        this.signInForm = this.formBuilder.group({
            userName: ['', [Validators.required, Validators.maxLength(50)]],
            password: ['', [Validators.required, Validators.maxLength(50)]]
        });

        this.signInForm.valueChanges
            .subscribe((data: any) => this.onValueChanged(data));

        this.onValueChanged();
    }

    private onValueChanged(data?: any) {
        console.log(data);
    }

So, when I launch it in browser, I have prepopulated fields "userName" and "passwords". And in console I have values '{ userName: "[email protected]", password: "" }' and as a result button "login" is disabled. But if I click somewhere on page it trigger onValueChanged and I see '{ userName: "[email protected]", password: "123456" }', and button "login" is enabled.

If I go in incognito mode. I have no prepopulated fields (they are empty), but when I populate (select) values, then in console I see '{ userName: "[email protected]", password: "123456" }', and button "login" is enabled without any extra click.

May be they are different events? Autofill and autocomplete? And angular works with them differently?

What is the best way to resolve it? And why onValueChanged function is executed only once when browser autofill fields?



Solution 1:[1]

The problem in Chrome browser: it does not allow access to the value of the password field after autofill (before user click on the page). So, there are two ways:

  1. remove validation of the password field;
  2. disable password autocompletion.

Solution 2:[2]

I found out that you can use autocomplete="new-password"

As is decribed here

This way your password fields will not be autofilled at all, and that was my case. There are many others autocomplete attributes available in the link above.

Solution 3:[3]

this is worked for me

1. before(it auto filling)

<div class="form-group form-row required">
     <input class="input p-l-20 form-control" formControlName='otp' type="text" name="otp" placeholder="Enter OTP">
</div>

2. after(now not auto filling, working fine)

<div class="form-group form-row required">
       <input class="input p-l-20 form-control" formControlName='otp' type="text" name="otp" placeholder="Enter OTP">
       <!-- this dummy input is solved my problem -->
       <input class="hide"  type="text">
</div>

note hide is bootstrap v4 class

Solution 4:[4]

I faced the same issue with Chrome v58.0.3029.96. Here is my workaround to keep validation and autocompletion:

export class SigninComponent extends UIComponentBase
{
    //--------------------------------------------------------------------------------------------
    // CONSTRUCTOR
    //--------------------------------------------------------------------------------------------
    constructor(viewModel: SigninViewModel)
    {
        super(viewModel);

        // Autofill password Chrome bug workaround
        if (navigator.userAgent.toLowerCase().indexOf('chrome') > -1)
        {
            this._autofillChrome = true;
            this.vm.FormGroup.valueChanges.subscribe(
                (data) =>
                {
                    if (this._autofillChrome && data.uname)
                    {
                        this._password = " ";
                        this._autofillChrome = false;
                    }
                });
        }
    }

    //--------------------------------------------------------------------------------------------
    // PROPERTIES
    //--------------------------------------------------------------------------------------------
    private _password: string;
    private _autofillChrome: boolean;

    //--------------------------------------------------------------------------------------------
    // COMMAND HANDLERS
    //--------------------------------------------------------------------------------------------
    private onFocusInput()
    {
        this._autofillChrome = false;
    }
}

And in my html:

<input mdInput 
       [placeholder]="'USERNAME_LABEL' | translate" 
       [formControl]="vm.FormGroup.controls['uname']" 
       (focus)="onFocusInput()" />
[...]
<input mdInput 
       type="password" 
       [placeholder]="'PASSWORD_LABEL' | translate" 
       [formControl]="vm.FormGroup.controls['password']" 
       (focus)="onFocusInput()"
       [(ngModel)]="_password" />

I hope it helps.

Note: vm is defined in UIComponentBase and refers to the view-model SigninViewModel of my component. The FormGroup instance is defined in the view-model as the business logic is strictly separated from the view in my application.

UPDATE

Since v59, it seems that the input's focus event is fired when auto-completing fields. So the above workaround doesn't work anymore. Here is the updated workaround, using a timeout to determine if fields are updated by the auto-completion or by the user (I haven't found a better way):

export class SigninComponent extends UIComponentBase
{
    //--------------------------------------------------------------------------------------------
    // CONSTRUCTOR
    //--------------------------------------------------------------------------------------------
    constructor(viewModel: SigninViewModel)
    {
        super(viewModel);

        // Autofill password Chrome bug workaround
        if (navigator.userAgent.toLowerCase().indexOf('chrome') > -1)
        {
            this._autofillChrome = true;
            setTimeout(
                () =>
                {
                    this._autofillChrome = false;
                },
                250 // 1/4 sec
            );
            this.vm.FormGroup.valueChanges.subscribe(
                (data) =>
                {
                    if (this._autofillChrome && data.uname)
                    {
                        this._password = " ";
                        this._autofillChrome = false;
                    }
                });
        }
    }

    //--------------------------------------------------------------------------------------------
    // PROPERTIES
    //--------------------------------------------------------------------------------------------
    private _password: string;
    private _autofillChrome: boolean;
}

And in my html:

<input mdInput 
       [placeholder]="'USERNAME_LABEL' | translate" 
       [formControl]="vm.FormGroup.controls['uname']" />
[...]
<input mdInput 
       type="password" 
       [placeholder]="'PASSWORD_LABEL' | translate" 
       [formControl]="vm.FormGroup.controls['password']" 
       [(ngModel)]="_password" />

Solution 5:[5]

I've found some workaround

  1. Create some var isDisabled: boolean = false UPDATE: 1.1 One more thing - var initCount: number = 0;

    ngOnInit(): void { this.isDisabled = false; this.initCount++; }

  2. Create methods

And this is the code:

// This method will be called async
onStart(): Observable<boolean> {
    if (this.loginFomrControl.hasError('required') && 
    this.passwordFormControl.hasError('required') && this.initCount == 1) {
      this.isDisabled = false;

      // increase init count so this method wound be called async
      this.initCount++;
    }

    return new BehaviorSubject<boolean>(true).asObservable();
  }

// This method will ve called on change
validateForm() {
    if (this.loginFormControl.value != '' && this.passwordFormControl.value != '') {
      this.isDisabled = false;
    } else if (this.loginFomrControl.value == '' || this.passwordFormControl.value == '') {
      this.isDisabled = true;
    }
  }
  1. Update you html with something like this:

Here is a sample of the HTML

<div class="container" *ngIf="onStart() | async">
<!-- Do Stuff -->
  <mat-form-field class="login-full-width">
    <input matInput placeholder="{{ 'login_email' | translate }}" id="login_email" [formControl]="loginFomrControl" (change)="validateForm()">
    <mat-error *ngIf="loginFomrControl.hasError('email') && !loginFomrControl.hasError('required')">
            {{'login_email_error' | translate}}
    </mat-error>
    <mat-error *ngIf="loginFomrControl.hasError('required')">
            {{ 'login_email' | translate }}
        <strong>{{ 'login_required' | translate }}</strong>
    </mat-error>
  </mat-form-field>
<!-- Do Stuff -->
</div>

Solution 6:[6]

For the sake of completeness: I have a non-reactive login form.

<form name="loginForm" role="form" (ngSubmit)="onLoginSubmit()">
  <input name="username" #usernameInp>
  <input type="password" name="password" #passwordInp>
  <button type="submit">Login</button>
</form>

If I use [(ngModel)]="username", the variable username of the associated component does not get populated if the browser autofill functionality kicks in.

Therefore, I instead work with ViewChild:

@ViewChild('usernameInp') usernameInp: ElementRef;
@ViewChild('passwordInp') passwordInp: ElementRef;
...
onLoginSubmit() {
  const username = this.usernameInp.nativeElement.value;
  const password = this.passwordInp.nativeElement.value;
  ...

This way, I can access the values provided by the browser once the user clicks on Login.

Solution 7:[7]

my solution 2019 version: Gist Hub

// <ion-input (change)="fixAutoFill($event, 'password')" #passwordInput autocomplete="off" formControlName="password" type="password"></ion-input>
// <ion-input (change)="fixAutoFill($event, 'username')" #usernameInput autocomplete="off" type="text" formControlName="username"></ion-input>


  fixAutoFill(event, type) {
    const value = event.target.value;
    if (type === 'username') {
      this.loginForm.patchValue({
        username: value
      }, {emitEvent: true, onlySelf: false});
    } else {
      this.loginForm.patchValue({
        password: value
      }, {emitEvent: true, onlySelf: false});
    }
    // console.log('after click', this.loginForm.value);
  }

  @ViewChild('usernameInput') usernameInput: IonInput;
  @ViewChild('passwordInput') passwordInput: IonInput;

Solution 8:[8]

http://webagility.com/posts/the-ultimate-list-of-hacks-for-chromes-forced-yellow-background-on-autocompleted-inputs

Try this out. Racked my brain for a bit but this did the trick.

input:-webkit-autofill {
  -webkit-transition-delay: 99999s;
}

Solution 9:[9]

I solved this issue by adding the attribute autocomplete="new-feildName" to input tag.

Example:

<input type="text" class="mr-input" formControlName="email" 
placeholder="Adresse e-mail" autocomplete="new-email"/>

Note: you should add autocomplete to every field is filled automatically.

Solution 10:[10]

I made a funtion

formIsValid(): boolean{ return this.form.valid; }

and than add to the button:

<button [disabled]="!formIsValid()" type="submit">LogIn</button>

It's worked for me

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Yaroslav Sivakov
Solution 2 rochasdv
Solution 3
Solution 4
Solution 5
Solution 6 bgerth
Solution 7
Solution 8 Vega
Solution 9 Ousama
Solution 10 PĂ ldi Gerg?