Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSON automatic handling #10

Merged
merged 1 commit into from
Apr 15, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 29 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ Cookies.set('name', 'value', { expires: 7, path: '/' });
Read cookie:

```javascript
Cookies.get('name'); // => "value"
Cookies.get('name'); // => 'value'
Cookies.get('nothing'); // => undefined
```

Read all available cookies:

```javascript
Cookies.get(); // => { "name": "value" }
Cookies.get(); // => { name: 'value' }
```

Delete cookie:
Expand All @@ -72,6 +72,28 @@ Cookies.remove('name', { path: '/' }); // => true

*Note: when deleting a cookie, you must pass the exact same path, domain and secure options that were used to set the cookie, unless you're relying on the default options that is.*

## JSON

js-cookie provides automatic JSON storage for cookies.

When creating a cookie you can pass an Array or Object Literal instead of a string in the value. If you do so, js-cookie store the string representation of the object according to the `JSON.stringify` api (if available):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly technical, it doesn't need to be an Array/Object literal, does it? E.g.

Cookies.set("foo", new Array("foo"))

In that case I'd rather write something like "...you can pass an instance of Array or Object..."

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we would be required to support it right? I don't see a clear reason for using an Array instance, if we were to support that then in that case we should for consistency also support Object instance, but Object can be anything, it can be a Date instance, a String instance, a custom object with methods without any relationship with key:value pairs, etc.

We could document that an "Array instance" is allowed, in this case it would work for Array constructors and literals, but then would we also support Objects for consistency? Do you think this is bad because ppl might read "Literals" as objects that should not be stored in variables? The intent here is just to limit the creation of the instances using literal notations, which AFAIK already handles all use-cases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could document that if it's not a valid JSON structure then it will use String(object) in the input, but then we would be enforcing behavior for invalid input, and any changes that does something else would be backwards incompatible.

Assuming we are using semver spec, backwards incompatible changes should only be release in major version (in this case v3.0).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but Object can be anything, it can be a Date instance, a String instance, a custom object with methods without any relationship with key:value pairs, etc.

Now I get your point. I was specifically remembering that once I needed to fix an issue so this works:

$.cookie("foo", new String("..."))

Than I thought, if we say "literals" we might be unnecessarily restricting users to use literals. I didn't think of all the other objects. I just thought new Array and new Object will probably work just as well already out of the box...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about:

When creating a cookie you can pass an array or plain object instead of a string

?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's use that!


```javascript
Cookies.set('name', { foo: 'bar' });
```

When reading a cookie with the default `Cookies.get()` api, you receive the stringified representation stored in the cookie:

```javascript
Cookies.get('name'); // => '{"foo":"bar"}'
```

When reading a cookie with the `Cookies.getJSON()` api, you receive the parsed representation of the string stored in the cookie according to the `JSON.stringify` api (if available):

```javascript
Cookies.getJSON('name'); // => { foo: 'bar' }
```

## RFC 6265

This project assumes [RFC 6265](http://tools.ietf.org/html/rfc6265#section-4.1.1) as a reference for everything. That said, some custom rules are applied in order to provide robustness and cross-browser compatibility.
Expand Down Expand Up @@ -120,11 +142,13 @@ If true, the cookie transmission requires a secure protocol (https). Default: `f
Provide a conversion function as optional last argument for reading, in order to change the cookie's value
to a different representation on the fly.

Example for parsing a value into a number:
Example for parsing the value from a cookie generated with PHP's `setcookie()` method:

```javascript
Cookies.set('foo', '42');
Cookies.get('foo', Number); // => 42
// 'cookie+with+space' => 'cookie with space'
Cookies.get('foo', function (value) {
return value.replace(/\+/g, ' ');
});
```

Dealing with cookies that have been encoded using `escape` (3rd party cookies):
Expand All @@ -133,8 +157,6 @@ Dealing with cookies that have been encoded using `escape` (3rd party cookies):
Cookies.get('foo', unescape);
```

You can pass an arbitrary conversion function.

## Contributing

Check out the [Contributing Guidelines](CONTRIBUTING.md)
Expand Down
56 changes: 40 additions & 16 deletions src/js.cookie.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,37 +29,52 @@
var unallowedCharsInValue = extend(unallowedChars, {
' ': '%20'
});
function encode(value, charmap) {

function encode (value, charmap) {
for (var character in charmap) {
value = value
.replace(new RegExp(character, 'g'), charmap[character]);
}
return value;
}

function decode(value, charmap) {
function decode (value, charmap) {
for (var character in charmap) {
value = value
.replace(new RegExp(charmap[character], 'g'), character);
}
return value;
}

function parseCookieValue(value) {
function processWrite (value) {
var stringified;
try {
stringified = JSON.stringify(value);
if (/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/.test(stringified)) {
value = stringified;
}
} catch(e) {}
return encode(String(value), unallowedCharsInValue);
}

function processRead (value, converter, json) {
if (value.indexOf('"') === 0) {
// This is a quoted cookie as according to RFC2068, unescape...
value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
}

return decode(value, unallowedCharsInValue);
}
value = decode(value, unallowedCharsInValue);

if (json) {
try {
value = JSON.parse(value);
} catch(e) {}
}

function read(s, converter) {
var value = parseCookieValue(s);
return isFunction(converter) ? converter(value) : value;
}

function extend() {
function extend () {
var key, options;
var i = 0;
var result = {};
Expand All @@ -72,15 +87,21 @@
return result;
}

function isFunction(obj) {
function isFunction (obj) {
return Object.prototype.toString.call(obj) === '[object Function]';
}

var api = function (key, value, options) {
var converter;

if (isFunction(value)) {
converter = value;
value = undefined;
}

// Write

if (arguments.length > 1 && !isFunction(value)) {
if (arguments.length > 1 && !converter) {
options = extend(api.defaults, options);

if (typeof options.expires === 'number') {
Expand All @@ -89,7 +110,7 @@
}

return (document.cookie = [
encode(key, unallowedCharsInName), '=', encode(String(value), unallowedCharsInValue),
encode(key, unallowedCharsInName), '=', processWrite(value),
options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
options.path ? '; path=' + options.path : '',
options.domain ? '; domain=' + options.domain : '',
Expand All @@ -113,21 +134,24 @@
cookie = parts.join('=');

if (key === name) {
// If second argument (value) is a function it's a converter...
result = read(cookie, value);
result = processRead(cookie, converter, this.json);
break;
}

// Prevent storing a cookie that we couldn't decode.
if (!key && (cookie = read(cookie)) !== undefined) {
result[name] = cookie;
if (!key) {
result[name] = processRead(cookie, converter, this.json);
}
}

return result;
};

api.get = api.set = api;
api.getJSON = function() {
return api.get.apply({
json: true
}, [].slice.call(arguments));
};
api.defaults = {};

api.remove = function (key, options) {
Expand Down
90 changes: 83 additions & 7 deletions test/tests.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// https://github.com/axemclion/grunt-saucelabs#test-result-details-with-qunit
(function() {
(function () {
"use strict";

var log = [];
Expand Down Expand Up @@ -216,10 +216,17 @@ test('defaults', function () {
ok(Cookies.set('c', 'v', { path: '/bar' }).match(/path=\/bar/), 'options argument has precedence');
});

test('Quote as the first character in the cookie value', function () {
expect(1);
Cookies.set('c', '"');
strictEqual(Cookies.get('c'), '"', 'should handle the quote character');
test('Quote in the cookie value', function () {
expect(3);

Cookies.set('quote', '"');
strictEqual(Cookies.get('quote'), '"', 'should print the quote character');

Cookies.set('without-last', '"content');
strictEqual(Cookies.get('without-last'), '"content', 'should print the quote character');

Cookies.set('without-first', 'content"');
strictEqual(Cookies.get('without-first'), 'content"', 'should print the quote character');
});

test('RFC 6265 disallowed characters in cookie-octet', function () {
Expand Down Expand Up @@ -342,10 +349,79 @@ test('[] used in name', function () {
strictEqual(document.cookie, '', 'delete the cookie');
});

module('conversion', lifecycle);
module('converters', lifecycle);

test('read converter', function() {
test('read converter', function () {
expect(1);
Cookies.set('c', '1');
strictEqual(Cookies.get('c', Number), 1, 'converts read value');
});

module('JSON handling', lifecycle);

test('Number', function () {
expect(2);
Cookies.set('c', 1);
strictEqual(Cookies.getJSON('c'), 1, 'should handle a Number');
strictEqual(Cookies.get('c'), '1', 'should return a String');
});

test('Boolean', function () {
expect(2);
Cookies.set('c', true);
strictEqual(Cookies.getJSON('c'), true, 'should handle a Boolean');
strictEqual(Cookies.get('c'), 'true', 'should return a Boolean');
});

test('Array Literal', function () {
expect(2);
Cookies.set('c', ['v']);
deepEqual(Cookies.getJSON('c'), ['v'], 'should handle Array Literal');
strictEqual(Cookies.get('c'), '["v"]', 'should return a String');
});

test('Array Constructor', function () {
/*jshint -W009 */
expect(2);
var value = new Array();
value[0] = 'v';
Cookies.set('c', value);
deepEqual(Cookies.getJSON('c'), ['v'], 'should handle Array Constructor');
strictEqual(Cookies.get('c'), '["v"]', 'should return a String');
});

test('Object Literal', function () {
expect(2);
Cookies.set('c', {k: 'v'});
deepEqual(Cookies.getJSON('c'), {k: 'v'}, 'should handle Object Literal');
strictEqual(Cookies.get('c'), '{"k":"v"}', 'should return a String');
});

test('Object Constructor', function () {
/*jshint -W010 */
expect(2);
var value = new Object();
value.k = 'v';
Cookies.set('c', value);
deepEqual(Cookies.getJSON('c'), {k: 'v'}, 'should handle Object Constructor');
strictEqual(Cookies.get('c'), '{"k":"v"}', 'should return a String');
});

test('Use String(value) for unsupported objects that do not stringify into JSON', function() {
expect(4);

Cookies.set('date', new Date(2015, 04, 13, 0, 0, 0, 0));
strictEqual(Cookies.get('date').indexOf('"'), -1, 'should not quote the stringified Date object');
strictEqual(Cookies.getJSON('date').indexOf('"'), -1, 'should not quote the stringified Date object');

Cookies.set('function', function (){});
strictEqual(Cookies.get('function'), undefined, 'should return undefined for function object');
strictEqual(Cookies.getJSON('function'), undefined, 'should return undefined for function object');
});

test('Call to read all cookies with mixed json', function () {
Cookies.set('c', { foo: 'bar' });
Cookies.set('c2', 'v');
deepEqual(Cookies.getJSON(), { c: { foo: 'bar' }, c2: 'v' }, 'returns JSON parsed cookies');
deepEqual(Cookies.get(), { c: '{"foo":"bar"}', c2: 'v' }, 'returns unparsed cookies');
});