The first item in my JavaScript toolkit: A number input spinner

Here's my confession. During the earlier years of my career, if I needed to use JavaScript, I relied on jQuery and plugins. Plugins allowed me to add functionality to websites when I didn't know how to build it myself and jQuery allowed me to do it quickly. Sure, this was enough to get by at first, but it didn't help me learn or understand JavaScript properly for myself.

I decided that the time had come to get my head around this popular language, so with some great teaching from Jeremy, I have finally reached a point where I can write some JavaScript myself. This time, I've been writing Object Oriented Javascript, rather than procedural JavaScript, which I was more familiar with before. The reason for building self-contained objects is so that the code can be modular and re-usable, which seems more robust and flexible for future maintenance. Also: it is particularly useful for the pattern library projects I've been working on recently.

In this example, I have written the JavaScript for a small, yet very common pattern: a number input element with controls to add and subtract from the value. This is the first item for my newly forming toolkit—yay!

Here's how I wrote the JavaScript for a number input spinner (the full code is available on Codepen).

1. Create a new object.

<code class="language-javascript">var Spinner = function(rootElement) {};
</code>

I have created a new object called Spinner. Everything inside the curly braces of this object is now the blueprint from which any other objects created from it will follow. The value inside the parenthesis represents what will be parsed into the function (coming up in step 2). This can be any word.

2. Create a new instance of the object for each element with the class .js_spin

<code class="language-javascript">
//Get all of the number input elements
var spins = document.querySelectorAll('.js_spin');

//Store the total number of number inputs
var spinsTotal = spins.length;

//A variable to store one number inputs
var spin;

//A counter for the loop
var i;

//Loop through each number input
for ( i = 0; i < spinsTotal; i = i + 1 ) {
   //Create a new Spin object for each number input
   spin = new Spinner(spins[i]);
   //Start the initialisation function
   spin.init();
}
</code>

Outside the object constructor function, I have used a loop to find every instance of .js_spin and create a new Spinner object for it. Without this approach, only the first .js_spin item on the page will work. Now we can re-use this pattern as many times as we like on the page.

The line spin = new Spinner(spins[i]); is how the new Spinner object is created. The parenthesis contains the specific instance of .js_spin being used to create the object, which is parsed into the object. You can see this on the constructor function in step 1.

spin.init(); starts the initialisation function, which is where everything begins for this object. It is a public method so it can be accessed from outside the object.

Now we need to make the spinner object do something, so we'll add some properties and methods.

3. Set variables and values for use in the object.

I need to store values in variables so that they can be re-used across the code. This means that if I need to change a value later on, I will only need to do it in one place.

<code class="language-javascript">
//Add button selector
var addButtonSelector = '.js_spin-add';

//Remove button selector
var removeButtonSelector = '.js_spin-remove';

//Number input selector
var numberInputSelector = '.js_spin-input';

//A variable to store the markup for the add button
var addButtonMarkup = '<button type="button" class="FormUnit-quantity FormUnit-quantity--add js_spin-add Icon Icon--isClosed">Add</button>';

//A variable to store the markup for the remove button
var removeButtonMarkup = '<button type="button" class="FormUnit-quantity FormUnit-quantity--remove js_spin-remove Icon Icon--remove">Remove</button>';

//Variable to store the root's container
var container;

//A variable for the markup of the number input pattern
var markup;

//A variable to store a number input
var numberInput;

//Variable to store the add button
var addButton;

//Variable to store the remove button
var removeButton;

//Store max value
var maxValue;

//Store min value
var minValue;

//Store step value
var step;

//Store new value
var newValue;

//Variable to store the loop counter
var i;
</code>

Add and remove buttons

I am generating add and remove buttons as progressive enhancements in JavaScript, so they only appear to the user if JavaScript is available. After all, they would be pointless if they couldn't actually be used. So, the markup contains a regular number input field, which works just fine without JavaScript, but when JavaScript is available, the add and remove buttons are added to the markup and the number input field is changed to a text field to remove the spinner. We'll look at how this is done next, but for now, we need variables to store the contents of the buttons and the button selectors.

Input field

The number input field needs to be accessed and changed, so there needs to be a selector and some variables to store the attribute values.

4. Create an initialisation method

This is a public method so it can be accessed outside the object. When a new instance of the object is created, the init() method is called to get it running. In this init() function, the number input and buttons are setup to make it look and behave how we want. An important step is to add click events to the buttons because we want the add button to add to the value and the remove button to remove from the input value. Both of these need functions with instructions on how to make this happen.

<code class="language-javascript">
//Initialisation function
this.init = function() {
    container = rootElement;

    //Get the markup inside the number input container
    markup = container.innerHTML;

    //Create a button to decrese the value by 1
    markup += removeButtonMarkup;
    
    //Create a button to increase the value by 1
    markup += addButtonMarkup;

    //Update the container with the new markup
    container.innerHTML = markup;

    //Get the add and remove buttons
    addButton = rootElement.querySelector(addButtonSelector);
    removeButton = rootElement.querySelector(removeButtonSelector);
    
    //Get the number input element
    numberInput = rootElement.querySelector(numberInputSelector);

    //Get min, max and step values
    if (numberInput.hasAttribute('max')) {
       maxValue = parseInt(numberInput.getAttribute('max'), 10);
    } else {
       maxValue = 99999;
    }
    if (numberInput.hasAttribute('min')) {
       minValue = parseInt(numberInput.getAttribute('min'), 10);
    } else {
      minValue = 0;
    }
    if (numberInput.hasAttribute('step')) {
       step = parseInt(numberInput.getAttribute('step'), 10);
    } else {
       step = 1;
    }
    
    //Change the number input type to text
    numberInput.setAttribute('type', 'text');
    
    //If there is there no pattern attribute, set it to only accept numbers
    if (!numberInput.hasAttribute('pattern')) {
        numberInput.setAttribute('pattern', '[0-9]');
    }
    
    //Add click events to the add and remove buttons
    addButton.addEventListener('click', add, false);  
    removeButton.addEventListener('click', remove, false);
};
</code>

5. Create some more methods

Here is the add function. It is a private method because it does not need to be accessed from outside the object and I do not want the instructions inside the function to be changed.

<code class="language-javascript">
//Function to add one to the quantity value
var add = function(ev) {
    newValue = parseInt(numberInput.value, 10) + step;
    //If the value is less than the max value
    if (newValue <= maxValue) {
       //Add one to the number input value
       numberInput.value = newValue;
       //Button is active
       removeButton.disabled = false;
    }
    //If the value is equal to the max value
    if (numberInput.value == maxValue || newValue > maxValue) {
       //Disable the button
       addButton.disabled = true;
    }
    ev.preventDefault();
};
</code>

In the full example on Codepen, there is also a remove function created in a similar way.

I have also created public methods for setting the button selectors and markup because we may not want them to be the same for each number input spinner. So, when a new object is created, it is possible to specify the button markup and selectors each time or they will just take the default values. This makes the code even more flexible and re-usable—I love it!

<code class="language-javascript">
//Methods for setting values
this.setAddButtonMarkup = function(markup) {
    addButtonMarkup = markup;
};
</code>

It seems like a small pattern, but I think my learning and understanding that has come from this is great. I know i'll use this code again on another project and I'm super excited about building the next pattern for my toolkit.