12

我有一个受控的 React 输入组件,我正在格式化输入,如 onChange 代码中所示。

<input type="TEL" id="applicantCellPhone" onChange={this.formatPhone} name="applicant.cellPhone" value={this.state["applicant.cellPhone"]}/>

然后我的 formatPhone 函数是这样的

formatPhone(changeEvent) {
let val = changeEvent.target.value;
let r = /(\D+)/g,
  first3 = "",
  next3 = "",
  last4 = "";
val = val.replace(r, "");
if (val.length > 0) {
  first3 = val.substr(0, 3);
  next3 = val.substr(3, 3);
  last4 = val.substr(6, 4);
  if (val.length > 6) {
    this.setState({ [changeEvent.target.name]: first3 + "-" + next3 + "-" + last4 });
  } else if (val.length > 3) {
    this.setState({ [changeEvent.target.name]: first3 + "-" + next3 });
  } else if (val.length < 4) {
    this.setState({ [changeEvent.target.name]: first3 });
  }
} else this.setState({ [changeEvent.target.name]: val });

}

当我尝试在中间某处删除/添加一个数字然后光标立即移动到字符串的末尾时,我开始面临这个问题。

我在Sophie的解决方案中看到了一个解决方案,但我认为这不适用于这里,因为 setState 无论如何都会导致渲染。我还尝试通过 setSelectionRange(start, end) 来操纵插入符号的位置,但这也无济于事。我认为导致渲染的 setState 使组件将编辑的值视为最终值并导致光标移动到末尾。

谁能帮我弄清楚如何解决这个问题?

4

5 回答 5

5

onChange单独是不够的。

案例1:如果target.value === 123|456那么您不知道如何'-'删除。与<del>或与<backspace>。所以你不知道结果值和插入符号位置应该是12|4-56or 123-|56

但是,如果您要保存以前的插入符号位置和值怎么办?假设在以前onChange你有

123-|456

现在你有了

123|456

这显然意味着用户按下了<backspace>. 但是来了...

案例2:用户可以通过鼠标改变光标位置。

onKeyDown救援:

function App() {

  const [value, setValue] = React.useState("")

  // to distinguish <del> from <backspace>
  const [key, setKey] = React.useState(undefined)

  function formatPhone(event) {
    const element = event.target
    let   caret   = element.selectionStart
    let   value   = element.value.split("")

    // sorry for magical numbers
    // update value and caret around delimiters
    if( (caret === 4 || caret === 8) && key !== "Delete" && key !== "Backspace" ) {
      caret++
    } else if( (caret === 3 || caret === 7) && key === "Backspace" ) {
      value.splice(caret-1,1)
      caret--
    } else if( (caret === 3 || caret === 7) && key === "Delete" ) {
      value.splice(caret,1);
    }

    // update caret for non-digits
    if( key.length === 1 && /[^0-9]/.test(key) ) caret--

    value = value.join("")
      // remove everithing except digits
      .replace(/[^0-9]+/g, "")
      // limit input to 10 digits
      .replace(/(.{10}).*$/,"$1")
      // insert "-" between groups of digits
      .replace(/^(.?.?.?)(.?.?.?)(.?.?.?.?)$/, "$1-$2-$3")
      // remove exescive "-" at the end
      .replace(/-*$/,"")

    setValue(value);

    // "setTimeout" to update caret after setValue
    window.requestAnimationFrame(() => {
      element.setSelectionRange(caret,caret)
    })
  }  
  return (
    <form autocomplete="off">
      <label for="Phone">Phone: </label>
      <input id="Phone" onChange={formatPhone} onKeyDown={event => setKey(event.key)} name="Phone" value={value}/>
    </form>
  )
}

密码箱

您可能还对该任务的某些库感兴趣。例如https://github.com/nosir/cleave.js但它移动插入符号的方式可能不符合您的口味。无论如何,它可能不是唯一的图书馆。

于 2020-04-23T22:19:46.717 回答
4

恐怕如果您放弃对 React 的控制,状态更改将不可避免地丢弃插入符号的位置,因此唯一的解决方案是自己处理它。

鉴于您的字符串操作并不是那么简单,因此保留“当前位置”最重要的是......

为了尝试更好地解决问题,我使用 react hooks 构建了一个解决方案,您可以在其中更好地查看发生了哪些状态更改

function App() {

  const [state, setState] = React.useState({});
  const inputRef = React.useRef(null);
  const [selectionStart, setSelectionStart] = React.useState(0);

  function formatPhone(changeEvent) {

    let r = /(\D+)/g, first3 = "", next3 = "", last4 = "";
    let old = changeEvent.target.value;
    let val = changeEvent.target.value.replace(r, "");

    if (val.length > 0) {
      first3 = val.substr(0, 3);
      next3 = val.substr(3, 3);
      last4 = val.substr(6, 4);
      if (val.length > 6) {
        val = first3 + "-" + next3 + "-" + last4;
      } else if (val.length > 3) {
        val = first3 + "-" + next3;
      } else if (val.length < 4) {
        val = first3;
      }
    }

    setState({ [changeEvent.target.name]: val });

    let ss = 0;
    while (ss<val.length) {
      if (old.charAt(ss)!==val.charAt(ss)) {
        if (val.charAt(ss)==='-') {
            ss+=2;
        }
        break;
      }
      ss+=1;
    }

    setSelectionStart(ss);
  }  

  React.useEffect(function () {
    const cp = selectionStart;
    inputRef.current.setSelectionRange(cp, cp);
  });

  return (
    <form autocomplete="off">
      <label for="cellPhone">Cell Phone: </label>
      <input id="cellPhone" ref={inputRef} onChange={formatPhone} name="cellPhone" value={state.cellPhone}/>
    </form>
  )  
}

ReactDOM.render(<App />, document.getElementById('root'))

链接到代码笔

我希望它有帮助

于 2020-04-20T11:46:41.453 回答
2

通过将光标位置保存在处理程序的开头并在呈现新状态后恢复它,光标位置将始终处于正确位置。

但是,由于添加-会改变光标位置,所以需要考虑它对初始位置的影响

import React, { useRef, useState, useLayoutEffect } from "react";

export default function App() {
  const [state, setState] = useState({ phone: "" });
  const cursorPos = useRef(null);
  const inputRef = useRef(null);
  const keyIsDelete = useRef(false);

  const handleChange = e => {
    cursorPos.current = e.target.selectionStart;
    let val = e.target.value;
    cursorPos.current -= (
      val.slice(0, cursorPos.current).match(/-/g) || []
    ).length;
    let r = /(\D+)/g,
      first3 = "",
      next3 = "",
      last4 = "";
    val = val.replace(r, "");
    let newValue;
    if (val.length > 0) {
      first3 = val.substr(0, 3);
      next3 = val.substr(3, 3);
      last4 = val.substr(6, 4);
      if (val.length > 6) {
        newValue = first3 + "-" + next3 + "-" + last4;
      } else if (val.length > 3) {
        newValue = first3 + "-" + next3;
      } else if (val.length < 4) {
        newValue = first3;
      }
    } else newValue = val;
    setState({ phone: newValue });
    for (let i = 0; i < cursorPos.current; ++i) {
      if (newValue[i] === "-") {
        ++cursorPos.current;
      }
    }
    if (newValue[cursorPos.current] === "-" && keyIsDelete.current) {
      cursorPos.current++;
    }
  };

  const handleKeyDown = e => {
    const allowedKeys = [
      "Delete",
      "ArrowLeft",
      "ArrowRight",
      "Backspace",
      "Home",
      "End",
      "Enter",
      "Tab"
    ];
    if (e.key === "Delete") {
      keyIsDelete.current = true;
    } else {
      keyIsDelete.current = false;
    }
    if ("0123456789".includes(e.key) || allowedKeys.includes(e.key)) {
    } else {
      e.preventDefault();
    }
  };

  useLayoutEffect(() => {
    if (inputRef.current) {
      inputRef.current.selectionStart = cursorPos.current;
      inputRef.current.selectionEnd = cursorPos.current;
    }
  });

  return (
    <div className="App">
      <input
        ref={inputRef}
        type="text"
        value={state.phone}
        placeholder="phone"
        onChange={handleChange}
        onKeyDown={handleKeyDown}
      />
    </div>
  );
}

在上面的代码中,这些部分将保存位置:

    cursorPos.current = e.target.selectionStart;
    let val = e.target.value;
    cursorPos.current -= (
      val.slice(0, cursorPos.current).match(/-/g) || []
    ).length;

这些将恢复它:

    for (let i = 0; i < cursorPos.current; ++i) {
      if (newValue[i] === "-") {
        ++cursorPos.current;
      }
    }

还有一个微妙的事情是,通过使用useState({phone:""})我们确保输入会重新渲染,因为它总是设置一个新对象。

CodeSandbox 示例是https://codesandbox.io/s/tel-formating-m1cg2?file=/src/App.js

于 2020-04-26T18:58:42.917 回答
2

您尝试的解决方案应该有效。

请注意 - 在反应中,状态是异步更新的。要在状态更新完成后立即执行您需要执行的操作,请使用setState.

根据文档

setState() 的第二个参数是一个可选的回调函数,一旦 setState 完成并重新渲染组件,就会执行该回调函数。

所以只需编写一个内联函数来做setSelectionRange并将它作为第二个参数传递给setState

像这样

...
this.setState({
    [changeEvent.target.name]: first3 + "-" + next3 + "-" + last4
},
    () => changeEvent.target.setSelectionRange(caretStart, caretEnd)
);
...

代码的工作副本在这里:

https://codesandbox.io/s/input-cursor-issue-4b7yg?file=/src/App.js

于 2020-04-21T03:10:20.943 回答
-1

您只需在 formatPhone 函数中添加以下行

if (!(event.keyCode == 8 || event.keyCode == 37 || event.keyCode == 39))

将此 if 条件添加到用 formatPhone 函数编写的整个代码中。

于 2022-01-06T14:26:23.620 回答