jquery.serializejson.js
341 lines
| 1 | /*! |
| 2 | SerializeJSON jQuery plugin. |
| 3 | https://github.com/marioizquierdo/jquery.serializeJSON |
| 4 | version 2.8.1 (Dec, 2016) |
| 5 | |
| 6 | Copyright (c) 2012, 2017 Mario Izquierdo |
| 7 | Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) |
| 8 | and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. |
| 9 | */ |
| 10 | (function (factory) { |
| 11 | if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. |
| 12 | define(['jquery'], factory); |
| 13 | } else if (typeof exports === 'object') { // Node/CommonJS |
| 14 | var jQuery = require('jquery'); |
| 15 | module.exports = factory(jQuery); |
| 16 | } else { // Browser globals (zepto supported) |
| 17 | factory(window.jQuery || window.Zepto || window.$); // Zepto supported on browsers as well |
| 18 | } |
| 19 | |
| 20 | }(function ($) { |
| 21 | "use strict"; |
| 22 | |
| 23 | // jQuery('form').serializeJSON() |
| 24 | $.fn.serializeJSON = function (options) { |
| 25 | var f, $form, opts, formAsArray, serializedObject, name, value, parsedValue, _obj, nameWithNoType, type, keys, skipFalsy; |
| 26 | f = $.serializeJSON; |
| 27 | $form = this; // NOTE: the set of matched elements is most likely a form, but it could also be a group of inputs |
| 28 | opts = f.setupOpts(options); // calculate values for options {parseNumbers, parseBoolens, parseNulls, ...} with defaults |
| 29 | |
| 30 | // Use native `serializeArray` function to get an array of {name, value} objects. |
| 31 | formAsArray = $form.serializeArray(); |
| 32 | f.readCheckboxUncheckedValues(formAsArray, opts, $form); // add objects to the array from unchecked checkboxes if needed |
| 33 | |
| 34 | // Convert the formAsArray into a serializedObject with nested keys |
| 35 | serializedObject = {}; |
| 36 | $.each(formAsArray, function (i, obj) { |
| 37 | name = obj.name; // original input name |
| 38 | value = obj.value; // input value |
| 39 | _obj = f.extractTypeAndNameWithNoType(name); |
| 40 | nameWithNoType = _obj.nameWithNoType; // input name with no type (i.e. "foo:string" => "foo") |
| 41 | type = _obj.type; // type defined from the input name in :type colon notation |
| 42 | if (!type) type = f.attrFromInputWithName($form, name, 'data-value-type'); |
| 43 | f.validateType(name, type, opts); // make sure that the type is one of the valid types if defined |
| 44 | |
| 45 | if (type !== 'skip') { // ignore inputs with type 'skip' |
| 46 | keys = f.splitInputNameIntoKeysArray(nameWithNoType); |
| 47 | parsedValue = f.parseValue(value, name, type, opts); // convert to string, number, boolean, null or customType |
| 48 | |
| 49 | skipFalsy = !parsedValue && f.shouldSkipFalsy($form, name, nameWithNoType, type, opts); // ignore falsy inputs if specified |
| 50 | if (!skipFalsy) { |
| 51 | f.deepSet(serializedObject, keys, parsedValue, opts); |
| 52 | } |
| 53 | } |
| 54 | }); |
| 55 | return serializedObject; |
| 56 | }; |
| 57 | |
| 58 | // Use $.serializeJSON as namespace for the auxiliar functions |
| 59 | // and to define defaults |
| 60 | $.serializeJSON = { |
| 61 | |
| 62 | defaultOptions: { |
| 63 | checkboxUncheckedValue: undefined, // to include that value for unchecked checkboxes (instead of ignoring them) |
| 64 | |
| 65 | parseNumbers: false, // convert values like "1", "-2.33" to 1, -2.33 |
| 66 | parseBooleans: false, // convert "true", "false" to true, false |
| 67 | parseNulls: false, // convert "null" to null |
| 68 | parseAll: false, // all of the above |
| 69 | parseWithFunction: null, // to use custom parser, a function like: function(val){ return parsed_val; } |
| 70 | |
| 71 | skipFalsyValuesForTypes: [], // skip serialization of falsy values for listed value types |
| 72 | skipFalsyValuesForFields: [], // skip serialization of falsy values for listed field names |
| 73 | |
| 74 | customTypes: {}, // override defaultTypes |
| 75 | defaultTypes: { |
| 76 | "string": function(str) { return String(str); }, |
| 77 | "number": function(str) { return Number(str); }, |
| 78 | "boolean": function(str) { var falses = ["false", "null", "undefined", "", "0"]; return falses.indexOf(str) === -1; }, |
| 79 | "null": function(str) { var falses = ["false", "null", "undefined", "", "0"]; return falses.indexOf(str) === -1 ? str : null; }, |
| 80 | "array": function(str) { return JSON.parse(str); }, |
| 81 | "object": function(str) { return JSON.parse(str); }, |
| 82 | "auto": function(str) { return $.serializeJSON.parseValue(str, null, null, {parseNumbers: true, parseBooleans: true, parseNulls: true}); }, // try again with something like "parseAll" |
| 83 | "skip": null // skip is a special type that makes it easy to ignore elements |
| 84 | }, |
| 85 | |
| 86 | useIntKeysAsArrayIndex: false // name="foo[2]" value="v" => {foo: [null, null, "v"]}, instead of {foo: ["2": "v"]} |
| 87 | }, |
| 88 | |
| 89 | // Merge option defaults into the options |
| 90 | setupOpts: function(options) { |
| 91 | var opt, validOpts, defaultOptions, optWithDefault, parseAll, f; |
| 92 | f = $.serializeJSON; |
| 93 | |
| 94 | if (options == null) { options = {}; } // options ||= {} |
| 95 | defaultOptions = f.defaultOptions || {}; // defaultOptions |
| 96 | |
| 97 | // Make sure that the user didn't misspell an option |
| 98 | validOpts = ['checkboxUncheckedValue', 'parseNumbers', 'parseBooleans', 'parseNulls', 'parseAll', 'parseWithFunction', 'skipFalsyValuesForTypes', 'skipFalsyValuesForFields', 'customTypes', 'defaultTypes', 'useIntKeysAsArrayIndex']; // re-define because the user may override the defaultOptions |
| 99 | for (opt in options) { |
| 100 | if (validOpts.indexOf(opt) === -1) { |
| 101 | throw new Error("serializeJSON ERROR: invalid option '" + opt + "'. Please use one of " + validOpts.join(', ')); |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | // Helper to get the default value for this option if none is specified by the user |
| 106 | optWithDefault = function(key) { return (options[key] !== false) && (options[key] !== '') && (options[key] || defaultOptions[key]); }; |
| 107 | |
| 108 | // Return computed options (opts to be used in the rest of the script) |
| 109 | parseAll = optWithDefault('parseAll'); |
| 110 | return { |
| 111 | checkboxUncheckedValue: optWithDefault('checkboxUncheckedValue'), |
| 112 | |
| 113 | parseNumbers: parseAll || optWithDefault('parseNumbers'), |
| 114 | parseBooleans: parseAll || optWithDefault('parseBooleans'), |
| 115 | parseNulls: parseAll || optWithDefault('parseNulls'), |
| 116 | parseWithFunction: optWithDefault('parseWithFunction'), |
| 117 | |
| 118 | skipFalsyValuesForTypes: optWithDefault('skipFalsyValuesForTypes'), |
| 119 | skipFalsyValuesForFields: optWithDefault('skipFalsyValuesForFields'), |
| 120 | typeFunctions: $.extend({}, optWithDefault('defaultTypes'), optWithDefault('customTypes')), |
| 121 | |
| 122 | useIntKeysAsArrayIndex: optWithDefault('useIntKeysAsArrayIndex') |
| 123 | }; |
| 124 | }, |
| 125 | |
| 126 | // Given a string, apply the type or the relevant "parse" options, to return the parsed value |
| 127 | parseValue: function(valStr, inputName, type, opts) { |
| 128 | var f, parsedVal; |
| 129 | f = $.serializeJSON; |
| 130 | parsedVal = valStr; // if no parsing is needed, the returned value will be the same |
| 131 | |
| 132 | if (opts.typeFunctions && type && opts.typeFunctions[type]) { // use a type if available |
| 133 | parsedVal = opts.typeFunctions[type](valStr); |
| 134 | } else if (opts.parseNumbers && (! isNaN(valStr) && isFinite(valStr))) { // auto: number |
| 135 | parsedVal = Number(valStr); |
| 136 | } else if (opts.parseBooleans && (valStr === "true" || valStr === "false")) { // auto: boolean |
| 137 | parsedVal = (valStr === "true"); |
| 138 | } else if (opts.parseNulls && valStr == "null") { // auto: null |
| 139 | parsedVal = null; |
| 140 | } |
| 141 | if (opts.parseWithFunction && !type) { // custom parse function (apply after previous parsing options, but not if there's a specific type) |
| 142 | parsedVal = opts.parseWithFunction(parsedVal, inputName); |
| 143 | } |
| 144 | |
| 145 | return parsedVal; |
| 146 | }, |
| 147 | |
| 148 | isObject: function(obj) { return obj === Object(obj); }, // is it an Object? |
| 149 | isUndefined: function(obj) { return obj === void 0; }, // safe check for undefined values |
| 150 | isValidArrayIndex: function(val) { return /^[0-9]+$/.test(String(val)); }, // 1,2,3,4 ... are valid array indexes |
| 151 | isNumeric: function(obj) { return obj - parseFloat(obj) >= 0; }, // taken from jQuery.isNumeric implementation. Not using jQuery.isNumeric to support old jQuery and Zepto versions |
| 152 | |
| 153 | optionKeys: function(obj) { if (Object.keys) { return Object.keys(obj); } else { var key, keys = []; for(key in obj){ keys.push(key); } return keys;} }, // polyfill Object.keys to get option keys in IE<9 |
| 154 | |
| 155 | |
| 156 | // Fill the formAsArray object with values for the unchecked checkbox inputs, |
| 157 | // using the same format as the jquery.serializeArray function. |
| 158 | // The value of the unchecked values is determined from the opts.checkboxUncheckedValue |
| 159 | // and/or the data-unchecked-value attribute of the inputs. |
| 160 | readCheckboxUncheckedValues: function (formAsArray, opts, $form) { |
| 161 | var selector, $uncheckedCheckboxes, $el, uncheckedValue, f, name; |
| 162 | if (opts == null) { opts = {}; } |
| 163 | f = $.serializeJSON; |
| 164 | |
| 165 | selector = 'input[type=checkbox][name]:not(:checked):not([disabled])'; |
| 166 | $uncheckedCheckboxes = $form.find(selector).add($form.filter(selector)); |
| 167 | $uncheckedCheckboxes.each(function (i, el) { |
| 168 | // Check data attr first, then the option |
| 169 | $el = $(el); |
| 170 | uncheckedValue = $el.attr('data-unchecked-value'); |
| 171 | if (uncheckedValue == null) { |
| 172 | uncheckedValue = opts.checkboxUncheckedValue; |
| 173 | } |
| 174 | |
| 175 | // If there's an uncheckedValue, push it into the serialized formAsArray |
| 176 | if (uncheckedValue != null) { |
| 177 | if (el.name && el.name.indexOf("[][") !== -1) { // identify a non-supported |
| 178 | throw new Error("serializeJSON ERROR: checkbox unchecked values are not supported on nested arrays of objects like '"+el.name+"'. See https://github.com/marioizquierdo/jquery.serializeJSON/issues/67"); |
| 179 | } |
| 180 | formAsArray.push({name: el.name, value: uncheckedValue}); |
| 181 | } |
| 182 | }); |
| 183 | }, |
| 184 | |
| 185 | // Returns and object with properties {name_without_type, type} from a given name. |
| 186 | // The type is null if none specified. Example: |
| 187 | // "foo" => {nameWithNoType: "foo", type: null} |
| 188 | // "foo:boolean" => {nameWithNoType: "foo", type: "boolean"} |
| 189 | // "foo[bar]:null" => {nameWithNoType: "foo[bar]", type: "null"} |
| 190 | extractTypeAndNameWithNoType: function(name) { |
| 191 | var match; |
| 192 | if (match = name.match(/(.*):([^:]+)$/)) { |
| 193 | return {nameWithNoType: match[1], type: match[2]}; |
| 194 | } else { |
| 195 | return {nameWithNoType: name, type: null}; |
| 196 | } |
| 197 | }, |
| 198 | |
| 199 | |
| 200 | // Check if this input should be skipped when it has a falsy value, |
| 201 | // depending on the options to skip values by name or type, and the data-skip-falsy attribute. |
| 202 | shouldSkipFalsy: function($form, name, nameWithNoType, type, opts) { |
| 203 | var f = $.serializeJSON; |
| 204 | |
| 205 | var skipFromDataAttr = f.attrFromInputWithName($form, name, 'data-skip-falsy'); |
| 206 | if (skipFromDataAttr != null) { |
| 207 | return skipFromDataAttr !== 'false'; // any value is true, except if explicitly using 'false' |
| 208 | } |
| 209 | |
| 210 | var optForFields = opts.skipFalsyValuesForFields; |
| 211 | if (optForFields && (optForFields.indexOf(nameWithNoType) !== -1 || optForFields.indexOf(name) !== -1)) { |
| 212 | return true; |
| 213 | } |
| 214 | |
| 215 | var optForTypes = opts.skipFalsyValuesForTypes; |
| 216 | if (type == null) type = 'string'; // assume fields with no type are targeted as string |
| 217 | if (optForTypes && optForTypes.indexOf(type) !== -1) { |
| 218 | return true |
| 219 | } |
| 220 | |
| 221 | return false; |
| 222 | }, |
| 223 | |
| 224 | // Finds the first input in $form with this name, and get the given attr from it. |
| 225 | // Returns undefined if no input or no attribute was found. |
| 226 | attrFromInputWithName: function($form, name, attrName) { |
| 227 | var escapedName, selector, $input, attrValue; |
| 228 | escapedName = name.replace(/(:|\.|\[|\]|\s)/g,'\\$1'); // every non-standard character need to be escaped by \\ |
| 229 | selector = '[name="' + escapedName + '"]'; |
| 230 | $input = $form.find(selector).add($form.filter(selector)); // NOTE: this returns only the first $input element if multiple are matched with the same name (i.e. an "array[]"). So, arrays with different element types specified through the data-value-type attr is not supported. |
| 231 | return $input.attr(attrName); |
| 232 | }, |
| 233 | |
| 234 | // Raise an error if the type is not recognized. |
| 235 | validateType: function(name, type, opts) { |
| 236 | var validTypes, f; |
| 237 | f = $.serializeJSON; |
| 238 | validTypes = f.optionKeys(opts ? opts.typeFunctions : f.defaultOptions.defaultTypes); |
| 239 | if (!type || validTypes.indexOf(type) !== -1) { |
| 240 | return true; |
| 241 | } else { |
| 242 | throw new Error("serializeJSON ERROR: Invalid type " + type + " found in input name '" + name + "', please use one of " + validTypes.join(', ')); |
| 243 | } |
| 244 | }, |
| 245 | |
| 246 | |
| 247 | // Split the input name in programatically readable keys. |
| 248 | // Examples: |
| 249 | // "foo" => ['foo'] |
| 250 | // "[foo]" => ['foo'] |
| 251 | // "foo[inn][bar]" => ['foo', 'inn', 'bar'] |
| 252 | // "foo[inn[bar]]" => ['foo', 'inn', 'bar'] |
| 253 | // "foo[inn][arr][0]" => ['foo', 'inn', 'arr', '0'] |
| 254 | // "arr[][val]" => ['arr', '', 'val'] |
| 255 | splitInputNameIntoKeysArray: function(nameWithNoType) { |
| 256 | var keys, f; |
| 257 | f = $.serializeJSON; |
| 258 | keys = nameWithNoType.split('['); // split string into array |
| 259 | keys = $.map(keys, function (key) { return key.replace(/\]/g, ''); }); // remove closing brackets |
| 260 | if (keys[0] === '') { keys.shift(); } // ensure no opening bracket ("[foo][inn]" should be same as "foo[inn]") |
| 261 | return keys; |
| 262 | }, |
| 263 | |
| 264 | // Set a value in an object or array, using multiple keys to set in a nested object or array: |
| 265 | // |
| 266 | // deepSet(obj, ['foo'], v) // obj['foo'] = v |
| 267 | // deepSet(obj, ['foo', 'inn'], v) // obj['foo']['inn'] = v // Create the inner obj['foo'] object, if needed |
| 268 | // deepSet(obj, ['foo', 'inn', '123'], v) // obj['foo']['arr']['123'] = v // |
| 269 | // |
| 270 | // deepSet(obj, ['0'], v) // obj['0'] = v |
| 271 | // deepSet(arr, ['0'], v, {useIntKeysAsArrayIndex: true}) // arr[0] = v |
| 272 | // deepSet(arr, [''], v) // arr.push(v) |
| 273 | // deepSet(obj, ['arr', ''], v) // obj['arr'].push(v) |
| 274 | // |
| 275 | // arr = []; |
| 276 | // deepSet(arr, ['', v] // arr => [v] |
| 277 | // deepSet(arr, ['', 'foo'], v) // arr => [v, {foo: v}] |
| 278 | // deepSet(arr, ['', 'bar'], v) // arr => [v, {foo: v, bar: v}] |
| 279 | // deepSet(arr, ['', 'bar'], v) // arr => [v, {foo: v, bar: v}, {bar: v}] |
| 280 | // |
| 281 | deepSet: function (o, keys, value, opts) { |
| 282 | var key, nextKey, tail, lastIdx, lastVal, f; |
| 283 | if (opts == null) { opts = {}; } |
| 284 | f = $.serializeJSON; |
| 285 | if (f.isUndefined(o)) { throw new Error("ArgumentError: param 'o' expected to be an object or array, found undefined"); } |
| 286 | if (!keys || keys.length === 0) { throw new Error("ArgumentError: param 'keys' expected to be an array with least one element"); } |
| 287 | |
| 288 | key = keys[0]; |
| 289 | |
| 290 | // Only one key, then it's not a deepSet, just assign the value. |
| 291 | if (keys.length === 1) { |
| 292 | if (key === '') { |
| 293 | o.push(value); // '' is used to push values into the array (assume o is an array) |
| 294 | } else { |
| 295 | o[key] = value; // other keys can be used as object keys or array indexes |
| 296 | } |
| 297 | |
| 298 | // With more keys is a deepSet. Apply recursively. |
| 299 | } else { |
| 300 | nextKey = keys[1]; |
| 301 | |
| 302 | // '' is used to push values into the array, |
| 303 | // with nextKey, set the value into the same object, in object[nextKey]. |
| 304 | // Covers the case of ['', 'foo'] and ['', 'var'] to push the object {foo, var}, and the case of nested arrays. |
| 305 | if (key === '') { |
| 306 | lastIdx = o.length - 1; // asume o is array |
| 307 | lastVal = o[lastIdx]; |
| 308 | if (f.isObject(lastVal) && (f.isUndefined(lastVal[nextKey]) || keys.length > 2)) { // if nextKey is not present in the last object element, or there are more keys to deep set |
| 309 | key = lastIdx; // then set the new value in the same object element |
| 310 | } else { |
| 311 | key = lastIdx + 1; // otherwise, point to set the next index in the array |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | // '' is used to push values into the array "array[]" |
| 316 | if (nextKey === '') { |
| 317 | if (f.isUndefined(o[key]) || !Array.isArray(o[key])) { |
| 318 | o[key] = []; // define (or override) as array to push values |
| 319 | } |
| 320 | } else { |
| 321 | if (opts.useIntKeysAsArrayIndex && f.isValidArrayIndex(nextKey)) { // if 1, 2, 3 ... then use an array, where nextKey is the index |
| 322 | if (f.isUndefined(o[key]) || !Array.isArray(o[key])) { |
| 323 | o[key] = []; // define (or override) as array, to insert values using int keys as array indexes |
| 324 | } |
| 325 | } else { // for anything else, use an object, where nextKey is going to be the attribute name |
| 326 | if (f.isUndefined(o[key]) || !f.isObject(o[key])) { |
| 327 | o[key] = {}; // define (or override) as object, to set nested properties |
| 328 | } |
| 329 | } |
| 330 | } |
| 331 | |
| 332 | // Recursively set the inner object |
| 333 | tail = keys.slice(1); |
| 334 | f.deepSet(o[key], tail, value, opts); |
| 335 | } |
| 336 | } |
| 337 | |
| 338 | }; |
| 339 | |
| 340 | })); |
| 341 |