Forms Poll

Poll

Declarative periodic polling for server-driven status changes. Add data-wf-poll to any element and the component fires a fetch on a configurable interval, injects response HTML into a target element, shows a WojoToast when the server sends one, and stops automatically when a terminal status is detected — no JavaScript required. Common uses: crypto payment confirmation, queue job progress, email verification, and live notification checks.

Basic Polling

The minimal setup is two attributes: data-wf-poll and data-wf-url. The component begins polling immediately on page load (every 3 s by default) and updates the data-wf-parent element with the html field from each response. Use data-wf-interval to change the cadence.

Waiting for first ping…
HTML
<!-- Polls /api/status every 3 s; injects data.html into #status-output -->
<div data-wf-poll
     data-wf-url="/api/status"
     data-wf-action="pollHeartbeat"
     data-wf-interval="3000"
     data-wf-parent="#status-output"
     data-wf-complete="innerHTML">
</div>

<div id="status-output">Waiting for first ping…</div>
Server Response
// Minimal response — only html is required
{ "html": "<span>Last ping: 14:23:45 · Tick #12</span>" }

// With a toast notification on each tick
{ "type": "info", "message": "3 new messages", "html": "<span>3</span>" }

// Append new content instead of replacing (data-wf-complete="append")
{ "html": "<li>New log entry 14:23:45</li>" }
PHP
case 'pollHeartbeat':
    if (session_status() === PHP_SESSION_NONE) session_start();
    $tick = ($_SESSION['poll_tick'] ?? 0) + 1;
    $_SESSION['poll_tick'] = $tick;
    json_out([
        'html' => '<span>Last ping: ' . date('H:i:s') . ' · Tick #' . $tick . '</span>',
    ]);

Status & Stop

Use data-wf-stop-on to declare a comma-separated list of terminal status values. On each tick the component reads the data-wf-stop-field key from the response (default: status ) and stops polling when it matches. Pair with data-wf-redirect-url and data-wf-redirect-on to redirect only on a specific terminal status (e.g. redirect on confirmed but not on expired ). The demo below cycles through pending → confirmed over five ticks.

Waiting for Payment Send USDC to the wallet address shown above. Tick 0/5.
HTML
<!--
  Polls every 2 s.
  Stops when data.status is "confirmed" or "expired".
  Redirects to /membership only if status was "confirmed".
  Waits 3 s before the redirect to let the user read the confirmation.
-->
<div data-wf-poll
     data-wf-url="/crypto-payment/status"
     data-wf-params='{"order_id":"<?php echo addslashes($order_id); ?>"}'
     data-wf-interval="2000"
     data-wf-stop-on="confirmed,expired"
     data-wf-redirect-on="confirmed"
     data-wf-redirect-url="/membership"
     data-wf-redirect-delay="3000"
     data-wf-parent="#payment-status"
     data-wf-complete="innerHTML">
</div>

<div id="payment-status">Waiting for payment…</div>
Server Response (GET/POST)
// Pending — no match on stop-on, polling continues
{ "status": "pending", "html": "<div class='…'>Waiting for confirmations…</div>" }

// Confirmed — matches stop-on; polling stops; redirect fires after 3 s
{ "status": "confirmed", "type": "positive", "message": "Payment confirmed!", "html": "…" }

// Expired — matches stop-on; polling stops; no redirect (not in redirect-on)
{ "status": "expired", "type": "negative", "message": "Payment expired.", "html": "…" }

// The server can also override the redirect URL at runtime
{ "status": "confirmed", "redirect": "/custom-success-page", "html": "…" }
PHP
case 'checkPayment':
    $orderId = trim($_GET['order_id'] ?? '');
    $payment = Payment::find($orderId);

    if (!$payment) {
        json_out(['status' => 'expired', 'type' => 'negative', 'message' => 'Order not found.']);
    }

    $html = match ($payment->status) {
        'confirmed' => render_confirmed_html($payment),
        'pending'   => render_pending_html($payment),
        default     => render_expired_html($payment),
    };

    json_out([
        'status'  => $payment->status,
        'html'    => $html,
        'type'    => $payment->status === 'confirmed' ? 'positive' : null,
        'message' => $payment->status === 'confirmed' ? 'Payment confirmed!' : null,
    ]);

Timeout & Countdown

Set data-wf-timeout (in milliseconds) to auto-stop polling after a maximum duration regardless of the response status. Pair it with data-wf-timer="#el" to display a live M:SS countdown in any element. The demo below polls for 20 seconds.

Expires in
0:20
Waiting…
HTML
<!-- Polls every 3 s; auto-stops after 30 minutes (1 800 000 ms) -->
<div data-wf-poll
     data-wf-url="/api/status"
     data-wf-interval="3000"
     data-wf-timeout="1800000"
     data-wf-stop-on="confirmed,expired"
     data-wf-parent="#status-output"
     data-wf-timer="#countdown">
</div>

<!-- Countdown display — updated every second by the component -->
<span id="countdown">30:00</span>
<div id="status-output"></div>

<!--
  The component writes M:SS to #countdown every second.
  When the timeout elapses, polling stops and wf:poll-stop fires with reason "timeout".
  Listen for it to show an "expired" message if needed:
-->
<script>
  document.querySelector('[data-wf-poll]').addEventListener('wf:poll-stop', e => {
    if (e.detail.reason === 'timeout') {
      document.getElementById('status-output').innerHTML =
        '<p>Session expired. <a href="/retry">Start a new request</a></p>';
    }
  });
</script>

Data Attributes

Attribute Default Description
data-wf-poll Required. Presence marker. Activates the component on the element.
data-wf-url="/path" Required. The URL to poll.
data-wf-interval="3000" 3000, or "3s" Time between poll requests. Accepts ms (3000), s (3s), m (1m), h (1.5h). Default: 3000.
data-wf-timeout="0" 0 Total polling duration before auto-stop. Same units (ms/s/m/h). 0 = no limit. Default: 0. 0 means no limit.
data-wf-method="GET" GET HTTP method. GET appends params to the query string; POST sends them as application/x-www-form-urlencoded.
data-wf-action="name" Optional action key merged into the request. Useful for routing on the server side.
data-wf-params='{"k":"v"}' JSON object merged into the GET query string or POST body on every tick. Use this for static identifiers like order_id or job_id.
data-wf-stop-on="a,b" Comma-separated values of data[stopField] that stop polling when matched. Example: confirmed,expired,failed.
data-wf-stop-field="status" status Response key checked against data-wf-stop-on. Change this if your endpoint uses a different field name.
data-wf-redirect-on="a" Subset of stop-on values that also trigger a redirect. Omit to redirect on any stop-on match. Example: confirmed (redirect on success but not on expiry).
data-wf-redirect-url="/path" Redirect destination. Overridden at runtime by a redirect field in the server response.
data-wf-redirect-delay="0" 0 Milliseconds to wait before the redirect fires. Useful to let the user read a confirmation message.
data-wf-parent="#el" CSS selector of the element to update with data.html on each successful tick.
data-wf-complete="innerHTML" innerHTML How to inject data.html into the parent: innerHTML replaces inner content, append adds to the end, prepend adds to the beginning.
data-wf-timer="#el" CSS selector of a countdown display element. The component writes the remaining time as M:SS every second. Requires data-wf-timeout to be set.
data-wf-csrf-header X-CSRF-Token CSRF header name. Token is read from <meta name="csrf-token"> and sent on every request.
data-wf-auto-start="false" true Set to false to prevent automatic polling on page load. Use the public API to start polling when ready.

Events & API

All events bubble and are cancelable. They are dispatched on the [data-wf-poll] element. Each instance also exposes a public API via el._wfPoll.

Event When Detail
wf:poll-start Polling begins (on .start() or autoStart ) { interval, timeout }
wf:poll-tick Each successful response received { data }
wf:poll-stop Polling stopped (any reason) { reason: 'manual'|'timeout'|'status', status? }
wf:poll-error Network or JSON parse error on a tick { error }
Public API
const poller = document.querySelector('[data-wf-poll]')._wfPoll

// Start polling (idempotent — safe to call when already running)
poller.start()

// Stop polling immediately
poller.stop()

// Tear down completely (removes listeners, clears timers)
poller.destroy()

// Listen for events
poller.el.addEventListener('wf:poll-tick', e => {
  console.log('Tick data:', e.detail.data)
})

poller.el.addEventListener('wf:poll-stop', e => {
  if (e.detail.reason === 'status') {
    console.log('Stopped on status:', e.detail.status)
  } else if (e.detail.reason === 'timeout') {
    console.log('Timed out')
  }
})

poller.el.addEventListener('wf:poll-error', e => {
  console.error('Poll error:', e.detail.error)
})
Manual Start (data-wf-auto-start="false")
<!-- Does not start automatically -->
<div id="my-poll"
     data-wf-poll
     data-wf-url="/api/status"
     data-wf-auto-start="false"
     data-wf-parent="#status">
</div>

<button type="button" onclick="document.getElementById('my-poll')._wfPoll.start()">
  Start Polling
</button>
<button type="button" onclick="document.getElementById('my-poll')._wfPoll.stop()">
  Stop
</button>