Cardinal Solutions / Front End Fridays

Workshop[0]

Classy jQuery

A brief introduction to how jQuery achieves some of its magic and some easy ways we can steal borrow that magic.

Example Repository:

 

https://github.com/ericrallen/vanilla.git

 

Clone the repository and checkout the workshop/0 branch

git clone https://github.com/ericrallen/vanilla.git

git checkout workshop/0

Technical Jargon

Disclaimer:  We are going to gloss over and simplify some stuff here.

Prototypal Inheritance

  • Also referred to as Prototypical Inheritance
  • The way that JavaScript Objects can inherit methods and properties from other Objects
  • Allows the jQuery plugin and method system to work
  • With great power comes great responsibility: prototypes technically allow you to overwrite almost every native method and there is much dissent over whether people should extend native prototypes or not
  • Basically allows you to create a template for an Object

Fluent Interface Pattern

  • This is what allows jQuery's method chaining to work
  • Design Pattern in which each method that isn't supposed to return a specific value returns a reference to the main Object containing the methods, allowing them to be called sequentially
  • One of numerous Design Patterns one can implement in JavaScript
  • Can basically just be referred to as method chaining; sometimes called "cascading"

HTML Nodes in JavaScript

  • Nodes are returned when you perform any kind of selection on the DOM like: querySelectorAll, getElementById, etc.
  • Some methods will give you a single Node, others will give you a NodeList which is an Array-like Object of HTML Nodes
  • Editing properties of a Node reference will make changes to the corresponding element in the DOM
  • jQuery Collections are basically modified NodeLists

Array-like Objects

  • Array-like objects look like Arrays and act sort of like Arrays, but are not actually Arrays
  • Cannot access native Array.prototype, but have a similar .length property, this means you cannot use .forEach(), .every(), .map(), etc.
  • Individual items are accessed like items in an Array via bracket notation
  • Are generally converted into Arrays in most libraries to make them easier to work with
  • A jQuery Collection is an Array-like object

arguments Object

  • Every JavaScript function (except for es2015 arrow functions) has a special function-scoped Array-like variable called arguments that does not need to be declared or initialized
  • Each item in the arguments object corresponds to a parameter passed to the function
  • The order of arguments corresponds to the order in which parameters were passed
  • This allows us to have a function that accepts any number of parameters without having to explicitly define each parameter when writing our function

Scope

  • In it's simplest terms, JavaScript has two scopes:  Function Scope and Global Scope
  • Global Scope (sometimes referred to as "window Scope" or just "the window") refers to any parameters of the global window object
  • Function Scope (sometimes referred to as "local scope") refers to the methods and variables available to the current function/closure
  • es2015 introduces Block Scope via use of let and const instead of var
  • You may only refer to variables and methods that are accessible in the current local scope or the global scope

DRY

  • Acronym for a common development principle:  Don't Repeat Yourself
  • This principle is focused on reducing redundant code
  • Benefits include:
    • ​a leaner codebase
    • ability to change code in a single place
    • reusable components
  • If code is "DRY" it exhibits a lack of repetition

Okay, enough of that boring stuff.

 

 

How do we make jQuery?

The basis of jQuery is a single constructor method jQuery() which is commonly rewritten using the shorthand $().

This method selects any elements that correspond to the selector passed to it and returns a reference to itself.

This reference is commonly referred to as a jQuery Collection, which is basically just a NodeList converted into an Array-like Object.

But, because all of jQuery's methods and plugins are defined as part of its prototype, we have access to all of them from the context of this Collection.

Ugh, still boring...

So, let's break down a very basic example of how this works.

//our main selector function
var $v = function(selector) {
    //get our Node List and convert it to an Array
    //using the Array.prototype.slice() method
    this.elements = [].slice.call(document.querySelectorAll(selector));

    //return a reference to $v so that we can continue chaining methods
    return this;
};

//map $v.fn to $v.prototype so it's easier to reference
//in plugins and other methods
$v.fn = $v.prototype = {};

//add a method to our prototype that will be accessible
//to our $v object at all times
$v.fn.addClass = function(classString) {
    //logic to add class name here

    //return a reference to our $v object
    //so that we can continue chaining methods
    return this;
};

A Very Simplified Example

var $v = function(selector) {

Here we are basically just adding a parameter to the window's scope named $v that expects one parameter (our selector string). We could also declare this function as window.$v().

 

Note:  The $ and _ are commonly used by JavaScript libraries as a shorthand because they are the only two non-alphabetic characters that can appear at the start of a function name. Also, having a single character method name can save file size, developer time, and potential typos.

this.elements = [].slice.call(document.querySelectorAll(selector));

Here we are taking the Node List returned by querySelectorAll() and converting it in an actual Array by using the slice() hack trick to have JavaScript convert it into an Array. We are storing this as the elements parameter because we aren't going to be doing things exactly as jQuery does under the hood.

 

Note:  Essentially, Array.prototype.slice() expects the this of the method to be an Array and by passing Function.prototype.call() an Array-like Object, we are having slice() turn it into an Array, which allows us to utilize native Array methods on it. This only works in IE9+.

return this;

And here is the lynch pin of our Fluent Interface Pattern. We return a reference to the current scope which gives us access to the entire $v.prototype and enables us to chain methods.

 

Note:  Method chaining can provide a nice interface for developers working with your libraries and can be used even in cases where prototypes are not utilized. You could do the same thing with many other JavaScript module patterns.

$v.fn = $v.prototype = {};

This is just a little bit of convenience for ourselves and other developers working with the library. By mapping $v.fn to $v.prototype, we save ourselves 7 characters every time we need to reference a prototype method. This saves us not only time and file size (both possibly negligible), but also from potential spelling errors.

 

Note:  Another benefit of this is that it sort of hides the prototypical structure of the library which may feel easier for newer developers who want to write plugins for the library or reference methods from the library's prototype without really understanding prototypes.

$v.fn.addClass = function(classString) {

Here we are just adding a method to our $v prototype. This addClass() method can be referenced by either $v.fn.addClass() or $v.prototype.addClass(), remember the $v.fn is mostly for convenience. Inside of our methods it will just be this.addClass() because we are returning a reference to our $v object.

 

Note:  We could define each method as a property of our initial prototype object that we set earlier using the Object literal notation {}, but by doing it this way we've made it much easier to separate methods out into various files and folders and easily mix and match methods we need for a library tailored to the needs of the current project we are working on.

Okay, now what?

Let's talk about .addClass()

About .addClass()

  • addClass() is a pretty simple method that takes a string and adds that string as a class to an element
  • It can utilize the .className or .classList (in IE10+) property of a Node
  • className returns a string of all classes applied to a Node, classList gives you a simple interface for adding, removing, and toggling single classes as well as checking to see if a Node has a class

element.className

  • We'll use .className for the sake of this example
  • Gives us a space-delimited string of classes applied to the element
  • If we just append a class to the .className property, what happens if it was already applied to the class?

About .hasClass()

  • Looks like we're going to need to be able to check if an element has a class before we start adding classes
  • Checking for presence of a substring in a string can be achieved most easily with .indexOf()
  • If the substring is not found, .indexOf() will return -1, otherwise it returns the index at which the string starts
  • We'll need to check if element.className.indexOf(classString) !== -1
//check to see if element has given class
$v.fn.hasClass = function(classString, element) {
    //if this class exists in the element's className property
    if(element.className.indexOf(classString) !== -1) {
        return true;
    }

    //by default, let's assume it doesn't
    return false;
};

Basic .hasClass() Implementation

But what about method chaining?

Some methods will necessarily have to return an actual value. In these cases, chaining is not an option.

But, our current implementation is also requiring an element to be passed as a second parameter, and that's not really ideal.

Could we change our code so that we can use:

$v('.some-selector').hasClass('testing')

and our .addClass() method can use:

this.hasClass('testing', element)

//check to see if element has given class
$v.fn.hasClass = function(classString, element) {
    //if an element was passed to the method
    if(element) {
        //if this class exists in the element's className property
        if(element.className.indexOf(classString) !== -1) {
            return true;
        }

        //by default, let's assume it doesn't
        return false;
    }

    //if no element parameter was passed to the method
    //use the first item in our collection
    if(this.elements[0].className.indexOf(classString) !== -1) {
        return true;
    }

    //again, let's assume this doesn't have the class
    return false;
};

Slightly Better .hasClass()

Okay, time to code!

 

Make your .addClass() method.

About .removeClass()

  • Removes a class from the .className property
  • You could use string manipulation for this, but let's play around with using an array and the .splice() method instead
  • To convert the .className string to an array, you can use the .split() method passing the delimiter (in this case a space: " ") as it's parameter
  • Once we have the array, we can use the .indexOf(classString) as the index to start our .splice()
  • After splicing, we can recreate the string using .join() and with the same delimiter

Updating .hasClass()

  • Can we adjust .hasClass() so that it can return our .indexOf() value if we want it to?
  • We could just run our own element.className.indexOf(classString) check in our .removeClass() method, but we're already doing that in .hasClass(), so can we keep things DRY?

More Code!

 

Let's get that .removeClass() built.

About .toggleClass()

  • This method checks to see if the element has the specified class and removes it, if the element does not have the class, it is added
  • Using .indexOf() is probably the best way to check for this again
  • Converting the .className string to an array first will allow us to just .splice() on the returned .indexOf() value if it is not -1 or use .push() to add the class to the array

Coding Time.

 

Let's get that .toggleClass() method written.

Okay, now let's break into that /test/ directory in the repository and make sure everything works together.

How can we implement the changes outlined in the numbered comments above .addClass(), .removeClass(), and .toggleClass()?

Can we make things more DRY?

Questions?

Front End Fridays | Workshop[0] | Classy jQuery

By Eric Allen

Front End Fridays | Workshop[0] | Classy jQuery

Basic introduction to how a bit of how jQuery works under the hood and how we can use those concepts to create our own, jQuery-like library to help people who are less comfortable with JavaScript but comfortable with jQuery start to feel less intimidated by vanilla JavaScript. The initial state of the library focuses on jQuery's class convenience methods.

  • 809