npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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:

  1. The browser default navigation is suppressed.
  2. A fetch() request is made that mirrors what the browser would have sent (method, encoding, body, query string).
  3. 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.
  4. Inside that function, jsFormWatcherContext gives you access to the form, the submitter, and an unblock() callback.

Sample server response body:

document.getElementById('result').textContent = 'Saved!';
jsFormWatcherContext.unblock();

Installation

npm install @qnc/js_form_watcher

Peer 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:3052

Manual test pages are served from manual_tests/ and cover all duplicate submission modes, timeout handling, and invalid response handling.