├── src ├── vite-env.d.ts ├── App.css ├── utils │ └── index.ts ├── hooks │ └── useRenderCount.tsx ├── pages │ ├── common │ │ └── FormLoader.tsx │ └── FoodDelivery │ │ ├── components │ │ ├── DeliveryAddressForm.tsx │ │ ├── CheckoutForm.tsx │ │ ├── MasterFoodDeliveryForm.tsx │ │ └── OrderedFoodItems.tsx │ │ └── FoodDeliveryForm.tsx ├── main.tsx ├── App.tsx ├── controls │ ├── TextField.tsx │ ├── Select.tsx │ └── SubmitButton.tsx ├── types │ └── index.ts ├── db │ └── index.ts ├── index.css ├── TypicalForm.tsx └── assets │ └── react.svg ├── vite.config.ts ├── tsconfig.node.json ├── .gitignore ├── README.md ├── .eslintrc.cjs ├── tsconfig.json ├── index.html ├── package.json └── public └── vite.svg /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const roundTo2DecimalPoint = (value: number) => 2 | Math.round((value + Number.EPSILON) * 100) / 100 -------------------------------------------------------------------------------- /src/hooks/useRenderCount.tsx: -------------------------------------------------------------------------------- 1 | export const useRenderCount = () => { 2 | let count = 0 3 | 4 | return () => { 5 | count++ 6 | return
Render count : {count/2}
7 | } 8 | } 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/common/FormLoader.tsx: -------------------------------------------------------------------------------- 1 | import { useFormState } from "react-hook-form" 2 | 3 | export default function FormLoader(props:any) { 4 | const { control } = props 5 | const { isLoading } = useFormState({ control }) 6 | return isLoading == false ||
7 | } 8 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css" 2 | import { FoodDeliveryForm } from "./pages/FoodDelivery/FoodDeliveryForm" 3 | import { TypicalForm } from "./TypicalForm" 4 | 5 | function App() { 6 | return ( 7 | <> 8 |
9 |
10 | 11 |
12 |
13 | 14 | ) 15 | } 16 | 17 | export default App 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # First Demo Project with Food Delivery App. 2 | ### This a Demo Project Created for the [Udemy Course - React Hook Form](https://bit.ly/3WQCZSK). 3 | ![Form Created with React Hook Form](https://github.com/CodAffection/Premium-React-Hook-Form-Course-with-Food-Delivery-App/assets/32505654/255b4daf-7254-49ce-b6f5-3b2f6fab8562) 4 | 5 | 6 | ![React Hook Form](https://github.com/CodAffection/Premium-React-Hook-Form-Course-with-Food-Delivery-App/assets/32505654/11693887-e57e-4668-9d3c-cad3442286e6) 7 | 8 | 9 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.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 | "resolveJsonModule": true, 13 | "isolatedModules": true, 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 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/controls/TextField.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, ForwardedRef } from "react" 2 | import { FieldError } from "react-hook-form" 3 | 4 | type TextFieldProps = React.InputHTMLAttributes & { 5 | label?: string 6 | error?: FieldError | undefined 7 | } 8 | 9 | export const TextField = forwardRef( 10 | (props: TextFieldProps, ref: ForwardedRef) => { 11 | const { type = "text", className = "", label, error, ...other } = props 12 | 13 | return ( 14 |
15 | 22 | {label && } 23 | {error &&
{error.message}
} 24 |
25 | ) 26 | } 27 | ) 28 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | Vite + React + TS 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "food-delivery-form", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-hook-form": "^7.46.1" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.2.15", 19 | "@types/react-dom": "^18.2.7", 20 | "@typescript-eslint/eslint-plugin": "^6.0.0", 21 | "@typescript-eslint/parser": "^6.0.0", 22 | "@vitejs/plugin-react": "^4.0.3", 23 | "eslint": "^8.45.0", 24 | "eslint-plugin-react-hooks": "^4.6.0", 25 | "eslint-plugin-react-refresh": "^0.4.3", 26 | "typescript": "^5.0.2", 27 | "vite": "^4.4.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | type CheckoutFormType = { 2 | paymentMethod: string 3 | deliveryIn: number 4 | } 5 | 6 | type DeliveryAddressFormType = { 7 | streetAddress: string 8 | landmark: string 9 | city: string 10 | state: string 11 | } 12 | 13 | 14 | type FoodDeliveryFormType = { 15 | address: DeliveryAddressFormType 16 | foodItems: OrderedFoodItemType[] 17 | } & MasterFoodDeliveryFormType & CheckoutFormType 18 | 19 | type FoodType = { 20 | foodId: number, 21 | name: string, 22 | price: number 23 | } 24 | 25 | type MasterFoodDeliveryFormType = { 26 | orderId: number 27 | orderNo: number 28 | customerName: string 29 | mobile: string 30 | email: string 31 | gTotal: number 32 | placedOn: Date 33 | } 34 | 35 | 36 | type OrderedFoodItemType = { 37 | foodId: number, 38 | price: number, 39 | quantity: number, 40 | totalPrice: number, 41 | } 42 | 43 | type SelectOptionType = string | 44 | { value: string, text: string } | 45 | { value: number, text: string } -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | export const getFoodItems = () => { 2 | return [ 3 | { foodId: 1, name: "Chicken Tenders", price: 3.5 }, 4 | { foodId: 2, name: "GC. Sandwich", price: 3.99 }, 5 | { foodId: 3, name: "Soup", price: 2.5 }, 6 | { foodId: 4, name: "Onion Rings", price: 2.99 }, 7 | { foodId: 5, name: "Fries", price: 1.99 }, 8 | { foodId: 6, name: "SP. Fries", price: 2.49 }, 9 | { foodId: 7, name: "Sweet Tea", price: 1.79 }, 10 | { foodId: 8, name: "Botttle Water", price: 1 }, 11 | { foodId: 9, name: "Canned Drinks", price: 1 }, 12 | ] as FoodType[] 13 | } 14 | 15 | const ORDER_KEY = 'order' 16 | export const createOrder = (order: FoodDeliveryFormType) => { 17 | localStorage.setItem(ORDER_KEY, JSON.stringify(order)) 18 | } 19 | 20 | export const fetchLastOrder = async () => { 21 | await new Promise((resolve) => setTimeout(resolve, 2000)) 22 | const order = localStorage.getItem(ORDER_KEY) 23 | if (order == null) return null 24 | else return JSON.parse(order) 25 | } -------------------------------------------------------------------------------- /src/controls/Select.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, ForwardedRef } from "react" 2 | import { FieldError } from "react-hook-form" 3 | 4 | type SelectFieldProps = React.SelectHTMLAttributes & { 5 | label?: string 6 | error?: FieldError | undefined 7 | options: SelectOptionType[] 8 | } 9 | 10 | export const Select = forwardRef( 11 | (props: SelectFieldProps, ref: ForwardedRef) => { 12 | const { className = "", label, options, error, ...other } = props 13 | 14 | return ( 15 |
16 | 23 | {label && } 24 | {error &&
{error.message}
} 25 |
26 | ) 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /src/controls/SubmitButton.tsx: -------------------------------------------------------------------------------- 1 | import { Control, useFormState } from "react-hook-form" 2 | import { useRenderCount } from "../hooks/useRenderCount" 3 | 4 | type SubmitButtonProps = React.ButtonHTMLAttributes & { 5 | control?: Control 6 | } 7 | 8 | 9 | export default function SubmitButton(props: SubmitButtonProps) { 10 | const { 11 | className = "btn-light", 12 | value, 13 | control = undefined, 14 | ...other 15 | } = props 16 | 17 | let isSubmitting = undefined 18 | if (control) ({ isSubmitting } = useFormState({ control })) 19 | 20 | return ( 21 | <> 22 | 42 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /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 | font-synthesis: none; 7 | text-rendering: optimizeLegibility; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-text-size-adjust: 100%; 11 | } 12 | 13 | body { 14 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif !important; 15 | margin: 0; 16 | display: flex; 17 | place-items: center; 18 | min-width: 320px; 19 | min-height: 100vh; 20 | } 21 | 22 | .error-feedback { 23 | width: 100%; 24 | margin-top: 0.25rem; 25 | font-size: 0.875em; 26 | color: #ea868f; 27 | } 28 | 29 | #foodItems { 30 | max-width: 600px; 31 | min-width: 600px; 32 | } 33 | 34 | #foodItems thead tr th:not(:first-child):not(:last-child) { 35 | width: 15%; 36 | } 37 | 38 | /* loading spinner */ 39 | .loader { 40 | position: absolute; 41 | left: 48%; 42 | top: 50%; 43 | z-index: 2147483647; 44 | width: 60px; 45 | height: 60px; 46 | border-radius: 75%; 47 | display: inline-block; 48 | border-top: 6px solid #fff; 49 | border-right: 6px solid transparent; 50 | box-sizing: border-box; 51 | animation: rotation 1s linear infinite; 52 | } 53 | 54 | @keyframes rotation { 55 | 0% { 56 | transform: rotate(0deg); 57 | } 58 | 100% { 59 | transform: rotate(360deg); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/FoodDelivery/components/DeliveryAddressForm.tsx: -------------------------------------------------------------------------------- 1 | import { useFormContext, useFormState } from "react-hook-form" 2 | import { TextField } from "../../../controls/TextField" 3 | import { useRenderCount } from "../../../hooks/useRenderCount" 4 | 5 | const RenderCount = useRenderCount() 6 | 7 | export const DeliveryAddressForm = () => { 8 | const { register } = useFormContext<{ address: DeliveryAddressFormType }>() 9 | 10 | const { errors } = useFormState<{ address: DeliveryAddressFormType }>({ 11 | name: "address", 12 | }) 13 | 14 | return ( 15 | <> 16 | {/* */} 17 |
Delivery Address
18 |
19 |
20 | 27 |
28 |
29 | 36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 | 44 |
45 |
46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/pages/FoodDelivery/components/CheckoutForm.tsx: -------------------------------------------------------------------------------- 1 | import { useFormContext, useFormState, useWatch } from "react-hook-form" 2 | import { Select } from "../../../controls/Select" 3 | import { useRenderCount } from "../../../hooks/useRenderCount" 4 | import { useEffect } from "react" 5 | 6 | const paymentOptions: SelectOptionType[] = [ 7 | { value: "", text: "Select" }, 8 | { value: "online", text: "Paid Online" }, 9 | { value: "COD", text: "Cash On Delivery" }, 10 | ] 11 | 12 | const deliveryInOptions: SelectOptionType[] = [ 13 | { value: 0, text: "Select" }, 14 | { value: 30, text: "Half an Hour" }, 15 | { value: 60, text: "1 Hour" }, 16 | { value: 120, text: "2 Hour" }, 17 | { value: 180, text: "3 Hour" }, 18 | ] 19 | 20 | const RenderCount = useRenderCount() 21 | 22 | export const CheckoutForm = () => { 23 | const { 24 | register 25 | } = useFormContext() 26 | 27 | const { errors } = useFormState({ 28 | name: ["paymentMethod", "deliveryIn"], 29 | exact:true, 30 | }) 31 | 32 | 33 | return ( 34 | <> 35 | {/* */} 36 |
Checkout Details
37 |
38 |
39 | 56 |
57 |
58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/pages/FoodDelivery/components/MasterFoodDeliveryForm.tsx: -------------------------------------------------------------------------------- 1 | import { useFormContext, useFormState } from "react-hook-form" 2 | import { TextField } from "../../../controls/TextField" 3 | import { useRenderCount } from "../../../hooks/useRenderCount" 4 | 5 | const RenderCount = useRenderCount() 6 | 7 | export const MasterFoodDeliveryForm = () => { 8 | const { register } = useFormContext() 9 | 10 | const { errors } = useFormState({ 11 | name: ["orderNo", "customerName", "mobile", "email"], 12 | exact: true, 13 | }) 14 | 15 | return ( 16 | <> 17 | {/* */} 18 | 19 | 20 |
21 |
22 | 23 |
24 |
25 | 32 |
33 |
34 |
35 |
36 | 43 |
44 |
45 | 56 |
57 |
58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/TypicalForm.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, SyntheticEvent, useState } from "react" 2 | import { useRenderCount } from "./hooks/useRenderCount" 3 | 4 | type FoodDeliveryFormType = { 5 | customerName: string 6 | mobile: string 7 | } 8 | 9 | type FoodDeliveryFormErrorType = { 10 | customerName: string 11 | mobile: string 12 | } 13 | 14 | const RenderCount = useRenderCount() 15 | 16 | export const TypicalForm = () => { 17 | const [values, setValues] = useState({ 18 | customerName: "", 19 | mobile: "", 20 | }) 21 | const [errors, setErrors] = useState({ 22 | customerName: "", 23 | mobile: "", 24 | }) 25 | 26 | const handleInputChange = (e: ChangeEvent) => { 27 | const { name, value } = e.target 28 | setValues({ ...values, [name]: value }) 29 | } 30 | 31 | const validateFormData = () => { 32 | let tempErrors: FoodDeliveryFormErrorType = { 33 | customerName: "", 34 | mobile: "", 35 | } 36 | if (values.customerName == "") 37 | tempErrors.customerName = "Customer name is required." 38 | if (values.mobile == "") tempErrors.mobile = "Mobile number is required." 39 | setErrors(tempErrors) 40 | 41 | return Object.values(tempErrors).every((x) => x == "") 42 | } 43 | 44 | const onSubmit = (e: SyntheticEvent) => { 45 | e.preventDefault() 46 | if (validateFormData()) console.log("form data", values) 47 | else console.log("form is invalid") 48 | } 49 | 50 | return ( 51 |
52 | 53 |
54 | 62 | 63 |
64 |
65 | 73 | 74 |
75 | 78 | 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /src/pages/FoodDelivery/FoodDeliveryForm.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useForm, 3 | FieldErrors, 4 | UseFormReturn, 5 | FormProvider, 6 | useWatch, 7 | } from "react-hook-form" 8 | import { useRenderCount } from "../../hooks/useRenderCount" 9 | import { CheckoutForm } from "./components/CheckoutForm" 10 | import { DeliveryAddressForm } from "./components/DeliveryAddressForm" 11 | import { MasterFoodDeliveryForm } from "./components/MasterFoodDeliveryForm" 12 | import SubmitButton from "../../controls/SubmitButton" 13 | import OrderedFoodItems from "./components/OrderedFoodItems" 14 | import { createOrder, fetchLastOrder } from "../../db" 15 | import FormLoader from "../common/FormLoader" 16 | 17 | const RenderCount = useRenderCount() 18 | 19 | const id: number = 0 20 | 21 | const initialValues: FoodDeliveryFormType = { 22 | orderId: 0, 23 | orderNo: new Date().valueOf(), 24 | customerName: "", 25 | mobile: "", 26 | email: "", 27 | placedOn: new Date(), 28 | gTotal: 0, 29 | paymentMethod: "", 30 | deliveryIn: 0, 31 | foodItems: [{ foodId: 0, price: 0, quantity: 0, totalPrice: 0 }], 32 | address: { 33 | streetAddress: "", 34 | landmark: "", 35 | city: "", 36 | state: "", 37 | }, 38 | } 39 | 40 | export const FoodDeliveryForm = () => { 41 | const methods: UseFormReturn = 42 | useForm({ 43 | mode: "onChange", 44 | shouldUnregister: true, 45 | defaultValues: async (): Promise => { 46 | if (id == 0) return new Promise((resolve) => resolve(initialValues)) 47 | else { 48 | const tempOrder = await fetchLastOrder() 49 | return new Promise((resolve) => 50 | resolve(tempOrder ? tempOrder : initialValues) 51 | ) 52 | } 53 | }, 54 | }) 55 | 56 | const { 57 | handleSubmit, 58 | control, 59 | resetField, 60 | reset, 61 | setError, 62 | clearErrors 63 | } = methods 64 | 65 | const onSubmit = async (formData: FoodDeliveryFormType) => { 66 | //add a delay 67 | await new Promise((resolve) => setTimeout(resolve, 1500)) 68 | formData.orderId = 1 69 | formData.placedOn = new Date() 70 | // setError("email",{type:"duplicateEmail",message:"The email already taken."}) 71 | // createOrder(formData) 72 | console.log("submitted form data", formData) 73 | } 74 | 75 | const onError = (err: FieldErrors) => { 76 | console.log("validation errors", err) 77 | } 78 | 79 | const onReset = () => { 80 | reset(initialValues, { keepErrors: true }) 81 | // clearErrors() 82 | } 83 | 84 | return ( 85 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 106 | 107 | ) 108 | } 109 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/FoodDelivery/components/OrderedFoodItems.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useFieldArray, 3 | useFormContext, 4 | useFormState, 5 | useWatch, 6 | } from "react-hook-form" 7 | import { TextField } from "../../../controls/TextField" 8 | import { useRenderCount } from "../../../hooks/useRenderCount" 9 | import { useState, useEffect, ChangeEvent } from "react" 10 | import { getFoodItems } from "../../../db" 11 | import { Select } from "../../../controls/Select" 12 | import { roundTo2DecimalPoint } from "../../../utils" 13 | 14 | const RenderCount = useRenderCount() 15 | 16 | export default function OrderedFoodItems() { 17 | const [foodList, setFoodList] = useState([]) 18 | const [foodOptions, setFoodOptions] = useState([]) 19 | 20 | useEffect(() => { 21 | const tempList: FoodType[] = getFoodItems() 22 | const tempOptions: SelectOptionType[] = tempList.map((x) => ({ 23 | value: x.foodId, 24 | text: x.name, 25 | })) 26 | setFoodList(tempList) 27 | setFoodOptions([{ value: 0, text: "Select" }, ...tempOptions]) 28 | }, []) 29 | 30 | const { register, setValue, getValues, trigger } = useFormContext< 31 | { gTotal: number } & { 32 | foodItems: OrderedFoodItemType[] 33 | } 34 | >() 35 | 36 | const { errors } = useFormState<{ foodItems: OrderedFoodItemType[] }>({ 37 | name: "foodItems", 38 | }) 39 | 40 | const { fields, append, remove } = useFieldArray<{ 41 | foodItems: OrderedFoodItemType[] 42 | }>({ 43 | name: "foodItems", 44 | rules: { 45 | required: { 46 | value: true, 47 | message: "No food in the order.", 48 | }, 49 | }, 50 | }) 51 | 52 | const selectedFoodItems: OrderedFoodItemType[] = useWatch({ 53 | name: "foodItems", 54 | }) 55 | useWatch({ name: "gTotal" }) 56 | 57 | useEffect(() => { 58 | updateGTotal() 59 | }, [selectedFoodItems]) 60 | 61 | const updateGTotal = () => { 62 | let gTotal = 0 63 | if (selectedFoodItems && selectedFoodItems.length > 0) 64 | gTotal = selectedFoodItems.reduce((sum, curr) => sum + curr.totalPrice, 0) 65 | setValue("gTotal", roundTo2DecimalPoint(gTotal)) 66 | } 67 | 68 | const onRowAdd = () => { 69 | append({ foodId: 0, price: 0, quantity: 0, totalPrice: 0 }) 70 | } 71 | 72 | const onRowDelete = (index: number) => { 73 | remove(index) 74 | } 75 | 76 | const onFoodChange = ( 77 | e: ChangeEvent, 78 | rowIndex: number 79 | ) => { 80 | const foodId = parseInt(e.target.value) 81 | let price: number 82 | if (foodId == 0) price = 0 83 | else price = foodList.find((x) => x.foodId == foodId)?.price || 0 84 | setValue(`foodItems.${rowIndex}.price`, price) 85 | updateRowTotalPrice(rowIndex) 86 | } 87 | 88 | const updateRowTotalPrice = (rowIndex: number) => { 89 | const { price, quantity } = getValues(`foodItems.${rowIndex}`) 90 | let totalPrice = 0 91 | if (quantity && quantity > 0) totalPrice = price * quantity 92 | setValue( 93 | `foodItems.${rowIndex}.totalPrice`, 94 | roundTo2DecimalPoint(totalPrice) 95 | ) 96 | } 97 | 98 | return ( 99 | <> 100 | {/* */} 101 |
Ordered Food Items
102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 118 | 119 | 120 | 121 | {fields.map((field, index) => ( 122 | 123 | 140 | 143 | 171 | 174 | 183 | 184 | ))} 185 | 186 | 187 | 188 | {fields && fields.length > 0 && ( 189 | 190 | 191 | 192 | 195 | 196 | 197 | )} 198 | {errors.foodItems?.root && ( 199 | 200 | 205 | 206 | )} 207 | 208 |
FoodPriceQuantityT. Price 110 | 117 |
124 | 141 | {"$" + getValues(`foodItems.${index}.price`)} 142 | 144 | { 153 | //add a delay 154 | await new Promise((resolve) => 155 | setTimeout(resolve, 1000) 156 | ) 157 | if (value && value > 9) return "OOS." 158 | else return true 159 | }, 160 | }, 161 | min: { 162 | value: 1, 163 | message: "< 1.", 164 | }, 165 | onChange: () => { 166 | updateRowTotalPrice(index) 167 | }, 168 | })} 169 | /> 170 | 172 | {"$" + getValues(`foodItems.${index}.totalPrice`)} 173 | 175 | 182 |
G. Total 193 | {"$" + getValues("gTotal")} 194 |
201 | 202 | {errors.foodItems?.root?.message} 203 | 204 |
209 | 210 | ) 211 | } 212 | --------------------------------------------------------------------------------