0

I know there are a lot of similar questions, but I haven't found exactly my case.

Using vanilla JS there is an input control in which a user types something. A date as an example (but could be a phone number or anything else with a fixed format). The input data is validated using element's change event. So when a user finishes (by pressing enter or leaving the control, or submitting, etc.) a validation occurs and if something is wrong an error message is shown.

For a good UX the validation error is cleared once a user starts typing again (i.e. tries to edit the error). This is needed so the user is not confused that data is 'invalid' while he types it because he hasn't finished yet. And when he does finish typing, data is revalidated again. We are not doing real-time validation since it looks confusing ("am I typing already invalid data??").

For example, typing unfinished date like 12.12 (without a year) will result in validation error. And when a user starts typing again, the error is cleared until user is finished.

Now consider a case:

  1. user types 12.12;
  2. presses enter;
  3. validation starts and results in an error;
  4. users clears input and types 12.12 again;
  5. presses enter;
  6. no validation occurs, since input element sees no changes in it's value, hence no change event.

So the question is, how to make input element believe that data is actually changed so the event is fired again when user finishes editing?

Not sure if emulating change event is a good idea (e.g. by dispatching it manually in blur or keypress=enter or anything similar).

I'm looking for something like an 'optimization' flag for input that when disabled will force it to dispatch change event regardless of actually changed value. Or something like invalidateElementValue that could be called inside element's input event.

15
  • You could use the "blur" event to run validation when the element loses focus. Commented Nov 12, 2023 at 15:44
  • "Not sure if emulating change event is a good idea": Why use the change event in the first place? Based on your description, it seems like it's not the right choice. Are you sure about how it works and which conditions you want to accept as "finalization" states? Commented Nov 12, 2023 at 15:51
  • A good user experience either does the validation on (form-)data submit or it silently does a non ennoying background validation wit every occurring input-event. Regarding validation, the change-event almost always is of no good use. Commented Nov 12, 2023 at 16:20
  • @Pointy unfortunately blur is not triggered when a user presses enter to finish editing without changing focus. Commented Nov 13, 2023 at 5:54
  • @jsejcksn change is triggered when a user actually finishes editing (changing focus, pressing enter, using auto-fill, etc.). Commented Nov 13, 2023 at 5:55

1 Answer 1

0

From some of the OP's and my above comments ...

A good user experience either does the validation on (form-)data submit or it silently does a non enoying background validation wit every occurring input-event. Regarding validation, the change-event almost always is of no good use. – Peter Seliger

@PeterSeliger background validation during typing is not a good UX since it confuses user that data he types right now is already invalid. Which event could you suggest in this case? – Kasbolat Kumakhov

As I proposed/stated, validation happens at either (form-)data submit or with every input-event or even at both event types. A good user experience comes with the kind and manner of how one interferes with a user's expectations. Thus, in order to provide the correct information, when hopefully needed, in the most supportive and least annoying way, one has to come up with some complex event- and data-handling. But this does not change anything about the suggested event-types.

Note

The beneath posted code is not a suggestion of how the OP's problem has to be solved and validation needs to be done. It is just meant to be a demonstrator in order to show the complexity level needed when it comes to gathering the correct data upon which all UX decisions are going to be made.

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>

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for such detailed post. Unfortunately the provided solution is not comfortable for users since it doesn't provide validation messages at the right time (e.g. after a user finished inputting data with either focus change or pressing enter). And also the sample shows that data is cleared after validation (e.g. when inputting 12.45 and pressing enter and clicking the field again the input is cleared and no validation happens when tabbing). We have decided to go with 'emulation' route.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.