@qnc/js_form_watcher
v2.0.1
Published
Intercepts HTML form submissions, sends them via fetch(), and executes the text/javascript response.
Readme
@qnc/js_form_watcher
Intercepts HTML form submissions, sends them via fetch(), and executes the text/javascript response body as a function.
How it works
When a form inside the watched container is submitted:
- The browser default navigation is suppressed.
- A
fetch()request is made that mirrors what the browser would have sent (method, encoding, body, query string). - The response must have content-type
text/javascript. Its body is executed as a function with the signature(jsFormWatcherContext: SubmissionContext) => void. The HTTP status code is not checked. - Inside that function,
jsFormWatcherContextgives you access to theform, thesubmitter, and anunblock()callback.
Sample server response body:
document.getElementById('result').textContent = 'Saved!';
jsFormWatcherContext.unblock();Installation
npm install @qnc/js_form_watcherPeer dependency: @qnc/type_utils ^1.3.0.
Declarative Usage — custom element
The simplest approach. Drop <qnc-js-form-watcher> around your form(s):
<script src="jsFormWatcher.js" type="module"></script>
<qnc-js-form-watcher data-duplicate-submission-mode="inert" data-timeout-ms="10000">
<form method="POST" action="/save">
<input name="title" />
<button type="submit">Save</button>
</form>
</qnc-js-form-watcher>The element creates a JsFormWatcher when connected to the DOM and destroys it when disconnected. Changing data-duplicate-submission-mode or data-timeout-ms automatically recreates the watcher.
Attributes
| Attribute | Default | Description |
|---|---|---|
| data-duplicate-submission-mode | "inert" | How to handle a new submission while a previous one is still in flight. See Duplicate submission modes. |
| data-timeout-ms | 86400000 (1 day) | Abort the request after this many milliseconds and fire jsformwatcher:submissionerror. |
Error Handling
On any error, we dispatch a cancelable, bubbling jsformwatcher:submissionerror CustomEvent.
The default action of this event is to alert() a simple message to the user. Call event.preventDefault() to suppress this alert.
The event contains information about the submission context and the error in event.detail. This conforms to the SubmissionError (details below). In particular, you will need to call event.detail.context.unblock() if you want to unblock the form after an error.
Sample:
<qnc-js-form-watcher
id="watcher"
data-duplicate-submission-mode="inert"
data-timeout-ms="5000"
>
<form method="POST" action="/save">…</form>
</qnc-js-form-watcher>
<script type="module">
document.querySelector('#watcher').addEventListener(
'jsformwatcher:submissionerror',
event => {
event.preventDefault(); // suppress the default alert
event.detail.context.unblock(); // allow resubmission
showToast('Something went wrong — please try again.');
}
);
</script>Programmatic Usage — JsFormWatcher class
For full control, instantiate JsFormWatcher directly. See js_form_watcher.d.ts for details.
Sample:
import { JsFormWatcher } from '@qnc/js_form_watcher';
const watcher = new JsFormWatcher({
container: document.getElementById('my-container'),
duplicateSubmissionMode: 'inert',
timeoutMs: 10_000,
onError: (error) => {
console.error('Form submission error', error.type);
error.context.unblock();
},
});Duplicate submission modes
Controls what happens with the form after the first submit event.
"inert" (default)
Whenever a submit event is handled we add the inert attribute on the container, making all descendant elements completely non-interactive (not clickable, not editable). Further submit events (if the user can even manage to trigger one) are ignored until the response function or error handler calls unblock().
Browsers prevent user-interaction with inert elements, but they don't style them differently in any way. We recommend something like this in your site stylesheet:
qnc-js-form-watcher[inert] { opacity: 0.4; }"block"
Like "inert", further submissions are silently ignored. However, instead of the inert attribute, a data-blocked attribute is set on the container. Descendants remain interactive (focusable, editable) while a submission is in-flight.
Use this option when you want the user to be able to interact with (but not submit) the form while a submission is pending.
You should make it clear to the user that they cannot submit the form while a submission is pending. Eg:
qnc-js-form-watcher[data-blocked] .submit-button { opacity: 0.4; }"replace"
Submissions are never blocked.
Whenever a new submission arrives, the previous in-flight request is aborted and replaced by the new one. Only the response from the most recent submission is executed. The unblock callback passed to the response function is a no-op.
SubmissionContext
Passed to both the response function (as jsFormWatcherContext) and to all error callbacks.
type SubmissionContext = {
/** The form element that was submitted. */
form: HTMLFormElement;
/** The element that triggered the submission (same as SubmitEvent.submitter). */
submitter: HTMLElement | null;
/** Removes the blocked/inert state and allows further submissions.
Has no effect when duplicateSubmissionMode is "replace". */
unblock: () => void;
};SubmissionError
Passed to onError (and as event.detail for jsformwatcher:submissionerror).
type SubmissionError =
| { context: SubmissionContext; type: "TIMEOUT" }
| { context: SubmissionContext; type: "NETWORK_ERROR" }
| { context: SubmissionContext; type: "INVALID_RESPONSE"; response: Response };"INVALID_RESPONSE" is used when a response was received but its content-type is not text/javascript, or its body could not be parsed as a JavaScript function.
Request encoding
The fetch request mirrors standard browser form submission. The body of the request is influenced by the form's enctype attribute.
File inputs are only included in the request for multipart/form-data. For GET/HEAD and application/x-www-form-urlencoded, File entries are silently skipped (matching browser behaviour).
Development
npm install
npm start # starts the dev server + esbuild watch on http://localhost:3052Manual test pages are served from manual_tests/ and cover all duplicate submission modes, timeout handling, and invalid response handling.
