Skip to content

Commit

Permalink
Add support for default value for radio button group in serialize data:
Browse files Browse the repository at this point in the history
 * Default is set to null but it's easy to override.
 * Add value assigners to control value assignment to data
  • Loading branch information
stephanebachelier committed May 27, 2015
1 parent 815c355 commit bd8f8f3
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 16 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
26 changes: 23 additions & 3 deletions apidoc.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,15 @@ You can register your own input readers, allowing you
to change how the data is read. To do this register
a callback function to an input type.

The default input reader for radio button is returning the value if button is checked or `null`.
```js
Backbone.Syphon.InputReaders.register('radio', function($el) {
return $el.prop('checked') ? $el.val() : null;
});
```

You can override the default behavior to return the value of the radio button independently of its status.

```js
Backbone.Syphon.InputReaders.register('radio', function(el){
return el.val();
Expand Down Expand Up @@ -264,8 +273,9 @@ extracted from the element. This is the last opportunity to prevent
bad data from getting serialized to your object.

The most common use of this is to ensure radio button groups are only
serialized by the one radio button that is selected, within the group. This
behavior is built in by default (see below).
serialized by the one radio button that is selected, within the group,
defaulting to `null` value if no button is selected. This behavior is
built in by default (see below).

### Assigning Your Own Validator

Expand Down Expand Up @@ -295,6 +305,16 @@ return a boolean (or truthy) value from the callback.
Return values of `true` or truthy will be valid and the assignment will occur. Return values
that are `false` or falsey will not be valid and the assignment will not occur.

### Overriding radio group output

Default will serialize a null value for a radio group if no button is selected. If you want a different behavior like no output if no radio button selected, you should override radio key assignment validator, like this:

```js
Backbone.Syphon.KeyAssignmentValidators.register("radio", function($el, key, value){
return $el.prop('checked');
});
```

### Assign A Key Assignment Validation Set

You can assign your own Key Validation Set by creating an instance of
Expand All @@ -315,7 +335,7 @@ just register and remove validations as needed.
There are two Key Assignment Validators built in to Syphon:

* default: everything is valid
* radio: only radio buttons that are selected are valid
* radio: only radio buttons that are selected are valid, or if a value different than `undefined` is returned by input reader. This should work for most cases, with radio button groups set to a `null` in case no buttons are selected.

## Handling Non-"input" Elements

Expand Down
7 changes: 7 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ Backbone.Syphon.serialize(view);
}
```

Serializing a form with no radio button checked will be serialized to :
```js
{
a: null
}
```

This behavior can be changed by registering a different set of Key Extractors, Input Readers, and Key Assignment
Validators. See the full
[API Documentation](https://github.com/marionettejs/backbone.syphon/blob/master/apidoc.md).
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');
});
});
});
5 changes: 5 additions & 0 deletions src/backbone.syphon.inputreaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ InputReaders.registerDefault(function($el) {
InputReaders.register('checkbox', function($el) {
return ($el.prop('indeterminate')) ? null : $el.prop('checked');
});

// Radio reader, returning the value if radio button is checked or null
InputReaders.register('radio', function($el) {
return $el.prop('checked') ? $el.val() : null;
});
Loading

0 comments on commit bd8f8f3

Please sign in to comment.