Comparing version 0.3.1 to 0.4.0
{ | ||
"name": "corridor", | ||
"version": "0.3.1", | ||
"version": "0.4.0", | ||
"description": "JSON/HTML data corridor for data-binding", | ||
@@ -5,0 +5,0 @@ "repository": { |
@@ -337,2 +337,49 @@ # corridor | ||
### merging arrays | ||
Merging arrays can be tricky, but in most cases corridor will make a good choice. | ||
In the last section, we looked at an example where the `authors` array contains normal string values. | ||
But let's look at what happens when the values are more complex. | ||
Consider this HTML: | ||
```html | ||
<table data-name="company.employees[]"> | ||
<tr> | ||
<td><input type="text" name="name" value="Bob" /></td> | ||
<td><input type="text" name="email" value="bob@company.com" /></td> | ||
</tr> | ||
<tr> | ||
<td><input type="text" name="name" value="Alice" /></td> | ||
<td><input type="text" name="email" value="alice@company.com" /></td> | ||
</tr> | ||
</table> | ||
``` | ||
For this, `corridor()` produces the following: | ||
```js | ||
{ | ||
"company": { | ||
"employees": [{ | ||
"name": "Bob", | ||
"email": "bob@company.com" | ||
},{ | ||
"name": "Alice", | ||
"email": "alice@company.com" | ||
}] | ||
} | ||
} | ||
``` | ||
The reason this works is that corridor checks each field under an arry to see if it can be safely merged into the last one. | ||
So when it finds Bob's `email`, it knows that it can safely add this key to the preceding Bob object without destroying data. | ||
But when it gets to Alice's `name`, it sees that it couldn't safely add the value. | ||
If it set the last object's `name` to Alice, then the `name` of Bob would be lost. | ||
So it creates a new element and sets its `name` instead. | ||
For more information on array merging, and how you can control it, see the API documentation. | ||
### toggling sections | ||
@@ -346,3 +393,3 @@ | ||
```html | ||
<fieldset data-role="role"> | ||
<fieldset data-role="toggleable"> | ||
<p> | ||
@@ -436,4 +483,4 @@ <label> | ||
corridor(root, data); | ||
corridor(null, data, options); | ||
corridor(root, data, options); | ||
corridor(null, data, opts); | ||
corridor(root, data, opts); | ||
``` | ||
@@ -516,2 +563,35 @@ | ||
##### merge options | ||
The `merge` option indicates which merging strategy corridor should use when merging two arrays. | ||
Choices are: | ||
* _auto_ - intelligently choose whether to concatenate the arrays, or deep merge them (default) | ||
* _concat_ - concatenate the arrays | ||
* _extend_ - deep merge each pair of items | ||
When in `auto` mode, the algorithm for choosing whether to concatenate or merge two arrays should work as follows: | ||
* if the original array is empty, choose `concat`, otherwise, | ||
* if the length of the other array is greater than one, choose `concat` (this is a strange case), otherwise, | ||
* determine if the last element of the original array can be safely merged with the first element of the second array, if so, recursively merge them, otherwise, | ||
* choose `concat`. | ||
The algorithm for deciding whether an object can be safely merged into a base object is as follows: | ||
* if either argument is a primitive (not an object or array), return false, otherwise, | ||
* if either argument is an array, return true (arrays can always be safely merged), otherwise, | ||
* recursively check the keys of the other object, if any can't be safely merged, return false, | ||
* return true. | ||
Both _auto_ and _concat_ are safe operations. | ||
In neither case is data lost. | ||
However, _extend_ is potentially (likely) unsafe—with this strategy, data is easily clobbered. | ||
In all cases, if either the original or other object is not an array, there is no ambiguity to resolve. | ||
When at least one argument is not array-like, the merge will produce an object such that no information is lost (other than what is specifically overwritten by colliding keys). | ||
For example, if the original object is an array (`["foo"]`) and the other object is a non-array-like object (`{"bar":"baz"}`). then the outcome of the merge will be an object that keeps all data in tact (`{"0":"foo","bar":"baz"}`). | ||
##### toggle options | ||
@@ -518,0 +598,0 @@ |
@@ -148,4 +148,4 @@ /** | ||
// build out full contribution | ||
contrib = buildup("\ufff0", elem, root); | ||
field = contrib.split("\ufff0").join('$$$'); | ||
contrib = buildup("\ufffc", elem, root); | ||
field = contrib.split("\ufffc").join('$$$'); | ||
@@ -161,3 +161,3 @@ // short-circuit if this field should be omitted | ||
// inject value into contribution | ||
value = contrib.replace("\ufff0", value); | ||
value = contrib.replace("\ufffc", value); | ||
@@ -210,4 +210,4 @@ // merge contribution into the result data | ||
// build up the target contribution | ||
// starting with "\ufff0" value tag | ||
target = JSON.parse(buildup(JSON.stringify("\ufff0"), elem, root)); | ||
// starting with the unicode object replacement character | ||
target = JSON.parse(buildup(JSON.stringify("\ufffc"), elem, root)); | ||
@@ -218,3 +218,3 @@ // insert into workspace | ||
// find path to target in workspace | ||
path = locate(workspace, "\ufff0"); | ||
path = locate(workspace, "\ufffc"); | ||
@@ -298,4 +298,13 @@ // set actual val into workspace to prevent false hits for future fields | ||
*/ | ||
enabledOnly: true | ||
enabledOnly: true, | ||
/** | ||
* Strategy to employ when merging two objects. | ||
* Recognized choices are: | ||
* - auto - intelligently merge the objects (default) | ||
* - concat - always concatenate arrays | ||
* - extend - iterate through items and merge them | ||
*/ | ||
merge: 'auto' | ||
}, | ||
@@ -406,3 +415,3 @@ | ||
field = "\ufff0", // start out with the target mark | ||
field = "\ufffc", // object replacement char to start | ||
@@ -417,8 +426,8 @@ parts = name | ||
p = p.replace(/^\s+|\s+$/g, ''); // trim each part | ||
field = field.replace("\ufff0", // add part to field specification | ||
p === '[]' ? "[\ufff0]" : "{" + JSON.stringify(p || 'undefined') + ":\ufff0}" | ||
field = field.replace("\ufffc", // add part to field specification | ||
p === '[]' ? "[\ufffc]" : "{" + JSON.stringify(p || 'undefined') + ":\ufffc}" | ||
); | ||
}); | ||
return field.split("\ufff0").join('$$$'); | ||
return field.split("\ufffc").join('$$$'); | ||
@@ -728,17 +737,47 @@ }, | ||
/** | ||
* Deep merge two plain object heirarchies. | ||
* Does not check for hasOwnProperty. | ||
* Does not deal with cyclical references (at all). | ||
* Concatenates arrays (rather than trying to merge their elements). | ||
* Doesn't guarantee that new cyclical relationships won't be created. | ||
* Doesn't guarantee good behavior when asymentrical types are encountered. | ||
* Deep merge one object into another. | ||
* | ||
* Notes: | ||
* - does not check for hasOwnProperty. | ||
* - does not deal with cyclical references (at all). | ||
* - concatenates arrays (rather than trying to merge their elements). | ||
* - doesn't guarantee that new cyclical relationships won't be created. | ||
* - doesn't guarantee good behavior when asymentrical types are encountered. | ||
* | ||
* @param {mixed} obj The base object to merge into. | ||
* @param {mixed} other The other object to merge into the base. | ||
* @param {mixed} opts Options to use for merge (optional). | ||
*/ | ||
merge = corridor.merge = function(obj, other) { | ||
merge = corridor.merge = function(obj, other, opts) { | ||
var i, ii, key; | ||
var | ||
strategy = defaults.merge, | ||
i, ii, key, tmp; | ||
if (toString.call(other) === '[object Array]') { | ||
if (opts && 'merge' in opts) { | ||
strategy = opts.merge; | ||
} | ||
if (arraylike(other)) { | ||
if (toString.call(obj) === '[object Array]') { | ||
for (i = 0, ii = other.length; i < ii; i++) { | ||
obj.push(other[i]); | ||
if (strategy === 'concat') { | ||
for (i = 0, ii = other.length; i < ii; i++) { | ||
obj.push(other[i]); | ||
} | ||
} else if (strategy === 'extend') { | ||
for (i = 0, ii = other.length; i < ii; i++) { | ||
obj[i] = merge(obj[i], other[i], opts); | ||
} | ||
} else { | ||
if (!obj.length || other.length > 1) { | ||
for (i = 0, ii = other.length; i < ii; i++) { | ||
obj.push(other[i]); | ||
} | ||
} else { | ||
if (safely(obj[obj.length - 1], other[0])) { | ||
obj[obj.length - 1] = merge(obj[obj.length - 1], other[0], opts); | ||
} else { | ||
obj.push(other[0]); | ||
} | ||
} | ||
} | ||
@@ -748,3 +787,3 @@ } else { | ||
if (i in obj && typeof obj[i] === 'object' && obj[i] !== null) { | ||
merge(obj[i], other[i]); | ||
obj[i] = merge(obj[i], other[i], opts); | ||
} else { | ||
@@ -756,5 +795,12 @@ obj[i] = other[i]; | ||
} else { | ||
if (toString.call(obj) === '[object Array]') { | ||
tmp = {}; | ||
for (i = 0, ii = obj.length; i < ii; i++) { | ||
tmp[i] = obj[i]; | ||
} | ||
obj = tmp; | ||
} | ||
for (key in other) { | ||
if (key in obj && typeof obj[key] === 'object' && obj[key] !== null) { | ||
merge(obj[key], other[key]); | ||
obj[key] = merge(obj[key], other[key], opts); | ||
} else { | ||
@@ -767,2 +813,88 @@ obj[key] = other[key]; | ||
return obj; | ||
}, | ||
/** | ||
* Determine whether a candidate object can be safely merged into a base object. | ||
* @param {mixed} obj The base object to test for merge safety. | ||
* @param {mixed} other The candidiate object to check for safe merge. | ||
*/ | ||
safely = corridor.safely = function(obj, other) { | ||
var | ||
typeObj = toString.call(obj), | ||
typeOther = toString.call(other), | ||
key; | ||
if (typeObj === '[object Array]') { | ||
if (typeOther === '[object Array]' || typeOther === '[object Object]') { | ||
return true; | ||
} | ||
} else if (typeObj === '[object Object]') { | ||
if (typeOther === '[object Array]') { | ||
return true; | ||
} | ||
if (typeOther === '[object Object]') { | ||
for (key in other) { | ||
if ((key in obj) && !safely(obj[key], other[key])) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
} | ||
return false; | ||
}, | ||
/** | ||
* Determine whether a given object can be converted to an array without losing data. | ||
* @param {mixed} obj The object to inspect. | ||
* @return {boolean} True if this object could be converted to an array without losing data. | ||
*/ | ||
arraylike = corridor.arraylike = function(obj) { | ||
var | ||
type = toString.call(obj), | ||
posInt = /0|[1-9]\d*/, | ||
length, | ||
key, | ||
i; | ||
if (type === '[object Array]') { | ||
return true; | ||
} else if (type !== '[object Object]') { | ||
return false; | ||
} | ||
if (('length' in obj) && !posInt.test(obj.length)) { | ||
return false; | ||
} | ||
length = 0; | ||
for (key in obj) { | ||
if (key !== 'length') { | ||
if (!posInt.test(key)) { | ||
return false; | ||
} | ||
length += 1; | ||
} | ||
} | ||
if (('length' in obj) && obj.length !== length) { | ||
return false; | ||
} | ||
if (length === 0) { | ||
return true; | ||
} | ||
for (i = 0; i < length; i++) { | ||
if (!(i in obj)) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
}; | ||
@@ -769,0 +901,0 @@ |
/** | ||
* test-merge.js - tests the merge() function. | ||
*/ | ||
exports['corridor.merge()'] = function(test) { | ||
exports['corridor.merge(objects)'] = function(test) { | ||
@@ -11,2 +11,43 @@ var | ||
suite = [{ | ||
obj: {}, | ||
other: { b: 'hi' }, | ||
expected: { b: 'hi' }, | ||
reason: 'all keys should be added to empty objects' | ||
},{ | ||
obj: { a: 'whut' }, | ||
other: { b: 'hi' }, | ||
expected: { a: 'whut', b: 'hi' }, | ||
reason: 'missing keys should be added to non-empty objects' | ||
},{ | ||
obj: { "person": { "name": "Bob" } }, | ||
other: { "person": { "email": "bob@company.com" } }, | ||
expected: { "person": { "name": "Bob", "email": "bob@company.com" } }, | ||
reason: 'nested objects should safely merge' | ||
},{ | ||
obj: { "person": { "name": "Bob" } }, | ||
other: { "person": { "name": "Alice" } }, | ||
expected: { "person": { "name": "Alice" } }, | ||
reason: 'nested conflicting objects should have their keys take precidence' | ||
}]; | ||
test.expect(suite.length); | ||
for (var i = 0, ii = suite.length; i < ii; i++) { | ||
(function(data){ | ||
var actual = corridor.merge(data.obj, data.other); | ||
test.equals(JSON.stringify(actual), JSON.stringify(data.expected), data.reason); | ||
})(suite[i]); | ||
} | ||
test.done(); | ||
}; | ||
exports['corridor.merge(arrays)'] = function(test) { | ||
var | ||
corridor = require('../src/corridor.js'), | ||
suite = [{ | ||
obj: ['a'], | ||
@@ -18,5 +59,10 @@ other: ['b'], | ||
obj: [{a: 'hi'}], | ||
other: [{a: 'there'}], | ||
expected: [{a: 'hi'}, {a: 'there'}], | ||
reason: 'arrays of conflicting objects should concatenate' | ||
},{ | ||
obj: [{a: 'hi'}], | ||
other: [{b: 'there'}], | ||
expected: [{a: 'hi'}, {b: 'there'}], | ||
reason: 'arrays of objects should concatenate' | ||
expected: [{a: 'hi', b: 'there'}], | ||
reason: 'arrays of non-conflicting objects should merge' | ||
},{ | ||
@@ -32,12 +78,43 @@ obj: {list: ['hi']}, | ||
reason: 'primitves should overwrite each other while arrays concatenate' | ||
}]; | ||
test.expect(suite.length); | ||
for (var i = 0, ii = suite.length; i < ii; i++) { | ||
(function(data){ | ||
var actual = corridor.merge(data.obj, data.other); | ||
test.equals(JSON.stringify(actual), JSON.stringify(data.expected), data.reason); | ||
})(suite[i]); | ||
} | ||
test.done(); | ||
}; | ||
exports['corridor.merge(mismatch)'] = function(test) { | ||
var | ||
corridor = require('../src/corridor.js'), | ||
suite = [{ | ||
obj: ['a'], | ||
other: {0:'b'}, | ||
expected: ['a', 'b'], | ||
reason: 'arraylike objects should behave like arrays for merging' | ||
},{ | ||
obj: {}, | ||
other: { b: 'hi' }, | ||
expected: { b: 'hi' }, | ||
reason: 'all keys should be added to empty objects' | ||
obj: {"foo":"bar"}, | ||
other: ["baz"], | ||
expected: {"foo":"bar",0:"baz"}, | ||
reason: 'an array should contribute numeric keys to an object' | ||
},{ | ||
obj: { a: 'whut' }, | ||
other: { b: 'hi' }, | ||
expected: { a: 'whut', b: 'hi' }, | ||
reason: 'missing keys should be added to non-empty objects' | ||
obj: ['baz'], | ||
other: {"foo":"bar"}, | ||
expected: {0:"baz","foo":"bar"}, | ||
reason: 'merging a non-array-like object into an array should objectify the array' | ||
},{ | ||
obj: {"deep":['baz']}, | ||
other: {"deep":{"foo":"bar"}}, | ||
expected: {"deep":{0:"baz","foo":"bar"}}, | ||
reason: 'merging a non-array-like sub-key into an array should objectify the array' | ||
}]; | ||
@@ -50,3 +127,3 @@ | ||
var actual = corridor.merge(data.obj, data.other); | ||
test.deepEqual(actual, data.expected, data.reason); | ||
test.equals(JSON.stringify(actual), JSON.stringify(data.expected), data.reason); | ||
})(suite[i]); | ||
@@ -59,1 +136,58 @@ } | ||
exports['corridor.merge(concat)'] = function(test) { | ||
var | ||
corridor = require('../src/corridor.js'), | ||
suite = [{ | ||
obj: [{a: 'hi'}], | ||
other: [{b: 'there'}], | ||
expected: [{a: 'hi'}, {b: 'there'}], | ||
reason: 'arrays of non-conflicting objects should concatenate in concat mode' | ||
}]; | ||
test.expect(suite.length); | ||
for (var i = 0, ii = suite.length; i < ii; i++) { | ||
(function(data){ | ||
var actual = corridor.merge(data.obj, data.other, {merge:'concat'}); | ||
test.equals(JSON.stringify(actual), JSON.stringify(data.expected), data.reason); | ||
})(suite[i]); | ||
} | ||
test.done(); | ||
}; | ||
exports['corridor.merge(extend)'] = function(test) { | ||
var | ||
corridor = require('../src/corridor.js'), | ||
suite = [{ | ||
obj: [{a: 'hi'}], | ||
other: [{b: 'there'}], | ||
expected: [{a: 'hi', b: 'there'}], | ||
reason: 'arrays of non-conflicting objects should concatenate in extend mode' | ||
},{ | ||
obj: [{a: 'hi'}, {b: 'sneak attack!'}], | ||
other: [{b: 'there'}], | ||
expected: [{a: 'hi', b: 'there'}, {b: 'sneak attack!'}], | ||
reason: 'arrays of objects should merge in extend mode' | ||
}]; | ||
test.expect(suite.length); | ||
for (var i = 0, ii = suite.length; i < ii; i++) { | ||
(function(data){ | ||
var actual = corridor.merge(data.obj, data.other, {merge:'extend'}); | ||
test.equals(JSON.stringify(actual), JSON.stringify(data.expected), data.reason); | ||
})(suite[i]); | ||
} | ||
test.done(); | ||
}; | ||
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
108273
16
1719
815