Forms Ajax Forms

Ajax Forms

Converts any HTML form into a fetch-based async submission with zero configuration. Add data-wf-ajax-form to your <form> element and the component handles body serialization, file detection, CSRF tokens, loading state, and success or error events automatically.

Basic

Add data-wf-ajax-form to any <form> . The component auto-inits on DOMContentLoaded and intercepts the submitted event. Listen for wf:success and wf:error events on the form element to handle the server response. The demo endpoint returns a JSON object with type , title , and message fields.

// Submit the form to see the response…
HTML
<form class="wf-form" data-wf-ajax-form action="/api/contact" method="POST">
  <div class="wf-field">
    <label for="name">Full name</label>
    <input class="wf-input" type="text" name="name" id="name" placeholder="Alice Johnson">
  </div>
  <div class="wf-field">
    <label for="email">Email address</label>
    <input class="wf-input" type="email" name="email" id="email" placeholder="alice@example.com">
  </div>
  <div class="wf-field">
    <label for="message">Message</label>
    <textarea class="wf-textarea" name="message" id="message"></textarea>
  </div>
  <button class="wf-btn wf-btn-primary" type="submit">Send</button>
</form>

Handling the response

Listen for wf:success and wf:error directly on the form element. Both events bubble.

JS
document.addEventListener('DOMContentLoaded', () => {
  const form = document.querySelector('[data-wf-ajax-form]')

  form.addEventListener('wf:success', e => {
    const { data, response } = e.detail
    // data = parsed JSON body (or text/blob depending on responseType)
    console.log(data.type, data.title, data.message)
  })

  form.addEventListener('wf:error', e => {
    const { error, response } = e.detail
    // response is null on a network-level failure
    console.error(error.message, response?.status)
  })

  form.addEventListener('wf:after-submit', e => {
    console.log('Request settled, success:', e.detail.success)
  })
})

Loader State

By default, submit buttons are disabled while the request is pending. Use data-wf-form-loader-class to add a loading CSS class to a target element while the request is in flight, and data-wf-form-loader-target to control which element receives it. The class is removed automatically when the request settles.

Loading class on a Submit button

The default target is the clicked Submit button. Set data-wf-form-loader-target="self" to be explicit.

HTML
<form data-wf-ajax-form
      action="/api/contact"
      method="POST"
      data-wf-form-loader-class="wf-button-loading"
      data-wf-reset-on-success>
  …
  <button class="wf-btn wf-btn-primary" type="submit">Submit</button>
</form>

Custom loader target

Point data-wf-form-loader-target at a CSS selector to load a spinner on a different element — such as the form's parent container.

HTML
<!-- Apply loader class to a parent element -->
<form data-wf-ajax-form
      action="/api/contact"
      method="POST"
      data-wf-form-loader-class="is-loading"
      data-wf-form-loader-target="parent">
  …
</form>

<!-- Apply loader class to a specific element -->
<form data-wf-ajax-form
      action="/api/contact"
      method="POST"
      data-wf-form-loader-class="wf-button-loading"
      data-wf-form-loader-target="#my-spinner">
  …
</form>

Callbacks & Options

For full programmatic control, instantiate WojoAjaxForm directly instead of relying on auto-init. Pass an options object with callback functions that mirror the event API. Return false from beforeSubmit to cancel the request.

Programmatic initialization

Callbacks and the event API can be used together — both fire for every request lifecycle step.

JS
import { WojoAjaxForm } from '/assets/js/wf-ajax-form.js'

document.addEventListener('DOMContentLoaded', () => {
  const el = document.getElementById('my-form')

  const form = new WojoAjaxForm(el, {
    // ── Request options ──────────────────────────────
    url:            '/api/contact',     // override form[action]
    method:         'POST',             // override form[method]
    responseType:   'json',             // 'json' | 'text' | 'blob'
    timeout:        8000,               // abort after 8 s (0 = disabled)
    disableOnSubmit: true,              // disable submit buttons while pending
    resetOnSuccess:  true,              // clear fields on 2xx response
    csrfHeader:     'X-CSRF-Token',     // '' to disable

    // ── Loading class ────────────────────────────────
    formLoaderClass:  'wf-button-loading',
    formLoaderTarget: 'self',           // 'self' | 'parent' | CSS selector

    // ── Callbacks ────────────────────────────────────
    beforeSubmit(formEl, detail) {
      // Return false to cancel the request
      if (!formEl.querySelector('[name=agree]').checked) return false
    },

    success(data, formEl, response) {
      console.log('Success:', data)
    },

    error(err, formEl, response) {
      console.error('Error:', err.message, response?.status)
    },

    afterSubmit(formEl, { success }) {
      console.log('Settled. Success:', success)
    },
  }).init()

  // Trigger programmatically (e.g. from another button)
  document.getElementById('trigger-btn').addEventListener('click', () => form.submit())
})

Timeout & abort

Set data-wf-timeout in milliseconds to automatically abort a request that takes too long. Call form.abort() to cancel the current request programmatically.

HTML
<!-- Abort if no response within 8 seconds -->
<form data-wf-ajax-form action="/api/contact" method="POST"
      data-wf-timeout="8000">
  …
</form>

CSRF token

The component reads a CSRF token from a <meta name="csrf-token"> tag and sends it as a request header. The default header name is X-CSRF-Token (compatible with Laravel and Rails). Set data-wf-csrf-header to use a different name, or an empty string to disable.

HTML
<!-- Place in <head> — the component reads this automatically -->
<meta name="csrf-token" content="<?= $csrfToken ?>">

<!-- Custom header name -->
<form data-wf-ajax-form action="/api/contact" method="POST"
      data-wf-csrf-header="X-My-Token">
  …
</form>

<!-- Disable CSRF header -->
<form data-wf-ajax-form action="/api/contact" method="POST"
      data-wf-csrf-header="">
  …
</form>

Master Form

Import wf-master-form.js once at the application level to enable the data-wf-submit pattern. A delegated click listener intercepts all matching buttons in the document — including buttons inside modals — POSTs the nearest parent form, and shows the server response as a WojoToast automatically. No per-element initialization is needed.

Button markup

The form element itself needs no attribute. The button drives the submission via its data attributes. Use data-wf-hide to fade out and hide the form on success.

HTML
<form class="wf-form">
  <!-- … fields … -->
  <button type="button"
          data-wf-submit
          data-wf-route="/api/contact"
          data-wf-form-action="send"
          data-wf-reset
          class="wf-btn wf-btn-primary">Send</button>
</form>

<!-- data-wf-hide: fade out and hide the form on success -->
<button type="button" data-wf-submit data-wf-route="/api/contact"
        data-wf-hide class="wf-btn wf-btn-primary">Send</button>

Response shape

The server must return JSON. A fields array is rendered as a wf-list inside the toast. A redirect string navigates after a short delay.

JSON
// Success — positive toast
{ "type": "positive", "title": "Sent!", "message": "We'll be in touch." }

// Validation error — fields list rendered inside the toast
{ "type": "negative", "title": "Missing fields", "message": "Please fill in:",
  "fields": ["Full name", "Valid email address"] }

// Redirect — navigates after 1.5 s
{ "type": "positive", "message": "Saved.", "redirect": "/dashboard" }

Inside a Modal

The data-wf-submit button works inside a WojoModal with no extra wiring. When the button lives in the modal footer — outside the <form> element — the master form listener walks up to the nearest .wf-modal__panel or <dialog> and queries down for the first form automatically. Use the declarative data-wf-modal API for trigger and close, and place the form inside the dialog body.

HTML
<!-- Trigger -->
<button type="button" class="wf-btn wf-btn-primary" data-wf-open="dlg-contact">
  Open contact form in modal
</button>

<!-- Modal — data-wf-modal enables declarative auto-init -->
<dialog id="dlg-contact" class="wf-modal"
        data-wf-modal
        data-wf-label="Contact us"
        data-wf-size="sm"
        data-wf-light-dismiss>
  <form id="contact-form" class="wf-form">
    <div class="wf-fields">
      <div class="wf-field">
        <label>Full name</label>
        <input class="wf-input" name="name" type="text" placeholder="Alice Johnson">
      </div>
      <div class="wf-field">
        <label>Email</label>
        <input class="wf-input" name="email" type="email" placeholder="alice@example.com">
      </div>
    </div>
    <div class="wf-field">
      <label>Message</label>
      <textarea class="wf-textarea" name="message"></textarea>
    </div>
  </form>
  <div data-wf-slot="footer">
    <button type="button" class="wf-btn" data-wf-close>Cancel</button>
    <button type="button"
            data-wf-submit
            data-wf-route="/api/contact"
            data-wf-form-action="send"
            data-wf-reset
            class="wf-btn wf-btn-primary">Send</button>
  </div>
</dialog>

Data Attributes

Attribute Default Description
data-wf-ajax-form Required. Marks the form element for auto-init on DOMContentLoaded .
data-wf-url form[action] Override the form's action URL for the fetch request.
data-wf-method form[method] Override the form's method . Accepts POST , PUT , PATCH , DELETE , GET .
data-wf-response-type json How to parse the response body. Accepts json , text , or blob .
data-wf-timeout 0 Abort the request after N milliseconds. 0 disables the timeout.
data-wf-disable-on-submit true Disable all [type=submit] elements inside the form while the request is pending.
data-wf-reset-on-success false Call form.reset() automatically after a 2xx response.
data-wf-csrf-header X-CSRF-Token Header name used to send the CSRF token read from <meta name="csrf-token"> . Set to an empty string to disable.
data-wf-form-loader-class wf-button-loading CSS class is added to the loader target element while the request is in flight.
data-wf-form-loader-target submit trigger Element that receives the loader class. Accepts a CSS selector, self (the Submit button), or parent (its parent element). Defaults to the clicked Submit button.

Events & API

Event Cancelable detail Description
wf:submit Yes { url, method, body } Fires before the fetch request. Call e.preventDefault() to cancel.
wf:success No { data, response } Fires on a 2xx HTTP response. data is the parsed response body.
wf:error No { error, response } Fires on a non-2xx response or a network error. response is null on network failure.
wf:after-submit No { success } Always fires after the request settles, whether it succeeded or failed.

Programmatic API

The instance exposes three methods. All are safe to call at any time.

JS
import { WojoAjaxForm } from '/assets/js/wf-ajax-form.js'

const form = new WojoAjaxForm(el, options).init()

form.submit()    // Trigger submission programmatically (as if user clicked submit)
form.abort()     // Cancel the current in-flight fetch request
form.destroy()   // Abort any pending request and remove all event listeners

// Access an auto-initialised instance
const instance = document.querySelector('form')._wfAjaxForm
instance.abort()

// Prevent submission from an event listener
form.el.addEventListener('wf:submit', e => {
  if (!isValid()) e.preventDefault()
})