DRY Then and Always in jQuery Promise

Viewed 88

I have the following example JS code:

function loadData(url, data) {
    return $.get(url, data ? data : {});
}

function example1() {

    showSpinner();

    $.when(loadData('/example1', { hello: 'world' }))
        .then(function (resp) {
            // do something with the data
        })
        .always(function(){
            hideSpinner();
        })
        .fail(function (jqXHR, textStatus, errorThrown) {
            handleError(jqXHR);
        });
}

function example2() {

    showSpinner();

    $.when(loadData('/example2', { hello: 'world' }))
        .then(function (resp) {
            // do something with the data
        })
        .then(function () {
            // do something else
        })
        .always(function(){
            hideSpinner();
        })
        .fail(function (jqXHR, textStatus, errorThrown) {
            handleError(jqXHR);
        });
}

Both example1() and example2() use the same promise logic and will do something different with the returned data and then always hide the spinner and handle the failures the same (if any). The issue is that I may want to do different things after the data is loaded in each example and have multiple then's follow.

However I am having to repeat the always and fail code. How can I make them more DRY so they are only written once but used in both scenarios (and other examples if need be). So I can't just move the when into a method and pass a callback as there maybe more than one and in different then`s. So this wouldn't work:

function customPromise(load, callback) {

    showSpinner();

    $.when(load)
    .then(function (resp) {
        callback(resp);
    })
    .always(function(){
        hideSpinner();
    })
    .fail(function (jqXHR, textStatus, errorThrown) {
        handleError(jqXHR);
    });

}

function example3() {
    customPromise(loadData('/example2', { hello: 'world' }));
}
3 Answers

Perhaps you can factor the .when() out into a function returning a promise itself, then call that and chain onto the returned promise with .then(), so something like foobar().then().then(). There's no law stating that always or fail must come last in the chain.

This is a rough sketch for a solution, I have not tested this.

function loadData(url, data) {
    return $.get(url, data ? data : {});
}

function wrapWhen(endpoint, data) {

    // return a promise
    return $.when(loadData(endpoint, data))
        .always(function(){
            hideSpinner();
        })
        .fail(function (jqXHR, textStatus, errorThrown) {
            handleError(jqXHR);
        });
}

function example1() {

    showSpinner();

    wrapWhen('/example1', {hello: 'world'})
        .then(function (resp) {
            // do something with the data
        });
}

function example2() {

    showSpinner();

    wrapWhen('/example2', {hello: 'world'})
        .then(function (resp) {
            // do something with the data
        })
        .then(function () {
            // do something else
        });
}

If you have successive and unknown number of callbacks, I suggest you move the whole processing to the created function:

function handleRequest(url, data, errorCallback) {
    // create promise
    let promise = $.get(url, data ? data : {});

    // if callback functions provided as args, chain them in `then()` calls
    if (arguments.length > 3) {
        let callbacks = Array.slice.call(arguments, 3);
        let fn = (results) => (results);
        let callback;
        for (let i = 0; i < callbacks.length; i++) {
            callback = callbacks[i] instanceof Function ? callbacks[i] : fn;
            promise.then(callback);
        }
    }

    // handle static behavior
    promise.always(() => {
        hideSpinner();
    })
    .fail(errorCallback);

    // return the created promise
    return promise;
}

You can now use this function as follows:

handleRequest(
    '/example',
    {},
    (jqXHR, textStatus, errorThrown) => {
        handleError(jqXHR);
    },
    (resp) => {
        // do something with the data
    },
    () => {
        // do something else
    }
);

I could think of something like this. Not sure if loadData should be held responsible for showing the spinner.

$(function() {
  example1();
  example2();
});

function loadData(url, data) {
  showSpinner();

  return $.get(url, data ? data : {})
    .always(function() {
      hideSpinner();
    })
    .fail(function(jqXHR, textStatus, errorThrown) {
      handleError(jqXHR);
    });
}

function example1() {
  $.when(loadData('https://jsonplaceholder.typicode.com/users', {
      hello: 'world'
    }))
    .then(function(resp) {
      console.log(resp.length + ' lengthed data received');
    });
}

function example2() {
  $.when(loadData('https://jsonplaceholder.typicode.com/posts', {
      hello: 'world'
    }))
    .then(function(resp) {
      return resp;
    })
    .then(function(data) {
      console.log(data.length + ' lengthed data received');
    });
}

function showSpinner() {
  console.log('Showing spinner...');
}

function hideSpinner() {
  console.log('Hiding spinner...');
}

function handleError(xhr) {
  console.error(xhr);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Related