blob: 3cd1537e6a7990746693b26dd22c13b5cbbb6343 [file] [log] [blame]
Scott Baker1b168b62014-10-28 12:03:28 -07001// 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
5Backbone.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
292Backbone.Syphon.TypeRegistry = function(){
293 this.registeredTypes = {};
294};
295
296// Borrow Backbone's `extend` keyword for our TypeRegistry
297Backbone.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.
341Backbone.Syphon.KeyExtractorSet = Backbone.Syphon.TypeRegistry.extend();
342
343// Built-in Key Extractors
344Backbone.Syphon.KeyExtractors = new Backbone.Syphon.KeyExtractorSet();
345
346// The default key extractor, which uses the
347// input element's "id" attribute
348Backbone.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
358Backbone.Syphon.InputReaderSet = Backbone.Syphon.TypeRegistry.extend();
359
360// Built-in Input Readers
361Backbone.Syphon.InputReaders = new Backbone.Syphon.InputReaderSet();
362
363// The default input reader, which uses an input
364// element's "value"
365Backbone.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.
371Backbone.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.
382Backbone.Syphon.InputWriterSet = Backbone.Syphon.TypeRegistry.extend();
383
384// Built-in Input Writers
385Backbone.Syphon.InputWriters = new Backbone.Syphon.InputWriterSet();
386
387// The default input writer, which sets an input
388// element's "value"
389Backbone.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.
395Backbone.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.
402Backbone.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
414Backbone.Syphon.KeyAssignmentValidatorSet = Backbone.Syphon.TypeRegistry.extend();
415
416// Build-in Key Assignment Validators
417Backbone.Syphon.KeyAssignmentValidators = new Backbone.Syphon.KeyAssignmentValidatorSet();
418
419// Everything is valid by default
420Backbone.Syphon.KeyAssignmentValidators.registerDefault(function(){ return true; });
421
422// But only the "checked" radio button for a given
423// radio button group is valid
424Backbone.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(".")`
438Backbone.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
467Backbone.Syphon.KeyJoiner = function(parentKey, childKey){
468 return parentKey + "[" + childKey + "]";
469}