Now.js Framework Documentation

Now.js Framework Documentation

FormError

EN 31 Oct 2025 01:30

FormError

Overview

FormError is a comprehensive error management system for forms in the Now.js Framework. It provides a complete set of tools for displaying, managing, and clearing validation errors and success messages in forms. The system supports both field-level and form-level error handling with automatic focus management, scrolling, accessibility features, and configurable auto-clear functionality.

Key Features

  • Field-level Error Display - Show validation errors next to specific form fields
  • General Message Display - Display form-level errors or success messages
  • Automatic Focus Management - Automatically focus on the first error field
  • Smart Scrolling - Scroll to error fields with configurable offset
  • Accessibility Support - ARIA attributes for screen readers (aria-invalid, aria-errormessage)
  • Auto-clear Messages - Automatically clear messages after a specified delay
  • Original Content Restoration - Preserve and restore original container content
  • Multiple Configuration Levels - Global config, form-specific config
  • Event System Integration - Emit events for all error operations
  • Flexible Container Detection - Auto-detect or specify error/success containers

Use Cases

  • Form Validation - Display inline validation errors for form fields
  • Server Response Errors - Show error messages from API responses
  • Success Notifications - Display success messages after form submission
  • Multi-step Forms - Manage errors across multiple form steps
  • Real-time Validation - Show/clear errors as user types or blurs fields
  • Accessibility Compliance - Ensure forms are accessible with proper ARIA attributes

Installation and Import

FormError is loaded with the Now.js Framework and is ready to use immediately via the window object:

// No import needed - ready to use immediately
console.log(window.FormError); // FormError object

Quick Start

Basic Field Error Display

<!-- Form with error containers -->
<form id="loginForm">
  <div class="form-control">
    <label for="email">Email</label>
    <input type="email" id="email" name="email">
    <div id="result_email" class="comment"></div>
  </div>

  <div class="form-control">
    <label for="password">Password</label>
    <input type="password" id="password" name="password">
    <div id="result_password" class="comment"></div>
  </div>

  <div id="form-message" class="form-message"></div>

  <button type="submit">Login</button>
</form>

<script>
  // Show field error
  FormError.showFieldError('email', 'Please enter a valid email address');

  // Show general error
  FormError.showGeneralError('Invalid login credentials', document.getElementById('loginForm'));

  // Clear specific field error
  FormError.clearFieldError('email');

  // Clear all errors
  FormError.clearAll();
</script>

Success Message Display

<script>
  // Show success message
  FormError.showSuccess('Login successful! Redirecting...', document.getElementById('loginForm'));

  // Auto-clear after 3 seconds
  setTimeout(() => {
    FormError.clearSuccess(document.getElementById('loginForm'));
  }, 3000);
</script>

Configuration

Global Configuration

Configure default behavior for all forms:

FormError.configure({
  // CSS class for invalid fields (default: 'invalid')
  errorClass: 'has-error',

  // CSS class for error messages (default: 'error')
  errorMessageClass: 'error-text',

  // Auto-focus first error field (default: true)
  autoFocus: true,

  // Auto-scroll to first error field (default: true)
  autoScroll: true,

  // Scroll offset in pixels (default: 100)
  scrollOffset: 120,

  // Enable debug mode (default: false)
  debug: true,

  // Show errors inline next to fields (default: true)
  showErrorsInline: true,

  // Show errors in notification system (default: false)
  showErrorsInNotification: false,

  // Auto-clear errors after delay (default: true)
  autoClearErrors: true,

  // Auto-clear delay in milliseconds (default: 5000)
  autoClearErrorsDelay: 3000,

  // Default error container ID (default: 'form-message')
  defaultErrorContainer: 'global-error',

  // Default success container ID (default: 'form-message')
  defaultSuccessContainer: 'global-success'
});

Form-specific Configuration

FormError automatically inherits configuration from FormManager instances:

<form
  data-form="registrationForm"
  data-error-container="registration-errors"
  data-success-container="registration-success"
  data-auto-focus="true"
  data-auto-scroll="false">
  <!-- form fields -->
</form>

<script>
  // FormError will use form-specific config when showing errors
  const form = document.querySelector('[data-form="registrationForm"]');

  // Uses form's error-container setting
  FormError.showGeneralError('Registration failed', form);
</script>

Configuration Priority

  1. Form-specific config (from FormManager instance)
  2. Global config (from FormError.config)
  3. Default config (hardcoded defaults)

Field Error Management

Show Field Error

Display validation error for a specific field:

// By field ID
FormError.showFieldError('email', 'Email is required');

// By field name
FormError.showFieldError('username', 'Username must be at least 6 characters');

// Multiple error messages (shows first)
FormError.showFieldError('password', [
  'Password is required',
  'Password must be at least 8 characters',
  'Password must contain a number'
]);

// With form context
const form = document.getElementById('myForm');
FormError.showFieldError('email', 'Invalid email', form);

// With custom options
FormError.showFieldError('email', 'Invalid email', form, {
  focus: false,  // Don't auto-focus
  scroll: false  // Don't auto-scroll
});

Error Container Structure

FormError expects this HTML structure for field errors:

<div class="form-control">
  <label for="fieldName">Field Label</label>
  <input type="text" id="fieldName" name="fieldName">

  <!-- Error message container (must have id="result_{fieldName}") -->
  <div id="result_fieldName" class="comment"></div>
</div>

What Happens When Showing Field Error

  1. Locate Field - Find element by ID or name attribute
  2. Store Original State - Save original message content if exists
  3. Add Error Classes - Add errorClass to field, parent .form-control, and field container
  4. Display Message - Show error message in result_{fieldName} container
  5. Set ARIA Attributes - Add aria-invalid="true" and aria-errormessage
  6. Focus Management - Focus first error field (if autoFocus enabled)
  7. Scroll to Field - Scroll to error field (if autoScroll enabled)
  8. Emit Event - Fire form:error event with error details

Clear Field Error

Remove validation error from a specific field:

// By field ID or name
FormError.clearFieldError('email');

// By field element
const emailField = document.getElementById('email');
FormError.clearFieldError(emailField);

What Happens When Clearing Field Error

  1. Remove Error Classes - Remove errorClass from field, parent, and container
  2. Restore Original Message - Restore original content of result_{fieldName} container
  3. Remove ARIA Attributes - Remove aria-invalid and aria-errormessage
  4. Clear State - Remove from internal errors map
  5. Emit Event - Fire form:clearError event

General Error Messages

Show General Error

Display form-level error message:

// With form element
const form = document.getElementById('loginForm');
FormError.showGeneralError('Invalid login credentials', form);

// With container ID
FormError.showGeneralError('Server error occurred', 'error-container');

// Without form (uses default container)
FormError.showGeneralError('An unexpected error occurred');

// With auto-clear
FormError.showGeneralError('Please fix the errors below', form);
// Auto-clears after 5 seconds (if autoClearErrors is enabled)

Container Detection Priority

FormError looks for error containers in this order:

  1. Form's data-error-container attribute

    <form data-error-container="my-errors">
  2. Element with [data-error-container] inside form

    <div data-error-container class="error-box"></div>
  3. Element with .form-message class

    <div class="form-message"></div>
  4. Element with .error-message class

    <div class="error-message"></div>
  5. Element with .login-message class

    <div class="login-message"></div>
  6. Default error container (from config)

    FormError.config.defaultErrorContainer // 'form-message'

Original Content Preservation

FormError preserves original container content:

<div id="form-message" class="form-message">
  <p>Welcome! Please log in to continue.</p>
</div>

<script>
  // Show error - original content saved
  FormError.showGeneralError('Login failed', document.getElementById('loginForm'));

  // Clear error - original content restored
  FormError.clearGeneralError('form-message');
  // Now shows "Welcome! Please log in to continue." again
</script>

Clear General Error

Remove form-level error message:

// By container ID
FormError.clearGeneralError('form-message');

// By form element
const form = document.getElementById('loginForm');
FormError.clearGeneralError(form);

// By container element
const container = document.getElementById('error-container');
FormError.clearGeneralError(container);

// Clear default container
FormError.clearGeneralError();

Success Messages

Show Success Message

Display success notification:

// With form element
const form = document.getElementById('loginForm');
FormError.showSuccess('Login successful!', form);

// With container ID
FormError.showSuccess('Profile updated successfully', 'success-container');

// Without form (uses default container)
FormError.showSuccess('Changes saved');

// With auto-clear
FormError.showSuccess('Registration complete', form);
// Auto-clears after 5 seconds (if autoClearErrors is enabled)

Success Container Detection

Similar to error containers, but looks for:

  1. Form's data-success-container attribute
  2. Element with [data-success-container] inside form
  3. Element with .form-message class
  4. Element with .success-message class
  5. Element with .login-message class
  6. Default success container (from config)

Clear Success Message

Remove success notification:

// By container ID
FormError.clearSuccess('success-container');

// By form element
const form = document.getElementById('loginForm');
FormError.clearSuccess(form);

// By container element
const container = document.getElementById('success-container');
FormError.clearSuccess(container);

// Clear default container
FormError.clearSuccess();

Bulk Operations

Show Multiple Errors

Display multiple field errors at once:

const errors = {
  email: 'Email is required',
  password: 'Password must be at least 8 characters',
  username: 'Username is already taken'
};

// Show all errors (clears existing errors first)
FormError.showErrors(errors);

// First error field gets focus and scroll
// Other errors are shown inline

// With custom options
FormError.showErrors(errors, {
  focus: false,  // Don't focus any field
  scroll: false  // Don't scroll
});

Clear All Messages

Remove all errors and success messages:

// Clear all errors in document
FormError.clearAll();

// Clear all errors in specific form
const form = document.getElementById('loginForm');
FormError.clearAll(form);

Clear Form-specific Messages

Clear all messages for a specific form:

// By form element
const form = document.getElementById('registrationForm');
FormError.clearFormMessages(form);

// By form ID
FormError.clearFormMessages('registrationForm');

// By form data-form attribute
FormError.clearFormMessages('myForm');

Focus and Scroll Management

Automatic Focus

First error field receives focus automatically:

FormError.showFieldError('email', 'Email is required');
FormError.showFieldError('password', 'Password is required');
// Email field gets focus (first error)

Disable Auto-focus

// Globally
FormError.configure({ autoFocus: false });

// Per operation
FormError.showFieldError('email', 'Error', null, { focus: false });

Automatic Scrolling

Smart scroll behavior:

// Scrolls to element only if not visible
FormError.scrollToElement(element);

// Element is considered visible if:
// - 30% of its height is visible in viewport
// - Either top or bottom is in viewport

Scroll Configuration

FormError.configure({
  autoScroll: true,      // Enable auto-scroll
  scrollOffset: 100      // Offset from top in pixels
});

Manual Scroll

const element = document.getElementById('email');
FormError.scrollToElement(element);

Accessibility Features

ARIA Attributes

FormError automatically manages ARIA attributes:

<!-- Before error -->
<input type="email" id="email" name="email">

<!-- After showing error -->
<input
  type="email"
  id="email"
  name="email"
  aria-invalid="true"
  aria-errormessage="result_email">
<div id="result_email" role="alert">Email is required</div>

Screen Reader Support

// Error messages are announced by screen readers
FormError.showFieldError('email', 'Email is required');

// aria-live regions for dynamic messages
// Error containers should have role="alert" for immediate announcement

Keyboard Navigation

// First error field receives focus for keyboard users
FormError.showErrors({
  email: 'Email is required',
  password: 'Password is required'
});
// Email field is focused, user can tab to password field

State Management

Check for Errors

// Check if any errors exist
if (FormError.hasErrors()) {
  console.log('Form has errors');
}

// Get number of errors
const count = FormError.getErrorsCount();
console.log(`${count} error(s) found`);

Get Error Details

// Get all current errors
const errors = FormError.getErrors();
// Returns: [
//   { field: 'email', element: <input>, messages: ['Email is required'] },
//   { field: 'password', element: <input>, messages: ['Password too short'] }
// ]

// Process errors
errors.forEach(error => {
  console.log(`Field: ${error.field}`);
  console.log(`Messages: ${error.messages.join(', ')}`);
  console.log(`Element:`, error.element);
});

Reset State

Clear all errors and reset internal state:

FormError.reset();
// Clears:
// - All field errors
// - All general messages
// - Original message cache
// - Last focused element
// - Internal state maps

Event System

FormError emits events for all operations:

Available Events

// Field error shown
Now.on('form:error', (data) => {
  console.log('Field error:', data.field, data.messages);
  console.log('Element:', data.element);
  console.log('Config:', data.config);
});

// Field error cleared
Now.on('form:clearError', (data) => {
  console.log('Error cleared:', data.field);
  console.log('Element:', data.element);
});

// Multiple errors shown
Now.on('form:errors', (data) => {
  console.log('Errors:', data.errors);
});

// General error shown
Now.on('form:generalError', (data) => {
  console.log('General error:', data.message);
  console.log('Container:', data.containerId);
});

// General error cleared
Now.on('form:clearGeneralError', (data) => {
  console.log('General error cleared:', data.containerId);
});

// Success message shown
Now.on('form:generalSuccess', (data) => {
  console.log('Success:', data.message);
  console.log('Container:', data.containerId);
});

// Success message cleared
Now.on('form:clearSuccess', (data) => {
  console.log('Success cleared:', data.containerId);
});

// All errors cleared
Now.on('form:clearAllErrors', (data) => {
  console.log('All errors cleared');
  if (data.form) {
    console.log('Form:', data.form);
  }
});

Event Usage Example

// Track errors for analytics
Now.on('form:error', (data) => {
  analytics.track('Form Error', {
    field: data.field,
    message: data.messages[0],
    formId: data.element.form?.id
  });
});

// Show custom notifications
Now.on('form:generalError', (data) => {
  if (data.config.showErrorsInNotification) {
    NotificationSystem.error(data.message);
  }
});

// Log error clearing
Now.on('form:clearAllErrors', () => {
  console.log('User cleared all form errors');
});

Integration with FormManager

FormError works seamlessly with FormManager:

Automatic Error Display

// FormManager automatically uses FormError for validation errors
const form = document.getElementById('loginForm');
const formManager = FormManager.init(form);

// When validation fails, FormError displays errors automatically
formManager.validate(); // Validation errors shown via FormError

Configuration Inheritance

<form
  data-form="myForm"
  data-error-class="has-error"
  data-error-message-class="error-text"
  data-auto-focus="true"
  data-auto-scroll="false"
  data-error-container="form-errors">

  <!-- FormError uses these settings -->
</form>

Manual Error Handling

// Show custom errors after form submission
Now.on('form:submit', async (data) => {
  try {
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(data.formData)
    });

    const result = await response.json();

    if (!result.success && result.errors) {
      // Show server validation errors
      FormError.showErrors(result.errors);
    }
  } catch (error) {
    FormError.showGeneralError('Server error occurred', data.form);
  }
});

Complete Examples

Example 1: Login Form with Error Handling

<!DOCTYPE html>
<html>
<head>
  <title>Login Form</title>
  <script src="/Now/Now.js"></script>
  <style>
    .form-control { margin-bottom: 1rem; }
    .form-control.invalid input { border-color: #dc3545; }
    .comment { min-height: 1.5rem; font-size: 0.875rem; }
    .comment.error { color: #dc3545; }
    .form-message { padding: 1rem; margin-bottom: 1rem; border-radius: 4px; }
    .form-message.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
    .form-message.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
  </style>
</head>
<body>
  <form id="loginForm" data-form="loginForm">
    <div id="form-message" class="form-message"></div>

    <div class="form-control">
      <label for="email">Email</label>
      <input type="email" id="email" name="email" required>
      <div id="result_email" class="comment"></div>
    </div>

    <div class="form-control">
      <label for="password">Password</label>
      <input type="password" id="password" name="password" required>
      <div id="result_password" class="comment"></div>
    </div>

    <button type="submit">Login</button>
  </form>

  <script>
    // Configure FormError
    FormError.configure({
      errorClass: 'invalid',
      autoFocus: true,
      autoScroll: true,
      autoClearErrors: false // Manual clearing for login forms
    });

    // Initialize form
    const form = document.getElementById('loginForm');

    // Handle form submission
    form.addEventListener('submit', async (e) => {
      e.preventDefault();

      // Clear previous errors
      FormError.clearAll(form);

      // Get form data
      const formData = new FormData(form);
      const data = Object.fromEntries(formData);

      // Client-side validation
      const errors = {};

      if (!data.email) {
        errors.email = 'Email is required';
      } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
        errors.email = 'Please enter a valid email address';
      }

      if (!data.password) {
        errors.password = 'Password is required';
      } else if (data.password.length < 8) {
        errors.password = 'Password must be at least 8 characters';
      }

      // Show validation errors
      if (Object.keys(errors).length > 0) {
        FormError.showErrors(errors);
        FormError.showGeneralError('Please fix the errors below', form);
        return;
      }

      // Submit to server
      try {
        const response = await fetch('/api/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data)
        });

        const result = await response.json();

        if (result.success) {
          FormError.showSuccess('Login successful! Redirecting...', form);
          setTimeout(() => {
            window.location.href = result.redirect || '/dashboard';
          }, 1500);
        } else {
          // Show server errors
          if (result.errors) {
            FormError.showErrors(result.errors);
          }
          FormError.showGeneralError(result.message || 'Login failed', form);
        }
      } catch (error) {
        FormError.showGeneralError('Network error. Please try again.', form);
      }
    });

    // Clear field error on input
    form.querySelectorAll('input').forEach(input => {
      input.addEventListener('input', () => {
        if (FormError.state.errors.has(input.name)) {
          FormError.clearFieldError(input.name);
        }
      });
    });
  </script>
</body>
</html>

Example 2: Registration Form with Multi-field Validation

<!DOCTYPE html>
<html>
<head>
  <title>Registration Form</title>
  <script src="/Now/Now.js"></script>
  <style>
    .form-control { margin-bottom: 1rem; }
    .form-control.invalid input { border-color: #dc3545; background-color: #fff5f5; }
    .comment { min-height: 1.5rem; font-size: 0.875rem; }
    .comment.error { color: #dc3545; }
    .form-message { padding: 1rem; margin-bottom: 1rem; border-radius: 4px; display: none; }
    .form-message.show { display: block; }
    .form-message.error { background: #f8d7da; color: #721c24; }
    .form-message.success { background: #d4edda; color: #155724; }
  </style>
</head>
<body>
  <form id="registrationForm" data-form="registrationForm">
    <div id="form-message" class="form-message"></div>

    <div class="form-control">
      <label for="username">Username</label>
      <input type="text" id="username" name="username" required>
      <div id="result_username" class="comment"></div>
    </div>

    <div class="form-control">
      <label for="email">Email</label>
      <input type="email" id="email" name="email" required>
      <div id="result_email" class="comment"></div>
    </div>

    <div class="form-control">
      <label for="password">Password</label>
      <input type="password" id="password" name="password" required>
      <div id="result_password" class="comment"></div>
    </div>

    <div class="form-control">
      <label for="confirmPassword">Confirm Password</label>
      <input type="password" id="confirmPassword" name="confirmPassword" required>
      <div id="result_confirmPassword" class="comment"></div>
    </div>

    <div class="form-control">
      <label>
        <input type="checkbox" id="terms" name="terms" required>
        I agree to the terms and conditions
      </label>
      <div id="result_terms" class="comment"></div>
    </div>

    <button type="submit">Register</button>
  </form>

  <script>
    // Configure FormError
    FormError.configure({
      errorClass: 'invalid',
      errorMessageClass: 'error',
      autoFocus: true,
      autoScroll: true,
      scrollOffset: 80,
      autoClearErrors: false
    });

    const form = document.getElementById('registrationForm');

    // Validation rules
    const validationRules = {
      username: {
        validate: (value) => {
          if (!value) return 'Username is required';
          if (value.length < 4) return 'Username must be at least 4 characters';
          if (!/^[a-zA-Z0-9_]+$/.test(value)) return 'Username can only contain letters, numbers, and underscores';
          return null;
        }
      },
      email: {
        validate: (value) => {
          if (!value) return 'Email is required';
          if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Please enter a valid email address';
          return null;
        }
      },
      password: {
        validate: (value) => {
          if (!value) return 'Password is required';
          if (value.length < 8) return 'Password must be at least 8 characters';
          if (!/[A-Z]/.test(value)) return 'Password must contain at least one uppercase letter';
          if (!/[a-z]/.test(value)) return 'Password must contain at least one lowercase letter';
          if (!/[0-9]/.test(value)) return 'Password must contain at least one number';
          return null;
        }
      },
      confirmPassword: {
        validate: (value, formData) => {
          if (!value) return 'Please confirm your password';
          if (value !== formData.password) return 'Passwords do not match';
          return null;
        }
      },
      terms: {
        validate: (value) => {
          if (!value) return 'You must agree to the terms and conditions';
          return null;
        }
      }
    };

    // Real-time validation on blur
    form.querySelectorAll('input').forEach(input => {
      input.addEventListener('blur', () => {
        validateField(input.name);
      });

      // Clear error on input
      input.addEventListener('input', () => {
        if (FormError.state.errors.has(input.name)) {
          FormError.clearFieldError(input.name);
        }
      });
    });

    // Validate single field
    function validateField(fieldName) {
      const rule = validationRules[fieldName];
      if (!rule) return true;

      const formData = new FormData(form);
      const data = Object.fromEntries(formData);
      const value = data[fieldName];

      const error = rule.validate(value, data);

      if (error) {
        FormError.showFieldError(fieldName, error, form, {
          focus: false,
          scroll: false
        });
        return false;
      } else {
        FormError.clearFieldError(fieldName);
        return true;
      }
    }

    // Validate all fields
    function validateForm() {
      const formData = new FormData(form);
      const data = Object.fromEntries(formData);
      const errors = {};

      Object.keys(validationRules).forEach(fieldName => {
        const rule = validationRules[fieldName];
        const value = data[fieldName];
        const error = rule.validate(value, data);

        if (error) {
          errors[fieldName] = error;
        }
      });

      return errors;
    }

    // Handle form submission
    form.addEventListener('submit', async (e) => {
      e.preventDefault();

      // Clear previous errors
      FormError.clearAll(form);

      // Validate all fields
      const errors = validateForm();

      if (Object.keys(errors).length > 0) {
        FormError.showErrors(errors);
        FormError.showGeneralError(`Please fix ${Object.keys(errors).length} error(s) below`, form);
        return;
      }

      // Submit to server
      const formData = new FormData(form);
      const data = Object.fromEntries(formData);

      try {
        const response = await fetch('/api/register', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data)
        });

        const result = await response.json();

        if (result.success) {
          FormError.showSuccess('Registration successful! Please check your email.', form);
          form.reset();

          setTimeout(() => {
            window.location.href = '/login';
          }, 2000);
        } else {
          // Show server validation errors
          if (result.errors) {
            FormError.showErrors(result.errors);
          }
          FormError.showGeneralError(result.message || 'Registration failed', form);
        }
      } catch (error) {
        FormError.showGeneralError('Network error. Please try again.', form);
      }
    });
  </script>
</body>
</html>

Example 3: Contact Form with Auto-clear Messages

<!DOCTYPE html>
<html>
<head>
  <title>Contact Form</title>
  <script src="/Now/Now.js"></script>
  <style>
    .form-control { margin-bottom: 1rem; }
    .form-control.invalid input,
    .form-control.invalid textarea { border-color: #dc3545; }
    .comment { min-height: 1.5rem; font-size: 0.875rem; }
    .comment.error { color: #dc3545; }
    #form-message {
      padding: 1rem;
      margin-bottom: 1rem;
      border-radius: 4px;
      display: none;
      animation: slideDown 0.3s ease-out;
    }
    #form-message.show { display: block; }
    #form-message.error { background: #f8d7da; color: #721c24; }
    #form-message.success { background: #d4edda; color: #155724; }

    @keyframes slideDown {
      from { opacity: 0; transform: translateY(-10px); }
      to { opacity: 1; transform: translateY(0); }
    }
  </style>
</head>
<body>
  <form id="contactForm" data-form="contactForm">
    <div id="form-message" class="form-message"></div>

    <div class="form-control">
      <label for="name">Name</label>
      <input type="text" id="name" name="name" required>
      <div id="result_name" class="comment"></div>
    </div>

    <div class="form-control">
      <label for="email">Email</label>
      <input type="email" id="email" name="email" required>
      <div id="result_email" class="comment"></div>
    </div>

    <div class="form-control">
      <label for="subject">Subject</label>
      <input type="text" id="subject" name="subject" required>
      <div id="result_subject" class="comment"></div>
    </div>

    <div class="form-control">
      <label for="message">Message</label>
      <textarea id="message" name="message" rows="5" required></textarea>
      <div id="result_message" class="comment"></div>
    </div>

    <button type="submit">Send Message</button>
  </form>

  <script>
    // Configure with auto-clear
    FormError.configure({
      errorClass: 'invalid',
      errorMessageClass: 'error',
      autoFocus: true,
      autoScroll: true,
      autoClearErrors: true,
      autoClearErrorsDelay: 4000 // Clear after 4 seconds
    });

    const form = document.getElementById('contactForm');

    // Add 'show' class when message is displayed
    Now.on('form:generalError', (data) => {
      document.getElementById(data.containerId).classList.add('show');
    });

    Now.on('form:generalSuccess', (data) => {
      document.getElementById(data.containerId).classList.add('show');
    });

    // Handle form submission
    form.addEventListener('submit', async (e) => {
      e.preventDefault();

      // Clear previous messages
      FormError.clearAll(form);

      // Get form data
      const formData = new FormData(form);
      const data = Object.fromEntries(formData);

      // Simple validation
      const errors = {};
      if (!data.name) errors.name = 'Name is required';
      if (!data.email) errors.email = 'Email is required';
      if (!data.subject) errors.subject = 'Subject is required';
      if (!data.message) errors.message = 'Message is required';

      if (Object.keys(errors).length > 0) {
        FormError.showErrors(errors);
        FormError.showGeneralError('Please fill in all required fields', form);
        return;
      }

      // Simulate API call
      try {
        // Show loading state
        const submitBtn = form.querySelector('button[type="submit"]');
        submitBtn.disabled = true;
        submitBtn.textContent = 'Sending...';

        await new Promise(resolve => setTimeout(resolve, 1500));

        // Simulate success
        FormError.showSuccess('Thank you! Your message has been sent successfully.', form);
        form.reset();

        // Messages auto-clear after 4 seconds (autoClearErrorsDelay)

        // Reset button
        submitBtn.disabled = false;
        submitBtn.textContent = 'Send Message';

      } catch (error) {
        FormError.showGeneralError('Failed to send message. Please try again.', form);

        // Reset button
        const submitBtn = form.querySelector('button[type="submit"]');
        submitBtn.disabled = false;
        submitBtn.textContent = 'Send Message';
      }
    });

    // Clear field errors on input
    form.querySelectorAll('input, textarea').forEach(field => {
      field.addEventListener('input', () => {
        if (FormError.state.errors.has(field.name)) {
          FormError.clearFieldError(field.name);
        }
      });
    });
  </script>
</body>
</html>

Example 4: Multi-step Form with Error Persistence

<!DOCTYPE html>
<html>
<head>
  <title>Multi-step Form</title>
  <script src="/Now/Now.js"></script>
  <style>
    .form-step { display: none; }
    .form-step.active { display: block; }
    .form-control { margin-bottom: 1rem; }
    .form-control.invalid input { border-color: #dc3545; }
    .comment { min-height: 1.5rem; font-size: 0.875rem; }
    .comment.error { color: #dc3545; }
    .form-message { padding: 1rem; margin-bottom: 1rem; border-radius: 4px; }
    .form-message.error { background: #f8d7da; color: #721c24; }
    .form-message.success { background: #d4edda; color: #155724; }
    .form-nav { margin-top: 1rem; display: flex; gap: 1rem; }
    .step-indicator { display: flex; gap: 1rem; margin-bottom: 1rem; }
    .step-indicator span {
      padding: 0.5rem 1rem;
      background: #e9ecef;
      border-radius: 4px;
    }
    .step-indicator span.active { background: #007bff; color: white; }
    .step-indicator span.completed { background: #28a745; color: white; }
  </style>
</head>
<body>
  <div class="step-indicator">
    <span class="active" data-step="1">Step 1: Personal Info</span>
    <span data-step="2">Step 2: Address</span>
    <span data-step="3">Step 3: Confirmation</span>
  </div>

  <form id="multiStepForm" data-form="multiStepForm">
    <div id="form-message" class="form-message"></div>

    <!-- Step 1: Personal Info -->
    <div class="form-step active" data-step="1">
      <h3>Personal Information</h3>

      <div class="form-control">
        <label for="firstName">First Name</label>
        <input type="text" id="firstName" name="firstName" required>
        <div id="result_firstName" class="comment"></div>
      </div>

      <div class="form-control">
        <label for="lastName">Last Name</label>
        <input type="text" id="lastName" name="lastName" required>
        <div id="result_lastName" class="comment"></div>
      </div>

      <div class="form-control">
        <label for="email">Email</label>
        <input type="email" id="email" name="email" required>
        <div id="result_email" class="comment"></div>
      </div>
    </div>

    <!-- Step 2: Address -->
    <div class="form-step" data-step="2">
      <h3>Address Information</h3>

      <div class="form-control">
        <label for="street">Street Address</label>
        <input type="text" id="street" name="street" required>
        <div id="result_street" class="comment"></div>
      </div>

      <div class="form-control">
        <label for="city">City</label>
        <input type="text" id="city" name="city" required>
        <div id="result_city" class="comment"></div>
      </div>

      <div class="form-control">
        <label for="zipcode">Zip Code</label>
        <input type="text" id="zipcode" name="zipcode" required>
        <div id="result_zipcode" class="comment"></div>
      </div>
    </div>

    <!-- Step 3: Confirmation -->
    <div class="form-step" data-step="3">
      <h3>Confirmation</h3>
      <div id="confirmationData"></div>
    </div>

    <div class="form-nav">
      <button type="button" id="prevBtn" style="display: none;">Previous</button>
      <button type="button" id="nextBtn">Next</button>
      <button type="submit" id="submitBtn" style="display: none;">Submit</button>
    </div>
  </form>

  <script>
    FormError.configure({
      errorClass: 'invalid',
      autoFocus: true,
      autoScroll: false, // Disable for multi-step
      autoClearErrors: false
    });

    const form = document.getElementById('multiStepForm');
    let currentStep = 1;
    const totalSteps = 3;
    const stepErrors = {}; // Track errors per step

    // Step fields configuration
    const stepFields = {
      1: ['firstName', 'lastName', 'email'],
      2: ['street', 'city', 'zipcode']
    };

    // Update UI
    function updateStepUI() {
      // Update step visibility
      document.querySelectorAll('.form-step').forEach(step => {
        step.classList.remove('active');
      });
      document.querySelector(`.form-step[data-step="${currentStep}"]`).classList.add('active');

      // Update step indicator
      document.querySelectorAll('.step-indicator span').forEach((span, index) => {
        const stepNum = index + 1;
        span.classList.remove('active', 'completed');
        if (stepNum < currentStep) {
          span.classList.add('completed');
        } else if (stepNum === currentStep) {
          span.classList.add('active');
        }
      });

      // Update navigation buttons
      document.getElementById('prevBtn').style.display = currentStep > 1 ? 'block' : 'none';
      document.getElementById('nextBtn').style.display = currentStep < totalSteps ? 'block' : 'none';
      document.getElementById('submitBtn').style.display = currentStep === totalSteps ? 'block' : 'none';

      // Show confirmation data on last step
      if (currentStep === 3) {
        showConfirmation();
      }

      // Clear form-level messages when changing steps
      FormError.clearGeneralError(form);
    }

    // Validate current step
    function validateCurrentStep() {
      const fields = stepFields[currentStep];
      if (!fields) return true; // No validation for confirmation step

      const formData = new FormData(form);
      const data = Object.fromEntries(formData);
      const errors = {};

      fields.forEach(field => {
        const value = data[field];
        if (!value) {
          errors[field] = `${field} is required`;
        }
      });

      // Additional validation
      if (currentStep === 1 && data.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
        errors.email = 'Please enter a valid email address';
      }

      if (currentStep === 2 && data.zipcode && !/^\d{5}$/.test(data.zipcode)) {
        errors.zipcode = 'Zip code must be 5 digits';
      }

      // Clear previous step errors
      FormError.clearFormMessages(form);

      if (Object.keys(errors).length > 0) {
        stepErrors[currentStep] = errors;
        FormError.showErrors(errors);
        FormError.showGeneralError('Please fix the errors before continuing', form);
        return false;
      } else {
        delete stepErrors[currentStep];
        return true;
      }
    }

    // Show confirmation
    function showConfirmation() {
      const formData = new FormData(form);
      const data = Object.fromEntries(formData);

      const html = `
        <h4>Personal Information</h4>
        <p><strong>Name:</strong> ${data.firstName} ${data.lastName}</p>
        <p><strong>Email:</strong> ${data.email}</p>

        <h4>Address</h4>
        <p><strong>Street:</strong> ${data.street}</p>
        <p><strong>City:</strong> ${data.city}</p>
        <p><strong>Zip Code:</strong> ${data.zipcode}</p>
      `;

      document.getElementById('confirmationData').innerHTML = html;
    }

    // Next button
    document.getElementById('nextBtn').addEventListener('click', () => {
      if (validateCurrentStep()) {
        currentStep++;
        updateStepUI();
      }
    });

    // Previous button
    document.getElementById('prevBtn').addEventListener('click', () => {
      currentStep--;
      updateStepUI();

      // Show previous step errors if any
      if (stepErrors[currentStep]) {
        FormError.showErrors(stepErrors[currentStep]);
      }
    });

    // Form submission
    form.addEventListener('submit', async (e) => {
      e.preventDefault();

      FormError.clearAll(form);

      const formData = new FormData(form);
      const data = Object.fromEntries(formData);

      try {
        const submitBtn = document.getElementById('submitBtn');
        submitBtn.disabled = true;
        submitBtn.textContent = 'Submitting...';

        // Simulate API call
        await new Promise(resolve => setTimeout(resolve, 1500));

        FormError.showSuccess('Registration completed successfully!', form);

        setTimeout(() => {
          window.location.href = '/dashboard';
        }, 2000);

      } catch (error) {
        FormError.showGeneralError('Submission failed. Please try again.', form);
        document.getElementById('submitBtn').disabled = false;
        document.getElementById('submitBtn').textContent = 'Submit';
      }
    });

    // Clear field errors on input
    form.querySelectorAll('input').forEach(input => {
      input.addEventListener('input', () => {
        if (FormError.state.errors.has(input.name)) {
          FormError.clearFieldError(input.name);
        }
      });
    });

    // Initialize
    updateStepUI();
  </script>
</body>
</html>

API Reference

Configuration

FormError.configure(options: {
  errorClass?: string;                  // CSS class for invalid fields (default: 'invalid')
  errorMessageClass?: string;           // CSS class for error messages (default: 'error')
  autoFocus?: boolean;                  // Auto-focus first error field (default: true)
  autoScroll?: boolean;                 // Auto-scroll to first error (default: true)
  scrollOffset?: number;                // Scroll offset in pixels (default: 100)
  debug?: boolean;                      // Enable debug logging (default: false)
  showErrorsInline?: boolean;           // Show inline field errors (default: true)
  showErrorsInNotification?: boolean;   // Show in notification system (default: false)
  autoClearErrors?: boolean;            // Auto-clear messages (default: true)
  autoClearErrorsDelay?: number;        // Auto-clear delay in ms (default: 5000)
  defaultErrorContainer?: string;       // Default error container ID (default: 'form-message')
  defaultSuccessContainer?: string;     // Default success container ID (default: 'form-message')
}): void

Field Errors

// Show field error
FormError.showFieldError(
  field: string,                        // Field ID or name
  message: string | string[],           // Error message(s)
  form?: HTMLElement | null,            // Form element (optional)
  options?: {
    focus?: boolean;                    // Override autoFocus
    scroll?: boolean;                   // Override autoScroll
  }
): void

// Clear field error
FormError.clearFieldError(
  field: string | HTMLElement           // Field ID, name, or element
): void

// Show multiple field errors
FormError.showErrors(
  errors: { [field: string]: string | string[] },
  options?: {
    focus?: boolean;
    scroll?: boolean;
  }
): void

General Messages

// Show general error
FormError.showGeneralError(
  message: string,                      // Error message
  form?: HTMLElement | string | null,   // Form element, container ID, or null
  options?: {}                          // Reserved for future use
): void

// Show success message
FormError.showSuccess(
  message: string,                      // Success message
  form?: HTMLElement | string | null,   // Form element, container ID, or null
  options?: {}                          // Reserved for future use
): void

// Clear general error
FormError.clearGeneralError(
  containerOrForm?: string | HTMLElement | null
): boolean

// Clear success message
FormError.clearSuccess(
  containerOrForm?: string | HTMLElement | null
): boolean

Bulk Operations

// Clear all messages
FormError.clearAll(
  form?: HTMLElement | null             // Specific form or all forms
): void

// Clear form-specific messages
FormError.clearFormMessages(
  form: HTMLElement | string            // Form element or ID
): boolean

Utilities

// Scroll to element
FormError.scrollToElement(
  element: HTMLElement                  // Element to scroll to
): void

// Check for errors
FormError.hasErrors(): boolean

// Get all errors
FormError.getErrors(): Array<{
  field: string;
  element: HTMLElement;
  messages: string[];
}>

// Get error count
FormError.getErrorsCount(): number

// Reset all state
FormError.reset(): void

// Get form-specific config
FormError.getFormConfig(
  form: HTMLElement                     // Form element
): ConfigObject

State Properties

FormError.state = {
  errors: Map<string, {                 // Current errors
    element: HTMLElement;
    messages: string[];
  }>,
  lastFocusedElement: HTMLElement | null, // Last auto-focused element
  originalMessages: Map<string, string>,  // Original field messages
  originalGeneralMessages: Map<string, {  // Original container content
    html: string;
    className: string;
  }>
}

FormError.config = {
  // Current configuration (see Configuration section)
}

Best Practices

1. Use Proper HTML Structure

<!-- Correct: Proper structure for error display -->
<div class="form-control">
  <label for="email">Email</label>
  <input type="email" id="email" name="email">
  <div id="result_email" class="comment"></div>
</div>

<!-- Incorrect: Missing error container -->
<div class="form-control">
  <label for="email">Email</label>
  <input type="email" id="email" name="email">
  <!-- No error container -->
</div>

2. Clear Errors Appropriately

// Good: Clear field error when user starts typing
input.addEventListener('input', () => {
  FormError.clearFieldError(input.name);
});

// Good: Clear all errors before new validation
FormError.clearAll(form);
const errors = validateForm();
if (errors) {
  FormError.showErrors(errors);
}

// Bad: Not clearing old errors before showing new ones
FormError.showFieldError('email', 'Error 1');
FormError.showFieldError('email', 'Error 2'); // Overwrites, but should clear first

3. Use Events for Integration

// Good: Listen to FormError events for custom behavior
Now.on('form:error', (data) => {
  // Track validation errors in analytics
  analytics.track('Validation Error', {
    field: data.field,
    message: data.messages[0]
  });
});

// Good: Custom notification integration
Now.on('form:generalError', (data) => {
  if (data.config.showErrorsInNotification) {
    showToast(data.message, 'error');
  }
});

4. Configure Per Use Case

// Login form: Don't auto-clear errors
FormError.configure({
  autoClearErrors: false
});

// Contact form: Auto-clear after delay
FormError.configure({
  autoClearErrors: true,
  autoClearErrorsDelay: 4000
});

// Registration form: Disable auto-scroll for long forms
FormError.configure({
  autoScroll: false
});

5. Preserve User Experience

// Good: Show first error and focus
FormError.showErrors(errors); // First field gets focus

// Good: Don't steal focus on blur validation
FormError.showFieldError('email', 'Invalid', form, {
  focus: false,
  scroll: false
});

// Bad: Stealing focus during typing
input.addEventListener('input', () => {
  FormError.showFieldError(input.name, 'Invalid', form); // Steals focus
});

6. Handle Server Errors Gracefully

// Good: Map server errors to fields
try {
  const response = await fetch('/api/submit', {
    method: 'POST',
    body: JSON.stringify(formData)
  });

  const result = await response.json();

  if (!result.success) {
    // Show field errors
    if (result.errors) {
      FormError.showErrors(result.errors);
    }

    // Show general error
    FormError.showGeneralError(result.message || 'Submission failed', form);
  }
} catch (error) {
  // Network error
  FormError.showGeneralError('Network error. Please try again.', form);
}

7. Accessibility Compliance

// Good: FormError automatically adds ARIA attributes
FormError.showFieldError('email', 'Invalid email');
// Result: aria-invalid="true" and aria-errormessage="result_email"

// Good: Use semantic HTML
<div id="result_email" role="alert">Email is required</div>

// Good: Ensure keyboard navigation
FormError.configure({
  autoFocus: true // First error field receives focus
});

8. Multi-step Form Error Management

// Good: Track errors per step
const stepErrors = {};

function validateStep(stepNumber) {
  const errors = validateStepFields(stepNumber);

  if (Object.keys(errors).length > 0) {
    stepErrors[stepNumber] = errors;
    FormError.showErrors(errors);
    return false;
  } else {
    delete stepErrors[stepNumber];
    return true;
  }
}

// Good: Show previous errors when going back
function goToPreviousStep() {
  currentStep--;
  if (stepErrors[currentStep]) {
    FormError.showErrors(stepErrors[currentStep]);
  }
}

Common Pitfalls

1. Missing Error Containers

Problem:

<!-- No result_ container -->
<input type="email" id="email" name="email">

Solution:

<input type="email" id="email" name="email">
<div id="result_email" class="comment"></div>

2. Incorrect Field Identifiers

Problem:

// Field has id="userEmail" but trying to show error for "email"
FormError.showFieldError('email', 'Invalid email'); // Won't work

Solution:

// Use correct field ID or name
FormError.showFieldError('userEmail', 'Invalid email');

3. Not Clearing Errors Before Validation

Problem:

// Old errors still visible
FormError.showErrors(newErrors); // Mixed with old errors

Solution:

// Always clear before showing new errors
FormError.clearAll(form);
FormError.showErrors(newErrors);

4. Focus Stealing During Input

Problem:

// Steals focus while user is typing
input.addEventListener('input', () => {
  FormError.showFieldError(input.name, 'Error', form); // autoFocus: true (default)
});

Solution:

// Disable focus for real-time validation
input.addEventListener('input', () => {
  FormError.showFieldError(input.name, 'Error', form, {
    focus: false,
    scroll: false
  });
});

5. Not Handling Original Content

Problem:

// Manually clearing containers loses original content
container.textContent = '';

Solution:

// Use FormError methods to preserve/restore content
FormError.clearGeneralError(container);
// Original content is restored automatically

6. Forgetting Form Context

Problem:

// Form-specific config not used
FormError.showGeneralError('Error'); // Uses default container

Solution:

// Pass form element to use form-specific config
const form = document.getElementById('myForm');
FormError.showGeneralError('Error', form); // Uses form's error-container

7. Not Handling Auto-clear Timing

Problem:

// Form resets before auto-clear fires
FormError.showSuccess('Saved!', form); // auto-clears after 5s
setTimeout(() => {
  window.location.href = '/dashboard'; // Navigates after 1s
}, 1000); // User never sees full 5 seconds

Solution:

// Coordinate timing or disable auto-clear
FormError.configure({ autoClearErrors: false });
FormError.showSuccess('Saved! Redirecting...', form);
setTimeout(() => {
  window.location.href = '/dashboard';
}, 2000); // Give user time to read message

8. Incorrect Error Format from Server

Problem:

// Server returns: { error: "Login failed" }
// But FormError expects: { fieldName: "message" }

Solution:

const result = await response.json();

if (!result.success) {
  // Handle general error
  if (result.error) {
    FormError.showGeneralError(result.error, form);
  }

  // Handle field errors
  if (result.errors) {
    FormError.showErrors(result.errors);
  }
}

Browser Compatibility

  • Modern Browsers: Full support (Chrome, Firefox, Safari, Edge)
  • IE11: Requires polyfills for Map, Object.entries
  • Mobile: Full support on iOS Safari, Chrome Android

Migration Guide

From Manual Error Handling

Before:

// Manual error display
document.getElementById('email').classList.add('invalid');
document.getElementById('result_email').textContent = 'Email is required';
document.getElementById('email').focus();

After:

// Using FormError
FormError.showFieldError('email', 'Email is required');
// Automatically: adds classes, shows message, focuses, scrolls

From Custom Error Manager

Before:

myErrorManager.showError({
  field: 'email',
  message: 'Invalid',
  focus: true
});

After:

FormError.showFieldError('email', 'Invalid');
// Same behavior with FormError API