skip to content

JavaScript: Making an asynchronous request before following an href

Sometimes you want to be able to 'capture' the event of a user clicking a link that takes them to another page. Not necessarily for tracking purposes, but also if the click indicates a choice that needs to be stored in a back-end database or transmitted to an external API. And without breaking SEO.

What doesn't work

In most browsers if you try to just use an onclick event to make the asynchronous request it will be cancelled before it is actually carried out, by the browser redirecting to the new page. For example:

<a onclick="callAjax(); return true;" href="/javascript/onclick-async-href/">click here</a>

This is because the browser does not 'wait' for the asynchronous call to complete - because it's asynchronous - before returning true which activates the link. And loading a new URL in the browser cancels any outstanding requests.

You can cancel the href event by instead returning false in the example above, in which case the Ajax call will take place, but then the link will not be followed.

Using a callback function

One possible solution is to use JavaScript to:

  1. disable the href event;
  2. make the Ajax asynchronus request;
  3. trigger a callback function that follows the href link.

Rather than doing this one link at a time, it would be helpful to be able to apply it to multiple links on a page at once.

Below you can see a demonstration of this. Each link will call a PHP script which does nothing other than sleep a set number of seconds before returning a response.

The response in turn triggers a callback function in the page which takes the browser to the link href (reloading the same page in this case). Click one of the links below to see this in action:

We're not going to go into the details of making Ajax calls or reponses here as that's been covered in detail in previous articles. And the sleep in this example is just a proxy for the execution time of a proper server-side script.

The point is that the link action is being deferred until after the response comes back from the asynchronous call.

The HTML markup for the above links is as follows:

<ul> <li><a class="ajax-link" data-wait="2" href="/javascript/onclick-async-href/">wait 2 seconds</a></li> <li><a class="ajax-link" data-wait="5" href="/javascript/onclick-async-href/">wait 5 seconds</a></li> <li><a class="ajax-link" data-wait="10" href="/javascript/onclick-async-href/">wait 10 seconds</a></li> </ul>

JavaScript code for this example

As you can see above, the links in our example have been marked up with class="ajax-wait" and data-wait="x" where x is the parameter to pass via POST to our Ajax script. The link itself we can access through the href attribute.

<script src="/scripts/AjaxRequestXML.js"></script> <script> const callAjax = function(waitTime, callbackFunction) { let params = {}; params.wait = waitTime; /* time to sleep in seconds */ return (new AjaxRequestXML()).post("ajax-wait.xml", params, callbackFunction); }; const AjaxClickHandler = function(e) { let el = e.target; let linkURL = el.href; let waitTime = el.dataset.wait; /* value from data-wait attribute */ let callback = () => self.location.href = linkURL; /* callback function redirects to link href */ e.preventDefault(); /* prevent link from being followed immediately */ el.style.cursor = "wait"; callAjax(waitTime, callback); }; document.querySelectorAll(".ajax-link").forEach(function(current) { current.addEventListener("click", AjaxClickHandler); }); </script>

The tricky part here is that we generate a callback function on the fly for each link and pass it to the AjaxRequestXML class so that it can be triggered there by the Ajax response handler.

A global callback function would be useless as it won't have access to the link href (linkURL). By defining the function within the scope of the click handler for each link, however, we can access that value.

For the curious, our Ajax response contains no more than the following:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <response> <command method="callback"/> </response>

Please feel free to use and modify this code as you see fit, and let us know using the comments section below if you have any questions.

Using the Fetch API

In modern browsers the old XMLHttpRequest has been superceded by the new and much more powerful Fetch API. Using Fetch we can do away with our Ajax class and make the POST request directly inline:

<script> const FetchClickHandler = (e) => { e.preventDefault(); /* prevent link from being followed immediately */ e.target.style.cursor = "wait"; let fetchOptions = { method: "POST", body: "wait=" + e.target.dataset.wait, /* value from data-wait attribute */ headers: { "Content-Type": "application/x-www-form-urlencoded" } }; fetch("ajax-wait.xml", fetchOptions) .then(() => { self.location.href = e.target.href; }); }; document.querySelectorAll(".ajax-link").forEach((current) => { current.addEventListener("click", FetchClickHandler); }); </script>

Using fetch the body of the request can be form data, JSON, or a range of other data types. What's important is that the correct Content-Type header accompanies the request.

Switch to Navigator.sendBeacon()

The problem with both the Ajax (XMLHTTPRequest) and Fetch approaches is that they expect to receive a response back from the server, and are cancelled by the browser if the requesting page is closed by following a link to a new URL.

This is where Navigator.sendBeacon() comes in. It's specifically designed for sending small amounts of data to the server without expecting a response. Google Analytics has been using this approach in compatible browsers since 2014.

Now there is no longer any need for a delay between the click event and the link being followed. We simply package and send the data we want to be recorded to the beacon while letting the link otherwise behave normally:

<script> const BeaconClickHandler = (e) => { let beaconData = new FormData(); beaconData.append("param", e.target.dataset.param); /* add POST key-pair */ navigator.sendBeacon("click-beacon.php", beaconData); }; document.querySelectorAll(".beacon-link").forEach((current) => { current.addEventListener("click", BeaconClickHandler); }); </script>

Of course the downside is that you no longer have any way of confirming that the data was received. Only that it was sent. Even a 404 error will not raise a notice.

If you want to use this for recording outbound links from your website check out this earlier article on the subject. Just replace the Ajax call with sendBeacon().

Drawbacks

The above approaches all have a common Achilles' heel in that if the link is right-clicked to "open in a new window (or tab)" the click event is bypassed and the new link opens directly.

There is no easy way to prevent this without either converting the link to pure JavaScript (no href) or inserting an interstitial URL, both of which break SEO.

Trackers get around this by monitoring other page events such as unload (no longer recommended), pagehide and visibilitychange to trigger beacon events.

References

< JavaScript

Post your comment or question
top