Processing Form Data Using Next.js 15 & Mantine Form Controls
Good job, you put together a slick looking form. Now - to process that form data, which means you'll need to get that form data onto the server. I have to say that Next.js 15 has moved the entire paradigm of handling form data a lot closer to the way we used to process forms back in the bad ol' days. They even gave us the action attribute back in the form tag. It's starting to feel like Christmas ("we goin' to AppleBees after this" - John Wick reference)....
I grabbed this code from my Getting Started With Forms page. It's as close to a real world use case as I could think of, seeing as most forms are a mix of form input elements, selects, radio buttons, etc. The main difference here is the addition of a server action, which I like to build out in a separate file as most forms will likely belong to a specific use-case but have relevant functionality (think addUser, updateUser, deleteUser....)
Big Picture: I usually build out my forms as client-side components since I lean on useState a lot and user interaction is way-mo-harder using server-side components. For this post I'm just showing how the server action gets called and then I display the results of the submitted form in the server console (not the browser console!) In a future post I'll do some server-side validation and prep some SQL to handle the database comms. For now just understand the client side form, appreciate the server action and know that end-to-end form processing is right around the corner....
1//actions.ts
2//this is the server action that handles server side error trapping, database calls, etc.
3'use server'
4
5type SubmitFormResponse = {
6 success: boolean;
7 errors: string[];
8 message: string;
9}
10
11export async function submitForm(formData:FormData): Promise<SubmitFormResponse> {
12
13 const errors:string[] = [];
14
15 //loop over the form and spit out all key/value pairs for debugging
16 const formDataObject = Object.fromEntries(formData.entries());
17 console.log('Form Data as Object:', formDataObject);
18
19
20 // Return a success response
21 return { success: true, errors, message: 'Successfully processed form' };
22}
23
1//the form is a client component, you can import a server action to process the form
2'use client'
3
4import React, {useState} from "react";
5import Link from 'next/link';
6import {
7 Button, Fieldset, Group, Modal, NumberInput, PinInput, Rating, ScrollArea, SegmentedControl, Switch, TextInput
8} from '@mantine/core';
9
10import {useDisclosure} from "@mantine/hooks";
11import styles from "@/app/mantine/getting-started-with-forms/ResponsiveInput.module.css";
12import {submitForm} from "@/app/actions/actions";
13
14export default function ProcessingTheFormData() {
15//form controls
16 const [serverMessageValue, setServerMessageValue] = useState('');
17 const [serverError, setServerError] = useState('');
18 const [firstValue, setFirstValue] = useState('');
19 const [firstError, setFirstError] = useState('');
20 const [lastValue, setLastValue] = useState('');
21 const [lastError, setLastError] = useState('');
22 const [pinValue, setPinValue] = useState('');
23 const [pinError, setPinError] = useState('');
24 const [switchChecked, setSwitchChecked]: [boolean, (value: (((prevState: boolean) => boolean) | boolean)) => void] = useState(true);
25 const [switchError, setSwitchError] = useState('');
26 const [ratingValue, setRatingValue] = useState(4);
27 const [ageValue, setAgeValue] = useState<any>('');
28 const [ageError, setAgeError] = useState('');
29 const [favTechValue, setFavTechValue] = useState('Next.js');
30 //modal controls
31 const [opened, {open, close}] = useDisclosure(false);
32 const [modalOpen, setModalOpen] = useState(false);
33
34 //error trapping on submit: [CLIENT SIDE]
35 const handleSubmit = async (formData: FormData) => {
36 //check for blanks
37 (!firstValue) ? setFirstError('Enter your first name') : setFirstError('');
38 (!lastValue) ? setLastError('Enter your last name') : setLastError('');
39 (pinValue.length < 4) ? setPinError('PIN must contain 4 numbers') : setPinError('');
40 (!ageValue) ? setAgeError('Select your age from the list') : setAgeError('');
41 (!switchChecked) ? setSwitchError('You must agree to this End User License Agreement before proceeding') : setSwitchError('');
42
43 //if no errors, then call the server action to process the form on the server
44 if (!firstValue || !lastValue || (pinValue.length < 4) || !switchChecked || !ageValue)
45 return;
46 else {
47 // Call the server action [SERVER SIDE]
48 const response = await submitForm(formData);
49
50 if (!response.message) {
51 setServerMessageValue('There was a problem talking with the server action'); // Set errors if present
52 }
53
54 if (response.success) {
55 setServerMessageValue(response.message); // Set success message
56 console.log(response.errors)
57 }
58 }
59 };
60
61 return (
62 <div className="shadow-md bg-white" style={{
63 marginLeft: "2px",
64 marginRight: "2px",
65 marginTop: "1px",
66 marginBottom: "4px",
67 border: "thin solid silver",
68 padding: "15px",
69 borderRadius: "10px"
70 }}>
71 <div className="text-lg">Original Form Demo Using Mantine Form Components</div>
72 <span>Fill out the form below and then click the Submit Form button. Your information is completely safe with me.</span>
73 <form action={handleSubmit}>
74 <div className="flex flex-col lg:flex-row">
75 <div className="w-full lg:w-1/2">
76 {/*I display any error messages immediately beneath the troubled form control*/}
77 {serverMessageValue && <span className="text-sm text-red-500">{serverMessageValue}</span>}
78
79 <Fieldset legend="Confidential Information"
80 style={{marginTop: "20px", fontWeight: "bold", width: "90%"}}>
81 <div className={styles.wrapper}>
82 <TextInput
83 label="First Name"
84 name="first"
85 description="Enter your first name (required)"
86 value={firstValue}
87 onChange={(event) => setFirstValue(event.currentTarget.value)}
88 required
89 className="pt-4"
90
91 />
92 {firstError && <span className="text-sm text-red-500">ERROR! {firstError}</span>}
93 </div>
94 <div className={styles.wrapper}>
95 <TextInput
96 label="Last Name"
97 name="last"
98 description="Enter your last name (required)"
99 value={lastValue}
100 onChange={(event) => setLastValue(event.currentTarget.value)}
101 required
102 className="pt-4"
103 />
104 {lastError && <span className="text-sm text-red-500">ERROR! {lastError}</span>}
105 </div>
106 </Fieldset>
107
108 <Fieldset legend="Super-Duper Confidential Information"
109 style={{marginTop: "20px", fontWeight: "bold", width: "90%"}}>
110 <div className="pt-5">
111 <div style={{fontSize: "14px", fontWeight: "500"}}>ATM PIN <span
112 style={{color: "red"}}>*</span>
113 </div>
114 <div style={{
115 fontSize: "12px",
116 color: "#868E96",
117 paddingBottom: "6px",
118 fontWeight: "700px"
119 }}>Enter your PIN. What, you don't trust me? (definitely required)
120 </div>
121 <PinInput value={pinValue} onChange={setPinValue} type="number" name="pin"/>
122 </div>
123 {pinError && <span className="text-sm text-red-500">ERROR! {pinError}</span>}
124 </Fieldset>
125
126 <Fieldset legend="Required... But Nobody Really Cares"
127 style={{marginTop: "20px", fontWeight: "bold", width: "90%"}}>
128 <div className="pt-5">
129 <div style={{fontSize: "14px", fontWeight: "500"}}>Your favorite web technology?
130 </div>
131 <div style={{
132 fontSize: "12px",
133 color: "#868E96",
134 paddingBottom: "4px",
135 fontWeight: "700px"
136 }}> Pick one
137 </div>
138 <SegmentedControl data={['React', 'Next.js', 'Angular', 'Vue', 'CSS']}
139 value={favTechValue} onChange={setFavTechValue}
140 className="responsive-control" name="favTech"/>
141 </div>
142
143 <div className="pt-5">
144
145 <NumberInput
146 label="Enter your age"
147 description="Don't lie...."
148 className="w-[100px]"
149 value={ageValue}
150 onChange={setAgeValue}
151 min={18}
152 max={99}
153 placeholder="18-99"
154 name="age"
155 />
156
157 {ageError && <span className="text-sm text-red-500">ERROR! {ageError}</span>}
158 </div>
159
160 </Fieldset>
161
162 <Fieldset legend="Finally, how would you rate this form?"
163 style={{marginTop: "20px", fontWeight: "bold", width: "90%"}}>
164 <div className="pt-5" style={{fontSize: "14px", fontWeight: "500"}}>Be honest (but 5
165 stars
166 please)
167 </div>
168 <div style={{
169 fontSize: "12px",
170 color: "#868E96",
171 paddingBottom: "6px",
172 fontWeight: "700px"
173 }}> Pick one
174 </div>
175 <Rating defaultValue={ratingValue} color="orange" value={ratingValue}
176 onChange={setRatingValue} name="rating"/>
177 </Fieldset>
178 </div>
179
180 <div className="w-full lg:w-1/2 pt-5">
181 <Fieldset legend="End User License Agreement"
182 style={{fontWeight: "bold", width: "90%"}}>
183 <span className="text-sm font-normal">Scroll to read, then agree to this agreement by agreeing
184 at the bottom of this agreement. But only if you agree!<br/><br/></span>
185 <ScrollArea h={730} style={{
186 paddingRight: "30px",
187 paddingLeft: "20px",
188 paddingTop: "10px",
189 border: "thin solid lightgray",
190 borderRadius: "5px"
191 }}>
192 <div className="text-lg font-normal">HumancentiPad Plot</div>
193 <div className="text-sm font-normal">
194 [...cited from Wikipedia]<br/>
195 After Eric Cartman boasts to his classmates of owning an iPad and mocks them for
196 not having one, he is humiliated when it is revealed that he actually does not
197 own
198 one....<snip /> <br/><br/>
199 </div>
200 <hr style={{color: "lightgrey"}}/>
201 <br/>
202 <Switch
203 style={{fontSize: "14px"}}
204 checked={switchChecked}
205 onChange={(event) => setSwitchChecked(event.currentTarget.checked)}
206 label="I agree that I've read this End User License Agreement and I'm OK with selling my soul to
207 Corporate America. I mean, did you NOT see this South Park episode???"
208 name="eulaAgreement"
209 />
210 <br/>
211 </ScrollArea>
212 {switchError && <span className="text-sm text-red-500">ERROR! {switchError}</span>}
213 </Fieldset>
214 </div>
215 </div>
216
217 <div className="flex items-center justify-center">
218 <Group mt="md" className="pt-6">
219 <Button type="submit">Submit Form</Button>
220 </Group>
221 </div>
222 </form>
223 </div>
224 )
225 }