Skip to content

Commit 8c9a08e

Browse files
committed
(de)serialize contenteditable elements
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
1 parent 4c42016 commit 8c9a08e

10 files changed

+115
-22
lines changed

apidoc.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ The document is the primary source for the API that Syphon
44
exposes, and provides information on how to correctly override
55
and configure the behaviors of Syphon.
66

7+
## Syphon.InputFetcher
8+
9+
Determines the input elements present in the form (or view).
10+
The default implementation will return elements of type `input`
11+
as well as elements with `contenteditable="true"`.
12+
713
## Syphon.KeyExtractorSet (Key Extractors)
814

915
When a form is serialized, all of the input elements are
@@ -37,14 +43,18 @@ register new extractors as needed (see below).
3743
### Default Key Extractor: element "name"
3844

3945
The default key extractor uses the `name` attribute of the form's
40-
input element as the key.
46+
input element as the key. If the `name` property is undefined, it
47+
will fall back to using the `data-name` property (which should
48+
therefore be defined in order to use Syphon with non-input elements,
49+
e.g. a `div` with `contenteditable="true"`).
4150

4251
For example, an HTML form that looks like this:
4352

4453
```html
4554
<form>
4655
<input name="foo" value="bar">
4756
<input type="checkbox" name="chk" checked>
57+
<div data-name="editor" contenteditable="true">my text</div>
4858
</form>
4959
```
5060

@@ -53,7 +63,8 @@ will produce this result, when serialized:
5363
```js
5464
{
5565
foo: "bar",
56-
chk: true
66+
chk: true,
67+
editor: "my text"
5768
}
5869
```
5970

@@ -138,6 +149,9 @@ jQuery's `val()` method. The checkbox reader, however, looks
138149
for whether or not the checkbox is checked and returns a
139150
boolean value.
140151

152+
In addition, the input reader will handle elements that are
153+
`contenteditable="true"` using jQuery's `html()` method.
154+
141155
### Default Input Reader Set
142156

143157
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,
201215
sets whether or not the checkbox is checked, and the radio writer will
202216
select the correct radio button in a radio button group.
203217

218+
In addition, the input writer will handle elements that are
219+
`contenteditable="true"` using jQuery's `html()` method.
220+
204221
### Default Input Writer Set
205222

206223
Syphon comes with a default input writer set in the

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ There some known limitations in Backbone.Syphon, partially by design and partial
440440

441441
* An input of type `checkbox` will return a boolean value. This can be overriden by replacing the Input Reader for checkboxes.
442442
* Yo avoid circular references, care should be taken when using Backbone.Relational. See (#33)[https://github.com/marionettejs/backbone.syphon/issues/33].
443+
* `contenteditable` fields will only be processed if they are explicitly assigned the `true` value: `contenteditable="true"`.
443444

444445
## Building Backbone.Syphon
445446

spec/javascripts/deserialize.spec.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,30 @@ describe('deserializing an object into a form', function() {
6969
});
7070
});
7171

72+
describe('when deserializing into a "contenteditable" element', function() {
73+
beforeEach(function() {
74+
this.View = Backbone.View.extend({
75+
render: function() {
76+
this.$el.html(
77+
'<form>' +
78+
'<div data-name="foo" contenteditable="true"></div>' +
79+
'</form>'
80+
);
81+
}
82+
});
83+
84+
this.view = new this.View();
85+
this.view.render();
86+
87+
Backbone.Syphon.deserialize(this.view, {foo: '<em>bar</em>'});
88+
this.result = this.view.$('div[data-name=foo]').html();
89+
});
90+
91+
it('should set the element\'s value to the corresponding value in the given object', function() {
92+
expect(this.result).to.equal('<em>bar</em>');
93+
});
94+
});
95+
7296
describe('when deserializing into a select box', function() {
7397
beforeEach(function() {
7498
this.View = Backbone.View.extend({

spec/javascripts/serialize.spec.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,41 @@ describe('serializing a form', function() {
268268
});
269269
});
270270

271+
describe('when serializing contenteditable elements', function() {
272+
beforeEach(function() {
273+
this.View = Backbone.View.extend({
274+
render: function() {
275+
this.$el.html(
276+
'<form>' +
277+
'<div data-name="foo" contenteditable="true">bar</div>' +
278+
'<span data-name="fu" contenteditable="true">baz</span>' +
279+
'<div data-name="trap" >bar</div>' +
280+
'<span data-name="trapAgain" >baz</span>' +
281+
'</form>'
282+
);
283+
}
284+
});
285+
286+
this.view = new this.View();
287+
this.view.render();
288+
289+
this.result = Backbone.Syphon.serialize(this.view);
290+
});
291+
292+
it('should return the value of a contenteditable "div" element', function() {
293+
expect(this.result.foo).to.equal('bar');
294+
});
295+
296+
it('should return the value of a contenteditable "span" element', function() {
297+
expect(this.result.fu).to.equal('baz');
298+
});
299+
300+
it('should ignore elements that are not "contenteditable"', function() {
301+
expect(this.result.trap).to.be.undefined;
302+
expect(this.result.trapAgain).to.be.undefined;
303+
});
304+
});
305+
271306
describe('when the view is actually a form', function() {
272307
beforeEach(function() {
273308
this.View = Backbone.View.extend({
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Backbone.Syphon.InputFetcher
2+
// ----------------------------
3+
4+
// If a dom element is given, just return the form fields and
5+
// elements with "contenteditable".
6+
// Otherwise, get the form fields (and contenteditable elements) from the view.
7+
Syphon.InputFetcher = function(viewOrForm) {
8+
var inputs = ':input, [contenteditable=true]';
9+
if (_.isUndefined(viewOrForm.$el)) {
10+
return $(viewOrForm).find(inputs);
11+
} else {
12+
return viewOrForm.$(inputs);
13+
}
14+
};

src/backbone.syphon.inputreaders.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ var InputReaders = Syphon.InputReaders = new InputReaderSet();
1111
// The default input reader, which uses an input
1212
// element's "value"
1313
InputReaders.registerDefault(function($el) {
14-
return $el.val();
14+
var editableProp = $el.attr('contenteditable');
15+
if (('' + editableProp).toLowerCase() !== 'true') {
16+
return $el.val();
17+
} else {
18+
return $el.html();
19+
}
1520
});
1621

1722
// Checkbox reader, returning a boolean value for

src/backbone.syphon.inputwriters.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ var InputWriters = Syphon.InputWriters = new InputWriterSet();
1111
// The default input writer, which sets an input
1212
// element's "value"
1313
InputWriters.registerDefault(function($el, value) {
14-
$el.val(value);
14+
if (('' + $el.attr('contenteditable')).toLowerCase() !== 'true') {
15+
$el.val(value);
16+
} else {
17+
$el.html(value);
18+
}
1519
});
1620

1721
// Checkbox writer, set whether or not the checkbox is checked

src/backbone.syphon.js

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Syphon.deserialize = function(view, data, options) {
8787
// Retrieve all of the form inputs
8888
// from the form
8989
var getInputElements = function(view, config) {
90-
var formInputs = getForm(view);
90+
var formInputs = config.inputFetcher(view);
9191

9292
formInputs = _.reject(formInputs, function(el) {
9393
var reject;
@@ -143,28 +143,20 @@ var getElementType = function(el) {
143143
return type.toLowerCase();
144144
};
145145

146-
// If a dom element is given, just return the form fields.
147-
// Otherwise, get the form fields from the view.
148-
var getForm = function(viewOrForm) {
149-
if (_.isUndefined(viewOrForm.$el)) {
150-
return $(viewOrForm).find(':input');
151-
} else {
152-
return viewOrForm.$(':input');
153-
}
154-
};
155-
156146
// Build a configuration object and initialize
157147
// default values.
158148
var buildConfig = function(options) {
159149
var config = _.clone(options) || {};
160150

161151
config.ignoredTypes = _.clone(Syphon.ignoredTypes);
162-
config.inputReaders = config.inputReaders || Syphon.InputReaders;
163-
config.inputWriters = config.inputWriters || Syphon.InputWriters;
164-
config.keyExtractors = config.keyExtractors || Syphon.KeyExtractors;
165-
config.keySplitter = config.keySplitter || Syphon.KeySplitter;
166-
config.keyJoiner = config.keyJoiner || Syphon.KeyJoiner;
167-
config.keyAssignmentValidators = config.keyAssignmentValidators || Syphon.KeyAssignmentValidators;
152+
// for each propery, attempt to use the value defined on 'config',
153+
// fallback to default value defined on 'Syphon'
154+
// (loop so jshint doesn't complain about cyclomatic complexity)
155+
_.each(['inputFetcher', 'inputReaders', 'inputWriters', 'keyExtractors',
156+
'keySplitter', 'keyJoiner', 'keyAssignmentValidators'], function(prop) {
157+
var capitalized = prop.charAt(0).toUpperCase() + prop.slice(1);
158+
config[prop] = config[prop] || Syphon[capitalized];
159+
});
168160

169161
return config;
170162
};

src/backbone.syphon.keyextractors.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ var KeyExtractors = Syphon.KeyExtractors = new KeyExtractorSet();
1111
// The default key extractor, which uses the
1212
// input element's "name" attribute
1313
KeyExtractors.registerDefault(function($el) {
14-
return $el.prop('name') || '';
14+
return $el.prop('name') || $el.data('name') || '';
1515
});

src/build/backbone.syphon.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
// @include ../backbone.syphon.js
3131
// @include ../backbone.syphon.typeregistry.js
3232
// @include ../backbone.syphon.keyextractors.js
33+
// @include ../backbone.syphon.inputfetcher.js
3334
// @include ../backbone.syphon.inputreaders.js
3435
// @include ../backbone.syphon.inputwriters.js
3536
// @include ../backbone.syphon.keyassignmentvalidators.js

0 commit comments

Comments
 (0)