当值被重新键入但未更改时,如何强制JS输入的“change”事件?

How to force JS input's 'change' event when value was retyped but not changed?

提问人:Kasbolat Kumakhov 提问时间:11/12/2023 更新时间:11/14/2023 访问量:109

问:

我知道有很多类似的问题,但我还没有找到确切的情况。

使用vanilla JS,有一个用户在其中键入内容的控件。以日期为例(但可以是电话号码或其他任何具有固定格式的内容)。使用元素的事件验证输入数据。因此,当用户完成(通过按下或离开控件或提交等)时,将进行验证,如果出现错误,则会显示错误消息。inputchangeenter

为了获得良好的用户体验,一旦用户再次开始键入(即尝试编辑错误),验证错误就会被清除。这是必需的,这样用户就不会在键入数据时因为尚未完成而混淆数据是“无效”的。当他完成输入时,数据将再次重新验证。我们没有进行实时验证,因为它看起来很混乱(“我输入的数据是否已经无效?”)。

例如,键入未完成的日期(没有年份)将导致验证错误。当用户再次开始键入时,错误将被清除,直到用户完成。12.12

现在考虑一个案例:

  1. 用户类型12.12;
  2. 印刷机enter;
  3. 验证开始并导致错误;
  4. 用户清除输入并再次键入;12.12
  5. 印刷机enter;
  6. 不会发生验证,因为元素的值没有变化,因此没有事件。inputchange

那么问题来了,如何让元素相信数据实际上已经改变,以便在用户完成编辑时再次触发事件?input

不确定模拟事件是否是一个好主意(例如,通过手动调度它或类似的东西)。changeblurkeypress=enter

我正在寻找类似“优化”标志的东西,当禁用时,无论实际更改的值如何,都会强制它调度事件。或者类似的东西可以称为 inside element 的事件。inputchangeinvalidateElementValueinput

JavaScript 表单 验证 HTML 输入

评论

0赞 Pointy 11/12/2023
您可以使用“blur”事件在元素失去焦点时运行验证。
0赞 jsejcksn 11/12/2023
"不确定模拟变更事件是否是个好主意“:为什么首先使用变更事件?根据您的描述,这似乎不是正确的选择。您确定它是如何工作的以及您希望接受哪些条件作为“最终确定”状态吗?
0赞 Peter Seliger 11/13/2023
良好的用户体验要么对(表单)数据提交进行验证,要么对每个发生的输入事件进行静默的后台验证。关于验证,-event 几乎总是没有用处。change
0赞 Kasbolat Kumakhov 11/13/2023
不幸的是,当用户在不改变焦点的情况下按下以完成编辑时,不会触发@Pointy。blurenter
0赞 Kasbolat Kumakhov 11/13/2023
当用户实际完成编辑(更改焦点、按 Enter 键、使用自动填充等)时,将触发@jsejcksn。change

答:

0赞 Peter Seliger 11/14/2023 #1

从一些 OP 和我上面的评论......

良好的用户体验要么对(表单)数据提交进行验证,要么对每个发生的输入事件进行无提示的后台验证。关于验证,-event 几乎总是没有用处。– 彼得·塞利格change

@PeterSeliger打字过程中的后台验证不是一个好的用户体验,因为它会让用户混淆他现在输入的数据已经无效。在这种情况下,您可以建议哪个事件?– 卡斯博拉特·库马霍夫

正如我所建议/陈述的,验证发生在(表单)数据或每个事件上,甚至在两种事件类型上。良好的用户体验取决于如何干扰用户的期望。因此,为了在需要时以最支持和最不令人讨厌的方式提供正确的信息,必须提出一些复杂的事件和数据处理。但这不会改变建议的事件类型。submitinput

注意

下面发布的代码并不是关于如何解决 OP 问题和需要进行验证的建议。它只是一个演示者,以显示收集正确数据时所需的复杂程度,这些数据将依据这些数据做出所有用户体验决策。

function handleInvalidatedRepetition(validationOutput) {
  validationOutput.classList.add('warning');
  validationOutput.value = 'This value has been invalidated before.'
}
function handleFailedValidation(validationRoot, control/*, validationOutput*/) {
  validationRoot.classList.add('validation-failed');
  // validationOutput.value = 'This is an invalid value.';
  control.blur();
}

function clearInvalidatedRepetition(control, validationOutput) {
  const invalidationsLookup = controlRegistry.get(control);
  if (
    invalidationsLookup &&
    !invalidationsLookup.has(control.value) &&
    validationOutput.classList.contains('warning')
  ) {
    validationOutput.classList.remove('warning');

    validationOutput.value = '';
  }
}
function clearValidationStates({ currentTarget: control }) {
  const validationRoot = control.closest('label[data-validation]');
  const validationOutput = validationRoot.querySelector('output');

  const invalidationsLookup = controlRegistry.get(control);

  if (validationRoot.classList.contains('validation-failed')) {
    validationRoot.classList.remove('validation-failed');

    control.value = '';
  }
  clearInvalidatedRepetition(control, validationOutput);
}

function assureNoDotChainedNumbers(evtOrControl) {
  let result;

  const isEvent = ('currentTarget' in evtOrControl);
  const control = isEvent && evtOrControl.currentTarget || evtOrControl;

  const invalidationsLookup = controlRegistry.get(control);
  if (invalidationsLookup) {

    const { value } = control;
    const isValid = !(/\d*(?:\.\d+)+/g).test(value);

    const validationRoot = control.closest('label[data-validation]');
    const validationOutput = validationRoot.querySelector('output');

    clearInvalidatedRepetition(control, validationOutput);

    if (!isEvent) {

      if (!isValid) {
        invalidationsLookup.add(value);

        handleFailedValidation(validationRoot, control, validationOutput);
      }
      result = isValid;

    } else if (!isValid && invalidationsLookup.has(value)) {

      handleInvalidatedRepetition(validationOutput);
    }
  }
  return result;
}

function validateFormData(elmForm) {
  return [...elmForm.elements]
    .filter(control =>
      !(/^(?:fieldset|output)$/).test(control.tagName.toLowerCase())
    )
    .every(control => {
      const validationType =
        control.closest('label[data-validation]')?.dataset.validation ?? '';

      if (!controlRegistry.has(control)) {
        controlRegistry.set(control, new Set);
      }
      return validationLookup[validationType]?.(control) ?? true;
    });
}
function handleFormSubmit(evt) {
  const success = validateFormData(evt.currentTarget);

  if (!success) {
    evt.preventDefault();
  }
  return success;
}


const validationLookup = {
  'no-dot-chained-numbers': assureNoDotChainedNumbers,
};
const eventTypeLookup = {
  'input-text': 'input',
}
const controlRegistry = new WeakMap;


function main() {
  const elmForm = document.querySelector('form');

  [...elmForm.elements]
    .filter(control =>
      !(/^(?:fieldset|output)$/.test(control.tagName.toLowerCase()))
    )
    .forEach(control => {
      const controlName = control.tagName.toLowerCase();
      const controlType = control.type && `-${ control.type }` || '';

      const eventType =
        eventTypeLookup[`${ controlName }${ controlType }`] ?? '';

      const validationType =
        control.closest('label[data-validation]')?.dataset.validation ?? '';

      const validationHandler = validationLookup[validationType];

      if (eventType && validationHandler) {

        control.addEventListener(eventType, validationHandler);
        control.addEventListener('focus', clearValidationStates);
      }
    });

  elmForm.addEventListener('submit', handleFormSubmit);
}
main();
body { margin: 0; }
ul { margin: 4px 0 0 0; }
fieldset { padding: 12px 16px 16px 16px; }
label { padding: 8px 12px 10px 12px; }
code { background-color: #eee; }
.validation-failed {
  outline: 1px dashed red;
  background-color: rgb(255 0 0 / 25%);
}
.warning { color: #ff9000; }
<form>
  <fieldset>
    <legend>No dot chained numbers</legend>

    <label data-validation="no-dot-chained-numbers">
      <span class="label">No dot chained numbers</span>
      <input type="text" placeholder="No dot chained numbers" />
      <output></output>
    </label>

  </fieldset>
</form>

<ul>
  <li>E.g. do type <code>12.45</code>.</li>
  <li>Press <code>&lt;Enter&gt;</code>.</li>
  <li>Focus the <code>input</code> element again.</li>
  <li>Type e.g. another dot chained number sequence.</li>
  <li>
    Maybe repeat the above task sequence by pressing <code>&lt;Enter&gt;</code> again.
  </li>
  <li>Do type input <code>12.45</code> again.</li>
  <li>... Try other stuff; play around ...</li>
</ul>

评论

0赞 Kasbolat Kumakhov 11/15/2023
感谢您提供如此详细的帖子。不幸的是,所提供的解决方案对用户来说并不舒服,因为它没有在正确的时间提供验证消息(例如,在用户通过更改焦点或按 .此外,该示例还显示,数据在验证后被清除(例如,当输入并再次按下并单击该字段时,输入被清除,并且在 ).我们决定走“仿真”路线。enter12.45entertabbing