Jamie736
Jamie736

Reputation: 35

Backspace and keyup working altogether weirdly

I am trying to set the minimum value of an input type number with jQuery. It is working fine when increasing/decreasing the value using buttons but when entering a custom value with the keyboard, it is messing up.

For instance, min value is set to 5, try to remove 5 and type 8, and it will become 58 instead of 8. type 13 and it will become 513.

Live demo: https://jsfiddle.net/wnaLf3vt/

$('input[type=number].cart__product-qty').on('mouseup keydown', function() {
  $(this).val(Math.min(10000, Math.max(5, $(this).val())));
});
// To prevent the button trigger the form submission

$('form').on('click', 'button:not([type="submit"])', function(e) {
  e.preventDefault();
})
input[type="number"] {
  -webkit-appearance: textfield;
  -moz-appearance: textfield;
  appearance: textfield;
}

input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
  -webkit-appearance: none;
}

.number-input {
  border: 2px solid #ddd;
  display: inline-flex;
}

.number-input,
.number-input * {
  box-sizing: border-box;
}

.number-input button {
  outline: none;
  -webkit-appearance: none;
  background-color: transparent;
  border: none;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  cursor: pointer;
  margin: 0;
  position: relative;
}

.number-input button:before,
.number-input button:after {
  display: inline-block;
  position: absolute;
  content: '';
  width: 1rem;
  height: 2px;
  background-color: #212121;
  transform: translate(-50%, -50%);
}

.number-input button.plus:after {
  transform: translate(-50%, -50%) rotate(90deg);
}

.number-input input[type=number] {
  font-family: sans-serif;
  max-width: 5rem;
  padding: .5rem;
  border: solid #ddd;
  border-width: 0 2px;
  font-size: 2rem;
  height: 3rem;
  font-weight: bold;
  text-align: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<form>
  <div class="number-input">
    <button onclick="this.parentNode.querySelector('input[type=number].cart__product-qty').stepDown()"></button>
    <input type="number" class="cart__product-qty" value="5" min="5">
    <button onclick="this.parentNode.querySelector('input[type=number].cart__product-qty').stepUp()" class="plus"></button>
  </div>
</form>

enter image description here

Upvotes: 1

Views: 59

Answers (4)

jsejcksn
jsejcksn

Reputation: 33701

I think you'll need to react to multiple event types in order to get the experience that you seem to want.

It seems as though the core of your problem relates to understanding the correlation between the value states of the number input in relation to different event types (e.g. blur, change, input, keyup).

I've composed a demonstration to help you visualize the HTMLInputElement.value and HTMLInputElement.valueAsNumber properties of a number input in response to each of the event types mentioned above: the most recent values for all event types will be displayed to make it easy to compare them visually.

You can select "Full page" after running the code snippet demo to expand the iframe to the full size of your viewport

<h1>Visualizing event data for <code>&lt;input type="number"&gt;</code></h1>
<pre><code class="output"></code></pre>
<input type="number" value="0" />

<!-- Just some styles for this demo -->
<style>body { --slight-contrast: hsla(0, 0%, 50%, 0.15); font-family: sans-serif; } h1 { font-size: 2rem; } pre { background-color: var(--slight-contrast); } code { background-color: var(--slight-contrast); border-radius: 0.25em; font-family: monospace; padding: 0.1em; } pre > code { background-color: transparent; padding: 0; } input[type="number"], pre { font-size: 1rem; padding: 0.5em; }</style>

<script>
  'use strict';

  const input = document.querySelector('input[type="number"]');
  const output = document.querySelector('code.output');
  const eventTypes = ['blur', 'change', 'input', 'keyup'];

  window.addEventListener('EXAMPLE_DATA_READY', ({detail: initiailze}) => {
    // Listen to each of the events on the input, and update the output after every event
    initialize(input, output, eventTypes);
  });
</script>

<!-- Feel free to ignore this script: it's just for formatting text and updating the output: -->
<script>
  'use strict';

  const encoding = {
    prefix: '%ENCODED_NUMBER',
    suffix: '%',
    decode (str) {
      return str.slice(this.prefix.length, this.suffix.length * -1);
    },
    get regexp () {
      return new RegExp(`"${this.prefix}.+${this.suffix}"`);
    },
    wrap (input) {
      return `${this.prefix}${Array.isArray(input) ? input[0] : input}${this.suffix}`;
    },
  };

  function encodeIfInfinite (n) {
    if (Number.isFinite(n)) return n;
    if (Number.isNaN(n)) return encoding.wrap`NaN`;
    if (Number.POSITIVE_INFINITY === n) return encoding.wrap`Infinity`;
    if (Number.NEGATIVE_INFINITY === n) return encoding.wrap`-Infinity`;
    throw new Error('Unexpected input');
  }

  const valueHistory = {
    toString () {
      const result = {};
      const iter = Object.entries(this);
      for (const [key, value] of iter) {
        if (!['function', 'object'].includes(typeof value)) {
          result[key] = value;
          continue;
        }

        const o = (result[key] ??= {});
        const iter = Object.entries(value);
        for (const [key, value] of iter) {
          o[key] = typeof value === 'number' ? encodeIfInfinite(value) : value;
        }
      }

      return JSON.stringify(result, null, 2).replaceAll(
        new RegExp(encoding.regexp, 'g'),
        str => encoding.decode(str).slice(1, -1),
      );
    },
  };

  function updateHistory (ev) {
    const {target: {value, valueAsNumber}, type} = ev;
    valueHistory[type] = {value, valueAsNumber};
  }

  function createUpdateFn (element) {
    return ev => {
      updateHistory(ev);
      element.textContent = valueHistory.toString();
    };
  }

  function initialize (input, output, eventTypes) {
    const update = createUpdateFn(output);

    for (const type of eventTypes) {
      input.addEventListener(type, update);
      // Initialize history:
      update({target: input, type});
    }
  }

  window.dispatchEvent(new CustomEvent('EXAMPLE_DATA_READY', {detail: initialize}));
</script>

Upvotes: 1

Twisty
Twisty

Reputation: 30893

Consider the following example.

$(function() {
  $.fn.stepDown = function() {
    var v = parseInt($(this).val());
    if ((v - 1) < parseInt($(this).attr("min"))) {
      return false;
    }
    var s = parseInt($(this).attr("step")) || 1;
    $(this).val(v - s);
  }
  
  $.fn.stepUp = function() {
    var v = parseInt($(this).val());
    var s = parseInt($(this).attr("step")) || 1;
    $(this).val(v + s);
  }

  $(".number-input button").click(function(e) {
    e.preventDefault();
    if ($(this).hasClass("plus")) {
      $(".number-input input").stepUp();
    } else {
      $(".number-input input").stepDown();
    }
  });

  $(".number-input input").change(function(e) {
    var v = parseInt($(this).val());
    var m = parseInt($(this).attr("min"));
    if (v < m) {
      $(this).val(m);
    }
  })
});
input[type="number"] {
  -webkit-appearance: textfield;
  -moz-appearance: textfield;
  appearance: textfield;
}

input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
  -webkit-appearance: none;
}

.number-input {
  border: 2px solid #ddd;
  display: inline-flex;
}

.number-input,
.number-input * {
  box-sizing: border-box;
}

.number-input button {
  outline: none;
  -webkit-appearance: none;
  background-color: transparent;
  border: none;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  cursor: pointer;
  margin: 0;
  position: relative;
}

.number-input button:before,
.number-input button:after {
  display: inline-block;
  position: absolute;
  content: '';
  width: 1rem;
  height: 2px;
  background-color: #212121;
  transform: translate(-50%, -50%);
}

.number-input button.plus:after {
  transform: translate(-50%, -50%) rotate(90deg);
}

.number-input input[type=number] {
  font-family: sans-serif;
  max-width: 5rem;
  padding: .5rem;
  border: solid #ddd;
  border-width: 0 2px;
  font-size: 2rem;
  height: 3rem;
  font-weight: bold;
  text-align: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<form>
  <div class="number-input">
    <button></button>
    <input type="number" class="cart__product-qty" value="59" min="5" />
    <button class="plus"></button>
  </div>
</form>

If a User enters 3 or 1 and then moves away from field, it will be changed to the Min. The user can still enter 41 or 10 manually as the check will not happen until change is triggered.

Also, input elements always contain a String value. Casting it to an integer can help ensure proper comparisons and Math.

Upvotes: 1

War10ck
War10ck

Reputation: 12508

I think the problem may be that the code checking for the minimum value is running on each keydown event rather than after input is complete. Try making the following modification:

Change: mouseup keydown to mouseup blur in your jQuery event handler.

Demo:

$('input[type=number].cart__product-qty').on('mouseup blur', function () {
  $(this).val(Math.min(10000, Math.max(5, $(this).val())));
});
  
$('form').on('click', 'button:not([type="submit"])', function(e){
  e.preventDefault();
})
input[type="number"] {
  -webkit-appearance: textfield;
  -moz-appearance: textfield;
  appearance: textfield;
}

input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
  -webkit-appearance: none;
}

.number-input {
  border: 2px solid #ddd;
  display: inline-flex;
}

.number-input,
.number-input * {
  box-sizing: border-box;
}

.number-input button {
  outline:none;
  -webkit-appearance: none;
  background-color: transparent;
  border: none;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  cursor: pointer;
  margin: 0;
  position: relative;
}

.number-input button:before,
.number-input button:after {
  display: inline-block;
  position: absolute;
  content: '';
  width: 1rem;
  height: 2px;
  background-color: #212121;
  transform: translate(-50%, -50%);
}

.number-input button.plus:after {
  transform: translate(-50%, -50%) rotate(90deg);
}

.number-input input[type=number] {
  font-family: sans-serif;
  max-width: 5rem;
  padding: .5rem;
  border: solid #ddd;
  border-width: 0 2px;
  font-size: 2rem;
  height: 3rem;
  font-weight: bold;
  text-align: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<form>
    <div class="number-input">
        <button onclick="this.parentNode.querySelector('input[type=number].cart__product-qty').stepDown()"></button>
        <input type="number" class="cart__product-qty" value="59" min="5">
        <button onclick="this.parentNode.querySelector('input[type=number].cart__product-qty').stepUp()" class="plus"></button>
    </div>
</form>

Notice that now when you leave the <input /> via a click outside or tab out, the validation rule runs once and the input is correctly updated.

Upvotes: 0

dave
dave

Reputation: 64657

When you delete it, the next time you press a key, before it is updated, you are calling

$(this).val(Math.min(10000, Math.max(5, $(this).val())));

which is effectively:

console.log(Math.min(10000, Math.max(5, '')))

or, 5. So it becomes 5, then gets the next number appended, so then when you press e.g. 8, it becomes 58.

A simple fix is:

if ($(this).val() !== '') {
    $(this).val(Math.min(10000, Math.max(5, $(this).val())));
}

$('input[type=number].cart__product-qty').on('mouseup keydown', function() {
  
  if ($(this).val() !== '') {
    $(this).val(Math.min(10000, Math.max(5, $(this).val())));
  }
});
// To prevent the button trigger the form submission

$('form').on('click', 'button:not([type="submit"])', function(e) {
  e.preventDefault();
})
input[type="number"] {
  -webkit-appearance: textfield;
  -moz-appearance: textfield;
  appearance: textfield;
}

input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
  -webkit-appearance: none;
}

.number-input {
  border: 2px solid #ddd;
  display: inline-flex;
}

.number-input,
.number-input * {
  box-sizing: border-box;
}

.number-input button {
  outline: none;
  -webkit-appearance: none;
  background-color: transparent;
  border: none;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  cursor: pointer;
  margin: 0;
  position: relative;
}

.number-input button:before,
.number-input button:after {
  display: inline-block;
  position: absolute;
  content: '';
  width: 1rem;
  height: 2px;
  background-color: #212121;
  transform: translate(-50%, -50%);
}

.number-input button.plus:after {
  transform: translate(-50%, -50%) rotate(90deg);
}

.number-input input[type=number] {
  font-family: sans-serif;
  max-width: 5rem;
  padding: .5rem;
  border: solid #ddd;
  border-width: 0 2px;
  font-size: 2rem;
  height: 3rem;
  font-weight: bold;
  text-align: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<form>
  <div class="number-input">
    <button onclick="this.parentNode.querySelector('input[type=number].cart__product-qty').stepDown()"></button>
    <input type="number" class="cart__product-qty" value="5" min="5">
    <button onclick="this.parentNode.querySelector('input[type=number].cart__product-qty').stepUp()" class="plus"></button>
  </div>
</form>

Upvotes: 0

Related Questions