Now.js Framework Documentation
FormManager - Form Handling & Validation
FormManager - Form Handling & Validation
Documentation for FormManager, the form management system for the Now.js Framework.
📋 Table of Contents
- Overview
- Installation & Import
- Getting Started
- Form Configuration
- Form Validation
- Form Submission
- AJAX Submission
- File Upload
- Form Data
- Field Persistence
- Security Integration
- Usage Examples
- API Reference
- Best Practices
- 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 objectGetting 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); // 1635789012345Field 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>2. Persist to Cookie
<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 instancefield(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 namefn(Function) - Validation functiondefaultMessage(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:
- FormManager detects
data-auto-load="true" - Reads
data-load-url="/api/v1/users/{id}" - Finds input with
data-from-url="id"→ extracts value from URL param?id=1 - Replaces
{id}in URL →/api/v1/users/1 - Requests
GET /api/v1/users/1 - Populates form fields by
nameattribute
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=5→GET /api/v1/reports/5?version=latest&lang=en?reportId=5&version=2→GET /api/v1/reports/5?version=2&lang=en?reportId=5&version=2&lang=th→GET /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 withdata-required="true"NOT_FOUND- API returned 404UNAUTHORIZED- API returned 401/403NETWORK_ERROR- Connection issuesINVALID_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>Related Documentation
- SecurityManager.md - CSRF & rate limiting
- AuthManager.md - Authentication integration
- RouterManager.md - Navigation after submit
- ResponseHandler.md - Response processing
- NotificationManager.md - User notifications