Goodbye $.ajax - The clock is ticking

| 7 minutes

Back then, when each browser had its own set and understanding of "supporting" JavaScript features, one knight in shiny armor saved us maiden developers and allowed us to focus on our tasks: jQuery. There was no necessity to remember every browser quirk or buggy implementation, jQuery was there and covered us.

But sometimes we have to let things go. The TYPO3 Core minimizes the usage of jQuery in an ongoing process. For example the querySelector allows to select a specific element by CSS selectors, similar to Sizzle, the selector engine developed and used by jQuery.

But other parts of jQuery get replaced as well with modern, native APIs. During December's Review Friday the new AJAX API got merged, which implements the Fetch API under the hood. Fetch uses Promises under the hood which makes is very easy to chain success or error handlers to a request.

The module is located at TYPO3/CMS/Core/Ajax/AjaxRequest and may be used with RequireJS. A very basic example looks like this:

require(['TYPO3/CMS/Core/Ajax/AjaxRequest'], function (AjaxRequest) {
  new AjaxRequest('https://httpbin.org/json').get().then(
    async function (response) {
      const data = await response.resolve();
      console.log(data);
    }
  );
});

But what happens here? We create an AjaxRequest object that only takes the target URL as argument. After that we're free in "decorating" the request. In this case, we send the request as GET and process the response. The response is of type AjaxResponse which exposes the methods resolve() and raw(). In this example we use resolve(), which checks whether the response contains a Content-Type header that matches to any JSON content. If this assumption is true, a JSON object is returned, otherwise we get a plaintext response that could contain any string.

What's going on with these async and await keywords? Simplified speaking, this is a shortcut to handle Promises in a more comfortable way.

I know what I'm doing

For more advanced usage, raw() may be used instead which returns the original response object:

require(['TYPO3/CMS/Core/Ajax/AjaxRequest'], function (AjaxRequest) {
  new AjaxRequest('https://httpbin.org/json').get().then(
    async function (response) {
      const response = response.raw();
      if (raw.headers.get('Content-Type') !== 'application/json') {
        console.warn('We didn\'t receive JSON, check your request.');
        return;
      }
      console.log(await raw.json());
    }
  );
});

Error error on the wall

That's all nice, but how are errors handled? Errors may happen anytime, either if the client's network is down or if the requested endpoint is not available anymore. The original implementation of Fetch is a bit strange here: errors sent by the remote (e.g. HTTP status 500) are not handled as failure, but client errors are. Since this is not feasible and doesn't improve the developers experience, AjaxRequest always throws a ResponseError, which contains the original response:

require(['TYPO3/CMS/Core/Ajax/AjaxRequest'], function (AjaxRequest) {
  new AjaxRequest('https://httpbin.org/status/500').get().then(
    function (response) {
      // Empty on purpose
    }, function (error) {
      console.error('Request failed because of error: ' + error.response.status + ' ' + error.response.statusText);
    }
  );
});

Attach query string on demand

But the API does more: if you need to add query arguments to the URL, call withQueryArguments() of the request object. The method accepts a string, an array and a (nested) object, the query string is generated and appened to the URL at the point the request is sent:

const request = new AjaxRequest('https://httpbin.org/json').withQueryArguments({foo: 'bar'});

withQueryArguments() clones the request object.

The API detects whether the URL already contains a query string and appends the passed data.

Other methods

All examples send GET requests. Of course, the AjaxRequest can send for example POST requests. For this, use the method post() and pass an object as argument, the object is automatically converted to a FormData object:

require(['TYPO3/CMS/Core/Ajax/AjaxRequest'], function (AjaxRequest) {
  const body = {
    foo: 'bar',
    baz: 'quo'
  };
  new AjaxRequest('https://example.com').post(body).then(
    async function (response) {
      console.log('Data has been sent');
    }
  );
});

There's also built-in support for PUT and DELETE, the methods are put() and delete() accordingly.

Change the defaults

The AjaxRequest already sets some sane defaults. For example each request sends cookies, as long the URLs are in the same origin. All responses, except those from GET requests, are uncached by default. If a single request has special needs, it's always possible to extend the request options. Each method to send a requests takes an additional argument to pass such request options:

require(['TYPO3/CMS/Core/Ajax/AjaxRequest'], function (AjaxRequest) {
  const request = new AjaxRequest('https://httpbin.org/json').get({redirect: 'error'});
});

We configure the request to throw an error in case a redirect happens. A lot more options are documented here.

Header photo by alexandra vicol on Unsplash.

Previous Post Next Post