Solve the bug of input box in React under Chinese input method

The following technical points will be involved: react # mobx composition start composition update composition end

 

Problem description

When using input, the input content is usually verified in two ways:

  1. Allow users to input and give error prompt;
  2. The user is not allowed to enter characters matched by regular functions or functions.

The existing requirements are as follows: "only English, numbers and Chinese characters are allowed to be input, and other special characters and symbols are not allowed to be input". Obviously, this scenario needs to use the second verification method.

Then I thought I was smart enough to write the following code (introducing the component library cloud react). When the input value changes (onChange event), I process the value bound to the input and replace all characters except English, numbers and Chinese characters with empty strings.

export default class CompositionDemo extends Component {
  constructor() {
     this.state = {
       value: ''
     };
  }
  
  onChange(evt) {
     this.setState({
       value: evt.target.value.replace(/[^a-zA-Z0-9\u4E00-\u9FA5]/g, '')
     });
  };
  
  render() {
    return <Input
        onChange={this.onChange.bind(this)}
          value={this.state.value}
       />
  }
}

Ordinary, ordinary, everything seems to be normal operation. As a result, when I input Pinyin, a magical thing happened: when I spell, except for the last word, the front one becomes a character.

what??? Question mark, do you have many friends?

So I embarked on a road of no return. Pooh, Pooh, Pooh, it opened the door of the new world, which may be a little heavy for me. It took me two days to see the new world.

Correct the reason: Pinyin input is a process. To be exact, in this process, every letter you input triggers the onChange event, and the product in the process of your input is eaten in the verification, leaving an empty string, so the above magical phenomenon occurs.

vi designhttp://www.maiqicn.com Complete collection of office resources websiteshttps://www.wode007.com

Solution

Two attributes are needed here: compositionstart and compositionend

To put it simply, when you start to use the input method for new input, it will trigger compositionstart. In fact, the intermediate process also has a function "compositionupdate". As the name suggests, it will be triggered when you enter an update; When the input method is finished, the composition end will be triggered.

Let's get to the point:

First, let's take a look at a very normal implementation of the Input component:

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default class InputDemo1 extends Component {

    constructor(props) {
        super(props);
        this.state = {
            value: '',
        };
    }
    static getDerivedStateFromProps({ value }, { value: preValue }) {
        if (value !== preValue) {
            return { value };
        }
        return null;
    }

    onChange = evt => {
        this.props.onChange(evt);
    };

    render() {
        return <input
            value={this.state.value}
            type="text"
            onChange={this.onChange}
        />
    }
}

The Input component has two application scenarios:

  1. Uncontrolled input box: the value of the input box cannot be controlled because the business party does not pass in value to the component;
  2. Controlled input box: the business party can control the value of the input box externally by passing value to the component.

There are no bug s in the uncontrolled input box when I use it. I won't repeat it here. I only talk about the controlled input box, that is, the scene we need to use in our needs (only English, numbers and Chinese characters are allowed, and other special characters and symbols are not allowed).

The compositionstart and compositionend mentioned earlier should come out: using the characteristics of these two attributes, if input is not allowed to trigger onChange event in the "process" of inputting Pinyin, verification will not be triggered naturally. Well, now that you have an idea, start coding.

We define a variable isOnComposition to determine whether it is "in process"

isOnComposition = false;

handleComposition = evt => {
  if (evt.type === 'compositionend') {
    this.isOnComposition = false;
    return;
  }

  this.isOnComposition = true;
};

 onChange = evt => {
   if (!this.isOnComposition) {
     this.props.onChange(evt);
   }
 };

render() {
  const commonProps = {
    onChange: this.onChange,
    onCompositionStart: this.handleComposition,
    onCompositionUpdate: this.handleComposition,
    onCompositionEnd: this.handleComposition,
  };
  return <input
    value={this.state.value}
    type="text"
      {...commonProps}
  />
}

Do you think it's so easy to solve?

Oh, you think too much!

I still use the demo at the beginning to test the code and find that things are a little magical. This time, I can't input Pinyin at all. Wow ~

I checked the following function calls when inputting Pinyin:
Yes, Ning didn't read it wrong. Only onCompositionstart and onCompositionupdate functions were triggered. At first, I thought it was logic that was written and circled by me. I thought about the reason (in fact, I thought for a long time. I'm a little stupid. Laugh):

The culprit is the value bound to the input. In the process of inputting Pinyin, state Value has not changed. Naturally, there will be no input value in the input. Without the input value, the input process cannot be completed, the compositionend cannot be triggered, and it is always in the "process".

So this time it's not the loop of program logic, it's the interruption.

So I thought about how to connect the interrupted program (yes, we'll pick it up when it breaks, ha) and complete the chain.

I have thought of many ways and read many ways on the Internet. Unfortunately, they can't solve my dilemma.

All kinds of sad can't bear to look back. Fortunately, I finally found a way: in fact, think about using state in the original code Value to control the change of input value, or did not put the control of when to input value in input into their own hands, and the concept of "in process" lost its meaning. Just state Value is also tied to input, that is, I play with my own and others play with others. So, there is the following code to let control back into my hands.

import React, { Component, createRef } from 'react';
import PropTypes from 'prop-types';

export default class InputDemo extends Component {

    inputRef = createRef();

    isOnComposition = false;

    componentDidMount() {
        this.setInputValue();
    }

    componentDidUpdate() {
        this.setInputValue();
    }

    setInputValue = () => {
        this.inputRef.current.value = this.props.value || ''
    };

    handleComposition = evt => {
        if (evt.type === 'compositionend') {
            this.isOnComposition = false;
            return;
        }

        this.isOnComposition = true;
    };

    onChange = evt => {
        if (!this.isOnComposition) {
            this.props.onChange(evt);
        }
    };

    render() {
        const commonProps = {
            onChange: this.onChange,
            onCompositionStart: this.handleComposition,
            onCompositionUpdate: this.handleComposition,
            onCompositionEnd: this.handleComposition,
        };
        return <input
            ref={this.inputRef}
            type="text"
            {...commonProps}
        />
    }
}

After testing, it's almost no problem.

Let's also take a look at Google browser and Firefox. Sure enough, there are still holes:

  1. Execution order in Firefox browser: compositionstart compositionend onChange
  2. Execution order in Google Browser: compositionstart onChange compositionend

Finally, do compatibility processing and modify the handleComposition function

handleComposition = evt => {
   if (evt.type === 'compositionend') {
     this.isOnComposition = false;

     // Google Browser: compositionstart onChange compositionend
     // Firefox browser: compositionstart compositionend onChange
     if (navigator.userAgent.indexOf('Chrome') > -1) {
       this.onChange(evt);
     }

     return;
   }

   this.isOnComposition = true;
 };

No matter what functions are executed in the middle, the onChange event needs to be executed in the end. Therefore, judgment is added and special treatment is made for Google browser (other browsers have not considered and handled it for the time being).

Postscript

Now that the text is over, I would like to say two points that need attention. In fact, they are also stepping on the pit:

  1. If the implementation of the Input component uses react PureComponent, the problem that will appear in the above requirements: when inputting special characters, they are replace d externally through regularization. In fact, the value passed into the Input component does not change, and the component render will not be triggered. This is because PureComponent optimizes the shouldComponentUpdate function. If it is found that the properties on props and state have not changed, the component will not be re rendered. Therefore, my temporary treatment is: use react Component, which encapsulates shouldComponentUpdate in component implementation.
  2. When using mobx externally, if observable is used to monitor value, it will be similar to the lace above - when inputting special characters, replace them through regularization. Mobx also finds that there is no change in value, so render will not be triggered. My temporary treatment is to use state. Although I don't think this is the best way, I can't think of other treatment methods at present.

Tags: Framework

Posted by nootkan on Sun, 15 May 2022 17:09:43 +0300