code snippets

Getting Started With Forms: Mantine Form Components

Don't build your forms from scratch (nobody does that anymore).

There are a metric-TON of popular React component libraries out there, including:


ARE YOU KIDDING ME?! And that's not even all of them! I went with the Mantine library which has mucho components, a starter guide for Next.js (Rich likes this), some really good looking charts and some handy tools. In a future post I'll be highlighting their Drawer component, which is a lot of fun for data-heavy pages.


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. When he and his mother Liane go to Best Buy to buy an iPad, the item's exorbitant price prompts her to suggest buying a less expensive Toshiba HandiBook. The demanding Cartman, who had his mind set on the iPad as a status symbol, loudly excoriates her in the middle of the store, accusing her of "f@$king" him. Humiliated, Liane leaves the store without buying him anything.

Meanwhile, Cartman's classmate and frequent nemesis Kyle Broflovski, who did not read the Terms and Conditions when agreeing to download the latest iTunes update, is pursued by shadowy agents from Apple Inc., who wish to perform several intrusive acts upon him, informing him that he agreed to them when he downloaded the update. Kyle attempts to flee the men and is incredulous when his friends tell him they all read the entire Terms and Conditions when they downloaded the latest update. Kyle seeks refuge at his father Gerald's law office. Still, the Apple agents taser Gerald, kidnap Kyle, and throw him in a cage with a Japanese man named Junichi Takiyama and a young woman who also failed to read the fine print of their purchased updates.

At a Stevenote address, Steve Jobs unveils the new product for which Kyle and the other two were kidnapped: the HUMANCENTiPAD, comprising the three kidnapped subjects on all fours and sewn together mouth to anus. Junichi Takiyama is in front, with an iPhone attached to his forehead; Kyle is in the middle; and the woman is at the rear, with an iPad attached to her anus. However, Jobs is disappointed when Kyle continues to sign agreements that are put in front of him without reading them first, and puts the "device" through tests in an attempt to make it read.

Meanwhile, Cartman appears on the talk show Dr. Phil to publicly accuse Liane of "f@$king" him. The audience misunderstands this to mean that she has sexually molested him. Cartman is given the first-ever HUMANCENTiPAD as Jobs unveils it to the public as a consolation gift. Cartman is elated to have a device that not only supports web browsing and email but also enables him to induce someone to ******** into Kyle's mouth.

Seeking to free his son, Gerald goes with Kyle's friends Stan, Kenny and Butters to an Apple Store, where the customer service agents known as "the Geniuses", after considerable deliberations, determine that they can void Kyle's agreement if Gerald, a PC user, signs up with Apple and creates a family account. Gerald consents, after which he, the Geniuses, and Kyle's friends go to the studio where Dr. Phil is produced. Jobs, complying with Gerald's new deal, reluctantly makes preparations to have Kyle separated from his two fellow victims. This enrages Cartman, whose dream is now being quashed. Cartman looks up to the heavens and angrily excoriates God, after which he is struck by a bolt of lightning. He is then shown recuperating in a hospital bed, crying while his mother flips through a book indifferently.




Summary: This post is to demonstrate the actual UI/form/Mantine controls on the front end (I'll go over form processing and server side functionality at a later time). To set this file up, I imported the Mantine controls (including the Modal screen), created local states for the actual form values and their possible errors. When the Submit Form button is clicked the handleSubmit() function kicks in and sets the error states, as needed, and displays any errors in red text immediately beneath the affected control. CSS has been mixed in (I'll clean it up later - hmmm probably not) but after all the errors clear I pump the form values into a modal box so you can see that I actually captured (but didn't save!) the submitted form values. In a future post instead of using this modal I'll use a server action to process the submitted values - stay tuned. I could ramble on and on here, but check the comments inline below, you'll get a better feel for what I actually did.

My 2 Cents Worth: I like working with the Mantine controls, they're consistent and straight-forward to implement. The docs are good/very good and there are more than a few examples to get you going. Chances are good that if you run into any problems with these controls, it's likely a Next.js/React issue and not a Mantine control issue. I'm going to get a lot of mileage out of their Modal box as it is very configurable and customizable. I could drone on about, " ... how Mantine offers this control and that control where none of the other libraries offer anything like it..." but features for all of the libraries will eventually 'bubble up to same' after enough time goes by....

Disclaimer: If you didn't like how I coded something in particular then change it for your own use case and quit bombing my inbox. I'm sensitive and you're hurting my feelings ;-)
1//mantine controls are client side, so don't try running them in a server component!
2'use client'
3
4import React, {useState} from "react";
5
6//import all of your mantine controls
7import {
8    TextInput,
9    Button,
10    Group,
11    PinInput,
12    SegmentedControl,
13    Rating,
14    Fieldset,
15    NumberInput,
16    ScrollArea,
17    Switch,
18    Modal
19} from '@mantine/core';
20
21// you need the useDisclosure hook for the Modal box
22import {useDisclosure} from '@mantine/hooks';
23
24import Image from "next/image";
25
26//form controls
27    const [firstValue, setFirstValue] = useState('');
28    const [firstError, setFirstError] = useState('');
29    const [lastValue, setLastValue] = useState('');
30    const [lastError, setLastError] = useState('');
31    const [pinValue, setPinValue] = useState('');
32    const [pinError, setPinError] = useState('');
33    const [switchChecked, setSwitchChecked]: [boolean, (value: (((prevState: boolean) => boolean) | boolean)) => void] = 
34    useState(true);
35    const [switchError, setSwitchError] = useState('');
36    const [ratingValue, setRatingValue] = useState(4);
37    const [ageValue, setAgeValue] = useState<any>('');
38    const [ageError, setAgeError] = useState('');
39    const [favTechValue, setFavTechValue] = useState('Next.js');
40    //modal controls
41    //I open and close the modal box programmatically, using the useDisclosure hook and state 
42    const [opened, {open, close}] = useDisclosure(false);
43    const [modalOpen, setModalOpen] = useState(false);
44
45    //error trapping on submit
46    const handleSubmit = () => {
47        //check for errors and state as needed. When everything checks out open the Modal
48        (!firstValue) ? setFirstError('Enter your first name') : setFirstError('');
49        (!lastValue) ? setLastError('Enter your last name') : setLastError('');
50        (pinValue.length < 4) ? setPinError('PIN must contain 4 numbers') : setPinError('');
51        (!ageValue) ? setAgeError('Select your age from the list') : setAgeError('');
52        (!switchChecked) ? setSwitchError('You must agree to this End User License Agreement before proceeding') : 
53        setSwitchError('');
54
55        //if no errors, then automatically open the modal to display results/confirmation
56        if (!firstValue || !lastValue || (pinValue.length < 4) || !switchChecked || !ageValue)
57            return;
58        else {
59            setModalOpen(true);
60        }
61    };
62    
63    return (
64        <div>
65            <div className="min-h-screen m-4">
66                <h2 className="w-full text-center text-xl">
67                    Getting Started With Forms: Mantine Form Components
68                </h2>
69
70                <div className="shadow-md bg-white" style={{
71                    marginLeft: "2px",
72                    marginRight: "2px",
73                    marginTop: "1px",
74                    marginBottom: "4px",
75                    border: "thin solid silver",
76                    padding: "15px",
77                    borderRadius: "10px"
78                }}>
79                    <div className="text-lg">Form Demo Using Mantine Form Components</div>
80                    <span>Fill out the form below and then click the Submit Form button. You&apos;re information is completely 
81                    safe with me. </span>
82                    <div className="flex">
83                        <div className="w-1/2">
84                            {/*I display any error messages immediately beneath the troubled form control*/}
85                            <Fieldset legend="Confidential Information"
86                                      style={{marginTop: "20px", fontWeight: "bold", width: "90%"}}>
87                                <TextInput
88                                    label="First Name"
89                                    name="first"
90                                    description="Enter your first name (required)"
91                                    value={firstValue}
92                                    onChange={(event) => setFirstValue(event.currentTarget.value)}
93                                    required
94                                    className="w-[300px] pt-4"
95                                />
96                                {firstError && <span className="text-sm text-red-500">ERROR! {firstError}</span>}
97                                
98                                <TextInput
99                                    label="Last Name"
100                                    name="last"
101                                    description="Enter your last name (required)"
102                                    value={lastValue}
103                                    onChange={(event) => setLastValue(event.currentTarget.value)}
104                                    required
105                                    className="w-[300px] pt-4"
106                                />
107                                {lastError && <span className="text-sm text-red-500">ERROR! {lastError}</span>}
108
109                            </Fieldset>
110
111                            <Fieldset legend="Super-Duper Confidential Information"
112                                      style={{marginTop: "20px", fontWeight: "bold", width: "90%"}}>
113                                <div className="pt-5">
114                                    <div style={{fontSize: "14px", fontWeight: "500"}}>ATM PIN <span
115                                        style={{color: "red"}}>*</span>
116                                    </div>
117                                    <div style={{
118                                        fontSize: "12px",
119                                        color: "#868E96",
120                                        paddingBottom: "6px",
121                                        fontWeight: "700px"
122                                    }}>Enter your PIN. What, you don&apos;t trust me? (definitely required)
123                                    </div>
124                                    <PinInput value={pinValue} onChange={setPinValue} type="number"/>
125                                </div>
126                                {pinError && <span className="text-sm text-red-500">ERROR! {pinError}</span>}
127                            </Fieldset>
128
129                            <Fieldset legend="Required... But Nobody Really Cares"
130                                      style={{marginTop: "20px", fontWeight: "bold", width: "90%"}}>
131                                <div className="pt-5">
132                                    <div style={{fontSize: "14px", fontWeight: "500"}}>Your favorite web technology?
133                                    </div>
134                                    <div style={{
135                                        fontSize: "12px",
136                                        color: "#868E96",
137                                        paddingBottom: "4px",
138                                        fontWeight: "700px"
139                                    }}> Pick one
140                                    </div>
141                                    <SegmentedControl data={['React', 'Next.js', 'Angular', 'Vue', 'CSS']}
142                                                      value={favTechValue} onChange={setFavTechValue}/>
143                                </div>
144
145                                <div className="pt-5">
146
147                                    <NumberInput
148                                        label="Enter your age"
149                                        description="Don't lie...."
150                                        className="w-[100px]"
151                                        value={ageValue}
152                                        onChange={setAgeValue}
153                                        min={18}
154                                        max={99}
155                                        placeholder="18-99"
156
157                                    />
158                                    {ageError && <span className="text-sm text-red-500">ERROR! {ageError}</span>}
159                                </div>
160
161                            </Fieldset>
162
163                            <Fieldset legend="Finally, how would you rate this form?"
164                                      style={{marginTop: "20px", fontWeight: "bold", width: "90%"}}>
165                                <div className="pt-5" style={{fontSize: "14px", fontWeight: "500"}}>Be honest....
166                                </div>
167                                <div style={{
168                                    fontSize: "12px",
169                                    color: "#868E96",
170                                    paddingBottom: "6px",
171                                    fontWeight: "700px"
172                                }}> Pick one
173                                </div>
174                                <Rating defaultValue={ratingValue} color="orange" value={ratingValue}
175                                        onChange={setRatingValue}/>
176                            </Fieldset>
177                        </div>
178
179                        <div className="w-1/2 pt-5">
180                            <Fieldset legend="End User License Agreement"
181                                      style={{fontWeight: "bold", width: "90%"}}>
182                                <span className="text-sm" style={{fontWeight: "normal"}}>Scroll to read, then agree to 
183                                this agreement by agreeing at the bottom of this agreement.<br/><br/></span>
184                                <ScrollArea h={730} style={{
185                                    paddingRight: "30px",
186                                    paddingLeft: "20px",
187                                    paddingTop: "10px",
188                                    border: "thin solid lightgray",
189                                    borderRadius: "5px"
190                                }}>
191                                    <div className="text-lg font-normal">HumancentiPad Plot</div>
192                                    <div className="text-sm font-normal">
193                                        After.... [EULA text here - saved for brevity]
194                                    </div>
195                                    <hr style={{color: "lightgrey"}}/>
196                                    <br/>
197                                    <Switch
198                                        style={{fontSize: "14px"}}
199                                        checked={switchChecked}
200                                        onChange={(event) => setSwitchChecked(event.currentTarget.checked)}
201                                        label="I agree that I've read this End User License Agreement and I'm OK with 
202                                        selling my soul to Corporate America. I mean, did you NOT see this South Park episode???"
203                                    />
204                                    <br/>
205                                </ScrollArea>
206                                {switchError && <span className="text-sm text-red-500">ERROR! {switchError}</span>}
207                            </Fieldset>
208                        </div>
209                    </div>
210
211                    <div className="flex items-center justify-center">
212                        <Group mt="md" className="pt-6">
213                            <Button onClick={handleSubmit}>Submit Form</Button>
214                        </Group>
215                    </div>
216                </div>
217            </div>
218
219            <Modal opened={modalOpen} onClose={() => setModalOpen(false)} withinPortal={true} title={
220                <Group>
221                    <Image
222                        src="/images/logo.png"
223                        alt="Modal Icon"
224                        width={60}
225                        height={60}
226                    />
227                    <span>Form Results - Thanks For Playing!</span>
228                </Group>
229            } withCloseButton={true}
230                   className="text-base shadow-md" transitionProps={{transition: 'fade', duration: 300}}
231            styles={{
232                title: {color: '#27272A',},
233                body: {color: '#27272A'},
234            }}>
235
236                <table style={{margin:'auto'}}>
237                    <tbody>
238                    <tr>
239                        <td align="right">First name: &nbsp; </td>
240                        <td align="left"> &nbsp; {firstValue}</td>
241                    </tr>
242                    <tr>
243                        <td align="right">Last name: &nbsp; </td>
244                        <td align="left"> &nbsp; {lastValue}</td>
245                    </tr>
246                    <tr>
247                        <td align="right">PIN: &nbsp; </td>
248                        <td align="left"> &nbsp; {pinValue}</td>
249                    </tr>
250                    <tr>
251                        <td align="right">Favorite Tech: &nbsp; </td>
252                        <td align="left"> &nbsp; {favTechValue}</td>
253                    </tr>
254                    <tr>
255                        <td align="right">Age: &nbsp; </td>
256                        <td align="left"> &nbsp; {ageValue}</td>
257                    </tr>
258                    <tr>
259                        <td align="right">Rating: &nbsp; </td>
260                        <td align="left"> &nbsp; {ratingValue} stars</td>
261                    </tr>
262                    <tr>
263                        <td align="right">EULA: &nbsp; </td>
264                        <td align="left"> &nbsp; {switchChecked ? 'Agreed' : 'Not Agreed'}</td>
265                    </tr>
266                    </tbody>
267                </table>
268                <div className="pt-3">
269                    Rest assured, none of your data has been saved or stored. No animals were harmed, no trees were 
270                    cut down and no water was polluted during the production of this form.
271                </div>
272            </Modal>
273        </div>
274    )