Skip to main content

A tiny, Promise-based vanilla JS Ajax/HTTP plugin with great browser support.

/*!
 * atomicjs v4.4.0
 *
 * A tiny, Promise-based vanilla JS Ajax/HTTP plugin with great browser support.
 *
 * https://github.com/cferdinandi/atomic/blob/master/dist/atomic.js
 * https://gomakethings.com/promise-based-xhr/
 *
 * Example Usage:
 *
 * // A basic GET request
 * atomic('https://some-url.com')
 *     .then(function (response) {
 *         console.log(response.data); // xhr.responseText
 *         console.log(response.xhr);  // full response
 *     })
 *     .catch(function (error) {
 *         console.log(error.status); // xhr.status
 *         console.log(error.statusText); // xhr.statusText
 *     });
 *
 * // A POST request
 * atomic('https://some-url.com', {
 *     method: 'POST'
 * })
 *     .then(function (response) {
 *         console.log(response.data); // xhr.responseText
 *         console.log(response.xhr);  // full response
 *     })
 *     .catch(function (error) {
 *         console.log(error.status); // xhr.status
 *         console.log(error.statusText); // xhr.statusText
 *     });
 *
 * Options:
 *
 *  atomic('https://some-url.com', {
 *     method: 'GET', // {String} the request type
 *     username: null, // {String} an optional username for authentication purposes
 *     password: null, // {String} an optional password for authentication purposes
 *     data: {}, // {Object|Array|String} data to be sent to the server
 *     headers: { // {Object} Adds headers to your request: request.setRequestHeader(key, value)
 *         'Content-type': 'application/x-www-form-urlencoded'
 *     },
 *     responseType: 'text', // {String} the response type (https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType)
 *     timeout: null, // {Integer} the number of milliseconds a request can take before automatically being terminated
 *     withCredentials: false // {Boolean} If true, send credentials with request (https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials)
 * });
 *
 * (c) 2019 Chris Ferdinandi
 * MIT License
 * https://github.com/cferdinandi/atomic
 */

(function(root, factory) {
    if (typeof define === "function" && define.amd) {
        define([], function() {
            return factory(root);
        });
    } else if (typeof exports === "object") {
        module.exports = factory(root);
    } else {
        window.atomic = factory(root);
    }
})(
    typeof global !== "undefined"
        ? global
        : typeof window !== "undefined"
        ? window
        : this,
    function(window) {
        "use strict";

        //
        // Variables
        //

        var settings;

        // Default settings
        var defaults = {
            method: "GET",
            username: null,
            password: null,
            data: {},
            headers: {
                "Content-type": "application/x-www-form-urlencoded"
            },
            responseType: "text",
            timeout: null,
            withCredentials: false
        };

        //
        // Methods
        //

        /**
         * Feature test
         * @return {Boolean} If true, required methods and APIs are supported
         */
        var supports = function() {
            return (
                "XMLHttpRequest" in window &&
                "JSON" in window &&
                "Promise" in window
            );
        };

        /**
         * Merge two or more objects together.
         *
         * @param   {Object}   objects  The objects to merge together
         * @returns {Object}            Merged values of defaults and options
         */
        var extend = function() {
            // Variables
            var extended = {};

            // Merge the object into the extended object
            var merge = function(obj) {
                for (var prop in obj) {
                    if (obj.hasOwnProperty(prop)) {
                        if (
                            Object.prototype.toString.call(obj[prop]) ===
                            "[object Object]"
                        ) {
                            extended[prop] = extend(extended[prop], obj[prop]);
                        } else {
                            extended[prop] = obj[prop];
                        }
                    }
                }
            };

            // Loop through each object and conduct a merge
            for (var i = 0; i < arguments.length; i++) {
                var obj = arguments[i];
                merge(obj);
            }

            return extended;
        };

        /**
         * Parse text response into JSON
         * @private
         * @param  {String} req The response
         * @return {Array}      A JSON Object of the responseText, plus the orginal response
         */
        var parse = function(req) {
            var result;
            if (
                settings.responseType !== "text" &&
                settings.responseType !== ""
            ) {
                return { data: req.response, xhr: req };
            }
            try {
                result = JSON.parse(req.responseText);
            } catch (e) {
                result = req.responseText;
            }
            return { data: result, xhr: req };
        };

        /**
         * Convert an object into a query string
         * @link   https://blog.garstasio.com/you-dont-need-jquery/ajax/
         * @param  {Object|Array|String} obj The object
         * @return {String}                  The query string
         */
        var param = function(obj) {
            // If already a string, or if a FormData object, return it as-is
            if (
                typeof obj === "string" ||
                Object.prototype.toString.call(obj) === "[object FormData]"
            )
                return obj;

            // If the content-type is set to JSON, stringify the JSON object
            if (
                /application\/json/i.test(settings.headers["Content-type"]) ||
                Object.prototype.toString.call(obj) === "[object Array]"
            )
                return JSON.stringify(obj);

            // Otherwise, convert object to a serialized string
            var encoded = [];
            for (var prop in obj) {
                if (obj.hasOwnProperty(prop)) {
                    encoded.push(
                        encodeURIComponent(prop) +
                            "=" +
                            encodeURIComponent(obj[prop])
                    );
                }
            }
            return encoded.join("&");
        };

        /**
         * Make an XHR request, returned as a Promise
         * @param  {String} url The request URL
         * @return {Promise}    The XHR request Promise
         */
        var makeRequest = function(url) {
            // Create the XHR request
            var request = new XMLHttpRequest();

            // Setup the Promise
            var xhrPromise = new Promise(function(resolve, reject) {
                // Setup our listener to process compeleted requests
                request.onreadystatechange = function() {
                    // Only run if the request is complete
                    if (request.readyState !== 4) return;

                    // Process the response
                    if (request.status >= 200 && request.status < 300) {
                        // If successful
                        resolve(parse(request));
                    } else {
                        // If failed
                        reject({
                            status: request.status,
                            statusText: request.statusText,
                            responseText: request.responseText
                        });
                    }
                };

                // Setup our HTTP request
                request.open(
                    settings.method,
                    url,
                    true,
                    settings.username,
                    settings.password
                );
                request.responseType = settings.responseType;

                // Add headers
                for (var header in settings.headers) {
                    if (settings.headers.hasOwnProperty(header)) {
                        request.setRequestHeader(
                            header,
                            settings.headers[header]
                        );
                    }
                }

                // Set timeout
                if (settings.timeout) {
                    request.timeout = settings.timeout;
                    request.ontimeout = function(e) {
                        reject({
                            status: 408,
                            statusText: "Request timeout"
                        });
                    };
                }

                // Add withCredentials
                if (settings.withCredentials) {
                    request.withCredentials = true;
                }

                // Send the request
                request.send(param(settings.data));
            });

            // Cancel the XHR request
            xhrPromise.cancel = function() {
                request.abort();
            };

            // Return the request as a Promise
            return xhrPromise;
        };

        /**
         * Instatiate Atomic
         * @param {String} url      The request URL
         * @param {Object} options  A set of options for the request [optional]
         */
        var Atomic = function(url, options) {
            // Check browser support
            if (!supports())
                throw "Atomic: This browser does not support the methods used in this plugin.";

            // Merge options into defaults
            settings = extend(defaults, options || {});

            // Make request
            return makeRequest(url);
        };

        //
        // Public Methods
        //

        return Atomic;
    }
);