JavaScript Cookie Object using Prototype

I have been using Prototype for the last year. I am still relatively new, but so far it does everything I need it to do natively. I have build many reusable scripts that do everything from adding simple events to automatic front-end form validation. With additional visual effects by the partnering Scriptaculous library, there isn’t much left. That is until I came across the need for front-end cookie management.

Why Front-End Cookie Management

As a back-end programmer, front-end cookie management seems silly. Why would I need or want to use something like JavaScript to manage cookies. Until recently, I would have just used PHP or Ruby for the job. However, I have found myself on contract as Lead Front-End developer. As such, those technologies are not available to me. Furthermore, tasking someone down on the IT side of the house can be a time consuming hassle. And came you blame them? I’m able to reverse the roles, and if some marketing guy asked me to set up a cookie to store XYZ, I’d laugh. Rightly so too, why should I waste such time storing things like text size, toggled modules, etc on the back-end. After all, aren’t such examples why cookies exist. User settings or preferences may necessitate back-end involvement, and these can be stored in a cookie for convenience. Yet, something dealing with the UI doesn’t really warrant involvement.

Where’s the Cookie Monster

So the obvious choice to manage cookies on front-end is JavaScript. During research, I came across two interesting pages. The first was a collection of Top 10 JavaScripts, with the top being cookie management functions ported from PHP. Second was a JavaScript class called CookieJar. At first, I didn’t quite understand the point. See to JavaScript the cookie comes across as a simple key value pair as semi-colon separate string in document.cookie. If you want to track several variables, you would need to set as many cookies. That would get old… err stale. Anyway, instead of having all these cookies floating around, the JavaScript CookieJar organized then for you.

Enter Prototype

CookieJar was a little primitive. The premise, to store the variables as a hash in a single cookie, was sound. But it didn’t actually handle cookie storage. Now this may be kitchen for the Object Oriented elitists, but I merged the two scripts. To regain some ground, I wanted my Cookie class to be a Singleton Gateway. As such, it should do everything required to access, manage, and maintain the Cookie. Since I was already using Prototype, I took advantage of its Hash object. I ended up with the script below.

var Cookie = {
  data: {},
  options: {expires: 1, domain: "", path: "", secure: false},

init: function(options, data) {
  Cookie.options = Object.extend(Cookie.options, options || {});

  var payload = Cookie.retrieve();
        if(payload) {
            Cookie.data = payload.evalJSON();
        }
        else {
            Cookie.data = data || {};
        }
        Cookie.store();
    },
    getData: function(key) {
        return Cookie.data[key];
    },
    setData: function(key, value) {
        Cookie.data[key] = value;
        Cookie.store();
    },
    removeData: function(key) {
        delete Cookie.data[key];
        Cookie.store();
    },
    retrieve: function() {
        var start = document.cookie.indexOf(Cookie.options.name + "=");

        if(start == -1) {
            return null;
        }
        if(Cookie.options.name != document.cookie.substr(start, Cookie.options.name.length)) {
            return null;
        }

        var len = start + Cookie.options.name.length + 1;
        var end = document.cookie.indexOf(';', len);

        if(end == -1) {
            end = document.cookie.length;
        }
        return unescape(document.cookie.substring(len, end));
    },
    store: function() {
        var expires = '';

        if (Cookie.options.expires) {
            var today = new Date();
            expires = Cookie.options.expires * 86400000;
            expires = ';expires=' + new Date(today.getTime() + expires);
        }

        document.cookie = Cookie.options.name + '=' + escape(Object.toJSON(Cookie.data)) + Cookie.getOptions() + expires;
    },
    erase: function() {
        document.cookie = Cookie.options.name + '=' + Cookie.getOptions() + ';expires=Thu, 01-Jan-1970 00:00:01 GMT';
    },
    getOptions: function() {
        return (Cookie.options.path ? ';path=' + Cookie.options.path : '') + (Cookie.options.domain ? ';domain=' + Cookie.options.domain : '') + (Cookie.options.secure ? ';secure' : '');
    }
};

Example Usage

Currently, the Cookie class only handles a single named cookie. This is acceptable since you can store multiple variables in a single cookie. However, I want to refactor this class from a Singleton to a Factory. Look for that in the future. In the meantime, here are some current sample uses:

Cookie that expires 90 days from visit, and sets a value:

    Cookie.init({name: 'yourdata', expires: 90});
    Cookie.setData('favorites', false);

Cookie that only lasts the session, with default data:

    Cookie.init({name: 'mydata'}, {foo: 'bar', x: 0});
    alert(Cookie.getData('foo'));

A Few Sweet Spots

I wanted all the cookie variables to be stored in a single cookie. The class does allow you to still make independent Cookies. In order to store data, I would need some format to store my Cookie data. What better than JSON, and Prototype has a toJSON method.

I also wanted to encapsulate the cookie data. Plus since I was storing the cookie data as a Hash, I may have future changes. So the getData, setData accessor methods can be used for data management. And In Rails-esk fashion, I auto-save the cookie at the end of setData.

Finally, the Cookie auto-loads or is auto-created depending on the name passed to init. It will look in document.cookie and if it doesn’t exist it will create a cookie with the settings provided. Finally, it loads the cookie data.

In Closing

Prototype did not natively extend JavaScript with a Cookie object. However, by leveraging a few other classes, and the scripts mentioned above, I came up with a quick solution. Given, this class does not contain everything and could benefit from a code review. In the future, I may revisit the loading and possibly build some convenience methods to allow Cookie['key'] instead of Cookie.getData('key'). Yet for what it does in 60 lines, it is more than able to handle my front-end needs.

24 thoughts on “JavaScript Cookie Object using Prototype

  1. I’ve tried using your code above but discovered some problems with Firefox 3.0.4 and Safari on Mac OS X.

    Sometimes when I tried to init the cookie with the Cookie.init({name: ‘foo’}) function I ended up with the error message: ‘Cookie.data is undefined’.

    • Sorenso,

      Thanks for the feedback, I reviewed the error and you are not setting initial cookie data. However, the code should be more robust. I have modified the code above, but the init function will now have the line

      Cookie.data = data || {};

      This essentially makes data an option parameter with an empty hash as a default.

      I also fixed my posting a comment. Thanks again.

  2. I believe there’s an error in the line:

    if(Cookie.options.name != document.cookie.substring(start, Cookie.options.name.length))

    javascript substring’s 2nd argument should be the index of the last character. Not the length. Look at the next definition:

    String.substr(start, length) //=ok

    String.substring(start, end) //=ok

    Your code worked with me when I replaced substring with substr.

    • Chris,

      Excellent spot. Sometimes all the languages get jumbled. substr is indeed the function I want. I only tested with one cookie (my bad), and therefore never reached a case where this function difference caused an error.

  3. Hey Jason,

    Very useful script, thanks for putting it together!

    In troubleshooting some issues with Safari’s cookie handling (not saving a cookie with a null expiration date) — I made a slight improvement to the store method to omit the expiration entirely.

    Thanks again for your work,

    Lance

  4. Great job.

    Just a remark.

    Your second example does not set a cookie that lasts for a session. It lasts a day.

    Best regards

  5. Great job.

    Your other example does not set cookie that lasts for a session.

    Because of your default constructor, it lasts for a day.

    options: { expires: 1

    should be

    options: { expires: null

    Best regards

  6. What’s the github repository? Like the above comment says, its difficult to find by searching due to the number of results :(

    Sounds like the above isn’t the latest code then?

    Thanks for a cool project!

  7. In the header

    window.onload = function() {

    inita();

    }

    function inita(){

    …things

    Cookie.init({name:’help’});

    if(Cookie.getData(‘show’)==null){

    alert(‘first show’);

    Cookie.init({name:’help’, expires: null}, {show:’remind’});

    }else if..

    }

    … things

    The alert shows, however what is placed into the cookie is "%7B%7D". Is there an issue with using this in the window.onload function.

  8. Hi Jason,

    where can i get an updated version of your Cookie Object. It is not working for me in safari and IE.

    Thanks!

  9. How to get your updated Cookie Object?

    I searched "Code" on GitHub for your "Cookie Object" and got the responses of "Code (9510)". I am not sure which one is yours.

  10. Glad to hear it. I see a lot of JQuery usage these days and am using it currently myself. However, as a developer I am naturally drawn to Prototype, with its more robust framework and low-level methods.

  11. Thank you for the script. I made a few additions that I thought someone might want.

    I removed the 4096 byte limitation per cookie that some browsers have by creating more cookies as needed.

    I would have loved to use localStorage but Opera Mobile 10 doesn’t support that feature so I had to hack something to make up for the shortcoming.

    There’s plenty of room for improvement, speed and space wise but I believe in the release early, release often mantra.

    Cheers!

    [code]

    var Cookie = {

    data: $H(),

    options: { expires: 1, domain: "", path: "", secure: false, max_cookie_size: 3840, max_cookie_count: 30 },

    init: function(options, data) {

    Cookie.options = Object.extend(Cookie.options, options || $H());

    // Import document.cookie into the Cookie.data hash

    Cookie._import()

    }

    , getItem: function(key) {

    if (!key) return Cookie.data.find(function() { return true })

    return Cookie.data.get(key);

    }

    , setItem: function(key, value) {

    Cookie.data.set(key, value);

    Cookie._store();

    }

    , removeItem: function(key) {

    Cookie.data.unset(key);

    Cookie._store();

    }

    , itemCount: function() {

    return Cookie.data.size()

    }

    , getOptions: function() {

    return (Cookie.options.path ? ';path=' + Cookie.options.path : '') + (Cookie.options.domain ? ';domain=' + Cookie.options.domain : '') + (Cookie.options.secure ? ';secure' : '');

    }

    , _import: function() {

    if (document.cookie.length == 0) return

    document.cookie.split(';').each(function(cookie) {

    var c = unescape(cookie)

    var eq_i = c.indexOf('=')

    $H(c.substring(eq_i + 1).evalJSON()).each(function(i) {

    Cookie.data.set(i.key, i.value)

    })

    Cookie._erase(c.substring(0, eq_i))

    })

    Cookie._store()

    }

    , _erase: function(cookie_name) {

    document.cookie = cookie_name + '=' + Cookie.getOptions() + ';expires=Thu, 01-Jan-1970 00:00:01 GMT';

    }

    , _eraseAll: function() {

    if (document.cookie.length == 0) return

    document.cookie.split(';').each(function(cookie) {

    var c = unescape(cookie)

    var eq_i = c.indexOf('=')

    Cookie._erase(c.substring(0, eq_i))

    })

    }

    , _store: function() {

    if (Cookie.data.size() == 0) {

    Cookie._eraseAll()

    return

    }

    var expires = '';

    if (Cookie.options.expires) {

    var today = new Date();

    expires = Cookie.options.expires * 86400000;

    expires = ';expires=' + new Date(today.getTime() + expires)

    }

    var cookie_index = 0

    var available_cookie_space = Cookie.options.max_cookie_size

    var tmp_entries = {}

    // create a new cookie for each max_cookie_size

    Cookie.data.each(function(entry) {

    tmp_entries[entry.key] = entry.value

    var tmp_entries_string = escape(Object.toJSON(tmp_entries))

    if (tmp_entries_string.length > available_cookie_space) {

    tmp_entries = {}

    tmp_entries[entry.key] = entry.value

    cookie_index++

    document.cookie = Cookie.options.name + cookie_index + '=' + escape(Object.toJSON(tmp_entries)) + Cookie.getOptions() + expires

    available_cookie_space = Cookie.options.max_cookie_size

    } else {

    document.cookie = Cookie.options.name + cookie_index + '=' + escape(Object.toJSON(tmp_entries)) + Cookie.getOptions() + expires

    available_cookie_space -= tmp_entries_string.length

    }

    })

    }

    };

    [/code]

  12. Richard,

    These are excellent additions. I have had several now from other people as well. There is a project on the horizon that will require heavy cookie usage. So I plan to implement all of these additions, and post the updated Cookie Object on GitHub. Look for an update in the next few weeks.

  13. Jason, That would be great.

    I’ve added a few more changes. Mostly cleanup with a nice bug squashed. Opera could not understand the expiry date with the old version so that’s fixed now.

  14. Do your cookie program or my program has bug?

    I want to keep the value of the user input (e.g. text box value) displayed after page refresh by a javascript.

    The following test web page is used.

    The alerts appear before and after the page refresh as expected on all browsers (FF, Chrome, Opera & IE).

    The input value in a text box still appear after page refresh for IE but not FF, Chrome, Opera.

    Please help to make it work for FF, Chrome, Opera.

    Cookie

    Rows Per Page

    var rowsPerPage;

    var rowsPerPageElem = document.getElementById("rowsPerPage");

    if (rowsPerPageElem.value == ”) {

    rowsPerPage = Cookie.getData(‘rowsPerPage’);

    if (rowsPerPage != undefined && rowsPerPage != ”)

    rowsPerPageElem.value = rowsPerPage;

    }

    alert(‘rowsPerPageElem.value ‘ + rowsPerPageElem.value);

    function init(v) {

    Cookie.init({name: ‘mydata4′, expires: 90});

    }

    function savevalue() {

    var rowsPerPageElem = document.getElementById("rowsPerPage");

    alert(‘savevalue rowsPerPageElem.value ‘ + rowsPerPageElem.value);

    Cookie.setData(‘rowsPerPage’, rowsPerPageElem.value);

    window.location.reload(true);

    }

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>