// Backbone.Syphon, v0.8.0 // ---------------------------------- // // Copyright (c) 2018 Derick Bailey, Muted Solutions, LLC. // Distributed under MIT license // // http://github.com/marionettejs/backbone.syphon (function(root, factory) { if (typeof define === 'function' && define.amd) { define(['underscore', 'backbone', 'jquery'], function(_, Backbone, $) { return factory(_, Backbone, $); }); } else if (typeof exports !== 'undefined') { var _ = require('underscore'); var Backbone = require('backbone'); var $ = require('jquery'); module.exports = factory(_, Backbone, $); } else { factory(root._, root.Backbone, root.jQuery); } }(this, function(_, Backbone, $) { 'use strict'; var previousSyphon = Backbone.Syphon; var Syphon = Backbone.Syphon = {}; Syphon.VERSION = '0.8.0'; Syphon.noConflict = function() { Backbone.Syphon = previousSyphon; return this; }; /* jshint maxstatements: 13, maxlen: 102, maxcomplexity: 8, latedef: false */ // 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 formInputs = getForm(view); formInputs = _.reject(formInputs, function(el) { var reject; var myType = getElementType(el); var extractor = config.keyExtractors.get(myType); var identifier = extractor($(el)); var foundInIgnored = _.find(config.ignoredTypes, function(ignoredTypeOrSelector) { return (ignoredTypeOrSelector === myType) || $(el).is(ignoredTypeOrSelector); }); var foundInInclude = _.includes(config.include, identifier); var foundInExclude = _.includes(config.exclude, identifier); if (foundInInclude) { reject = false; } else { if (config.include) { reject = true; } else { reject = (foundInExclude || foundInIgnored); } } return reject; }); return formInputs; }; // 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'; } } if (('' + $el.attr('contenteditable')).toLowerCase() === 'true') { type = 'contenteditable'; } // Always return the type as lowercase // so it can be matched to lowercase // type registrations. return type.toLowerCase(); }; // If a dom element is given, just return the form fields. // Otherwise, get the form fields from the view. var getForm = function(viewOrForm) { if (_.isUndefined(viewOrForm.$el)) { return $(viewOrForm).find(':input, [contenteditable]'); } else { return viewOrForm.$(':input, [contenteditable]'); } }; // 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; }; // 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 var TypeRegistry = Syphon.TypeRegistry = function() { this.registeredTypes = {}; }; // Borrow Backbone's `extend` keyword for our TypeRegistry TypeRegistry.extend = Backbone.Model.extend; _.extend(TypeRegistry.prototype, { // Get the registered item by type. If nothing is // found for the specified type, the default is // returned. get: function(type) { if (_.has(this.registeredTypes, type)) { return this.registeredTypes[type]; } else { return this.registeredTypes['default']; } }, // 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 (_.has(this.registeredTypes, type)) { delete this.registeredTypes[type]; } } }); // Key Extractors // -------------- // Key extractors produce the "key" in `{key: "value"}` // pairs, when serializing. var KeyExtractorSet = Syphon.KeyExtractorSet = TypeRegistry.extend(); // Built-in Key Extractors var KeyExtractors = Syphon.KeyExtractors = new KeyExtractorSet(); // The default key extractor, which uses the // input element's "name" attribute KeyExtractors.registerDefault(function($el) { return $el.prop('name') || $el.data('name') || ''; }); // Input Readers // ------------- // Input Readers are used to extract the value from // an input element, for the serialized object result var InputReaderSet = Syphon.InputReaderSet = TypeRegistry.extend(); // Built-in Input Readers var InputReaders = Syphon.InputReaders = new InputReaderSet(); // The default input reader, which uses an input // element's "value" InputReaders.registerDefault(function($el) { return $el.val(); }); // contenteditable reader, which returns the HTML // contained by the element InputReaders.register('contenteditable', function($el) { return $el.html(); }); // Checkbox reader, returning a boolean value for // whether or not the checkbox is checked. InputReaders.register('checkbox', function($el) { return ($el.prop('indeterminate')) ? null : $el.prop('checked'); }); // Input Writers // ------------- // Input Writers are used to insert a value from an // object into an input element. var InputWriterSet = Syphon.InputWriterSet = TypeRegistry.extend(); // Built-in Input Writers var InputWriters = Syphon.InputWriters = new InputWriterSet(); // The default input writer, which sets an input // element's "value" InputWriters.registerDefault(function($el, value) { $el.val(value); }); // contenteditable writer, which changes the HTML contained by // the element InputWriters.register('contenteditable', function($el, value) { $el.html(value); }); // Checkbox writer, set whether or not the checkbox is checked // depending on the boolean value. InputWriters.register('checkbox', function($el, value) { if (value === null) { $el.prop('indeterminate', true); } else { $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. InputWriters.register('radio', function($el, value) { var checked = _.isUndefined(value) ? false : $el.val() === value.toString(); $el.prop('checked', checked); }); // 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. var KeyAssignmentValidatorSet = Syphon.KeyAssignmentValidatorSet = TypeRegistry.extend(); // Build-in Key Assignment Validators var KeyAssignmentValidators = Syphon.KeyAssignmentValidators = new KeyAssignmentValidatorSet(); // Everything is valid by default KeyAssignmentValidators.registerDefault(function() { return true; }); // But only the "checked" radio button for a given // radio button group is valid 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(".")` Syphon.KeySplitter = function(key) { var matches = key.match(/[^\[\]]+/g); var lastKey; if (key.length > 1 && 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]" Syphon.KeyJoiner = function(parentKey, childKey) { return parentKey + '[' + childKey + ']'; }; return Backbone.Syphon; }));