// Backbone.Syphon, v0.4.1 // Copyright (c)2012 Derick Bailey, Muted Solutions, LLC. // Distributed under MIT license // http://github.com/derickbailey/backbone.syphon (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['underscore', "jquery", "backbone"], factory); } }(this, function (_, jQuery, Backbone) { Backbone.Syphon = (function(Backbone, $, _){ var Syphon = {}; // Ignore Element Types // -------------------- // Tell Syphon to ignore all elements of these types. You can // push new types to ignore directly in to this array. Syphon.ignoredTypes = ["button", "submit", "reset", "fieldset"]; // Syphon // ------ // Get a JSON object that represents // all of the form inputs, in this view. // Alternately, pass a form element directly // in place of the view. Syphon.serialize = function(view, options){ var data = {}; // Build the configuration var config = buildConfig(options); // Get all of the elements to process var elements = getInputElements(view, config); // Process all of the elements _.each(elements, function(el){ var $el = $(el); var type = getElementType($el); // Get the key for the input var keyExtractor = config.keyExtractors.get(type); var key = keyExtractor($el); // Get the value for the input var inputReader = config.inputReaders.get(type); var value = inputReader($el); // Get the key assignment validator and make sure // it's valid before assigning the value to the key var validKeyAssignment = config.keyAssignmentValidators.get(type); if (validKeyAssignment($el, key, value)){ var keychain = config.keySplitter(key); data = assignKeyValue(data, keychain, value); } }); // Done; send back the results. return data; }; // Use the given JSON object to populate // all of the form inputs, in this view. // Alternately, pass a form element directly // in place of the view. Syphon.deserialize = function(view, data, options){ // Build the configuration var config = buildConfig(options); // Get all of the elements to process var elements = getInputElements(view, config); // Flatten the data structure that we are deserializing var flattenedData = flattenData(config, data); // Process all of the elements _.each(elements, function(el){ var $el = $(el); var type = getElementType($el); // Get the key for the input var keyExtractor = config.keyExtractors.get(type); var key = keyExtractor($el); // Get the input writer and the value to write var inputWriter = config.inputWriters.get(type); var value = flattenedData[key]; // Write the value to the input inputWriter($el, value); }); }; // Helpers // ------- // Retrieve all of the form inputs // from the form var getInputElements = function(view, config){ var form = getForm(view); var elements = form.elements; elements = _.reject(elements, function(el){ var reject; var type = getElementType(el); var extractor = config.keyExtractors.get(type); var identifier = extractor($(el)); var foundInIgnored = _.include(config.ignoredTypes, type); var foundInInclude = _.include(config.include, identifier); var foundInExclude = _.include(config.exclude, identifier); if (foundInInclude){ reject = false; } else { if (config.include){ reject = true; } else { reject = (foundInExclude || foundInIgnored); } } return reject; }); return elements; }; // Determine what type of element this is. It // will either return the `type` attribute of // an `` element, or the `tagName` of // the element when the element is not an ``. var getElementType = function(el){ var typeAttr; var $el = $(el); var tagName = $el[0].tagName; var type = tagName; if (tagName.toLowerCase() === "input"){ typeAttr = $el.attr("type"); if (typeAttr){ type = typeAttr; } else { type = "text"; } } // Always return the type as lowercase // so it can be matched to lowercase // type registrations. return type.toLowerCase(); }; // If a form element is given, just return it. // Otherwise, get the form element from the view. var getForm = function(viewOrForm){ if (_.isUndefined(viewOrForm.$el) && viewOrForm.tagName.toLowerCase() === 'form'){ return viewOrForm; } else { return viewOrForm.$el.is("form") ? viewOrForm.el : viewOrForm.$("form")[0]; } }; // 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; return config; }; // Assigns `value` to a parsed JSON key. // // The first parameter is the object which will be // modified to store the key/value pair. // // The second parameter accepts an array of keys as a // string with an option array containing a // single string as the last option. // // The third parameter is the value to be assigned. // // Examples: // // `["foo", "bar", "baz"] => {foo: {bar: {baz: "value"}}}` // // `["foo", "bar", ["baz"]] => {foo: {bar: {baz: ["value"]}}}` // // When the final value is an array with a string, the key // 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) { if (!keychain){ return obj; } var key = keychain.shift(); // build the current object we need to store data if (!obj[key]){ obj[key] = _.isArray(key) ? [] : {}; } // 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; } } // recursive parsing of the array, depth-first if (keychain.length > 0){ assignKeyValue(obj[key], keychain, value); } return obj; }; // Flatten the data structure in to nested strings, using the // provided `KeyJoiner` function. // // Example: // // This input: // // ```js // { // widget: "wombat", // foo: { // bar: "baz", // baz: { // quux: "qux" // }, // quux: ["foo", "bar"] // } // } // ``` // // With a KeyJoiner that uses [ ] square brackets, // should produce this output: // // ```js // { // "widget": "wombat", // "foo[bar]": "baz", // "foo[baz][quux]": "qux", // "foo[quux]": ["foo", "bar"] // } // ``` var flattenData = function(config, data, parentKey){ var flatData = {}; _.each(data, function(value, keyName){ var hash = {}; // If there is a parent key, join it with // the current, child key. if (parentKey){ keyName = config.keyJoiner(parentKey, keyName); } if (_.isArray(value)){ keyName += "[]"; hash[keyName] = value; } else if (_.isObject(value)){ hash = flattenData(config, value, keyName); } else { hash[keyName] = value; } // Store the resulting key/value pairs in the // final flattened data object _.extend(flatData, hash); }); return flatData; }; return Syphon; })(Backbone, jQuery, _); // Type Registry // ------------- // Type Registries allow you to register something to // an input type, and retrieve either the item registered // for a specific type or the default registration Backbone.Syphon.TypeRegistry = function(){ this.registeredTypes = {}; }; // Borrow Backbone's `extend` keyword for our TypeRegistry Backbone.Syphon.TypeRegistry.extend = Backbone.Model.extend; _.extend(Backbone.Syphon.TypeRegistry.prototype, { // Get the registered item by type. If nothing is // found for the specified type, the default is // returned. get: function(type){ var item = this.registeredTypes[type]; if (!item){ item = this.registeredTypes["default"]; } return item; }, // Register a new item for a specified type register: function(type, item){ this.registeredTypes[type] = item; }, // Register a default item to be used when no // item for a specified type is found registerDefault: function(item){ this.registeredTypes["default"] = item; }, // Remove an item from a given type registration unregister: function(type){ if (this.registeredTypes[type]){ delete this.registeredTypes[type]; } } }); // Key Extractors // -------------- // Key extractors produce the "key" in `{key: "value"}` // pairs, when serializing. Backbone.Syphon.KeyExtractorSet = Backbone.Syphon.TypeRegistry.extend(); // Built-in Key Extractors Backbone.Syphon.KeyExtractors = new Backbone.Syphon.KeyExtractorSet(); // The default key extractor, which uses the // input element's "id" attribute Backbone.Syphon.KeyExtractors.registerDefault(function($el){ return $el.prop("name"); }); // Input Readers // ------------- // Input Readers are used to extract the value from // an input element, for the serialized object result Backbone.Syphon.InputReaderSet = Backbone.Syphon.TypeRegistry.extend(); // Built-in Input Readers Backbone.Syphon.InputReaders = new Backbone.Syphon.InputReaderSet(); // The default input reader, which uses an input // element's "value" Backbone.Syphon.InputReaders.registerDefault(function($el){ return $el.val(); }); // Checkbox reader, returning a boolean value for // whether or not the checkbox is checked. Backbone.Syphon.InputReaders.register("checkbox", function($el){ var checked = $el.prop("checked"); return checked; }); // Input Writers // ------------- // Input Writers are used to insert a value from an // object into an input element. Backbone.Syphon.InputWriterSet = Backbone.Syphon.TypeRegistry.extend(); // Built-in Input Writers Backbone.Syphon.InputWriters = new Backbone.Syphon.InputWriterSet(); // The default input writer, which sets an input // element's "value" Backbone.Syphon.InputWriters.registerDefault(function($el, value){ $el.val(value); }); // Checkbox writer, set whether or not the checkbox is checked // depending on the boolean value. Backbone.Syphon.InputWriters.register("checkbox", function($el, value){ $el.prop("checked", value); }); // Radio button writer, set whether or not the radio button is // checked. The button should only be checked if it's value // equals the given value. Backbone.Syphon.InputWriters.register("radio", function($el, value){ $el.prop("checked", $el.val() === value); }); // Key Assignment Validators // ------------------------- // Key Assignment Validators are used to determine 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 data from getting serialized to your object. Backbone.Syphon.KeyAssignmentValidatorSet = Backbone.Syphon.TypeRegistry.extend(); // Build-in Key Assignment Validators Backbone.Syphon.KeyAssignmentValidators = new Backbone.Syphon.KeyAssignmentValidatorSet(); // Everything is valid by default Backbone.Syphon.KeyAssignmentValidators.registerDefault(function(){ return true; }); // But only the "checked" radio button for a given // radio button group is valid Backbone.Syphon.KeyAssignmentValidators.register("radio", function($el, key, value){ return $el.prop("checked"); }); // Backbone.Syphon.KeySplitter // --------------------------- // This function is used to split DOM element keys in to an array // of parts, which are then used to create a nested result structure. // returning `["foo", "bar"]` results in `{foo: { bar: "value" }}`. // // Override this method to use a custom key splitter, such as: // ``, `return key.split(".")` Backbone.Syphon.KeySplitter = function(key){ var matches = key.match(/[^\[\]]+/g); if (key.indexOf("[]") === key.length - 2){ lastKey = matches.pop(); matches.push([lastKey]); } return matches; } // Backbone.Syphon.KeyJoiner // ------------------------- // Take two segments of a key and join them together, to create the // de-normalized key name, when deserializing a data structure back // in to a form. // // Example: // // With this data strucutre `{foo: { bar: {baz: "value", quux: "another"} } }`, // the key joiner will be called with these parameters, and assuming the // join happens with "[ ]" square brackets, the specified output: // // `KeyJoiner("foo", "bar")` //=> "foo[bar]" // `KeyJoiner("foo[bar]", "baz")` //=> "foo[bar][baz]" // `KeyJoiner("foo[bar]", "quux")` //=> "foo[bar][quux]" Backbone.Syphon.KeyJoiner = function(parentKey, childKey){ return parentKey + "[" + childKey + "]"; } return Backbone.Syphon; }));