Scott Baker | 1b168b6 | 2014-10-28 12:03:28 -0700 | [diff] [blame] | 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, $, _){ |
| 6 | var Syphon = {}; |
| 7 | |
| 8 | // Ignore Element Types |
| 9 | // -------------------- |
| 10 | |
| 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"]; |
| 14 | |
| 15 | // Syphon |
| 16 | // ------ |
| 17 | |
| 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){ |
| 23 | var data = {}; |
| 24 | |
| 25 | // Build the configuration |
| 26 | var config = buildConfig(options); |
| 27 | |
| 28 | // Get all of the elements to process |
| 29 | var elements = getInputElements(view, config); |
| 30 | |
| 31 | // Process all of the elements |
| 32 | _.each(elements, function(el){ |
| 33 | var $el = $(el); |
| 34 | var type = getElementType($el); |
| 35 | |
| 36 | // Get the key for the input |
| 37 | var keyExtractor = config.keyExtractors.get(type); |
| 38 | var key = keyExtractor($el); |
| 39 | |
| 40 | // Get the value for the input |
| 41 | var inputReader = config.inputReaders.get(type); |
| 42 | var value = inputReader($el); |
| 43 | |
| 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); |
| 50 | } |
| 51 | }); |
| 52 | |
| 53 | // Done; send back the results. |
| 54 | return data; |
| 55 | }; |
| 56 | |
| 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); |
| 64 | |
| 65 | // Get all of the elements to process |
| 66 | var elements = getInputElements(view, config); |
| 67 | |
| 68 | // Flatten the data structure that we are deserializing |
| 69 | var flattenedData = flattenData(config, data); |
| 70 | |
| 71 | // Process all of the elements |
| 72 | _.each(elements, function(el){ |
| 73 | var $el = $(el); |
| 74 | var type = getElementType($el); |
| 75 | |
| 76 | // Get the key for the input |
| 77 | var keyExtractor = config.keyExtractors.get(type); |
| 78 | var key = keyExtractor($el); |
| 79 | |
| 80 | // Get the input writer and the value to write |
| 81 | var inputWriter = config.inputWriters.get(type); |
| 82 | var value = flattenedData[key]; |
| 83 | |
| 84 | // Write the value to the input |
| 85 | inputWriter($el, value); |
| 86 | }); |
| 87 | }; |
| 88 | |
| 89 | // Helpers |
| 90 | // ------- |
| 91 | |
| 92 | // Retrieve all of the form inputs |
| 93 | // from the form |
| 94 | var getInputElements = function(view, config){ |
| 95 | var form = getForm(view); |
| 96 | var elements = form.elements; |
| 97 | |
| 98 | elements = _.reject(elements, function(el){ |
| 99 | var reject; |
| 100 | var type = getElementType(el); |
| 101 | var extractor = config.keyExtractors.get(type); |
| 102 | var identifier = extractor($(el)); |
| 103 | |
| 104 | var foundInIgnored = _.include(config.ignoredTypes, type); |
| 105 | var foundInInclude = _.include(config.include, identifier); |
| 106 | var foundInExclude = _.include(config.exclude, identifier); |
| 107 | |
| 108 | if (foundInInclude){ |
| 109 | reject = false; |
| 110 | } else { |
| 111 | if (config.include){ |
| 112 | reject = true; |
| 113 | } else { |
| 114 | reject = (foundInExclude || foundInIgnored); |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | return reject; |
| 119 | }); |
| 120 | |
| 121 | return elements; |
| 122 | }; |
| 123 | |
| 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){ |
| 129 | var typeAttr; |
| 130 | var $el = $(el); |
| 131 | var tagName = $el[0].tagName; |
| 132 | var type = tagName; |
| 133 | |
| 134 | if (tagName.toLowerCase() === "input"){ |
| 135 | typeAttr = $el.attr("type"); |
| 136 | if (typeAttr){ |
| 137 | type = typeAttr; |
| 138 | } else { |
| 139 | type = "text"; |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | // Always return the type as lowercase |
| 144 | // so it can be matched to lowercase |
| 145 | // type registrations. |
| 146 | return type.toLowerCase(); |
| 147 | }; |
| 148 | |
| 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'){ |
| 153 | return viewOrForm; |
| 154 | } else { |
| 155 | return viewOrForm.$el.is("form") ? viewOrForm.el : viewOrForm.$("form")[0]; |
| 156 | } |
| 157 | }; |
| 158 | |
| 159 | // Build a configuration object and initialize |
| 160 | // default values. |
| 161 | var buildConfig = function(options){ |
| 162 | var config = _.clone(options) || {}; |
| 163 | |
| 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; |
| 171 | |
| 172 | return config; |
| 173 | }; |
| 174 | |
| 175 | // Assigns `value` to a parsed JSON key. |
| 176 | // |
| 177 | // The first parameter is the object which will be |
| 178 | // modified to store the key/value pair. |
| 179 | // |
| 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. |
| 183 | // |
| 184 | // The third parameter is the value to be assigned. |
| 185 | // |
| 186 | // Examples: |
| 187 | // |
| 188 | // `["foo", "bar", "baz"] => {foo: {bar: {baz: "value"}}}` |
| 189 | // |
| 190 | // `["foo", "bar", ["baz"]] => {foo: {bar: {baz: ["value"]}}}` |
| 191 | // |
| 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; } |
| 198 | |
| 199 | var key = keychain.shift(); |
| 200 | |
| 201 | // build the current object we need to store data |
| 202 | if (!obj[key]){ |
| 203 | obj[key] = _.isArray(key) ? [] : {}; |
| 204 | } |
| 205 | |
| 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); |
| 210 | } else { |
| 211 | obj[key] = value; |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | // recursive parsing of the array, depth-first |
| 216 | if (keychain.length > 0){ |
| 217 | assignKeyValue(obj[key], keychain, value); |
| 218 | } |
| 219 | |
| 220 | return obj; |
| 221 | }; |
| 222 | |
| 223 | // Flatten the data structure in to nested strings, using the |
| 224 | // provided `KeyJoiner` function. |
| 225 | // |
| 226 | // Example: |
| 227 | // |
| 228 | // This input: |
| 229 | // |
| 230 | // ```js |
| 231 | // { |
| 232 | // widget: "wombat", |
| 233 | // foo: { |
| 234 | // bar: "baz", |
| 235 | // baz: { |
| 236 | // quux: "qux" |
| 237 | // }, |
| 238 | // quux: ["foo", "bar"] |
| 239 | // } |
| 240 | // } |
| 241 | // ``` |
| 242 | // |
| 243 | // With a KeyJoiner that uses [ ] square brackets, |
| 244 | // should produce this output: |
| 245 | // |
| 246 | // ```js |
| 247 | // { |
| 248 | // "widget": "wombat", |
| 249 | // "foo[bar]": "baz", |
| 250 | // "foo[baz][quux]": "qux", |
| 251 | // "foo[quux]": ["foo", "bar"] |
| 252 | // } |
| 253 | // ``` |
| 254 | var flattenData = function(config, data, parentKey){ |
| 255 | var flatData = {}; |
| 256 | |
| 257 | _.each(data, function(value, keyName){ |
| 258 | var hash = {}; |
| 259 | |
| 260 | // If there is a parent key, join it with |
| 261 | // the current, child key. |
| 262 | if (parentKey){ |
| 263 | keyName = config.keyJoiner(parentKey, keyName); |
| 264 | } |
| 265 | |
| 266 | if (_.isArray(value)){ |
| 267 | keyName += "[]"; |
| 268 | hash[keyName] = value; |
| 269 | } else if (_.isObject(value)){ |
| 270 | hash = flattenData(config, value, keyName); |
| 271 | } else { |
| 272 | hash[keyName] = value; |
| 273 | } |
| 274 | |
| 275 | // Store the resulting key/value pairs in the |
| 276 | // final flattened data object |
| 277 | _.extend(flatData, hash); |
| 278 | }); |
| 279 | |
| 280 | return flatData; |
| 281 | }; |
| 282 | |
| 283 | return Syphon; |
| 284 | })(Backbone, jQuery, _); |
| 285 | |
| 286 | // Type Registry |
| 287 | // ------------- |
| 288 | |
| 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 = {}; |
| 294 | }; |
| 295 | |
| 296 | // Borrow Backbone's `extend` keyword for our TypeRegistry |
| 297 | Backbone.Syphon.TypeRegistry.extend = Backbone.Model.extend; |
| 298 | |
| 299 | _.extend(Backbone.Syphon.TypeRegistry.prototype, { |
| 300 | |
| 301 | // Get the registered item by type. If nothing is |
| 302 | // found for the specified type, the default is |
| 303 | // returned. |
| 304 | get: function(type){ |
| 305 | var item = this.registeredTypes[type]; |
| 306 | |
| 307 | if (!item){ |
| 308 | item = this.registeredTypes["default"]; |
| 309 | } |
| 310 | |
| 311 | return item; |
| 312 | }, |
| 313 | |
| 314 | // Register a new item for a specified type |
| 315 | register: function(type, item){ |
| 316 | this.registeredTypes[type] = item; |
| 317 | }, |
| 318 | |
| 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; |
| 323 | }, |
| 324 | |
| 325 | // Remove an item from a given type registration |
| 326 | unregister: function(type){ |
| 327 | if (this.registeredTypes[type]){ |
| 328 | delete this.registeredTypes[type]; |
| 329 | } |
| 330 | } |
| 331 | }); |
| 332 | |
| 333 | |
| 334 | |
| 335 | |
| 336 | // Key Extractors |
| 337 | // -------------- |
| 338 | |
| 339 | // Key extractors produce the "key" in `{key: "value"}` |
| 340 | // pairs, when serializing. |
| 341 | Backbone.Syphon.KeyExtractorSet = Backbone.Syphon.TypeRegistry.extend(); |
| 342 | |
| 343 | // Built-in Key Extractors |
| 344 | Backbone.Syphon.KeyExtractors = new Backbone.Syphon.KeyExtractorSet(); |
| 345 | |
| 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"); |
| 350 | }); |
| 351 | |
| 352 | |
| 353 | // Input Readers |
| 354 | // ------------- |
| 355 | |
| 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(); |
| 359 | |
| 360 | // Built-in Input Readers |
| 361 | Backbone.Syphon.InputReaders = new Backbone.Syphon.InputReaderSet(); |
| 362 | |
| 363 | // The default input reader, which uses an input |
| 364 | // element's "value" |
| 365 | Backbone.Syphon.InputReaders.registerDefault(function($el){ |
| 366 | return $el.val(); |
| 367 | }); |
| 368 | |
| 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"); |
| 373 | return checked; |
| 374 | }); |
| 375 | |
| 376 | |
| 377 | // Input Writers |
| 378 | // ------------- |
| 379 | |
| 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(); |
| 383 | |
| 384 | // Built-in Input Writers |
| 385 | Backbone.Syphon.InputWriters = new Backbone.Syphon.InputWriterSet(); |
| 386 | |
| 387 | // The default input writer, which sets an input |
| 388 | // element's "value" |
| 389 | Backbone.Syphon.InputWriters.registerDefault(function($el, value){ |
| 390 | $el.val(value); |
| 391 | }); |
| 392 | |
| 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); |
| 397 | }); |
| 398 | |
| 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); |
| 404 | }); |
| 405 | |
| 406 | // Key Assignment Validators |
| 407 | // ------------------------- |
| 408 | |
| 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. |
| 413 | |
| 414 | Backbone.Syphon.KeyAssignmentValidatorSet = Backbone.Syphon.TypeRegistry.extend(); |
| 415 | |
| 416 | // Build-in Key Assignment Validators |
| 417 | Backbone.Syphon.KeyAssignmentValidators = new Backbone.Syphon.KeyAssignmentValidatorSet(); |
| 418 | |
| 419 | // Everything is valid by default |
| 420 | Backbone.Syphon.KeyAssignmentValidators.registerDefault(function(){ return true; }); |
| 421 | |
| 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"); |
| 426 | }); |
| 427 | |
| 428 | |
| 429 | // Backbone.Syphon.KeySplitter |
| 430 | // --------------------------- |
| 431 | |
| 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" }}`. |
| 435 | // |
| 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); |
| 440 | |
| 441 | if (key.indexOf("[]") === key.length - 2){ |
| 442 | lastKey = matches.pop(); |
| 443 | matches.push([lastKey]); |
| 444 | } |
| 445 | |
| 446 | return matches; |
| 447 | } |
| 448 | |
| 449 | |
| 450 | // Backbone.Syphon.KeyJoiner |
| 451 | // ------------------------- |
| 452 | |
| 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 |
| 455 | // in to a form. |
| 456 | // |
| 457 | // Example: |
| 458 | // |
| 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: |
| 462 | // |
| 463 | // `KeyJoiner("foo", "bar")` //=> "foo[bar]" |
| 464 | // `KeyJoiner("foo[bar]", "baz")` //=> "foo[bar][baz]" |
| 465 | // `KeyJoiner("foo[bar]", "quux")` //=> "foo[bar][quux]" |
| 466 | |
| 467 | Backbone.Syphon.KeyJoiner = function(parentKey, childKey){ |
| 468 | return parentKey + "[" + childKey + "]"; |
| 469 | } |