Now.js Framework Documentation

Now.js Framework Documentation

FormManager - Form Handling & Validation

EN 25 Nov 2025 11:17

FormManager - Form Handling & Validation

Documentation for FormManager, the form management system for the Now.js Framework.

📋 Table of Contents

  1. Overview
  2. Installation & Import
  3. Getting Started
  4. Form Configuration
  5. Form Validation
  6. Form Submission
  7. AJAX Submission
  8. File Upload
  9. Form Data
  10. Field Persistence
  11. Security Integration
  12. Usage Examples
  13. API Reference
  14. Best Practices
  15. Common Pitfalls

Overview

FormManager is a comprehensive form management system that supports validation, AJAX submission, file uploads, and security features.

Key Features

  • Auto Initialization: Automatically find and initialize forms
  • Form Validation: Field and form validation
  • AJAX Submission: Submit forms via AJAX
  • File Upload: Support file uploads with progress bar
  • Field Persistence: Persist field values (localStorage/cookie)
  • CSRF Protection: Defend against CSRF attacks
  • Rate Limiting: Limit submission frequency
  • Double Submit Prevention: Prevent duplicate submissions
  • Custom Validators: Register custom validators
  • Error Handling: Robust error handling
  • Success/Error Messages: Show success or error messages
  • Redirect Handling: Manage redirects after submission
  • Remember Me: Remember usernames (login forms)
  • Intended URL: Store intended URL before login

When to Use FormManager

✅ Use FormManager when:

  • You want automatic form handling
  • You need validation and AJAX submission
  • You need file upload with progress
  • You need security features (CSRF, rate limiting)
  • You need field persistence

❌ Do not use when:

  • The form is extremely simple and doesn't need validation
  • You prefer traditional (non-AJAX) submission

Installation and Import

FormManager 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.FormManager); // FormManager object

Getting Started

Basic Setup

// Initialize FormManager
await FormManager.init({
  debug: false,
  ajaxSubmit: true,
  autoValidate: true,
  resetAfterSubmit: false,
  preventDoubleSubmit: true,
  showLoadingOnSubmit: true,
  validateOnInput: true,
  validateOnBlur: true
});

console.log('FormManager initialized!');

Simple Form

<!-- Basic form with data-form attribute (required for auto-init) -->
<form data-form="contact" method="POST" action="/api/contact">
  <div>
    <label>Name:</label>
    <input type="text" name="name" required>
  </div>

  <div>
    <label>Email:</label>
    <input type="email" name="email" required>
  </div>

  <div>
    <label>Message:</label>
    <textarea name="message" required minlength="10"></textarea>
  </div>

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

Form Flow

User fills form
      ↓
User clicks submit
      ↓
┌────────────────────┐
│  Prevent Default   │
│  - Stop submission │
└─────────┬──────────┘
          ↓
┌────────────────────┐
│  Security Checks   │
│  - Rate limiting   │
│  - CSRF token      │
└─────────┬──────────┘
          ↓
┌────────────────────┐
│  Validate Form     │
│  - Required fields │
│  - Format checks   │
│  - Custom rules    │
└─────────┬──────────┘
          ↓
    Valid? ─── No ──> Show errors
      │
     Yes
      ↓
┌────────────────────┐
│  Collect Data      │
│  - FormData        │
│  - JSON object     │
│  - Files           │
└─────────┬──────────┘
          ↓
┌────────────────────┐
│  Submit (AJAX)     │
│  - Show loading    │
│  - Send request    │
│  - Handle response │
└─────────┬──────────┘
          ↓
┌────────────────────┐
│  Process Response  │
│  - Success message │
│  - Error handling  │
│  - Redirect        │
└────────────────────┘

Form Configuration

1. Data Attributes

<form
  data-form="login"
  data-ajax-submit="true"
  data-auto-validate="true"
  data-confirm="Are you sure you want to submit?"
  data-reset-after-submit="false"
  data-prevent-double-submit="true"
  data-show-loading-on-submit="true"
  data-redirect="/dashboard"
  data-success-message="Login successful!"
  data-error-message="Login failed">

  <!-- Form fields -->
</form>

Note: data-confirm shows a confirmation dialog before submitting

2. Security Configuration

<form
  data-form="secure"
  data-csrf="true"
  data-csrf-token="..."
  data-rate-limit="true"
  data-rate-limit-limit="5"
  data-rate-limit-window="60"
  data-validation="true"
  data-sanitize-input="true">

  <!-- Form fields -->
</form>

3. Validation Configuration

<form
  data-form="validated"
  data-validate-on-submit="true"
  data-validate-on-input="true"
  data-validate-on-blur="true"
  data-validate-only-dirty="true"
  data-auto-focus-error="true"
  data-auto-scroll-to-error="true">

  <!-- Form fields -->
</form>

4. Error Display Configuration

<form
  data-form="errors"
  data-show-errors-inline="true"
  data-show-errors-in-notification="true"
  data-error-container=".error-container"
  data-error-class="error"
  data-auto-clear-errors="true"
  data-auto-clear-errors-delay="5000">

  <!-- Form fields -->
</form>

5. Success Configuration

<form
  data-form="success"
  data-success-redirect="/thank-you"
  data-success-message="Form submitted successfully!"
  data-show-success-inline="true"
  data-show-success-in-notification="true"
  data-success-container=".success-container">

  <!-- Form fields -->
</form>

Form Validation

1. Built-in Validators

<!-- Required -->
<input
  type="text"
  name="name"
  required
  data-error-required="Please enter your name">

<!-- Email -->
<input
  type="email"
  name="email"
  required
  data-error-email="Please enter a valid email">

<!-- URL -->
<input
  type="url"
  name="website"
  data-error-url="Please enter a valid URL">

<!-- Number -->
<input
  type="number"
  name="age"
  data-error-number="Please enter a valid number">

<!-- Min/Max -->
<input
  type="number"
  name="age"
  min="18"
  max="100"
  data-error-min="Age must be at least 18"
  data-error-max="Age must be no more than 100">

<!-- Min/Max Length -->
<input
  type="text"
  name="username"
  minlength="3"
  maxlength="20"
  data-error-minlength="Username must be at least 3 characters"
  data-error-maxlength="Username must be no more than 20 characters">

<!-- Pattern -->
<input
  type="text"
  name="phone"
  pattern="^[0-9]{10}$"
  data-error-pattern="Please enter a 10-digit phone number">

<!-- Match (confirm password) -->
<input
  type="password"
  name="password"
  id="password">

<input
  type="password"
  name="confirm_password"
  data-validate-match="password"
  data-error-match="Passwords do not match">

2. Custom Validators

// Register custom validator
FormManager.registerValidator('username', (value, element) => {
  // Only alphanumeric and underscore
  return /^[a-zA-Z0-9_]+$/.test(value);
}, 'Username can only contain letters, numbers, and underscores');

HTML

<input
  type="text"
  name="username"
  data-validate-username="true"
  data-error-username="Invalid username format">

3. Async Validators

// Register async validator
FormManager.registerValidator('unique-email', async (value, element) => {
  const response = await fetch(`/api/check-email?email=${value}`);
  const data = await response.json();
  return data.available;
}, 'This email is already taken');
<input
  type="email"
  name="email"
  data-validate-unique-email="true"
  data-error-unique-email="This email is already registered">

4. Custom Validation Function

<input
  type="text"
  name="code"
  data-validate-fn="validateCode">

<script>
async function validateCode(value, field, instance) {
  // Custom validation logic
  const response = await fetch(`/api/validate-code?code=${value}`);
  const result = await response.json();

  if (!result.valid) {
    return result.message || 'Invalid code';
  }

  return true;
}
</script>

5. Validation Events

// Listen to validation events

// Field validation
document.addEventListener('form:field:change', (e) => {
  const { formId, field, name, value } = e.detail;
  console.log(`Field ${name} changed to:`, value);
});

// Form validation
document.addEventListener('form:validate', (e) => {
  const { formId, isValid, errors } = e.detail;

  if (isValid) {
    console.log('Form is valid');
  } else {
    console.log('Validation errors:', errors);
  }
});

// Validation failed
document.addEventListener('form:validation:failed', (e) => {
  const { formId, errors } = e.detail;
  console.log('Validation failed:', errors);
});

Form Submission

1. Traditional Submission

<!-- Non-AJAX submission -->
<form
  data-form="traditional"
  data-ajax-submit="false"
  method="POST"
  action="/submit">

  <input type="text" name="name" required>
  <button type="submit">Submit</button>
</form>

2. AJAX Submission

<!-- AJAX submission (default) -->
<form
  data-form="ajax"
  data-ajax-submit="true"
  data-action="/api/submit"
  data-method="POST">

  <input type="text" name="name" required>
  <button type="submit">Submit</button>
</form>

3. Custom HTTP Method

<!-- PUT request -->
<form
  data-form="update"
  data-method="PUT"
  data-action="/api/users/123">

  <input type="text" name="name">
  <button type="submit">Update</button>
</form>

<!-- DELETE request -->
<form
  data-form="delete"
  data-method="DELETE"
  data-action="/api/users/123">

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

4. Submission Events

// Before submission
document.addEventListener('form:submitting', (e) => {
  const { formId, form } = e.detail;
  console.log('Form submitting:', formId);
});

// After successful submission
document.addEventListener('form:submitted', (e) => {
  const { formId, response } = e.detail;
  console.log('Form submitted:', formId);
  console.log('Response:', response);
});

// On error
document.addEventListener('form:error', (e) => {
  const { formId, response, error } = e.detail;
  console.log('Form error:', formId);
  console.log('Error:', error || response);
});

5. Double Submit Prevention

<!-- Prevent double submissions -->
<form
  data-form="secure"
  data-prevent-double-submit="true"
  data-double-submit-timeout="2000">

  <!-- Prevents multiple submissions within 2 seconds -->
  <button type="submit">Submit</button>
</form>

AJAX Submission

1. JSON Submission

<form data-form="json" data-action="/api/users">
  <input type="text" name="name" value="John">
  <input type="email" name="email" value="john@example.com">
  <button type="submit">Submit</button>
</form>

Sent as:

{
  "name": "John",
  "email": "john@example.com",
  "_token": "csrf_token_here"
}

2. FormData Submission

<form data-form="upload" enctype="multipart/form-data">
  <input type="file" name="file">
  <input type="text" name="title">
  <button type="submit">Upload</button>
</form>

Sent as: multipart/form-data

3. Response Handling

// Server response format
{
  "success": true,
  "message": "Operation successful",
  "data": {
    "user": {...},
    "token": "..."
  },
  "redirectUrl": "/dashboard"
}

// Or error response
{
  "success": false,
  "message": "Validation failed",
  "errors": {
    "email": "Email is already taken",
    "password": "Password is too weak"
  }
}

4. Custom Response Processing

// Listen to response
document.addEventListener('form:submitted', async (e) => {
  const { formId, response } = e.detail;

  if (response.success) {
    // Process success
    if (response.data) {
      console.log('User data:', response.data.user);
    }

    // Custom redirect
    if (response.redirectUrl) {
      await Router.navigate(response.redirectUrl);
    }
  } else {
    // Process errors
    if (response.errors) {
      Object.entries(response.errors).forEach(([field, message]) => {
        console.log(`Field ${field}:`, message);
      });
    }
  }
});

5. Response Actions Format

FormManager supports processing actions from the server through ResponseHandler using a single action object format (not an array):

Correct Format:

// ✅ Correct - Single action object
return $this->successResponse([
    'data' => [...],
    'actions' => [
        'type' => 'reload',
        'closeModal' => true,
        'reload' => '[data-table="users"]'
    ]
], 'Saved successfully');

Incorrect Format:

// ❌ Wrong - Array of actions (not supported)
return $this->successResponse([
    'data' => [...],
    'actions' => [
        ['type' => 'notification', 'message' => 'Success'],
        ['type' => 'closeModal'],
        ['type' => 'reload']
    ]
]);

Supported Action Types:

1. Reload Table

'actions' => [
    'type' => 'reload',
    'closeModal' => true,  // Close modal before reload
    'reload' => '[data-table="table-name"]'  // Table selector
]

2. Redirect

'actions' => [
    'type' => 'redirect',
    'url' => '/dashboard',  // Destination URL
    'closeModal' => true    // optional
]

3. Close Modal Only

'actions' => [
    'type' => 'closeModal'
]

4. Show Notification

'actions' => [
    'type' => 'notification',
    'message' => 'Operation successful',
    'level' => 'success'  // success, error, warning, info
]

5. Combined Actions

// Show toast + close modal + reload table
'actions' => [
    'type' => 'reload',
    'message' => 'Saved successfully',  // Shows toast automatically
    'closeModal' => true,
    'reload' => '[data-table="users"]'
]

Execution Flow:

1. Server sends response with actions
   ↓
2. FormManager receives response
   ↓
3. Passes to ResponseHandler.process()
   ↓
4. ResponseHandler processes actions:
   - Show toast (if message exists)
   - Close modal (if closeModal: true)
   - Reload table (if reload exists)
   - Redirect (if url exists)

File Upload

1. Single File Upload

<form data-form="upload" data-action="/api/upload">
  <input type="file" name="avatar" accept="image/*" required>
  <button type="submit">Upload</button>
</form>

2. Multiple File Upload

<form data-form="multi-upload" data-action="/api/upload">
  <input type="file" name="files" multiple accept="image/*">
  <button type="submit">Upload Files</button>
</form>

3. Upload Progress

<form data-form="upload-progress" data-action="/api/upload">
  <input type="file" name="file">

  <!-- Progress bar (auto-generated) -->
  <div class="upload-progress" style="display:none">
    <div class="progress">
      <div class="progress-bar" role="progressbar"></div>
    </div>
    <div class="progress-text"></div>
  </div>

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

4. Upload Events

// Track upload progress
document.addEventListener('form:upload-progress', (e) => {
  const { loaded, total, percent } = e.detail;

  console.log(`Upload progress: ${percent}%`);
  console.log(`Loaded: ${loaded} / Total: ${total}`);

  // Update custom UI
  updateProgressBar(percent);
});

5. File Validation

<input
  type="file"
  name="avatar"
  accept="image/jpeg,image/png"
  data-max-size="5242880"
  data-error-file-size="File must be less than 5MB"
  data-error-file-type="Only JPEG and PNG files are allowed">

<script>
// Custom file validation
FormManager.registerValidator('file-size', (value, element, param) => {
  if (!element.files || element.files.length === 0) return true;

  const maxSize = parseInt(param);
  const file = element.files[0];

  return file.size <= maxSize;
}, 'File size exceeds the limit');
</script>

Form Data

1. Get Form Data

// Get form instance
const form = FormManager.getInstance('contact');

// Get form data as JSON
const data = FormManager.getValues('contact');
console.log(data);
// { name: "John", email: "john@example.com" }

// Or by element
const formElement = document.querySelector('form[data-form="contact"]');
const data2 = FormManager.getValues(formElement);

2. Set Form Data

// Get form instance
const instance = FormManager.getInstance('profile');

// Set field values
instance.elements.get('name').value = 'John Doe';
instance.elements.get('email').value = 'john@example.com';
instance.state.data.name = 'John Doe';
instance.state.data.email = 'john@example.com';

// Trigger change events
instance.elements.get('name').dispatchEvent(new Event('change'));

3. Reset Form

// Reset form to original state
const instance = FormManager.getInstance('contact');
FormManager.resetForm(instance);

// Or via element
const formElement = document.querySelector('form[data-form="contact"]');
const instance2 = FormManager.getInstanceByElement(formElement);
FormManager.resetForm(instance2);

4. Form State

const instance = FormManager.getInstance('contact');

// Check if form is modified
console.log(instance.state.modified); // true/false

// Check if form is valid
console.log(instance.state.valid); // true/false

// Check if form is submitting
console.log(instance.state.submitting); // true/false

// Get form errors
console.log(instance.state.errors);
// { email: "Invalid email", password: "Too short" }

// Get submit count
console.log(instance.state.submitCount); // 2

// Get last submit time
console.log(instance.state.lastSubmitTime); // 1635789012345

Field Persistence

1. Persist to localStorage

<form data-form="preferences">
  <!-- Auto-save to localStorage -->
  <input
    type="text"
    name="username"
    data-persist="true"
    data-persist-on="submit">

  <select
    name="language"
    data-persist="true"
    data-persist-on="submit">
    <option value="en">English</option>
    <option value="th">ไทย</option>
  </select>

  <button type="submit">Save</button>
</form>
<form data-form="settings">
  <!-- Save to cookie -->
  <input
    type="checkbox"
    name="remember"
    data-persist="cookie"
    data-persist-ttl-days="30">

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

3. Custom Persistence Key

<input
  type="text"
  name="email"
  data-persist="true"
  data-persist-key="user_email_pref">

4. TTL (Time to Live)

<!-- Expire after 7 days -->
<input
  type="text"
  name="search"
  data-persist="true"
  data-persist-ttl-days="7">

5. Remember Me (Login Forms)

<form data-form="login" data-action="/api/auth/login">
  <input type="text" name="username" required>
  <input type="password" name="password" required>

  <!-- Remember username checkbox -->
  <label>
    <input type="checkbox" name="remember" id="remember">
    Remember me
  </label>

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

Behavior:

  • When checked: save username to localStorage
  • Next time the username will be auto-filled
  • Password is not stored (for security)

Security Integration

1. CSRF Protection

<form data-form="secure" data-csrf="true">
  <!-- CSRF token added automatically -->
  <input type="hidden" name="_token" value="...">

  <input type="text" name="data">
  <button type="submit">Submit</button>
</form>

Auto-detection:

<!-- CSRF token from meta tag -->
<meta name="csrf-token" content="your_csrf_token">

<!-- Or from cookie -->
<!-- XSRF-TOKEN cookie -->

2. Rate Limiting

<form
  data-form="contact"
  data-rate-limit="true"
  data-rate-limit-limit="5"
  data-rate-limit-window="60">

  <!-- Max 5 submissions per 60 seconds -->
  <button type="submit">Submit</button>
</form>

Handle rate limit:

document.addEventListener('form:rateLimit', (e) => {
  const { formId, rateLimitResult } = e.detail;
  const { retryAfter } = rateLimitResult;

  showNotification(
    `Too many requests. Try again in ${retryAfter} seconds`,
    'warning'
  );
});

3. Input Sanitization

<form
  data-form="secure"
  data-sanitize-input="true">

  <!-- Input will be sanitized before submission -->
  <input type="text" name="comment">
  <button type="submit">Submit</button>
</form>

4. Security Events

// CSRF error
document.addEventListener('security:error', (e) => {
  const { message, status, code } = e.detail;

  if (code === 'csrf_invalid') {
    console.log('CSRF validation failed');
    // Refresh CSRF token
  }
});

// CSRF refreshed
document.addEventListener('csrf:refreshed', (e) => {
  const { token } = e.detail;
  console.log('New CSRF token:', token);
});

Usage Examples

1. Login Form

<form
  data-form="login"
  data-action="/api/auth/login"
  data-method="POST"
  data-ajax-submit="true"
  data-use-intended-url="true"
  data-redirect="/dashboard"
  data-auto-fill-intended-url="true">

  <div class="form-group">
    <label>Email:</label>
    <input
      type="email"
      name="username"
      required
      autofocus
      data-error-required="Please enter your email">
  </div>

  <div class="form-group">
    <label>Password:</label>
    <input
      type="password"
      name="password"
      required
      minlength="6"
      data-error-required="Please enter your password"
      data-error-minlength="Password must be at least 6 characters">
  </div>

  <div class="form-group">
    <label>
      <input type="checkbox" name="remember" id="remember">
      Remember me
    </label>
  </div>

  <!-- Intended URL (auto-filled) -->
  <input type="hidden" name="intended_url">

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

<script>
// Handle successful login
document.addEventListener('form:submitted', async (e) => {
  const { formId, response } = e.detail;

  if (formId === 'login' && response.success) {
    // AuthManager will handle token storage automatically
    showNotification('Login successful!', 'success');

    // Redirect handled automatically by FormManager
  }
});

// Handle login error
document.addEventListener('form:error', (e) => {
  const { formId, response } = e.detail;

  if (formId === 'login') {
    if (response.status === 401) {
      showNotification('Invalid credentials', 'error');
    } else {
      showNotification(response.message || 'Login failed', 'error');
    }
  }
});
</script>

2. Registration Form

<form
  data-form="register"
  data-action="/api/auth/register"
  data-redirect="/welcome"
  data-success-message="Account created successfully!">

  <div class="form-group">
    <label>Name:</label>
    <input
      type="text"
      name="name"
      required
      minlength="2"
      data-error-required="Please enter your name"
      data-error-minlength="Name must be at least 2 characters">
  </div>

  <div class="form-group">
    <label>Email:</label>
    <input
      type="email"
      name="email"
      required
      data-validate-unique-email="true"
      data-error-required="Please enter your email"
      data-error-email="Please enter a valid email"
      data-error-unique-email="This email is already registered">
  </div>

  <div class="form-group">
    <label>Username:</label>
    <input
      type="text"
      name="username"
      required
      minlength="3"
      maxlength="20"
      pattern="^[a-zA-Z0-9_]+$"
      data-validate-unique-username="true"
      data-error-required="Please choose a username"
      data-error-pattern="Username can only contain letters, numbers, and underscores"
      data-error-unique-username="This username is already taken">
  </div>

  <div class="form-group">
    <label>Password:</label>
    <input
      type="password"
      name="password"
      id="password"
      required
      minlength="8"
      data-validate-strong-password="true"
      data-error-required="Please enter a password"
      data-error-minlength="Password must be at least 8 characters"
      data-error-strong-password="Password must contain uppercase, lowercase, number, and special character">
  </div>

  <div class="form-group">
    <label>Confirm Password:</label>
    <input
      type="password"
      name="confirm_password"
      required
      data-validate-match="password"
      data-error-required="Please confirm your password"
      data-error-match="Passwords do not match">
  </div>

  <div class="form-group">
    <label>
      <input
        type="checkbox"
        name="terms"
        required
        data-error-required="You must accept the terms">
      I accept the <a href="/terms">Terms of Service</a>
    </label>
  </div>

  <button type="submit">Create Account</button>
</form>

<script>
// Custom validators for registration
FormManager.registerValidator('unique-email', async (value) => {
  const response = await fetch(`/api/check-email?email=${value}`);
  const data = await response.json();
  return data.available;
}, 'This email is already registered');

FormManager.registerValidator('unique-username', async (value) => {
  const response = await fetch(`/api/check-username?username=${value}`);
  const data = await response.json();
  return data.available;
}, 'This username is already taken');

FormManager.registerValidator('strong-password', (value) => {
  // Must contain: uppercase, lowercase, number, special char
  const hasUpper = /[A-Z]/.test(value);
  const hasLower = /[a-z]/.test(value);
  const hasNumber = /[0-9]/.test(value);
  const hasSpecial = /[!@#$%^&*]/.test(value);

  return hasUpper && hasLower && hasNumber && hasSpecial;
}, 'Password must contain uppercase, lowercase, number, and special character');
</script>

3. Contact Form

<form
  data-form="contact"
  data-action="/api/contact"
  data-reset-after-submit="true"
  data-success-message="Thank you! We'll get back to you soon."
  data-show-success-in-notification="true">

  <div class="row">
    <div class="col-md-6">
      <label>Name:</label>
      <input
        type="text"
        name="name"
        required
        data-error-required="Please enter your name">
    </div>

    <div class="col-md-6">
      <label>Email:</label>
      <input
        type="email"
        name="email"
        required
        data-error-required="Please enter your email"
        data-error-email="Please enter a valid email">
    </div>
  </div>

  <div class="form-group">
    <label>Subject:</label>
    <select name="subject" required data-error-required="Please select a subject">
      <option value="">-- Select Subject --</option>
      <option value="general">General Inquiry</option>
      <option value="support">Technical Support</option>
      <option value="sales">Sales</option>
      <option value="feedback">Feedback</option>
    </select>
  </div>

  <div class="form-group">
    <label>Message:</label>
    <textarea
      name="message"
      rows="5"
      required
      minlength="10"
      maxlength="1000"
      data-error-required="Please enter your message"
      data-error-minlength="Message must be at least 10 characters"
      data-error-maxlength="Message must be no more than 1000 characters"></textarea>
    <div class="form-text">
      <span id="charCount">0</span> / 1000 characters
    </div>
  </div>

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

<script>
// Character counter
const textarea = document.querySelector('textarea[name="message"]');
const charCount = document.getElementById('charCount');

textarea.addEventListener('input', () => {
  charCount.textContent = textarea.value.length;

  if (textarea.value.length > 1000) {
    charCount.style.color = 'red';
  } else {
    charCount.style.color = '';
  }
});
</script>

4. Profile Update Form

<form
  data-form="profile"
  data-action="/api/profile"
  data-method="PUT"
  data-success-message="Profile updated successfully!">

  <div class="form-group">
    <label>Profile Picture:</label>
    <input
      type="file"
      name="avatar"
      accept="image/*"
      data-max-size="5242880"
      data-error-file-size="File must be less than 5MB">

    <div class="current-avatar">
      <img id="avatarPreview" src="/uploads/avatar.jpg" alt="Avatar">
    </div>
  </div>

  <div class="form-group">
    <label>Name:</label>
    <input
      type="text"
      name="name"
      value="John Doe"
      required>
  </div>

  <div class="form-group">
    <label>Email:</label>
    <input
      type="email"
      name="email"
      value="john@example.com"
      required>
  </div>

  <div class="form-group">
    <label>Bio:</label>
    <textarea
      name="bio"
      rows="4"
      maxlength="500">Web developer and designer</textarea>
  </div>

  <div class="form-group">
    <label>Website:</label>
    <input
      type="url"
      name="website"
      value="https://johndoe.com"
      data-error-url="Please enter a valid URL">
  </div>

  <button type="submit">Update Profile</button>
</form>

<script>
// Preview avatar before upload
const avatarInput = document.querySelector('input[name="avatar"]');
const avatarPreview = document.getElementById('avatarPreview');

avatarInput.addEventListener('change', (e) => {
  const file = e.target.files[0];
  if (file) {
    const reader = new FileReader();
    reader.onload = (e) => {
      avatarPreview.src = e.target.result;
    };
    reader.readAsDataURL(file);
  }
});

// Handle success
document.addEventListener('form:submitted', (e) => {
  const { formId, response } = e.detail;

  if (formId === 'profile' && response.success) {
    // Update UI with new data
    if (response.data.avatar) {
      avatarPreview.src = response.data.avatar;
    }
  }
});
</script>

5. Multi-Step Form

<form
  data-form="wizard"
  data-action="/api/onboarding"
  data-validate-on-submit="false">

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

    <div class="form-group">
      <label>Name:</label>
      <input type="text" name="name" required>
    </div>

    <div class="form-group">
      <label>Email:</label>
      <input type="email" name="email" required>
    </div>

    <button type="button" class="btn-next">Next</button>
  </div>

  <!-- Step 2: Account -->
  <div class="step" data-step="2" style="display:none">
    <h3>Step 2: Account Setup</h3>

    <div class="form-group">
      <label>Username:</label>
      <input type="text" name="username" required>
    </div>

    <div class="form-group">
      <label>Password:</label>
      <input type="password" name="password" id="password" required>
    </div>

    <div class="form-group">
      <label>Confirm Password:</label>
      <input
        type="password"
        name="confirm_password"
        data-validate-match="password"
        required>
    </div>

    <button type="button" class="btn-prev">Previous</button>
    <button type="button" class="btn-next">Next</button>
  </div>

  <!-- Step 3: Preferences -->
  <div class="step" data-step="3" style="display:none">
    <h3>Step 3: Preferences</h3>

    <div class="form-group">
      <label>Language:</label>
      <select name="language">
        <option value="en">English</option>
        <option value="th">ไทย</option>
      </select>
    </div>

    <div class="form-group">
      <label>Notifications:</label>
      <label>
        <input type="checkbox" name="notifications[]" value="email">
        Email
      </label>
      <label>
        <input type="checkbox" name="notifications[]" value="sms">
        SMS
      </label>
    </div>

    <button type="button" class="btn-prev">Previous</button>
    <button type="submit">Complete</button>
  </div>

  <!-- Progress indicator -->
  <div class="progress-steps">
    <div class="step-indicator active" data-step="1">1</div>
    <div class="step-indicator" data-step="2">2</div>
    <div class="step-indicator" data-step="3">3</div>
  </div>
</form>

<script>
let currentStep = 1;
const totalSteps = 3;

// Next button
document.querySelectorAll('.btn-next').forEach(btn => {
  btn.addEventListener('click', async () => {
    // Validate current step
    const stepElement = document.querySelector(`.step[data-step="${currentStep}"]`);
    const fields = stepElement.querySelectorAll('input, select, textarea');

    let valid = true;
    for (const field of fields) {
      const instance = FormManager.getInstance('wizard');
      const isValid = await FormManager.validateField(instance, field);
      if (!isValid) {
        valid = false;
        break;
      }
    }

    if (valid && currentStep < totalSteps) {
      // Hide current step
      stepElement.style.display = 'none';

      // Show next step
      currentStep++;
      document.querySelector(`.step[data-step="${currentStep}"]`).style.display = 'block';

      // Update progress
      document.querySelector(`.step-indicator[data-step="${currentStep}"]`).classList.add('active');
    }
  });
});

// Previous button
document.querySelectorAll('.btn-prev').forEach(btn => {
  btn.addEventListener('click', () => {
    if (currentStep > 1) {
      // Hide current step
      document.querySelector(`.step[data-step="${currentStep}"]`).style.display = 'none';

      // Show previous step
      currentStep--;
      document.querySelector(`.step[data-step="${currentStep}"]`).style.display = 'block';

      // Update progress
      document.querySelector(`.step-indicator[data-step="${currentStep + 1}"]`).classList.remove('active');
    }
  });
});
</script>

6. Dynamic Form Fields

<form data-form="dynamic" data-action="/api/contacts">
  <div id="contactsContainer">
    <div class="contact-row">
      <input type="text" name="contacts[0][name]" placeholder="Name" required>
      <input type="email" name="contacts[0][email]" placeholder="Email" required>
      <button type="button" class="btn-remove" style="display:none">Remove</button>
    </div>
  </div>

  <button type="button" id="addContact">Add Contact</button>
  <button type="submit">Submit</button>
</form>

<script>
let contactIndex = 1;

document.getElementById('addContact').addEventListener('click', () => {
  const container = document.getElementById('contactsContainer');

  const row = document.createElement('div');
  row.className = 'contact-row';
  row.innerHTML = `
    <input type="text" name="contacts[${contactIndex}][name]" placeholder="Name" required>
    <input type="email" name="contacts[${contactIndex}][email]" placeholder="Email" required>
    <button type="button" class="btn-remove">Remove</button>
  `;

  container.appendChild(row);
  contactIndex++;

  // Show remove buttons
  document.querySelectorAll('.btn-remove').forEach(btn => {
    btn.style.display = 'inline-block';
  });

  // Re-scan form to initialize new fields
  FormManager.scan(container);
});

// Remove contact
document.addEventListener('click', (e) => {
  if (e.target.classList.contains('btn-remove')) {
    e.target.closest('.contact-row').remove();

    // Hide remove button if only one contact left
    const rows = document.querySelectorAll('.contact-row');
    if (rows.length === 1) {
      rows[0].querySelector('.btn-remove').style.display = 'none';
    }
  }
});
</script>

API Reference

Methods

init(options)

Initialize FormManager with configuration

Parameters:

  • options (Object) - Configuration options

Returns: Promise<FormManager>

Example:

await FormManager.init({
  debug: false,
  ajaxSubmit: true,
  autoValidate: true
});

initForm(form)

Initialize a specific form

Parameters:

  • form (HTMLFormElement) - Form element

Returns: Object - Form instance

Example:

const formElement = document.querySelector('form[data-form="contact"]');
const instance = FormManager.initForm(formElement);

getInstance(formId)

Get form instance by ID

Parameters:

  • formId (string) - Form ID from data-form attribute

Returns: Object|undefined - Form instance

Example:

const instance = FormManager.getInstance('contact');
console.log(instance.state.valid);

getInstanceByElement(formElement)

Get form instance by element

Parameters:

  • formElement (HTMLFormElement) - Form element

Returns: Object|null - Form instance

Example:

const form = document.querySelector('form');
const instance = FormManager.getInstanceByElement(form);

getValues(identifier)

Get form data as plain object

Parameters:

  • identifier (string|HTMLFormElement|Object) - Form ID, element, or instance

Returns: Object|null - Form data

Example:

const data = FormManager.getValues('contact');
console.log(data);
// { name: "John", email: "john@example.com" }

validateForm(instance)

Validate entire form

Parameters:

  • instance (Object) - Form instance

Returns: Promise<boolean> - True if valid

Example:

const instance = FormManager.getInstance('contact');
const isValid = await FormManager.validateForm(instance);

if (isValid) {
  console.log('Form is valid');
} else {
  console.log('Validation errors:', instance.state.errors);
}

validateField(instance, field)

Validate single field

Parameters:

  • instance (Object) - Form instance
  • field (HTMLElement) - Field element

Returns: Promise<boolean> - True if valid

Example:

const instance = FormManager.getInstance('contact');
const emailField = document.querySelector('input[name="email"]');
const isValid = await FormManager.validateField(instance, emailField);

registerValidator(name, fn, defaultMessage)

Register custom validator

Parameters:

  • name (string) - Validator name
  • fn (Function) - Validation function
  • defaultMessage (string) - Default error message

Returns: void

Example:

FormManager.registerValidator('username', (value, element) => {
  return /^[a-zA-Z0-9_]+$/.test(value);
}, 'Username can only contain letters, numbers, and underscores');

resetForm(instance)

Reset form to original state

Parameters:

  • instance (Object) - Form instance

Returns: void

Example:

const instance = FormManager.getInstance('contact');
FormManager.resetForm(instance);

destroyForm(instance)

Destroy form instance and cleanup

Parameters:

  • instance (Object) - Form instance

Returns: void

Example:

const instance = FormManager.getInstance('contact');
FormManager.destroyForm(instance);

scan(container)

Scan container for forms and initialize them

Parameters:

  • container (HTMLElement) - Container element (default: document)

Returns: Array<HTMLFormElement> - Found forms

Example:

// Scan entire document
FormManager.scan();

// Scan specific container
const modal = document.getElementById('modal');
FormManager.scan(modal);

Best Practices

1. Always Use data-form Attribute

<!-- ✅ Good - explicit opt-in -->
<form data-form="contact">
  <!-- Form fields -->
</form>

<!-- ❌ Bad - won't be initialized -->
<form>
  <!-- FormManager won't touch this -->
</form>

2. Provide Error Messages

<!-- ✅ Good - custom error messages -->
<input
  type="email"
  name="email"
  required
  data-error-required="Please enter your email address"
  data-error-email="Please enter a valid email address">

<!-- ❌ Bad - generic error messages -->
<input type="email" name="email" required>

3. Use Appropriate Validators

<!-- ✅ Good - specific validators -->
<input
  type="email"
  name="email"
  required
  data-validate-email="true">

<input
  type="url"
  name="website"
  data-validate-url="true">

<!-- ❌ Bad - no validation -->
<input type="text" name="email">
<input type="text" name="website">

4. Handle Responses Properly

// ✅ Good - handle both success and error
document.addEventListener('form:submitted', (e) => {
  const { formId, response } = e.detail;

  if (response.success) {
    showSuccess(response.message);
  } else {
    showError(response.message);
  }
});

document.addEventListener('form:error', (e) => {
  const { formId, error } = e.detail;
  showError(error.message);
});

// ❌ Bad - only handle success
document.addEventListener('form:submitted', (e) => {
  showSuccess('Done!');
});

5. Clean Up Dynamic Forms

// ✅ Good - destroy when removing
function removeForm(formElement) {
  FormManager.destroyFormByElement(formElement);
  formElement.remove();
}

// ❌ Bad - just remove element
function removeForm(formElement) {
  formElement.remove();  // Memory leak!
}

Auto-Load Form Data from API

FormManager supports automatically loading data from an API when a form opens, using URL parameters to determine what to load.

Basics: Single Parameter

<!-- URL: /form.html?id=1 -->
<form data-form="edit-user"
      data-action="/api/v1/users"
      data-method="PUT"
      data-load-url="/api/v1/users/{id}"
      data-auto-load="true">

  <!-- Developer must create input fields - no auto-generation -->
  <input type="hidden" name="id" data-from-url="id" />

  <div class="form-group">
    <label>Name</label>
    <input type="text" name="name" required />
  </div>

  <div class="form-group">
    <label>Email</label>
    <input type="email" name="email" required />
  </div>

  <div class="form-group">
    <label>Phone</label>
    <input type="tel" name="phone" />
  </div>

  <button type="submit">Update User</button>
</form>

Flow:

  1. FormManager detects data-auto-load="true"
  2. Reads data-load-url="/api/v1/users/{id}"
  3. Finds input with data-from-url="id" → extracts value from URL param ?id=1
  4. Replaces {id} in URL → /api/v1/users/1
  5. Requests GET /api/v1/users/1
  6. Populates form fields by name attribute

API Response:

{
  "success": true,
  "data": {
    "id": 1,
    "name": "John Doe",
    "email": "john@example.com",
    "phone": "0812345678"
  }
}

Multiple Parameters

<!-- URL: /form.html?userId=5&addressId=12 -->
<form data-form="edit-address"
      data-action="/api/v1/users/{userId}/addresses"
      data-method="PUT"
      data-load-url="/api/v1/users/{userId}/addresses/{addressId}"
      data-auto-load="true">

  <!-- Developer creates input fields - no worry about duplicates -->
  <input type="hidden" name="userId" data-from-url="userId" />
  <input type="hidden" name="addressId" data-from-url="addressId" />

  <div class="form-group">
    <label>Street</label>
    <input type="text" name="street" required />
  </div>

  <div class="form-group">
    <label>City</label>
    <input type="text" name="city" required />
  </div>

  <div class="form-group">
    <label>Postal Code</label>
    <input type="text" name="zipcode" />
  </div>

  <button type="submit">Update Address</button>
</form>

Result:

  • Load from: GET /api/v1/users/5/addresses/12
  • Submit to: PUT /api/v1/users/5/addresses/12

Optional Parameters with Defaults

<!-- URL: /form.html?reportId=5 or ?reportId=5&version=2&lang=en -->
<form data-form="edit-report"
      data-load-url="/api/v1/reports/{reportId}?version={version}&lang={lang}"
      data-auto-load="true">

  <input type="hidden" name="reportId" data-from-url="reportId" data-required="true" />
  <input type="hidden" name="version" data-from-url="version" data-default="latest" />
  <input type="hidden" name="lang" data-from-url="lang" data-default="en" />

  <div class="form-group">
    <label>Report Title</label>
    <input type="text" name="title" required />
  </div>

  <div class="form-group">
    <label>Content</label>
    <textarea name="content" rows="10"></textarea>
  </div>

  <button type="submit">Save Report</button>
</form>

URL Examples:

  • ?reportId=5GET /api/v1/reports/5?version=latest&lang=en
  • ?reportId=5&version=2GET /api/v1/reports/5?version=2&lang=en
  • ?reportId=5&version=2&lang=thGET /api/v1/reports/5?version=2&lang=th

Data Attributes for Auto-Load

On form element:

Attribute Type Description Example
data-auto-load Boolean Enable auto-load "true"
data-load-url String URL pattern (supports {param}) "/api/users/{id}"
data-load-method String HTTP method for loading (default: GET) "GET"
data-on-load-error String Callback function for load errors "handleLoadError"

On input element:

Attribute Type Description Example
data-from-url String Name of URL parameter to extract "userId"
data-default String Default value if not in URL "latest"
data-required Boolean Required (won't load if missing) "true"

Error Handling

<form data-load-url="/api/v1/users/{userId}/orders/{orderId}"
      data-auto-load="true"
      data-on-load-error="handleLoadError">

  <input type="hidden" name="userId" data-from-url="userId" data-required="true" />
  <input type="hidden" name="orderId" data-from-url="orderId" data-required="true" />

  <!-- form fields -->
</form>

<script>
function handleLoadError(error) {
  if (error.code === 'MISSING_REQUIRED_PARAM') {
    NotificationManager.error('Please specify User ID and Order ID in URL');
  } else if (error.code === 'NOT_FOUND') {
    NotificationManager.error('Data not found');
    setTimeout(() => {
      window.location.href = '/orders';
    }, 2000);
  } else {
    NotificationManager.error('Error occurred: ' + error.message);
  }
}
</script>

Error Codes:

  • MISSING_REQUIRED_PARAM - Missing parameter with data-required="true"
  • NOT_FOUND - API returned 404
  • UNAUTHORIZED - API returned 401/403
  • NETWORK_ERROR - Connection issues
  • INVALID_RESPONSE - Invalid API response format

Complete Example: Edit Order Item

<!DOCTYPE html>
<html>
<head>
  <title>Edit Order Item</title>
  <link rel="stylesheet" href="/Now/css/Now.css">
</head>
<body>
  <!-- URL: /edit-order-item.html?orderId=1001&itemId=3 -->

  <div class="container">
    <h1>Edit Order Item</h1>

    <form data-form="edit-order-item"
          data-action="/api/v1/orders/{orderId}/items"
          data-method="PUT"
          data-load-url="/api/v1/orders/{orderId}/items/{itemId}"
          data-auto-load="true"
          data-ajax-submit="true"
          data-on-load-error="handleLoadError">

      <!-- Hidden fields - Developer creates them -->
      <input type="hidden" name="orderId" data-from-url="orderId" data-required="true" />
      <input type="hidden" name="itemId" data-from-url="itemId" data-required="true" />

      <div class="form-group">
        <label>Product Name</label>
        <input type="text" name="productName" readonly class="readonly" />
      </div>

      <div class="form-group">
        <label>Quantity *</label>
        <input type="number" name="quantity" min="1" required />
      </div>

      <div class="form-group">
        <label>Price *</label>
        <input type="number" name="price" step="0.01" min="0" required />
      </div>

      <div class="form-group">
        <label>Discount (%)</label>
        <input type="number" name="discount" min="0" max="100" value="0" />
      </div>

      <div class="form-actions">
        <button type="button" class="button" onclick="history.back()">Cancel</button>
        <button type="submit" class="button primary">Update Item</button>
      </div>
    </form>
  </div>

  <script src="/Now/Now.js"></script>
  <script>
    // Initialize FormManager
    await FormManager.init({
      debug: true,
      ajaxSubmit: true
    });

    // Error handler
    function handleLoadError(error) {
      console.error('Load error:', error);

      if (error.code === 'MISSING_REQUIRED_PARAM') {
        NotificationManager.error('Missing order ID or item ID in URL');
        setTimeout(() => window.location.href = '/orders', 2000);
      } else if (error.code === 'NOT_FOUND') {
        NotificationManager.error('Order item not found');
        setTimeout(() => window.location.href = '/orders', 2000);
      } else {
        NotificationManager.error('Failed to load data: ' + error.message);
      }
    }

    // Success handler
    document.addEventListener('form:submitted', (e) => {
      const { formId, response } = e.detail;

      if (formId === 'edit-order-item' && response.success) {
        NotificationManager.success('Order item updated successfully');
        setTimeout(() => {
          window.location.href = '/orders?id=' + response.data.orderId;
        }, 1000);
      }
    });
  </script>
</body>
</html>

API Endpoint (PHP):

// GET /api/v1/orders/{orderId}/items/{itemId}
public function show(Request $request) {
    $orderId = $request->get('orderId')->toInt();
    $itemId = $request->get('itemId')->toInt();

    $item = $this->db->table('order_items')
        ->join('products', 'products.id', 'order_items.product_id')
        ->where([
            ['order_items.id', $itemId],
            ['order_items.order_id', $orderId]
        ])
        ->select(
            'order_items.*',
            'products.name as productName'
        )
        ->first();

    if (!$item) {
        return $this->errorResponse('Order item not found', 404);
    }

    return $this->successResponse([
        'data' => $item
    ]);
}

// PUT /api/v1/orders/{orderId}/items/{itemId}
public function update(Request $request) {
    $orderId = $request->get('orderId')->toInt();
    $itemId = $request->post('itemId')->toInt();
    $quantity = $request->post('quantity')->toInt();
    $price = $request->post('price')->toFloat();
    $discount = $request->post('discount', 0)->toFloat();

    // Validation
    if ($quantity < 1) {
        return $this->errorResponse('Quantity must be at least 1', 400);
    }

    // Update
    $updated = $this->db->update('order_items', [
        ['id', $itemId],
        ['order_id', $orderId]
    ], [
        'quantity' => $quantity,
        'price' => $price,
        'discount' => $discount,
        'updated_at' => date('Y-m-d H:i:s')
    ]);

    if ($updated) {
        return $this->successResponse([
            'data' => [
                'orderId' => $orderId,
                'itemId' => $itemId
            ],
            'actions' => [
                ['type' => 'notification', 'level' => 'success', 'message' => 'Updated successfully']
            ]
        ]);
    }

    return $this->errorResponse('Failed to update', 500);
}

Summary of Auto-Load Features

Developer Control - Developers create input fields (no auto-generation)
Multiple Parameters - Supports multiple URL parameters
Optional Parameters - Supports default values
Error Handling - Clear error codes and callbacks
Path + Query Params - Supports both /users/{id} and ?version={v}
Validation - Validates required parameters before loading
Integration - Works with ResponseHandler and AJAX submit

All Form Data Attributes Reference

Core Attributes

Attribute Type Default Description
data-form String - Required - Form ID for identification
data-ajax-submit Boolean true Submit form via AJAX
data-action String form.action URL endpoint for submission
data-method String POST HTTP method (GET, POST, PUT, DELETE, PATCH)
data-confirm String - Confirmation message before submit

Validation Attributes

Attribute Type Default Description
data-auto-validate Boolean true Validate before submit
data-validate-on-input Boolean true Validate while typing
data-validate-on-blur Boolean true Validate on field blur
data-validate-only-dirty Boolean true Validate only modified fields
data-auto-focus-error Boolean true Focus first error field

Auto-Load Attributes

Attribute Type Default Description
data-auto-load Boolean false Enable auto-load from API
data-load-url String - URL pattern for loading (supports {param})
data-load-method String GET HTTP method for loading
data-on-load-error String - Callback function on load error

Security Attributes

Attribute Type Default Description
data-csrf Boolean true Enable CSRF protection
data-csrf-token String auto CSRF token (auto-detect if not specified)
data-rate-limit Boolean true Enable rate limiting
data-sanitize-input Boolean true Perform input sanitization

UI/UX Attributes

Attribute Type Default Description
data-prevent-double-submit Boolean true Prevent double submission
data-show-loading-on-submit Boolean true Show loading state
data-loading-text String "Processing..." Text while processing
data-reset-after-submit Boolean false Reset form after success
data-auto-clear-errors Boolean true Auto-clear errors
data-auto-clear-errors-delay Number 5000 Delay before clearing errors (ms)

Response Handling Attributes

Attribute Type Default Description
data-redirect String - URL to redirect after success
data-redirect-delay Number 1000 Delay before redirect (ms)
data-success-message String auto Message to show on success
data-error-message String auto Message to show on error
data-show-errors-inline Boolean true Show errors near fields
data-show-errors-in-notification Boolean false Show errors in notification

File Upload Attributes

Attribute Type Default Description
data-max-file-size Number - Maximum file size (bytes)
data-allowed-extensions String - Allowed file extensions (comma-separated)
data-show-progress Boolean true Show upload progress

Input Element Attributes (Auto-Load)

Attribute Type Default Description
data-from-url String - URL parameter name to extract value
data-default String - Default value if not in URL
data-required Boolean false Required (won't load if missing)

Common Pitfalls

1. Missing data-form Attribute

<!-- ❌ Bad - won't initialize -->
<form action="/submit" method="POST">
  <input type="text" name="name">
  <button type="submit">Submit</button>
</form>

<!-- ✅ Good - will initialize -->
<form data-form="contact" action="/submit" method="POST">
  <input type="text" name="name">
  <button type="submit">Submit</button>
</form>

2. Forgetting to Handle Errors

// ❌ Bad - no error handling
document.addEventListener('form:submitted', (e) => {
  showSuccess('Submitted!');
});

// ✅ Good - handle errors
document.addEventListener('form:submitted', (e) => {
  const { response } = e.detail;

  if (response.success) {
    showSuccess(response.message);
  } else {
    showError(response.message);
  }
});

3. Not Validating Before Custom Actions

// ❌ Bad - no validation
document.querySelector('button.custom').addEventListener('click', async () => {
  const data = FormManager.getValues('contact');
  await sendData(data);
});

// ✅ Good - validate first
document.querySelector('button.custom').addEventListener('click', async () => {
  const instance = FormManager.getInstance('contact');
  const isValid = await FormManager.validateForm(instance);

  if (isValid) {
    const data = FormManager.getValues('contact');
    await sendData(data);
  }
});

4. Mixing Form Submission Methods

<!-- ❌ Bad - conflicting settings -->
<form
  data-form="mixed"
  data-ajax-submit="true"
  action="/submit"
  method="POST">
  <!-- Will AJAX submit, but action/method ignored -->
</form>

<!-- ✅ Good - consistent -->
<form
  data-form="ajax"
  data-ajax-submit="true"
  data-action="/api/submit"
  data-method="POST">
  <!-- Clear AJAX submission -->
</form>