blob: 3cd1537e6a7990746693b26dd22c13b5cbbb6343 [file] [log] [blame]
// 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 + "]";
}