1 // Backbone.Syphon, v0.4.1
2 // Copyright (c)2012 Derick Bailey, Muted Solutions, LLC.
3 // Distributed under MIT license
4 // http://github.com/derickbailey/backbone.syphon
5 Backbone.Syphon = (function(Backbone, $, _){
8 // Ignore Element Types
9 // --------------------
11 // Tell Syphon to ignore all elements of these types. You can
12 // push new types to ignore directly in to this array.
13 Syphon.ignoredTypes = ["button", "submit", "reset", "fieldset"];
18 // Get a JSON object that represents
19 // all of the form inputs, in this view.
20 // Alternately, pass a form element directly
21 // in place of the view.
22 Syphon.serialize = function(view, options){
25 // Build the configuration
26 var config = buildConfig(options);
28 // Get all of the elements to process
29 var elements = getInputElements(view, config);
31 // Process all of the elements
32 _.each(elements, function(el){
34 var type = getElementType($el);
36 // Get the key for the input
37 var keyExtractor = config.keyExtractors.get(type);
38 var key = keyExtractor($el);
40 // Get the value for the input
41 var inputReader = config.inputReaders.get(type);
42 var value = inputReader($el);
44 // Get the key assignment validator and make sure
45 // it's valid before assigning the value to the key
46 var validKeyAssignment = config.keyAssignmentValidators.get(type);
47 if (validKeyAssignment($el, key, value)){
48 var keychain = config.keySplitter(key);
49 data = assignKeyValue(data, keychain, value);
53 // Done; send back the results.
57 // Use the given JSON object to populate
58 // all of the form inputs, in this view.
59 // Alternately, pass a form element directly
60 // in place of the view.
61 Syphon.deserialize = function(view, data, options){
62 // Build the configuration
63 var config = buildConfig(options);
65 // Get all of the elements to process
66 var elements = getInputElements(view, config);
68 // Flatten the data structure that we are deserializing
69 var flattenedData = flattenData(config, data);
71 // Process all of the elements
72 _.each(elements, function(el){
74 var type = getElementType($el);
76 // Get the key for the input
77 var keyExtractor = config.keyExtractors.get(type);
78 var key = keyExtractor($el);
80 // Get the input writer and the value to write
81 var inputWriter = config.inputWriters.get(type);
82 var value = flattenedData[key];
84 // Write the value to the input
85 inputWriter($el, value);
92 // Retrieve all of the form inputs
94 var getInputElements = function(view, config){
95 var form = getForm(view);
96 var elements = form.elements;
98 elements = _.reject(elements, function(el){
100 var type = getElementType(el);
101 var extractor = config.keyExtractors.get(type);
102 var identifier = extractor($(el));
104 var foundInIgnored = _.include(config.ignoredTypes, type);
105 var foundInInclude = _.include(config.include, identifier);
106 var foundInExclude = _.include(config.exclude, identifier);
114 reject = (foundInExclude || foundInIgnored);
124 // Determine what type of element this is. It
125 // will either return the `type` attribute of
126 // an `<input>` element, or the `tagName` of
127 // the element when the element is not an `<input>`.
128 var getElementType = function(el){
131 var tagName = $el[0].tagName;
134 if (tagName.toLowerCase() === "input"){
135 typeAttr = $el.attr("type");
143 // Always return the type as lowercase
144 // so it can be matched to lowercase
145 // type registrations.
146 return type.toLowerCase();
149 // If a form element is given, just return it.
150 // Otherwise, get the form element from the view.
151 var getForm = function(viewOrForm){
152 if (_.isUndefined(viewOrForm.$el) && viewOrForm.tagName.toLowerCase() === 'form'){
155 return viewOrForm.$el.is("form") ? viewOrForm.el : viewOrForm.$("form")[0];
159 // Build a configuration object and initialize
161 var buildConfig = function(options){
162 var config = _.clone(options) || {};
164 config.ignoredTypes = _.clone(Syphon.ignoredTypes);
165 config.inputReaders = config.inputReaders || Syphon.InputReaders;
166 config.inputWriters = config.inputWriters || Syphon.InputWriters;
167 config.keyExtractors = config.keyExtractors || Syphon.KeyExtractors;
168 config.keySplitter = config.keySplitter || Syphon.KeySplitter;
169 config.keyJoiner = config.keyJoiner || Syphon.KeyJoiner;
170 config.keyAssignmentValidators = config.keyAssignmentValidators || Syphon.KeyAssignmentValidators;
175 // Assigns `value` to a parsed JSON key.
177 // The first parameter is the object which will be
178 // modified to store the key/value pair.
180 // The second parameter accepts an array of keys as a
181 // string with an option array containing a
182 // single string as the last option.
184 // The third parameter is the value to be assigned.
188 // `["foo", "bar", "baz"] => {foo: {bar: {baz: "value"}}}`
190 // `["foo", "bar", ["baz"]] => {foo: {bar: {baz: ["value"]}}}`
192 // When the final value is an array with a string, the key
193 // becomes an array, and values are pushed in to the array,
194 // allowing multiple fields with the same name to be
195 // assigned to the array.
196 var assignKeyValue = function(obj, keychain, value) {
197 if (!keychain){ return obj; }
199 var key = keychain.shift();
201 // build the current object we need to store data
203 obj[key] = _.isArray(key) ? [] : {};
206 // if it's the last key in the chain, assign the value directly
207 if (keychain.length === 0){
208 if (_.isArray(obj[key])){
209 obj[key].push(value);
215 // recursive parsing of the array, depth-first
216 if (keychain.length > 0){
217 assignKeyValue(obj[key], keychain, value);
223 // Flatten the data structure in to nested strings, using the
224 // provided `KeyJoiner` function.
238 // quux: ["foo", "bar"]
243 // With a KeyJoiner that uses [ ] square brackets,
244 // should produce this output:
248 // "widget": "wombat",
249 // "foo[bar]": "baz",
250 // "foo[baz][quux]": "qux",
251 // "foo[quux]": ["foo", "bar"]
254 var flattenData = function(config, data, parentKey){
257 _.each(data, function(value, keyName){
260 // If there is a parent key, join it with
261 // the current, child key.
263 keyName = config.keyJoiner(parentKey, keyName);
266 if (_.isArray(value)){
268 hash[keyName] = value;
269 } else if (_.isObject(value)){
270 hash = flattenData(config, value, keyName);
272 hash[keyName] = value;
275 // Store the resulting key/value pairs in the
276 // final flattened data object
277 _.extend(flatData, hash);
284 })(Backbone, jQuery, _);
289 // Type Registries allow you to register something to
290 // an input type, and retrieve either the item registered
291 // for a specific type or the default registration
292 Backbone.Syphon.TypeRegistry = function(){
293 this.registeredTypes = {};
296 // Borrow Backbone's `extend` keyword for our TypeRegistry
297 Backbone.Syphon.TypeRegistry.extend = Backbone.Model.extend;
299 _.extend(Backbone.Syphon.TypeRegistry.prototype, {
301 // Get the registered item by type. If nothing is
302 // found for the specified type, the default is
305 var item = this.registeredTypes[type];
308 item = this.registeredTypes["default"];
314 // Register a new item for a specified type
315 register: function(type, item){
316 this.registeredTypes[type] = item;
319 // Register a default item to be used when no
320 // item for a specified type is found
321 registerDefault: function(item){
322 this.registeredTypes["default"] = item;
325 // Remove an item from a given type registration
326 unregister: function(type){
327 if (this.registeredTypes[type]){
328 delete this.registeredTypes[type];
339 // Key extractors produce the "key" in `{key: "value"}`
340 // pairs, when serializing.
341 Backbone.Syphon.KeyExtractorSet = Backbone.Syphon.TypeRegistry.extend();
343 // Built-in Key Extractors
344 Backbone.Syphon.KeyExtractors = new Backbone.Syphon.KeyExtractorSet();
346 // The default key extractor, which uses the
347 // input element's "id" attribute
348 Backbone.Syphon.KeyExtractors.registerDefault(function($el){
349 return $el.prop("name");
356 // Input Readers are used to extract the value from
357 // an input element, for the serialized object result
358 Backbone.Syphon.InputReaderSet = Backbone.Syphon.TypeRegistry.extend();
360 // Built-in Input Readers
361 Backbone.Syphon.InputReaders = new Backbone.Syphon.InputReaderSet();
363 // The default input reader, which uses an input
365 Backbone.Syphon.InputReaders.registerDefault(function($el){
369 // Checkbox reader, returning a boolean value for
370 // whether or not the checkbox is checked.
371 Backbone.Syphon.InputReaders.register("checkbox", function($el){
372 var checked = $el.prop("checked");
380 // Input Writers are used to insert a value from an
381 // object into an input element.
382 Backbone.Syphon.InputWriterSet = Backbone.Syphon.TypeRegistry.extend();
384 // Built-in Input Writers
385 Backbone.Syphon.InputWriters = new Backbone.Syphon.InputWriterSet();
387 // The default input writer, which sets an input
389 Backbone.Syphon.InputWriters.registerDefault(function($el, value){
393 // Checkbox writer, set whether or not the checkbox is checked
394 // depending on the boolean value.
395 Backbone.Syphon.InputWriters.register("checkbox", function($el, value){
396 $el.prop("checked", value);
399 // Radio button writer, set whether or not the radio button is
400 // checked. The button should only be checked if it's value
401 // equals the given value.
402 Backbone.Syphon.InputWriters.register("radio", function($el, value){
403 $el.prop("checked", $el.val() === value);
406 // Key Assignment Validators
407 // -------------------------
409 // Key Assignment Validators are used to determine whether or not a
410 // key should be assigned to a value, after the key and value have been
411 // extracted from the element. This is the last opportunity to prevent
412 // bad data from getting serialized to your object.
414 Backbone.Syphon.KeyAssignmentValidatorSet = Backbone.Syphon.TypeRegistry.extend();
416 // Build-in Key Assignment Validators
417 Backbone.Syphon.KeyAssignmentValidators = new Backbone.Syphon.KeyAssignmentValidatorSet();
419 // Everything is valid by default
420 Backbone.Syphon.KeyAssignmentValidators.registerDefault(function(){ return true; });
422 // But only the "checked" radio button for a given
423 // radio button group is valid
424 Backbone.Syphon.KeyAssignmentValidators.register("radio", function($el, key, value){
425 return $el.prop("checked");
429 // Backbone.Syphon.KeySplitter
430 // ---------------------------
432 // This function is used to split DOM element keys in to an array
433 // of parts, which are then used to create a nested result structure.
434 // returning `["foo", "bar"]` results in `{foo: { bar: "value" }}`.
436 // Override this method to use a custom key splitter, such as:
437 // `<input name="foo.bar.baz">`, `return key.split(".")`
438 Backbone.Syphon.KeySplitter = function(key){
439 var matches = key.match(/[^\[\]]+/g);
441 if (key.indexOf("[]") === key.length - 2){
442 lastKey = matches.pop();
443 matches.push([lastKey]);
450 // Backbone.Syphon.KeyJoiner
451 // -------------------------
453 // Take two segments of a key and join them together, to create the
454 // de-normalized key name, when deserializing a data structure back
459 // With this data strucutre `{foo: { bar: {baz: "value", quux: "another"} } }`,
460 // the key joiner will be called with these parameters, and assuming the
461 // join happens with "[ ]" square brackets, the specified output:
463 // `KeyJoiner("foo", "bar")` //=> "foo[bar]"
464 // `KeyJoiner("foo[bar]", "baz")` //=> "foo[bar][baz]"
465 // `KeyJoiner("foo[bar]", "quux")` //=> "foo[bar][quux]"
467 Backbone.Syphon.KeyJoiner = function(parentKey, childKey){
468 return parentKey + "[" + childKey + "]";