Source: forms.js

/*!
 * Module Forms
 */

/**
 * @namespace Forms
 */

const MODULE_NAME = 'Forms';



//###[ IMPORTS ]########################################################################################################

import {isFunction, isPlainObject, hasValue} from './basic.js';



//###[ EXPORTS ]########################################################################################################

/**
 * @namespace Forms:formDataToObject
 */

/**
 * Constructs a plain object from an existing FormData object or a given form element.
 *
 * The idea of this function is, to make working with form data easier in programmatic contexts by allowing operations
 * like optional chaining and "in" operators. This might especially come in handy if you need to do programmatic
 * validations.
 *
 * Additionally, this function streamlines field names, by discarding PHP-style array field name conventions like
 * "files[]", by removing the brackets. So, if you have a field named "files[]" and another field named "files",
 * both will just end up in one "files"-field, having an array as a value containing all combined values.
 *
 * Keep in mind, that the status of form fields in a form matters when retrieving FormData from a form element.
 * Disabled fields will not be included for example. Make sure to handle this before using the data.
 *
 * On Internet Explorers, this function needs a polyfill, which is not included in annex, due to its size and
 * complexity, since IEs, while supporting FormData basically, are lacking all functions to access values of an existing
 * FormData object, thereby making it impossible to iterate its values.
 *
 * @param {FormData|HTMLFormElement} formDataOrForm - either an existing FormData object or a form, from which we can retrieve formdata
 * @returns {Object} plain object, containing all form values based on the exisiting fields as key/value-pairs
 *
 * @memberof Forms:formDataToObject
 * @alias formDataToObject
 * @see objectToFormData
 * @see https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData
 * @see https://github.com/jimmywarting/FormData
 * @example
 * const formData = new FormData();
 * formData.append('secrethash', 123456789);
 * formData.append('firstname', 'Paul');
 * formData.append('lastname', 'Atreides');
 * formData.append('houses', 'Atreides');
 * formData.append('houses', 'Fremen');
 * formData.append('houses', 'Corrino');
 * formData.append('diary', new File(['Dear Diary, ...'], 'diary.txt', {type : 'text/plain', lastModified : new Date()}));
 * formData.append('instagramPage', new Blob(['<html>...</html>'], {type : 'text/html'}));
 * formDataToObject(formData)
 * =>
 * {
 *   secrethash : '123456789',
 * 	 firstname : 'Paul',
 * 	 lastname : 'Atreides',
 * 	 houses : ['Atreides', 'Fremen', 'Corrino'],
 * 	 diary : File,
 * 	 instagramPage : Blob
 * }
 */
export function formDataToObject(formDataOrForm){
	let formData;

	// let's do duck-typing to allow polyfills
	if(
		isFunction(formDataOrForm.append)
		&& isFunction(formDataOrForm.getAll)
		&& isFunction(formDataOrForm.entries)
	){
		formData = formDataOrForm;
	} else {
		try {
			formData = new FormData(formDataOrForm);
		} catch(ex){
			formData = null;
		}
	}

	if( !hasValue(formData) ) return null;

	const formDataObject = {};

	Array.from(formData.entries()).forEach(([key, values]) => {
		if( key.endsWith('[]') ){
			key = key.slice(0, -2);
		}
		values = [].concat(values);

		if( !hasValue(formDataObject[key]) ){
			formDataObject[key] = (values.length === 1) ? values[0] : values;
		} else {
			formDataObject[key] = [].concat(formDataObject[key], values);
		}
	});

	return formDataObject;
}



/**
 * @namespace Forms:objectToFormData
 */

/**
 * Constructs a FormData object, to be used in requests, from a given (plain) object, iterating its entries.
 *
 * Additionally, this function streamlines field names, by discarding PHP-style array field name conventions like
 * "files[]", by removing the brackets. So, if you have a field named "files[]" and another field named "files",
 * both will just end up in one "files"-field.
 *
 * Files and Blobs can be provided as-is (constructed programmatically of retrieved from file inputs via `.files`).
 * Alternatively (and if you manually want to define the filename), you can provide plain objects to describe a File or
 * Blob to add to the FormData:
 * - use {file : File, ?name : String} to add "file" as a File and optionally set "name" to define a filename, taking
 *   precedence over what is already defined in the File object itself
 * - use {blob : Blob|String, ?name : String, ?mimeType : String} to add "blob" as a Blob (if this is a string, it
 *   will be treated as the content of a new Blob), optionally using "name" as the filename (I'd recommend to set this)
 *   and optionally setting the file type via the MIME type defined in "mimeType".
 *
 * In contrast to `formDataToObject`, this function does not need a polyfill in Internet Explorer, since it only uses
 * the FormData constructor and the `.append()` method, which are both supported.
 *
 * @param {Object} formDataObject - object to iterate, to create FormData based on its entries
 * @returns {FormData} FormData object to be used in a request
 *
 * @memberof Forms:objectToFormData
 * @alias objectToFormData
 * @see formDataToObject
 * @see https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData
 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
 * @example
 * const formData = objectToFormData({
 *   secrethash : 123456789,
 * 	 firstname : 'Paul',
 * 	 lastname : 'Atreides',
 * 	 houses : ['Atreides', 'Fremen', 'Corrino'],
 * 	 diary : {file : new File(['Dear Diary, ...'], 'diary.txt', {type : 'text/plain', lastModified : new Date()})},
 * 	 instagramPage : {blob : '<html>...</html>', name : 'instagram.html', mimeType : 'text/html'},
 * }).getAll('houses');
 * => ['Atreides', 'Fremen', 'Corrino']
 */
export function objectToFormData(formDataObject){
	const formData = new FormData();

	Object.entries(formDataObject).forEach(([fieldName, fieldValue]) => {
		if( fieldName.endsWith('[]') ){
			fieldName = fieldName.slice(0, -2);
		}

		[].concat(fieldValue).forEach(fieldValue => {
			if( isPlainObject(fieldValue) ){
				if( hasValue(fieldValue.file) && (fieldValue.file instanceof File) ){
					formData.append(
						fieldName,
						fieldValue.file,
						hasValue(fieldValue.name) ? `${fieldValue.name}` : undefined
					);
				} else if( hasValue(fieldValue.blob) ){
					const blob = (fieldValue.blob instanceof Blob)
						? fieldValue.blob
						: new Blob(
							[`${fieldValue.blob}`],
							hasValue(fieldValue.mimeType) ? {type : `${fieldValue.mimeType}`} : undefined
						)
					;

					formData.append(
						fieldName,
						blob,
						hasValue(fieldValue.name) ? `${fieldValue.name}` : undefined
					);
				} else {
					formData.append(fieldName, `${fieldValue}`);
				}
			} else {
				formData.append(fieldName, fieldValue);
			}
		});
	});

	return formData;
}