├── .gitignore ├── README.md ├── bun.lockb ├── eslint.config.js ├── index.html ├── package.json ├── public └── vite.svg ├── src ├── App.css ├── App.tsx ├── assets │ └── react.svg ├── components │ └── CustomForm │ │ ├── CustomForm.tsx │ │ ├── components │ │ ├── CustomInput.css │ │ ├── CustomInput.tsx │ │ └── index.ts │ │ ├── index.ts │ │ └── models │ │ ├── form.model.ts │ │ └── index.ts ├── index.css ├── main.tsx └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📝 React Custom Form with Zod Validation 2 | 3 | This repository contains an example of a custom form in React using `react-hook-form` and `zod` for data validation. The form includes fields for name, email, password, and password confirmation, all validated according to a schema defined with `zod`. 4 | 5 | ## 🚀 Technologies Used 6 | 7 | - **React**: JavaScript library for building user interfaces. 8 | - **React Hook Form**: Library for managing forms in React simply and flexibly. 9 | - **Zod**: Library for schema-based data validation. 10 | 11 | - **TypeScript**: A superset of JavaScript that adds static types to the language. 12 | 13 | ## 📂 Project Structure 14 | 15 | 16 | ```bash 17 | src/ 18 | │ 19 | 20 | ├── components/ 21 | │ ├── CustomForm/ 22 | │ │ └── CustomForm.tsx # Main form component 23 | │ └── CustomInput/ 24 | │ └── CustomInput.tsx # Reusable component for form fields 25 | │ 26 | ├── models/ 27 | │ └── index.ts # Type definitions and validation schema with Zod 28 | │ 29 | ├── App.tsx # Main application component 30 | ├── index.tsx # React entry point 31 | └── App.css # General application styles 32 | ``` 33 | 34 | ## 🛠️ Installation 35 | 36 | 1. Clone the repository: 37 | ```bash 38 | 39 | git clone https://github.com/your-username/repo-name.git 40 | cd repo-name 41 | ``` 42 | 43 | 2. Install dependencies: 44 | ```bash 45 | npm install 46 | ``` 47 | 48 | 3. Run the application: 49 | ```bash 50 | npm start 51 | ``` 52 | 53 | 54 | ## 📜 Usage 55 | 56 | This project provides a solid base for creating forms in React with robust validation. The modular structure allows you to easily add new fields and validations. 57 | 58 | ### Customization 59 | 60 | You can modify the validation schema in `src/models/index.ts` to suit the needs of your form. 61 | 62 | ### Example Schema with Zod 63 | 64 | ```typescript 65 | import * as z from "zod"; 66 | 67 | export const schema = z.object({ 68 | name: z.string().min(1, "Name is required"), 69 | email: z.string().email("Must be a valid email"), 70 | password: z.string().min(8, "Password must be at least 8 characters long"), 71 | confirmPassword: z.string().min(8, "You must confirm your password") 72 | .refine((val, ctx) => val === ctx.parent.password, { 73 | message: "Passwords must match", 74 | }), 75 | }); 76 | 77 | 78 | export type FormValues = z.infer; 79 | 80 | ``` 81 | 82 | 83 | ## 🤝 Contributions 84 | 85 | Contributions are welcome! If you have any improvements or find any bugs, feel free to open an issue or submit a pull request. 86 | 87 | 88 | ## 📝 License 89 | 90 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. 91 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gentleman-Programming/Gentleman-React-Form/5b5859f752fbc7c94f3fad724109e1e2a3fa9d03/bun.lockb -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gentlemanform", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@hookform/resolvers": "^3.9.0", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.3.1", 16 | "react-hook-form": "^7.52.2", 17 | "zod": "^3.23.8" 18 | }, 19 | "devDependencies": { 20 | "@eslint/js": "^9.9.0", 21 | "@types/react": "^18.3.3", 22 | "@types/react-dom": "^18.3.0", 23 | "@vitejs/plugin-react": "^4.3.1", 24 | "eslint": "^9.9.0", 25 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 26 | "eslint-plugin-react-refresh": "^0.4.9", 27 | "globals": "^15.9.0", 28 | "typescript": "^5.5.3", 29 | "typescript-eslint": "^8.0.1", 30 | "vite": "^5.4.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.css' 2 | import CustomForm from './components/CustomForm/CustomForm' 3 | 4 | function App() { 5 | 6 | return ( 7 | <> 8 | 9 | 10 | ) 11 | } 12 | 13 | export default App 14 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/CustomForm/CustomForm.tsx: -------------------------------------------------------------------------------- 1 | import { zodResolver } from "@hookform/resolvers/zod"; 2 | import { SubmitHandler, useForm } from "react-hook-form"; 3 | import InputForm from "./components/CustomInput"; 4 | import { FormValues, schema } from "./models"; 5 | 6 | const CustomForm = () => { 7 | const { control, handleSubmit, formState: { errors } } = useForm({ 8 | resolver: zodResolver(schema), 9 | mode: "onBlur" 10 | }); 11 | 12 | const onSubmit: SubmitHandler = (data) => { 13 | console.log(data) 14 | } 15 | 16 | return ( 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | export default CustomForm; 28 | -------------------------------------------------------------------------------- /src/components/CustomForm/components/CustomInput.css: -------------------------------------------------------------------------------- 1 | 2 | .is-invalid { 3 | border-color: red; 4 | } 5 | 6 | .error { 7 | color: red; 8 | max-width: 200px; 9 | margin-top: 0.25rem; 10 | } 11 | 12 | .form-group { 13 | margin-bottom: 1rem; 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | .form-group label { 19 | margin-bottom: 0.5rem; 20 | font-weight: bold; 21 | } 22 | 23 | .form-group input { 24 | max-width: 200px; 25 | } 26 | 27 | .form-control { 28 | padding: 0.5rem; 29 | border: 1px solid grey; 30 | border-radius: 0.25rem; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/components/CustomForm/components/CustomInput.tsx: -------------------------------------------------------------------------------- 1 | import { Control, Controller, FieldError } from "react-hook-form"; 2 | import './CustomInput.css' 3 | import { FormValues } from "../models"; 4 | 5 | interface Props { 6 | name: keyof FormValues; 7 | control: Control; 8 | label: string; 9 | type?: string; 10 | error?: FieldError; 11 | } 12 | 13 | const InputForm = ({ name, control, label, type, error }: Props) => { 14 | return ( 15 |
16 | 17 | 21 | 22 | } 23 | /> 24 | {error &&

{error.message}

} 25 |
26 | ) 27 | } 28 | 29 | export default InputForm; 30 | -------------------------------------------------------------------------------- /src/components/CustomForm/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CustomInput' 2 | -------------------------------------------------------------------------------- /src/components/CustomForm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CustomForm' 2 | -------------------------------------------------------------------------------- /src/components/CustomForm/models/form.model.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const schema = z.object({ 4 | name: z.string().min(1, "El nombre es obligatorio"), 5 | email: z.string().email("Correo inválido").min(1, "El correo es obligatorio"), 6 | password: z.string().min(6, "La contraseña debe de tener al menos 6 caracteres"), 7 | confirmPassword: z.string().min(6, "La confirmación debe tener al menos 6 caracteres") 8 | }).refine(data => data.password === data.confirmPassword, { 9 | message: "Las contraseñas son diferentes", 10 | path: ['confirmPassword'] 11 | }) 12 | 13 | export type FormValues = z.infer; 14 | -------------------------------------------------------------------------------- /src/components/CustomForm/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './form.model' 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | @media (prefers-color-scheme: light) { 58 | :root { 59 | color: #213547; 60 | background-color: #ffffff; 61 | } 62 | a:hover { 63 | color: #747bff; 64 | } 65 | button { 66 | background-color: #f9f9f9; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | --------------------------------------------------------------------------------