diff --git a/CHANGELOG b/CHANGELOG index a7ca26b..b63fc52 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,29 @@ *SVN* +* Complete rewrite of the Hash class. + + !! BACKWARDS COMPATIBILITY CHANGE !! This new version of Hash is NOT backwards compatible with the former Hash class. + + Properties are now hidden away in an private store to prevent the risk of collision with Hash's instance and mixed-in methods. + This implies that properties of the hash can no longer be set, accessed or deleted directly: use the new Hash#get(key), Hash#set(key, value) and Hash#unset(key) instance methods instead. + + - Make $H(object) equivalent to new Hash(object). Both now return a new (cloned) instance of Hash in all circumstances. + - Make Hash#merge non-destructive. + + - Add Hash#update (a destructive version of Hash#merge). + - Add Hash#clone (returns a new, cloned instance of Hash). + - Add Hash#toObject (returns a clone of the contained object). + - Add Hash#get(key) (returns the value of the specified property). + - Add Hash#set(key, value) (sets the value of the given property. returns the value). + - Add Hash#unset(key) (deletes the specified property and returns its value). + - Add Hash.from as a alias to $H for consistency with Array.from. + - Add Object.toQueryString. + + - Deprecate Hash.toQueryString (use Object.toQueryString or the instance method Hash#toQueryString instead). + + - Remove Hash#remove (use Hash#unset instead). + - Remove Hash.toJSON (use Object.toJSON or the instance method Hash#toJSON instead). [sam, Tobie Langel] + * Element#wrap now returns the wrapper instead of the element being wrapped. [sam] * Namespace all custom event names to avoid conflicts with native DOM events. [sam] diff --git a/src/ajax.js b/src/ajax.js index 9a63358..09c7d88 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -85,7 +85,7 @@ Ajax.Request = Class.create(Ajax.Base, { this.parameters = params; - if (params = Hash.toQueryString(params)) { + if (params = Object.toQueryString(params)) { // when GET, append parameters to URL if (this.method == 'get') this.url += (this.url.include('?') ? '&' : '?') + params; diff --git a/src/base.js b/src/base.js index 638ab84..82234e1 100644 --- a/src/base.js +++ b/src/base.js @@ -97,6 +97,10 @@ Object.extend(Object, { return '{' + results.join(', ') + '}'; }, + toQueryString: function(object) { + return $H(object).toQueryString(); + }, + toHTML: function(object) { return object && object.toHTML ? object.toHTML() : String.interpret(object); }, diff --git a/src/deprecated.js b/src/deprecated.js index 59d91fe..cc1d7dd 100644 --- a/src/deprecated.js +++ b/src/deprecated.js @@ -1,5 +1,7 @@ /*------------------------------- DEPRECATED -------------------------------*/ +Hash.toQueryString = Object.toQueryString; + var Toggle = { display: Element.toggle }; Element.Methods.childOf = Element.Methods.descendantOf; diff --git a/src/form.js b/src/form.js index 3380505..0d823e8 100644 --- a/src/form.js +++ b/src/form.js @@ -25,7 +25,7 @@ var Form = { return result; }); - return options.hash ? data : Hash.toQueryString(data); + return options.hash ? data : Object.toQueryString(data); } }; @@ -132,7 +132,7 @@ Form.Element.Methods = { if (value != undefined) { var pair = { }; pair[element.name] = value; - return Hash.toQueryString(pair); + return Object.toQueryString(pair); } } return ''; diff --git a/src/hash.js b/src/hash.js index 2046a89..0f31759 100644 --- a/src/hash.js +++ b/src/hash.js @@ -1,132 +1,119 @@ -var Hash = function(object) { - if (object instanceof Hash) this.merge(object); - else Object.extend(this, object || { }); -}; - -Object.extend(Hash, { - toQueryString: function(obj) { - var parts = []; - parts.add = arguments.callee.addPair; - - this.prototype._each.call(obj, function(pair) { - if (!pair.key) return; - var value = pair.value; - - if (value && typeof value == 'object') { - if (Object.isArray(value)) value.each(function(value) { - parts.add(pair.key, value); - }); - return; - } - parts.add(pair.key, value); - }); - - return parts.join('&'); - }, - - toJSON: function(object) { - var results = []; - this.prototype._each.call(object, function(pair) { - var value = Object.toJSON(pair.value); - if (value !== undefined) results.push(pair.key.toJSON() + ': ' + value); - }); - return '{' + results.join(', ') + '}'; - } -}); - -Hash.toQueryString.addPair = function(key, value, prefix) { - key = encodeURIComponent(key); - if (value === undefined) this.push(key); - else this.push(key + '=' + (value == null ? '' : encodeURIComponent(value))); -}; - -Object.extend(Hash.prototype, Enumerable); -Object.extend(Hash.prototype, { - _each: function(iterator) { - for (var key in this) { - var value = this[key]; - if (value && value == Hash.prototype[key]) continue; - - var pair = [key, value]; - pair.key = key; - pair.value = value; - iterator(pair); - } - }, - - keys: function() { - return this.pluck('key'); - }, - - values: function() { - return this.pluck('value'); - }, - - index: function(value) { - var match = this.detect(function(pair) { - return pair.value === value; - }); - return match && match.key; - }, - - merge: function(hash) { - return $H(hash).inject(this, function(mergedHash, pair) { - mergedHash[pair.key] = pair.value; - return mergedHash; - }); - }, - - remove: function() { - var result; - for(var i = 0, length = arguments.length; i < length; i++) { - var value = this[arguments[i]]; - if (value !== undefined){ - if (result === undefined) result = value; - else { - if (!Object.isArray(result)) result = [result]; - result.push(value); - } - } - delete this[arguments[i]]; - } - return result; - }, - - toQueryString: function() { - return Hash.toQueryString(this); - }, - - inspect: function() { - return '#'; - }, - - toJSON: function() { - return Hash.toJSON(this); - } -}); - function $H(object) { - if (object instanceof Hash) return object; return new Hash(object); }; -// Safari iterates over shadowed properties -if (function() { - var i = 0, Test = function(value) { this.key = value }; - Test.prototype.key = 'foo'; - for (var property in new Test('bar')) i++; - return i > 1; -}()) Hash.prototype._each = function(iterator) { - var cache = []; - for (var key in this) { - var value = this[key]; - if ((value && value == Hash.prototype[key]) || cache.include(key)) continue; - cache.push(key); - var pair = [key, value]; - pair.key = key; - pair.value = value; - iterator(pair); +var Hash = Class.create(Enumerable, (function() { + if (function() { + var i = 0, Test = function(value) { this.key = value }; + Test.prototype.key = 'foo'; + for (var property in new Test('bar')) i++; + return i > 1; + }()) { + function each(iterator) { + var cache = []; + for (var key in this._object) { + var value = this._object[key]; + if (cache.include(key)) continue; + cache.push(key); + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + } + } else { + function each(iterator) { + for (var key in this._object) { + var value = this._object[key], pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + } } -}; + + function toQueryPair(key, value) { + if (Object.isUndefined(value)) return key; + return key + '=' + encodeURIComponent(String.interpret(value)); + } + + return { + initialize: function(object) { + this._object = object instanceof Hash ? object.toObject() : Object.clone(object); + }, + + _each: each, + + set: function(key, value) { + return this._object[key] = value; + }, + + get: function(key) { + return this._object[key]; + }, + + unset: function(key) { + var value = this._object[key]; + delete this._object[key]; + return value; + }, + + toObject: function() { + return Object.clone(this._object); + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + index: function(value) { + var match = this.detect(function(pair) { + return pair.value === value; + }); + return match && match.key; + }, + + merge: function(object) { + return this.clone().update(object); + }, + + update: function(object) { + return new Hash(object).inject(this, function(result, pair) { + result.set(pair.key, pair.value); + return result; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + var key = encodeURIComponent(pair.key), values = pair.value; + + if (values && typeof values == 'object') { + if (Object.isArray(values)) + return values.map(toQueryPair.curry(key)).join('&'); + } + return toQueryPair(key, values); + }).join('&'); + }, + + inspect: function() { + return '#'; + }, + + toJSON: function() { + return Object.toJSON(this.toObject()); + }, + + clone: function() { + return new Hash(this); + } + } +})()); + +Hash.from = $H; \ No newline at end of file diff --git a/test/unit/base.html b/test/unit/base.html index bd3feab..2b5e37b 100644 --- a/test/unit/base.html +++ b/test/unit/base.html @@ -230,7 +230,11 @@ assertHashEqual({foo: 'foo', bar: [1, 2, 3], bla: null}, Object.extend(object, {bla: null})); }}, - + + testObjectToQueryString: function() { with(this) { + assertEqual('a=A&b=B&c=C&d=D%23', Object.toQueryString({a: 'A', b: 'B', c: 'C', d: 'D#'})); + }}, + testObjectClone: function() { with(this) { var object = {foo: 'foo', bar: [1, 2, 3]}; assertNotIdentical(object, Object.clone(object)); diff --git a/test/unit/form.html b/test/unit/form.html index 4adcb2e..f5dad10 100644 --- a/test/unit/form.html +++ b/test/unit/form.html @@ -368,7 +368,7 @@ // return params assertHashEqual(expected, Form.serialize('various', true)); // return string - assertEnumEqual(Hash.toQueryString(expected).split('&').sort(), + assertEnumEqual(Object.toQueryString(expected).split('&').sort(), Form.serialize('various').split('&').sort()); assertEqual('string', typeof $('form').serialize({ hash:false })); diff --git a/test/unit/hash.html b/test/unit/hash.html index 2c8098c..9d2d186 100644 --- a/test/unit/hash.html +++ b/test/unit/hash.html @@ -50,28 +50,61 @@ value_undefined: { a:"b", c:undefined }, value_null: { a:"b", c:null }, - value_zero: { a:"b", c:0 }, - - dangerous: { - _each: 'E', - map: 'M', - keys: 'K', - values: 'V', - collect: 'C', - inject: 'I' - } + value_zero: { a:"b", c:0 } }; new Test.Unit.Runner({ + testSet: function(){ with(this) { + var h = $H({a: 'A'}) + + assertEqual('B', h.set('b', 'B')); + assertHashEqual({a: 'A', b: 'B'}, h); + + assertUndefined(h.set('c')); + assertHashEqual({a: 'A', b: 'B', c: undefined}, h); + }}, + + testGet: function(){ with(this) { + assertEqual('A', $H({a: 'A'}).get('a')); + assertUndefined($H({}).get('a')); + }}, + + testUnset: function(){ with(this) { + var hash = $H(Fixtures.many); + assertEqual('B', hash.unset('b')); + assertHashEqual({a:'A', c: 'C', d:'D#'}, hash); + assertUndefined(hash.unset('z')); + assertHashEqual({a:'A', c: 'C', d:'D#'}, hash); + }}, + + testToObject: function(){ with(this) { + var hash = $H(Fixtures.many), object = hash.toObject(); + assertInstanceOf(Object, object); + assertHashEqual(Fixtures.many, object); + assertNotIdentical(Fixtures.many, object); + hash.set('foo', 'bar'); + assertHashNotEqual(object, hash.toObject()); + }}, testConstruct: function(){ with(this) { - var h = $H(Fixtures.one); - assertNotIdentical(Fixtures.one, h); - assertIdentical(h, $H(h)); - - var h2 = new Hash(h); - assertNotIdentical(h, h2); - assertHashEqual(h, h2); + var object = Object.clone(Fixtures.one); + var h = new Hash(object), h2 = $H(object); + assertInstanceOf(Hash, h); + assertInstanceOf(Hash, h2); + + assertHashEqual({}, new Hash()); + assertHashEqual(object, h); + assertHashEqual(object, h2); + + h.set('foo', 'bar'); + assertHashNotEqual(object, h); + + var clone = $H(h); + assertInstanceOf(Hash, clone); + assertHashEqual(h, clone); + h.set('foo', 'foo'); + assertHashNotEqual(h, clone); + assertIdentical($H, Hash.from); }}, testKeys: function(){ with(this) { @@ -87,8 +120,8 @@ assertEnumEqual($w('A B C D#'), $H(Fixtures.many).values().sort()); assertEnumEqual($w('function function'), $H(Fixtures.functions).values().map(function(i){ return typeof i })); - assertEqual(9, $H(Fixtures.functions).quad(3)); - assertEqual(6, $H(Fixtures.functions).plus(3)); + assertEqual(9, $H(Fixtures.functions).get('quad')(3)); + assertEqual(6, $H(Fixtures.functions).get('plus')(3)); }}, testIndex: function(){ with(this) { @@ -104,22 +137,32 @@ }}, testMerge: function(){ with(this) { - assertHashEqual(Fixtures.many, $H(Fixtures.many).merge()); - assertHashEqual(Fixtures.many, $H(Fixtures.many).merge({})); - assertHashEqual(Fixtures.many, $H(Fixtures.many).merge($H())); - assertHashEqual({a:'A', b:'B', c:'C', d:'D#', aaa:'AAA' }, $H(Fixtures.many).merge({aaa: 'AAA'})); - assertHashEqual({a:'A#', b:'B', c:'C', d:'D#' }, $H(Fixtures.many).merge(Fixtures.one)); + var h = $H(Fixtures.many); + assertNotIdentical(h, h.merge()); + assertNotIdentical(h, h.merge({})); + assertInstanceOf(Hash, h.merge()); + assertInstanceOf(Hash, h.merge({})); + assertHashEqual(h, h.merge()); + assertHashEqual(h, h.merge({})); + assertHashEqual(h, h.merge($H())); + assertHashEqual({a:'A', b:'B', c:'C', d:'D#', aaa:'AAA' }, h.merge({aaa: 'AAA'})); + assertHashEqual({a:'A#', b:'B', c:'C', d:'D#' }, h.merge(Fixtures.one)); }}, - - testRemove: function(){ with(this) { - var hash = $H(Fixtures.many); - var values = hash.remove('b', 'c'); - assertHashEqual({a:'A', d:'D#'}, hash); - assertEnumEqual($w('B C'), values); + + testUpdate: function(){ with(this) { + var h = $H(Fixtures.many); + assertIdentical(h, h.update()); + assertIdentical(h, h.update({})); + assertHashEqual(h, h.update()); + assertHashEqual(h, h.update({})); + assertHashEqual(h, h.update($H())); + assertHashEqual({a:'A', b:'B', c:'C', d:'D#', aaa:'AAA' }, h.update({aaa: 'AAA'})); + assertHashEqual({a:'A#', b:'B', c:'C', d:'D#', aaa:'AAA' }, h.update(Fixtures.one)); }}, testToQueryString: function(){ with(this) { assertEqual('', $H({}).toQueryString()); + assertEqual('a%23=A', $H({'a#': 'A'}).toQueryString()); assertEqual('a=A%23', $H(Fixtures.one).toQueryString()); assertEqual('a=A&b=B&c=C&d=D%23', $H(Fixtures.many).toQueryString()); assertEqual("a=b&c", $H(Fixtures.value_undefined).toQueryString()); @@ -132,10 +175,7 @@ assertEqual("", $H(Fixtures.multiple_empty).toQueryString()); assertEqual("stuff%5B%5D=%24&stuff%5B%5D=a&stuff%5B%5D=%3B", $H(Fixtures.multiple_special).toQueryString()); assertHashEqual(Fixtures.multiple_special, $H(Fixtures.multiple_special).toQueryString().toQueryParams()); - - var danger = $w("_each=E collect=C inject=I keys=K map=M values=V"); - assertEnumEqual(danger, Hash.toQueryString(Fixtures.dangerous).split('&').sort()); - assertEnumEqual(danger, $H(Fixtures.dangerous).toQueryString().split('&').sort()); + assertIdentical(Object.toQueryString, Hash.toQueryString); }}, testInspect: function(){ with(this) { @@ -143,12 +183,17 @@ assertEqual("#", $H(Fixtures.one).inspect()); assertEqual("#", $H(Fixtures.many).inspect()); }}, + + testClone: function(){ with(this) { + var h = $H(Fixtures.many); + assertHashEqual(h, h.clone()); + assertInstanceOf(Hash, h.clone()); + assertNotIdentical(h, h.clone()); + }}, testToJSON: function(){ with(this) { assertEqual('{\"b\": [false, true], \"c\": {\"a\": \"hello!\"}}', $H({'b': [undefined, false, true, undefined], c: {a: 'hello!'}}).toJSON()); - - assertEqual('E', eval('(' + $H(Fixtures.dangerous).toJSON() + ')')._each); }} }, 'testlog'); // ]]> diff --git a/test/unit/unit_tests.html b/test/unit/unit_tests.html index d21a42a..b98ed27 100644 --- a/test/unit/unit_tests.html +++ b/test/unit/unit_tests.html @@ -58,7 +58,7 @@ testIsRunningFromRake: function() { with(this) { if (window.location.toString().startsWith('http')) { assert(isRunningFromRake); - info('These tests are runingn from rake.') + info('These tests are running from rake.') } else { assert(!isRunningFromRake); info('These tests are *not* running from rake.')