【问题标题】:React controlled input cursor jumps反应控制的输入光标跳跃
【发布时间】:2018-02-10 12:53:02
【问题描述】:

我正在使用 React 并格式化了一个受控输入字段,当我写一些数字并在输入字段外单击时,它可以正常工作。但是,当我想编辑输入时,光标会跳到输入字段中值的前面。这仅发生在 IE 中,而不发生在例如铬合金。我已经看到,对于一些程序员来说,光标会跳到值的后面。所以我认为我的光标跳到前面的原因是因为该值在输入字段中向右而不是向左对齐。这是一个情景:

我的第一个输入是 1000 然后我想将其编辑为10003,但结果是 31000

有没有办法控制光标不跳转?

【问题讨论】:

    标签: reactjs input text-cursor


    【解决方案1】:

    根据您的问题猜测一下,您的代码很可能与此类似:

    this.setState({value: e.target.value})} />

    如果您的事件使用onBlur 处理,这可能会有所不同,但本质上是相同的问题。这里的行为,许多人称之为 React “错误”,实际上是预期的行为。

    输入控件的值不是控件加载时的初始值,而是绑定到this.state 的底层value。当状态改变时,控件会被 React 重新渲染。

    本质上,这意味着控件由 React 重新创建并由状态的值填充。问题是它无法知道重新创建之前的光标位置。

    我发现解决这个问题的一种方法是在重新渲染之前记住光标位置,如下所示:

    { this.cursor = e.target.selectionStart; this.setState({value: e.target.value}); }} onFocus={(e) => { e.target.selectionStart = this.cursor; }} />

    【讨论】:

    • 用钩子怎么做?
    • @JuliodeLeon - 使用useState 作为cursorPos。但这很糟糕,因为有一些边缘情况需要处理已删除和粘贴的输入。
    • 特别是对于钩子,您可以执行以下操作(基于@abhinav-prabhakar 建议):const inputRef = useRef(null); useEffect(() => { inputRef.current.focus(); }, [inputRef]); 然后<input ref={inputRef} /> 我只是在此处添加它以免混淆答案和脱离问题的上下文,使其更加混乱。
    【解决方案2】:

    这是<input/> 标记的直接替换。它是一个简单的功能组件,使用钩子来保存和恢复光标位置:

    import React, { useEffect, useRef, useState } from 'react';
    
    const ControlledInput = (props) => {
       const { value, onChange, ...rest } = props;
       const [cursor, setCursor] = useState(null);
       const ref = useRef(null);
    
       useEffect(() => {
          const input = ref.current;
          if (input) input.setSelectionRange(cursor, cursor);
       }, [ref, cursor, value]);
    
       const handleChange = (e) => {
          setCursor(e.target.selectionStart);
          onChange && onChange(e);
       };
    
       return <input ref={ref} value={value} onChange={handleChange} {...rest} />;
    };
    
    export default ControlledInput;
    

    【讨论】:

    • 这很漂亮,你是个传奇。我在这里尝试了其他答案,但这不仅确实有效,而且效果很好(没有光标跳跃然后向后移动,非常流畅的用户体验),而且它非常易于使用! ?
    • 请注意,input.setSelecetionRange() 不能与 type="number" 一起使用——如果这是您想要的,我发现我必须使用 type="text" inputMode="numeric"
    • 谢谢,这对我也有用。我在打字稿中使用textarea,所以我不得不change the code一点:)
    【解决方案3】:

    这是我的解决方案:

    import React, { Component } from "react";
    
    class App extends Component {
      constructor(props) {
        super(props);
        this.state = {
          name: ""
        };
    
        //get reference for input
        this.nameRef = React.createRef();
    
        //Setup cursor position for input
        this.cursor;
      }
    
      componentDidUpdate() {
        this._setCursorPositions();
      }
    
      _setCursorPositions = () => {
        //reset the cursor position for input
        this.nameRef.current.selectionStart = this.cursor;
        this.nameRef.current.selectionEnd = this.cursor;
      };
    
      handleInputChange = (key, val) => {
        this.setState({
          [key]: val
        });
      };
    
      render() {
        return (
          <div className="content">
            <div className="form-group col-md-3">
              <label htmlFor="name">Name</label>
              <input
                ref={this.nameRef}
                type="text"
                autoComplete="off"
                className="form-control"
                id="name"
                value={this.state.name}
                onChange={event => {
                  this.cursor = event.target.selectionStart;
                  this.handleInputChange("name", event.currentTarget.value);
                }}
              />
            </div>
          </div>
        );
      }
    }
    
    export default App;
    
    

    【讨论】:

      【解决方案4】:

      这是一个简单的解决方案。为我工作。

      <Input
      ref={input=>input && (input.input.selectionStart=input.input.selectionEnd=this.cursor)}
      value={this.state.inputtext} 
      onChange={(e)=>{
      this.cursor = e.target.selectionStart;
      this.setState({inputtext: e.target.value})
      />
      

      解释:

      我们在这里所做的是将光标位置保存在 onChange() 中,现在当标签由于状态值的变化而重新渲染时,执行 ref 代码,并在 ref 代码内部恢复光标位置。

      【讨论】:

        【解决方案5】:

        如果您使用的是 textarea,那么这里是基于 Daniel Loiterton's code 使用 TypeScript 的钩子:

        interface IControlledTextArea {
            value: string
            onChange: ChangeEventHandler<HTMLTextAreaElement> | undefined
            [x: string]: any
        }
        
        const ControlledTextArea = ({ value, onChange, ...rest }: IControlledTextArea) => {
            const [cursor, setCursor] = useState(0)
            const ref = useRef(null)
        
            useEffect(() => {
                const input: any = ref.current
                if (input) {
                    input.setSelectionRange(cursor, cursor)
                }
            }, [ref, cursor, value])
        
            const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
                setCursor(e.target.selectionStart)
                onChange && onChange(e)
            }
        
            return <textarea ref={ref} value={value} onChange={handleChange} {...rest} />
        }
        

        【讨论】:

          【解决方案6】:

          我的光标总是跳到行尾。这个解决方案似乎解决了这个问题(来自 github):

          import * as React from "react";
          import * as ReactDOM from "react-dom";
          
          class App extends React.Component<{}, { text: string }> {
            private textarea: React.RefObject<HTMLTextAreaElement>;
            constructor(props) {
              super(props);
              this.state = { text: "" };
              this.textarea = React.createRef();
            }
          
            handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
              const cursor = e.target.selectionStart;
              this.setState({ text: e.target.value }, () => {
                if (this.textarea.current != null)
                  this.textarea.current.selectionEnd = cursor;
              });
            }
          
            render() {
              return (
                <textarea
                  ref={this.textarea}
                  value={this.state.text}
                  onChange={this.handleChange.bind(this)}
                />
              );
            }
          }
          
          ReactDOM.render(<App />, document.getElementById("root"));
          

          【讨论】:

            【解决方案7】:

            这是我的解决方案

            const Input = () => {
                const [val, setVal] = useState('');
                const inputEl = useRef(null);
            
                const handleInputChange = e => {
                  const { value, selectionEnd } = e.target;
                  const rightCharsCount = value.length - selectionEnd;
                  const formattedValue = parseInt(value.replace(/\D/g, ''), 10).toLocaleString();
                  const newPosition = formattedValue.length - rightCharsCount;
            
                  setVal(formattedValue);
            
                  setTimeout(() => {
                    inputEl.current.setSelectionRange(newPosition, newPosition);
                  }, 0);
                };
            
                return <input ref={inputEl} value={val} onChange={handleInputChange} />;
            };
            

            【讨论】:

              【解决方案8】:
              // Here is a custom hook to overcome this problem:
              
              import { useRef, useCallback, useLayoutEffect } from 'react'
              /**
               * This hook overcomes this issue {@link https://github.com/reduxjs/react-redux/issues/525}
               * This is not an ideal solution. We need to figure out why the places where this hook is being used
               * the controlled InputText fields are losing their cursor position when being remounted to the DOM
               * @param {Function} callback - the onChangeCallback for the inputRef
               * @returns {Function} - the newCallback that fixes the cursor position from being reset
               */
              const useControlledInputOnChangeCursorFix = callback => {
                const inputCursor = useRef(0)
                const inputRef = useRef(null)
              
                const newCallback = useCallback(
                  e => {
                    inputCursor.current = e.target.selectionStart
                    if (e.target.type === 'text') {
                      inputRef.current = e.target
                    }
                    callback(e)
                  },
                  [callback],
                )
              
                useLayoutEffect(() => {
                  if (inputRef.current) {
                    inputRef.current.setSelectionRange(inputCursor.current, inputCursor.current)
                  }
                })
              
                return newCallback
              }
              
              export default useControlledInputOnChangeCursorFix
              
              // Usage:
              
              import React, { useReducer, useCallback } from 'react'
              import useControlledInputOnChangeCursorFix from '../path/to/hookFolder/useControlledInputOnChangeCursorFix'
              
              // Mimics this.setState for a class Component
              const setStateReducer = (state, action) => ({ ...state, ...action })
              
              const initialState = { street: '', address: '' }
              
              const SomeComponent = props => {
                const [state, setState] = useReducer(setStateReducer, initialState)
              
                const handleOnChange = useControlledInputOnChangeCursorFix(
                  useCallback(({ target: { name, value } }) => {
                    setState({ [name]: value })
                  }, []),
                )
              
                const { street, address } = state
              
                return (
                  <form>
                    <input name='street' value={street} onChange={handleOnChange} />
                    <input name='address' value={address} onChange={handleOnChange} />
                  </form>
                )
              }
              

              【讨论】:

                【解决方案9】:

                我尝试了上述所有解决方案,但没有一个对我有用。相反,我更新了 onKeyUp React 合成事件类型上的 e.currentTarget.selectionStarte.currentTarget.selectionEnd。例如:

                const [cursorState, updateCursorState] = useState({});
                const [formState, updateFormState] = useState({ "email": "" });
                
                const handleOnChange = (e) => {
                    // Update your state & cursor state in your onChange handler
                    updateCursorState(e.target.selectionStart);
                    updateFormState(e.target.value);
                }
                
                <input
                    name="email"
                    value={formState.email}
                    onChange={(e) => handleOnChange(e)}
                    onKeyUp={(e) => {
                        // You only need to update your select position in the onKeyUp handler:
                        e.currentTarget.selectionStart = cursorState.cursorPosition;
                        e.currentTarget.selectionEnd = cursorState.cursorPosition;
                    }}
                />
                

                另外,请注意selectionStartselectionEnd getter 不适用于email 类型的输入字段。

                【讨论】:

                  【解决方案10】:

                  对于在react-native-web 中遇到此问题的任何人,这里是用 TypeScript 编写的解决方案

                  const CursorFixTextInput = React.forwardRef((props: TextInputProps, refInput: ForwardedRef<TextInput>) => {
                      if(typeof refInput === "function") {
                          console.warn("CursorFixTextInput needs a MutableRefObject as reference to work!");
                          return <TextInput key={"invalid-ref"} {...props} />;
                      }
                  
                      if(!("HTMLInputElement" in self)) {
                          return <TextInput key={"no-web"} {...props} />;
                      }
                  
                      const { value, onChange, ...restProps } = props;
                      const defaultRefObject = useRef<TextInput>(null);
                      const refObject: RefObject<TextInput> = refInput || defaultRefObject;
                      const [ selection, setSelection ] = useState<SelectionState>(kInitialSelectionState);
                  
                      useEffect(() => {
                          if(refObject.current instanceof HTMLInputElement) {
                              refObject.current.setSelectionRange(selection.start, selection.end);
                          }
                      }, [ refObject, selection, value ]);
                  
                      return (
                          <TextInput
                              ref={refObject}
                              value={value}
                              onChange={event => {
                                  const eventTarget = event.target as any;
                                  if(eventTarget instanceof HTMLInputElement) {
                                      setSelection({
                                          start: eventTarget.selectionStart,
                                          end: eventTarget.selectionEnd
                                      });
                                  }
                  
                                  if(onChange) {
                                      onChange(event);
                                  }
                              }}
                              {...restProps}
                          />
                      )
                  });
                  

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 2021-12-21
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2011-08-13
                    相关资源
                    最近更新 更多