5
\$\begingroup\$

The standard HTML range input element suffers from a number of limitations that caused me to implement the web component presented here.

First, it is unnecessarily complicated to style the standard HTML range input element to make it look the same in all web browsers (see here and here).

Second, the standard HTML range input element is designed to be used in a horizontal manner from left to right. Turning it into a vertical slider can be done but is again web browser-specific (see here). In order to invert its minimum and maximum the element needs to be transformed accordingly (see here).

Lastly, the standard HTML range input element does not support visual indications of the values that it can be set to. Such indications are an important aspect of this web component and it is where it derives its name from.

The result of that effort is a dependency-free web component stored in a single file. Its content is shown below:

export const OrientationEnum = Object.freeze({
  LeftToRight: "left-to-right",
  RightToLeft: "right-to-left",
  TopToBottom: "top-to-bottom",
  BottomToTop: "bottom-to-top"
});

const webComponentName      = "summbit-discrete-slider";
const nameTrack             = "track";
const nameMinimum           = "min";
const nameMaximum           = "max";
const nameValue             = "value";
const nameOrientation       = "orientation";
const nameThumb             = "thumb";
const nameDot               = "dot";
const nameDotContainer      = "dot-container";
const keysToIncrement       = ["ArrowUp", "ArrowRight"];
const keysToDecrement       = ["ArrowDown", "ArrowLeft"];
const propertyTrackColor    = `--${webComponentName}-private-track-color`;
const propertyProgressColor = `--${webComponentName}-private-progress-color`;
const propertyThumbDiameter = `--${webComponentName}-private-thumb-diameter`;
const propertyThumbPosition = `--${webComponentName}-private-thumb-position`;
const initialMinimum        = 0;
const initialMaximum        = 5;
const template              = document.createElement("template");
template.innerHTML          = `
<div id="${nameTrack}" class="${nameTrack}-${OrientationEnum.LeftToRight}" tabindex="0">
  <span id="${nameMinimum}" class="${nameMinimum}-${OrientationEnum.LeftToRight}" tabindex="-1"></span>
  <span id="${nameMaximum}" class="${nameMaximum}-${OrientationEnum.LeftToRight}" tabindex="-1"></span>
  <span id="${nameDotContainer}" class="${nameDotContainer}-${OrientationEnum.LeftToRight}" tabindex="-1"></span>
  <span id="${nameThumb}" class="${nameThumb}-${OrientationEnum.LeftToRight}" tabindex="-1">
    <span class="thumb-center-dot" tabindex="-1"></span>
  </span>
</div>
<style>
:host {
  ${propertyTrackColor}: var(--${webComponentName}-track-color, #a8bec9);
  ${propertyProgressColor}: var(--${webComponentName}-progress-color, #0079c9);
  ${propertyThumbDiameter}: var(--${webComponentName}-thumb-diameter, 20px);
  --${webComponentName}-private-thumb-color: var(--${webComponentName}-thumb-color, #ffffff);
  --${webComponentName}-private-track-width: var(--${webComponentName}-track-width, 4px);
  --${webComponentName}-private-track-offset: calc(calc(100% - var(--${webComponentName}-private-track-width)) / 2);
  --${webComponentName}-private-thumb-stroke: calc(var(--${webComponentName}-private-track-width) / 2);
  --${webComponentName}-private-thumb-radius: calc(var(${propertyThumbDiameter}) / 2);
  --${webComponentName}-private-thumb-offset: calc(calc(100% - var(${propertyThumbDiameter})) / 2);
  --${webComponentName}-private-dot-diameter: calc(var(--${webComponentName}-private-track-width) * 2);
  --${webComponentName}-private-dot-container-offset-front-side: calc(calc(var(${propertyThumbDiameter}) - var(--${webComponentName}-private-dot-diameter)) / 2);
  --${webComponentName}-private-dot-container-offset-long-side: calc(calc(100% - var(--${webComponentName}-private-dot-diameter)) / 2);
  all: initial;
  touch-action: none;
  display: inline-block;
  padding: 0;
  margin: 0;
}

.${nameTrack}-${OrientationEnum.LeftToRight},
.${nameTrack}-${OrientationEnum.RightToLeft},
.${nameTrack}-${OrientationEnum.TopToBottom},
.${nameTrack}-${OrientationEnum.BottomToTop} {
  touch-action: none;
  position: relative;
  width: 100%;
  height: 100%;
}

.${nameTrack}-${OrientationEnum.LeftToRight},
.${nameTrack}-${OrientationEnum.RightToLeft} {
  min-width: 150px;
  min-height: var(${propertyThumbDiameter});
}

.${nameTrack}-${OrientationEnum.TopToBottom},
.${nameTrack}-${OrientationEnum.BottomToTop} {
  min-height: 150px;
  min-width: var(${propertyThumbDiameter});
}

.${nameMinimum}-${OrientationEnum.LeftToRight},
.${nameMinimum}-${OrientationEnum.RightToLeft},
.${nameMinimum}-${OrientationEnum.TopToBottom},
.${nameMinimum}-${OrientationEnum.BottomToTop} {
  touch-action: none;
  position: absolute;
  background-color: var(${propertyProgressColor});
}

.${nameMaximum}-${OrientationEnum.LeftToRight},
.${nameMaximum}-${OrientationEnum.RightToLeft},
.${nameMaximum}-${OrientationEnum.TopToBottom},
.${nameMaximum}-${OrientationEnum.BottomToTop} {
  touch-action: none;
  position: absolute;
  background-color: var(${propertyTrackColor});
}

.${nameMinimum}-${OrientationEnum.LeftToRight},
.${nameMaximum}-${OrientationEnum.RightToLeft} {
  top: var(--${webComponentName}-private-track-offset);
  left: var(--${webComponentName}-private-thumb-radius);
  width: var(${propertyThumbPosition});
  height: var(--${webComponentName}-private-track-width);
}

.${nameMinimum}-${OrientationEnum.RightToLeft},
.${nameMaximum}-${OrientationEnum.LeftToRight} {
  top: var(--${webComponentName}-private-track-offset);
  left: calc(var(${propertyThumbPosition}) + var(--${webComponentName}-private-thumb-radius));
  right: var(--${webComponentName}-private-thumb-radius);
  height: var(--${webComponentName}-private-track-width);
}

.${nameMinimum}-${OrientationEnum.TopToBottom},
.${nameMaximum}-${OrientationEnum.BottomToTop} {
  top: var(--${webComponentName}-private-thumb-radius);
  left: var(--${webComponentName}-private-track-offset);
  width: var(--${webComponentName}-private-track-width);
  height: var(${propertyThumbPosition});
}

.${nameMinimum}-${OrientationEnum.BottomToTop},
.${nameMaximum}-${OrientationEnum.TopToBottom} {
  top: calc(var(${propertyThumbPosition}) + var(--${webComponentName}-private-thumb-radius));
  left: var(--${webComponentName}-private-track-offset);
  width: var(--${webComponentName}-private-track-width);
  bottom: var(--${webComponentName}-private-thumb-radius);
}

.${nameDotContainer}-${OrientationEnum.LeftToRight},
.${nameDotContainer}-${OrientationEnum.RightToLeft},
.${nameDotContainer}-${OrientationEnum.TopToBottom},
.${nameDotContainer}-${OrientationEnum.BottomToTop} {
  touch-action: none;
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.${nameDotContainer}-${OrientationEnum.LeftToRight},
.${nameDotContainer}-${OrientationEnum.RightToLeft} {
  top: var(--${webComponentName}-private-dot-container-offset-long-side);
  left: var(--${webComponentName}-private-dot-container-offset-front-side);
  right: var(--${webComponentName}-private-dot-container-offset-front-side);
  bottom: var(--${webComponentName}-private-dot-container-offset-long-side);
}

.${nameDotContainer}-${OrientationEnum.TopToBottom},
.${nameDotContainer}-${OrientationEnum.BottomToTop} {
  top: var(--${webComponentName}-private-dot-container-offset-front-side);
  left: var(--${webComponentName}-private-dot-container-offset-long-side);
  right: var(--${webComponentName}-private-dot-container-offset-long-side);
  bottom: var(--${webComponentName}-private-dot-container-offset-front-side);
}

.${nameDotContainer}-${OrientationEnum.LeftToRight} {
  flex-direction: row;
}

.${nameDotContainer}-${OrientationEnum.RightToLeft} {
  flex-direction: row-reverse;
}

.${nameDotContainer}-${OrientationEnum.TopToBottom} {
  flex-direction: column;
}

.${nameDotContainer}-${OrientationEnum.BottomToTop} {
  flex-direction: column-reverse;
}

.${nameDot} {
  touch-action: none;
  width: var(--${webComponentName}-private-dot-diameter);
  height: var(--${webComponentName}-private-dot-diameter);
  border-radius: 50%;
}

.${nameThumb}-${OrientationEnum.LeftToRight},
.${nameThumb}-${OrientationEnum.RightToLeft},
.${nameThumb}-${OrientationEnum.TopToBottom},
.${nameThumb}-${OrientationEnum.BottomToTop} {
  touch-action: none;
  position: absolute;
  width: var(${propertyThumbDiameter});
  height: var(${propertyThumbDiameter});
  border-radius: 50%;
  border: var(--${webComponentName}-private-thumb-stroke) solid var(${propertyProgressColor});
  box-sizing: border-box;
  background-color: var(--${webComponentName}-private-thumb-color);
  display: flex;
  align-items: center;
  justify-content: center;
}

.${nameThumb}-${OrientationEnum.LeftToRight},
.${nameThumb}-${OrientationEnum.RightToLeft} {
  top: var(--${webComponentName}-private-thumb-offset);
  left: var(${propertyThumbPosition});
}

.${nameThumb}-${OrientationEnum.TopToBottom},
.${nameThumb}-${OrientationEnum.BottomToTop} {
  top: var(${propertyThumbPosition});
  left: var(--${webComponentName}-private-thumb-offset);
}

.thumb-center-dot {
  touch-action: none;
  width: var(--${webComponentName}-private-dot-diameter);
  height: var(--${webComponentName}-private-dot-diameter);
  border-radius: 50%;
  background-color: var(${propertyProgressColor});
}

.${nameTrack}-${OrientationEnum.LeftToRight}:focus,
.${nameTrack}-${OrientationEnum.RightToLeft}:focus,
.${nameTrack}-${OrientationEnum.TopToBottom}:focus,
.${nameTrack}-${OrientationEnum.BottomToTop}:focus,
.${nameMinimum}-${OrientationEnum.LeftToRight}:focus,
.${nameMinimum}-${OrientationEnum.RightToLeft}:focus,
.${nameMinimum}-${OrientationEnum.TopToBottom}:focus,
.${nameMinimum}-${OrientationEnum.BottomToTop}:focus,
.${nameMaximum}-${OrientationEnum.LeftToRight}:focus,
.${nameMaximum}-${OrientationEnum.RightToLeft}:focus,
.${nameMaximum}-${OrientationEnum.TopToBottom}:focus,
.${nameMaximum}-${OrientationEnum.BottomToTop}:focus,
.${nameDotContainer}-${OrientationEnum.LeftToRight}:focus,
.${nameDotContainer}-${OrientationEnum.RightToLeft}:focus,
.${nameDotContainer}-${OrientationEnum.TopToBottom}:focus,
.${nameDotContainer}-${OrientationEnum.BottomToTop}:focus,
.${nameDot}:focus,
.${nameThumb}-${OrientationEnum.LeftToRight}:focus,
.${nameThumb}-${OrientationEnum.RightToLeft}:focus,
.${nameThumb}-${OrientationEnum.TopToBottom}:focus,
.${nameThumb}-${OrientationEnum.BottomToTop}:focus,
.thumb-center-dot:focus {
  outline: none;
}

.${nameTrack}-${OrientationEnum.LeftToRight}:focus-visible,
.${nameTrack}-${OrientationEnum.RightToLeft}:focus-visible,
.${nameTrack}-${OrientationEnum.TopToBottom}:focus-visible,
.${nameTrack}-${OrientationEnum.BottomToTop}:focus-visible {
  outline: 5px auto Highlight;
  outline: 5px auto -webkit-focus-ring-color;
}
</style>
`;

export default class SummbitDiscreteSlider extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot.appendChild(template.content.cloneNode(true));
    this._minimum             = Number.NaN;
    this._maximum             = Number.NaN;
    this._value               = 0;
    this._startValue          = 0;
    this._orientation         = OrientationEnum.LeftToRight;
    this._trackElement        = this.shadowRoot.getElementById(nameTrack);
    this._minimumElement      = this.shadowRoot.getElementById(nameMinimum);
    this._maximumElement      = this.shadowRoot.getElementById(nameMaximum);
    this._dotContainerElement = this.shadowRoot.getElementById(nameDotContainer);
    this._thumbElement        = this.shadowRoot.getElementById(nameThumb);
    this._resizeObserver      = new ResizeObserver(this._resizeHandler.bind(this));
    this.ondragstart          = () => false;
    this.addEventListener("pointerdown", this._pointerDownHandler.bind(this));
    this.addEventListener("keydown", this._keyDownHandler.bind(this));
  }

  static get observedAttributes() {
    return [nameMinimum, nameMaximum, nameValue, nameOrientation];
  }

  attributeChangedCallback(attributeName, oldValue, newValue) {
    if(newValue !== null) {
      switch(attributeName) {
        case nameMinimum:
          this._minimum = parseInt(newValue);
          this._createAllDots();
          this._updateThumbPosition();
          this._restrictValueToMinimum();
          break;
        case nameMaximum:
          this._maximum = parseInt(newValue);
          this._createAllDots();
          this._updateThumbPosition();
          this._restrictValueToMaximum();
          break;
        case nameValue:
          this._value = this._clampValue(parseInt(newValue));
          this._createAllDots();
          this._updateThumbPosition();
          this._updateDotColorsAndValues();
          break;
        case nameOrientation:
          this._orientation = newValue;
          this._updateElementStyle();
          this._createAllDots();
          this._updateThumbPosition();
          this._updateDotColorsAndValues();
          break;
      }
    }
  }

  connectedCallback() {
    this._createAllDots();
    this._resizeObserver.observe(this, { box: "content-box" });
  }

  set min(value) {
    this.setAttribute(nameMinimum, value);
  }

  get min() {
    return this._minimum;
  }

  set max(value) {
    this.setAttribute(nameMaximum, value);
  }

  get max() {
    return this._maximum;
  }

  set value(value) {
    this.setAttribute(nameValue, this._clampValue(value));
  }

  get value() {
    return this._value;
  }

  set orientation(value) {
    this.setAttribute(nameOrientation, value);
  }

  get orientation() {
    return this._orientation;
  }

  _resizeHandler(entries) {
    this._updateThumbPosition();
  }

  _pointerDownHandler(event) {
    this.setPointerCapture(event.pointerId);
    this.onpointermove = this._pointerMoveHandler.bind(this);
    this.onpointerup   = this._pointerUpHandler.bind(this);
    this._startValue   = this._value;
    this._updateValue(event);
  }

  _pointerMoveHandler(event) {
    this._updateValue(event);
  }

  _pointerUpHandler(event) {
    this.onpointermove = null;
    this.onpointerup   = null;
    if(this._startValue != this._value) {
      this._dispatchChangeEvent();
    }
  }

  _keyDownHandler(event) {
    if(keysToIncrement.includes(event.code)) {
      this._incrementValue();
    } else if(keysToDecrement.includes(event.code)) {
      this._decrementValue();
    }
  }

  _createAllDots() {
    this._initializeLimits();
    let currentDotCount  = this._dotContainerElement.childElementCount;
    let requiredDotCount = this._maximum - this._minimum + 1;
    this._createAdditionalDots(requiredDotCount - currentDotCount);
    this._deleteObsoleteDots(currentDotCount - requiredDotCount);
  }

  _initializeLimits() {
    if(isNaN(this._minimum)) {
      this._minimum = initialMinimum;
    }
    if(isNaN(this._maximum)) {
      this._maximum = initialMaximum;
    }
  }

  _createAdditionalDots(dotCount) {
    if(dotCount > 0) {
      for(let i = 0; i < dotCount; i++) {
        this._dotContainerElement.appendChild(this._createDot());
      }
      this._updateDotColorsAndValues();
    }
  }

  _deleteObsoleteDots(dotCount) {
    if(dotCount > 0) {
      for(let i = 0; i < dotCount; i++) {
        this._dotContainerElement.removeChild(this._dotContainerElement.lastChild);
      }
      this._updateDotColorsAndValues();
    }
  }

  _createDot() {
    let dot       = document.createElement("span");
    dot.className = nameDot;
    dot.tabIndex  = -1;
    return dot;
  }

  _updateDotColorsAndValues() {
    let computedStyle = window.getComputedStyle(this);
    let progressColor = computedStyle.getPropertyValue(propertyProgressColor);
    let trackColor    = computedStyle.getPropertyValue(propertyTrackColor);
    let dot, value;
    for(let i = 0; i < this._dotContainerElement.children.length; i++) {
      value                     = i + this._minimum;
      dot                       = this._dotContainerElement.children[i];
      dot.dataset.value         = value;
      dot.style.backgroundColor = value > this._value ? trackColor : progressColor;
    }
  }

  _updateThumbPosition() {
    for(let i = 0; i < this._dotContainerElement.children.length; i++) {
      if(this._dotContainerElement.children[i].dataset.value == this._value) {
        switch(this._orientation) {
          case OrientationEnum.LeftToRight:
          case OrientationEnum.RightToLeft:
            this._trackElement.style.setProperty(propertyThumbPosition, this._dotContainerElement.children[i].offsetLeft + "px", "");
            return;
          case OrientationEnum.TopToBottom:
          case OrientationEnum.BottomToTop:
            this._trackElement.style.setProperty(propertyThumbPosition, this._dotContainerElement.children[i].offsetTop + "px", "");
            return;
        }
      }
    }
  }

  _clampValue(value) {
    if(!isNaN(this._minimum) && value < this._minimum) {
      return this._minimum;
    } else if(!isNaN(this._maximum) && value > this._maximum) {
      return this._maximum;
    }
    return value;
  }

  _restrictValueToMinimum() {
    if(this._value < this._minimum) {
      this.value = this._minimum;
    }
  }

  _restrictValueToMaximum() {
    if(this._value > this._maximum) {
      this.value = this._maximum;
    }
  }

  _incrementValue() {
    if(this._value < this._maximum) {
      this.value += 1;
      this._dispatchInputEvent();
      this._dispatchChangeEvent();
    }
  }

  _decrementValue() {
    if(this._value > this._minimum) {
      this.value -= 1;
      this._dispatchInputEvent();
      this._dispatchChangeEvent();
    }
  }

  _updateValue(event) {
    let index = Math.round(this._calculateProgress(event) * (this._maximum - this._minimum));
    let value = parseInt(this._dotContainerElement.children[index].dataset.value);
    if(this._value != value) {
      this.value = value;
      this._dispatchInputEvent();
    }
  }

  _calculateProgress(event) {
    let thumbDiameter = parseFloat(window.getComputedStyle(this).getPropertyValue(propertyThumbDiameter));
    let thumbRadius   = thumbDiameter / 2;
    let trackLength, position;
    switch(this._orientation) {
      case OrientationEnum.LeftToRight:
        trackLength = this._trackElement.offsetWidth - thumbDiameter;
        position    = event.clientX - thumbRadius - this._trackElement.getBoundingClientRect().left;
        break;
      case OrientationEnum.RightToLeft:
        trackLength = this._trackElement.offsetWidth - thumbDiameter;
        position    = this._trackElement.getBoundingClientRect().right - event.clientX - thumbRadius;
        break;
      case OrientationEnum.TopToBottom:
        trackLength = this._trackElement.offsetHeight - thumbDiameter;
        position    = event.clientY - thumbRadius - this._trackElement.getBoundingClientRect().top;
        break;
      case OrientationEnum.BottomToTop:
        trackLength = this._trackElement.offsetHeight - thumbDiameter;
        position    = this._trackElement.getBoundingClientRect().bottom - event.clientY - thumbRadius;
        break;
    }
    return Math.min(Math.max(position, 0), trackLength) / trackLength;
  }

  _updateElementStyle() {
    this._trackElement       .className = `${nameTrack}-${this._orientation}`;
    this._minimumElement     .className = `${nameMinimum}-${this._orientation}`;
    this._maximumElement     .className = `${nameMaximum}-${this._orientation}`;
    this._dotContainerElement.className = `${nameDotContainer}-${this._orientation}`;
    this._thumbElement       .className = `${nameThumb}-${this._orientation}`;
  }

  _dispatchInputEvent() {
    this.shadowRoot.dispatchEvent(new CustomEvent("input", { bubbles: true, composed: true, detail: { value: this._value }}));
  }

  _dispatchChangeEvent() {
    this.shadowRoot.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true, detail: { value: this._value }}));
  }
}

if(!customElements.get(webComponentName)) {
  customElements.define(webComponentName, SummbitDiscreteSlider);
}

This code has been released to the public on GitHub under this license.

The instructions on how to use it are available here. An HTML test page is available here.

Please note that symbol names are long and descriptive on purpose. The goal of this file is to be as understandable for humans as possible. Those symbol names are later minified.

Note also that private symbol names start with an underscore (_) in order to mark them accordingly. At the time of writing this, private class fields are not available yet in Firefox according to caniuse.com. Once available, the underscores will be replaced with the # character.

I'm looking for hints/comments/advice in general i.e. HTML, CSS and JavaScript best practices, style tips, etc. are all welcome.


Known issues:

  • Safari on iOS 14 seems to have a bug in regards to the setPointerCapture API. Moving the thumb only works if the pointer is located within the slider. This possible bug is corroborated by this StackOverflow post.
\$\endgroup\$
3
  • 1
    \$\begingroup\$ First thing I checked was keyboard support :) and +1 for that: However I do find the flipped sliders anti-intuitive. As in keyboard left makes slider go right, and up makes slider go down etc. I would also have added Home / End. Second is focus. There is no indication on which element has focus unless one use TAB to move between them. I like the overall layout (esp the stop marks). I am also wondering if it should replace HTML sliders, as in: if user has JS disabled, display HTML variant, else replace it with the custom. If in a Web-App OK, then one need JS, else it should work regardless. \$\endgroup\$ – user3342816 May 31 at 18:13
  • \$\begingroup\$ Thanks for your comment, @user3342816. Depending on how you look at it, which keys do what for each configuration of the slider can be confusing. On a test page, like the one linked, it can make sense the way it is because how you increment or decrement is consistent across all sliders. For just one slider configuration, which is likely the more probable case, it might really be just confusing. So, I consider changing it on the right-to-left and top-to-bottom configuration. Regarding the focus indication: From what I see on the default slider it only appears if tabs is pressed. \$\endgroup\$ – ackh May 31 at 20:04
  • \$\begingroup\$ I'm pretty new to the whole web development / web front-end design thing so I might just be ignorant to the fact that JavaScript might be disabled in a web browser. From what I see in today's web, judging from my personal perspective, it is just utterly impractical to browse the web without JavaScript enabled. So, I really wonder whether I need to consider that case at all. I mean, it might be an actual thing but I abandoned the idea of disabling JavaScript in any of my web browsers years ago. \$\endgroup\$ – ackh May 31 at 20:12
3
+50
\$\begingroup\$

It looks like you've put a lot of work into this and it appears to function well. There are only a few suggestions - see below.

I agree with the comment by user3342816 that the flipped sliders seem counter-intuitive.

Suggestions

Reflows

The tag was not originally added to the post but that should definitely be a consideration for UI elements. Perhaps it is becoming less of a concern as browsers are being optimized, but it is wise to Minimize browser reflows. That means instead of adding elements to the DOM in a loop - e.g. in _createAdditionalDots(), add elements to a documentFragment or temporary element and then add the contents of that element in a single step. And for removing elements - e.g. as _deleteObsoleteDots() uses a loop, a different approach might be better.

Loops

The method updateThumbPosition() could use a for...of loop since variable i is only used to dereference the collection this._dotContainerElement.children, though performance might be impacted

const vs let

Using const when re-assignment isn't needed is a good habit because it can help avoid accidental re-assignment and other bugs. For example, the method _createDot() has one such variable:

_createDot() {
  let dot       = document.createElement("span");
  dot.className = nameDot;
  dot.tabIndex  = -1;
  return dot;
}

The variable dot is never re-assigned so it can be declared with const instead of let.

Constant naming convention

A lot of idiomatic Javascript follows conventions from other C-based languages, including true constants in ALL_CAPS. This isn't a requirement but does help anyone reading the code spot values that are symbolic constants. More on this topic can be found here.

Increment and Decrement operators

In method _incrementValue() this line exists:

this.value += 1;

It could be simplified using the increment operator:

this.value++;

And similarly, in _decrementValue() the second line:

this.value -= 1;

can be simplified using the decrement operator:

this.value--;
\$\endgroup\$
2
  • \$\begingroup\$ Thanks a lot for your review. What you brought up are all good points and I will address them accordingly. \$\endgroup\$ – ackh Jun 11 at 6:17
  • \$\begingroup\$ For reference, the points you brought up (except "constant naming convention") are addressed in the version I put up GitHub. \$\endgroup\$ – ackh Jul 26 at 16:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.