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

@alsocoder/apna-form

v0.1.2

Published

React form layer with React Hook Form, Zod validation, and connected Apna field components.

Readme

@alsocoder/apna-form

React form layer built on React Hook Form with optional Zod validation and connected Apna field components.

Features

  • ApnaForm with Zod schema validation
  • Connected fields: Input, Textarea, OTP, Select, DatePicker, DateRange, DateTime, Editor, Upload, Media (optional)
  • Field arrays (ApnaFormFieldArray)
  • Conditional fields (ApnaFormWatch, showWhen)
  • Multi-step forms (ApnaFormSteps)
  • Auto-save drafts (enableAutoSave + onAutoSave)
  • Dirty state (ApnaFormStatus, useApnaForm)
  • Async validation (Zod .refine())
  • JSON form builder (ApnaFormBuilder) with smart responsive grid
  • Native HTML <form> submit support
  • React Hook Form compatible (useApnaForm, ApnaFormField)

Install

npm install @alsocoder/apna-form react-hook-form zod @hookform/resolvers

Peer dependencies: react, react-dom, react-hook-form.

@alsocoder/apna-form bundles these as dependencies — you do not install them separately:

  • @alsocoder/apna-input
  • @alsocoder/apna-select
  • @alsocoder/apna-date-picker
  • @alsocoder/apna-editor
  • @alsocoder/apna-upload

Optional: media picker

For ApnaFormMediaPicker or builder type: "media":

npm install @alsocoder/apna-media-library

Setup

import "@alsocoder/apna-form/styles.css"

This bundles Apna Input, Select, DatePicker, Editor, Upload, and form layout styles.

Important: Import this after your global CSS. Avoid generic rules like button[type="button"] { padding; background; } — they override Select/Editor/DatePicker icon buttons. The bundle includes a button guard layer for common Apna UI buttons.

With media picker

import "@alsocoder/apna-form/styles.css"
import "@alsocoder/apna-media-library/styles.css"
import { ApnaMediaProvider } from "@alsocoder/apna-media-library"

export function App() {
  return (
    <ApnaMediaProvider adapter={yourAdapter}>
      <YourForm />
    </ApnaMediaProvider>
  )
}

Basic usage

import { z } from "zod"
import {
  ApnaForm,
  ApnaFormInput,
  ApnaFormSelect,
  ApnaFormDatePicker,
} from "@alsocoder/apna-form"
import "@alsocoder/apna-form/styles.css"

const schema = z.object({
  name: z.string().min(2),
  country: z.string(),
  dob: z.string(),
})

export function ProfileForm() {
  return (
    <ApnaForm
      schema={schema}
      defaultValues={{ name: "", country: "", dob: "" }}
      onSubmit={(values) => console.log(values)}
    >
      <ApnaFormInput name="name" label="Name" required />
      <ApnaFormSelect name="country" options={countries} label="Country" />
      <ApnaFormDatePicker name="dob" label="Date of birth" />
      <button type="submit">Save</button>
    </ApnaForm>
  )
}

Conditional fields

<ApnaFormWatch names={["country"]}>
  {(country) =>
    country === "IN" ? (
      <ApnaFormInput name="pincode" label="PIN code" format="digits" />
    ) : null
  }
</ApnaFormWatch>

Or declarative:

<ApnaFormInput name="gstin" showWhen={(values) => values.country === "IN"} />

Multi-step

<ApnaForm schema={schema} onSubmit={save}>
  <ApnaFormSteps>
    <ApnaFormStep id="personal" title="Personal" fields={["name", "email"]}>
      <ApnaFormInput name="name" label="Name" />
      <ApnaFormInput name="email" label="Email" format="email" />
    </ApnaFormStep>
    <ApnaFormStep id="address" title="Address" fields={["country"]}>
      <ApnaFormSelect name="country" options={countries} label="Country" />
    </ApnaFormStep>
    <ApnaFormStepActions />
  </ApnaFormSteps>
</ApnaForm>

Field arrays

<ApnaFormFieldArray name="phones">
  {(fields, { append, remove }) => (
    <>
      {fields.map((field, index) => (
        <ApnaFormInput
          key={field.id}
          name={`phones.${index}.number`}
          label={`Phone ${index + 1}`}
        />
      ))}
      <button type="button" onClick={() => append({ number: "" })}>
        Add phone
      </button>
    </>
  )}
</ApnaFormFieldArray>

Auto-save

<ApnaForm
  schema={schema}
  enableAutoSave
  autoSaveDebounceMs={800}
  onAutoSave={(values) => saveDraft(values)}
  onSubmit={publish}
>
  ...
</ApnaForm>

Form builder

ApnaFormBuilder renders fields from a JSON config on a 12-column responsive grid.

| Prop | Purpose | |------|---------| | columns | Default fields per row per breakpoint (span = 12 / columns) | | field.colSpan | Per-field override (1–12 columns) | | field.showWhen | Conditionally show/hide fields; hidden fields are removed from the grid (no empty cells) | | type: "editor" | Always full width (12 columns) on every breakpoint | | type: "upload" | File upload — tall field group (full width like textarea); pass upload on ApnaForm / ApnaFormBuilder (or per-field props.upload) | | type: "media" | Lazy-loaded media picker — same grid behavior as input/select (optional peer) | | type: "textarea" | Tall field group — only pairs with other tall fields (e.g. upload), otherwise full width |

Smart layout

The builder groups fields by layout type:

  • compact — input, select, date, dateRange, datetime, otp, media
  • tall — textarea, upload
  • full — editor

Within each group, fields auto-size per row: 3 → one-third each, 2 → half each, 1 → full width. Mixed types (e.g. input + textarea) never share a row.

Breakpoints: default (mobile), sm 640px, md 768px, lg 1024px, xl 1280px.

const config = [
  { type: "input", name: "title", label: "Title", colSpan: { md: 8 } },
  { type: "select", name: "role", label: "Role", colSpan: { md: 4 }, props: { options: roles } },
  { type: "date", name: "startDate", label: "Start date" },
  { type: "editor", name: "description", label: "Description" },
  {
    type: "input",
    name: "pincode",
    label: "PIN code",
    showWhen: (values) => values.country === "IN",
    props: { format: "digits", maxLength: 6 },
  },
] as const

<ApnaFormBuilder
  columns={{ default: 1, md: 2, lg: 3 }}
  config={config}
  schema={schema}
  onSubmit={save}
/>

Editor, upload, and media field types are lazy-loaded in the builder — their JS chunks load only when used in config.

File upload (built-in)

Pass an upload function on ApnaForm or ApnaFormBuilder. Upload fields inherit it unless overridden in field props:

import type { ApnaUploadFn } from "@alsocoder/apna-upload"

const uploadFile: ApnaUploadFn = async (file, { signal, onProgress }) => {
  onProgress(50)
  const res = await fetch("/api/upload", { method: "POST", body: formData, signal })
  onProgress(100)
  return res.json() // e.g. { file: { path: "/uploads/..." } }
}

<ApnaFormBuilder
  upload={uploadFile}
  transformUploadResult={(result) => result.file.path}
  defaultUploadProps={{
    accept: "image/*",
    maxSize: 5 * 1024 * 1024,
    resultMode: "url",
    beforeUpload: async (file) => {
      // compress, crop, watermark, etc.
      return file
    },
  }}
  config={[
    { type: "upload", name: "resume", label: "Resume", props: { accept: ".pdf" } },
    { type: "upload", name: "avatar", label: "Avatar", props: { upload: otherHandler } },
  ]}
  schema={schema}
  onSubmit={save}
/>
<ApnaFormUpload name="resume" label="Resume" accept=".pdf" />

Value shape depends on resultMode:

| resultMode | Single field | Multiple field | |--------------|--------------|----------------| | "url" (default) | string | string[] | | "file" | File | File[] |

Match your Zod schema to the chosen resultMode.

Media picker (optional)

npm install @alsocoder/apna-media-library

Wrap your app with ApnaMediaProvider, import media styles, then use in builder or as a connected field:

{ type: "media", name: "cover", label: "Cover", props: { folder: "blog/covers" } }
<ApnaFormMediaPicker name="avatar" label="Avatar" folder="misc" />

For editor image insertion via media library, pass imageUpload.pick in editor props.

Advanced: custom field

<ApnaFormField
  name="email"
  render={({ field, fieldState }) => (
    <ApnaInput
      value={field.value}
      onValueChange={field.onChange}
      onBlur={field.onBlur}
      error={fieldState.error?.message}
    />
  )}
/>

Yup (optional)

import { createYupResolver } from "@alsocoder/apna-form"

const resolver = await createYupResolver(yupSchema)
<ApnaForm resolver={resolver} onSubmit={save}>...</ApnaForm>

Development

npm install
npm run build

License

MIT