2
\$\begingroup\$

I've found the following example of an infinite slider to use on a project but as I will have multiple instances I have converted it to a prototype.

The original example https://medium.com/@claudiaconceic/infinite-plain-javascript-slider-click-and-touch-events-540c8bd174f2

My conversion

function Slideshow(slider) {

    const _self = this;

    this.slider = slider;
    this.sliderItems = slider.querySelector('.slides');
    this.next = slider.querySelector('.control.next');
    this.prev = slider.querySelector('.control.prev');

    this.posX1 = 0;
    this.posX2 = 0;
    this.posInitial = null;
    this.posFinal = null;
    this.threshold = 100;
    this.slides = this.sliderItems.getElementsByClassName('slide');
    this.slidesLength = this.slides.length;
    this.slideSize = this.sliderItems.getElementsByClassName('slide')[0].offsetWidth;
    this.firstSlide = this.slides[0];
    this.lastSlide = this.slides[this.slidesLength - 1];
    this.cloneFirst = this.firstSlide.cloneNode(true);
    this.cloneLast = this.lastSlide.cloneNode(true);
    this.index = 0;
    this.allowShift = true;

    // Listen for mousedown events,
    // when they happen, call the dragStart function
    this.sliderItems.onmousedown = (ev) => {
        this.dragStart.call(_self, ev);
    }
    
    // Touch Events 
    this.sliderItems.addEventListener('touchstart', (ev) => {this.dragStart(ev)});
    this.sliderItems.addEventListener('touchend', (ev) => {this.dragEnd(ev)});
    this.sliderItems.addEventListener('touchmove', (ev) => {this.dragAction(ev)});
    
    // Click Events
    this.next.addEventListener('click', () => this.shiftSlide(1));
    this.prev.addEventListener('click', () => this.shiftSlide(-1));
    
    
    // Transition Events
    this.sliderItems.addEventListener('transitionend', this.checkIndex.bind(_self));

    this.slide.call(this);
}

Slideshow.prototype.slide = function() {
    this.cloneSlides.call(this);
}

// Clone Slides
Slideshow.prototype.cloneSlides = function() {

    this.sliderItems.appendChild(this.cloneFirst);
    this.sliderItems.insertBefore(this.cloneLast, this.firstSlide);
    this.slider.classList.add('loaded');

}

// Drag Start
Slideshow.prototype.dragStart = function(event) {
  _self = this;

  event = event || window.event;
  event.preventDefault();
  this.posInitial = this.sliderItems.offsetLeft;
  
  if(event.type === 'touchstart') {
    this.posX1 = event.touches[0].clientX;
  } else {
    this.posX1 = event.clientX;
    document.onmouseup = (ev) => {
      this.dragEnd.call(_self, ev)
    }
    // document.onmousemove = this.dragAction;
    document.onmousemove = (ev) => {
      this.dragAction.call(_self, ev);
    }
  }

}

// Drag Action
Slideshow.prototype.dragAction = function(event) {
  
  event = event || window.event;
  
  if(event.type === 'touchmove') {
    this.posX2 = this.posX1 - event.touches[0].clientX;
    this.posX1 = event.touches[0].clientX;
  } else {
    this.posX2 = this.posX1 - event.clientX;
    this.posX1 = event.clientX;
  }
  
  this.sliderItems.style.left = (this.sliderItems.offsetLeft - this.posX2) + "px";
}

// Drag Action
Slideshow.prototype.dragEnd = function(ev) {
  this.posFinal = this.sliderItems.offsetLeft;
  
  if(this.posFinal - this.posInitial < -this.threshold) {
    this.shiftSlide(1, 'drag');
  } else if(this.posFinal - this.posInitial > this.threshold) {
    this.shiftSlide(-1, 'drag');
  } else {
    this.sliderItems.style.left = (this.posInitial) + "px";
  }
  
  document.onmouseup = null;
  document.onmousemove = null;
}

// Shift Slide
Slideshow.prototype.shiftSlide = function(dir, action) {
  this.sliderItems.classList.add('shifting');
    
  if(this.allowShift) {
    if(!action) {
      this.posInitial = this.sliderItems.offsetLeft;
    }
    
    if(dir == 1) {
      this.sliderItems.style.left = (this.posInitial - this.slideSize) + "px";
      this.index++;
    } else if(dir == -1) {
      this.sliderItems.style.left = (this.posInitial + this.slideSize) + "px";
      this.index--;
    }
  };
  
  this.allowShift = false;
}


// Check Index
Slideshow.prototype.checkIndex = function() {
  this.sliderItems.classList.remove('shifting');
  
  if(this.index == -1) {
    this.sliderItems.style.left = -(this.slidesLength * this.slideSize) + "px";
    this.index = this.slidesLength - 1;
  }
  
  if(this.index == this.slidesLength) {
    this.sliderItems.style.left = -(1 * this.slideSize) + "px";
    this.index = 0;
  }
  
  this.allowShift = true;
}


window.addEventListener('load', function () {
    const slider = document.querySelectorAll('.slider');

    slider.forEach((slide) => {
        new Slideshow(slide);
    })
})
@import url('https://fonts.googleapis.com/css?family=Roboto');



:root {
    --slider-width: 400px;
    --slider-height: 300px;
}

* { box-sizing: border-box; }

body {
    height: 100%;
    background-color: #7656d6;
    color: #333;
    font-family: 'Roboto', sans-serif;
    text-align: center;
    letter-spacing: 0.15em;
    font-size: 22px;
}

.slider {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: var(--slider-width);
    height: var(--slider-height);
    box-shadow: 3px 3px 10px rgba(0,0,0,.2);
}

.wrapper {
    overflow: hidden;
    position: relative;
    width: var(--slider-width);
    height: var(--slider-height);
    z-index: 1;
}

.slides {
    display: flex;
    position: relative;
    top: 0;
    left: calc(var(--slider-width) * -1);
    width: 10000px;
}

.slides.shifting {
    transition: left .2s ease-out;
}

.slide {
    width: var(--slider-width);
    height: var(--slider-height);
    cursor: pointer;
    display: flex;
    flex-direction: column;
    justify-content: center;
    transition: all 1s;
    position: relative;
    background: #FFCF47;
    border-radius: 2px;
}

/*.slider.loaded {*/
.slider.loaded .slide:nth-child(2),
.slider.loaded .slide:nth-child(7) { background: #FFCF47 }
.slider.loaded .slide:nth-child(1),
.slider.loaded .slide:nth-child(6) { background: #7ADCEF }
.slider.loaded .slide:nth-child(3) { background: #3CFF96 }
.slider.loaded .slide:nth-child(4) { background: #a78df5 }
.slider.loaded .slide:nth-child(5) { background: #ff8686 }
/*}*/

.control {
    position: absolute;
    top: 50%;
    width: 50px;
    height: 50px;
    background: #fff;
    border-radius: 50px;
    margin-top: -20px;
    box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.3);
    z-index: 2;
}

.prev,
.next {
    background-size: 22px;
    background-position: center;
    background-repeat: no-repeat;
    cursor: pointer;
}

.prev {
    background-image: url(https://cdn0.iconfinder.com/data/icons/navigation-set-arrows-part-one/32/ChevronLeft-512.png);
    left: -20px;
}

.next {
    background-image: url(https://cdn0.iconfinder.com/data/icons/navigation-set-arrows-part-one/32/ChevronRight-512.png);
    right: -20px;
}

.prev:active,
.next:active {
    transform: scale(.8);
}
<div id="slider" class="slider">
    <div class="wrapper">
        <div id="slides" class="slides">
            <span class="slide">Slide 1</span>
            <span class="slide">Slide 2</span>
            <span class="slide">Slide 3</span>
            <span class="slide">Slide 4</span>
            <span class="slide">Slide 5</span>
        </div>
    </div>
    <a href="" id="prev" class="control prev"></a>
    <a href="" id="next" class="control next"></a>
</div>

Now it seems I have done the conversion correctly, and works as expected when compared to the original. But I think what I am looking to know is how well have I done this conversion?

Use of this Using and tracking this is confusing at times. Have I been over-the-top in my use of this?

Calling (or Binding) this Its a concept that is still new to me, are there areas where I have needed to use call()? Have I used call() when I should be using bind()?

I'm sure there are other areas I am not aware of that can be improved upon, so any input that can help improve this would be excellent.

\$\endgroup\$
1
\$\begingroup\$

Question Responses

Using and tracking this is confusing at times. Have I been over-the-top in my use of this?

I don't feel it is "over-the-top" though storing a reference to this in another variable is a sign that context isn't bound properly. Bear in mind that an arrow function "Does not have its own bindings to this or super, and should not be used as methods."1 so referencing this within an arrow function declared inside another function will be the same context as outside the arrow function.

Calling (or Binding) this Its a concept that is still new to me, are there areas where I have needed to call()? Have I used call() when I should be using bind()?

Using call(this) adds unnecessary complexity which could confuse readers. If the function is declared on the object (either directly or via its prototype) then the context when it is called regularly will be set to this. The original code contains this line at the end of the constructor:

this.slide.call(this);

That can simply be a regular function call:

this.slide();

The same is true in the slide method. It can simply call this.cloneSlides().

There are places where bind could be used to create a bound function - for example:

this.sliderItems.onmousedown = (ev) => {
   this.dragStart.call(_self, ev);
}

This could simply be a reference without the excess arrow function, since dragStart is a function:

this.sliderItems.onmousedown = this.dragStart.bind(this);

See the code down below which achieves the same thing without using .call().

Bug

Clicking the next and previous anchor links makes the browser navigate to another page. There are various ways to stop this, including calling preventDefault() in the Javascript handler. For more solutions see answers to this StackOverflow question. Actually I see you mentioned something about that in your post Showing form on btn click - preventDefault of submit btn, then remove listener.

Review suggestions

Class syntax

The ES6 Class syntax could be used to simplify the prototype syntax.

Variable names

The constructor method has this line to select items by class name slides:

this.sliderItems = slider.querySelector('.slides');

The name sliderItems makes me think it would be multiple elements, but yet it is a single element. A more appropriate name would be something like sliderContainer.

DOM selection

As I mentioned in a previous review, querySelector() can be slower than other DOM methods like getElementById and getElementsByClassName(). So the line above could simply be:

this.sliderContainer = slider.getElementsByClassName('slides')[0];

Event handlers

The original code registering event handlers like this:

document.onmouseup = (ev) => {
     this.dragEnd.call(_self, ev)
}

While it may not be necessary for this code, a drawback to this approach is that it doesn't allow multiple event handlers to be used. That could be achieved with addEventListener and cleared with removeEventListener(). One important thing to note about removeEventListener() is that the listener to be removed must be a reference to the function added - so for a bound method it needs to be a reference to the function that was bound when addEventListener() was called - typically this requires storing the bound function in a variable so it can be used in both places.

Prefer strict equality comparisons

There are some comparisons using loose comparisons:

if(dir == 1) {

A good habit and recommendation of many style guides is to use strict equality operators (i.e. ===, !==). The problem with loose comparisons is that it has so many weird rules one would need to memorize in order to be confident in its proper usage.

Minimize DOM access

In the slideshow constructor there are two lookups for elements with class name 'slide' within three lines:

this.slides = this.sliderItems.getElementsByClassName('slide');
this.slidesLength = this.slides.length;
this.slideSize = this.sliderItems.getElementsByClassName('slide')[0].offsetWidth;

DOM access is expensive. The line to set the sliderSize can simply reference this.slides[0] instead of re-querying the DOM.

Default parameters

ES6 functions can have default parameters.

The dragAction method can be simplified from:

Slideshow.prototype.dragAction = function(event) {

    event = event || window.event;

To:

Slideshow.prototype.dragAction = function(event = window.event) {

Use of window.event

There is a note on the MDN documentation for window.event:

You should avoid using this property in new code, and should instead use the Event passed into the event handler function. This property is not universally supported and even when supported introduces potential fragility to your code.

Updated Code

The code below uses suggestions from above. Notice it has no .call() calls.

class Slideshow { //function Slideshow(slider) {
  constructor(slider) {
    this.slider = slider;
    this.sliderContainer = slider.getElementsByClassName('slides')[0];
    this.next = slider.querySelector('.control.next');
    this.prev = slider.querySelector('.control.prev');

    this.posX1 = 0;
    this.posX2 = 0;
    this.posInitial = null;
    this.posFinal = null;
    this.threshold = 100;
    this.slides = this.sliderContainer.getElementsByClassName('slide');
    this.slidesLength = this.slides.length;
    this.slideSize = this.slides[0].offsetWidth;
    this.firstSlide = this.slides[0];
    this.lastSlide = this.slides[this.slidesLength - 1];
    this.cloneFirst = this.firstSlide.cloneNode(true);
    this.cloneLast = this.lastSlide.cloneNode(true);
    this.index = 0;
    this.allowShift = true;
    //Bound methods for adding and removing event listeners
    this.boundDragAction = this.dragAction.bind(this);
    this.boundDragEnd = this.dragEnd.bind(this);

    // Listen for mousedown events,
    // when they happen, call the dragStart function
    //this.sliderItems.onmousedown = this.dragStart.bind(this);
    this.sliderContainer.addEventListener('mousedown', this.dragStart.bind(this));

    // Touch Events 
    this.sliderContainer.addEventListener('touchstart', this.dragStart.bind(this));
    this.sliderContainer.addEventListener('touchend', this.dragEnd.bind(this));
    this.sliderContainer.addEventListener('touchmove', this.dragAction.bind(this));

    // Click Events
    this.next.addEventListener('click', e => this.shiftSlide(1) || e.preventDefault());
    this.prev.addEventListener('click', e => this.shiftSlide(-1) || e.preventDefault());


    // Transition Events
    this.sliderContainer.addEventListener('transitionend', this.checkIndex.bind(this));

    this.slide();
  }

  slide() { //Slideshow.prototype.slide = function() {
    this.cloneSlides();
  }

  // Clone Slides
  cloneSlides() { //Slideshow.prototype.cloneSlides = function() {

    this.sliderContainer.appendChild(this.cloneFirst);
    this.sliderContainer.insertBefore(this.cloneLast, this.firstSlide);
    this.slider.classList.add('loaded');

  }

  // Drag Start
  dragStart(event) { //Slideshow.prototype.dragStart = function(event) {
    event = event || window.event;
    event.preventDefault();
    this.posInitial = this.sliderContainer.offsetLeft;

    if (event.type === 'touchstart') {
      this.posX1 = event.touches[0].clientX;
    } else {
      this.posX1 = event.clientX;
      //document.onmouseup = this.dragEnd.bind(this);
      document.addEventListener('mouseup', this.boundDragEnd);
      // document.onmousemove = this.dragAction;
      //document.onmousemove = this.dragAction.bind(this);
      document.addEventListener('mousemove', this.boundDragAction);
    }

  }

  // Drag Action
  dragAction(event = window.event) { //Slideshow.prototype.dragAction = function(event) {
    

    if (event.type === 'touchmove') {
      this.posX2 = this.posX1 - event.touches[0].clientX;
      this.posX1 = event.touches[0].clientX;
    } else {
      this.posX2 = this.posX1 - event.clientX;
      this.posX1 = event.clientX;
    }

    this.sliderContainer.style.left = (this.sliderContainer.offsetLeft - this.posX2) + "px";
  }

  // Drag Action
  dragEnd(ev) { //Slideshow.prototype.dragEnd = function(ev) {
    this.posFinal = this.sliderContainer.offsetLeft;

    if (this.posFinal - this.posInitial < -this.threshold) {
      this.shiftSlide(1, 'drag');
    } else if (this.posFinal - this.posInitial > this.threshold) {
      this.shiftSlide(-1, 'drag');
    } else {
      this.sliderContainer.style.left = (this.posInitial) + "px";
    }

    //document.onmouseup = null;
    //document.onmousemove = null;
    document.removeEventListener('mouseup', this.boundDragEnd);
    document.removeEventListener('mousemove', this.boundDragAction);
  }

  // Shift Slide
  shiftSlide(dir, action) { //Slideshow.prototype.shiftSlide = function(dir, action) {
    this.sliderContainer.classList.add('shifting');

    if (this.allowShift) {
      if (!action) {
        this.posInitial = this.sliderContainer.offsetLeft;
      }
      if (dir === 1) {
        this.sliderContainer.style.left = (this.posInitial - this.slideSize) + "px";
        this.index++;
      } else if (dir === -1) {
        this.sliderContainer.style.left = (this.posInitial + this.slideSize) + "px";
        this.index--;
      }
    };

    this.allowShift = false;
  }


  // Check Index
  checkIndex() { //Slideshow.prototype.checkIndex = function() {
    this.sliderContainer.classList.remove('shifting');

    if (this.index === -1) {
      this.sliderContainer.style.left = -(this.slidesLength * this.slideSize) + "px";
      this.index = this.slidesLength - 1;
    }

    if (this.index === this.slidesLength) {
      this.sliderContainer.style.left = -(1 * this.slideSize) + "px";
      this.index = 0;
    }

    this.allowShift = true;
  }
}


window.addEventListener('load', function() {
  const sliderElements = document.getElementsByClassName('slider');

  [...sliderElements].forEach((slide) => {
    new Slideshow(slide);
  })
})
@import url('https://fonts.googleapis.com/css?family=Roboto');



:root {
    --slider-width: 400px;
    --slider-height: 300px;
}

* { box-sizing: border-box; }

body {
    height: 100%;
    background-color: #7656d6;
    color: #333;
    font-family: 'Roboto', sans-serif;
    text-align: center;
    letter-spacing: 0.15em;
    font-size: 22px;
}

.slider {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: var(--slider-width);
    height: var(--slider-height);
    box-shadow: 3px 3px 10px rgba(0,0,0,.2);
}

.wrapper {
    overflow: hidden;
    position: relative;
    width: var(--slider-width);
    height: var(--slider-height);
    z-index: 1;
}

.slides {
    display: flex;
    position: relative;
    top: 0;
    left: calc(var(--slider-width) * -1);
    width: 10000px;
}

.slides.shifting {
    transition: left .2s ease-out;
}

.slide {
    width: var(--slider-width);
    height: var(--slider-height);
    cursor: pointer;
    display: flex;
    flex-direction: column;
    justify-content: center;
    transition: all 1s;
    position: relative;
    background: #FFCF47;
    border-radius: 2px;
}

/*.slider.loaded {*/
.slider.loaded .slide:nth-child(2),
.slider.loaded .slide:nth-child(7) { background: #FFCF47 }
.slider.loaded .slide:nth-child(1),
.slider.loaded .slide:nth-child(6) { background: #7ADCEF }
.slider.loaded .slide:nth-child(3) { background: #3CFF96 }
.slider.loaded .slide:nth-child(4) { background: #a78df5 }
.slider.loaded .slide:nth-child(5) { background: #ff8686 }
/*}*/

.control {
    position: absolute;
    top: 50%;
    width: 50px;
    height: 50px;
    background: #fff;
    border-radius: 50px;
    margin-top: -20px;
    box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.3);
    z-index: 2;
}

.prev,
.next {
    background-size: 22px;
    background-position: center;
    background-repeat: no-repeat;
    cursor: pointer;
}

.prev {
    background-image: url(https://cdn0.iconfinder.com/data/icons/navigation-set-arrows-part-one/32/ChevronLeft-512.png);
    left: -20px;
}

.next {
    background-image: url(https://cdn0.iconfinder.com/data/icons/navigation-set-arrows-part-one/32/ChevronRight-512.png);
    right: -20px;
}

.prev:active,
.next:active {
    transform: scale(.8);
}
<div id="slider" class="slider">
    <div class="wrapper">
        <div id="slides" class="slides">
            <span class="slide">Slide 1</span>
            <span class="slide">Slide 2</span>
            <span class="slide">Slide 3</span>
            <span class="slide">Slide 4</span>
            <span class="slide">Slide 5</span>
        </div>
    </div>
    <a href="" id="prev" class="control prev"></a>
    <a href="" id="next" class="control next"></a>
</div>

\$\endgroup\$

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.