Wednesday, 1 March, 2017 UTC


Summary

If you're like me, you are always on the lookout for jQuery libraries to use, and there are thousands of them out in the wild, maybe not thousands but enough to solve just about any problem you have.
In this tutorial I'd like to show you how to build a cookie library without jQuery. Why should you build a cookie library instead of using an existing cookie library? It is important to learn the fundamentals of JavaScript.
After studying this tutorial you can go back to using any jQuery cookie library but deep down you’ll be satisfied, knowing you can go through the library source code without JavaScript fatigue. Even if you never use your own cookie library, you’ll have a better understanding of how each one you use functions internally.
We’ll cover the following:
  1. Creating a cookie.
  2. Reading a cookie.
  3. Checking for an existing cookie.
  4. Listing all cookies.
  5. Removing a cookie.
Ready? Let's dive in!
What are cookies?
Cookies are data stored in small text files, on your computer. If a server sends a web page to a browser, the connection is shut down, and the server forgets everything about the user and the browser. Cookies were designed to answer the question "How does the server remember information about the user?".
Prerequisites
Note:
  1. Important words are highlighted.
  2. Bold words emphasise a point.
  3. Previous / Next code appears like this . . . .
There are not many prerequisites for this project because we will make use of CodePen for demos and the full code for this tutorial is on Github. You can follow the demo or setup a new CodePen. You will need to have a fair understanding of JavaScript context and methods. In addition, you will need a local HTTP server e.g. (Node.js, PHP, Python or BrowserSync).
We'll start out by getting the window and document object, we'll then walk through building out core methods that will associate with the current browser cookie.
Setup
Let's setup our cookie library. First, create a file cookie.js, this file hold our methods, and open with Sublime Text (or Atom, VIM, WebStorm).
$ touch cookie.js && open cookie.js -a 'Sublime Text'
Inside the file we'll add an IIFE (Immediately-Invoked Function Expression) that takes in two parameters, document and window object.
The if / else statement checks if Node.js, AMD is defined otherwise set it to window.
Note: The window object has the document object in it and contains properties like navigator, print, history, etc.
cookie.js
(function (document, window) {
    'use strict';

    // our cookiet object will be here.
    . . .

     if (typeof define === 'function' && define.amd) {
         define([], function () {
             return cookiet;
        });
    } else {
        window.cookiet = cookiet;
    }
}(document, window));
To test if the script works, you'll need to include the cookie.js file in a HTML file and you can add a console.log('yeet! It works!'). You'll need to have a local server running because cookies does not work with file:// URI scheme. I will be using Python Server, but you can use any one you'd like.
$ touch index.html && open index.html -a 'Sublime Text'
index.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>cookie</title>
    </head>
    <body>
        <script src="./cookie.js"></script> 
    </body>
</html>
cookie.js
. . .

console.log('yeet! It works!');

. . .
Here is how to start Python server with Python 2.7. Navigate to the folder where your cookie.js and index.html exist and run:
$ python -m SimpleHTTPServer 8000
Now open your web browser (Google Chrome preferably), and open your Developer Tools > Console, you should see yeet! It works! on your console.
Just to recap: you should have index.html and cookie.js files opened in your favorite text editor, and a Python Server (or any other server running).
All done? Sweet let's get started with building out the methods.
Read
To code the read method we need to first create a containing object if you remember we exported an object window.cookiet = cookiet; in the setup section above. Create the cookiet object below the 'use strict' line.
. . .
'use strict';

// our cookiet object will be here.
var cookiet = {

};

. . .
With the cookiet object created, let's code out the read method.
. . .

var  cookiet = {
    read: function(params) {
        var name = params.name;

        // If there's no cookie name / key provided return undefined.
        if (!name) return;

        // split the cookie into two by using '=', and assigns
        // an array to `parts variable`.
        var parts = document.cookie.split(name + '=');

        // check the length of parts, since it should be cookie name 
        // and cookie value and decodes the token
        if (parts.length === 2) return decodeURIComponent(parts.pop().split(';').shift());

        // Force a return of undefined if not found
        return;
    },
};

. . .
The read method requires an object with a property of cookie name e.g. ({ name: 'hi' }) passed into it. We check if the cookie name was provided, if it's not we return undefined. Since cookie is a document property we use the .split method on it by providing the cookie name passed into the function with string concat + and =. You might be wondering, what's happening? Hold on tight, let's look at a sample cookie string.
document.cookie
"hi=again; hello=world" // output
In the code above document.cookie is a string containing a semicolon-separated list of all cookies (i.e. key=value pairs), joined by =. hi=again; is a cookie.
The if checks the length and then decode the cookie value using decodeURIComponent method. If the cookie with the name provided is found, we return it. If not we return undefined. If you don't understand what .pop, .shift() does, I recommend you read them up.
Whew! That was mouthful.
Create
The create method takes in params object, and we check to see if the property values exists using the || logical operator. If it does not exist we set an alternative false or ' ' value.
Next, we check for the cookie name if it is false we return false, else we encodeURIComponent the cookie name and value to ensure they do not contain any commas, semicolons, or whitespace. Set path specified or use default, path must be absolute. Set domain (e.g., 'example.com' or 'subdomain.example.com') if not specified, defaults to the host current document location. Set secure to only transmitted over secure protocol as https (SSL). Using the httpOnly when generating a cookie helps mitigate the risk of client side script accessing the protected cookie.
. . .

read: function(params) {...},
create: function(params) {
    params.name = params.name || false; // cookie name / key
    params.value = params.value || ''; // cookie value
    params.expires = params.expires || false; // cookie expires (days)
    params.path = params.path || '/'; // cookie path. defaults to '/' the whole website.

    if (params.name) {
        var cookie = encodeURIComponent(params.name) + '=' + encodeURIComponent(params.value) + ';';
        var path    = 'path=' + params.path + ';';
        var domain  = params.domain ? 'domain=' + params.domain + ';' : '';
        var secure  = params.secure ? 'secure;' : '';
        var httpOnly  = params.httpOnly ? 'httpOnly;' : '';
        var expires = '';

        // If the params object contains expires in days.
        if (params.expires) {
            // using "expires" because IE doesn't support "max-age".
            params.expires = new Date(new Date().getTime() + 
                parseInt(params.expires, 10) * 1000 * 60 * 60 * 24);
            // use toUTCString method to convert expires date to a string, 
            // using the UTC time zone.
            expires = 'expires=' + params.expires.toUTCString() + ';';
        }

        // assign all the concatenated values to document.cookie.
        document.cookie = cookie + expires + path + domain + secure + httpOnly;
        return true;
    }

    return false;
},

. . .
The expires calculation isn't as complex as it looks, first convert the days specified to miliseconds. If the days provided is a string, we convert to Number using parseInt in base 10 and sum it up with new Date().getTime() current time, then assign the value to expires. e.g. (2 Days * 1000 Miliseconds * 60 Minutes * 60 Seconds * 24 Hours), will equal 1.728e+8 or (172800000) Miliseconds. If the cookie was successfully saved we return true.
Note: If the expires is not specified it will expire at the end of the session.
Exists
The exists method is pretty straightforward, since we are checking if a cookie exist, first we check if the params object has a property name and is defined. If it's not we return undefined, if it exists we call the read method we defined above providing the params object and return true if found, otherwise we return false.
. . .

read: function(params) {...},
create: function(params) {...},
exists: function(params) {
    // checks the `params` object for property name
    if (!params || !params.name) {
        return;
    }

    // call the read method providing the `params` object as parameter
    if (this.read(params)) {
        return true;
    }

    return false;
},

. . .
ListAsObject
This methods gets all the cookies on for a specific domain, then split document.cookie using ; if it's defined otherwise returns an empty object. We'll need to loop through the found cookies by decrementing the length of the cookies. You'd need to split using = to get an array of length 2, then decode the cookie using decodeURIComponent method, and set the first item of the array as the object key and the second as the value. Return the cookies object when done looping.
. . .

read: function(params) {...},
create: function(params) {...},
exists: function(params) {...},
listAsObject: function() {
    var cookiesObj = {}; // an empty object to store retrieved cookies.
    var cookies = document.cookie ? document.cookie.split('; ') : [];
    var len = cookies.length; // length of keys.
    var cookie;

    if (!cookies) {
        return cookiesObj;
    }

    while (len--) {
        cookie = cookies[len].split('=');
        cookiesObj[decodeURIComponent(cookie[0])] = decodeURIComponent(cookie[1]);
    }

    return cookiesObj;
},

. . .
Remove
Removing a cookie is really easy. First we check the params object, then we call the read method we defined above providing the params object and return true if removed otherwise we return false. You can also remove cookie by specifying the path or domain. But it's safe to use the cookie name.
. . .

read: function(params) {...},
create: function(params) {...},
exists: function(params) {...},
listAsObject: function() {...},
remove: function(params) {
    if (!params) return;

    if (this.read(params)) {
        return this.create({
            name: params.name,
            value: ' ', // set value to empty string
            expires: -1, // reset expires
            path: params.path,
            domain: params.domain
        });
    }

    return false;
}

. . .
Conclusion
Congrats! By now you should have a more detailed understanding of how cookies work and should be able to implement other methods like getting all the keys or clearing all cookies. If you need deeper information, I recommend you check out MDN. You can find the full code for this tutorial here.
Make sure to leave any thoughts, questions or concerns in the comments below. I would love to see them.