Search code examples
javascriptdomdom-events

Programmatically setting the date input's value in a change event handler triggers change event in Firefox


Update (22nd April 2024):

The bug has now been patched in Firefox version 125. Bug Report

Original Problem/Issue

I want to render an error whenever someone chooses a date that is off day (like Saturday or Sunday, it may be any one of the day from an array of closing days).

I have HTML:

<div>
  <input type="date" id="date-input">
</div>

<br>

<div id="error-container">
</div>

CSS:

.d-error{
  background: orangered;
  color: white;
  padding: 4px 8px;
}

Here is the JavaScript:

'use strict';

const dateInput = document.getElementById('date-input');
const errorContainer = document.getElementById('error-container');
const closingDays = [0, 6];
const weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday','Friday', 'Saturday'];

const displayError = function ({ parentEl, message, position = 'prepend', durationSec = 0 }) {
  const errorElement = document.createElement('p');
  errorElement.classList.add('d-error');
  errorElement.textContent = message;
  parentEl[position](errorElement);

  if (durationSec) {
    setTimeout(() => {
      errorElement.remove();
    }, durationSec * 1000);
  }
};

const handleDateChange = function (event) {
  const selectedDay = new Date(event.currentTarget.value).getDay();

  if (closingDays.includes(selectedDay)) {
    displayError({
      parentEl: errorContainer,
      message: `We are closed on ${weekDays[selectedDay]}. Please select another day.`,
      durationSec: 5
    });

    event.currentTarget.value = '';
  }
};

dateInput.addEventListener('change', handleDateChange);

This works, but the error is rendered twice. Once due to user change and once due to programmatically changing the value of the input event.currentTarget.value = '';. So, how to temporarily remove the change event listener and reattach to it? Or any other way to solve this issue?

I tried:

const handleDateChange = function(event) {
  ...

  event.currentTarget.removeEventListener('change', handleDateChange);
  event.currentTarget.value = '';  
  event.currentTarget.addEventListener('change', handleDateChange);

  ...
}

But this did not fix the issue.

Update:

Firstly, as pointed out by @Sebastian, programmatically setting the input's value does not trigger a change event. So there must be something else that is causing the event to trigger twice.

I am extremely sorry for not testing out in other browsers other than Firefox. The issue seems to only be in Firefox as in chromium based browser, everything works as expected (works in Firefox android though). So, I am guessing it is some kind of bug in Firefox, which I alone cannot confirm. Please try running the code snippet in different browsers.

'use strict';

const dateInput = document.getElementById('date-input');
const errorContainer = document.getElementById('error-container');
const closingDays = [0, 6];
const weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

const displayError = function({
  parentEl,
  message,
  position = 'prepend',
  durationSec = 0
}) {
  const errorElement = document.createElement('p');
  errorElement.classList.add('d-error');
  errorElement.textContent = message;
  parentEl[position](errorElement);

  if (durationSec) {
    setTimeout(() => {
      errorElement.remove();
    }, durationSec * 1000);
  }
};

const handleDateChange = function(event) {
  const selectedDay = new Date(event.currentTarget.value).getDay();

  if (closingDays.includes(selectedDay)) {
    displayError({
      parentEl: errorContainer,
      message: `We are closed on ${weekDays[selectedDay]}. Please select another day.`,
      durationSec: 5
    });

    event.currentTarget.value = '';
  }
};

dateInput.addEventListener('change', handleDateChange);
.d-error {
  background: orangered;
  color: white;
  padding: 4px 8px;
}
<div>
  <input type="date" id="date-input">
</div>

<br>

<div id="error-container">
</div>


Solution

  • Here is a shorter Version of the snippet to reproduce the bug in Firefox (121). The OP was right that the second event is really somehow caused by programmatically setting the value of the input field inside the event handler (and this should not be the case, according to the first note here in the specs). However, the value printed to the console in the second event is identical to the value of the first event. Furthermore setting the value outside the event handler does not trigger the event at all (as it should be). In the meantime this inconsistent behavior was reported by the OP here on bugzilla (it might be related to this other one).

    function onChange(event) {
      console.log('changed to ' + dateInput.value)
      dateInput.value = ''; // somehow triggers a second event in Firefox
    };
    
    dateInput.addEventListener('change', onChange);
    dateInput.value = '2024-02-07'; // does not trigger the event!
    <div>
      <input type="date" id="dateInput">
    </div>

    Other browsers seem to be fine. In particular Chromium 121 behaves as expected (only one Event). As a workaround for Firefox, the OP's attempt was not far from working. It turns out, that the following works for not listening to the second event (though see better solution below)

    dateInput.removeEventListener('change', onChange);
    dateInput.value = ''; // somehow triggers a second event
    setTimeout(() => dateInput.addEventListener('change', onChange), 0);
    

    It seems that the extra event is not dispatched immediately by Firefox (so not within the same call stack). Therefore adding a timeout before re-attaching the listener helps. Code might be more transparent and performant though, if you use a boolean flag, as suggested by @isherwood.

    As noted in the comments by @AayushKarna , the visible feedback of resetting the input field in the event handler is still missing in Firefox. Therefore, it also has to be set with an extra timeout. But then it doesn't trigger an event in Firefox anymore, so that removing the event-listener becomes superflous. I still leave the above suggestion in the answer, because it was explicitly asked for in the question. Nevertheless, the much simpler workaround-snippet is now:

    function onChange(event) {
      console.log('changed to ' + dateInput.value)
      setTimeout(() => dateInput.value = '', 0);
    };
    
    dateInput.addEventListener('change', onChange);
    <div>
      <input type="date" id="dateInput">
    </div>

    Even in Firefox, the value of the change is then printed only once and the input field is also visibly reset.

    Update: Apparently (according to the bug report) the bug will be fixed in Version 125 of Firefox. Note that the above workaround for prior Versions has still its shortcomings (see comment of @Kaiido below)