From c6160b9a6a3b1975d9e67cf010a81a193a3a5153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bachelier?= Date: Wed, 15 Apr 2015 11:01:07 +0200 Subject: [PATCH] Add value assigners to control value assignment to data --- SpecRunner.html | 1 + spec/javascripts/serialize.spec.js | 12 +- spec/javascripts/valueAssigners.spec.js | 236 ++++++++++++++++++++++++ src/backbone.syphon.js | 16 +- src/backbone.syphon.valueassigners.js | 43 +++++ src/build/backbone.syphon.js | 1 + 6 files changed, 297 insertions(+), 12 deletions(-) create mode 100644 spec/javascripts/valueAssigners.spec.js create mode 100644 src/backbone.syphon.valueassigners.js diff --git a/SpecRunner.html b/SpecRunner.html index 0fc8494..cb75e2c 100644 --- a/SpecRunner.html +++ b/SpecRunner.html @@ -47,6 +47,7 @@ +
diff --git a/spec/javascripts/serialize.spec.js b/spec/javascripts/serialize.spec.js index 3647385..bfd099d 100644 --- a/spec/javascripts/serialize.spec.js +++ b/spec/javascripts/serialize.spec.js @@ -250,7 +250,7 @@ describe('serializing a form', function() { this.$el.html( '
' + '' + - '' + + '' + '' + '
' ); @@ -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'); }); }); diff --git a/spec/javascripts/valueAssigners.spec.js b/spec/javascripts/valueAssigners.spec.js new file mode 100644 index 0000000..a2ef36b --- /dev/null +++ b/spec/javascripts/valueAssigners.spec.js @@ -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( + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + ); + } + }); + + 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( + '
' + + '' + + '' + + '
' + ); + } + }); + + 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'); + }); + }); +}); diff --git a/src/backbone.syphon.js b/src/backbone.syphon.js index f15d9e3..deab3e6 100644 --- a/src/backbone.syphon.js +++ b/src/backbone.syphon.js @@ -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 // -------------------- @@ -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)); } }); @@ -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; }; @@ -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(); @@ -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; diff --git a/src/backbone.syphon.valueassigners.js b/src/backbone.syphon.valueassigners.js new file mode 100644 index 0000000..2a9e944 --- /dev/null +++ b/src/backbone.syphon.valueassigners.js @@ -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; + } + } + }; +}); diff --git a/src/build/backbone.syphon.js b/src/build/backbone.syphon.js index ca04c05..9260fa7 100644 --- a/src/build/backbone.syphon.js +++ b/src/build/backbone.syphon.js @@ -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; }));