Cardinal Solutions / Front End Fridays

Workshop[1]

Objectifying

Truthiness

A brief continuation of our dive into 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/1 branch

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

git checkout workshop/1

Technical Jargon

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

Side Effects

  • Unintended consequences generated by your code
  • Can be hard to find and debug
  • Often result from a method trying to do too many things

Command Query Separation

  • A common principal of imperative programming
  • Basically states that a method should return a value or set a value, but never both
  • Is supposed to help prevent errors by limiting possible side effects
  • Is sort of ignored in a lot of jQuery's API, and a number of other APIs (We're going to pretty much ignore it, too)

Shallow vs Deep

  • Refers to how a copy or merge treats nested properties with the same keys
  • If replacing the property of the first object with the property of the second object, it is shallow
  • If merging the property of the first object with the property of the second object, it is deep
  • Deep copies and merges can be expensive and slow with very large or deeply nested data

Data-* Attributes

  • Data attributes are html element attributes that are prefixed with "data-" and can be used to store any arbitrary data
  • Are frequently used in JavaScript to attach state and other useful information to DOM Nodes

Object Properties

  • Object properties in JavaScript have several configurable options:  enumerable, writable, and configurable
  • Enumerable - property shows up during enumeration process; basically this means the property actually exists on this object and not its prototype
  • Writable - property value can be changed via assignment
  • Configurable - property can be edited, deleted, and have it's options altered
  • All three default to false

Boolean Attributes

  • Some HTML attributes are Boolean
  • The attribute's presence is the equivalent of it being true; it's absence is considered false
  • These attributes should not be assigned any value, but if they must, only an empty, quoted string or the canonical name of the attribute in quotes are valid values
  • Examples:  selected, checked, disabled, controls
  • For convenience they can be referenced and set via dot notation on the HTML Node's Object
  • Sometimes referred to as reflexive attributes

Mutation

  • Changing the value of a variable, an object property, an item in array, etc. is considered mutation
  • Some programming paradigms, like Functional Programming, prefer passing a value to a function and having it return a new value over mutating values
  • Mutation can potentially lead to side effects and can make testing some scenarios more difficult

For...in Loops

  • A loop useful for iterating over the properties of an object
  • Iterates over EVERY enumerable property of an object and enumerable properties inherited from it's constructor's prototype
  • Iterates in an arbitrary order, so do rely on it moving in a predictable sequence
  • It is recommended to not alter any property other than the one in the current iteration due to potential side effects
  • Object.keys() can return only the enumerable properties of the object, but then have to iterate through that array
  • Object.prototype.hasOwnProperty() can check if a property actually belongs to the object

Recursion

  • A recursive function calls itself, this act of a functioning invoking itself again is called recursion
  • Loops could be rewritten as recursive functions (and some programming paradigms, like Functional Programming, promote this)
  • Useful when you need to call the same function with differing parameters inside a loop
  • Can lead to crashes if used incorrectly, but we won't be diving into Trampoline Functions and Tail Call Optimization here, feel encouraged to look them up on your own
  • ES6 should alleviate most of JavaScript's issues with recursion

Ternary Operators

  • Shorthand for simple if...else statements
  • Syntax:  (condition) ? trueValue : falseValue
  • Useful when writing out an if...else would generate extra overhead or significantly impact DRYness of the code
  • Recommended to surround condition with parentheses, but not required
  • Can make code harder to parse and reason about, but can be very powerful when used carefully

Getters and Setters

  • Getters: return the value of the specified property
  • Setters: take a value and set the specified property to that value
  • Generally used to normalize methods of retrieving and mutating property values
  • Useful when you want to perform some action whenever a property value is accessed or altered
  • Frequently found in JavaScript frameworks

Type Coercion

  • Some common JavaScript types:  Integer, String, Array, Object, Function, Date
  • Coercion is the process of converting from one type to another
  • JavaScript can do this automatically depending on how a value is used (example:  "testing" + 1 === "testing1")
  • Allows us to check truthiness or falsiness
  • Using === enforces equality and type comparison
  • Using == uses Type Coercion to compare values

Truthy & Falsey Values

  • JavaScript Type Coercion allows for any value to be tested against values of other types
  • Because of Type Coercion any value can be evaluated as true or false
  • Falsey values: empty String (""), falsenullundefined0NaN
  • Truthy values: Any non-falsey value

Okay, enough of that boring stuff.

 

 

How do we keep making jQuery?

A lot of jQuery's plugins utilize objects to store options. Most plugins also allow developers to override some or all of these options via providing their own configuration object.

This configuration extension is achieved via using jQuery's $.extend() method, which will take any number of objects and merge their properties into the first object provided.

By default, $.extend() performs a shallow merge, but you can optionally pass a boolean value as the first parameter to tell jQuery you'd like to perform a deep merge.

The first object passed to $.extend() will be mutated, so you'll frequently see people pass an empty object literal ({}) as the first object so that no mutation occurs.

Ugh, still boring...

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

//extend an object with N other objects where N > 0
$v.extend = function() {
    //initialize empty array to store references to objects
    //currently in the  `arguments` Array-like Object
    var objs = [];

    //iterate through our arguments Array-like object
    for(var a = 0; a < arguments.length; a++) {
        //add each argument to our `objs` array
        objs[a] = arguments[a];
    }

    //take the first object from the arguments
    //store it as our container
    //remove it from the array using `Array.prototype.shift()`
    var container = objs.shift();

    //iterate through the rest of our argument objects
    objs.forEach(function(item) {
        //iterate through properties in item object
        for(var prop in item) {
            //make sure this is an enumerable property
            if(item.hasOwnProperty(prop)) {
                //set container object's `prop`
                //to the value of extension object's `prop`
                container[prop] = item[prop];
            }
        }
    }, this);

    //return the merged objects as a single object
    return container;
};

Wait a minute...

We spent all of Workshop[0] talking about and using $v.fn, why aren't we using $v.fn.extend() here?

In jQuery, jQuery.extend() allows you extend any object with any other objects.

 

jQuery.fn.extend(), on the other hand, is used to actually extend the jQuery.fn prototype. This allows you to add your own methods to the prototype or overwrite other methods.

There are some other interesting things going on here, too, like the Array.prototype.shift() method.

 

Array.prototype.shift() removes the first item from an array and returns it.

 

This is handy for situations like this where you want to do something different with the first item of the array but also want to loop over the other items.

We're also using Object.prototype.hasOwnProperty() to see if property is enumerable or not while iterating over an object using a for...in loop.

You may have noticed that this is a shallow extension.

 

We'll need to do some more work to make it deep.

First, We need to let the developer specify they would like this to be a deep merge.

//by default we'll use a shallow merge
var deep = false;

//logic to convert arguments to objs array goes here
//we've already got this in our method

if(typeof objs[0] === 'boolean') {
    deep = objs.shift();
}

//logic iterating over our objs array goes here

We use Array.prototype.shift() again to remove the boolean value from our objs Array because we won't need it in the Array anymore.

Second, we'll need to adjust our merging logic if deep is true.

 

If we need to perform a deep merge, we need to know if the property we are merging exists and will need to use recursion when merging properties that are also objects to make sure that they are also merged deeply.

//iterate through the rest of our argument objects
objs.forEach(function(item) {
    //iterate through properties in item object
    for(var prop in item) {
        //make sure this is an enumerable property
        if(item.hasOwnProperty(prop)) {
            //if this isn't a deep merge
            if(!deep) {
                //set container object's `prop`
                container[prop] = item[prop];
            //we need to go deep
            } else {
                //DEEP MERGE LOGIC HERE
            }
        }
    }
}, this);

Some Modifications to Our Current Loop

//check for enumerable property container object
if(container.hasOwnProperty(prop)) {
    //if this property is an object
    if(typeof item[prop] === 'object') {
        //recursive deep merge
        container[prop] = $.extend(
            deep,
            (container[prop]) ? container[prop] : {},
            item[prop]
        );
    //otherwise, set the container object's property
    } else {
        container[prop] = item[prop];
    }
//if property didn't exist on container
} else {
    //define new property on container with property's value
    Object.defineProperty(container, prop, {
        value: item[prop],
        writable: true,
        enumerable: true,
        configurable: true
    });
}

Deep Merge Logic

BONUS: How could we utilize our $v.extend() method to also allow us to have $v.fn.extend() work like jQuery.fn.extend() and extend our prototype?

Merging Objects is okay, but now what?

jQuery.find()

If you've never used jQuery.find(), you should definitely be using it.

What Does jQuery.find() do?

jQuery.find() takes the previous jQuery collection and uses it as the context for finding the elements that match the selector passed to the .find() method.

Why Does That Matter?

You may have seen a selector written like this $('#id a') well, this selector ends up being remapped to $('#id').find('a'), so using .find() can actually save a small amount of execution time. In older browsers, the amount of time saved can be quite large.

How Can We Implement .find()?

Well, we'll need to take the $v.elements Array and create a new array of the result of .querySelectorAll() on each of those elements passing in the value that was sent to .find()

Okay, let's look at some code.

//replace the current this.elements
//with a new collection created by running
//`querySelectorAll()` from each item in collection
// and looking for the specified selector
$v.fn.find = function(str) {
    //store current collection
    var elements = this.elements;

    //reset this.elements to empty Array
    this.elements = [];

    //iterate through current collection
    elements.forEach(function(item) {
        //add the result of `.querySelectorAll()` for the selector
        //to the this.elements Array
        this.elements = this.elements.concat(
            [].slice.call(item.querySelectorAll(str))
        );
    }, this);

    //return reference to $v object for method chaining
    return this;
};

$v.find() Implementation

What is this.elements.concat()?

Array.prototype.concat() is a method that appends one Array to another.

 

In this case we use it to take the $v.elements Array and add the results of each call of .querySelectorAll().

This one is short, but can be kind of confusing.

 

Any questions about .find()?

BONUS: Try to take our $v() constructor method and allow it to receive space-separated selector lists and have it use $v.fn.find() to generate the correct collection of elements.

Dealing with Attrbutes

jQuery provides several methods for dealing with HTML Attributes, including:  attr(), data(), and prop()

 

Each method does certain things differently, we won't be digging into or recreating the functionality for every case, though.

  • Used for setting an attribute value for every item in the current collection
  • Can also be used to return an attribute value of the first item in the collection
  • Performs some cross-browser consistency magic, but we won't be recreating that here
  • Will not update for Boolean Attributes as they change

.attr()

  • Used for setting a data-* attribute value for every item in the current collection
  • Can also be used to return a data-* attribute value of the first item in the collection
  • jQuery generates a local cache of data-* attributes, but we won't be dealing with caching them
  • In our case, .data() is basically just going to call .attr(), but prepend the attribute name with "data-"
  • Old versions of jQuery also provide this functionality for old browsers that do not support data-* attributes

.data()

  • Used for setting a Boolean Attribute value for every item in the current collection
  • Can also be used to return a Boolean Attribute value of the first item in the collection
  • Should be used for setting or retrieving any Boolean Attribute since they do not necessarily update the HTML Attribute when changed
  • Should be used for attributes like value, but jQuery provides a .val() method for that

.prop()

Let's start simple.

Calling .attr() Inside .data()

//set or retrieve a data-* attribute value
$.fn.data = function(attribute, value) {
    //make sure an attribute name was passed
    if(typeof attr !== 'undefined') {
        //by default we'll want to prepend "data-"
        var prepend = true;

        //if the attribute name already starts with "data-"
        if(attribute && attribute.indexOf('data-') === 0) {
            prepend = false;
        }

        //call the .attr() method and make
        //sure we prepend "data-" to attribute if needed
        return this.attr(
            (prepend) ? 'data-' + attribute : attribute,
            value
        );
    //no attribute provided, skip it
    } else {
        //return reference to $v for method chaining
        return this;
    }
};

Okay, What About .attr()?

//method for getting/setting an attribute's value
$v.fn.attr = function(attribute, value) {
    //if value was provided
    if(typeof value !== 'undefined') {
        //iterate through collection of elements
        this.elements.forEach(function(item, index, array) {
            //if the value being passed is null, we want to remove the attribute
            if(value === null) {
                array[index].removeAttribute(attribute);
            //if the value isn't null, we want to set the attribute
            } else {
                array[index].setAttribute(attribute, value);
            }
        }, this);

        //return reference to `$v` for method chaining
        return this;
    //if value wasn't provided return value
    //getter version of this method not eligible for method chaining
    } else {
        //if attribute name was provided
        if(typeof attribute !== 'undefined') {
            //return value of `attribute` for first item in collection
            return this.elements[0].getAttribute(attribute);
        }
    }
};

And .prop()?

//method for getting/setting a boolean attribute's value
$v.fn.prop = function(attribute, value) {
    //if value was provided
    if(typeof value !== 'undefined') {
        //make sure we're dealing with a Boolean
        value = !!value;

        //iterate through collection of elements
        this.elements.forEach(function(item, index, array) {
            //if the value being passed is null, remove the attribute
            if(value === null) {
                array[index].removeAttribute(attribute);
            //if the value isn't null, we want to set the attribute
            } else {
                array[index].attribute = value;
            }
        }, this);

        //return reference to `$v` for method chaining
        return this;
    //if value wasn't provided return the actual value
    //getter version of this method not eligible for method chaining
    } else {
        //if attribute name was provided
        if(typeof attribute !== 'undefined') {
            //return boolean value of `attribute` for first item in collection
            return !!this.elements[0][attribute];
        }
    }
};

!!

  • This is shorthand to take any value the user might have passed to our $v.prop() method and make sure it's a boolean value
  • Uses Type Coercion
  • Is just NOT(NOT value))
  • If value was falsey:  !!value === false
  • If value was truthy:  !!value === true
  • Is not always the best way to achieve this, but works well for this particular case

BONUS: What if we wanted a convenience method like $v.fn.data() for other namespaced attributes like aria-* attributes ($v.fn.aria())?

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

Questions?

Front End Fridays | Workshop[1] | Objectifying Truthiness

By Eric Allen

Front End Fridays | Workshop[1] | Objectifying Truthiness

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. This builds upon the concepts discussed in Workshop[0] and deals with how jQuery deals with attributes, properties, extending Objects, and querying the DOM within specific contexts using .find().

  • 736