Aman Explains

Settings

Do you even need JavaScript for your basic HTML forms?

August 27, 2023• ☕️☕️ 10 min read

JavaScript has grown up to be a powerful language. Starting from its usage of making things interactive in the browser to running complex web servers, it has come a long way. It’s the functional requirements that have driven the growth trajectory of JavaScript, along with the small learning curve compared to other languages. As much as this language has seen its growth, it has been equally abused by developers to do things that it shouldn’t do or should be done by other languages comparably less powerful than JavaScript.

This concept is well known as The Rule of Least Power. Given an option of selecting a single language to implement something on the web, this rule suggests picking a language that is the least powerful among all. The rationale is that the less powerful the language, the more accessible experience it can provide to its consumer.

This is the theme of this article where we will find out what exactly it means, with an example of building a basic login form using HTML only — the least powerful language. We will talk about why reaching out to JavaScript is not always mandatory, even though it’s often a tempting solution that comes to our mind due to lack of awareness.

Let’s get started.

Table of Contents


Who’s more powerful

It shouldn’t come as a surprise that the out of three main languages that set the foundation of our web platform - HTML, CSS, and JavaScript, the order of their power can be seen as follows:

    HTML    <      CSS    <   JavaScript 
(least powerful)           (most powerful)

HTML is a declarative language and is the least powerful among all. It is how we came to experience the first HTML document that was published in late 1991 by Tim Berners-Lee. Due to its least powerful characteristics, other software can leverage this trait to build solutions on top of it. Assistive technology is one such example that rely on semantic tags and WAI-ARIA to provide an accessible and inclusive experience. Another example would be The Semantic Web which is an attempt to make internet data machine readable. CSS follows the same declarative principles but is more powerful than HTML.

JavaScript is known to be the most powerful among all and thus has been exploited to build applications of all scales. Sadly, its own success has become its weakness. We are building apps that are JS-heavy and can’t be operated, even at a minimum, with JS disabled. We have lost in delivering an inclusive experience to our users. Isn’t that the reason, the web community has shifted towards Server-rendered apps, rather than SPAs (single-page applications)? Isn’t that why we are seeing the advent of frameworks like Astro, Gatsby, and Remix that advocate the concept of delivering less JavaScript to our end users?

There’s nothing fundamentally wrong with JavaScript. It’s just a most powerful language and thus should be used for powerful tasks that HTML and CSS can’t do. Starting with the least powerful language for implementing a critical functionality will make it accessible to all the end users, and then we can gradually enhance it with the layer of JS to achieve complex interactions that are not possible with HTML and CSS only. The idea is not new; progressive web apps are born out of this philosophy.


Talk is cheap, show me the code

With the theory aside and rationale understood, it’s time we build our basic login form using HTML only. We will use semantic HTML tags to lay the foundation, and gradually add less-know html attributes to provide the required functionalities.

Our login form should provide the following functional requirements:

  • Email and password are required input fields.
  • A basic email validation check should be in place.
  • Pressing the Enter button should invoke the form submission handler.
  • Pressing the submit button should submit the form to a given endpoint.
  • Visually highlights any area that indicates a success or error to help users distinguish their actions.

Here’s the bare minimum HTML we need:

<form>
  <label>
    Email: <input type="email" />
  </label>
  <label>
    Password: <input type="password" />
  </label>
  <button>Log in</button>
</form>

Using correct input type plays a crucial role. In addition to specifying the type of control to render (checkbox, radio, etc.), it also provides additional benefits for text validation (type="email") and relevant keyboard support.

Let’s get into the interesting part: making our form feature-complete.


Required fields

To make email and password a required field, all we have to do is add a required attribute to these fields. Doing so will ensure that the form is not submitted if these fields have no values.

We don’t have to imperatively check for non-empty field values using JavaScript anymore. By using required, we have declaratively stated our intent to the Browser that these fields are required. The browser will take care of it implicitly for us. Moreover, providing this attribute will cause screen readers to announce "required" to the users which may otherwise be missed if we only rely on the commonly used asterisk identifier *.

<form>
  <label>
    Email: <input type="email" required />
  </label>
  <label>
    Password: <input type="password" required />
  </label>
  <button>Log in</button>
</form>
Live Preview

Without filling in any values, try submitting the form by pressing the Login in button. You will met with some warning popups asking you to fill in some values. No extra work and you get these features for free. And more importantly, this feature will work even if JavaScript is disabled in the user agent (browser). Give it a try!

Note: For good user experience, don’t just use * indicator for the required field; make it obvious by other means.


Email validation check

Form validation is what tricks most developers into thinking that JavaScript implementation is inevitable. In my opinion, this is due to the following reasons:

  • Devs mistakenly think that HTML is nothing short of a programming language, but it is only meant for laying out the basic structure of a web page. It has no significant power.
  • Additionally, it’s the excessive reliance on JavaScript tooling/libraries (like React) to build everything, that one might completely miss other things happening in the web community.

If all you have is a hammer, everything looks like a nail

Depending upon the type of input field, HTML offers a few attributes that we can leverage to enforce basic validation. The pattern attribute is one of such attributes that accepts a regular expression as its value. The input’s value must match this pattern to pass constraint validation, otherwise, the form will not be submitted.

By using the correct email input type, we have already ensured that the input type is automatically validated for invalid email address before the form can be submitted. This default email validation algorithm is pretty basic and checks that the entered text is at least in the correct form to be a legitimate email address.

In the example above, try submitting the form by filling up an invalid value in the email field. You should see a warning popup asking you to provide a legitimate email address as shown in the screenshot below.

Warning popup when user provides an invalid email

For other advance email validation requirements, you can lean onto this pattern attribute and provide a valid JavaScript regular expression, along with maxlenth and minlength attribute.

The following example demonstrates the usage of these attributes. Here we are imposing a check that the provided email value should start with at least one character and end with @example.com. See regex101.com for an explanation of this RegExp.

<form>
  <label>
    Email: <input type="email" required pattern=".+@example\.com" />
  </label>
  <label>
    Password: <input type="password" required />
  </label>
  <button>Log in</button>
</form>

The screenshot below shows the pattern attribute in action:

Warning popup showing invalid email format

For your convenience, here’s the live preview of the example form above. Try providing an invalid value in the email input field and you should see a warning bubble indicating that the format is not valid. Cool isn’t it?

Live Preview


Form Styling

To meet our styling requirements, we can simply use form pseudo classes that the user-agent (browser) injects for us automatically. To highlight a valid input we can target :valid pseudo-class and style it accordingly. Similarly, for invalid inputs we rely on :invalid pseudo-class.

<style>
  input:focus:valid {
      outline: 2px solid green;
  }
  input:focus:invalid {
      outline: 2px solid red;
  }
</style>
<form>
  <label>
    Email: <input type="email" required pattern=".+@example\.com" />
  </label>
  <label>
    Password: <input type="password" required />
  </label>
  <button>Log in</button>
</form>

Take a look at the recorded video below. It displays how the input field changes its color based on the value provided by the user at runtime.

using invalid pseudo classes to style invalid format

In the live preview below, when you provide an invalid format for the email, it gets highlighted with a red outline. Provide a valid format and the outline will dynamically turn green, indicating to the user that the value is valid. I recommend you give it a try to see it yourself. No need to use JavaScript to dynamically inject different classes. It’s all handled by us via CSS (the second most powerful language).

It’s so satisfying in my opinion. Don’t you agree?

Live Preview


Submit the form when Enter key is pressed

I have seen many implementations of forms where the only way to submit the form is to click the submit button. But we need to be mindful that the pointer devices are not the only way the users may want to submit their form - pressing the Enter key should submit the form too.

A naive implementation of such a form relies on preventing the default behaviour of the form’s submit event at the wrong place:

<form>
  <label>
    Email: <input type="email" required pattern=".+@example\.com" />
  </label>
  <label>
    Password: <input type="password" required />
  </label>
  <button>Log in</button>
</form>
<script>
  document.getElementById('login').addEventListener('click', e => {
   e.preventDefault();
   // Handle form submission here
   console.log('No more in-built form validation for you :(');
  })
</script>

If you go ahead and submit the form, you will not receive any required field or invalid email feedback as you were getting in the previous example above. You have broken one nice feature, and now have to rely on JavaScript to implement validation checks — bloating your client with more unnecessary JavaScript.

Try the live preview of the example form above, and see it yourself.

Live Preview

*As we have used event.preventDefault inside the click handler, we have broken down a nice auto validation feature.


I used the wrong place intentionally. Attaching an event handler to the button, like we did, wasn’t wrong. You may want to prevent the default behaviour of reloading the page when the form is submitted, as in SPAs. It was wrong as it was also preventing other nice features, and required was one of them.

So, how do you go about implementing the form submission on both explicit button click, and pressing the enter key? The answer is simple: let the browser (user agent) handles it for you. Use the language of least power: the HTML.

<form>
  <label>
    Email: <input type="email" required pattern=".+@example\.com" />
  </label>
  <label>
    Password: <input type="password" required />
  </label>
  <button>Log in</button>
</form>

In this case, we have removed our event handler from the button. Surprisingly, it is unknown to many developers that a button has a submit type by default when it’s a child of a form element.

By default, a button inside a form element has a submit type

The button, which is of type submit, doesn’t need any explicit event handler. Pressing the Enter key or clicking the Log in button will dispatch the form’s submit event automatically for us, resulting in a form submission if all validation checks are passed.

In the form given below, try bringing focus on any of the fields and press the Enter key. If values are missing or invalid, you should get a warning feedback.

Live Preview

Moreover, if you press the Enter key when the form is valid, the user-agent (browser) will try to submit the form for you. And the default behaviour in this case is to submit the form to the route containing the form itself. More on this in the next section.

Note: explicitly providing type="button" attribute to the Log in button above will also break this auto dispatch functionality, and will not submit the form to the server.

Wait a second, what about sending out all the filled-in form data to the back-end API for processing? Don’t we need JavaScript to extract all these form values, and post them out using a fetch API call?

Well, this is true for an AJAX request where you don’t want the browser window to reload the new document when response data is received. This is an imperative approach of collecting user data via JavaScript and sending it asynchronously in the background, updating only the parts of the UI that require changes.

let’s cover that in our next functional requirement.


Processing the form

In an AJAX call the form is not used for data submission, but merely for collecting the data, and the app takes control over when the user tries to submit the form.

For sending a request declaratively, you can use the form element to define how the data will be sent. All of its attributes let you configure the request to be sent. The two most important attributes of the form element are action and method.

  • action: defines the URL where data gets sent. If this attribute is missing, the data will be sent to the URL of the current page containing the form.

  • method: defines which HTTP method is used to transmit the data. Only allowed method are post, get and dialog.

Now, let’s modify our login form example and provide some values for these attributes.

<form action="/login" method="POST">
  <label>
    Email: <input type="email" name="email" required pattern=".+@example\.com" />
  </label>
  <label>
    Password: <input type="password" name="password" required />
  </label>
  <button>Log in</button>
</form>

For the action attribute, we are sending the data to the back-end with a relative /login URL hosted on the same origin.

Also, by default, when you send the form using the POST method, the data is included in the request body with content-type of application/x-www.form-urlencoded.

If you observe closely, we have also provided the names for the input fields. These names act as keys when sending the form data to the server. The keys (the name of the input field) and values are encoded in key-value tuples separated by '&' and '=' between them.

Following is the live preview of the example above.

Live Preview

Note that when you submit the form above, the browser will reload the /login document, and you should get Not Found (404). This is an expected behaviour, as we don’t have any /login route hosted on this origin.

Upon the form submission, the browser will send a POST request that has the following structure. The payload is URL encoded.

POST / HTTP/2.0
Host: amanexplains.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 38

email=aman%40example.com&password=test

You can also specify the content type explicitly using enctype attribute.

From the server (back-end) perspective, if your login form is served by an Express server at the home route (/), the /login endpoint could look something like the following:

// express nodejs server 
const app = express(); 

// home page route
app.get('/', (req, res) => {
  res.send(/* Home page containing login form */);
});

// form processing route
app.post('/login', (req, res) => {
  // process incoming data in the request body
})


Conclusion

Use HTML (the least powerful language) to provide an inclusive experience wherever possible, and gradually complement it with CSS and JavaScript to enhance the UX. You don’t necessarily need JavaScript for everything. Keep it for the scenarios where HTML alone is not sufficient and you need more control and customization to provide a smooth UX, e.g., making AJAX calls.

Resources


Amandeep Singh

Written by Amandeep Singh. Developer @  Avarni  Sydney. Tech enthusiast and a pragmatic programmer.