Repository

js/Mobilizing/net/Ajax.js

import EventEmitter from "../core/util/EventEmitter";

// Events
/**
* Fired when the readystate of the request changes
* @event readystatechange
* @param {Ajax} ajax The Ajax instance
*/
const EVT_READYSTATE_CHANGE = "readystatechange";

/**
* Fired when the request is opened
* @event start
* @param {Ajax} ajax The Ajax instance
*/
const EVT_START = "start";

/**
* Fired when an operation is in progress
* @event progress
* @param {Ajax} ajax The Ajax instance
*/
const EVT_PROGRESS = "progress";

/**
* Fired when the request has finished loading
* @event load
* @param {Ajax} ajax The Ajax instance
* @param {Boolean} success Whether the request was successfull or not
*/
const EVT_LOAD = "load";

/**
* Fired when the request has finished loading and the status is greater or equal to 200 and less than 300, or is equal to 304
* @event success
* @param {Ajax} ajax The Ajax instance
*/
const EVT_SUCCESS = "success";

/**
* Fired when the request encountered an error, or if it finished loading with an error status
* @event error
* @param {Ajax} ajax The Ajax instance
*/
const EVT_ERROR = "error";

/**
* Ajax is a helper class used to simplify the use of the XMLHttpRequest API
*
* @example
*    //TODO
*/
export default class Ajax {


    /**
    * @param {Object} params Parameters object, given by the constructor.
    * @param {String} params.url The URL to which the request is sent
    * @param {String} [params.method = "GET"] The method used for the request (GET, POST, or PUT)
    * @param {Object} [params.data = null] Data to be send along with the request
    * @param {Object} [params.headers = null] An object of header key/value pairs to send along with requests
    * @param {Boolean} [params.async = true] Whether the request is asynchronous or not
    * @param {Boolean} [params.autoSend = true] Whether the request should be automatically sent
    * @param {Boolean} [params.withCredentials = null] Whether cross-site Access-Control requests should be made using credentials such as cookies or authorization headers
    * @param {String} [params.responseType = ""] The responseType this request must return
    * @param {Number} [params.timeout = null] The number of milliseconds the request can take before automatically being terminated
    * @param {Function} [params.onReadyStateChange = null] A function to be called when the readyState of the request changes
    * @param {Function} [params.onStart = null] A function to be called when the request starts
    * @param {Function} [params.onProgress = null] A function to be called when the request progresses
    * @param {Function} [params.onLoad = null] A function to be called when the request finishes
    * @param {Function} [params.onSuccess = null] A function to be called if the request succeeds
    * @param {Function} [params.onError = null] A function to be called if the request fails
    */
    constructor({
        url = "",
        method = "GET",
        async = true,
        data = null,
        headers = null,
        autoSend = true,
        withCredentials = null,
        timeout = null,
        responseType = "text",
        onReadyStateChange = null,
        onStart = null,
        onProgress = null,
        onLoad = null,
        onSuccess = null,
        onError = null,
    } = {}) {
        this.url = url;
        this.method = method;
        this.async = async;
        this.data = data;
        this.headers = headers;
        this.autoSend = autoSend;
        this.withCredentials = withCredentials;
        this.timeout = timeout;
        this.responseType = responseType;
        this.onReadyStateChange = onReadyStateChange;
        this.onStart = onStart;
        this.onProgress = onProgress;
        this.onLoad = onLoad;
        this.onSuccess = onSuccess;
        this.onError = onError;

        this._xhr = new XMLHttpRequest();

        this.events = new EventEmitter({ "scope": this });

        // bind custom callbacks to events
        if (this.onReadyStateChange) {
            this.events.on(EVT_READYSTATE_CHANGE, this.onReadyStateChange);
        }

        if (this.onStart) {
            this.events.on(EVT_START, this.onStart);
        }

        if (this.onProgress) {
            this.events.on(EVT_PROGRESS, this.onProgress);
        }

        if (this.onLoad) {
            this.events.on(EVT_LOAD, this.onLoad);
        }

        if (this.onSuccess) {
            this.events.on(EVT_SUCCESS, this.onSuccess);
        }

        if (this.onError) {
            this.events.on(EVT_ERROR, this.onError);
        }

        this._xhr.addEventListener("readystatechange", this.onXHRReadyStateChange.bind(this));

        // if this is a GET request, the data needs to be treated as URL parameters
        if (this.method === "GET" && this.data) {
            const params = [];

            Object.keys(this._data).forEach((key) => {
                params.push(`${key}=${encodeURIComponent(this._data[key])}`);
            });

            this._url += `?${params.join("&")}`;

            this._data = null;
        }

        this.setResponseType(this.responseType);

        if (this.withCredentials !== null) {
            this.setWithCredentials(this.withCredentials);
        }

        if (this.timeout !== null) {
            this.setTimeout(this.timeout);
        }

        if (this.autoSend) {
            this.send(this.data);
        }
    }

    /**
    * A helper function to create a request of type GET
    *
    * @param {Object} params Parameters object to pass to the constructor.
    */
    static get(params) {
        return new Ajax(Object.assign({}, params, {
            method: "GET"
        }));
    }

    /**
    * A helper function to create a request of type POST
    *
    * @param {Object} params Parameters object to pass to the constructor.
    */
    static post(params) {
        return new Ajax(Object.assign({}, params, {
            method: "POST"
        }));
    }

    /**
    * A helper function to create a request of type PUT
    *
    * @param {Object} params Parameters object to pass to the constructor.
    */
    static put(params) {
        return new Ajax(Object.assign({}, params, {
            method: "PUT"
        }));
    }

    /**
    * The readystatechange event handler
    *
    * @private
    */
    onXHRReadyStateChange() {
        this.events.trigger(EVT_READYSTATE_CHANGE, this);

        switch (this.getReadyState()) {
            case XMLHttpRequest.OPENED:
                this.events.trigger(EVT_START, this);
                break;

            case XMLHttpRequest.LOADING:
                this.events.trigger(EVT_PROGRESS, this);
                break;

            case XMLHttpRequest.DONE:
                {
                    const status = this.getStatus();
                    let success = false;

                    if (status === 200 && status < 300 || status === 304) {
                        success = true;
                    }
                    // local requests can return a status of 0 even if no error occurs
                    else if (status === 0 && !this._xhr.error) {
                        success = true;
                    }

                    this.events.trigger(EVT_LOAD, this, success);

                    if (success) {
                        this.events.trigger(EVT_SUCCESS, this);
                    }
                    else {
                        this.events.trigger(EVT_ERROR, this);
                    }

                    break;
                }
        }
    }

    /**
    * Get the XMLHttpRequest instance for the request
    * see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
    *
    * @return {XMLHttpRequest} The XMLHttpRequest instance
    */
    getXHR() {
        return this._xhr;
    }

    /**
    * Wrapping method for the setRequestHeader method of XMLHttpRequest
    * see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader
    *
    * @param {String} header The name of the header
    * @param {String} value The value of the header
    */
    setRequestHeader(header, value) {
        this._xhr.setRequestHeader(header, value);
    }

    /**
    * Wrapping method for the withCredentials property of XMLHttpRequest
    * see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
    *
    * @param {Boolean} value Whether cross-site Access-Control requests should be made using credentials such as cookies or authorization headers
    */
    setWithCredentials(value) {
        this._xhr.withCredentials = value;
    }

    /**
    * Wrapping method for the timeout property of XMLHttpRequest
    * see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout
    *
    * @param {Number} value The number of milliseconds the request can take before automatically being terminated
    */
    setTimeout(value) {
        this._xhr.timeout = value;
    }

    /**
    * Wrapping method for the responseType property of XMLHttpRequest
    * see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responsetype
    *
    * @param {String} value The type of response to be given by this request. Possible values are : "arraybuffer", "text", "json", "document", "blob"
    */
    setResponseType(value) {
        this._xhr.responseType = value;
    }

    /**
    * Wrapping method for the send method of XMLHttpRequest
    * see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/send
    *
    * @param {Mixed} data The data to send along the request
    */
    send(data) {
        this._xhr.open(this.method, this.url, this.async);

        // set custom headers, they must be set after opening the XHR
        if (this.headers !== null) {
            Object.keys(this.headers).forEach((name) => {
                this.setRequestHeader(name, this.headers[name]);
            });
        }

        this._xhr.send(data);
    }

    /**
    * Wrapping method for abort method of XMLHttpRequest
    * see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/abort
    *
    */
    abort() {
        this._xhr.abort();
    }

    /**
    * Wrapping method for status property of XMLHttpRequest
    * see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/status
    *
    * @return {Number} The status of the request
    */
    getStatus() {
        return this._xhr.status;
    }

    /**
    * Wrapping method for statusText property of XMLHttpRequest
    * see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/statusText
    *
    * @return {DOMString} The status message of the request
    */
    getStatusText() {
        return this._xhr.statusText;
    }

    /**
    * Wrapping method for readyState property of XMLHttpRequest
    * see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState
    *
    * @return {Number} The state the XMLHttpRequest client is in
    */
    getReadyState() {
        return this._xhr.readyState;
    }

    /**
    * Wrapping method for responseType property of XMLHttpRequest
    * see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType
    *
    * @return {String} The response"s type
    */
    getResponseType() {
        return this._xhr.responseType;
    }

    /**
    * Wrapping method for response property of XMLHttpRequest
    * see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/response
    *
    * @return {Mixed} The response"s body
    */
    getResponse() {
        return this._xhr.response;
    }

    /**
    * Wrapping method for responseText property of XMLHttpRequest
    * see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseText
    *
    * @return {String} The text response
    */
    getResponseText() {
        return this._xhr.responseText;
    }

    /**
    * Return the XMLHttpRequest responseText as a JSON object
    *
    * @return {Object} The responseText parsed as a JSON object
    */
    getJSON() {
        return JSON.parse(this.getResponseText());
    }
}