Обходной путь автозаполнения браузера AngularJS с помощью директивы


111

При отправке формы в AngularJS и использовании функции запоминания пароля в браузере и при последующей попытке входа вы позволяете браузеру заполнить форму входа с именем пользователя и паролем, $scopeмодель не будет изменена на основе автозаполнения.

Единственный грязный прием, который я нашел, - это использовать следующую директиву:

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);
        }
    };
}]);

Проблема в том, что ngModel.$setViewValue(element.val());не меняет ни модель, ни представление на основе element.val()возвращаемого значения. Как я могу этого добиться?


На первый взгляд этот код выглядит нормально ... но где остальное (разметка и т. Д.)? У вас есть скрипка или кусок, который мы можем увидеть?
Бен Леш

Вот пример : plnkr.co/edit/CHrBAVU9Ycl2ex2DRr6R Я не уверен, работает ли он непосредственно на плункере, потому что он работает в iframe.
lucassp

1
Вам не нужна область видимости. $ Apply внутри тайм-аута angular $. Вам может понадобиться это внутри собственного window.setTimeout. Но лучше использовать angular.
gorpacrate

1
Для этой проблемы существует "официальное" исправление полифиллов от Angular dev tbosch . Подробности см. В ответе stackoverflow.com/a/25687396/3009639 ниже.
orszaczky 05

К сожалению, "официальное" исправление - это заброшенное ПО и не работает во многих браузерах. Проблемы были зарегистрированы, но не решены, поэтому поиск продолжается ...
cyberwombat

Ответы:


45

По-видимому, это известная проблема с Angular и в настоящее время открыта.

Я не уверен, что вы могли бы здесь сделать, кроме какой-то работы, как вы пытаетесь. Похоже, ты на правильном пути. Мне не удалось заставить свой браузер попытаться запомнить пароль для вашего блока, поэтому я не уверен, что это сработает, но посмотрите:

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>

Думаю, вам просто нужно немного упростить свой подход. Единственное, что я определенно рекомендую, - это проверить ngModel.$pristineи убедиться, что вы не перезаписываете ввод некорректного пользователя. Кроме того, 3 секунды, вероятно, слишком долго. Вам не нужно вызывать $ apply () в $ timeout, BTW, он должен автоматически поставить в очередь $ дайджест.

Настоящая загвоздка: сможет ли ваш браузер превзойти Angular по производительности? А как насчет моего браузера?

Скорее всего, это война, в которой невозможно победить, поэтому Angular (или Knockout) не смог ее быстро решить. Нет никакой гарантии состояния данных во вводе во время первоначального выполнения директивы. Даже во время инициализации Angular ... Так что это сложная проблема.


1
Хорошее замечание о $ pristine. Ну, 3 секунды просто для тестирования. Диалоговое окно «Сохранить пароль», похоже, работает в Safari. Это еще одна проблема, которую я должен изучить.
lucassp

Просто обманите его локальным хранилищем. :) ... Я буду думать об этом. Но я сомневаюсь в возможности заставить автозаполнение работать с любыми двусторонними привязками.
Бен Леш

Да, но мне все еще нужно иметь возможность обновлять модель из директивы, чтобы она работала.
lucassp

1
Я шутил насчет localStorage, вы бы не хотели хранить там пароли.
Бен Леш

1
Это не будет работать с Safari и автозаполнением кредитной карты, так как это автозаполнение запускается в любое время пользователем
Даллас Кларк,

35

Вот решение, которое гораздо менее хакерское, чем другие представленные решения, и семантически правильное 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);
    }
  };
});

Затем вы просто прикрепляете директиву к своей форме:

<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>

2
Этот сработал. Мы попробовали большинство из вышеперечисленных без каких-либо результатов. Спасибо!!! Результат на mobbr.com
Патрик Саваль

1
Поскольку здесь используется jQuery, не ожидается, что он будет работать без jQuery.
Quinn Strahl

1
Вот и 2018 год, и это все еще исправление. Спасибо, Иезекииль!
Скотт Мэнни

35

Вам не нужно использовать a $timeoutили что-то подобное. Вы можете использовать систему событий.

Я думаю, что это более Angularish и не зависит от jQuery или перехвата пользовательских событий.

Например, в вашем обработчике отправки:

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

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

И тогда у вас может быть такая autofillдиректива:

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

Наконец, ваш HTML будет таким:

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

14
В моем случае кнопка отправки отключена, пока поля ввода пусты.
gorpacrate

4
у вас никогда не было проблем с этим, поскольку это асинхронно? где вызов входа на сервер уже уходит до того, как событие было реплицировано, и у директивы было время обновить область?
Sander

12

Больше не нужно взламывать! Angular dev tbosch создал полифилл, который запускает событие изменения, когда браузер меняет поля формы без запуска события изменения:

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

На данный момент они не будут встраивать это в код Angular, поскольку это исправление ошибки для браузера, а также работает без Angular (например, для простых приложений jQuery).

«Полифилл будет проверять наличие изменений при загрузке документа, а также при оставлении ввода (только в той же форме). Однако вы можете запустить проверку вручную, если хотите.

В проекте есть как модульные, так и полуавтоматические тесты, поэтому у нас наконец-то есть место для сбора всех различных вариантов использования вместе с необходимыми настройками браузера.

Обратите внимание: этот полифилл работает с простыми приложениями AngularJS, с приложениями AngularJS / jQuery, но также и с простыми приложениями jQuery, которые не используют Angular ».

Его можно установить с помощью:

bower install autofill-event --save

Добавьте скрипт autofill-event.js после jQuery или Angular на свою страницу.

Это сделает следующее:

  • после DOMContentLoaded: проверьте все поля ввода
  • осталось поле: проверьте все остальные поля в той же форме

API (для запуска проверки вручную):

  • $el.checkAndTriggerAutoFillEvent(): Выполнить проверку всех элементов DOM в данном элементе jQuery / jQLite.

Как это устроено

  1. Запомните все изменения элементов ввода пользователем (прослушивание событий изменения), а также JavaScript (путем перехвата $ el.val () для элементов jQuery / jQLite). Это измененное значение сохраняется в элементе в частной собственности.

  2. Проверка элемента на автозаполнение: сравните текущее значение элемента с запомненным значением. Если он другой, инициируйте событие изменения.

зависимости

AngularJS или jQuery (работает с одним или обоими)

Дополнительная информация и источник на странице github .

Исходный выпуск Angular №1460 на Github можно прочитать здесь.


2
В моих случаях это не работает. checkAndTriggerAutoFillEvent () запускается и генерирует события, но AngularJS никогда не обновляет свои модели и считает поля пустыми.
Splaktar

У меня не было проблем с использованием этого полиилла. Если вы не можете заставить его работать должным образом, вам следует создать отдельный вопрос, включая некоторый код. Если вы обнаружили ошибку, вам следует проверить тикеты на github или открыть новую.
orszaczky

Да, я думаю, что это связано с этой ошибкой: github.com/tbosch/autofill-event/issues/11
Splaktar

1
ngModelOptionsв сочетании со autofill-eventсредствами, которые теперь можно указать в changeкачестве одного из updateOnсобытий, чтобы убедиться, что ваша модель правильно синхронизирована.
morloch

10

Грязный код, проверьте, исправлена ли проблема https://github.com/angular/angular.js/issues/1460#issuecomment-18572604, прежде чем использовать этот код. Эта директива запускает события при заполнении поля, а не только перед отправкой (это необходимо, если вам нужно обработать ввод перед отправкой)

 .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);
                   }
               };
});

5

Похоже на четкое прямое решение. Не требуется jQuery.

ОБНОВИТЬ:

  • Модель обновляется только тогда, когда значение модели не равно фактическому входному значению.
  • Проверка не прекращается при первом автозаполнении. Например, если вы хотите использовать другую учетную запись.

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();
        }
    }
}]);

4
+1 хорошее решение, подобное тому, что я придумал. Единственное дополнение, которое я бы предложил, - это сначала проверить, находится ли модель $pristineперед ее изменением, а затем после ее изменения сохранить ее $pristineсостояние. В противном случае вы обнаружите, что если форма загружается без данных автозаполнения, указанная выше директива все равно обновит модель и впоследствии сделает это, dirtyдаже если элемент управления формы все еще должен считаться нетронутым. пример здесь, используя вашу директиву. Я закомментировал код, который хотел бы добавить к нему: jsfiddle.net/OACDesigns/L8Sja/4
OACDesigns

1
также, я думаю, scope:trueдолжно бытьscope:{}
OACDesigns

4

Решение 1 [Использование $ timeout]:

Директива:

app.directive('autoFillSync', function($timeout) {
    return {
      require: 'ngModel',
      link: function(scope, elem, attrs, model) {
          var origVal = elem.val();
          $timeout(function () {
              var newVal = elem.val();
              if(model.$pristine && origVal !== newVal) {
                  model.$setViewValue(newVal);
              }
          }, 500);
      }
    };
});

HTML:

<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>

Решение 2 [Использование угловых событий]:

Ссылка: ответ Беко

Директива:

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

HTML:

<form name="myForm" ng-submit="login()">
  <label for="username">Username</label>
  <input type="text" id="username" name="username" ng-model="username" autofill/><br/>
  <label for="password">Password</label>
  <input type="password" id="password" name="password" ng-model="password" autofill/><br/>
  <button type="submit">Login</button>
</form>

Решение 3 [Использование вызовов метода ретрансляции]:

Директива:

app.directive('autoFill', function() {
    return {
        restrict: 'A',
        link: function(scope,element) {
            scope.submit = function(){
                scope.username = element.find("#username").val();
                scope.password = element.find("#password").val();
                scope.login();//call a login method in your controller or write the code here itself
            }

        }
    };
});

HTML:

<form name="myForm" auto-fill ng-submit="submit()">
   <label for="username">Username</label>
   <input type="text" id="username" name="username" ng-model="username" />
   <label for="password">Password</label>
   <input type="password" id="password" name="password" ng-model="password" />
   <button type="submit">Login</button>
</form>

2
Решение 1 какое-то время работало очень хорошо, но теперь на angularjs 1.4.9 оно больше не работает. model.$validДиректива check in возвращает истину как до, так и после $setViewValue, но элемент все еще с ng-invalidклассом
Джон

4

Что ж, самый простой способ имитировать поведение браузера, поэтому, если есть проблема с событием изменения, просто запустите его самостоятельно. Намного проще.

Директива:

yourModule.directive('triggerChange', function($sniffer) {
    return {
        link : function(scope, elem, attrs) {
            elem.on('click', function(){
                $(attrs.triggerChange).trigger(
                    $sniffer.hasEvent('input') ? 'input' : 'change'
                );
            });
        },
        priority : 1
    }
});

HTML:

<form >
    <input data-ng-model="user.nome" type="text" id="username">

    <input data-ng-model="user.senha" type="password" id="password" >

    <input type="submit" data-ng-click="login.connect()" id="btnlogin" 
           data-trigger-change="#password,#username"/>
</form>

Вы можете сделать несколько вариантов, например, поместить директиву в форму и запустить событие на всех входах с помощью класса .dirty при отправке формы.


3

Это способ jQuery:

$(window).load(function() {
   // updates autofilled fields
   window.setTimeout(function() {
     $('input[ng-model]').trigger('input');
   }, 100);
 });

Это способ Angular:

 app.directive('autofill', ['$timeout', function ($timeout) {
    return {
        scope: true,
        require: 'ngModel',
        link: function (scope, elem, attrs, ctrl) {
            $timeout(function(){
                $(elem[0]).trigger('input');
                // elem.trigger('input'); try this if above don't work
            }, 200)
        }
    }
}]);

HTML

<input type="number" autofill /> 

2
Совершенно неугловой способ.
gorpacrate

1
@gorpacrate: А теперь проверьте это с помощью angular.
Нищит Дханани

2
На вкладках Angular Way слишком много пробелов. Просто прикалываюсь. Я люблю горизонтальную прокрутку.
Даниэль Бировски Попески 01

это не работает в angular, поскольку для элемента нет триггера функции. Я думаю, что для этого потребуется включить jquery
Томас

1
triggerHandler('input')должно быть хорошо, если у вас нет полнофункционального jquery
Mutmatt

1

Вот еще один обходной путь, который менее хакерский, но требует дополнительного кода в контроллере.

HTML:

<form ng-submit="submitForm()" ng-controller="FormController">
    <input type="text" ng-model="username" autocomplete-username>
    <input type="submit">
</form>

Директива (CoffeeScript):

directives.directive 'autocompleteUsername', ->
    return (scope, element) ->
        scope.getUsername = ->
            element.val()

контроллер:

controllers.controller 'FormController', [->
    $scope.submitForm = ->
        username = $scope.getUsername?() ? $scope.username
        # HTTP stuff...
]

У вас есть это в ванильном JavaScript?
f1lt3r

CoffeeScript.org предоставляет переводчик: готово здесь
jab

1

Это единственное решение, которое я нашел, которое позволило всем моим проверкам Angular работать так, как задумано, включая отключение / включение кнопки отправки. Устанавливается с беседкой и 1 тегом сценария. Bazinga!

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


1
эта поли-заливка не подходит для кнопки отправки / привязки ввода к состоянию формы. если вы не нажмете на странице любую точку, в которой хотите запустить дайджест (кажется, что действие заставляет браузер выполнять какое-то реальное заполнение), тогда кнопка становится активной. Тестовая страница с ручными тестами
e-cloud

1

У меня сработало изменение значения модели вместо использования функции тайм-аута.

Вот мой код:

module.directive('autoFill', [ function() {
    return {
        require: 'ngModel',
        link:function(scope, element, attr, ngModel) {
            var origVal = element.val();
            if(origVal){
                ngModel.$modelValue = ngModel.$modelValue || origVal;
            }
        }
    };
}]);

1

Однострочный обходной путь в обработчике отправки (требуется jQuery):

if (!$scope.model) $scope.model = $('#input_field').val();

На самом деле это не однострочник, если он находится в директиве, что, безусловно, должно быть.
isherwood

0

Я заставляю $ setValue (val ()) отправить: (это работает без jQuery)

   var ValidSubmit = ['$parse', function ($parse) {
    return {
        compile: function compile(tElement, tAttrs, transclude) {
            return {
                post: function postLink(scope, element, iAttrs, controller) {
                    var form = element.controller('form');
                    form.$submitted = false;
                    var fn = $parse(iAttrs.validSubmit);
                    element.on('submit', function(event) {
                        scope.$apply(function() {
                            var inputs = element.find('input');
                            for(var i=0; i < inputs.length; i++) {
                                var ele = inputs.eq(i);
                                var field = form[inputs[i].name];
                                field.$setViewValue(ele.val());
                            }
                            element.addClass('ng-submitted');
                            form.$submitted = true;
                            if(form.$valid) {
                                fn(scope, {$event:event});
                            }
                        });
                    });
                    scope.$watch(function() { return form.$valid}, function(isValid) {
                        if(form.$submitted == false) return;
                        if(isValid) {
                            element.removeClass('has-error').addClass('has-success');
                        } else {
                            element.removeClass('has-success');
                            element.addClass('has-error');
                        }
                    });
                }
            }
        }
    }
}]
app.directive('validSubmit', ValidSubmit);

0

Я новичок в Angularjs, но я нашел простое решение этой проблемы => Заставьте Angular переоценить выражение ... изменив его! (конечно, вам нужно запомнить начальное значение, чтобы вернуться в исходное состояние) Вот как это происходит в вашей функции контроллера для отправки формы:

    $scope.submit = function () {
                var oldpassword = $scope.password;
                $scope.password = '';
                $scope.password = oldpassword;
//rest of your code of the submit function goes here...

где, конечно, значение, введенное при вводе пароля, было установлено Windows, а не пользователем.


0

Вы можете попробовать этот код:

yourapp.directive('autofill',function () {

    return {
        scope: true,
        require: 'ngModel',
        link: function (scope, elem, attrs, ctrl) {
            var origVal = elem.val();
            if (origVal != '') {
                elem.trigger('input');
            }
        }
    }
});

0

Незначительная модификация этого ответа ( https://stackoverflow.com/a/14966711/3443828 ): используйте интервал $ вместо тайм-аута $, чтобы вам не приходилось гонять браузер.

mod.directive('autoFillSync', function($interval) {
    function link(scope, element, attrs, ngModel) {
        var origVal = element.val();
        var refresh = $interval(function() {
          if (!ngModel.$pristine) {
            $interval.cancel(refresh);
          }else{
            var newVal = element.val();
            if (origVal !== newVal) {
              ngModel.$setViewValue(newVal);
              $interval.cancel(refresh);
            }
          }
        }, 100);
    }

    return {
      require: 'ngModel',
      link: link
    }
  });

0

Это решение, которое я использовал в своих формах.

.directive('autofillSync', [ function(){
  var link = function(scope, element, attrs, ngFormCtrl){
    element.on('submit', function(event){
      if(ngFormCtrl.$dirty){
        console.log('returning as form is dirty');
        return;
      }   
      element.find('input').each(function(index, input){
        angular.element(input).trigger('input');
      }); 
    }); 
  };  
  return {
    /* negative priority to make this post link function run first */
    priority:-1,
    link: link,
    require: 'form'
  };  
}]);

И шаблон формы будет

<form autofill-sync name="user.loginForm" class="login-form" novalidate ng-submit="signIn()">
    <!-- Input fields here -->
</form>

Таким образом, я смог запустить любые парсеры / средства форматирования, которые у меня есть на моей ng-модели, и сделать функцию отправки прозрачной.


0

Решение без директив:

.run(["$window", "$rootElement", "$timeout", function($window, $rootElement, $timeout){

        var event =$window.document.createEvent("HTMLEvents");
        event.initEvent("change", true, true);

        $timeout(function(){

            Array.apply(null, $rootElement.find("input")).forEach(function(item){
                if (item.value.length) {
                    item.$$currentValue = item.value;
                    item.dispatchEvent(event);
                }
            });

        }, 500);
    }])

0

Это простое исправление, которое работает во всех случаях, которые я тестировал в Firefox и Chrome. Обратите внимание, что с верхним ответом (директива без тайм-аута) у меня были проблемы с -

  • Кнопки браузера назад / вперед, не запускайте повторно события загрузки страницы (поэтому исправление не применяется)
  • Загрузка учетных данных через некоторое время после загрузки страницы. например, в Firefox дважды щелкните поле входа и выберите из сохраненных учетных данных.
  • Требуется решение, которое обновляется перед отправкой формы, поскольку я отключаю кнопку входа в систему до тех пор, пока не будет предоставлен действительный ввод

Это исправление, очевидно, очень глупое и хакерское, но оно работает в 100% случаев -

function myScope($scope, $timeout) {
    // ...
    (function autoFillFix() {
        $timeout(function() { 
            $('#username').trigger('change'); 
            $('#password').trigger('change'); 
            autoFillFix(); }, 500);                    
    })();
}

1
можно заменить $timeoutна, $intervalчтобы дать будущим разработчикам немного больше контекста и избавиться от происходящих там странно выглядящих рекурсивных вызовов
Mutmatt

0

Ни одно из этих решений не помогло моему варианту использования. У меня есть поля формы, которые используют ng-change для отслеживания изменений. Использование $watchне помогает, так как оно не запускается автозаполнением. Поскольку у меня нет кнопки отправки, нет простого способа запустить некоторые решения, и мне не удалось использовать интервалы.

В итоге я отключил автозаполнение - не идеально, но гораздо менее запутанно для пользователя.

<input readonly onfocus="this.removeAttribute('readonly');">

Нашел ответ здесь


-1

Если вы используете jQuery, вы можете сделать это при отправке формы:

HTML:

<form ng-submit="submit()">
    <input id="email" ng-model="password" required 
           type="text" placeholder="Your email">
    <input id="password" ng-model="password" required 
           type="password" placeholder="Password">
</form>

JS:

 $scope.submit = function() {
     $scope.password = $('#password').val();
}

1
Хотя это может сработать, для более сложных форм это нецелесообразно.
origin1tech

-1

Если вы хотите, чтобы это было просто, просто получите значение с помощью javascript

В вашем контроллере angular js:

var username = document.getElementById ('имя пользователя'). значение;


Я думаю, вы не понимаете, в чем проблема
aelor
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.