Now.js Framework Documentation

Now.js Framework Documentation

FormManager

EN 15 Apr 2026 12:31

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

  1. FormManager reads the fields listed in data-watch-fields.
  2. When one of the fields in data-watch-trigger changes, FormManager calls data-watch-api. If data-watch-on-load is not set to false, the watched API is also called once after the initial load payload has been applied.
  3. The response payload is merged back into the form state with setFormData().
  4. Existing bindings such as data-text, data-attr, data-if, and data-for update 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

  1. The form submits via AJAX.
  2. FormManager normalizes the success payload into a canonical schema similar to TableManager:
{
  data: [...],
  meta: {
    page: 1,
    pageSize: 20,
    total: 23,
    totalPages: 2
  },
  filters: {},
  options: {}
}
  1. The full normalized payload is exposed on context.state.
  2. The primary data source is exposed on context.data.
  3. Pagination buttons are rendered into data-submit-pagination-target and update the field named by data-submit-page-field before 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>