AngularJS browser autofill workaround by using a directive

AngularJS browser autofill workaround by using a directive

When submitting a form in AngularJS and use the browser remember password functionality, and in a subsequent login attempt you let the browser fill in the login form with the username and password, the $scope model won’t be changed based on the autofill.
The only dirty hack I found is to use the following directive:
app.directive(“xsInputSync”, [“$timeout” , function($timeout) {
return {
restrict : “A”,
require: “?ngModel”,
link : function(scope, element, attrs, ngModel) {
$timeout(function() {
if (ngModel.$viewValue && ngModel.$viewValue !== element.val()) {
scope.apply(function() {
ngModel.$setViewValue(element.val());
});
}
console.log(scope);
console.log(ngModel.$name);
console.log(scope[ngModel.$name]);
}, 3000);
}
};
}]);

The problem is that the ngModel.$setViewValue(element.val()); doesn’t change the model nor the view based on the element.val() returned value. How can I accomplish that?

Solutions/Answers:

Solution 1:

Apparently this is a known issue with Angular and is currently open

I’m not sure what you could do here besides some sort of work around like you’re trying. It seems you’re on the right track. I couldn’t get my browser to try to remember a password for your plunk, so I’m not sure if this will work but have a look:

app.directive('autoFillSync', function($timeout) {
   return {
      require: 'ngModel',
      link: function(scope, elem, attrs, ngModel) {
          var origVal = elem.val();
          $timeout(function () {
              var newVal = elem.val();
              if(ngModel.$pristine && origVal !== newVal) {
                  ngModel.$setViewValue(newVal);
              }
          }, 500);
      }
   }
});
<form name="myForm" ng-submit="login()">
   <label for="username">Username</label>
   <input type="text" id="username" name="username" ng-model="username" auto-fill-sync/><br/>
   <label for="password">Password</label>
   <input type="password" id="password" name="password" ng-model="password" auto-fill-sync/><br/>
   <button type="submit">Login</button>
</form>

I think you just need to simplify your approach a bit. The one thing I definitely recommend is to check ngModel.$pristine and make sure you’re not overwriting some poor user’s input. Also, 3 seconds is probably too long. You shouldn’t have to call $apply() in a $timeout, BTW, it should queue a $digest for you automatically.

The real catch: Will your browser beat Angular to execution? What about my browser?

This is probably an unwinnable war, which is why Angular (or Knockout) hasn’t been able to solve it readily. There’s no guarantee of the state of the data in your input at the time of the directive’s initial execution. Not even at the time of Angular’s initialization…. So it’s a tricky problem to solve.

Solution 2:

Here is a solution that is far less hacky than other solutions presented and is semantically sound AngularJS: http://victorblog.com/2014/01/12/fixing-autocomplete-autofill-on-angularjs-form-submit/

myApp.directive('formAutofillFix', function() {
  return function(scope, elem, attrs) {
    // Fixes Chrome bug: https://groups.google.com/forum/#!topic/angular/6NlucSskQjY
    elem.prop('method', 'POST');

    // Fix autofill issues where Angular doesn't know about autofilled inputs
    if(attrs.ngSubmit) {
      setTimeout(function() {
        elem.unbind('submit').submit(function(e) {
          e.preventDefault();
          elem.find('input, textarea, select').trigger('input').trigger('change').trigger('keydown');
          scope.$apply(attrs.ngSubmit);
        });
      }, 0);
    }
  };
});

Then you simply attach the directive to your form:

<form ng-submit="submitLoginForm()" form-autofill-fix>
  <div>
    <input type="email" ng-model="email" ng-required />
    <input type="password" ng-model="password" ng-required />
    <button type="submit">Log In</button>
  </div>
</form>

Solution 3:

You don’t have to use a $timeout or anything like this. You can use an event system.

I think it’s more Angularish and does not depend on jQuery or custom event catching.

For example on your submit handler:

$scope.doLogin = function() {
    $scope.$broadcast("autofill:update");

    // Continue with the login.....
};

And then you can have an autofill directive like this:

.directive("autofill", function () {
    return {
        require: "ngModel",
        link: function (scope, element, attrs, ngModel) {
            scope.$on("autofill:update", function() {
                ngModel.$setViewValue(element.val());
            });
        }
    }
});

Finally, your HTML will be like:

<input type="text" name="username" ng-model="user.id" autofill="autofill"/>

Solution 4:

Dirty code, check if issue https://github.com/angular/angular.js/issues/1460#issuecomment-18572604 is fixed before using this code.
This directive triggers events when field is filled, not only before submit (it’s necessary if you have to handle input before submit)

 .directive('autoFillableField', function() {
    return {
                   restrict: "A",
                   require: "?ngModel",
                   link: function(scope, element, attrs, ngModel) {
                       setInterval(function() {
                           var prev_val = '';
                           if (!angular.isUndefined(attrs.xAutoFillPrevVal)) {
                               prev_val = attrs.xAutoFillPrevVal;
                           }
                           if (element.val()!=prev_val) {
                               if (!angular.isUndefined(ngModel)) {
                                   if (!(element.val()=='' && ngModel.$pristine)) {
                                       attrs.xAutoFillPrevVal = element.val();
                                       scope.$apply(function() {
                                           ngModel.$setViewValue(element.val());
                                       });
                                   }
                               }
                               else {
                                   element.trigger('input');
                                   element.trigger('change');
                                   element.trigger('keyup');
                                   attrs.xAutoFillPrevVal = element.val();
                               }
                           }
                       }, 300);
                   }
               };
});

Solution 5:

No need to hack anymore! Angular dev tbosch made a polyfill that triggers a change event when the browser changes form fields without triggering a change event:

https://github.com/tbosch/autofill-event

For now they won’t build this into the Angular code, as this is a bugfix for the browser, and also works without Angular (e.g. for plain jQuery apps).

“The polyfill will check for changes on document load and also when an input is left (only in the same form). However, you can trigger the check manually if you want to.

The project has unit tests as well as semi automatic tests, so we finally have a place to collect all the different use case together with the required browser settings.

Please note: This polyfill works with plain AngularJS apps, with AngularJS/jQuery apps but also with plain jQuery apps that do not use Angular.”

It can be installed with:

bower install autofill-event --save

Add the script autofill-event.js after jQuery or Angular in your page.

This will do the following:

  • after DOMContentLoaded: check all input fields
  • a field is left: check all other fields in the same form

API (to manually trigger the check):

  • $el.checkAndTriggerAutoFillEvent(): Execute the check for all DOM elements in the given jQuery / jQLite element.

How it works

  1. Remember all changes to input elements by the user (listening for change events) and also by JavaScript (by intercepting $el.val() for jQuery / jQLite elements). That changed value is stored on the element in a private property.

  2. Checking an element for auto fill: Compare the current value of the element with the remembered value. If it’s different, trigger a change event.

Dependencies

AngularJS or jQuery (works with either one or both)

More info and source on the github page.

Original Angular Issue #1460 on Github can be read here.

Solution 6:

Seems like clear straight ahead solution. No jQuery needed.

UPDATE:

  • Model is updated only when model value isn’t equal to actual input
    value.
  • Checking doesn’t stop on first autofill. In case if you wish to use
    another account for example.

app.directive('autofillable', ['$timeout', function ($timeout) {
    return {
        scope: true,
        require: 'ngModel',
        link: function (scope, elem, attrs, ctrl) {
            scope.check = function(){
                var val = elem[0].value;
                if(ctrl.$viewValue !== val){
                    ctrl.$setViewValue(val)
                }
                $timeout(scope.check, 300);
            };
            scope.check();
        }
    }
}]);