diff --git a/npm-dynamic-form/README.md b/npm-dynamic-form/README.md new file mode 100644 index 0000000..1ae21ea --- /dev/null +++ b/npm-dynamic-form/README.md @@ -0,0 +1,163 @@ +# Dynamic Form Component + +A highly customizable and easy-to-use dynamic form component built with React, TypeScript, and React Hook Form. This component allows you to generate a form dynamically based on an array of field configurations, making it suitable for various use cases. + +## Installation +To install the package, run: +```bash +npm install dynamic-form-component +``` + +## Peer Dependencies +Ensure you have the following peer dependencies installed: +* `react` +* `react-dom` +* `react-hook-form` +* `react-icons` + +You can install them with: +```bash +npm install react react-dom react-hook-form react-icons +``` + +## Usage +### Importing The Component +```bash +import DynamicForm, { IFormField } from 'dynamic-form-component'; +``` +### Example Usage +Below is an example of how to use the `DynamicForm` component in your react project: +```js +import DynamicForm, { IFormField } from 'dynamic-form-component'; +import { RegisterOptions } from 'react-hook-form'; +import { AiOutlineLock, AiOutlineMail, AiOutlineNumber, AiOutlineUser } from 'react-icons/ai'; + +const App = () => { + const formFields: IFormField[] = [ + { + id: 'name', + errorId: 'nameErr', + label: 'Name', + type: 'text', + placeholder: 'Enter your name', + required: true, + validation: { + required: 'Name is required', + maxLength: { + value: 20, + message: 'Name must be less than 20 characters', + }, + }, + icon: <AiOutlineUser /> + }, + { + id: 'email', + errorId: 'emailErr', + label: 'Email', + type: 'email', + placeholder: 'Enter your email', + validation: { + required: 'Email is required', + pattern: { + value: /^\S+@\S+$/i, + message: 'Enter a valid email address', + }, + }, + icon: <AiOutlineMail /> + }, + { + id: 'age', + errorId: 'ageError', + label: 'Age', + type: 'number', + placeholder: 'Enter your age', + validation: { + min: { + value: 18, + message: 'You must be at least 18 years old', + }, + }, + icon: <AiOutlineNumber /> + }, + { + id: 'password', + errorId: 'passwordErr', + label: 'Password', + type: 'password', + placeholder: 'Enter your Password', + validation: { + required: 'Password is required', + }, + icon: <AiOutlineLock /> + }, + { + id: 'hobbies', + errorId: 'subscriptionErr', + label: 'Hobbies', + type: 'checkbox', + options: [ + { value: 'reading', label: "Reading" }, + { value: 'sports', label: "Sports" }, + { value: 'music', label: "Music" }, + { value: 'travelling', label: "Travelling" }, + ], + validation: {}, + }, + { + id: 'gender', + errorId: 'genderErr', + label: 'Gender', + type: 'select', + options: [ + { value: 'male', label: 'Male' }, + { value: 'female', label: 'Female' }, + { value: 'other', label: 'Other' }, + ], + validation: { + required: 'Gender is required', + }, + }, + ]; + + const handleFormSubmit = (data: Record<string, any>) => { + console.log('Form Data:', data); + }; + + return ( + <div> + <h1>Dynamic Form Example</h1> + <DynamicForm fields={formFields} onSubmit={handleFormSubmit} /> + </div> + ) +} + +export default App +``` +### Field Configuration +* id: Unique identifier for the field. +* errorId: Identifier for displaying error messages. +* label: Label for the form field. +* type: Type of the field (text, number, email, password, checkbox, select). +* options: (Optional) Array of options for select and checkbox fields. +* placeholder: (Optional) Placeholder text for the input. +* required: (Optional) Boolean indicating if the field is required. +* validation: (Optional) Validation rules based on React Hook Form's RegisterOptions. +* icon: (Optional) Icon component to display alongside the input. + +### Form Submission +The `onSubmit` function passed to `DynamicForm` will receive the form data as a `Record<string, any>`. You can handle form submission by implementing your own logic in this function. + +Example Form Data +```bash + { + "name": "John Doe", + "email": "john.doe@example.com", + "age": 25, + "password": "password123", + "hobbies": Array ["reading", "travelling"], + "gender": "male" + } +``` + +## Contributing +If you have any ideas, suggestions, or issues, feel free to open an issue or contribute with a pull request. \ No newline at end of file diff --git a/npm-dynamic-form/dist/assets/interfaces.d.ts b/npm-dynamic-form/dist/assets/interfaces.d.ts new file mode 100644 index 0000000..6af711f --- /dev/null +++ b/npm-dynamic-form/dist/assets/interfaces.d.ts @@ -0,0 +1,20 @@ +import { RegisterOptions, FieldValues } from "react-hook-form"; +export interface IFormField { + id: string; + errorId: string; + label: string; + type: 'text' | 'number' | 'email' | 'password' | 'checkbox' | 'select'; + options?: { + value: string; + label: string; + }[]; + placeholder?: string; + required?: boolean; + validation?: RegisterOptions; + icon?: React.ReactNode; + value?: string; +} +export interface DynamicFormProps { + fields: IFormField[]; + onSubmit: (data: FieldValues) => void; +} diff --git a/npm-dynamic-form/dist/assets/interfaces.js b/npm-dynamic-form/dist/assets/interfaces.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/npm-dynamic-form/dist/assets/interfaces.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/npm-dynamic-form/dist/components/DynamicForm.d.ts b/npm-dynamic-form/dist/components/DynamicForm.d.ts new file mode 100644 index 0000000..5816559 --- /dev/null +++ b/npm-dynamic-form/dist/components/DynamicForm.d.ts @@ -0,0 +1,4 @@ +import { DynamicFormProps } from '../assets/interfaces'; +import React from 'react'; +declare const DynamicForm: React.FC<DynamicFormProps>; +export default DynamicForm; diff --git a/npm-dynamic-form/dist/components/DynamicForm.js b/npm-dynamic-form/dist/components/DynamicForm.js new file mode 100644 index 0000000..3119c41 --- /dev/null +++ b/npm-dynamic-form/dist/components/DynamicForm.js @@ -0,0 +1,36 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const react_1 = require("react"); +const react_hook_form_1 = require("react-hook-form"); +const ai_1 = require("react-icons/ai"); +const react_2 = __importDefault(require("react")); +const DynamicForm = ({ fields, onSubmit }) => { + const [showPassword, setShowPassword] = (0, react_1.useState)(false); + const { register, handleSubmit, formState: { errors } } = (0, react_hook_form_1.useForm)({ mode: 'all' }); + const renderField = (field) => { + var _a, _b; + const error = (_a = errors[field.id]) === null || _a === void 0 ? void 0 : _a.message; + const isPasswordField = field.type === 'password'; + const inputType = isPasswordField && showPassword ? 'text' : field.type; + return (react_2.default.createElement("div", { id: 'input-wrapper' }, + react_2.default.createElement("div", { id: 'input-container' }, field.type === 'checkbox' && field.options ? (react_2.default.createElement("div", { className: 'grid grid-cols-2 gap-4' }, field.options.map(option => (react_2.default.createElement("div", { key: option.value, className: 'flex gap-1 items-center justify-center' }, + react_2.default.createElement("label", { htmlFor: `${field.id}-${option.value}`, className: 'ml-2 mt-1' }, + option.label, + " :"), + react_2.default.createElement("input", Object.assign({ id: `${field.id}-${option.value}` }, register(field.id, Object.assign({}, field.validation)), { type: field.type, value: option.value, className: 'w-max' }))))))) : field.type === 'select' ? (react_2.default.createElement("select", Object.assign({}, register(field.id, Object.assign({}, field.validation)), { id: field.id }), (_b = field.options) === null || _b === void 0 ? void 0 : _b.map((option) => (react_2.default.createElement("option", { key: option.value, value: option.value }, option.label))))) : (react_2.default.createElement("div", { className: 'flex items-center p-1 border' }, + field.icon && react_2.default.createElement("div", { className: 'ml-2' }, field.icon), + react_2.default.createElement("input", Object.assign({ id: field.id }, register(field.id, Object.assign({}, field.validation)), { type: inputType, placeholder: field.placeholder })), + isPasswordField && (react_2.default.createElement("div", { id: "prop-icon", className: "hover:cursor-pointer mr-3", onClick: () => setShowPassword(!showPassword) }, showPassword ? react_2.default.createElement(ai_1.AiOutlineEye, null) : react_2.default.createElement(ai_1.AiOutlineEyeInvisible, null)))))), + error && react_2.default.createElement("span", { id: field.errorId }, error))); + }; + return (react_2.default.createElement("main", { className: 'md:w-3/4 m-auto md:h-0' }, + react_2.default.createElement("form", { onSubmit: handleSubmit(onSubmit), className: "flex flex-col md:w-1/2 m-auto p-5 gap-5 md:mt-5 md:border border-black" }, + fields.map((field) => (react_2.default.createElement("div", { key: field.id }, + react_2.default.createElement("label", { htmlFor: field.id }, field.label), + renderField(field)))), + react_2.default.createElement("button", { type: 'submit', disabled: Object.keys(errors).length ? true : false, className: 'px-4 py-2 bg-slate-400 hover:bg-slate-600 hover:transition hover:text-white rounded-lg disabled:bg-gray-600 disabled:hover:bg-gray-600' }, "Submit")))); +}; +exports.default = DynamicForm; diff --git a/npm-dynamic-form/dist/index.d.ts b/npm-dynamic-form/dist/index.d.ts new file mode 100644 index 0000000..08beb73 --- /dev/null +++ b/npm-dynamic-form/dist/index.d.ts @@ -0,0 +1,4 @@ +import DynamicForm from './components/DynamicForm'; +import { IFormField, DynamicFormProps } from './assets/interfaces'; +export default DynamicForm; +export { IFormField, DynamicFormProps }; diff --git a/npm-dynamic-form/dist/index.js b/npm-dynamic-form/dist/index.js new file mode 100644 index 0000000..1d11079 --- /dev/null +++ b/npm-dynamic-form/dist/index.js @@ -0,0 +1,7 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const DynamicForm_1 = __importDefault(require("./components/DynamicForm")); +exports.default = DynamicForm_1.default; diff --git a/npm-dynamic-form/package-lock.json b/npm-dynamic-form/package-lock.json new file mode 100644 index 0000000..1972190 --- /dev/null +++ b/npm-dynamic-form/package-lock.json @@ -0,0 +1,147 @@ +{ + "name": "dynamic-form-component", + "version": "1.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dynamic-form-component", + "version": "1.0.1", + "license": "MIT", + "devDependencies": { + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "react-hook-form": "^7.0.0", + "react-icons": "^4.0.0", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-hook-form": "^7.0.0", + "react-icons": "^4.0.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", + "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.53.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz", + "integrity": "sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==", + "dev": true, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-icons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", + "dev": true, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/npm-dynamic-form/package.json b/npm-dynamic-form/package.json new file mode 100644 index 0000000..7ff7b4b --- /dev/null +++ b/npm-dynamic-form/package.json @@ -0,0 +1,41 @@ +{ + "name": "dynamic-form-component", + "version": "1.0.12", + "description": "A dynamic form component for React that renders forms based on provided configuration.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "prepare": "npm run build" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-hook-form": "^7.0.0", + "react-icons": "^4.0.0" + }, + "devDependencies": { + "typescript": "^5.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "react-hook-form": "^7.0.0", + "react-icons": "^4.0.0" + }, + "files": [ + "dist", + "README.md" + ], + "author": "Shrey Purohit", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/ShreyPurohit/Medium/tree/6c29c1c77ee5a29c4b5915e6669c28449931dbbe/React-Projects/dynamic-form-medium" + }, + "keywords": [ + "react", + "form", + "dynamic", + "react-hook-form", + "typescript" + ] +} \ No newline at end of file diff --git a/npm-dynamic-form/src/assets/interfaces.ts b/npm-dynamic-form/src/assets/interfaces.ts new file mode 100644 index 0000000..7c150cf --- /dev/null +++ b/npm-dynamic-form/src/assets/interfaces.ts @@ -0,0 +1,19 @@ +import { RegisterOptions, FieldValues } from "react-hook-form"; + +export interface IFormField { + id: string; + errorId: string; + label: string; + type: 'text' | 'number' | 'email' | 'password' | 'checkbox' | 'select'; + options?: { value: string; label: string }[]; + placeholder?: string; + required?: boolean; + validation?: RegisterOptions; + icon?: React.ReactNode; + value?: string +} + +export interface DynamicFormProps { + fields: IFormField[]; + onSubmit: (data: FieldValues) => void; +} \ No newline at end of file diff --git a/npm-dynamic-form/src/components/DynamicForm.tsx b/npm-dynamic-form/src/components/DynamicForm.tsx new file mode 100644 index 0000000..f1a8be1 --- /dev/null +++ b/npm-dynamic-form/src/components/DynamicForm.tsx @@ -0,0 +1,98 @@ +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai'; +import { DynamicFormProps, IFormField } from '../assets/interfaces'; +import React from 'react'; + + +const DynamicForm: React.FC<DynamicFormProps> = ({ fields, onSubmit }) => { + const [showPassword, setShowPassword] = useState<boolean>(false); + + const { register, handleSubmit, formState: { errors } } = useForm({ mode: 'all' }); + + const renderField = (field: IFormField) => { + const error = errors[field.id]?.message as string; + + const isPasswordField = field.type === 'password'; + const inputType = isPasswordField && showPassword ? 'text' : field.type; + + return ( + <div id='input-wrapper'> + <div id='input-container'> + { + field.type === 'checkbox' && field.options ? ( + <div className='grid grid-cols-2 gap-4'> + {field.options.map(option => ( + <div key={option.value} className='flex gap-1 items-center justify-center'> + <label htmlFor={`${field.id}-${option.value}`} className='ml-2 mt-1'>{option.label} :</label> + <input + id={`${field.id}-${option.value}`} + {...register(field.id, { ...field.validation })} + type={field.type} + value={option.value} + className='w-max' + /> + </div> + ))} + </div> + ) : field.type === 'select' ? ( + <select + {...register(field.id, { ...field.validation })} + id={field.id} + > + {field.options?.map((option) => ( + <option key={option.value} value={option.value}> + {option.label} + </option> + ))} + </select> + ) : ( + <div className='flex items-center p-1 border'> + {field.icon && <div className='ml-2'>{field.icon}</div>} + <input + id={field.id} + {...register(field.id, { ...field.validation })} + type={inputType} + placeholder={field.placeholder} + /> + {isPasswordField && ( + <div + id="prop-icon" + className="hover:cursor-pointer mr-3" + onClick={() => setShowPassword(!showPassword)} + > + {showPassword ? <AiOutlineEye /> : <AiOutlineEyeInvisible />} + </div> + )} + </div> + ) + } + </div> + {error && <span id={field.errorId}>{error}</span>} + </div> + ); + }; + + return ( + <main className='md:w-3/4 m-auto md:h-0'> + <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col md:w-1/2 m-auto p-5 gap-5 md:mt-5 md:border border-black"> + {fields.map((field) => ( + <div key={field.id}> + <label htmlFor={field.id}>{field.label}</label> + {renderField(field)} + </div> + )) + } + <button + type='submit' + disabled={Object.keys(errors).length ? true : false} + className='px-4 py-2 bg-slate-400 hover:bg-slate-600 hover:transition hover:text-white rounded-lg disabled:bg-gray-600 disabled:hover:bg-gray-600' + > + Submit + </button> + </form > + </main> + ); +}; + +export default DynamicForm; \ No newline at end of file diff --git a/npm-dynamic-form/src/index.ts b/npm-dynamic-form/src/index.ts new file mode 100644 index 0000000..164f884 --- /dev/null +++ b/npm-dynamic-form/src/index.ts @@ -0,0 +1,5 @@ +import DynamicForm from './components/DynamicForm' +import { IFormField, DynamicFormProps } from './assets/interfaces' + +export default DynamicForm +export { IFormField, DynamicFormProps } \ No newline at end of file diff --git a/npm-dynamic-form/tsconfig.json b/npm-dynamic-form/tsconfig.json new file mode 100644 index 0000000..832b21d --- /dev/null +++ b/npm-dynamic-form/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": [ + "src" + ], /* Include only the src directory */ + "compilerOptions": { + "jsx": "react", + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "module": "commonjs" /* Specify what module code is generated. */, + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, + "outDir": "dist", + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + "strict": true /* Enable all strict type-checking options. */, + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} \ No newline at end of file