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 🙏

© 2025 – Pkg Stats / Ryan Hefner

sndp-easy-form

v1.0.2

Published

A lightweight, performant form management library for React and Next.js

Downloads

302

Readme

sndp-easy-form 🚀

A lightweight (~9kb), high-performance form management library for React and Next.js with full TypeScript support.

npm version License: MIT

✨ Features

  • 🚀 High Performance - Minimal re-renders with uncontrolled inputs
  • 📦 Lightweight - Only ~9kb gzipped, zero dependencies
  • 🎯 TypeScript First - Full type safety and IntelliSense support
  • React Hooks - Modern hooks-based API
  • 🔄 Next.js Ready - Works with both CSR and SSR
  • 🎨 Flexible Validation - Built-in and custom validation rules
  • 📝 Dynamic Forms - Support for field arrays and conditional fields
  • 🌐 Native HTML - Works with standard HTML form elements

📦 Installation

npm install sndp-easy-form
yarn add sndp-easy-form
pnpm add sndp-easy-form

🚀 Quick Start

import { useForm } from 'sndp-easy-form';

function MyForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('username', { required: 'Username is required' })} />
      {errors.username && <span>{errors.username.message}</span>}
      
      <button type="submit">Submit</button>
    </form>
  );
}

📚 API Reference

useForm()

The main hook for managing form state and validation.

Options

const formMethods = useForm({
  mode: 'onSubmit',           // 'onSubmit' | 'onBlur' | 'onChange' | 'onTouched' | 'all'
  reValidateMode: 'onChange', // 'onSubmit' | 'onBlur' | 'onChange'
  defaultValues: {},          // Default form values
  criteriaMode: 'firstError'  // 'firstError' | 'all'
});

Return Values

  • register - Register input fields with validation rules
  • handleSubmit - Handle form submission
  • watch - Watch field values
  • setValue - Programmatically set field values
  • getValue - Get a single field value
  • getValues - Get all form values
  • reset - Reset form to default values
  • setError - Set field error manually
  • clearErrors - Clear field errors
  • setFocus - Set focus on a field
  • unregister - Unregister a field
  • formState - Form state object

formState

{
  isDirty: boolean;              // Form has been modified
  dirtyFields: object;           // Fields that have been modified
  touchedFields: object;         // Fields that have been touched
  isSubmitted: boolean;          // Form has been submitted
  isSubmitting: boolean;         // Form is currently submitting
  isSubmitSuccessful: boolean;   // Last submission was successful
  isValid: boolean;              // Form is valid
  submitCount: number;           // Number of submission attempts
  errors: object;                // Field errors
}

📖 Complete Examples

Basic Form with Validation

import { useForm } from 'sndp-easy-form';

function LoginForm() {
  const { 
    register, 
    handleSubmit, 
    formState: { errors, isSubmitting } 
  } = useForm();

  const onSubmit = async (data) => {
    await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>Email</label>
        <input 
          {...register('email', { 
            required: 'Email is required',
            pattern: {
              value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
              message: 'Invalid email address'
            }
          })} 
          placeholder="Email"
        />
        {errors.email && <span className="error">{errors.email.message}</span>}
      </div>

      <div>
        <label>Password</label>
        <input 
          type="password"
          {...register('password', { 
            required: 'Password is required',
            minLength: {
              value: 8,
              message: 'Password must be at least 8 characters'
            }
          })} 
          placeholder="Password"
        />
        {errors.password && <span className="error">{errors.password.message}</span>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}

Custom Validation

import { useForm } from 'sndp-easy-form';

function SignupForm() {
  const { register, handleSubmit, watch, formState: { errors } } = useForm();
  
  const password = watch('password');

  const onSubmit = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('username', {
          required: 'Username is required',
          minLength: { value: 3, message: 'Minimum 3 characters' },
          validate: async (value) => {
            const response = await fetch(`/api/check-username?username=${value}`);
            const available = await response.json();
            return available || 'Username already taken';
          }
        })}
        placeholder="Username"
      />
      {errors.username && <span>{errors.username.message}</span>}

      <input
        type="password"
        {...register('password', {
          required: 'Password is required',
          minLength: { value: 8, message: 'Minimum 8 characters' }
        })}
        placeholder="Password"
      />
      {errors.password && <span>{errors.password.message}</span>}

      <input
        type="password"
        {...register('confirmPassword', {
          required: 'Please confirm password',
          validate: (value) => 
            value === password || 'Passwords do not match'
        })}
        placeholder="Confirm Password"
      />
      {errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}

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

Dynamic Field Array

import { useForm, useFieldArray } from 'sndp-easy-form';

function ContactForm() {
  const { register, control, handleSubmit } = useForm({
    defaultValues: {
      contacts: [{ name: '', email: '', phone: '' }]
    }
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'contacts'
  });

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h3>Contacts</h3>
      
      {fields.map((field, index) => (
        <div key={field._id} style={{ marginBottom: '20px', padding: '10px', border: '1px solid #ddd' }}>
          <input 
            {...register(`contacts.${index}.name`, { 
              required: 'Name is required' 
            })} 
            placeholder="Name" 
          />
          
          <input 
            {...register(`contacts.${index}.email`, { 
              required: 'Email is required',
              pattern: {
                value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
                message: 'Invalid email'
              }
            })} 
            placeholder="Email" 
          />
          
          <input 
            {...register(`contacts.${index}.phone`)} 
            placeholder="Phone (optional)" 
          />
          
          <button type="button" onClick={() => remove(index)}>
            Remove
          </button>
        </div>
      ))}
      
      <button 
        type="button" 
        onClick={() => append({ name: '', email: '', phone: '' })}
      >
        Add Contact
      </button>
      
      <button type="submit">Submit</button>
    </form>
  );
}

Form Context (Multiple Components)

import { useForm, FormProvider, useFormContext } from 'sndp-easy-form';

function ParentForm() {
  const methods = useForm();

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <FormProvider formMethods={methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        <PersonalInfo />
        <AddressInfo />
        <button type="submit">Submit</button>
      </form>
    </FormProvider>
  );
}

function PersonalInfo() {
  const { register, formState: { errors } } = useFormContext();

  return (
    <div>
      <h3>Personal Information</h3>
      <input 
        {...register('firstName', { required: 'First name is required' })} 
        placeholder="First Name"
      />
      {errors.firstName && <span>{errors.firstName.message}</span>}
      
      <input 
        {...register('lastName', { required: 'Last name is required' })} 
        placeholder="Last Name"
      />
      {errors.lastName && <span>{errors.lastName.message}</span>}
    </div>
  );
}

function AddressInfo() {
  const { register, formState: { errors } } = useFormContext();

  return (
    <div>
      <h3>Address</h3>
      <input 
        {...register('street', { required: 'Street is required' })} 
        placeholder="Street"
      />
      {errors.street && <span>{errors.street.message}</span>}
      
      <input 
        {...register('city', { required: 'City is required' })} 
        placeholder="City"
      />
      {errors.city && <span>{errors.city.message}</span>}
    </div>
  );
}

Watching Form Values

import { useForm } from 'sndp-easy-form';

function ConditionalForm() {
  const { register, handleSubmit, watch } = useForm();
  
  const showAddress = watch('needsShipping');
  const country = watch('country');

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <label>
        <input 
          type="checkbox" 
          {...register('needsShipping')} 
        />
        Need shipping?
      </label>

      {showAddress && (
        <div>
          <input {...register('address')} placeholder="Address" />
          
          <select {...register('country')}>
            <option value="">Select Country</option>
            <option value="US">United States</option>
            <option value="IN">India</option>
            <option value="UK">United Kingdom</option>
          </select>

          {country === 'US' && (
            <input {...register('zipCode')} placeholder="ZIP Code" />
          )}
          
          {country === 'IN' && (
            <input {...register('pinCode')} placeholder="PIN Code" />
          )}
        </div>
      )}

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

Programmatic Form Control

import { useForm } from 'sndp-easy-form';

function ProgrammaticForm() {
  const { register, handleSubmit, setValue, reset, setFocus } = useForm({
    defaultValues: {
      username: '',
      email: '',
      age: ''
    }
  });

  const fillDemoData = () => {
    setValue('username', 'johndoe');
    setValue('email', '[email protected]');
    setValue('age', '25');
    setFocus('username');
  };

  const resetForm = () => {
    reset();
  };

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <input {...register('username')} placeholder="Username" />
      <input {...register('email')} placeholder="Email" />
      <input {...register('age')} type="number" placeholder="Age" />

      <div>
        <button type="button" onClick={fillDemoData}>
          Fill Demo Data
        </button>
        <button type="button" onClick={resetForm}>
          Reset Form
        </button>
        <button type="submit">Submit</button>
      </div>
    </form>
  );
}

🎯 Validation Rules

Built-in Validation Rules

register('fieldName', {
  required: 'This field is required',
  // or
  required: { value: true, message: 'Required' },

  min: { value: 18, message: 'Must be at least 18' },
  max: { value: 100, message: 'Must be less than 100' },
  
  minLength: { value: 3, message: 'Minimum 3 characters' },
  maxLength: { value: 20, message: 'Maximum 20 characters' },
  
  pattern: {
    value: /^[A-Za-z]+$/,
    message: 'Only letters allowed'
  },

  validate: (value) => {
    return value.includes('@') || 'Must contain @';
  },
  // or multiple validation functions
  validate: {
    positive: (value) => parseFloat(value) > 0 || 'Must be positive',
    lessThan100: (value) => parseFloat(value) < 100 || 'Must be less than 100'
  }
})

50+ Custom Validation Examples

Text Input Validations

// No numbers allowed
register('name', {
  validate: (v) => v.split('').every(c => isNaN(Number(c)) || c === ' ') || 'Numbers not allowed'
});

// No special characters
register('username', {
  validate: (v) => v.split('').every(c => "abcdefghijklmnopqrstuvwxyz ".includes(c.toLowerCase())) || 'Special characters not allowed'
});

// Must be uppercase
register('code', {
  validate: (v) => v === v.toUpperCase() || 'Must be in uppercase'
});

// Must be lowercase
register('slug', {
  validate: (v) => v === v.toLowerCase() || 'Must be in lowercase'
});

// Not empty (no spaces only)
register('title', {
  validate: (v) => v.trim().length > 0 || 'Cannot be only spaces'
});

// Forbidden words
register('username', {
  validate: (v) => !v.toLowerCase().includes('admin') || 'Cannot contain word admin'
});

// Single word (no spaces)
register('identifier', {
  validate: (v) => !v.trim().includes(' ') || 'Spaces not allowed'
});

// Must start with specific letter
register('productCode', {
  validate: (v) => v.toLowerCase().startsWith('a') || 'Must start with letter A'
});

Password Validations

const { register, watch } = useForm();
const username = watch('username');
const password = watch('password');

// Strong password with multiple validations
register('password', {
  validate: {
    hasSymbol: (v) => v.includes('@') || v.includes('#') || 'Must include @ or #',
    hasNumber: (v) => v.split('').some(c => !isNaN(Number(c))) || 'Must include a number',
    hasUpper: (v) => v.split('').some(c => c === c.toUpperCase() && c !== c.toLowerCase()) || 'Must have uppercase letter',
    hasLower: (v) => v.split('').some(c => c === c.toLowerCase() && c !== c.toUpperCase()) || 'Must have lowercase letter',
    min8Chars: (v) => v.length >= 8 || 'Password too short (min 8)',
    noSpace: (v) => !v.includes(' ') || 'No spaces allowed',
    isNotCommon: (v) => !['12345678', 'password', 'qwerty'].includes(v) || 'Too common password',
    hasSpecial: (v) => "!@#$%^&*".split('').some(s => v.includes(s)) || 'Special character required'
  }
});

// Password cannot contain username
register('password', {
  validate: (v) => !v.toLowerCase().includes(username?.toLowerCase()) || 'Password cannot contain username'
});

// Confirm password match
register('confirmPassword', {
  validate: (v) => v === password || 'Passwords do not match'
});

Number Input Validations

// Must be positive
register('amount', {
  validate: (v) => Number(v) > 0 || 'Must be a positive number'
});

// Age validation
register('age', {
  validate: {
    isAdult: (v) => Number(v) >= 18 || 'Must be at least 18',
    maxAge: (v) => Number(v) <= 100 || 'Age cannot exceed 100'
  }
});

// Even/Odd numbers
register('evenNumber', {
  validate: (v) => Number(v) % 2 === 0 || 'Only even numbers allowed'
});

register('oddNumber', {
  validate: (v) => Number(v) % 2 !== 0 || 'Only odd numbers allowed'
});

// Divisible by specific number
register('quantity', {
  validate: (v) => Number(v) % 5 === 0 || 'Must be multiple of 5'
});

// Range validation
register('score', {
  validate: (v) => (Number(v) >= 10 && Number(v) <= 50) || 'Must be between 10 and 50'
});

// Not zero
register('value', {
  validate: (v) => Number(v) !== 0 || 'Value cannot be zero'
});

// Integers only (no decimals)
register('count', {
  validate: (v) => Number.isInteger(Number(v)) || 'Decimals not allowed'
});

// Maximum price
register('price', {
  validate: (v) => Number(v) <= 5000 || 'Price cannot be more than 5000'
});

Email & URL Validations

// Basic email validations
register('email', {
  validate: {
    hasAt: (v) => v.includes('@') || 'Missing @ symbol',
    hasDot: (v) => v.includes('.') || 'Missing . (dot) in email',
    longEnough: (v) => v.length > 10 || 'Email address too short'
  }
});

// Exclude specific domains
register('email', {
  validate: (v) => !v.endsWith('@gmail.com') || 'Gmail not allowed'
});

// Company email only
register('workEmail', {
  validate: (v) => v.endsWith('@company.com') || 'Use company email only'
});

// No underscore in email
register('email', {
  validate: (v) => !v.includes('_') || 'Underscore not allowed in email'
});

// URL must start with http/https
register('website', {
  validate: (v) => v.startsWith('http') || 'URL must start with http or https'
});

Date & Time Validations

// Date cannot be in future
register('birthdate', {
  validate: (v) => new Date(v) <= new Date() || 'Date cannot be in future'
});

// Only specific day of week (Sunday)
register('meetingDate', {
  validate: (v) => new Date(v).getDay() === 0 || 'Only Sundays allowed'
});

// Date range
register('startDate', {
  validate: (v) => new Date(v) >= new Date('2020-01-01') || 'Date must be after 2020'
});

// Specific year only
register('eventDate', {
  validate: (v) => new Date(v).getFullYear() === 2024 || 'Only 2024 dates allowed'
});

Checkbox, Radio & File Validations

// Terms must be accepted
register('terms', {
  validate: (v) => v === true || 'Must accept terms and conditions'
});

// Privacy policy agreement
register('privacy', {
  validate: (v) => v === true || 'Privacy policy agreement required'
});

// Select/dropdown required
register('country', {
  validate: (v) => v !== '' || 'Please select an option'
});

// Blocked options
register('region', {
  validate: (v) => v !== 'Blocked' || 'Service not available in your area'
});

File & Image Upload Validations

// File is required
register('avatar', {
  validate: (v) => (v && v.length > 0) || 'File is required'
});

// File size validation (min and max)
register('document', {
  validate: {
    minSize: (v) => v[0]?.size >= 10 * 1024 || 'File is too small (min 10KB)',
    maxSize: (v) => v[0]?.size <= 2 * 1024 * 1024 || 'Max size 2MB'
  }
});

// Only images allowed
register('photo', {
  validate: (v) => v[0]?.type.startsWith('image/') || 'Only images allowed'
});

// Only PDF allowed
register('resume', {
  validate: (v) => v[0]?.type === 'application/pdf' || 'Only PDF allowed'
});

// Specific image formats only (PNG/JPG)
register('profilePic', {
  validate: (v) => ['image/png', 'image/jpeg'].includes(v[0]?.type) || 'Only PNG/JPG allowed'
});

// Multiple file formats allowed
register('attachment', {
  validate: (v) => {
    const allowedTypes = ['application/pdf', 'image/png', 'image/jpeg', 'application/msword'];
    return allowedTypes.includes(v[0]?.type) || 'Invalid file format';
  }
});

// Maximum number of files
register('gallery', {
  validate: (v) => v.length <= 3 || 'Maximum 3 files allowed'
});

// Minimum number of files
register('certificates', {
  validate: (v) => v.length >= 2 || 'At least 2 files required'
});

// File name length validation
register('file', {
  validate: (v) => v[0]?.name.length < 50 || 'File name too long (max 50 chars)'
});

// No special characters in file name
register('upload', {
  validate: (v) => !v[0]?.name.includes('#') || 'File name cannot contain #'
});

// File extension validation
register('document', {
  validate: (v) => {
    const validExtensions = ['.pdf', '.doc', '.docx'];
    const fileName = v[0]?.name.toLowerCase();
    return validExtensions.some(ext => fileName.endsWith(ext)) || 'Invalid file type';
  }
});

// Combined file validation
register('coverImage', {
  validate: {
    required: (v) => (v && v.length > 0) || 'Image is required',
    isImage: (v) => v[0]?.type.startsWith('image/') || 'Only images allowed',
    maxSize: (v) => v[0]?.size <= 5 * 1024 * 1024 || 'Max size 5MB',
    specificFormat: (v) => ['image/png', 'image/jpeg', 'image/webp'].includes(v[0]?.type) || 
      'Only PNG, JPG, or WebP allowed',
    nameLength: (v) => v[0]?.name.length < 100 || 'File name too long'
  }
});

Advanced Image Validation with Dimensions

function ImageUploadForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  // Validate image dimensions
  const validateImageDimensions = (files) => {
    return new Promise((resolve) => {
      if (!files || files.length === 0) {
        resolve('Image is required');
        return;
      }

      const file = files[0];
      const img = new Image();
      const objectUrl = URL.createObjectURL(file);

      img.onload = () => {
        URL.revokeObjectURL(objectUrl);
        
        // Check minimum dimensions
        if (img.width < 800 || img.height < 600) {
          resolve('Image must be at least 800x600 pixels');
          return;
        }

        // Check maximum dimensions
        if (img.width > 4000 || img.height > 4000) {
          resolve('Image too large (max 4000x4000 pixels)');
          return;
        }

        // Check aspect ratio (16:9)
        const aspectRatio = img.width / img.height;
        if (Math.abs(aspectRatio - 16/9) > 0.1) {
          resolve('Image must have 16:9 aspect ratio');
          return;
        }

        resolve(true);
      };

      img.onerror = () => {
        URL.revokeObjectURL(objectUrl);
        resolve('Invalid image file');
      };

      img.src = objectUrl;
    });
  };

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <input
        type="file"
        accept="image/*"
        {...register('banner', {
          validate: {
            required: (v) => (v && v.length > 0) || 'Banner image is required',
            fileSize: (v) => v[0]?.size <= 3 * 1024 * 1024 || 'Max size 3MB',
            fileType: (v) => ['image/png', 'image/jpeg'].includes(v[0]?.type) || 'Only PNG/JPG',
            dimensions: validateImageDimensions
          }
        })}
      />
      {errors.banner && <span className="error">{errors.banner.message}</span>}

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

Multiple Files with Individual Validation

function MultiFileUpload() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const validateAllFiles = (files) => {
    if (!files || files.length === 0) {
      return 'Please select at least one file';
    }

    // Check total number of files
    if (files.length > 5) {
      return 'Maximum 5 files allowed';
    }

    // Check each file
    for (let i = 0; i < files.length; i++) {
      const file = files[i];

      // Check file size
      if (file.size > 10 * 1024 * 1024) {
        return `File "${file.name}" is too large (max 10MB)`;
      }

      // Check file type
      const allowedTypes = ['image/png', 'image/jpeg', 'application/pdf'];
      if (!allowedTypes.includes(file.type)) {
        return `File "${file.name}" has invalid format`;
      }

      // Check file name
      if (file.name.length > 100) {
        return `File "${file.name}" has too long name`;
      }
    }

    // Check total size of all files
    const totalSize = Array.from(files).reduce((sum, file) => sum + file.size, 0);
    if (totalSize > 20 * 1024 * 1024) {
      return 'Total file size cannot exceed 20MB';
    }

    return true;
  };

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <input
        type="file"
        multiple
        accept="image/*,.pdf"
        {...register('documents', {
          validate: validateAllFiles
        })}
      />
      {errors.documents && <span className="error">{errors.documents.message}</span>}

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

Complex Combined Validations

function RegistrationForm() {
  const { register, handleSubmit, watch, formState: { errors } } = useForm();
  
  const password = watch('password');
  const username = watch('username');

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <input
        {...register('username', {
          validate: {
            minLength: (v) => v.length >= 5 || 'Minimum 5 characters required',
            maxLength: (v) => v.length <= 20 || 'Maximum 20 characters allowed',
            noSpecialChars: (v) => v.split('').every(c => 
              "abcdefghijklmnopqrstuvwxyz0-9".includes(c.toLowerCase())
            ) || 'Only letters and numbers allowed',
            noAdmin: (v) => !v.toLowerCase().includes('admin') || 'Cannot contain "admin"'
          }
        })}
        placeholder="Username"
      />
      {errors.username && <span>{errors.username.message}</span>}

      <input
        type="password"
        {...register('password', {
          validate: {
            min8: (v) => v.length >= 8 || 'Minimum 8 characters',
            hasUpper: (v) => /[A-Z]/.test(v) || 'Must have uppercase letter',
            hasLower: (v) => /[a-z]/.test(v) || 'Must have lowercase letter',
            hasNumber: (v) => /\d/.test(v) || 'Must have a number',
            hasSpecial: (v) => /[!@#$%^&*]/.test(v) || 'Must have special character',
            noUsername: (v) => !v.toLowerCase().includes(username?.toLowerCase()) || 
              'Password cannot contain username',
            notCommon: (v) => !['password', '12345678', 'qwerty123'].includes(v.toLowerCase()) || 
              'Password too common'
          }
        })}
        placeholder="Password"
      />
      {errors.password && <span>{errors.password.message}</span>}

      <input
        type="password"
        {...register('confirmPassword', {
          validate: (v) => v === password || 'Passwords must match'
        })}
        placeholder="Confirm Password"
      />
      {errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}

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

🔧 Next.js Integration

App Router (Next.js 13+)

'use client';

import { useForm } from 'sndp-easy-form';

export default function ContactPage() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = async (data) => {
    const response = await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    
    if (response.ok) {
      alert('Message sent successfully!');
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name', { required: 'Name is required' })} />
      {errors.name && <p>{errors.name.message}</p>}
      
      <textarea {...register('message', { required: 'Message is required' })} />
      {errors.message && <p>{errors.message.message}</p>}
      
      <button type="submit">Send</button>
    </form>
  );
}

Pages Router (Next.js 12 and below)

import { useForm } from 'sndp-easy-form';

export default function ContactPage() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = async (data) => {
    // Handle form submission
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* Your form fields */}
    </form>
  );
}

🎨 TypeScript Support

import { useForm } from 'sndp-easy-form';

interface FormData {
  username: string;
  email: string;
  age: number;
  terms: boolean;
}

function TypedForm() {
  const { 
    register, 
    handleSubmit, 
    formState: { errors } 
  } = useForm<FormData>({
    defaultValues: {
      username: '',
      email: '',
      age: 0,
      terms: false
    }
  });

  const onSubmit = (data: FormData) => {
    console.log(data);
    // TypeScript knows the exact shape of data
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('username', { required: true })} />
      <input {...register('email', { required: true })} />
      <input type="number" {...register('age', { min: 18 })} />
      <input type="checkbox" {...register('terms', { required: true })} />
      <button type="submit">Submit</button>
    </form>
  );
}

📊 Comparison with Other Libraries

| Feature | sndp-easy-form | react-hook-form | formik | |---------|----------------|-----------------|--------| | Bundle Size | ~9kb | ~8.5kb | ~15kb | | TypeScript | ✅ Full | ✅ Full | ✅ Full | | Uncontrolled | ✅ | ✅ | ❌ | | Performance | ⚡ Excellent | ⚡ Excellent | 🐢 Good | | Learning Curve | 📚 Easy | 📚 Easy | 📚 Medium | | Field Arrays | ✅ | ✅ | ✅ | | Next.js Support | ✅ | ✅ | ✅ |

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License

MIT © sndp bag

🐛 Bug Reports & Feature Requests

If you encounter any bugs or have feature requests, please create an issue on GitHub.

📧 Contact

For questions or support, reach out to: sndp bag


Made with ❤️ by sndp bag