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

formodel

v0.2.0

Published

Typed, model-seeded fluent builder that turns a plain data model into a strongly-typed UI form config. Framework-agnostic TypeScript — no runtime dependencies.

Readme

formodel

Typed, framework-agnostic form configuration for TypeScript. Describe your form as a plain data model, and formodel's fluent builder hands back a strongly-typed, serializable config you can render with React, Vue, Angular, Svelte, or vanilla JavaScript. ✨

formodel is a tiny, zero-dependency TypeScript library for building type-safe UI form configs from a data model. You configure each field once; the builder infers every control's node type and removes that field from the next step — so a form is impossible to misconfigure. The result is plain data your renderer can walk in a single loop.

Features

  • 🔒 Type-safe by construction — the builder's methods are your model's keys; each field is set exactly once, with per-field types inferred.
  • 🧩 Framework-agnostic — the config is plain data, so it renders in React, Vue, Angular, Svelte, Solid, or the bare DOM.
  • 📦 Zero dependencies — small, tree-shakeable ESM that ships its own type definitions.
  • 🧠 Smart field inference — text, number, email, password, date, select, radio, checkbox, file, nested groups, and arrays.
  • ✍️ Terse or explicit — concise control helpers (text(), select(), array()) or plain node objects, your call.
  • 🗂️ First-class arrays — repeating groups with add/remove/replace/sort, file uploads, and multi-selects, each typed to the element.
  • 🪶 Works in plain JavaScript — full type-checking and autocomplete via // @ts-check + JSDoc, no build step required.
  • 🌿 Immutable branchingfork() for isolated form variants from a shared base.

Why formodel?

Most form code tangles three things together: the data shape, how each field should look, and how it gets rendered. formodel pulls them apart.

  • One typed source of truth. Your form's structure and metadata live in a single object, derived straight from your data model. Change the model, and the types tell you what to update.
  • Your renderer is just a loop. The output is plain data — { firstName: { type: 'text', label: 'First name' }, … }. Walk it and render however you like. The same config drives React today and vanilla JS tomorrow.
  • You can't misconfigure it. Configure a key and it disappears from the next step — so you can't forget a field or set it twice, and each field's node type is inferred (a select wants options, an array wants an item config, and so on).
  • No lock-in, no weight. Pure TypeScript, zero dependencies, tree-shakeable.

Install

pnpm add formodel
# or: npm i formodel / yarn add formodel

Quick start — build a typed form config

import { UiFormConfigBuilder } from 'formodel';

export const signupConfig = UiFormConfigBuilder.from({
  firstName: '',
  email: '',
  role: '',
  subscribe: false,
})
  .firstName({ type: 'text', label: 'First name' })
  .email({ type: 'email', label: 'Email' })
  .role({
    type: 'select',
    label: 'Role',
    options: [
      { value: 'admin', description: 'Admin' },
      { value: 'user', description: 'User' },
    ],
  })
  .subscribe({ type: 'checkbox', label: 'Subscribe to the newsletter', value: true });

// signupConfig is typed as UiFormGroupConfig<typeof model>

When the last field is set you get the finished config back automatically — no .build(), no guesswork. 🎯

Less typing: control helpers

Rather not spell out { type, label } every time? Import the matching helper — text, textarea, number, email, password, phone, url, date, file, select, multiselect, radio, checkbox, array:

import { UiFormConfigBuilder, text, email, phone, number, select, checkbox } from 'formodel';

const config = UiFormConfigBuilder.from({ firstName: '', email: '', phone: '', age: 0, subscribe: false })
  .firstName(text('First name', { placeholder: 'Jane' }))
  .email(email('Email'))
  .phone(phone('Phone'))
  .age(number('Age', { min: 0, max: 120 }))
  .subscribe(checkbox('Subscribe to the newsletter'));

Each helper takes the label first and an optional second argument for the extras (placeholder, min / max, accept, …). select / multiselect / radio take their options, and array(itemConfig, { addable, removable }) builds a repeating group. They return the same typed nodes as the plain object form — use whichever reads better.

Custom field kinds

The built-in kinds cover ordinary fields. For a widget your renderer knows about — a device picker, a company-lookup field — customInputType<Value, Props>(kind) mints a typed helper used just like text()/select(). The node is value-matched: it only type-checks on a field whose value is assignable to Value, so you can't put a Device[] picker on a string field.

import { customInputType } from 'formodel';

// declare once: value type Device[], props { max?: number }
const devicePicker = customInputType<Device[], { max?: number }>('devicePicker');

const config = UiFormConfigBuilder.from({ devices: [] as Device[] })
  .devices(devicePicker({ max: 5 })); // → { type: 'devicePicker', props: { max: 5 } }

The node carries type (the kind your renderer resolves) and a typed props bag; a control with no config takes no props (customInputType<string>('nip')nip()). It's the same plain-data output as every other node, so a renderer can map it like any control.

Render a form in React

The config is just an object keyed by field name, so a generic renderer is a map over its entries:

import { signupConfig } from './signup-config';
import type { UiFormControlNode } from 'formodel';

function Field({ name, node }: { name: string; node: UiFormControlNode }) {
  if (node.type === 'select') {
    return (
      <label>
        {node.label}
        <select name={name}>
          {node.options.map((o) => (
            <option key={String(o.value)} value={String(o.value)}>
              {o.description}
            </option>
          ))}
        </select>
      </label>
    );
  }

  if (node.type === 'textarea') {
    return (
      <label>
        {node.label}
        <textarea name={name} />
      </label>
    );
  }

  if (node.type === 'checkbox') {
    return (
      <label>
        <input type="checkbox" name={name} /> {node.label}
      </label>
    );
  }

  // text, email, password, number, tel, url, date, file… map 1:1 to <input type>
  return (
    <label>
      {node.label}
      <input type={node.type} name={name} placeholder={node.placeholder} />
    </label>
  );
}

export function SignupForm() {
  return (
    <form>
      {Object.entries(signupConfig).map(([name, node]) => (
        <Field key={name} name={name} node={node} />
      ))}
    </form>
  );
}

Because the control type values line up with HTML input types, the common fields need no special-casing at all.

Render a form in vanilla JavaScript

No framework, no build step — and still fully typed. Add // @ts-check and pull in formodel's types with a JSDoc import(), and your editor type-checks plain JavaScript:

// @ts-check
import { UiFormConfigBuilder } from 'formodel';

const config = UiFormConfigBuilder.from({ firstName: '', email: '' })
  .firstName({ type: 'text', label: 'First name' })
  .email({ type: 'email', label: 'Email' });

/**
 * @param {string} name
 * @param {import('formodel').UiFormControlNode} node
 * @returns {HTMLLabelElement}
 */
function createField(name, node) {
  const label = document.createElement('label');
  label.textContent = node.label ?? name;

  const input = document.createElement('input');
  input.type = node.type; // 'text', 'email', … — checked against the node type
  input.name = name;

  label.append(input);
  return label;
}

const form = document.createElement('form');

// `config` is inferred, so `node` is a UiFormControlNode here — no annotation needed
for (const [name, node] of Object.entries(config)) {
  form.append(createField(name, node));
}

document.body.append(form);

More runnable, type-checked samples (React, vanilla JS, arrays, fork) live in examples/.

How fields map

  • Scalar keys take a UiFormControlNodetext, textarea, number, email, password, tel, url, select, date, radio, checkbox, file.
  • Array keys take an ArrayFormControlNode<Element>, typed to the element. Arrays are first-class:
    • a repeating group (type: 'array') with an itemConfig and addable / removable / replaceable / sortable actions plus minItems / maxItems,
    • a multi-select (type: 'select', multiple: true),
    • or a file upload (type: 'file', with accept / multiple / maxFiles) for attachment lists.
  • Object keys take (meta, childBuilder) and nest into a group.
  • Date fields map to a date control.
  • null and function fields are skipped — at runtime and in the type.

Branching with fork

Need a couple of variants from a shared base? fork gives you an isolated snapshot — branches never step on each other:

const base = UiFormConfigBuilder.from({ name: '', role: '' })
  .name({ type: 'text', label: 'Name' });

const admin = UiFormConfigBuilder.fork(base).role({ type: 'text', label: 'Admin role' });
const guest = UiFormConfigBuilder.fork(base).role({ type: 'text', label: 'Guest role' });

API

  • UiFormConfigBuilder.from(model) — start a builder whose methods are model's keys.
  • UiFormConfigBuilder.fork(builder) — branch an in-progress builder into an isolated copy.
  • Types: UiFormControlNode, ArrayFormControlNode, UiFormGroupConfig, UiFormGroupNode, and each control node (TextFormControlNode, SelectFormControlNode, FileFormControlNode, …).

Develop

pnpm install
pnpm test            # runtime specs (Vitest)
pnpm test:types      # type-level assertions (vitest --typecheck)
pnpm build           # emit dist/ (ESM + .d.ts)
pnpm examples:check  # type-check the examples against the public API

License

0BSD © dorian_dev