Build a contact form without JavaScript


Being a Frontend Engineer does not mean specializing in a single framework. Of course, you can have your preferences and work with them. But our job always involves picking up the right tool for the job. The ability to choose the right approach to close the ticket in front of you it probably even matters more.
That's why I was delighted to see a challenge that didn't require any knowledge of JavaScript. The task's protagonist was HTML.
Depending on when you started coding, maybe you did not focus much on how to structure and which elements to use when you write HTML in your apps. But let me tell you, while it is not a programming language, HTML is incredibly powerful and a great point of connection for other languages.
I don't want to transform this article into an HTML deep dive, but you must know how powerful it is to solve this challenge elegantly.
Since the challenge does not allow JavaScript, you have to rely on the features of the language that are most useful for you. For example, you cannot use onClick
on the button that will send the form.
On top of that, an app is not considered completed only when we tested it, and it works as expected. We also have to think about how the user will be able to use it, and this means having the app accessible and able to provide the correct information to our visitors.
Now that we have a clearer picture of the topic for this challenge, it's time to check the starting code they provided me.
import submitForm from './submitForm';
export default function App() {
return (
<form
// Ignore the onSubmit prop, it's used by GFE to
// intercept the form submit event to check your solution.
onSubmit={submitForm}>
<input type="text" />
</form>
);
}
Please don't start thinking something like: "But that's a React component! Why there's a onSubmit
there? I thought JavaScript wasn't allowed!
Because while that's true, the fact that we use React and the onSubmit
callback has nothing to do with our challenge.
The GreatFrontEnd platform uses onSubmit
to validate the data the form is sending, but if you open the submitForm.js
file, you'll soon discover that it uses the native FormData
interface to handle the data and no other React way of accessing the values sent by the form
.
Let's break down the features that we need to implement to solve this challenge:
The
form
can collect and send all the data even if JavaScript is disabled (rely on element attributes and browser API to send them).Each
input
must have a properlabel
that will focus it if clicked.button
must be able tosubmit
the form.
If you open the challenge on GreatFrontEnd, you'll see that the brief includes other specs that will help you gather more information on how to solve it. However, I've listed only these three because this is where I want to focus in this article.
Sending data via HTML
Even though the React component has an onSubmit
, once you open the submitForm.js
file you'll soon discover that there are some checks in place to make sure the form
uses standard HTML to send the data.
At line 9, it checks the URL inside the action
attribute:
if (form.action !== SUBMIT_URL) {
alert("Incorrect form action value");
return;
}
While at line 14 checks for the proper method
that form
uses:
if (form.method.toLowerCase() !== "post") {
alert("Incorrect form method value");
return;
}
Even if the limit of "no JavaScript allowed" didn't ring a bell, these checks should remind us that to make a form
work in HTML, we must specify the action
and method
attributes. Well, both have defaults (#
for action
and get
for method
), but in our challenge, we need to customize both:
The value of
action
has to be set to the endpoint made available (stored as the constantSUBMIT_URL
insubmitForm.js
).A standard request from a
form
is a GET type of request, while our API only accepts POST requests.
We have these checks in JavaScript, but we need to send data via standard HTML form
with the proper attributes applied.
import submitForm, { SUBMIT_URL } from "./submitForm";
export default function App() {
return (
<form
onSubmit={submitForm}
method="post"
action={SUBMIT_URL}
/>
);
}
I wanted to highlight this step (be aware form
auto closes for brevity), but it does nothing new that we already analyzed together. We imported SUBMIT_URL
from submitForm.js
and used for the action
attribute, and we also explicitly set the method
to POST.
input
s must have a label
The code sample that we had at the beginning had only one input
, but we need to do a bit better than that.
<label htmlFor="name">Name</label>
<input type="text" id="name" name="name" />
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" />
<label htmlFor="message">Message</label>
<textarea id="message" name="message" />
That's how I made the elements inside the form
accessible and able to send the correct information to our action
.
There's nothing magic here: simple HTML attributes and elements that help our users understand what information we require for each field. Each label
is connected to its input
with the htmlFor
prop (that gets converted to the standard for
attribute during render).
On top of that, we already set the correct keys for the information we need to send to the API. Each field has a name
attribute corresponding to the endpoint provided: name
, email
, and message
.
The button
in charge of sending
I've to be honest here: I lied in the section title because button
is not the only way we can send the data in our form
. You can also send the data collected by pressing Enter
inside any text input
(sorry, textarea
goes to a new line).
Users are used to seeing a functioning button at the end of each form
, so let's make them happy:
<button>Send</button>
That's it.
We satisfied the requirement of having a Send text inside a button
, but we had nothing more to do on the element itself. Why? The standard behavior of a button
inside a form is to submit
the form itself.
A button
element has few other values for the type
attribute, but here, the important thing to know is that the default value of this attribute is submit
.
And that's why our form
can send the data collected even with JavaScript disabled.
This is the complete code that I wrote to solve the challenge:
import submitForm, { SUBMIT_URL } from "./submitForm";
export default function App() {
return (
<form
onSubmit={submitForm}
method="post"
action={SUBMIT_URL}
style={{
display: "flex",
flexDirection: "column",
alignItems: "start",
gap: "12px",
}}
>
<label htmlFor="name">Name</label>
<input type="text" id="name" name="name" />
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" />
<label htmlFor="message">Message</label>
<textarea id="message" name="message" />
<button>Send</button>
</form>
);
}
Comparing to the proposed solution
Lucky me, both solutions are similar, which is already a success. Analyzing the code, though, I noticed that my approach was a bit different. I aimed at speed of implementation, like relying on the style
attribute instead of leveraging the connected stylesheet, while the proposed solution has more leverage in the basic approach.
export default function App() {
return (
<form
onSubmit={submitForm}
action="https://www.greatfrontend.com/api/questions/contact-form"
method="post">
<div>
<label htmlFor="name-input">Name</label>
<input id="name-input" name="name" type="text" />
</div>
<div>
<label htmlFor="email-input">Email</label>
<input id="email-input" name="email" type="email" />
</div>
<div>
<label htmlFor="message-input">Message</label>
<textarea
id="message-input"
name="message"></textarea>
</div>
<div>
<button>Send</button>
</div>
</form>
);
}
The differences with my code are minimal: a hardcoded action
and a wrapping div
for each group of fields.
But I also want to share the styles.css
of the solution. There's nothing new there, but he reached for it while I did not, and Iโm writing it to remember next time ๐
body {
font-family: sans-serif;
}
form {
display: flex;
flex-direction: column;
row-gap: 12px;
}
label {
font-size: 12px;
}
input,
textarea {
display: block;
}
Clean and straightforward, leveraging only standard HTML elements.
I hope you found it helpful. I shared my thought process and findings while I was working on this GFE 75 challenge because I think it will help you and me. I must understand what I am doing deeply to explain it, and maybe you will discover something new that I haven't had time to work on yet.
In any case, let me remember that many of the challenges in the GreatFrontEnd are free, and you're doing a disservice to yourself if you do not work on some challenges occasionally. I know this because I've done it in the past, but it's time for a change ๐
Subscribe to my newsletter
Read articles from Andrea Barghigiani directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Andrea Barghigiani
Andrea Barghigiani
Born as WordPress theme developer and switched on React.js while the CMS was still fighting with its own community about the Gutenberg editor. Now working daily building full-stack applications with Next.js