naezith
naezith

Reputation: 671

Autofill does not trigger onChange

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?

Upvotes: 53

Views: 40748

Answers (11)

Enes Emre Akbulut
Enes Emre Akbulut

Reputation: 21

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

Upvotes: 0

Karim Ayachi
Karim Ayachi

Reputation: 717

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.

Upvotes: 50

Tanguy Pauvret
Tanguy Pauvret

Reputation: 21

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

Upvotes: 2

Steve G
Steve G

Reputation: 13377

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

Upvotes: 0

Filin
Filin

Reputation: 1

useEffect(() => {
  onChange(value);
}, 
[value, onChange]);

That worked for me.

Upvotes: -4

lehm.ro
lehm.ro

Reputation: 762

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

Upvotes: 4

NemanjaLazic
NemanjaLazic

Reputation: 702

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

Upvotes: -1

Cyber Valdez
Cyber Valdez

Reputation: 311

Using oninput instead of onchange would do the trick

Upvotes: 1

Ed Lucas
Ed Lucas

Reputation: 7345

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.

Upvotes: 1

Ahmad Kayali
Ahmad Kayali

Reputation: 32

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

Upvotes: -1

Ashish Santikari
Ashish Santikari

Reputation: 443

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

Does this fix your issue?

Upvotes: -5

Related Questions