| // Backbone.Syphon, v0.4.1 |
| // Copyright (c)2012 Derick Bailey, Muted Solutions, LLC. |
| // Distributed under MIT license |
| // http://github.com/derickbailey/backbone.syphon |
| 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 `<input>` element, or the `tagName` of |
| // the element when the element is not an `<input>`. |
| 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: |
| // `<input name="foo.bar.baz">`, `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 + "]"; |
| } |