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.
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.
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.
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
Lance,
You are welcome and thanks for providing code. Looks great, I am going to try it out and I will update the script and post a download link in the article.
Can you post a simple example of this in use?
Wow, how lame of me. I didn’t realize there was no example. I have added a section above with examples. Thanks!
Great job.
Just a remark.
Your second example does not set a cookie that lasts for a session. It lasts a day.
Best regards
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
Can’t be bothered doing AJAX to set a cookie for this one special case, great that I can use your code to set it client side.
Thank you for sharing, I really appreciate it!
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!
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.
Hi Jason,
where can i get an updated version of your Cookie Object. It is not working for me in safari and IE.
Thanks!
Thanks for this, it saved me a bunch of time.
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.
Nice prototype plugin, very powerful yet simple to use.
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.
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]
Good Stuff Jason — thanks for your efforts.
I am using this now in a project where prototype & jquery are being used concurrently.
JQuery has a plugin for cookie management (plugins.jquery.com/files/jquery.cookie.js.txt ), but I liked your implementation in prototype better
cheers
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.
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.
I will be adding/roganizing the latest code to GitHub soon. Hopefully later this week.
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);
}