When you begin moving from server-side web development to a client-side approach to driving your user interfaces, a common first experiment is to enhance an existing button with some JavaScript that runs when the button is clicked. For example, clicking a button that should request and display the current time from a server is basically the “hello world” of AJAX.

Unfortunately, that common learning progression is prone to a confusing outcome when you’re working with buttons within form elements. The problem is that you need to take into account that the browser’s form submission mechanism is not interrupted by client-side JavaScript by default.

A common example

An early experiment that you might try is augmenting a button with jQuery so that its click event is handled on the client-side. Rather than a full POST to the server, you could route a request to the server asynchronously with $.ajax instead.

Displaying the server’s current DateTime.Now is a good way to ensure that requests are being sent and processed immediately. So, we’ll make a request for that when the button is clicked and then display the result in another element.

For example, here’s some markup that you might see during that kind of transition from ASP.NET WebForms a client-side UI:

That structure with the form container is the norm coming from ASP.NET WebForms pages. Since most of a page’s markup is wrapped in a single form element in WebForms, even simple test pages often end up like this.

To handle click events on the button and re-route them to an AJAX endpoint, you might use jQuery to intercept the button’s click event like this:

Unfortunately, clicking the button will seem to do nothing now, leading you to question your JavaScript, Web API endpoint, or maybe even your fundamental understanding of how AJAX works. The real problem turns out to be much simpler than those things, but it can be quite a nuisance to track down the first time.

Identifying what’s going wrong

Cracking open the developer tools in your browser of choice and navigating to its network details section helps illuminate what’s going wrong. For example, this is roughly what you’d see in Chrome’s developer tools when you click the button:

Canceled? No, no, no...

Canceled? No, no, no…

Notice that the AJAX request to /api/date did begin, but immediately turns red and has a (canceled) status. Next, there’s a GET to localhost, and you can see all of the page’s assets being loaded after that. Unfortunately, that also seems to have killed our pending $.ajax request.

Why did this happen? Clicking the button did run our JavaScript code, which we can verify by seeing that the request began. However, it also triggered a form submission since that’s the browser’s default behavior when buttons inside forms are clicked.

Since the browser’s built-in behavior for submitting forms works by making a full roundtrip to the server, that immediately interrupts the AJAX request in progress because the browser is navigating away from the current page. Essentially, the result is the same as if you had clicked the button and then refreshed the page instantly afterward, before the AJAX request had a chance to complete.

Making requests to a local development server running on a quick machine, the page reload after form submission happens quickly. Notice that you can’t see any visible indication at all that the page has just been replaced in the animation above, even if you’re looking carefully for the flicker. It just seems like clicking the button doesn’t do anything.

Note: You will need to enable the option that preserves the network log across multiple requests in order to see the entire sequence of requests shown above. Otherwise, your network details may only show new entries beginning with the GET to localhost and you wouldn’t see the AJAX request at all.

Putting a leash on the browser’s default behavior

There are a couple ways to tame the browser’s form submission mechanism and make the button work asynchronously, as expected.

  • return false – In almost all cases, an event handler function returning false causes the browser to cancel any subsequent behaviors that the event would normally have triggered (like submitting a form). Using return false works, but you need to be careful about where you place it since the return statement can abort your code early and it can have unintended side-effects.
  • preventDefault() – The browser sends an event parameters to handlers, which details information about the action that triggered the handler. That event object also exposes a method that prevents the browser’s default reaction to that event: preventDefault(). For click handlers on submit buttons inside forms, that includes preventing the form submission.

Since return false does have the issue of sometimes breaking other code and plugins when it stops propagation, I use preventDefault() by… default.

Capturing the event object that jQuery passes into its event handlers and calling preventDefault() on it is as easy as this:

Now, preventDefault() stops the browser from submitting the form when we click the button, the browser continues listening for a response from the server, and our $.ajax success callback has an opportunity to run when that eventually happens:

premature-submission-fixed-with-preventdefault-sm

Now there’s just the single AJAX request to /api/date and no page reload.

If you want to play around with the example in the screenshots, you can view that example running live on Azure or find the source code on GitHub.

Conclusion

Other than the actual solution, the most important takeaway here is to use the browser’s developer tools. Thinking your way out of these kind of tricky issues is certainly possible, but it’s far more helpful to use the browser’s tooling to get better insight into what’s really happening.

The tooling in modern browsers is incredibly powerful if you learn to use it fully.

Finally, don’t feel bad if you ran into this problem. It seems face-palm obvious once you see what’s happening, but it really is awfully easy to miss when you’re focused on the AJAX code and not the form.