Web APIs

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.

By JavaScriptDoc Team
formdataformsuploadsajaxjavascript

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

  1. Always validate form data

    const formData = new FormData(form);
    if (!formData.has('required_field')) {
      throw new Error('Required field missing');
    }
    
  2. Handle file uploads properly

    const file = formData.get('file');
    if (file && file.size > MAX_SIZE) {
      throw new Error('File too large');
    }
    
  3. Use appropriate content types

    // FormData automatically sets multipart/form-data
    fetch('/upload', {
      method: 'POST',
      body: formData,
      // Don't set Content-Type header!
    });
    
  4. 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!