code snippets

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....


Original Form Demo Using Mantine Form Components
Fill out the form below and then click the Submit Form button. Your information is completely safe with me.
Confidential Information

Enter your first name (required)

Enter your last name (required)

Super-Duper Confidential Information
ATM PIN *
Enter your PIN. What, you don't trust me? (definitely required)
Required... But Nobody Really Cares
Your favorite web technology?
Pick one

Don't lie....

Finally, how would you rate this form?
Be honest (but 5 stars please)
Pick one
End User License AgreementScroll to read, then agree to this agreement by agreeing at the bottom of this agreement. But only if you agree!

HumancentiPad Plot
[...cited from Wikipedia]
After Eric Cartman boasts to his classmates of owning an iPad and mocks them for not having one, he is humiliated when it is revealed that he actually does not own one....<snip />





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&apos;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....&lt;snip /&gt; <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    }

Summary: This form uses client and server side validation/processing. With the help of the Mantine form controls a user fills out and submits the form which immediately invokes the handleSubmit() method (line 35). After a quick check for errors on the client side, the server action submitForm() is invoked (line 48) and the form data is processed on the server (remember submitForm() is actually imported from the /actions folder - which lives on the server (hence "server action")). At the moment a simple console.log() is spitting back the submitted form data on the server (check your terminal window for the server's response), but in the next post I'll configure the database query and take this thing end-to-end.