Skip to content

dkzlv/nanoform

Repository files navigation

Nano Stores Form

A tiny form library for Nano Stores.

  • Small. The core is only 350 bytes.
  • Phenomenal performance. Only rerenders those fields that got updates.
  • Nano Stores first. Finally, have your form logic outside of components. Plays nicely with store events, computed stores, remote queries, and the rest.
  • UI agnostic. Use native <form>, any UI kit, React Native, or any mix of them.
  • Unbeatable TS support. Very strict typing of nested fields, so you don't waste time debugging.
Sponsored by Evil Martians

Install

npm install nanostores @nanostores/form

Usage

See Nano Stores docs about using the store and subscribing to store’s changes in UI frameworks.

Nanoform gives you a couple of helpers. Let's go one by one.

nanoform that returns FormStore

The core is the form store and its initial shape. nanoform returns a deepMap from the Nano Stores standard exports.

import { nanoform } from '@nanostores/form';

export const $authForm = nanoform<{email1?: string, email2?: string, password?: string}>({});

const AuthForm = () => {
  const { email1, email2, password } = useStore($authForm);

  return (
    <>
      <input
        type="email"
        value={email1}
        onChange={e => $authForm.setKey('email1', e.currentTarget.value)} />
        {/* render email2, password */}
    </>
  );
};

Here you go. You can already use this store in the UI. But it has a few issues:

  1. it will rerender the whole form like crazy on each keystroke! Sometimes it's fine, but most of the time it's not.
  2. also, it'd be good to have some decent validation of the email1 and email2 equality.

We'll tackle them one by one.

FormStore.getField that returns FieldStore

Here comes the core helper: .getField. It's a strictly typed function which gets the object path to the field and returns a stable store for this field. Stable means that it won't change its identity if you call this function many times with the same key.

The return is just another deepMap, so it works with everything Nano Stores provides you with (lifecycles, computed, etc.).

The most magical part is that its value is in two-way sync with the parent form, so whenever any of them changes, the other changes as well.

Let's add a simple check if emails are the same.

import { computed } from 'nanostores';
import { useStore } from '@nanostores/react';

// Will only recalc when these two fields change
// You can use the same approach to run schema validation on the whole form data
// or just a subtree of it.
const $emailsAreSame = computed(
  [$authForm.getField("email1"), $authForm.getField("email2")],
  (email1, email2) => email1 === email2
);

// Will only rerender when this field is touched
const Email1 = () => {
  const $field = $authForm.getField("email1");
  return (
    <input
      type="email"
      value={useStore($field)}
      onChange={(e) => $field.set(e.currentTarget.value)}
    />
  );
};

FormStore.onSubmit

When you create a form store, you can provide a second optional argument for the form submission method.

import { nanoform } from '@nanostores/form';
import { useStore } from '@nanostores/react';

export const $simpleForm = nanoform<{field1?: string}>(
  {},
  async (data) => {
    // sending data somewhere, validation, etc.
  }
);

const Form = () => {
  return (
    <form onSubmit={$simpleForm.onSubmit}>
      ...
    </form>
  );
};

It's essentially a small quality-of-life improvement. It:

  1. no double submissions can happen (we await for the submit function to resolve);
  2. we call preventDefault;
  3. we reset form's value to initial upon successful submission.

FieldStore.onChange

All of the FieldStore instances have a onChange function on them.

This is the second most important helper. Just wrap your form with this function to get a stable identity onChange callback added to your fields:

import { nanoform, formatDate, type FieldStore } from "@nanostores/form";
import { useStore } from "@nanostores/react";

const $form = nanoform<{ str?: string; dt?: Date; num?: number; agreed?: boolean }>({});

const App = () => {
  return (
    <>
      <Input type="text" $field={$form.getField("str")} />
      <Input type="date" $field={$form.getField("dt")} />
      <Input type="number" $field={$form.getField("num")} />
      <Input type="checkbox" $field={$form.getField("num")} />
    </>
  );
};

const Input = ({
  $field,
  type,
}: {
  $field: FieldStore;
  type: JSX.IntrinsicElements["input"]["type"];
}) => {
  const value = useStore($field);
  return (
    <input
      type={type}
      value={type === "date" ? formatDate(value as Date) : value}
      onChange={$field.onChange}
    />
  );
};

The neat thing is that it will use value, valueAsNumber, valueAsDate and checked based on the type this input has. So you'll get the value casted to the correct type completely automatically by the browser.

FormStore.reset and FieldStore.reset

When creating the form, we make a structuredClone of the initial provided value.

Call form.reset() to reset the form to its initial value, call field.reset() to reset this specific field.

Recipes

Usage with @nanostores/query

Form submission plays really nice with @nanostores/query mutations. Just provide the mutate function directly to the form call.

type AuthData = { email?: string; password?: string; agreed?: boolean };

const $signup = createMutationStore<AuthData>(async ({ data }) => {
  // I'll leave this to reader's imagination
});

const $form = nanoform<AuthData>({}, $signup.mutate);

const Signup = () => {
  const { loading, error } = useStore($signup);

  return (
    <form onSubmit={$form.onSubmit}>
      <Input type="email" $field={$form.getField("email")} />
      <Input type="date" $field={$form.getField("password")} />
      <Input type="checkbox" $field={$form.getField("agreed")} />

      <button disabled={loading}>Sign up</button>
      {error && <p>Something terrible happened</p>}
    </form>
  );
};

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published