diff --git a/apidoc.md b/apidoc.md index b2616bd..447cbdf 100644 --- a/apidoc.md +++ b/apidoc.md @@ -4,6 +4,12 @@ The document is the primary source for the API that Syphon exposes, and provides information on how to correctly override and configure the behaviors of Syphon. +## Syphon.InputFetcher + +Determines the input elements present in the form (or view). +The default implementation will return elements of type `input` +as well as elements with `contenteditable="true"`. + ## Syphon.KeyExtractorSet (Key Extractors) When a form is serialized, all of the input elements are @@ -37,7 +43,10 @@ register new extractors as needed (see below). ### Default Key Extractor: element "name" The default key extractor uses the `name` attribute of the form's -input element as the key. +input element as the key. If the `name` property is undefined, it +will fall back to using the `data-name` property (which should +therefore be defined in order to use Syphon with non-input elements, +e.g. a `div` with `contenteditable="true"`). For example, an HTML form that looks like this: @@ -45,6 +54,7 @@ For example, an HTML form that looks like this:
+
my text
``` @@ -53,7 +63,8 @@ will produce this result, when serialized: ```js { foo: "bar", - chk: true + chk: true, + editor: "my text" } ``` @@ -138,6 +149,9 @@ jQuery's `val()` method. The checkbox reader, however, looks for whether or not the checkbox is checked and returns a boolean value. +In addition, the input reader will handle elements that are +`contenteditable="true"` using jQuery's `html()` method. + ### Default Input Reader Set Syphon comes with a default input reader set in the @@ -201,6 +215,9 @@ every form of input using jQuery's `val()` method. The checkbox reader, sets whether or not the checkbox is checked, and the radio writer will select the correct radio button in a radio button group. +In addition, the input writer will handle elements that are +`contenteditable="true"` using jQuery's `html()` method. + ### Default Input Writer Set Syphon comes with a default input writer set in the diff --git a/readme.md b/readme.md index b506351..306a3fb 100644 --- a/readme.md +++ b/readme.md @@ -440,6 +440,7 @@ There some known limitations in Backbone.Syphon, partially by design and partial * An input of type `checkbox` will return a boolean value. This can be overriden by replacing the Input Reader for checkboxes. * Yo avoid circular references, care should be taken when using Backbone.Relational. See (#33)[https://github.com/marionettejs/backbone.syphon/issues/33]. +* `contenteditable` fields will only be processed if they are explicitly assigned the `true` value: `contenteditable="true"`. ## Building Backbone.Syphon diff --git a/spec/javascripts/deserialize.spec.js b/spec/javascripts/deserialize.spec.js index 56992f6..aae28bf 100644 --- a/spec/javascripts/deserialize.spec.js +++ b/spec/javascripts/deserialize.spec.js @@ -69,6 +69,30 @@ describe('deserializing an object into a form', function() { }); }); + describe('when deserializing into a "contenteditable" element', function() { + beforeEach(function() { + this.View = Backbone.View.extend({ + render: function() { + this.$el.html( + '
' + + '
' + + '
' + ); + } + }); + + this.view = new this.View(); + this.view.render(); + + Backbone.Syphon.deserialize(this.view, {foo: 'bar'}); + this.result = this.view.$('div[data-name=foo]').html(); + }); + + it('should set the element\'s value to the corresponding value in the given object', function() { + expect(this.result).to.equal('bar'); + }); + }); + describe('when deserializing into a select box', function() { beforeEach(function() { this.View = Backbone.View.extend({ diff --git a/spec/javascripts/serialize.spec.js b/spec/javascripts/serialize.spec.js index 48c47c9..5861335 100644 --- a/spec/javascripts/serialize.spec.js +++ b/spec/javascripts/serialize.spec.js @@ -268,6 +268,41 @@ describe('serializing a form', function() { }); }); + describe('when serializing contenteditable elements', function() { + beforeEach(function() { + this.View = Backbone.View.extend({ + render: function() { + this.$el.html( + '
' + + '
bar
' + + 'baz' + + '
bar
' + + 'baz' + + '
' + ); + } + }); + + this.view = new this.View(); + this.view.render(); + + this.result = Backbone.Syphon.serialize(this.view); + }); + + it('should return the value of a contenteditable "div" element', function() { + expect(this.result.foo).to.equal('bar'); + }); + + it('should return the value of a contenteditable "span" element', function() { + expect(this.result.fu).to.equal('baz'); + }); + + it('should ignore elements that are not "contenteditable"', function() { + expect(this.result.trap).to.be.undefined; + expect(this.result.trapAgain).to.be.undefined; + }); + }); + describe('when the view is actually a form', function() { beforeEach(function() { this.View = Backbone.View.extend({ diff --git a/src/backbone.syphon.inputfetcher.js b/src/backbone.syphon.inputfetcher.js new file mode 100644 index 0000000..c4eeb01 --- /dev/null +++ b/src/backbone.syphon.inputfetcher.js @@ -0,0 +1,14 @@ +// Backbone.Syphon.InputFetcher +// ---------------------------- + +// If a dom element is given, just return the form fields and +// elements with "contenteditable". +// Otherwise, get the form fields (and contenteditable elements) from the view. +Syphon.InputFetcher = function(viewOrForm) { + var inputs = ':input, [contenteditable=true]'; + if (_.isUndefined(viewOrForm.$el)) { + return $(viewOrForm).find(inputs); + } else { + return viewOrForm.$(inputs); + } +}; diff --git a/src/backbone.syphon.inputreaders.js b/src/backbone.syphon.inputreaders.js index 2636d28..711eee7 100644 --- a/src/backbone.syphon.inputreaders.js +++ b/src/backbone.syphon.inputreaders.js @@ -11,7 +11,12 @@ var InputReaders = Syphon.InputReaders = new InputReaderSet(); // The default input reader, which uses an input // element's "value" InputReaders.registerDefault(function($el) { - return $el.val(); + var editableProp = $el.attr('contenteditable'); + if (('' + editableProp).toLowerCase() !== 'true') { + return $el.val(); + } else { + return $el.html(); + } }); // Checkbox reader, returning a boolean value for diff --git a/src/backbone.syphon.inputwriters.js b/src/backbone.syphon.inputwriters.js index a84b828..2db1f68 100644 --- a/src/backbone.syphon.inputwriters.js +++ b/src/backbone.syphon.inputwriters.js @@ -11,7 +11,11 @@ var InputWriters = Syphon.InputWriters = new InputWriterSet(); // The default input writer, which sets an input // element's "value" InputWriters.registerDefault(function($el, value) { - $el.val(value); + if (('' + $el.attr('contenteditable')).toLowerCase() !== 'true') { + $el.val(value); + } else { + $el.html(value); + } }); // Checkbox writer, set whether or not the checkbox is checked diff --git a/src/backbone.syphon.js b/src/backbone.syphon.js index d5b6b95..13f9b90 100644 --- a/src/backbone.syphon.js +++ b/src/backbone.syphon.js @@ -87,7 +87,7 @@ Syphon.deserialize = function(view, data, options) { // Retrieve all of the form inputs // from the form var getInputElements = function(view, config) { - var formInputs = getForm(view); + var formInputs = config.inputFetcher(view); formInputs = _.reject(formInputs, function(el) { var reject; @@ -143,28 +143,20 @@ var getElementType = function(el) { return type.toLowerCase(); }; -// If a dom element is given, just return the form fields. -// Otherwise, get the form fields from the view. -var getForm = function(viewOrForm) { - if (_.isUndefined(viewOrForm.$el)) { - return $(viewOrForm).find(':input'); - } else { - return viewOrForm.$(':input'); - } -}; - // Build a configuration object and initialize // default values. var buildConfig = function(options) { var config = _.clone(options) || {}; config.ignoredTypes = _.clone(Syphon.ignoredTypes); - config.inputReaders = config.inputReaders || Syphon.InputReaders; - config.inputWriters = config.inputWriters || Syphon.InputWriters; - config.keyExtractors = config.keyExtractors || Syphon.KeyExtractors; - config.keySplitter = config.keySplitter || Syphon.KeySplitter; - config.keyJoiner = config.keyJoiner || Syphon.KeyJoiner; - config.keyAssignmentValidators = config.keyAssignmentValidators || Syphon.KeyAssignmentValidators; + // for each propery, attempt to use the value defined on 'config', + // fallback to default value defined on 'Syphon' + // (loop so jshint doesn't complain about cyclomatic complexity) + _.each(['inputFetcher', 'inputReaders', 'inputWriters', 'keyExtractors', + 'keySplitter', 'keyJoiner', 'keyAssignmentValidators'], function(prop) { + var capitalized = prop.charAt(0).toUpperCase() + prop.slice(1); + config[prop] = config[prop] || Syphon[capitalized]; + }); return config; }; diff --git a/src/backbone.syphon.keyextractors.js b/src/backbone.syphon.keyextractors.js index eea8d0d..075868c 100644 --- a/src/backbone.syphon.keyextractors.js +++ b/src/backbone.syphon.keyextractors.js @@ -11,5 +11,5 @@ var KeyExtractors = Syphon.KeyExtractors = new KeyExtractorSet(); // The default key extractor, which uses the // input element's "name" attribute KeyExtractors.registerDefault(function($el) { - return $el.prop('name') || ''; + return $el.prop('name') || $el.data('name') || ''; }); diff --git a/src/build/backbone.syphon.js b/src/build/backbone.syphon.js index ca04c05..c4c685b 100644 --- a/src/build/backbone.syphon.js +++ b/src/build/backbone.syphon.js @@ -30,6 +30,7 @@ // @include ../backbone.syphon.js // @include ../backbone.syphon.typeregistry.js // @include ../backbone.syphon.keyextractors.js + // @include ../backbone.syphon.inputfetcher.js // @include ../backbone.syphon.inputreaders.js // @include ../backbone.syphon.inputwriters.js // @include ../backbone.syphon.keyassignmentvalidators.js