React Mobx compositionStart compositionUpdate compositionEnd

Problem description

When using input, it is common to validate the input in one of two ways:

  1. Allow the user to enter, and make an error prompt;

  2. The user is not allowed to enter characters that the re or function matches.

The requirements are as follows: Only English characters, digits, and Chinese characters are allowed. Other special characters and symbols are not allowed. Obviously, this scenario requires the second validation method.

Then I thought I was smart enough to write the following code (using the component library Cloud-React) to process the value bound to the input when the value changes (onChange event) and replace all characters other than English, numbers, and Chinese characters with an empty string.

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}
   	/>}}Copy the code

As a result, when I typed pinyin, something magical happened: everything in front of me became characters except the last character.

what??? Question mark, do you have a lot of friends?

So, I set foot on a road of no return, bah, bah, is to open the door of the new world, is the door may be a little heavy for me, push two days to see the new world.

The reason: Pinyin input is a process in which each letter you type triggers an onChange event, and the product is eaten during verification, leaving behind an empty string.

The solution

Two attributes are needed:

compositionstart

compositionend

In simple terms, compositionStart is triggered when you start a new input using an input method. There is also a function compositionUpdate that is triggered when you type an update, as the name suggests. Compositionend is raised when the end input method is entered.

Let’s get down to business:

First, let’s look at a perfectly 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}
        />}}Copy the code

The Input component can be used in two scenarios:

  1. Uncontrolled input box: the business side does not pass value to the component and cannot control the value of the input box.

  2. Controlled input boxes: A business can externally control the value of an input box by passing a value to a component.

There is no bug in the uncontrolled input box in my use. I will not repeat it here. Here I will only talk about the controlled input box, which is the scene we need to use in our requirement (only English, numbers and Chinese characters are allowed to enter, and other special characters and symbols are not allowed to enter).

Compositionstart and compositionEnd will not trigger the verification if the onChange event is not triggered during the input process.

Let’s define a variable isOnComposition to determine if it’s 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} / >
}
Copy the code

You think this is gonna be easy?

Oh, you think too much!

I still used the original demo to test the code, and found that things were a little more magical, this time the pinyin did not enter at all

The function onCompositionstart and onCompositionupdate are only triggered. At first I thought it was the logic that was looped. Then I thought about the reason.

State. Value does not change during the input process. There will be no input value in the input process.

So it’s not a program logic loop, it’s broken.

Then I thought about how to connect the broken program (yes, we pick it up when it breaks, ha) and finish the chain.

I think of a lot of ways, also read a lot of ways on the Internet, but unfortunately can not solve my dilemma.

In fact, considering that the original code used state.value to control input value changes, but still did not control the input value in their own hands, the concept of “process” is meaningless. As long as state.value is tied to input, I play with mine and others play with others. So, here’s the code that puts control back in 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} / >}}Copy the code

I’ve tested it, and it’s no problem.

Also take a look at Chrome and Firefox, and sure enough, there are bugs:

  1. The execution sequence in Firefox is compositionStart compositionEnd onChange

  2. Compositionstart onChange compositionEnd

Finally, for compatibility, modify the handleComposition function

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

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

     return;
   }

   this.isOnComposition = true;
 };
Copy the code

Because no matter what functions are executed in the middle, the onChange event will need to be executed in the end, so the judgment is added and special processing is made for Google Browser (other browsers have not been considered and processed).

Full code github.com/liyuan-meng…

Afterword.

At this point, the end of the text, I also want to say two places need to pay attention to, in fact, also stepped on the pit:

  1. If the implementation of the Input component uses the react. PureComponent, there are problems with the above requirements: When a special character is typed, the external re replaces it, and the value passed inside the Input component actually doesn’t change at all, nor does it trigger the component render. This is because the PureComponent is optimized for shouldComponentUpdate. If the props and state properties are not changed, the component will not be rerendered. ShouldComponentUpdate is encapsulated in the component implementation using react.component.

  2. When mobx is used externally, a similar situation happens when an Observable listens for a value. When a special character is typed, the regex replaces the value with the regex, and mobx finds that the value hasn’t changed, so it doesn’t trigger render. My temporary solution is to use state. Although I think this is not the best solution, I can’t think of any other solution at present.