Believe it or not, but it’s not a good idea to disable a submit button when the associated form is in invalid state 👮. You can read about cons of such approach in this great blog post. On the other hand, you may face a problem that I had to deal with recently, namely handling large forms which usually take more space than a viewport height. Imagine that the very first control is invalid and a user clicks the submit button. Since, the element is off-screen, she/he cannot figure out immediately what went wrong, because an error message is rendered just below the control element. The solution to the problem was to scroll a document, once an invalid form has been submitted, so that the first invalid control appeared at the top of the viewport 🎆 . In this blog post I would like to present how I dealt with the challenge in Angular application.
I’ve prepared a simple demo so that you can take a look at a live example here. The form component has the following markup:
with the associated logic:
In order to make the form element take more height than the viewport size and distinguish invalid controls, I simply applied some css rules 🌈 :
Steps to take
To solve the problem in question, you need to perform two things once a form has been submitted:
- find the first invalid control element,
- imperatively scroll the container element (e.g. window) so that the control element appears at the top of the viewport.
Simple as that 😏 .
The easiest solution, which is easy to make it framework-agnostic as well, is to simply take the first invalid control html element and focus on it. A browser default behavior ensures that the focused DOM node appears within the viewport. If you use Angular, it’s super-easy to grab the control element, since the framework appends special css classes to form control elements based on their current status. As a result, you can query for the first element with .ng-invalid class within a form container in order to grab the node to focus:
However, you may not be satisfied with the default scroll behavior which happens immediately without a smooth transition 😢. Let’s improve the basic approach, so that it’s more user friendly.
Basic solution with smooth scroll
A good news is that you no longer have to use a third-party library to perform smooth scrolling. You can specify a desired scroll behavior as a part of the scroll method’s config:
With a little bit of maths, you can quickly calculate the top offset value ➕. If you have any fixed navbar, you need to take its height in account as well ⚠️.
The scroll action happens in a smooth manner, however the control element is no longer focused. This is not a desired behavior when it comes to accessability. You cannot simply call the focus method immediately, since it would not allow for smooth scrolling. The action has to be performed once the scroll has finished. RxJS to the rescue 🚁:
With just several lines of code I accomplished the goal. However, there’s a room for two improvements, namely making it reusable (so that you don’t have to copy-paste the code in every long form component or extend a base class) and allowing to scroll relative to other container than the window.
Encapsulate logic in directive
Wouldn’t it be grateful to simply apply a directive to a form element in order to enable the feature that I’ve just implemented at the component level? Of course, it would! Let’s dive into the code:
The scrolling-related code can be extracted from the component into the directive. The action may need to be performed once a form has been submitted, so you need to use a HostListener to keep track of the appropriate event. In addition, the code has to be executed only if the form is invalid, hence you need to get an access to the FormGroupDirective instance with the aid of dependency injection.
The usage of the directive is dead simple, you just need to apply it to a form element 🚀:
and the component’s code no longer contains the scrolling-related parts:
Custom scroll container
The current solution assumes that a form overflow results in a scrollbar appearing at the viewport level. However, you may limit the area for a form element by wrapping it into a container with the maximum width specified. In such scenario, the current solution is insufficient. In order to make it handle the aforementioned case, you need to get a reference to the container in the directive and apply some more maths ➕ ➕.
You can mark the wrapper with the aid of a custom directive which simply exposes the underlying html element:
In the directive containing scrolling-related code, you need to take into account an optional existence of such wrapper element:
With the aid of dependency injection, a reference to the wrapper directive can be obtained. Note that it’s optional, since you may stick with the default one which is a browser window. The getter for a container checks the directive existence and returns a fallback window object if it’s not provided. In addition, you need to alter the method for calculating the top offset so that it takes into account different usages.
In this blog post, I explained how to accomplish the goal of navigating to the first invalid form control once a form has been submitted. Angular makes it easy to grab a reference to such control by applying css classes reflecting its current state (ng-valid, ng-invalid, ng-touched etc.). Focusing such element and making use of a browser default behavior is a low-hanging fruit 🍎. However, in order to ensure smooth scrolling, you need to call the scroll method with a user-defined behavior. In addition, you should focus the control once the container has been scrolled for accessability concerns ♿️. In the most basic scenario, the code may end up in a component, however it’s a good idea to extract it into a directive to make it easily reusable and composable with other directives applied to a form element.
Feel free to play around with the example:
I hope you liked the post and learned something new 👍 If so, please give me some applause 👏