JavaScript FormData API: Complete Form Handling Guide
Master the FormData API in JavaScript for handling form data. Learn file uploads, form serialization, and AJAX form submissions.
JavaScript FormData API: Complete Form Handling Guide
The FormData API provides a powerful way to construct form data that can be sent using XMLHttpRequest or Fetch API, making it perfect for handling forms, file uploads, and multipart data.
Understanding the FormData API
FormData objects represent form fields and their values, which can be sent using the same format a form would use if the encoding type were set to "multipart/form-data".
// Create FormData from a form element
const form = document.getElementById('myForm');
const formData = new FormData(form);
// Create empty FormData
const emptyFormData = new FormData();
// Add data to FormData
formData.append('username', 'john_doe');
formData.append('email', 'john@example.com');
formData.append('age', 25);
// Add file
const fileInput = document.getElementById('fileInput');
formData.append('avatar', fileInput.files[0]);
// Add multiple values for the same key
formData.append('hobbies', 'reading');
formData.append('hobbies', 'gaming');
// Check if key exists
console.log(formData.has('username')); // true
// Get value
console.log(formData.get('username')); // "john_doe"
// Get all values for a key
console.log(formData.getAll('hobbies')); // ["reading", "gaming"]
// Delete a key
formData.delete('age');
// Set value (replaces if exists)
formData.set('email', 'newemail@example.com');
Form Handling
Advanced Form Manager
class FormManager {
constructor(formElement, options = {}) {
this.form = formElement;
this.options = {
validateOnSubmit: true,
validateOnChange: false,
validateOnBlur: true,
ajaxSubmit: true,
resetAfterSubmit: false,
showProgress: true,
...options,
};
this.validators = new Map();
this.errors = new Map();
this.isSubmitting = false;
this.init();
}
// Initialize form manager
init() {
// Prevent default form submission
this.form.addEventListener('submit', (e) => {
e.preventDefault();
this.handleSubmit();
});
// Setup field validation
if (this.options.validateOnChange || this.options.validateOnBlur) {
this.setupFieldValidation();
}
// Setup file input previews
this.setupFilePreviews();
}
// Handle form submission
async handleSubmit() {
if (this.isSubmitting) return;
this.isSubmitting = true;
this.clearErrors();
try {
// Validate form
if (this.options.validateOnSubmit) {
const isValid = await this.validate();
if (!isValid) {
this.isSubmitting = false;
return;
}
}
// Get form data
const formData = this.getFormData();
// Show progress if enabled
if (this.options.showProgress) {
this.showSubmitProgress();
}
// Submit form
if (this.options.ajaxSubmit) {
const response = await this.submitAjax(formData);
this.handleSubmitSuccess(response);
} else {
this.form.submit();
}
// Reset form if enabled
if (this.options.resetAfterSubmit) {
this.reset();
}
} catch (error) {
this.handleSubmitError(error);
} finally {
this.isSubmitting = false;
this.hideSubmitProgress();
}
}
// Get form data
getFormData(options = {}) {
const {
includeDisabled = false,
includeEmpty = true,
processValues = true,
} = options;
const formData = new FormData();
const elements = this.form.elements;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
// Skip elements without name
if (!element.name) continue;
// Skip disabled elements unless specified
if (element.disabled && !includeDisabled) continue;
// Handle different input types
const value = this.getElementValue(element);
// Skip empty values unless specified
if (!includeEmpty && !value) continue;
// Process value if enabled
const processedValue = processValues
? this.processValue(element, value)
: value;
// Add to FormData
if (processedValue !== null) {
if (Array.isArray(processedValue)) {
processedValue.forEach((val) => formData.append(element.name, val));
} else if (element.type === 'file' && element.files.length > 0) {
for (let j = 0; j < element.files.length; j++) {
formData.append(element.name, element.files[j]);
}
} else {
formData.append(element.name, processedValue);
}
}
}
return formData;
}
// Get element value
getElementValue(element) {
switch (element.type) {
case 'checkbox':
return element.checked ? element.value || 'on' : null;
case 'radio':
return element.checked ? element.value : null;
case 'select-multiple':
const selected = [];
for (let i = 0; i < element.options.length; i++) {
if (element.options[i].selected) {
selected.push(element.options[i].value);
}
}
return selected;
case 'file':
return element.files;
default:
return element.value;
}
}
// Process field value
processValue(element, value) {
// Custom processing based on data attributes
if (element.dataset.type === 'number') {
return parseFloat(value) || 0;
}
if (element.dataset.type === 'boolean') {
return value === 'true' || value === '1' || value === 'on';
}
if (element.dataset.type === 'json') {
try {
return JSON.parse(value);
} catch {
return value;
}
}
if (element.dataset.transform === 'uppercase') {
return value.toUpperCase();
}
if (element.dataset.transform === 'lowercase') {
return value.toLowerCase();
}
return value;
}
// Add validator
addValidator(fieldName, validator, message = 'Invalid value') {
if (!this.validators.has(fieldName)) {
this.validators.set(fieldName, []);
}
this.validators.get(fieldName).push({
validator,
message,
});
}
// Validate form
async validate() {
this.clearErrors();
let isValid = true;
for (const [fieldName, validators] of this.validators) {
const element = this.form.elements[fieldName];
if (!element) continue;
const value = this.getElementValue(element);
for (const { validator, message } of validators) {
try {
const result = await validator(value, element, this.form);
if (result === false || typeof result === 'string') {
this.addError(
fieldName,
typeof result === 'string' ? result : message
);
isValid = false;
break;
}
} catch (error) {
this.addError(fieldName, error.message || message);
isValid = false;
break;
}
}
}
return isValid;
}
// Setup field validation
setupFieldValidation() {
const elements = this.form.elements;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (!element.name || !this.validators.has(element.name)) continue;
if (this.options.validateOnChange) {
element.addEventListener('change', () => {
this.validateField(element.name);
});
}
if (this.options.validateOnBlur) {
element.addEventListener('blur', () => {
this.validateField(element.name);
});
}
}
}
// Validate single field
async validateField(fieldName) {
const validators = this.validators.get(fieldName);
if (!validators) return true;
const element = this.form.elements[fieldName];
if (!element) return true;
const value = this.getElementValue(element);
this.clearFieldError(fieldName);
for (const { validator, message } of validators) {
try {
const result = await validator(value, element, this.form);
if (result === false || typeof result === 'string') {
this.addError(
fieldName,
typeof result === 'string' ? result : message
);
return false;
}
} catch (error) {
this.addError(fieldName, error.message || message);
return false;
}
}
return true;
}
// Add error
addError(fieldName, message) {
if (!this.errors.has(fieldName)) {
this.errors.set(fieldName, []);
}
this.errors.get(fieldName).push(message);
this.displayFieldError(fieldName, message);
}
// Clear all errors
clearErrors() {
this.errors.clear();
// Remove error displays
this.form.querySelectorAll('.field-error').forEach((el) => el.remove());
this.form.querySelectorAll('.has-error').forEach((el) => {
el.classList.remove('has-error');
});
}
// Clear field error
clearFieldError(fieldName) {
this.errors.delete(fieldName);
const element = this.form.elements[fieldName];
if (element) {
const container = element.closest('.form-field') || element.parentElement;
container.classList.remove('has-error');
const errorEl = container.querySelector('.field-error');
if (errorEl) errorEl.remove();
}
}
// Display field error
displayFieldError(fieldName, message) {
const element = this.form.elements[fieldName];
if (!element) return;
const container = element.closest('.form-field') || element.parentElement;
container.classList.add('has-error');
// Remove existing error
const existingError = container.querySelector('.field-error');
if (existingError) existingError.remove();
// Add new error
const errorEl = document.createElement('div');
errorEl.className = 'field-error';
errorEl.textContent = message;
element.parentNode.insertBefore(errorEl, element.nextSibling);
}
// Submit form via AJAX
async submitAjax(formData) {
const method = this.form.method || 'POST';
const action = this.form.action || window.location.href;
const response = await fetch(action, {
method: method.toUpperCase(),
body: formData,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
// Handle submit success
handleSubmitSuccess(response) {
const event = new CustomEvent('form:success', {
detail: { response, form: this.form },
});
this.form.dispatchEvent(event);
// Show success message
this.showMessage('Form submitted successfully!', 'success');
}
// Handle submit error
handleSubmitError(error) {
const event = new CustomEvent('form:error', {
detail: { error, form: this.form },
});
this.form.dispatchEvent(event);
// Show error message
this.showMessage(error.message || 'An error occurred', 'error');
}
// Show message
showMessage(message, type = 'info') {
const messageEl = document.createElement('div');
messageEl.className = `form-message form-message-${type}`;
messageEl.textContent = message;
this.form.insertBefore(messageEl, this.form.firstChild);
setTimeout(() => messageEl.remove(), 5000);
}
// Show submit progress
showSubmitProgress() {
const submitBtn = this.form.querySelector('[type="submit"]');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.dataset.originalText = submitBtn.textContent;
submitBtn.textContent = 'Submitting...';
}
}
// Hide submit progress
hideSubmitProgress() {
const submitBtn = this.form.querySelector('[type="submit"]');
if (submitBtn && submitBtn.dataset.originalText) {
submitBtn.disabled = false;
submitBtn.textContent = submitBtn.dataset.originalText;
delete submitBtn.dataset.originalText;
}
}
// Reset form
reset() {
this.form.reset();
this.clearErrors();
// Clear file previews
this.form.querySelectorAll('.file-preview').forEach((el) => el.remove());
}
// Convert FormData to object
formDataToObject(formData) {
const obj = {};
for (const [key, value] of formData) {
if (key in obj) {
if (!Array.isArray(obj[key])) {
obj[key] = [obj[key]];
}
obj[key].push(value);
} else {
obj[key] = value;
}
}
return obj;
}
// Setup file previews
setupFilePreviews() {
const fileInputs = this.form.querySelectorAll('input[type="file"]');
fileInputs.forEach((input) => {
input.addEventListener('change', (e) => {
this.showFilePreview(e.target);
});
});
}
// Show file preview
showFilePreview(input) {
const files = input.files;
if (!files.length) return;
// Remove existing preview
const existingPreview = input.parentElement.querySelector('.file-preview');
if (existingPreview) existingPreview.remove();
const preview = document.createElement('div');
preview.className = 'file-preview';
for (let i = 0; i < files.length; i++) {
const file = files[i];
const item = document.createElement('div');
item.className = 'file-preview-item';
if (file.type.startsWith('image/')) {
const img = document.createElement('img');
img.src = URL.createObjectURL(file);
img.onload = () => URL.revokeObjectURL(img.src);
item.appendChild(img);
}
const info = document.createElement('div');
info.className = 'file-info';
info.textContent = `${file.name} (${this.formatFileSize(file.size)})`;
item.appendChild(info);
preview.appendChild(item);
}
input.parentElement.appendChild(preview);
}
// Format file size
formatFileSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
}
// Usage
const form = document.getElementById('userForm');
const formManager = new FormManager(form, {
ajaxSubmit: true,
resetAfterSubmit: false,
});
// Add validators
formManager.addValidator('email', (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value) || 'Please enter a valid email address';
});
formManager.addValidator('password', (value) => {
if (value.length < 8) {
return 'Password must be at least 8 characters long';
}
return true;
});
formManager.addValidator('avatar', (files) => {
if (files && files[0]) {
const file = files[0];
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
return 'File size must be less than 5MB';
}
if (!file.type.startsWith('image/')) {
return 'Please upload an image file';
}
}
return true;
});
// Listen for form events
form.addEventListener('form:success', (e) => {
console.log('Form submitted successfully:', e.detail.response);
});
form.addEventListener('form:error', (e) => {
console.error('Form submission error:', e.detail.error);
});
File Upload Handling
Advanced File Uploader
class FileUploader {
constructor(options = {}) {
this.options = {
url: '/upload',
method: 'POST',
maxFileSize: 10 * 1024 * 1024, // 10MB
maxFiles: 10,
allowedTypes: [],
chunkSize: 1024 * 1024, // 1MB chunks
parallel: 3,
autoUpload: true,
...options,
};
this.queue = [];
this.activeUploads = new Map();
this.completedUploads = new Map();
}
// Add files to upload queue
addFiles(files) {
const filesToAdd = Array.from(files).slice(
0,
this.options.maxFiles - this.queue.length
);
filesToAdd.forEach((file) => {
if (this.validateFile(file)) {
const uploadItem = {
id: this.generateId(),
file,
progress: 0,
status: 'pending',
error: null,
response: null,
};
this.queue.push(uploadItem);
this.emit('fileAdded', uploadItem);
}
});
if (this.options.autoUpload) {
this.startUpload();
}
}
// Validate file
validateFile(file) {
// Check file size
if (file.size > this.options.maxFileSize) {
this.emit('validationError', {
file,
error: `File size exceeds ${this.formatSize(this.options.maxFileSize)}`,
});
return false;
}
// Check file type
if (this.options.allowedTypes.length > 0) {
const isAllowed = this.options.allowedTypes.some((type) => {
if (type.includes('*')) {
const [category] = type.split('/');
return file.type.startsWith(category + '/');
}
return file.type === type;
});
if (!isAllowed) {
this.emit('validationError', {
file,
error: 'File type not allowed',
});
return false;
}
}
return true;
}
// Start upload process
async startUpload() {
while (
this.queue.length > 0 &&
this.activeUploads.size < this.options.parallel
) {
const item = this.queue.shift();
this.uploadFile(item);
}
}
// Upload single file
async uploadFile(item) {
item.status = 'uploading';
this.activeUploads.set(item.id, item);
this.emit('uploadStart', item);
try {
if (item.file.size > this.options.chunkSize) {
await this.uploadChunked(item);
} else {
await this.uploadSingle(item);
}
item.status = 'completed';
this.completedUploads.set(item.id, item);
this.emit('uploadComplete', item);
} catch (error) {
item.status = 'error';
item.error = error.message;
this.emit('uploadError', item);
} finally {
this.activeUploads.delete(item.id);
this.startUpload(); // Process next in queue
}
}
// Upload file in single request
async uploadSingle(item) {
const formData = new FormData();
formData.append('file', item.file);
// Add metadata
formData.append('filename', item.file.name);
formData.append('size', item.file.size);
formData.append('type', item.file.type);
const xhr = new XMLHttpRequest();
// Setup progress tracking
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
item.progress = (e.loaded / e.total) * 100;
this.emit('uploadProgress', item);
}
});
// Setup completion handling
return new Promise((resolve, reject) => {
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
item.response = JSON.parse(xhr.responseText);
resolve(item.response);
} catch {
item.response = xhr.responseText;
resolve(item.response);
}
} else {
reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Upload failed: Network error'));
});
xhr.addEventListener('abort', () => {
reject(new Error('Upload aborted'));
});
// Store XHR for cancellation
item.xhr = xhr;
xhr.open(this.options.method, this.options.url);
// Add custom headers
if (this.options.headers) {
Object.entries(this.options.headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value);
});
}
xhr.send(formData);
});
}
// Upload file in chunks
async uploadChunked(item) {
const chunks = Math.ceil(item.file.size / this.options.chunkSize);
const uploadId = this.generateId();
for (let i = 0; i < chunks; i++) {
const start = i * this.options.chunkSize;
const end = Math.min(start + this.options.chunkSize, item.file.size);
const chunk = item.file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('uploadId', uploadId);
formData.append('chunkIndex', i);
formData.append('totalChunks', chunks);
formData.append('filename', item.file.name);
const response = await fetch(this.options.url, {
method: this.options.method,
body: formData,
});
if (!response.ok) {
throw new Error(`Chunk upload failed: ${response.status}`);
}
// Update progress
item.progress = ((i + 1) / chunks) * 100;
this.emit('uploadProgress', item);
}
// Finalize upload
const finalizeData = new FormData();
finalizeData.append('uploadId', uploadId);
finalizeData.append('filename', item.file.name);
finalizeData.append('action', 'finalize');
const finalResponse = await fetch(this.options.url, {
method: this.options.method,
body: finalizeData,
});
if (!finalResponse.ok) {
throw new Error('Failed to finalize upload');
}
item.response = await finalResponse.json();
}
// Cancel upload
cancelUpload(id) {
const item = this.activeUploads.get(id);
if (item && item.xhr) {
item.xhr.abort();
item.status = 'cancelled';
this.emit('uploadCancelled', item);
}
}
// Cancel all uploads
cancelAll() {
this.queue = [];
this.activeUploads.forEach((item, id) => {
this.cancelUpload(id);
});
}
// Retry failed upload
retryUpload(id) {
const item = [...this.completedUploads.values()].find((i) => i.id === id);
if (item && item.status === 'error') {
item.status = 'pending';
item.error = null;
item.progress = 0;
this.queue.push(item);
this.startUpload();
}
}
// Get upload statistics
getStatistics() {
const all = [
...this.queue,
...this.activeUploads.values(),
...this.completedUploads.values(),
];
return {
total: all.length,
pending: all.filter((i) => i.status === 'pending').length,
uploading: all.filter((i) => i.status === 'uploading').length,
completed: all.filter((i) => i.status === 'completed').length,
failed: all.filter((i) => i.status === 'error').length,
totalSize: all.reduce((sum, i) => sum + i.file.size, 0),
uploadedSize: all.reduce((sum, i) => {
if (i.status === 'completed') return sum + i.file.size;
if (i.status === 'uploading')
return sum + (i.file.size * i.progress) / 100;
return sum;
}, 0),
};
}
// Event emitter methods
on(event, callback) {
if (!this.listeners) this.listeners = {};
if (!this.listeners[event]) this.listeners[event] = [];
this.listeners[event].push(callback);
}
emit(event, data) {
if (!this.listeners || !this.listeners[event]) return;
this.listeners[event].forEach((callback) => callback(data));
}
// Helper methods
generateId() {
return `upload-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
formatSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unit = 0;
while (size >= 1024 && unit < units.length - 1) {
size /= 1024;
unit++;
}
return `${size.toFixed(2)} ${units[unit]}`;
}
}
// Usage
const uploader = new FileUploader({
url: '/api/upload',
maxFileSize: 50 * 1024 * 1024, // 50MB
allowedTypes: ['image/*', 'application/pdf'],
chunkSize: 2 * 1024 * 1024, // 2MB chunks
parallel: 2,
});
// Listen to events
uploader.on('fileAdded', (item) => {
console.log('File added:', item.file.name);
});
uploader.on('uploadProgress', (item) => {
console.log(
`Upload progress: ${item.file.name} - ${item.progress.toFixed(2)}%`
);
});
uploader.on('uploadComplete', (item) => {
console.log('Upload complete:', item.file.name, item.response);
});
uploader.on('uploadError', (item) => {
console.error('Upload error:', item.file.name, item.error);
});
// Add files
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (e) => {
uploader.addFiles(e.target.files);
});
// Drag and drop
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
const files = e.dataTransfer.files;
uploader.addFiles(files);
});
Dynamic Form Builder
Form Builder Implementation
class FormBuilder {
constructor(container, options = {}) {
this.container = container;
this.options = {
theme: 'default',
validation: true,
...options,
};
this.fields = [];
this.fieldTypes = new Map();
this.registerDefaultFieldTypes();
}
// Register default field types
registerDefaultFieldTypes() {
// Text input
this.registerFieldType('text', {
render: (field) => `
<div class="form-field">
<label for="${field.id}">${field.label}</label>
<input type="text"
id="${field.id}"
name="${field.name}"
placeholder="${field.placeholder || ''}"
${field.required ? 'required' : ''}
${field.readonly ? 'readonly' : ''}
value="${field.value || ''}">
</div>
`,
getValue: (element) => element.value,
setValue: (element, value) => (element.value = value),
});
// Textarea
this.registerFieldType('textarea', {
render: (field) => `
<div class="form-field">
<label for="${field.id}">${field.label}</label>
<textarea id="${field.id}"
name="${field.name}"
rows="${field.rows || 4}"
placeholder="${field.placeholder || ''}"
${field.required ? 'required' : ''}
${field.readonly ? 'readonly' : ''}>${field.value || ''}</textarea>
</div>
`,
getValue: (element) => element.value,
setValue: (element, value) => (element.value = value),
});
// Select
this.registerFieldType('select', {
render: (field) => `
<div class="form-field">
<label for="${field.id}">${field.label}</label>
<select id="${field.id}"
name="${field.name}"
${field.required ? 'required' : ''}
${field.multiple ? 'multiple' : ''}>
${field.placeholder ? `<option value="">${field.placeholder}</option>` : ''}
${field.options
.map(
(opt) => `
<option value="${opt.value}"
${field.value === opt.value ? 'selected' : ''}>
${opt.label}
</option>
`
)
.join('')}
</select>
</div>
`,
getValue: (element) => {
if (element.multiple) {
return Array.from(element.selectedOptions).map((opt) => opt.value);
}
return element.value;
},
setValue: (element, value) => {
if (element.multiple && Array.isArray(value)) {
Array.from(element.options).forEach((opt) => {
opt.selected = value.includes(opt.value);
});
} else {
element.value = value;
}
},
});
// Checkbox
this.registerFieldType('checkbox', {
render: (field) => `
<div class="form-field">
<label>
<input type="checkbox"
id="${field.id}"
name="${field.name}"
value="${field.value || 'on'}"
${field.checked ? 'checked' : ''}
${field.required ? 'required' : ''}>
${field.label}
</label>
</div>
`,
getValue: (element) => element.checked,
setValue: (element, value) => (element.checked = !!value),
});
// Radio group
this.registerFieldType('radio', {
render: (field) => `
<div class="form-field">
<label>${field.label}</label>
<div class="radio-group">
${field.options
.map(
(opt, index) => `
<label>
<input type="radio"
name="${field.name}"
value="${opt.value}"
${field.value === opt.value ? 'checked' : ''}
${field.required && index === 0 ? 'required' : ''}>
${opt.label}
</label>
`
)
.join('')}
</div>
</div>
`,
getValue: (element) => {
const checked = element.form.elements[element.name];
for (let i = 0; i < checked.length; i++) {
if (checked[i].checked) {
return checked[i].value;
}
}
return null;
},
setValue: (element, value) => {
const radios = element.form.elements[element.name];
for (let i = 0; i < radios.length; i++) {
radios[i].checked = radios[i].value === value;
}
},
});
// File input
this.registerFieldType('file', {
render: (field) => `
<div class="form-field">
<label for="${field.id}">${field.label}</label>
<input type="file"
id="${field.id}"
name="${field.name}"
${field.multiple ? 'multiple' : ''}
${field.accept ? `accept="${field.accept}"` : ''}
${field.required ? 'required' : ''}>
${field.help ? `<small class="help-text">${field.help}</small>` : ''}
</div>
`,
getValue: (element) => element.files,
setValue: (element, value) => {
// Files cannot be programmatically set for security reasons
console.warn('Cannot programmatically set file input value');
},
});
// Date input
this.registerFieldType('date', {
render: (field) => `
<div class="form-field">
<label for="${field.id}">${field.label}</label>
<input type="date"
id="${field.id}"
name="${field.name}"
value="${field.value || ''}"
min="${field.min || ''}"
max="${field.max || ''}"
${field.required ? 'required' : ''}>
</div>
`,
getValue: (element) => element.value,
setValue: (element, value) => (element.value = value),
});
// Custom HTML
this.registerFieldType('html', {
render: (field) => `
<div class="form-field form-html">
${field.content}
</div>
`,
getValue: () => null,
setValue: () => {},
});
}
// Register custom field type
registerFieldType(type, definition) {
this.fieldTypes.set(type, definition);
}
// Add field
addField(fieldConfig) {
const field = {
id:
fieldConfig.id ||
`field-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
type: 'text',
name: fieldConfig.name || fieldConfig.id,
label: '',
value: null,
...fieldConfig,
};
this.fields.push(field);
return field;
}
// Remove field
removeField(fieldId) {
const index = this.fields.findIndex((f) => f.id === fieldId);
if (index > -1) {
this.fields.splice(index, 1);
}
}
// Add section
addSection(title, fields = []) {
this.addField({
type: 'html',
content: `<h3 class="form-section-title">${title}</h3>`,
});
fields.forEach((field) => this.addField(field));
}
// Build form
build() {
const form = document.createElement('form');
form.className = `dynamic-form theme-${this.options.theme}`;
// Render fields
this.fields.forEach((field) => {
const fieldType = this.fieldTypes.get(field.type);
if (fieldType) {
const fieldHtml = fieldType.render(field);
const wrapper = document.createElement('div');
wrapper.innerHTML = fieldHtml;
form.appendChild(wrapper.firstElementChild);
}
});
// Add submit button
const submitButton = document.createElement('button');
submitButton.type = 'submit';
submitButton.textContent = 'Submit';
submitButton.className = 'form-submit';
form.appendChild(submitButton);
// Clear container and append form
this.container.innerHTML = '';
this.container.appendChild(form);
// Initialize form manager if validation enabled
if (this.options.validation) {
this.formManager = new FormManager(form);
}
return form;
}
// Get form data
getFormData() {
const form = this.container.querySelector('form');
if (!form) return null;
const data = {};
this.fields.forEach((field) => {
const fieldType = this.fieldTypes.get(field.type);
if (fieldType && field.name) {
const element = form.elements[field.name];
if (element) {
data[field.name] = fieldType.getValue(element);
}
}
});
return data;
}
// Set form data
setFormData(data) {
const form = this.container.querySelector('form');
if (!form) return;
Object.entries(data).forEach(([name, value]) => {
const field = this.fields.find((f) => f.name === name);
if (!field) return;
const fieldType = this.fieldTypes.get(field.type);
const element = form.elements[name];
if (fieldType && element) {
fieldType.setValue(element, value);
}
});
}
// Load form schema
loadSchema(schema) {
this.fields = [];
if (schema.title) {
this.addField({
type: 'html',
content: `<h2>${schema.title}</h2>`,
});
}
if (schema.description) {
this.addField({
type: 'html',
content: `<p class="form-description">${schema.description}</p>`,
});
}
if (schema.fields) {
schema.fields.forEach((field) => this.addField(field));
}
if (schema.sections) {
schema.sections.forEach((section) => {
this.addSection(section.title, section.fields);
});
}
}
// Export schema
exportSchema() {
return {
fields: this.fields.map((field) => {
const { id, ...fieldData } = field;
return fieldData;
}),
};
}
// Clear form
clear() {
this.fields = [];
this.container.innerHTML = '';
}
}
// Usage
const formContainer = document.getElementById('form-container');
const builder = new FormBuilder(formContainer, {
theme: 'modern',
validation: true,
});
// Load form schema
builder.loadSchema({
title: 'User Registration',
description: 'Please fill out all required fields',
sections: [
{
title: 'Personal Information',
fields: [
{
type: 'text',
name: 'firstName',
label: 'First Name',
required: true,
placeholder: 'Enter your first name',
},
{
type: 'text',
name: 'lastName',
label: 'Last Name',
required: true,
placeholder: 'Enter your last name',
},
{
type: 'date',
name: 'birthDate',
label: 'Date of Birth',
required: true,
max: new Date().toISOString().split('T')[0],
},
],
},
{
title: 'Account Details',
fields: [
{
type: 'text',
name: 'email',
label: 'Email Address',
required: true,
placeholder: 'email@example.com',
},
{
type: 'select',
name: 'country',
label: 'Country',
required: true,
placeholder: 'Select your country',
options: [
{ value: 'us', label: 'United States' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'ca', label: 'Canada' },
],
},
{
type: 'checkbox',
name: 'newsletter',
label: 'Subscribe to newsletter',
value: 'yes',
},
],
},
],
});
// Build the form
const form = builder.build();
// Handle form submission
form.addEventListener('submit', (e) => {
e.preventDefault();
const formData = builder.getFormData();
console.log('Form data:', formData);
// Convert to FormData for submission
const fd = new FormData();
Object.entries(formData).forEach(([key, value]) => {
if (value instanceof FileList) {
for (let i = 0; i < value.length; i++) {
fd.append(key, value[i]);
}
} else if (Array.isArray(value)) {
value.forEach((v) => fd.append(key, v));
} else {
fd.append(key, value);
}
});
// Submit form data
fetch('/api/register', {
method: 'POST',
body: fd,
});
});
FormData Utilities
FormData Helper Functions
class FormDataUtils {
// Convert FormData to JSON object
static toJSON(formData) {
const obj = {};
for (const [key, value] of formData.entries()) {
// Handle multiple values
if (key in obj) {
if (!Array.isArray(obj[key])) {
obj[key] = [obj[key]];
}
obj[key].push(value);
} else {
obj[key] = value;
}
}
return obj;
}
// Convert JSON object to FormData
static fromJSON(obj) {
const formData = new FormData();
Object.entries(obj).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((item) => formData.append(key, item));
} else if (value !== null && value !== undefined) {
formData.append(key, value);
}
});
return formData;
}
// Merge multiple FormData objects
static merge(...formDataObjects) {
const merged = new FormData();
formDataObjects.forEach((fd) => {
for (const [key, value] of fd.entries()) {
merged.append(key, value);
}
});
return merged;
}
// Filter FormData entries
static filter(formData, predicate) {
const filtered = new FormData();
for (const [key, value] of formData.entries()) {
if (predicate(key, value)) {
filtered.append(key, value);
}
}
return filtered;
}
// Map FormData entries
static map(formData, transformer) {
const mapped = new FormData();
for (const [key, value] of formData.entries()) {
const [newKey, newValue] = transformer(key, value);
if (newKey !== null && newValue !== null) {
mapped.append(newKey, newValue);
}
}
return mapped;
}
// Get all values for a key
static getAll(formData, key) {
const values = [];
for (const [k, v] of formData.entries()) {
if (k === key) {
values.push(v);
}
}
return values;
}
// Check if FormData is empty
static isEmpty(formData) {
for (const _ of formData.entries()) {
return false;
}
return true;
}
// Count entries
static count(formData) {
let count = 0;
for (const _ of formData.entries()) {
count++;
}
return count;
}
// Serialize FormData to URL encoded string
static toURLEncoded(formData) {
const params = new URLSearchParams();
for (const [key, value] of formData.entries()) {
params.append(key, value);
}
return params.toString();
}
// Create FormData from URL encoded string
static fromURLEncoded(urlEncoded) {
const params = new URLSearchParams(urlEncoded);
const formData = new FormData();
for (const [key, value] of params.entries()) {
formData.append(key, value);
}
return formData;
}
// Debug FormData (console log all entries)
static debug(formData, label = 'FormData') {
console.group(label);
for (const [key, value] of formData.entries()) {
if (value instanceof File) {
console.log(`${key}:`, {
name: value.name,
size: value.size,
type: value.type,
lastModified: new Date(value.lastModified),
});
} else {
console.log(`${key}:`, value);
}
}
console.groupEnd();
}
// Validate FormData against schema
static validate(formData, schema) {
const errors = [];
// Check required fields
if (schema.required) {
schema.required.forEach((field) => {
if (!formData.has(field)) {
errors.push({
field,
error: 'Required field missing',
});
}
});
}
// Validate field types
if (schema.fields) {
Object.entries(schema.fields).forEach(([field, rules]) => {
if (formData.has(field)) {
const value = formData.get(field);
// Type validation
if (rules.type === 'number' && isNaN(value)) {
errors.push({
field,
error: 'Must be a number',
});
}
// Pattern validation
if (rules.pattern && !new RegExp(rules.pattern).test(value)) {
errors.push({
field,
error: rules.message || 'Invalid format',
});
}
// File validation
if (value instanceof File) {
if (rules.maxSize && value.size > rules.maxSize) {
errors.push({
field,
error: `File too large (max: ${rules.maxSize} bytes)`,
});
}
if (rules.accept && !rules.accept.includes(value.type)) {
errors.push({
field,
error: 'Invalid file type',
});
}
}
}
});
}
return {
valid: errors.length === 0,
errors,
};
}
// Create multipart body with boundary
static toMultipart(formData, boundary = null) {
if (!boundary) {
boundary =
'----WebKitFormBoundary' + Math.random().toString(36).substr(2, 16);
}
let body = '';
for (const [key, value] of formData.entries()) {
body += `--${boundary}\r\n`;
if (value instanceof File) {
body += `Content-Disposition: form-data; name="${key}"; filename="${value.name}"\r\n`;
body += `Content-Type: ${value.type || 'application/octet-stream'}\r\n\r\n`;
// Note: In real implementation, you'd need to handle binary data properly
body += '[File content would go here]\r\n';
} else {
body += `Content-Disposition: form-data; name="${key}"\r\n\r\n`;
body += `${value}\r\n`;
}
}
body += `--${boundary}--\r\n`;
return {
boundary,
body,
contentType: `multipart/form-data; boundary=${boundary}`,
};
}
}
// Usage examples
const formData = new FormData();
formData.append('name', 'John Doe');
formData.append('email', 'john@example.com');
formData.append('hobbies', 'reading');
formData.append('hobbies', 'gaming');
// Convert to JSON
const json = FormDataUtils.toJSON(formData);
console.log('JSON:', json);
// Debug FormData
FormDataUtils.debug(formData, 'User Form Data');
// Validate FormData
const schema = {
required: ['name', 'email'],
fields: {
email: {
pattern: '^[^@]+@[^@]+\\.[^@]+$',
message: 'Invalid email format',
},
},
};
const validation = FormDataUtils.validate(formData, schema);
console.log('Validation:', validation);
// Filter FormData
const filtered = FormDataUtils.filter(formData, (key, value) => {
return key !== 'hobbies';
});
// Serialize to URL encoded
const urlEncoded = FormDataUtils.toURLEncoded(formData);
console.log('URL Encoded:', urlEncoded);
Best Practices
-
Always validate form data
const formData = new FormData(form); if (!formData.has('required_field')) { throw new Error('Required field missing'); }
-
Handle file uploads properly
const file = formData.get('file'); if (file && file.size > MAX_SIZE) { throw new Error('File too large'); }
-
Use appropriate content types
// FormData automatically sets multipart/form-data fetch('/upload', { method: 'POST', body: formData, // Don't set Content-Type header! });
-
Clean up file references
// Revoke object URLs after use URL.revokeObjectURL(objectUrl);
Conclusion
The FormData API is essential for modern form handling:
- Easy form serialization with automatic encoding
- File upload handling with progress tracking
- Dynamic form building capabilities
- Multipart data support
- AJAX form submission integration
- Cross-browser compatibility
Key takeaways:
- Use FormData for all form submissions
- Handle file uploads with proper validation
- Build dynamic forms programmatically
- Implement proper error handling
- Monitor upload progress for better UX
- Always validate data on both client and server
Master the FormData API to create robust, user-friendly form experiences!