Autofill does not trigger onChange
Asked Answered
B

11

52

I have a login form, when the page is opened, Chrome automatically fills the credentials. However, onChange event of input elements are not being triggered.

Clicking anywhere on the page makes them registered in value attribute of the input, which is what we need. I tried doing event.target.click(), that did not help.

In the onChange I set the values into state like:

this.setState({ email: event.target.value })

And the submit button is disabled if email.length === 0.

So even if page shows that credentials are filled, submit button still looks disabled.

Since clicking anywhere registers the values, clicking the submit button also registers them before submitting. So it works perfectly.

It is not about timing though, field is never being filled. When I open the Developer Console and check, the value field is empty, until I click somewhere.

Only issue here is that the button looks disabled because email input value is empty until something is clicked. Is there a hacky solution for this?

Boehmer answered 19/3, 2019 at 15:29 Comment(8)
can you post a link to jsfiddle? That would be of great help.Launalaunce
If you disable basic functionality, you also have to check for enabling it on startup. So put the 'if email.length' check somewhere After the browser has fully loaded all data so you detect that the input is auto filled. A basic timeout could work.Lavinialavinie
It is never being filled. When I open the Developer Console and check, the value field is empty, until I click somewhere.Boehmer
Did you ever solve this? I am having the same issue. Page loads and the input (email address) is filled, but the value of the input ref is empty until the page is interacted with (a click will do), so second render it's fine. I should mention this is in Chrome only; Firefox works fine.Stewartstewed
if you can provide some code it would be betterCeliaceliac
Just use oninput as well as onchangeAutoroute
@Stewartstewed providing an oninput handler aswell as onchange did the trick for me, as suggested in the second answer. Should be the accepted answer.Autoroute
I know that this is not a solution for a described problem, but if you have a problem in submitting a form with autofilled values then put focus on an input. After user clicks submit button the browser will somehow register autofilled values with onchange/onblur event.Mizell
I
47

I stumbled across this issue because I had the same problem. Not in React, but the problem is universal. Maybe this will help anyone else who stumbles upon this.

None of the other answers actually answer the problem. The problem as described by the OP is that Chrome doesn't set the value of the inputs until there has been user interaction with the page. This can be clicking on the page, or even typing random things into the console.

After user interaction has occurred the value of the inputs are set and the change event is fired.

You can even visually see this, as the styling of the autofilled text is different when first presented and changes to the styling of the inputs once interaction has taken place.

Also: triggering a click or setting focus or whatever from code doesn't help, only real human interaction.

So polling for the value is not a solution, because the value will stay empty if the page is not clicked. Also stating that you can't rely on the change event is not really relevant, since the event is properly fired, upon the delayed setting of the value. It's the indefinite delay that's the problem.

You can however poll for the existence of Chrome's pseudo CSS classes. In Chrome 83 (june 2020) these are :-internal-autofill-selected and :-internal-autofill-previewed. These do change regularly however. Also, they are not directly present after rendering. So you should set a sufficient timeout, or poll for it.

Sloppy example in vanilla JavaScript:

var input = document.getElementById('#someinput');

setTimeout(() => {
   if (input.matches(':-internal-autofill-selected')) {
      /* do something */
   }
}, 500);

[edit 2024 by @John Jerry]

The :autofill CSS pseudo-class matches when an element has its value autofilled by the browser. The class stops matching if the user edits the field. For other browser:

var input = document.getElementById('#someinput');

setTimeout(() => {
   if (input.matches(':autofill')) {
      /* do something */
   }
}, 500);

[/edit]

You still can't get the value at this point, so in that regard it still doesn't solve the problem, but at least you can enable the submit button based on the existence of autofilled data (as the OP wanted to do). Or do some other visual stuff.

Ingesta answered 4/6, 2020 at 16:31 Comment(6)
what about what Cyber Valdez said, use oninput! worked straight away for me.Autoroute
@JessePepper No, oninput behaves the same as onchange (currently tested on Chrome 90) as in that it only fires after user interaction (the OP's problem). My answer is still the only answer that addresses this and provides some sort of solution to the OP's problem. Can you please revert the uncalled for downvote? Thank you.Ingesta
I like this answer from Karim. I will point out, be careful if you have different/competing "autofill" behavior. When Google Chrome autofill was working, I got the "font switch" / wait for user click as Karim described. (i.sstatic.net/uvwqs.gif) When LastPass extension autofill was working; I got the proper font and valueChange event. (i.sstatic.net/D3QhJ.gif)Toehold
Also, let's remember the difference between autofill vs autocomplete; as Karim says, this question is about autofill (autocomplete implies some user interaction). And finally, as Karim points out the CSS pseudoclass may change; in 2017 it seems like people query for CSS pseudoclass :-webkit-autofill (but now please see Karim's use of :-internal-autofill-selected)Toehold
One more point, as Karim says, even if you poll/watch for a CSS pseudoclass, this does not mean the input has value (even though the input appears to have a value); Karim says "You still can't get the value ... so it still doesn't solve the problem". This was true for me - my app needs the input value to do lookup. so I had no choice but to wait for user to click on the page. I could not successfully use autocomplete="off" to defeat autofill, unless I got aggressive with [type="search"](https://mcmap.net/q/57965/-chrome-ignores-autocomplete-quot-off-quot) (used on both username and password)Toehold
Plus the auto-fill is styled completely differently from the rest of the input until someone clicks on the page. How idiotic. I wonder if anyone of the current developers of Chrome remember a time when they actually had to compete with other browsers.Ross
K
3

The answer would be to either disable autocomplete for a form using autocomplete="off" in your form or poll at regular interval to see if its filled.

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:
  1. Firefox 4, IE 7, and IE 8 don't dispatch the change event.
  2. Safari 5 and Chrome 9 do dispatch the change event.
  • For other form fields:
  1. IE 7 and IE 8 don't dispatch the change event.
  2. Firefox 4 does dispatch the change change event when users select a value from a list of suggestions and tab out of the field.
  3. Chrome 9 does not dispatch the change event.
  4. Safari 5 does dispatch the change event.

You best options are to either disable autocomplete for a form using autocomplete="off" in your form or poll at regular interval to see if its filled.

For your question on whether it is filled on or before document.ready again it varies from browser to browser and even version to version. For username/password fields only when you select a username password field is filled. So altogether you would have a very messy code if you try to attach to any event.

You can have a good read on this http://avernet.blogspot.in/2010/11/autocomplete-and-javascript-change.html (not https!)

Have a look at this thread: Detecting Browser Autofill

Kloman answered 27/1, 2020 at 11:12 Comment(3)
Good Read link cannot be reachedWigeon
Polling does not work due to the simple fact that the input values will never be filled until a physical click is detected. You can poll for an infinite amount of time, but you'll still be left with empty values.Collen
Has this changed in recent times?Skidway
H
2

A solution was found in this discussion on react repository https://github.com/facebook/react/issues/1159 It's done by calling a check function multiple times using timeout, to check the '*:-webkit-autofill'.

const [wasInitiallyAutofilled, setWasInitiallyAutofilled] = useState(false)

useEffect(() => {
    /**
     * The field can be prefilled on the very first page loading by the browser
     * By the security reason browser limits access to the field value from JS level and the value becomes available
     * only after first user interaction with the page
     * So, even if the Formik thinks that the field is not touched by user and empty,
     * it actually can have some value, so we should process this edge case in the form logic
     */
    const checkAutofilled = () => {
      const autofilled = !!document.getElementById('field')?.matches('*:-webkit-autofill')
      setWasInitiallyAutofilled(autofilled)
    }
    // The time when it's ready is not very stable, so check few times
    const timeout1 = setTimeout(checkAutofilled, 500);
    const timeout2 = setTimeout(checkAutofilled, 1000);
    const timeout3 = setTimeout(checkAutofilled, 2000);
    // Cleanup function
    return () => {
      clearTimeout(timeout1);
      clearTimeout(timeout2);
      clearTimeout(timeout3);
    };
  }, [])
Herring answered 2/2, 2023 at 10:18 Comment(0)
M
1

event.target.click() will "simulate" a click and fire onClick() events, but doesn't act like a physical click in the browser window.

Have you already tried using focus() instead of (or in addition to) click()? You could try putting the focus on your input element and then check to see if the value is then set. If that works, you could add this.setState({ email: event.target.value }) on focus as well as on click.

Marijn answered 24/1, 2020 at 19:10 Comment(0)
F
1

Using oninput instead of onchange would do the trick

Fetid answered 24/9, 2020 at 7:52 Comment(4)
Don't know why this isn't the top answer!Autoroute
Because it isn't an answer to the OP's problem. oninput (just like onchange) only fires after user interaction. This was the OP's problem and is not solved by using oninput.Ingesta
It does not workElectrum
what this did for me is highlighted the input fields like they are filled and value is not showingArium
K
0

A slightly better (IMO) solution that does not require a setTimeout().

To side-step this, you can add a handler to each input's onAnimationStart event, check if the animationName is "mui-auto-fill", and then check if the input has a -webkit-autofill pseudo class, to see if the browser has auto-filled the field. You'll also want to handle the "mui-auto-fill-cancel" case for scenarios where the form is auto-filled and the user clears the values (to reset shrink.)

For example:

const [passwordHasValue, setPasswordHasValue] = React.useState(false);

// For re-usability, I made this a function that accepts a useState set function
// and returns a handler for each input to use, since you have at least two 
// TextFields to deal with.
const makeAnimationStartHandler = (stateSetter) => (e) => {
  const autofilled = !!e.target?.matches("*:-webkit-autofill");
  if (e.animationName === "mui-auto-fill") {
    stateSetter(autofilled);
  }

  if (e.animationName === "mui-auto-fill-cancel") {
    stateSetter(autofilled);
  }
};
...

<TextField
  type="password"
  id="password"
  inputProps={{
    onAnimationStart: makeAnimationStartHandler(setPasswordHasValue)
  }}
  InputLabelProps={{
    shrink: passwordHasValue
  }}
  label="Password"
  value={password}
  onChange={(e) => {
    setPassword(e.target.value);
    ...
  }}
/>

The result on load should appear as:

example

Update with cancel -- allows user to clear out the form field, after load with auto-fill, and the label is reset:

example with reset field

FYI: I made my makeAnimationStartHandler a function that accepts a React.useState() setter as a param and returns a handler that actually does the work because your example has two fields and I wanted to 1) reduce code and 2) allow you to still handle the manual entry use-case for each field separately, if desired.

Working Example: https://z3vxm7.csb.app/
Working CodeSandbox: https://codesandbox.io/s/autofill-and-mui-label-shrink-z3vxm7?file=/Demo.tsx

Karlenekarlens answered 5/8, 2023 at 14:48 Comment(1)
Nice try, but your code is invalid. In CodeSandbox. After autofill and removing one character by backspace input occurs like empty (but it's not)Sabaean
H
0

In our case, we were experiencing this problem on iOS Chrome browsers. Following this answer we needed to add a check for input.hasAttribute('chrome-autofilled')

var input = document.getElementById('#someinput');

setTimeout(() => {
   if (input.hasAttribute('chrome-autofilled')) {
      /* do something */
   }
}, 500);
Holbein answered 9/2 at 11:55 Comment(0)
B
-1

No need to trigger a click event, I think it could be better if you set the email state in componentDidMount like this

this.setState({ email: document.getElementById('email').value })

so once componentDidMount() is invoked you will get the email filled by Chrome and update the state then render the submit button and make it enabled.

You can read more about React life cycle on this link

Backstitch answered 26/1, 2020 at 9:50 Comment(2)
It sounds like the issue is occurring after rendering, so checking on componentDidMount might not make a difference. Hopefully, the OP can weigh in and give us some clarity.Marijn
Setting debugger inside of useEffect, the .value of the email input is empty, and useEffect does not fire a second time (array param is omitted) after Chrome fills the input. It's not until I interact with the page (causing a render) that useEffect fires a second time and can detect the value in the email field that was put there by Chrome.Stewartstewed
A
-1

Have a same problem. My solution was:

function LoginForm(props: Props) {

    const usernameElement = React.useRef<HTMLInputElement>(null);
    const passwordElement = React.useRef<HTMLInputElement>(null);

    const [username, setUsername] = React.useState<string>('');
    const [password, setPassword] = React.useState<string>('');

    React.useEffect(() => {
        //handle autocomplite
        setUsername(usernameElement?.current?.value ?? '');
        setPassword(passwordElement?.current?.value ?? '');
    }, [usernameElement, passwordElement])


    return (
        <form className='loginform'>

            <input ref={usernameElement} defaultValue={username} type='text' onInput={(event: React.ChangeEvent<HTMLInputElement>) => {
                setUsername(event.target.value);
            }} />

            <input ref={passwordElement} defaultValue={password} type='password' onInput={(event: React.ChangeEvent<HTMLInputElement>) => {
                setPassword(event.target.value);
            }} />


            <button type='button' onClick={() => {
                console.log(username, password);
            }}>Login</button>
        </form>
    )
}
Aftertime answered 4/8, 2022 at 10:38 Comment(0)
C
-4
useEffect(() => {
  onChange(value);
}, 
[value, onChange]);

That worked for me.

Chauffer answered 6/2, 2023 at 16:21 Comment(0)
L
-5

I have created a sample app here https://codesandbox.io/s/ynkvmv16v

Does this fix your issue?

Launalaunce answered 19/3, 2019 at 15:49 Comment(6)
No, because e.target.value is always empty. That's the issue.Boehmer
Did you try logging the event value? Your code snippet above is very wrong. event.target.email should be event.target.value. Also, can you tell me your browser version where you are facing this issue?Launalaunce
Corrected that typo. Chrome macOS 73.0.3683.75Boehmer
Logging which event value? onChange is not being triggered, so there is no event.Boehmer
I am on the same version of chrome and I don't find the issue. Do you mind sharing your code via codesandbox.io?Launalaunce
Please provide more than an external link to increase the value of the thread here. The link may be unavailable in the future. Possibly copy a simple version of your solution to your answer here. :)Kloman

© 2022 - 2024 — McMap. All rights reserved.