Skip to content

Commit

Permalink
(de)serialize contenteditable elements
Browse files Browse the repository at this point in the history
Form elements are now fetched with `Syphon.InputFetcher`, instead of
the private `getForm` function. By default, this will fetch `contenteditable`
elements in addition to inputs and checkboxes.

"Content editable" fields are handled as follows:

* must have `contenteditable="true"` (having only `contenteditable` present
    is *NOT* sufficient, even though it conforms to the HTML spec)
* must define a `data-name` property, which is used like a standard `input`
    field's `name` property
* data in read/written using jQuery's `html()` method
  • Loading branch information
davidsulc committed Jun 9, 2015
1 parent 4c42016 commit 8c9a08e
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 22 deletions.
21 changes: 19 additions & 2 deletions apidoc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -37,14 +43,18 @@ 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:

```html
<form>
<input name="foo" value="bar">
<input type="checkbox" name="chk" checked>
<div data-name="editor" contenteditable="true">my text</div>
</form>
```

Expand All @@ -53,7 +63,8 @@ will produce this result, when serialized:
```js
{
foo: "bar",
chk: true
chk: true,
editor: "my text"
}
```

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 24 additions & 0 deletions spec/javascripts/deserialize.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'<form>' +
'<div data-name="foo" contenteditable="true"></div>' +
'</form>'
);
}
});

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

Backbone.Syphon.deserialize(this.view, {foo: '<em>bar</em>'});
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('<em>bar</em>');
});
});

describe('when deserializing into a select box', function() {
beforeEach(function() {
this.View = Backbone.View.extend({
Expand Down
35 changes: 35 additions & 0 deletions spec/javascripts/serialize.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'<form>' +
'<div data-name="foo" contenteditable="true">bar</div>' +
'<span data-name="fu" contenteditable="true">baz</span>' +
'<div data-name="trap" >bar</div>' +
'<span data-name="trapAgain" >baz</span>' +
'</form>'
);
}
});

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({
Expand Down
14 changes: 14 additions & 0 deletions src/backbone.syphon.inputfetcher.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
7 changes: 6 additions & 1 deletion src/backbone.syphon.inputreaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/backbone.syphon.inputwriters.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 9 additions & 17 deletions src/backbone.syphon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
};
Expand Down
2 changes: 1 addition & 1 deletion src/backbone.syphon.keyextractors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') || '';
});
1 change: 1 addition & 0 deletions src/build/backbone.syphon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 8c9a08e

Please sign in to comment.