Writing JavaScript components with jQuery
February 12, 2018
I know I know. “jQuery? That’s sooo 2013…” But actually, there are lots of reasons why you might want to use jQuery over other frameworks. Here are some of mine:
- You don’t want to use React/Vue/Angular because it’s overkill for what you want to do
- You don’t want to render your interface using JavaScript
- You want to use a jQuery based library/plugin
- You’re not the only front-end dev in your team and everyone has to study the framework if you want to use one
Not to say that you shouldn’t use React/Vue/Angular; I use React all the time. But I recognize that it doesn’t fit all the use-cases. So for the basic stuff, I stick to jQuery.
On with it then
So first thing’s first, let’s start with the assumptions.
- You’re using ES6
- You’re using Webpack/Browserify or something similar to compile your JS
- You’re using npm based libraries
If you’re not familiar with the technologies I just listed, then I suggest you first read up on modern JavaScript. It might be painful at first, but trust me it’s totally worth it. Modern JavaScript is really good. It’s what allowed me to write maintainable jQuery code with little fuss. Here are a few resources to get you started.
Think Components™️
For this blog post, I’m going to define a component as a user interface element that only cares about itself. Any code you write should only affect the component’s internals, and the component should be able to function independently. The only way for a component to affect and be affected by externalities is through event handlers and public methods.
Enough about theory. Let’s write some code.
We’re going to build a carousel! Basic stuff. We have some arrows that navigate the slides. Also we have those dot things.
Sidenote: We won’t focus on building out the styles for this. This blog post is all about the JavaScript.
Experience tells me that we can split this up into four components.
- CarouselSlider - the sliding bit
- CarouselDots - the dots on the bottom
- CarouselControls - the left and right arrows
- CarouselBlock - ties the previous three components up and orchestrates interactions between them
We’ll put each component in its own js file.
Sometimes components are simpler and don’t need to be broken down further, but I wanted to show a fairly complex example.
The HTML
We’ll start by writing the markup because it’ll be hard to write styles and JS without it. Here’s the markup I came up with.
<div class="carousel-block">
<div class="carousel-slider">
<ul class="carousel-slider__slide-list">
<li class="carousel-slider__slide">
<img src="https://placehold.it/1280x800?text=1" alt="Horse 1">
</li>
<!-- More slides here -->
</ul>
</div>
<div class="carousel-controls">
<button class="carousel-controls__button carousel-controls__prev">◀</button>
<button class="carousel-controls__button carousel-controls__next">▶</button>
</div>
<div class="carousel-dots">
<button class="carousel-dots__dot carousel-dots__dot--active"></button>
<button class="carousel-dots__dot"></button>
<!-- More dots here -->
</div>
</div>
The markup has four parts, carousel-slider
, carousel-controls
, carousel-dots
, and the main wrapper carousel-block
. These elements represent our four components.
The styles aren’t really relevant to what we’re building so I’ll omit them from the post. The only thing worth mentioning is the carousel-dots__dot--active
class controls the dot’s active state.
CarouselSlider.js
This component is responsible for rendering the slides themselves. Let’s take a look at the source code.
import $ from 'jquery'
class CarouselSlider {
constructor({root}) {
// Select HTML nodes
this.root = $(root)
this.slides = this.root.find('.carousel-slider__slide')
this.slideList = this.root.children('.carousel-slider__slide-list')
}
get slideCount() {
return this.slides.length
}
slideTo(index) {
// The transform property controls the position of the slider
this.slideList.css({transform: `translateX(-${100 * index}%)`});
}
}
export default CarouselSlider
There’s quite a bit going on here so let’s go through the interesting bits.
import $ from 'jquery'
That’s right. No global jQuery. We install it via npm and import it in our code.
class CarouselSlider {
constructor({root}) {
this.root = $(root)
Here we define a CarouselSlider
class which takes a root
parameter in its constructor. root
serves as the top-level element of this component. Accepting the root element in the constructor makes it easier to reuse the component because it allows us to create more than a single instance of this component, linked to different HTML nodes.
this.slideList = this.root.children('.carousel-slider__slide-list')
this.slides = this.slideList.children('.carousel-slider__slide')
}
Here we’re selecting the root’s child components, the slideList
and all the slide
s
get slideCount() {
return this.slides.length
}
A getter that returns the number of slides present in the carousel.
slideTo(index) {
this.slideList.css({transform: `translateX(-${100 * index}%)`});
}
A method that flips the carousel to the slide on the given index
. In this example, I implemented the slider using transforms.
Let’s look at how we can use the CarouselSlider
class.
const slider = new CarouselSlider({root: '.carousel-slider'})
console.log(slider.slideCount) // Prints out the number of slides
console.log(slider.slideTo(1)) // Slides to the second slide
Awesome right? We wrote all that code so that we can control the slider without having to call any low-level jQuery methods.
CarouselDots.js
We’re doing something special in this component. We want to be able to click on a dot and for the slider to slide to that dot’s slide.
import $ from 'jquery'
import EventEmitter from 'eventemitter3'
class CarouselDots extends EventEmitter {
constructor({root}) {
super()
this.root = $(root)
this.dots = this.root.children('.carousel-dots__dot')
// Bind events
this.dots.on('click', ev => this._handleClick(ev))
}
_handleClick(ev) {
ev.preventDefault()
const index = this.dots.index(ev.target)
this.highlightDot(index)
// Fire the clickdot event
this.emit('clickdot', index)
}
highlightDot(index) {
this.dots.removeClass('carousel-dots__dot--active')
this.dots.eq(index).addClass('carousel-dots__dot--active')
}
}
export default CarouselDots
We’re used to being able to use the on
method with jQuery objects to listen to click/change/etc. events, but did you know that we can also fire our own custom events? That’s what EventEmitter
is for. We extend CarouselDots
from EventEmitter
so that we can attach event listeners to the CarouselDots
instance.
Here’s how that’s used:
const dots = new CarouselDots({root: '.carousel-dots'})
dots.on('clickdot', index => console.log(`Clicked dot ${index}`))
We set the clickdot
event to be fired in the _handleClick
method. _handleClick
is called when one of our dots is clicked. We figure out which dot triggered the event, and emit the clickdot
event with this.emit('clickdot', index)
. All the listeners will then be called, passing in the index
.
Emitting and handling events is what ties all our components together.
There’s another method on CarouselDots
called highlightDot
. It does what it says on the tin: you pass in an index and it’ll highlight that dot.
In this example, we’re using eventemitter3 as our event emitter library. Node has it’s own built-in event emitter, but I like eventemitter3 because it’s optimized to work in the browser.
CarouselControls.js
import $ from 'jquery'
import EventEmitter from 'eventemitter3'
class CarouselControls extends EventEmitter {
constructor({root}) {
super()
this.root = $(root)
this.buttons = this.root.children('.carousel-controls__button')
this.buttons.on('click', ev => this._handleClick(ev))
}
_handleClick(ev) {
ev.preventDefault()
const target = $(ev.target)
// Fire clicknext or clickprev, depending on which button was clicked
if (target.hasClass('carousel-controls__next')) {
this.emit('clicknext')
} else {
this.emit('clickprev')
}
}
}
export default CarouselControls
Again we’re using EventEmitter
for this component. This time, we’re firing the clicknext
or clickprev
events when the left or right buttons are clicked. Unlike the previous component, we don’t pass any data along with our event.
const controls = new CarouselControls({root: '.carousel-controls'})
controls.on('clicknext', () => console.log('Next was clicked'))
controls.on('clickprev', () => console.log('Prev was clicked'))
CarouselBlock.js
This is where it all comes together.
import $ from 'jquery'
import CarouselControls from '../CarouselControls'
import CarouselDots from '../CarouselDots'
import CarouselSlider from '../CarouselSlider'
import mod from '../../../lib/mod' // custom modulo function
class CarouselBlock {
constructor({root}) {
this.root = $(root)
// Instantiate sub-components
this.slides = new CarouselSlider({
root: this.root.children('.carousel-slider')[0]
})
this.controls = new CarouselControls({
root: this.root.children('.carousel-controls')[0]
})
this.dots = new CarouselDots({
root: this.root.children('.carousel-dots')[0]
})
this.currentIndex = 0
this.controls.on('clicknext', () => this.nextSlide())
this.controls.on('clickprev', () => this.prevSlide())
this.dots.on('clickdot', index => this.slideTo(index))
}
slideTo(index) {
this.slides.slideTo(index)
this.dots.highlightDot(index)
this.currentIndex = index
}
nextSlide() {
// We use modulo here to keep index from going out of bounds.
let nextIndex = mod(this.currentIndex + 1, this.slides.slideCount)
this.slideTo(nextIndex)
}
prevSlide() {
let prevIndex = mod(this.currentIndex - 1, this.slides.slideCount)
this.slideTo(prevIndex)
}
}
export default CarouselBlock
We create an instance of each of the sub-components in the constructor. We attach event listeners to clicknext
, clickprev
, and clickdot
events, which when called will call the appropriate methods. We also keep track of the currently displayed slide with the currentIndex
variable. This is our single source of truth for which slide is currently displayed.
We have three methods, slideTo
, nextSlide
, and prevSlide
.
slideTo
just takes an index and calls methods onslider
anddots
to update their state.nextSlide
calculates what the next index should be (using the modulo operator) and callsslideTo
- ditto for
prevSlide
, except it calculates what the previous index should be.
As a bonus, we can also use these methods on the CarouselBlock
instance.
const carousel = new CarouselBlock({root: '.carousel-block'})
carousel.nextSlide()
carousel.prevSlide()
carousel.slideTo(0)
This will be useful if we want to incorporate additional complexity to the carousel, like adding a timer that scrolls the slide every X seconds or something.
Since we’re passing in root
in the constructor, we can have multiple independent instances of CarouselBlock
.
const carousel1 = new CarouselBlock({root: '#carousel1'})
const carousel2 = new CarouselBlock({root: '#carousel2'})
So that’s it, pretty much.
Do I really need jQuery?
Not really. If you can do with something lighter like Zepto.js, or even just Vanilla javascript, then that’s your call. What makes this approach great isn’t really jQuery, but the independent structure of each component, communicating only through public methods and event listeners.
Now go forth and write better javascript! 🍻