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

observer-form

v0.2.0

Published

Lightweight observer-pattern form library

Readme

observer-form

npm version bundle size license

A tiny, framework-agnostic form library built on the observer pattern.

Philosophy

Most form libraries are tightly coupled to a specific framework. React Hook Form needs React. Angular Reactive Forms need Angular. Formik, VeeValidate, and others each lock you into an ecosystem and ship kilobytes of runtime code to do it.

observer-form takes a different approach:

  • The observer pattern, not framework magic. Fields publish changes, observers subscribe. It is a well-understood design pattern that works the same way regardless of what renders your UI.
  • Zero runtime dependencies. Only TypeScript and Vite are used at build time. Nothing is shipped to the consumer beyond the library itself.
  • Native browser APIs. MutationObserver detects when inputs or forms leave the DOM. AbortController manages event listener lifecycles. Standard input events drive state updates. No polyfills, no abstraction layers.
  • Automatic cleanup. When an input is removed from the DOM, its event listeners and subscriptions are torn down automatically. When the entire form is removed, everything is cleaned up. You do not need to manage lifecycle manually.
  • Minimal surface area. One function (createForm) and one concept (observers). The entire library is ~250 lines of TypeScript with an ESM-only build, tree-shaking enabled, and sideEffects: false.

The result is a form state layer that weighs almost nothing, runs anywhere there is a DOM, and gets out of your way.

Installation

npm install observer-form
pnpm add observer-form
yarn add observer-form

Quick Start

import { createForm } from 'observer-form';

const form = createForm({
  initialValues: { name: '', email: '' },
  onSubmit: () => console.log('Submitted:', form.state),
  config: {},
});

// Register inputs by passing DOM elements
document.querySelectorAll('form input').forEach((input) => {
  form.registerField(input);
});

// Subscribe to field changes
form.subscribe('email', {
  update: ({ name, value }) => {
    console.log(`${name} changed to: ${value}`);
  },
});

// Read state at any time
console.log(form.state.email);

API Reference

createForm(options): FormApi

The single entry point. Creates a form instance and returns the API to interact with it.

import { createForm } from 'observer-form';
import type { FormOptions } from 'observer-form';

const form = createForm(options);

FormOptions

| Property | Type | Required | Description | |---|---|---|---| | initialValues | Record<string, any> | Yes | Default values for each field, keyed by field name. | | onSubmit | () => void | Yes | Callback invoked on form submission. | | config | {} | Yes | Configuration object for the form. | | validation | () => void | No | Validation function. | | triggerValidation | () => void | No | Manually trigger validation. | | formFields | Record<string, any> | No | Field-level configuration. | | formState | Record<string, any> | No | Additional form state. |

FormApi

The object returned by createForm.

state

form.state; // Record<string, any>

A plain object holding the current value of every registered field. Updated automatically on every input event. Read it at any time to get the latest values.

registerField(input: HTMLInputElement)

form.registerField(inputElement);

Binds a DOM input element to the form. The element's name attribute determines which key in state it maps to. Once registered:

  • The input event listener updates state[name] and notifies all subscribers for that field.
  • A MutationObserver watches for the element's removal from the DOM and cleans up automatically.

subscribe(fieldName: string, observer: Observer)

form.subscribe('email', {
  update: ({ name, value }) => {
    // called whenever the "email" field changes
  },
});

Adds an observer that is called every time the specified field changes. Multiple observers can subscribe to the same field. The same observer instance will not be added twice (internally stored in a Set).

unsubscribe(fieldName: string, observer: Observer)

form.unsubscribe('email', observer);

Removes a previously added observer for the specified field.

notify(data: NotifyData)

form.notify({ name: 'email', value: '[email protected]' });

Manually triggers all observers for a field. Useful for programmatic updates that bypass DOM events.

Types

type Observer = {
  update: (data: NotifyData) => void;
};

type NotifyData = {
  name: string;
  value: string;
};

Framework Examples

Since observer-form works with the DOM directly, it integrates with any framework. The pattern is always the same: get a reference to the DOM input, call registerField, and clean up when the component unmounts.

Vanilla JavaScript

<form id="signup">
  <input name="name" type="text" placeholder="Name" />
  <input name="email" type="email" placeholder="Email" />
  <button type="submit">Sign Up</button>
</form>

<p id="preview"></p>

<script type="module">
  import { createForm } from 'observer-form';

  const form = createForm({
    initialValues: { name: '', email: '' },
    onSubmit: () => console.log('Submitted:', form.state),
    config: {},
  });

  document.querySelectorAll('#signup input').forEach((input) => {
    form.registerField(input);
  });

  form.subscribe('name', {
    update: ({ value }) => {
      document.getElementById('preview').textContent = `Hello, ${value}`;
    },
  });
</script>

React

import { useEffect, useRef } from 'react';
import { createForm } from 'observer-form';

export default function SignupForm() {
  const nameRef = useRef(null);
  const emailRef = useRef(null);
  const formRef = useRef(null);

  useEffect(() => {
    const form = createForm({
      initialValues: { name: '', email: '' },
      onSubmit: () => console.log('Submitted:', form.state),
      config: {},
    });

    formRef.current = form;

    if (nameRef.current) form.registerField(nameRef.current);
    if (emailRef.current) form.registerField(emailRef.current);

    form.subscribe('name', {
      update: ({ value }) => console.log('Name:', value),
    });

    // Cleanup is automatic when inputs leave the DOM,
    // but you can also read form.state on unmount if needed.
  }, []);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Form state:', formRef.current?.state);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={nameRef} name="name" type="text" placeholder="Name" />
      <input ref={emailRef} name="email" type="email" placeholder="Email" />
      <button type="submit">Sign Up</button>
    </form>
  );
}

How It Works

observer-form is composed of four internal modules wired together by createForm:

registerField(input)
       |
       v
  input event fires
       |
       v
  state[fieldName] = value
       |
       v
  notifier.notify({ name, value })
       |
       v
  subscriberStore.get(name)
       |
       v
  observer.update({ name, value })  <-- your callback
  • SubscriberStore -- a Map<string, Set<Observer>> that holds per-field subscriptions. Deduplicates observers automatically.
  • Notifier -- looks up observers by field name and calls update() on each.
  • FieldRegistry -- binds each HTMLInputElement to state updates and notifications via the input event.
  • CleanupManager -- uses AbortController to tear down event listeners and MutationObserver to detect when inputs or the form are removed from the DOM, cleaning up everything automatically.

License

MIT