Skip to content

Commit

Permalink
Add value assigners to control value assignment to data
Browse files Browse the repository at this point in the history
  • Loading branch information
stephanebachelier committed Apr 15, 2015
1 parent 7674f9c commit c6160b9
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 12 deletions.
1 change: 1 addition & 0 deletions SpecRunner.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
<script src="spec/javascripts/keyExtractors.spec.js"></script>
<script src="spec/javascripts/serialize.nested.spec.js"></script>
<script src="spec/javascripts/serialize.spec.js"></script>
<script src="spec/javascripts/valueAssigners.spec.js"></script>
</head>
<body>
<div id="mocha"></div>
Expand Down
12 changes: 9 additions & 3 deletions spec/javascripts/serialize.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ describe('serializing a form', function() {
this.$el.html(
'<form>' +
'<input type="radio" name="foo" value="foo">' +
'<input type="radio" name="foo" value="bar" checked>' +
'<input type="radio" name="foo" value="bar">' +
'<input type="radio" name="foo" value="baz">' +
'</form>'
);
Expand All @@ -260,11 +260,17 @@ describe('serializing a form', function() {
this.view = new this.View();
this.view.render();

this.result = Backbone.Syphon.serialize(this.view);
});

it('should return the value null when no selected radio button', function() {
var result = Backbone.Syphon.serialize(this.view);
expect(result.foo).to.equal(null);
});

it('should only return the value of the selected radio button', function() {
expect(this.result.foo).to.equal('bar');
this.view.$('[value=bar]').click();
var result = Backbone.Syphon.serialize(this.view);
expect(result.foo).to.equal('bar');
});
});

Expand Down
236 changes: 236 additions & 0 deletions spec/javascripts/valueAssigners.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
describe('value assigners', function() {
describe('by default', function() {
beforeEach(function() {
this.valueAssigners = Backbone.Syphon.ValueAssigners;
this.obj = {
foo: undefined,
bar: []
};
});

describe('for default type', function() {
beforeEach(function() {
var defaultValueAssigner = this.valueAssigners.get();
this.valueAssignerFn = defaultValueAssigner(1);
});

describe('for a scalar', function() {
it('should set value', function() {
this.valueAssignerFn(this.obj, 'foo');
expect(this.obj.foo).to.equal(1);
});

it('should override the value', function() {
this.obj.foo = 0;
this.valueAssignerFn(this.obj, 'foo');
expect(this.obj.foo).to.equal(1);
});
});

describe('for an array', function() {
it('should set value', function() {
this.valueAssignerFn(this.obj, 'bar');
expect(this.obj.bar).to.have.length(1);
expect(this.obj.bar[0]).to.equal(1);
});

it('should override the value', function() {
this.obj.bar.push(0);
this.valueAssignerFn(this.obj, 'bar');
expect(this.obj.bar[1]).to.equal(1);
});
});
});

describe('for radio type', function() {
beforeEach(function() {
this.radioValueAssigner = this.valueAssigners.get('radio');
});

describe('for a scalar', function() {
it('should set value', function() {
var valueAssignerFn = this.radioValueAssigner(1);
valueAssignerFn(this.obj, 'foo');
expect(this.obj.foo).to.equal(1);
});

it('should be possible to set a null value', function() {
var valueAssignerFn = this.radioValueAssigner(null);
valueAssignerFn(this.obj, 'foo');
expect(this.obj.foo).to.equal(null);
});

it('should override existing null value', function() {
this.obj.foo = null;

var valueAssignerFn = this.radioValueAssigner(1);
valueAssignerFn(this.obj, 'foo');
expect(this.obj.foo).to.equal(1);
});

it('should override existing undefined value', function() {
this.obj.foo = undefined;

var valueAssignerFn = this.radioValueAssigner(1);
valueAssignerFn(this.obj, 'foo');
expect(this.obj.foo).to.equal(1);
});

it('should not override existing value different from undefined or null', function() {
this.obj.foo = 1;

var valueAssignerFn = this.radioValueAssigner(2);
valueAssignerFn(this.obj, 'foo');
expect(this.obj.foo).to.equal(1);
});
});

describe('for an array', function() {
it('should set value', function() {
var valueAssignerFn = this.radioValueAssigner(1);
valueAssignerFn(this.obj, 'bar');
expect(this.obj.bar).to.have.length(1);
expect(this.obj.bar[0]).to.equal(1);
});

it('should be possible to set a null value', function() {
var valueAssignerFn = this.radioValueAssigner(null);
valueAssignerFn(this.obj, 'bar');
expect(this.obj.bar).to.have.length(1);
expect(this.obj.bar[0]).to.equal(null);
});

it('should override a null value', function() {
var valueAssignerFn = this.radioValueAssigner(1);
this.obj.bar.push(null);
valueAssignerFn(this.obj, 'bar');
expect(this.obj.bar).to.have.length(1);
expect(this.obj.bar[0]).to.equal(1);
});

it('should override an undefined value', function() {
var valueAssignerFn = this.radioValueAssigner(1);
this.obj.bar.push(undefined);
valueAssignerFn(this.obj, 'bar');
expect(this.obj.bar).to.have.length(1);
expect(this.obj.bar[0]).to.equal(1);
});

it('should not override a non null or undefined value', function() {
var valueAssignerFn = this.radioValueAssigner(1);
this.obj.bar.push(0);
valueAssignerFn(this.obj, 'bar');
expect(this.obj.bar).to.have.length(1);
expect(this.obj.bar[0]).to.equal(0);
});
});
});
});

describe('when serializing a form', function() {
beforeEach(function() {
this.View = Backbone.View.extend({
render: function() {
this.$el.html(
'<form>' +
'<input type="text" name="text" value="foo">' +
'<input type="checkbox" name="bar">' +
'<input type="checkbox" name="bar2" checked>' +
'<input type="radio" name="foo" value="a">' +
'<input type="radio" name="foo" value="b">' +
'<input type="radio" name="foo2" value="a" checked>' +
'<input type="radio" name="foo2" value="b">' +
'<input type="radio" name="radio[]" value="a" checked>' +
'<input type="radio" name="radio[]" value="b">' +
'</form>'
);
}
});

this.view = new this.View();
this.view.render();
this.result = Backbone.Syphon.serialize(this.view);
});

it('should have a correct value for text input', function() {
expect(this.result).to.have.ownProperty('text');
expect(this.result.text).to.equal('foo');
});

it('should have a correct value for a non checked checkbox', function() {
expect(this.result).to.have.ownProperty('bar');
expect(this.result.bar).to.equal(false);
});

it('should have a correct value for a checked checkbox', function() {
expect(this.result).to.have.ownProperty('bar2');
expect(this.result.bar2).to.equal(true);
});

it('should set a null value for non checked radio', function() {
expect(this.result).to.have.ownProperty('foo');
expect(this.result.foo).to.equal(null);
});

it('should set a null value for checked radio', function() {
expect(this.result).to.have.ownProperty('foo2');
expect(this.result.foo2).to.equal('a');
});

it('should have a correct value for radio array', function() {
expect(this.result).to.have.ownProperty('radio');
expect(this.result.radio).to.be.an('array');
expect(this.result.radio).to.have.length(1);
expect(this.result.radio[0]).to.equal('a');
});
});

describe('when specifying value assigners in the options for serialize', function() {
beforeEach(function() {
this.View = Backbone.View.extend({
render: function() {
this.$el.html(
'<form>' +
'<input name="bar" value="a">' +
'<input name="foo" value="b">' +
'</form>'
);
}
});

this.valueAssigners = new Backbone.Syphon.ValueAssignerSet();

// this value assigners add a prefix to all values.
this.valueAssigners.registerDefault(function(value) {
var prefixValue = function(value) {
return 'foo-' + value;
};

return function(obj, key) {
var v = prefixValue(value);

if (_.isArray(obj[key])) {
obj[key].push(v);
} else {
obj[key] = v;
}
};
});

this.view = new this.View();
this.view.render();

this.result = Backbone.Syphon.serialize(this.view, {
valueAssigners: this.valueAssigners
});
});

it('should use the specified value assigners', function() {
expect(this.result).to.have.ownProperty('bar');
expect(this.result.bar).to.equal('foo-a');

expect(this.result).to.have.ownProperty('foo');
expect(this.result.foo).to.equal('foo-b');
});
});
});
16 changes: 7 additions & 9 deletions src/backbone.syphon.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* jshint maxstatements: 13, maxlen: 102, maxcomplexity: 8, latedef: false */
/* jshint maxstatements: 13, maxlen: 102, maxcomplexity: 9, latedef: false */

// Ignore Element Types
// --------------------
Expand Down Expand Up @@ -41,7 +41,8 @@ Syphon.serialize = function(view, options) {
var validKeyAssignment = config.keyAssignmentValidators.get(type);
if (validKeyAssignment($el, key, value)) {
var keychain = config.keySplitter(key);
data = assignKeyValue(data, keychain, value);
var valueAssigner = config.valueAssigners.get(type);
data = assignKeyValue(data, keychain, valueAssigner(value));
}
});

Expand Down Expand Up @@ -165,6 +166,7 @@ var buildConfig = function(options) {
config.keySplitter = config.keySplitter || Syphon.KeySplitter;
config.keyJoiner = config.keyJoiner || Syphon.KeyJoiner;
config.keyAssignmentValidators = config.keyAssignmentValidators || Syphon.KeyAssignmentValidators;
config.valueAssigners = config.valueAssigners || Syphon.ValueAssigners;

return config;
};
Expand All @@ -190,7 +192,7 @@ var buildConfig = function(options) {
// becomes an array, and values are pushed in to the array,
// allowing multiple fields with the same name to be
// assigned to the array.
var assignKeyValue = function(obj, keychain, value) {
var assignKeyValue = function(obj, keychain, valueAssignerFn) {
if (!keychain) { return obj; }

var key = keychain.shift();
Expand All @@ -202,16 +204,12 @@ var assignKeyValue = function(obj, keychain, value) {

// if it's the last key in the chain, assign the value directly
if (keychain.length === 0) {
if (_.isArray(obj[key])) {
obj[key].push(value);
} else {
obj[key] = value;
}
valueAssignerFn(obj, key);
}

// recursive parsing of the array, depth-first
if (keychain.length > 0) {
assignKeyValue(obj[key], keychain, value);
assignKeyValue(obj[key], keychain, valueAssignerFn);
}

return obj;
Expand Down
43 changes: 43 additions & 0 deletions src/backbone.syphon.valueassigners.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Value Assigners
// -------------------------

// Value Assigners are used to whether or not a
// key should be assigned to a value, after the key and value have been
// extracted from the element. This is the last opportunity to prevent
// bad obj[key] from getting serialized to your object.

var ValueAssignerSet = Syphon.ValueAssignerSet = TypeRegistry.extend();

// Build-in Key Assignment Values
var ValueAssigners = Syphon.ValueAssigners = new ValueAssignerSet();

// return value by default
ValueAssigners.registerDefault(function(value) {
return function(obj, key) {
if (_.isArray(obj[key])) {
obj[key].push(value);
} else {
obj[key] = value;
}
};
});

// radio group button can have only one value assigned.
ValueAssigners.register('radio', function(value) {
var emptyValueFn = function(value) {
return _.isNull(value) || _.isUndefined(value) || _.isObject(value) && _.isEmpty(value);
};

return function(obj, key) {
if (_.isArray(obj[key])) {
if (!obj[key].length || (obj[key].length && emptyValueFn(obj[key][0]))) {
obj[key] = [value];
}
} else {
// default is initialized to {}
if (emptyValueFn(obj[key])) {
obj[key] = value;
}
}
};
});
1 change: 1 addition & 0 deletions src/build/backbone.syphon.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
// @include ../backbone.syphon.keyassignmentvalidators.js
// @include ../backbone.syphon.keysplitter.js
// @include ../backbone.syphon.keyjoiner.js
// @include ../backbone.syphon.valueassigners.js

return Backbone.Syphon;
}));

0 comments on commit c6160b9

Please sign in to comment.