http/ajax.js

/**
 * Wrapper class for XHR
 * @module http/ajax
 * @memberOf http
 */
const {HttpRq, HttpContent} = require("./request");
const {HttpRs} = require("./response");
const T = require("../core/types");
const {forEach} = require("../core/collections");


/**
 * A wrapper class for {@link XMLHttpRequest} to facilitate sending requests and handling events
 * @class
 */
class Ajax {
     /**
     * @constructor
     * @param {HttpMethod|HttpRq} m - Request method string or {@link http.HttpRq}
     * @param {String} [url]
     * @param {Object} [params] - Request parameters object
     * @param {Object?} [headers] - Headers object
     * @param {HttpContent?} [content] - Optional http content {@link HttpContent}
     */
    constructor(m, url, params = {}, headers = {}, content = new HttpContent()) {
         /** @type {HttpRq}
          * @see {http.HttpRq}
          */
        this.rq = {}
         /** @type {http.HttpRs}
          * @see {http.HttpRs}
          */
         this.rs = {}
        if (m instanceof HttpRq) {
            this.rq = m
        } else {
            this.rq = new HttpRq(m, url, params, headers, content);
        }
        // Fields
        this.rs = {readyState: 0};
        this.xhr = new XMLHttpRequest();

        this.preparedCallback = function (rq) {
        };

        this.progressCallback = function (ev, rq) {
        };

        this.uploadProgressCallback = function (xhr) {
        };

        this.successCallback = function (rq, rs) {
        };
        this.uploadFinishCallback = function (xhr) {

        };
        this.failCallback = function (rq, rs) {
        };
        Object.defineProperty(this, 'xhr', {enumerable: false})
    }

    // Methods
    /**
     * Set header
     * @param {String} n - header name
     * @param {String} [v] - header value
     * @returns {Ajax}
     */
    header(n, v) {
        this.rq.setHeader(n, v);
        return this;
    };

    /**
     * Add/Set headers
     * @param {Object} hdrs - headers object
     * @returns {Ajax}
     */
    headers(hdrs = {}) {
        forEach(hdrs, (v, k) => {
            this.rq.setHeader(k, v);
        });
        return this;
    };

    /**
     * @param {Function} callbackRqRs
     * @returns {Ajax}
     */
    onSuccess(callbackRqRs) {
        this.successCallback = callbackRqRs;
        return this;
    };

    /**
     * @param {Function} callbackRqRs
     * @returns {Ajax}
     */
    onUploadSuccess(callbackRqRs) {
        this.uploadFinishCallback = callbackRqRs;
        return this;
    };

    /**
     * @param {Function} callbackRqRs
     * @returns {Ajax}
     */
    onFail(callbackRqRs) {
        this.failCallback = callbackRqRs;
        return this;
    };

    /**
     * @param {Function} callbackRqRs
     * @returns {Ajax}
     */
    onProgress(callbackRqRs) {
        this.progressCallback = callbackRqRs;
        return this;
    };

    /**
     * @param {Function} callbackRqRs
     * @returns {Ajax}
     */
    onUploadProgress(callbackRqRs) {
        this.uploadProgressCallback = callbackRqRs;
        return this;
    };

    /**
     * Set custom content {@link HttpContent}
     * @param {HttpContent} content
     * @returns {Ajax}
     */
    withContent(content={}) {
        switch (content.type) {
            case 'json': this.rq.jsonContent(content.data); break;
            case 'xml': this.rq.xmlContent(content.data); break;
            case 'form': this.rq.formContent(content.data); break;
            case 'form_multipart': this.rq.formMultiPartContent(content.data); break;
            case 'form_urlencoded': this.rq.formUrlEncodedContent(content.data); break;
            default: this.rq.setContent(content.type, content.data);
        }
        return this;
    }

    /**
     * Set xml request data
     * @param {XMLDocument} data
     * @returns {Ajax}
     */
    xmlData(data) {
        this.rq.xmlContent(data);
        return this;
    }

    /**
     * Set form-data request data
     * @param {String|Node} form
     * @returns {Ajax}
     */
    formData(form) {
        this.rq.formContent(form);
        return this;
    };

    /**
     * Set json request data
     * @param {String|Object} data
     * @returns {Ajax}
     */
    jsonData(data) {
        this.rq.jsonContent(data);
        return this;
    };

    /**
     * Set url-encoded request data
     * @param {Object} data - simple data object
     * @returns {Ajax}
     */
    urlEncodedData(data) {
        this.rq.formUrlEncodedContent(data);
        return this;
    };

    _prepare(reset) {
        if (this.isPrepared && !reset) {
            // self.onprepare && self.onprepare(self.rq);
            return this
        }

        // prepare url
        let url = this.rq.url;

        if (this.rq.args && !T.isEmpty(this.rq.args)) {
            url.indexOf('?') >= 0 || (url += '?');
            url += this.rq.buildUrlEncoded();
        }
        reset && (this.xhr = new XMLHttpRequest());
        this.xhr.open(this.rq.method, url);

        // prepare headers
        for (let h in this.rq.headers) {
            if (this.rq.headers.hasOwnProperty(h))
                this.xhr.setRequestHeader(h, this.rq.headers[h]);
        }

        this.isPrepared = true;
        this.preparedCallback && this.preparedCallback(this.rq);
        // preparedCallback && preparedCallback();
        return this;
    };

    /**
     * Send XHR request
     * @param {Function} [finishCallback] - called after all other callbacks
     * @returns {Ajax}
     */
    send(finishCallback) {
        this._prepare();
        let ajax = this;
        let xhr = this.xhr;
        this.xhr.onreadystatechange = function (ev) {

            // onloadend
            if (xhr.readyState === 4) {
                let callback;
                if (xhr.status >= 200 && xhr.status <= 399) {
                    callback = ajax.successCallback;
                } else {
                    callback = ajax.failCallback;
                }
                ajax.rs = new HttpRs(xhr);
                finishCallback && finishCallback(ajax.rq, ajax.rs, ajax.xhr);
                callback && callback(ajax.rq, ajax.rs, ajax.xhr);
            }
        };
        this.xhr.onprogress = function (ev) {
            ajax.progressCallback && ajax.progressCallback(ev, ajax);
        };

        this.xhr.upload.onprogress = function (ev) {
            ajax.uploadProgressCallback && ajax.uploadProgressCallback(ev, ajax);
        };

        this.xhr.upload.onloadend = function (ev) {
            ajax.uploadFinishCallback && ajax.uploadFinishCallback(ev, ajax);
        };

        try {
            this.xhr.send(this.rq.content.data);
        } catch (e) {
            this.onFail(e);
        }

        return ajax;
    }

    /**
     * Send request with Promise
     * @return {Promise<Ajax>}
     */
    async sendAsync() {
        const ajax = this;
        const promise = new Promise((res, rej) => {
            ajax.onSuccess(()=>{
                return res(ajax)
            });
            ajax.onFail(()=>rej(ajax));
        });
        ajax.send();
        return promise;
    }
}

module.exports = {Ajax: Ajax};