<!-- 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>
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.
// 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>" }
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.
<!--
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>
// 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": "…" }
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.
<!-- 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 } |
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)
})
<!-- 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>