Skip to content

schalkventer/frrm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

30 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ‡ frrm

Tiny 0.5kb Zod-based, HTML form abstraction that goes brr.

⭐ If you find this tool useful please consider giving it a star on Github ⭐

Basic Example

The latest v2 of frrm uses Zod v4 (npm install frrm). If you want to use frrm with Zod v3 then you need to install version 1.4.2 (npm install frrm@1). Note currently the API is exactly the same, the only difference is the Zod version you plan on using.

JavaScript

import { create, attach } from 'frrm'
import { z } from 'zod'

const handler = create({
  /**
   * If string value then will replace submit button text with provided value
   * while server request is resolving. Will also disable all buttons and
   * inputs. If `true` is passed the label won't be replaced, but everything will
   * still disable. Alternatively you can pass a callback if you want to
   * manually handle the busy state (will prevent default behaviour).
   */
  onBusy: "Loading...",

  /**
   * Applies client-side Zod validation to determine whether `onSubmit` should
   * fire.
   */
  schema: z.object({
    email: z.string().min(1, { message: "Email value is required" }).email({
      message: "Email is not formatted correctly",
    }),
    password: z
      .string()
      .min(1, {
        message: "Password value is required",
      })
      .min(6, {
        message: "Password is required to be at least 6 characters",
      }),
  }),

  /**
   * Will inject error message into the provided DOM element. Alternatively a
   * callback can be provided that accepts both `timestamp` and `value`
   * properties. 
   * 
   * Note that error is automatically removed when the form is
   * submitted again - likewise a `null` value will be passed to the callback
   * (if used).
   */
  onError: document.querySelector('[role="alert"]')!,

  onSubmit: (submission) => {
    /**
     * Fake server request that takes 4 seconds to resolve, and throws on
     * incorrect email or password.
     */
    return new Promise((resolve) => {
      try {
        setTimeout(() => {
          if (submission.email !== "[email protected]")
            resolve(Error("Invalid email"));

          if (submission.password !== "hunter2")
            resolve(Error("Invalid password"));

          resolve(undefined);
        }, 4000);
      } catch (error) {
        console.error(error);
        resolve(Error("Something went wrong"));
      }
    });
  },
});

/**
 * If you are using a framework like React, then you can simply pass the
 * instance to the onSubmit handler (see example further down page). 
 * However, if you are using plain JavaScript,  then you need to 
 * attach the event listener manually. You can use the `attach`
 * function to do this if you want, since it provides a returned object 
 * with a `remove` method for cleanup.
 */
attach(document.querySelector("form")!, handler);

CSS

@keyframes enter {
  from {
    transform: translateY(-0.2rem);
    opacity: 0.5;
  }
  
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

form {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1rem;
}

[role="alert"] > * {
  background: rgba(255, 0, 0, 0.05);
  padding: 1rem;
  animation: enter 0.3s ease;
}

HTML

<form>
  <label>
    <span>Email:</span>
    <input type="email" name="email" />
  </label>

  <div>
    <label>
      <span>Password:</span>
      <input type="password" name="password" />
    </label>
  </div>

  <div role="alert" aria-live="assertive"></div>
  <button type="submit">Login</button>
</form>

React Example

When using React you can simply pass the handler as is to onSubmit.

import { z } from "zod";
import { useState } from "react";
import { create } from "./react";

export const Example = () => {
  const [message, setMessage] = useState({
    value: null,
    timestamp: Date.now(),
  });

  return (
    <form
      className="form"
      onSubmit={create({
        schema,
        onSubmit: fromServer,
        onError: setMessage,
        onBusy: "Loading...",

        schema: z.object({
          email: z.string().min(1, { message: "Email value is required" }).email({
            message: "Email is not formatted correctly",
          }),
          password: z
            .string()
            .min(1, {
              message: "Password value is required",
            })
            .min(6, {
              message: "Password is required to be at least 6 characters",
            }),
        }),
      })}
    >
      <label>
        <span>Email:</span>
        <input type="email" name="email" />
      </label>

      <div>
        <label>
          <span>Password:</span>
          <input type="password" name="password" />
        </label>
      </div>

      <div>
        {message.value && (
          <div className="message" key={`${message.value}-${message.timestamp}`}>
            {message.value}
          </div>
        )}
      </div>

      <button type="submit">
        Login
      </button>
    </form>
  );
};

Is it really 0.5kb?

Pretty much. Technically it is ~0.537kb . This is the minified code:

const e=e=>{const{onSubmit:t,schema:r,onError:a}=e;return async e=>{e.preventDefault(),a({value:null,timestamp:Date.now()});const n=e.currentTarget,o=Object.fromEntries(new FormData(n));try{const e=r.parse(o),n=await t(e);n&&a({value:n,timestamp:Date.now()})}catch(e){if(e.errors.length)return n.querySelector(`[name="${e.errors[0].path[0]}"]`).focus(),a({value:e.errors[0].message,timestamp:Date.now()});throw e}}},t=(e,t)=>(e.addEventListener("submit",t),{remove:()=>e.removeEventListener("submit",t)});export{t as attach,e as create};

About

πŸ‡ Tiny 0.5kb Zod-based, HTML form abstraction that goes brr.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •