Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change event fires extra times before IME composition ends #3926

Open
chenxsan opened this issue May 21, 2015 · 56 comments
Open

Change event fires extra times before IME composition ends #3926

chenxsan opened this issue May 21, 2015 · 56 comments

Comments

@chenxsan
Copy link

chenxsan commented May 21, 2015

Extra details


Original Issue

When I was trying this example from https://facebook.github.io/react/blog/2013/11/05/thinking-in-react.html, any Chinese characters inputted by Chinese pinyin input method would fire too many renders like:

screen shot 2015-05-21 at 14 04 36

Actually I would expect those not to fire before I confirm the Chinese character.

Then I tried another kind of input method - wubi input method, I got this:

screen shot 2015-05-21 at 14 17 15

It's weird too. So I did a test in jQuery:

screen shot 2015-05-21 at 14 05 12

Only after I press the space bar to confirm the character, the keyup event would fire.

I know it might be different between the implementation of jQuery keyup and react onChange , but I would expect the way how jQuery keyup handles Chinese characters instead of react's onChange.

@chenxsan chenxsan changed the title Change event fires too much when inputing Chinese characters. Change event fires too many times when inputing Chinese characters. May 21, 2015
@sophiebits
Copy link
Collaborator

cc @salier :) – What should we do here?

@hellendag
Copy link

I think we should not fire onChange until the IME string is committed.

One way to handle this in ChangeEventPlugin would be to ignore all input events between compositionstart and compositionend, then use the input event immediately following compositionend.

I did some quick testing on OSX Chrome and Firefox with Simplified Pinyin and 2-Set Korean, and the event order and data seem correct enough. (I predict that we'll have problems with IE Korean, but we may get lucky.)

I think we may continue to see issues with alternative input methods like the Google Input Tools extension, but there may be workarounds for that.

@Bertg
Copy link

Bertg commented Sep 15, 2015

This also influences how dialectic characters are typed for latin languages. For example on OS X inserting an é will fail using an international keyboard. Even the long press e and then use variant are failing here.

Sorry this seems to not be related. My apologies.

@jasonslyvia
Copy link
Contributor

Is there any update? Suffering from this issue too.

@sophiebits
Copy link
Collaborator

None currently – this is not a high priority for us right now. I'd be happy to look at a pull request if anyone dives into fixing this.

@pswai
Copy link

pswai commented Oct 2, 2015

@salier It seems like IE does not fire input event after compositionend. I have tested on IE11 and Edge on Windows 10. It fires properly in Chrome and Firefox.

lingceng added a commit to lingceng/ajax-chosen that referenced this issue Apr 28, 2016
Similar problem with:
Change event fires too many times when inputing Chinese characters
facebook/react#3926
lingceng added a commit to lingceng/ajax-chosen that referenced this issue Apr 29, 2016
Similar problem with:
Change event fires too many times when inputing Chinese characters
facebook/react#3926
@suhaotian
Copy link

in ie 9, Change event fires too many times when inputing Chinese characters again

@zhaoyao91
Copy link

zhaoyao91 commented Aug 5, 2016

Hello, Facebook guys, in fact this issue causes a SERIOUS problem: we can't update input asynchronously with Chinese input.
For example, we can't use meteor reactive datasources or stores like redux, because they all feedback update asynchronously.
Here is a simplest example to show this problem, it use setTimeout to do async update:
https://jsfiddle.net/liyatang/bq6oss6z/1/

I really hope you can fix this quickly, so that we won't waste efforts to workaround it here and there and over and over again.

Thanks.

Here is my workaround. If anyone face the same problem, you can have a look

@eyesofkids
Copy link

eyesofkids commented Aug 16, 2016

I made a simple example to demo how to use compositionstart and compositionend events to prevent inputing Chinese IME on onchange event problem.
Here is the link: https://jsfiddle.net/eyesofkids/dcxvas28/8/

@zhaoyao91
Copy link

zhaoyao91 commented Aug 17, 2016

@eyesofkids nice work, this could be made as the default implementation of onChange for input, textarea ...

@suhaotian
Copy link

nice work !

@julen
Copy link
Contributor

julen commented Aug 21, 2016

I was hitting the same issue and @eyesofkids' workaround works perfectly (thank you!).

After having the workaround in place, I was diving into React's source code to at least try to add a failing test for this —hoping to later add the expected behavior to the library— although it seems a bit complicated for someone unfamiliar with the internals.

Initially I was expecting that a test similar to what's already available for ChangeEventPlugin should work, i.e. simulating a native compositionStart/compositionUpdate and checking no onChange callback was fired; also checking onChange would only be fired once compositionEnd was simulated. However this doesn't seem to work.

Hence I was thinking that maybe checking for ChangeEventPlugin.extractEvents() would be a feasible approach, similar to what's done in the tests for SelectEventPlugin. Here for some reason I always get undefined when extracting the events though.
For reference, this is the test code I tried within ChangeEventPlugin-test.js:

  var EventConstants = require('EventConstants');
  var ReactDOMComponentTree = require('ReactDOMComponentTree');
  var topLevelTypes = EventConstants.topLevelTypes;

  function extract(node, topLevelEvent) {
    return ChangeEventPlugin.extractEvents(
      topLevelEvent,
      ReactDOMComponentTree.getInstanceFromNode(node),
      {target: node},
      node
    );
  }

  function cb(e) {
    expect(e.type).toBe('change');
  }
  var input = ReactTestUtils.renderIntoDocument(
    <input onChange={cb} value='foo' />
  );

  ReactTestUtils.SimulateNative.compositionStart(input);

  var change = extract(input, topLevelTypes.topChange);
  expect(change).toBe(null);

I'm afraid I don't know exactly how one is supposed to debug these tests—otherwise I'd have a clearer picture of what's going on. Any guidance on how to proceed or any other pointers would be highly appreciated.

@julen
Copy link
Contributor

julen commented Sep 13, 2016

The workaround suddenly broke in Chrome 53+ and it seems it is not valid anymore because they changed the order compositionend is fired: previously it happened before textInput, now after textInput. As a consequence of this, change won't be fired if it is aborted while in composition 😕.

@suhaotian
Copy link

https://github.com/suhaotian/react-input maybe help someone

@eyesofkids
Copy link

eyesofkids commented Oct 5, 2016

There is a tricky solution for Chrome v53. To call the handlechange after compositionend is fired.

handleComposition  = (event) => {

    if(event.type === 'compositionend'){
      onComposition = false

      //fire change method to update for Chrome v53
      this.handleChange(event)

    } else{
      onComposition = true
    }
  }

check the demo here: https://jsfiddle.net/eyesofkids/dcxvas28/11/

@vincent714
Copy link

@chenxsan did you find out the solution?
you can detect the compositionStart and let a variable equal to true.
Then to use the variable, which you set, at onChange to see if it should fire the query

@RebaekSoparkKitchen
Copy link

any updates?

Hi, I face the same issue when I want to do some operation in handleChange function, and my solution is just use composition event to handle it.

If it works for you, extract it to hooks and we use reuse it easily.

import React, { useState } from 'react';

export default function Solution() {
  const [value, setValue] = useState('');
  const [onComposition, setOnComposition] = useState(false);

  function handleComposition(event) {
    if (event.type === 'compositionstart') {
      setOnComposition(true);
    }
    if (event.type === 'compositionend') {
      setOnComposition(false);
    }
  }
  function handleChange(e) {
    let temp = e.target.value;
    if (!onComposition) {
      // any operation you want to do...
      temp = temp.replace(/z/g, 'Z');
    }
    setValue(temp);
  }

  return (
    <input
      onChange={handleChange}
      value={value}
      onCompositionStart={handleComposition}
      onCompositionEnd={handleComposition}
    />
  );
}

@krmao
Copy link

krmao commented Sep 29, 2021

also this code https://zhuanlan.zhihu.com/p/415365117

@eyesofkids
Copy link

eyesofkids commented Jul 31, 2022

also this code https://zhuanlan.zhihu.com/p/415365117

A simpler component:

// refs: https://zhuanlan.zhihu.com/p/415365117

import { useRef, useState, forwardRef } from 'react'

function InputIME(props, ref) {
  const { onChange, value, ...otherProps } = props

  // log if on composition
  const onComposition = useRef(false)
  // temp input
  const [inputValue, setInputValue] = useState('')

  const _onChange = (event) => {
    setInputValue(event.target.value)

    // IME method start
    if (event.type === 'compositionstart') {
      onComposition.current = true
      return
    }

    // IME method end
    if (event.type === 'compositionend') {
      onComposition.current = false
    }

    // handle parent onChange
    if (!onComposition.current) {
      onChange(event)
    }
  }

  return (
    <input
      {...otherProps}
      ref={ref}
      value={inputValue}
      onChange={_onChange}
      onCompositionEnd={_onChange}
      onCompositionStart={_onChange}
    />
  )
}

export default forwardRef(InputIME)

@kamikat
Copy link

kamikat commented Dec 27, 2022

Another hook-ish workaround:

import { useEffect, useState, useRef } from "react";

export function useAsyncInputRef<T extends HTMLElement & { value: string }>(
  value: string,
  forwardRef?: React.RefObject<T>
) {
  const [compositionFlag, setComposition] = useState(false);
  const newRef = useRef<T>(null);
  const ref = forwardRef || newRef;

  useEffect(() => {
    const el = ref.current;
    if (el) {
      const compositionStart = () => setComposition(true);
      const compositionEnd = () => setComposition(false);
      el.addEventListener("compositionstart", compositionStart);
      el.addEventListener("compositionend", compositionEnd);
      return () => {
        el.removeEventListener("compositionstart", compositionStart);
        el.removeEventListener("compositionend", compositionEnd);
        setComposition(false);
      };
    }
  }, [ref]);

  useEffect(() => {
    if (ref.current && compositionFlag === false) {
      ref.current.value = value;
    }
  }, [compositionFlag, value, ref]);

  return ref;
}
render(
  <input
    type="text"
    ref={useAsyncInputRef(value)}
    onChange={(e) => setValue(e.target.value)}
  />
);

Live demo: https://codepen.io/kamikat/pen/OJwMJLr

@17x
Copy link

17x commented Feb 14, 2023

Here is another one implementation:

interface Props extends React.InputHTMLAttributes<HTMLInputElement> {
  onChange: (e) => void;
  onRealChange?: (e) => void;
  comp?: React.FunctionComponent;
}

const CompositionEventInput: FC<Props> = ({
  onChange,
  onRealChange,
  comp: Comp,
  ...rest
}: Props) => {
  const [isComposition, setIsComposition] = useState(false);
  const [value, setValue] = useState("");

  const common = (e) => {
    const { type } = e;

    switch (type) {
      case "compositionstart":
        setIsComposition(true);
        break;

      case "compositionend":
        if (isComposition) {
          onChange(e);
        }

        setIsComposition(false);
        break;

      case "change":
        const newValue = e.target.value.trim();

        setValue(newValue);

        if (onRealChange) {
          onRealChange(newValue);
        }

        if (!isComposition) {
          onChange(e);
        }
        break;
    }
  };

  const events: Partial<HTMLAttributes<Function>> = {
    onKeyDown: common,
    onKeyUp: common,
    onCompositionEnd: common,
    onCompositionStart: common,
    onCompositionUpdate: common,
    onChange: common
  };

  const props = {
    value,
    ...events
  };

  if (Comp) {
    return <Comp {...props} />;
  }

  // @ts-ignore
  return <input {...props} {...rest} />;
};

CodeSandBox DEMO Link

@zheeeng
Copy link

zheeeng commented Jun 8, 2023

Is there an official improvement plan for this? I'm suffering from using workarounds anywhere.

@weihong1028
Copy link

weihong1028 commented Jun 8, 2023 via email

@lethi2k
Copy link

lethi2k commented Jan 11, 2024

import React, { useState } from 'react';

export default function Solution() {
  const [onComposition, setOnComposition] = useState(false);

  function handleComposition(event) {
    if (event.type === 'compositionstart') {
      setOnComposition(true);
    }
    if (event.type === 'compositionend') {
      setOnComposition(false);
    }
  }
  function onPressEnter(e) {
    if (!onComposition) {
       // code here
    }
  }

  return (
    <input
      onPressEnter={onPressEnter}
      value={value}
      onCompositionStart={handleComposition}
      onCompositionEnd={handleComposition}
    />
  );
}

@weihong1028
Copy link

weihong1028 commented Jan 11, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.