Now.js Framework Documentation
FormManager
FormManager
Overview
FormManager is the form management system in Now.js Framework. It supports validation, auto-submit, and API integration.
When to use:
- Need form handling
- Need validation
- Need AJAX submission
- Need auto-enhance form elements
Why use it:
- ✅ Automatic validation
- ✅ AJAX submission
- ✅ Loading states
- ✅ Error display
- ✅ Auto-enhance elements
- ✅ Multiple submit handlers
- ✅ Declarative result binding for search/list forms
Basic Usage
HTML Declarative
<form data-component="form"
data-action="/api/users"
data-method="POST">
<input type="text" name="name" required>
<input type="email" name="email" required>
<button type="submit">Submit</button>
</form>With Success Redirect
<form data-component="form"
data-action="/api/users"
data-success-redirect="/users">
...
</form>With Notification
<form data-component="form"
data-action="/api/contact"
data-success-message="Message sent successfully!">
...
</form>Data Attributes
| Attribute | Description |
|---|---|
data-component="form" |
Initialize form |
data-action |
API endpoint |
data-method |
HTTP method (POST, PUT, PATCH) |
data-success-redirect |
Redirect URL on success |
data-success-message |
Success notification |
data-error-message |
Error notification |
data-validate |
Enable validation |
data-confirm |
Confirmation message |
data-load-cache |
Enable caching for data-load-api GET requests |
data-load-cache-time |
Cache TTL in milliseconds for data-load-api |
data-load-options-cache |
Enable caching for data-load-options-api GET requests |
data-load-options-cache-time |
Cache TTL in milliseconds for data-load-options-api |
data-watch-api |
API endpoint that should be called again when watched fields change |
data-watch-method |
HTTP method for data-watch-api (GET by default) |
data-watch-fields |
Comma-separated field names/ids to send to the watched API |
data-watch-trigger |
Comma-separated field names/ids that trigger the watched API when they change |
data-watch-debounce |
Debounce in milliseconds before the watched API is called |
data-watch-on-load |
Call the watched API once after initial form data is ready (true by default, set to false to avoid the extra initial call) |
data-submit-target |
CSS selector for the container that should be rebound from a successful AJAX response |
data-submit-pagination-target |
CSS selector for the container where pagination buttons should be rendered |
data-submit-query-params |
Update the current URL query string from form values after a successful AJAX submit |
data-submit-query-fields |
Comma-separated field names that should be written into the URL query string |
data-submit-page-field |
Field name used for page changes when pagination re-submits the same form |
Note: named form fields are submitted exactly like a native form submit. If your sort, filter, keyword, category, or hidden paging inputs live in the same form, they will be re-sent automatically on every AJAX submit and pagination click.
Declarative Watched API Binding
Use this pattern when a form has derived UI that depends on multiple fields and you want the server to return a payload that can be rebound with normal TemplateManager directives.
How it works
- FormManager reads the fields listed in
data-watch-fields. - When one of the fields in
data-watch-triggerchanges, FormManager callsdata-watch-api. Ifdata-watch-on-loadis not set tofalse, the watched API is also called once after the initial load payload has been applied. - The response payload is merged back into the form state with
setFormData(). - Existing bindings such as
data-text,data-attr,data-if, anddata-forupdate automatically.
Example: Derived Leave Preview
<form data-form="leave-request"
data-load-api="api/eleave/request/get"
data-watch-api="api/eleave/request/policy"
data-watch-fields="id,leave_id,start_date,start_period,end_date,end_period"
data-watch-trigger="leave_id,start_date,start_period,end_date,end_period"
data-watch-debounce="150">
<select name="leave_id" data-options-key="leave_id" data-attr="value:leave_id"></select>
<input type="date" name="start_date" data-attr="value:start_date">
<input type="date" name="end_date" data-attr="value:end_date">
<aside data-text="preview.leave_type_detail"></aside>
<input type="text" data-attr="value:preview.days" readonly>
<div class="comment" data-text="preview.days_note"></div>
<div data-if="preview.balance_summary">
<div data-for="year in preview.balance_summary.years">
<template>
<div>
<strong data-text="year.heading_text"></strong>
<div data-text="year.summary_text"></div>
</div>
</template>
</div>
</div>
</form>Response Shape
The watched API can return any payload that setFormData() can bind. A common pattern is to return a nested preview object plus any supporting option collections.
{
"success": true,
"data": {
"preview": {
"leave_type_detail": "Vacation • 10 days/year",
"days": "1.5",
"days_note": "Calculated automatically from the selected date range",
"balance_message": "",
"balance_summary": {
"years": []
}
}
}
}Use watched API binding when the UI is a pure function of current form values. If the response should execute server actions such as notification, redirect, modal, or form, use requestApi instead.
data-watch-on-load defaults to true. If data-load-api already returns the derived state you need, set data-watch-on-load="false" to avoid a second initial request.
Use requestApi with data-response-bind="template" when the response should update a non-form target or when the page needs explicit control over which request parameters are sent.
When a result form should keep its filters shareable or refresh-safe, add data-submit-query-params="true" and optionally data-submit-query-fields="year,leave_id,...". FormManager will update the browser query string with the submitted values after each successful AJAX submit.
Declarative Result Binding
Use this pattern when a form should submit with AJAX, bind the response into a result container, and let FormManager create pagination buttons automatically.
How it works
- The form submits via AJAX.
- FormManager normalizes the success payload into a canonical schema similar to TableManager:
{
data: [...],
meta: {
page: 1,
pageSize: 20,
total: 23,
totalPages: 2
},
filters: {},
options: {}
}- The full normalized payload is exposed on
context.state. - The primary data source is exposed on
context.data. - Pagination buttons are rendered into
data-submit-pagination-targetand update the field named bydata-submit-page-fieldbefore re-submitting the same form.
Example: Search Form With Cards
<form data-form="partSearch"
action="api/parts/search/get"
method="get"
data-ajax-submit="true"
data-submit-target="#partResults"
data-submit-pagination-target="#partResultsPagination"
data-submit-page-field="page">
<input type="text" name="q" placeholder="Search...">
<select name="category_id">
<option value="">All categories</option>
</select>
<input type="hidden" name="page" value="1">
<input type="hidden" name="limit" value="20">
<button type="submit">Search</button>
</form>
<section id="partResults" class="hidden" data-class="hidden:!submitted" data-on-load="hydratePartResults">
<header>
<p data-if="hasData">
Showing <strong data-text="pagination.from"></strong>
-
<strong data-text="pagination.to"></strong>
of <strong data-text="meta.total"></strong>
</p>
</header>
<div class="grid" data-if="hasData">
<div data-for="item in data">
<template>
<article class="card">
<h3 data-text="item.name"></h3>
<p data-text="item.part_no"></p>
</article>
</template>
</div>
</div>
<p data-if="empty">No results</p>
</section>
<div id="partResultsPagination"></div>Expected API Response
{
"success": true,
"data": {
"data": [{"id": 1, "name": "Gear", "part_no": "GEAR-001"}],
"total": 23,
"page": 1,
"limit": 20,
"pages": 2
}
}Optional data-on-load hydration
function hydratePartResults(element, context) {
const rows = Array.isArray(context.data) ? context.data : [];
const meta = context.state?.meta || {};
console.log('rows', rows);
console.log('page', meta.page, 'of', meta.totalPages);
}JavaScript API
// Get form instance
const form = FormManager.getInstance(element);
// Submit programmatically
await form.submit();
// Reset form
form.reset();
// Set values
form.setValues({
name: 'John',
email: 'john@example.com'
});
// Get values
const data = form.getValues();
// Validate
const isValid = form.validate();Validation
HTML5 Validation
<input type="text" name="name" required minlength="2" maxlength="50">
<input type="email" name="email" required>
<input type="number" name="age" min="18" max="100">
<input type="url" name="website" pattern="https?://.+">Custom Validation
<input type="text" name="username"
data-validate="username"
data-validate-message="Username must be 3-20 characters">FormManager.addValidator('username', (value) => {
return /^[a-zA-Z0-9_]{3,20}$/.test(value);
});Async Validation
FormManager.addValidator('unique-email', async (value) => {
const response = await ApiService.get(`/api/check-email?email=${value}`);
return response.data.available;
});Events
| Event | When Triggered | Detail |
|---|---|---|
form:submit |
Form submitted | {form, data} |
form:success |
Submission success | {form, response} |
form:error |
Submission error | {form, error} |
form:validate |
Validation run | {form, valid} |
form:reset |
Form reset | {form} |
document.getElementById('my-form').addEventListener('form:success', (e) => {
console.log('Form submitted:', e.detail.response);
});API Reference
FormManager.getInstance(element)
Get form instance
FormManager.submit(element)
Submit form
FormManager.validate(element)
Validate form
Returns: boolean
FormManager.reset(element)
Reset form
FormManager.setValues(element, data)
Set form values
FormManager.getValues(element)
Get form values
Returns: Object
FormManager.addValidator(name, fn)
Add custom validator
Real-World Examples
Contact Form
<form data-component="form"
data-action="/api/contact"
data-method="POST"
data-success-message="Message sent successfully!"
data-success-reset="true">
<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>Message</label>
<textarea name="message" required minlength="10"></textarea>
</div>
<button type="submit">Send</button>
</form>Edit Form
<form data-component="form"
data-action="/api/users/{{id}}"
data-method="PUT"
data-success-redirect="/users"
data-confirm="Confirm save?">
<input type="hidden" name="id" value="{{id}}">
<input type="text" name="name" value="{{name}}">
<input type="email" name="email" value="{{email}}">
<button type="submit">Save</button>
</form>Search Form With Automatic Pagination
<form data-form="usersSearch"
action="/api/users/search"
method="get"
data-ajax-submit="true"
data-submit-target="#userResults"
data-submit-pagination-target="#userResultsPagination"
data-submit-page-field="page">
<input type="text" name="search" placeholder="Keyword">
<select name="status">
<option value="">All statuses</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
<input type="hidden" name="page" value="1">
<input type="hidden" name="limit" value="20">
<button type="submit">Filter</button>
</form>
<div id="userResults">
<div data-for="user in data">
<template>
<article>
<strong data-text="user.name"></strong>
</article>
</template>
</div>
</div>
<div id="userResultsPagination"></div>File Upload
<form data-component="form"
data-action="/api/upload"
data-enctype="multipart/form-data">
<input type="file" name="document"
accept=".pdf,.doc,.docx"
required>
<button type="submit">Upload</button>
</form>With Custom Handler
const form = document.getElementById('custom-form');
form.addEventListener('form:submit', async (e) => {
e.preventDefault();
const formData = FormManager.getValues(form);
// Custom processing
formData.processed = true;
try {
const response = await ApiService.post('/api/custom', formData);
NotificationManager.success('Success!');
} catch (error) {
NotificationManager.error(error.message);
}
});CSS for Validation
/* Invalid field */
.form-group.invalid input,
.form-group.invalid textarea,
.form-group.invalid select {
border-color: #ef4444;
}
/* Error message */
.form-group .error-message {
color: #ef4444;
font-size: 0.875rem;
margin-top: 4px;
}
/* Valid field */
.form-group.valid input {
border-color: #22c55e;
}
/* Loading state */
form.loading button[type="submit"] {
opacity: 0.7;
pointer-events: none;
}
form.loading button[type="submit"]::after {
content: ' ⏳';
}Common Pitfalls
⚠️ 1. Must Have name Attribute
<!-- ❌ Missing name -->
<input type="text" id="username">
<!-- ✅ Has name -->
<input type="text" name="username">⚠️ 2. Button type
<!-- ❌ Default can submit -->
<button>Click</button>
<!-- ✅ Specify type -->
<button type="submit">Submit</button>
<button type="button">Cancel</button>Related Documentation
- ElementManager - Form elements
- ApiService - API calls
- NotificationManager - Notifications