Vue: Chrome Autofill does not trigger change

Created on 14 Nov 2017  ·  26Comments  ·  Source: vuejs/vue

Version

2.5.3

Reproduction link

https://jsfiddle.net/n2kcp15b/

Steps to reproduce

  1. open any form that has a textfield
  2. auto complete it from chrome auto fill
  3. check the attached v-model and see that it didn't update

What is expected?

update the v-model from what ever chrome autofilled

What is actually happening?

no update is happening


This only happens on Chrome on iOS (safari works fine). I also got reports it has the same issue on Chrome on Android but i didn't confirm.

This seems to be still an issue and i saw a few github issues that reported the same thing months ago but are now closed.

browser quirks

Most helpful comment

You may add "name" attribute. It works for me in last Chrome ☮️

All 26 comments

Currently the workaround i have is:

setInterval(() => {
  let value = $('#' + this.id).val();
  if(value != '' && this.value != value) {
    this.value = value;
  }
}, 50);

It works fine on Android, cannot test on iOS but there's a known issue with iOS Safari not emitting events when autocompleting, so, unfortunately, I'm not sure we can do something.
@VinceG There must be an event you can listen to instead of checking the value every 50ms, Maybe listening to the blur event will work

I don't think this is something we can fix at the framework level, because it's buggy behavior in one specific browser (not firing input events on autocomplete). A userland workaround seems to be the only way here - I'd suggest only adding the setInterval when you know you are in that specific browser, alternatively, perform the manual sync once before submitting.

Wrap elements in <form> to trigger OnChange event

This issue is not unique to Vue and other front end frameworks have tried to tackle this as well

The General Problem

According to this post on Detecting Browser Autofill from StackOverflow

The problem is autofill is handled differently by different browsers. Some dispatch the change event, some don't. So it is almost impossible to hook onto an event which is triggered when browser autocompletes an input field.

Change event trigger for different browsers:

  • For username / password fields:

    • Firefox 4 & IE 7 - _don't dispatch_ the change event.

    • Safari 5 & Chrome 9 - _do dispatch_ the change event.

  • For other form fields:

    • IE 7 - _don't dispatch_ the change event.

    • Firefox 4 - _does dispatch_ the change change event, but only when users select a value from a list of suggestions and tab out of the field.

    • Chrome 9 - _does not dispatch_ the change event.

    • Safari 5 - _does dispatch_ the change event.

Hack For WebKit browsers

According to the MDN docs for the :-webkit-autofill CSS pseudo-class:

The :-webkit-autofill CSS pseudo-class matches when an element has its value autofilled by the browser

So, leveraging that property:

We can define a void transition css rule on the desired <input> element once it is :-webkit-autofilled. JS will then be able to hook onto the animationstart event.

Credit to the Klarna UI team for coming up with the solution here:

klarna/ui/Field/styles.scss:

:-webkit-autofill {
    /* Expose a hook for JavaScript when auto fill is shown. */
    /* JavaScript can capture 'animationstart' events */
    animation-name: onAutoFillStart;

    // Make the backgound color become yellow _really slowly_
    transition: background-color 50000s ease-in-out 0s;
 }

:not(:-webkit-autofill) {
    /* Expose a hook for JS onAutoFillCancel */
    /* JavaScript can capture 'animationstart' events */
    animation-name: onAutoFillCancel;
}

klarna/ui/Field/index.js:

this.refs.input.addEventListener('animationstart', (e) => {
  switch (e.animationName) {
    case defaultStyles.onAutoFillStart:
      return this.onAutoFillStart()

    case defaultStyles.onAutoFillCancel:
      return this.onAutoFillCancel()
  }
})

Outside of that, you're probably going to have to resort to polling for changes, since we can't guarantee an event will fire.

The library tbosch/autofill-event bills itself as "A polyfill to fire a change event when the browser auto fills form fields".

@KyleMit Thanks for the details explanation. It's a-shame that we still don't have a proper consistent API to work with across browsers.

You may add "name" attribute. It works for me in last Chrome ☮️

You may add "name" attribute. It works for me in last Chrome

This trick works great, thanks!

You may add "name" attribute. It works for me in last Chrome ☮️

Confirm, It works! It looks like all fields that can be autofiled should have "name" attribute

I have the same problem on Macos 10.13.3, chrome 70.0.3538.110.I find that just change when you click on any other non-input place, the change event will trigger, This seems to be triggered when the input's blur event fires.This is really weird.

You may add "name" attribute. It works for me in last Chrome ☮️

This not works for me,I do form validation when the blur event fires. When the blur event is triggered for the first time,the model of username and password was empty string.But when you click on any other non-input place, the change event will trigger,The model will get the autofilled value, This is really weird

It works fine on Android, cannot test on iOS but there's a known issue with iOS Safari not emitting events when autocompleting, so, unfortunately, I'm not sure we can do something.
@VinceG There must be an event you can listen to instead of checking the value every 50ms, Maybe listening to the blur event will work
I agree with you, I think it will trigger change when blur event is occurred
do something like this:

handleBlur ($evt) { 
   this.$nextTick().then(() => {
       // The model here will get the autofilled value
   })
}

@yyx990803 why need this $nextTick()?

For me it worked fine, _if_ there was a single form field. With multiple fields, it wouldn't work. I had to have them wrapped in <form> tag for it to work.

@rmaclean-ee You do not really understand what I mean,If you just click the login button to login, there is no problem.But if you bind blur event to the form field, when the blur event trigger you get the value, the value is empty, I think that Chrome remembers the password may be set the value to form fied when the blur event is triggered or after blur event is triggered.

I wasn't responding to your point on the nextTick, I was responding to the original requester about causes for it not filling in v-model.

As I said, making sure it is wrapped in a form tag solved my issue and my v-model is correctly filled in now without the need for any workarounds.

Has anyone got a working solution to this? I've tried a lot of the solutions proposed and still no luck

@clem109 I totally have a working solution, but maybe you have hit something new? Can share some code of your page so maybe we can work out what specific aspect is not gelling

<script>
  data () {
      return {
        email: '',
        password: ''
      }
    },
  methods: {
        onBlur (event) {
        if (event.type === 'email') {
          this.email = event.target.value
        }
        if (event.type === 'password') {
          this.password = event.target.value
        }
      },

      handleSubmit () {
        this.$validator.validateAll().then(success => {
          if (success) {
            this.$emit('submitted', {
              email: this.email,
              password: this.password
            })
          }
        })
      }
    }
  }
  }
</script>

<template lang='pug'>

      form(@submit.prevent='handleSubmit')
        .login-form__fields
          text-input(
            id='email',
            type='email',
            name='email',
            ref='email'
            label='Email address',
            v-model='email',
            v-validate='"required|email"',
            @blur='onBlur',
            @change='onBlur',
            :error='errors.has("email")',
            :feedback='errors.first("email")'
          )

          password-input(
            id='password',
            label='Password',
            name='password',
            ref='password',
            @blur='onBlur',
            @change='onBlur',
            @focus='onBlur',
            v-model='password',
            v-validate='"required"',
            :error='errors.has("password")',
            :feedback='errors.first("password")'
          )

I've omitted some aspects which were not necessary, I have this working now on iOS safari but iOS chrome it won't work at all...

Thanks for the help @rmaclean-ee

I know this issue has been closed for some time now, but just for the sake of documentation and future Googles:

I'm having the same problem that @WormGirl detailed on Chrome 72.0.3626.121, MacOS 10.14.3. Clicking anywhere or calling something as simple as offsetHeight (from the console, can't seem to get this to work from inside a component consistently) causes a redraw and properly triggers the expected events.

Interestingly enough, calling foo.matches(':webkit-autofill') also returns false during this state, which means the entire JavaScript side of things is not being updated in general (even polling the value attribute returns empty quotes).

For reference, I have two inputs, email and password, and they both are wrapped in a <form> element and have name attributes. This doesn't make a difference as suggested by earlier comments.

I was able to get away with only CSS selectors (which work correctly), but this is definitely just a browser-specific bug.

I've faced similar problem with autofill and v-model usage in Chrome browser on MacOS (Firefox is fine) and nothing mentioned above fixed the problem for me.
But my user-case is a little bit more specific.

I've been using v-model on VueComponent which passes its down to inner <input /> using :value="value" & @input binding. The problem which I've encountered is that autofilled value injected by chrome (without any events) is being overwritten by Vue's bindings (bug does not appear when used on plain <input /> with v-model).

The hack is instead of always binding value to <input /> use conditional binding:

<template>
   <input v-bind="{ /* any bindings you need */ ...valueBinding } @input="onInput" />
</template>
....
computed: {
  valueBinding() {
      /*
        Fix to Chrome autocomplete bug
        Autofilled value gets overriden by v-model or .sync binding 
        So if value is null we just dont bind it 
       and allow chrome to pass whatever it wants first 
      */
      let binding = {};

      if (this.value) { 
        binding.value = this.value;
      }

      return binding;
  }
}

It looks dirty, feels dirty and it is actually dirty. But it works for me and it took a lot of time to come up with solution. Hopes it will help somebody.

@PinkiNice what is b? it's not defined (trying to fix my bug here )

bug of chrome, not vue.
remove those css bound to the value of input-element, as a nowaday solution

head>
    <title>自动填充与JS-Stack</title>
    <meta charset="utf-8">
</head>

<form method="get">
    <input id="username" name="username" placeholder="姓名" type="text" onchange="onChange(this)"/>
    <input id="password" name="password" placeholder="密码" type="password" onchange="onChange(this)"/>
    <input type="submit" value="Submit"/>
</form>

<script>
    var username = document.getElementById('username');
    var password = document.getElementById('password');
    function show(tag){
        console.log(tag, 'username=', username.value, '!');
        console.log(tag, 'password=', password.value, '!');
    }
    function onChange(input){
        //alert会切换focus
        //alert(input.name+" changed!");
        console.log('input:', input.name, '=', input.value);  
        show('after change:');
    }

    show('step 1:');

    // focus切换触发JS-Stack运行
    if(false){
        username.onfocus = password.onfocus = window.onfocus=function(event){
            console.log("onfocus", event.target)
        }
        username.onblur = password.onblur = window.onblur=function(){
            console.log("onblur", event.target)
        }
    }

    // 通过定时器无法运行JS-Stack: autocomplete对应的focus|change|blur事件
    window.setTimeout(function(){
        show('step 2:'); 
        // window.setTimeout(function(){
        //  show('step 3:'); 
        //},1000);
    },1000);
</script>

It works fine on Android, cannot test on iOS but there's a known issue with iOS Safari not emitting events when autocompleting, so, unfortunately, I'm not sure we can do something.
@VinceG There must be an event you can listen to instead of checking the value every 50ms, Maybe listening to the blur event will work
I agree with you, I think it will trigger change when blur event is occurred
do something like this:

handleBlur ($evt) { 
   this.$nextTick().then(() => {
       // The model here will get the autofilled value
   })
}

@yyx990803 why need this $nextTick()?

This is just like setTimeout.

Based on facebook/react#1159, it looks like the input fields have the value before page load, so for vuetify (can easily be adapted for anything else), I added this code to my login dialog, and put refs on both text fields.

Typescript w/ vue-property-decorator:

@Component export default class extends Vue {
    username = "";
    password = "";

    @Ref() readonly usernameBox!: Vue;
    @Ref() readonly passwordBox!: Vue;

    mounted() {
        this.username = this.usernameBox.$el.querySelector("input")!.value;
        this.password = this.passwordBox.$el.querySelector("input")!.value;
    }
}

Vanilla JS:

export default {
    data: () => ({
        username: "",
        password: "",
    }),
    mounted() {
        this.username = this.$refs.usernameBox.$el.querySelector("input").value;
        this.password = this.$refs.passwordBox.$el.querySelector("input").value;
    }
}

Edit:

This approach works for material design inputs, preventing overlapping text, but the referenced issue also has an excellent comment by @thomasjulianstoelen describing why this behavior is, and how to fix it. Simply encasing things in form elements does not (usually) work. The form must have a submit button, or the user must hit enter within the form before the values become readable. I suggest reading the entire comment, but here's a summary:

The reason password auto-fills can't be read is because it would be quite easy to setup a phishing attack, sending the user a malicious page, which can immediately read their passwords that the browser has auto-filled. To remedy this, you must wrap your auto-filled password boxes in a form and have a proper submit button. Once the form is submitted, the browser acknowledges this as user confirmation that the site can have access to their credentials, which then become readable.

You could set autocomplete="off" attribute(HTML5), or record input value in url hash.

The best way for me:

const input = this.$refs.input;
      this.autofilled = window.getComputedStyle(input, ':-webkit-autofill').animationName === 'onAutoFillStart';
Was this page helpful?
0 / 5 - 0 ratings

Related issues

bfis picture bfis  ·  3Comments

hiendv picture hiendv  ·  3Comments

wufeng87 picture wufeng87  ·  3Comments

lmnsg picture lmnsg  ·  3Comments

bdedardel picture bdedardel  ·  3Comments