PROWARE technologies
PROWARE technologies

AJAX Library for React/REST API

The following function is commonly used in AJAX REST API calls like that which is used with React. The ajax2() function submits the AJAX request and is a Promise.

If not already familiar with what a REST API is then see the REST API Tutorial.

While ajax2() should work on IE6 thanks to its synchronous operation when a Promise is not available, React and React-DOM probably are not compatible with this old browser.

This function, ajax2(), only supports submitting JSON in the request body.

JSX is not used here because of the size of the files to support it.

Download all the code for this project: REACTAJAX2.zip

ajax2.js

// ajax2.js
/*
request = {
	verb: "GET POST PUT PATCH DELETE",
	path: "/api/",
	headers: {header1:"value1",header2:"value2"},
	data: '{"is":"json"}' or Object,
	onprogress: function(percent){}
};
*/
function ajax2(request) {
	var obj = "object";
	var undef = "undefined";
	if (typeof request != obj) { request = {}; }
	var canPromise = (typeof Promise != undef);
	var xmlobj;
	if (typeof XMLHttpRequest != undef) { // must use typeof operator for compatibility reasons
		xmlobj = new XMLHttpRequest();
	}
	else if (typeof window.ActiveXObject != undef) {
		var aVersions = ["MSXML2.XMLHttp.5.0", "MSXML2.XMLHttp.4.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp", "Microsoft.XMLHttp"];
		for (var i = 0; i < aVersions.length; i++) {
			try {
				xmlobj = new window.ActiveXObject(aVersions[i]);
				break;
			} catch (err) {
				//void
			}
		}
	}
	if (typeof xmlobj != obj) {
		return {then:function(){return{catch:function(ca){ca("XMLHttpRequest object could not be created");}}}};
	}
	if(typeof request.onprogress == "function" && typeof xmlobj.upload == obj) {
		xmlobj.upload.addEventListener("progress", function (event) {
			request.onprogress(Math.floor(event.loaded / event.total * 100));
		});
	}
	// if no verb is specified then use "get"; if no path is specified then use the current file
	xmlobj.open(request.verb || "get", request.path || location.pathname, canPromise);
	xmlobj.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
	if(typeof request.headers == obj) {
		for(var prop in request.headers) {
			xmlobj.setRequestHeader(prop, request.headers[prop]);
		}
	}
	if(typeof request.data == obj) {
		try {
			request.data = JSON.stringify(request.data);
		}
		catch {
		}
	}
	xmlobj.send(request.data || null);
	if(canPromise) {
		return new Promise(function (resolve, reject) {
			xmlobj.onreadystatechange = function () {
				if (xmlobj.readyState == 4) {
					if (xmlobj.status >= 200 && xmlobj.status < 300) {
						resolve(xmlobj.responseText);
					}
					else {
						reject(xmlobj.statusText);
					}
				}
			};
		});
	}
	else {
		if (xmlobj.status >= 200 && xmlobj.status < 300) {
			return {then:function(th){th(xmlobj.responseText);return{catch:function(){}}}};
		}
		else {
			return {then:function(){return{catch:function(ca){ca(xmlobj.statusText);}}}};
		}
	}
}

Here is example React code using the above AJAX library.

// customers.modern.js
'use strict';

function renderTable(obj) {
	var ths = [];
	ths[ths.length] = React.createElement("th", {colspan:"4"}, "Customers");
	var trs = [];
	trs[trs.length] = React.createElement("tr", null, ths);
	var thead = React.createElement("thead", null, trs);
	ths = [];
	ths[ths.length] = React.createElement("th", null, "NAME");
	ths[ths.length] = React.createElement("th", null, "ADDRESS");
	ths[ths.length] = React.createElement("th", null, "ZIP");
	ths[ths.length] = React.createElement("th");
	trs = [];
	trs[trs.length] = React.createElement("tr", null, ths);
	for(var i = 0; obj.state.array && i < obj.state.array.length; i++) { // could use array.forEach() instead
		var tds = [];
		tds[tds.length] = React.createElement("td", null, obj.state.array[i].name);
		tds[tds.length] = React.createElement("td", null, obj.state.array[i].addr);
		tds[tds.length] = React.createElement("td", null, obj.state.array[i].zip);
		var a = React.createElement("a", {class:"btn btn-danger", custid:obj.state.array[i].id, onClick:obj.delete}, "delete");
		tds[tds.length] = React.createElement("td", null, a);
		trs[trs.length] = React.createElement("tr", null, tds);
	}
	var tbody = React.createElement("tbody", null, trs);
	return React.createElement("table", {class:"table table-bordered table-striped table-hover"}, [thead,tbody]);
}

function renderForm(obj) {
	var span = React.createElement("span", {class:"input-group-addon"}, "Name");
	var text = React.createElement("input", {class:"form-control",type:"text",name:"name",onChange:obj.change,autocomplete:"off",required:1});
	var divs = [React.createElement("div", {class:"input-group input-group-lg"}, [span, text])];
	divs[divs.length] = React.createElement("br");
	span = React.createElement("span", {class:"input-group-addon"}, "Address");
	text = React.createElement("input", {class:"form-control",type:"text",name:"addr",onChange:obj.change,autocomplete:"off",required:1});
	divs[divs.length] = React.createElement("div", {class:"input-group input-group-lg"}, [span, text]);
	divs[divs.length] = React.createElement("br");
	span = React.createElement("span", {class:"input-group-addon"}, "Zipcode");
	text = React.createElement("input", {class:"form-control",type:"text",name:"zip",onChange:obj.change,autocomplete:"off",required:1});
	var span2 = React.createElement("span", {class:"input-group-btn"}, React.createElement("button",{class:"btn btn-primary",type:"submit"},"Add Customer"));
	divs[divs.length] = React.createElement("div", {class:"input-group input-group-lg"}, [span, text, span2]);
	var form = React.createElement("form", {onSubmit:obj.submit}, divs);
	return form;
}

function setProgressBars(percent) {
	var progs = document.getElementsByClassName("progress-bar");
	for(var i = 0; i < progs.length; i++) {
		if(100 == percent) {
			progs[i].style.width = '0';
		}
		else {
			progs[i].style.width = percent + '%';
		}
	}
}

class Customers extends React.Component {

	constructor(props) {
		super(props);
		this.state = {
			array: [],
			form: { name: "", addr: "", zip: ""}
		};
		this.submit = this.submit.bind(this);
		this.change = this.change.bind(this);
		this.delete = this.delete.bind(this);
	}

	componentDidMount() {
		var t = this;
		ajax2({
			verb: "GET",
			path: "/api/customers",
			onprogress: function (percent) { setProgressBars(percent); }
		}).then(function (res) {
			t.setState({array:JSON.parse(res), form: t.state.form});
		}).catch(function (err) {
			console.log(err);
		});
	}

	delete(event) {
		var t = this;
		var custid = parseInt(event.target.getAttribute("custid"));
		ajax2({
			verb: "DELETE",
			path: "/api/customers/" + custid,
			onprogress: function (percent) { setProgressBars(percent); }
		}).then(function (res) {
			var deleted = JSON.parse(res);
			for(var i = 0; i < t.state.array.length; i++) { // could use array.filter() instead
				if(t.state.array[i].id == deleted.id) {
					t.state.array.splice(i, 1);
					break;
				}
			}
			t.setState(t.state);
		}).catch(function (err) {
			console.log(err);
		});
	}

	submit(event) {
		var t = this;
		ajax2({
			verb: "POST",
			path: "/api/customers",
			data: t.state.form,
			onprogress: function (percent) { setProgressBars(percent); }
		}).then(function (res) {
			t.state.array[t.state.array.length] = JSON.parse(res);
			t.setState(t.state);
			document.forms[0].reset();
		}).catch(function (err) {
			console.log(err);
		});
		event.preventDefault();
	}

	change(event) {
		var form = event.target.form;
		this.setState({ array: this.state.array, form: {name: form.name.value, addr: form.addr.value, zip: form.zip.value} });
	}

	render() {
		return React.createElement("div", {class:"container"}, [renderTable(this), renderForm(this), React.createElement("div", {class:"progress"}, React.createElement("div",{class:"progress-bar progress-bar-striped",role:"progressbar"}))]);
	}
}

ReactDOM.render(React.createElement(Customers), document.getElementById("root"));

The above code is not compatible with IE11 because it uses the class keyword among other things (the code working with arrays is old school). It is EcmaScript 6 code and IE11 is compatible with EcmaScript 5. This code does the same thing as above; it's just harder to read, but if compatibility with IE11 is paramount then this is how it must look.

// customers.js
'use strict';

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

function renderTable(obj) {
	var ths = [];
	ths[ths.length] = React.createElement("th", { colspan: "4" }, "Customers");
	var trs = [];
	trs[trs.length] = React.createElement("tr", null, ths);
	var thead = React.createElement("thead", null, trs);
	ths = [];
	ths[ths.length] = React.createElement("th", null, "NAME");
	ths[ths.length] = React.createElement("th", null, "ADDRESS");
	ths[ths.length] = React.createElement("th", null, "ZIP");
	ths[ths.length] = React.createElement("th");
	trs = [];
	trs[trs.length] = React.createElement("tr", null, ths);
	for (var i = 0; obj.state.array && i < obj.state.array.length; i++) { // could use array.forEach() instead
		var tds = [];
		tds[tds.length] = React.createElement("td", null, obj.state.array[i].name);
		tds[tds.length] = React.createElement("td", null, obj.state.array[i].addr);
		tds[tds.length] = React.createElement("td", null, obj.state.array[i].zip);
		var a = React.createElement("a", { class: "btn btn-danger", custid: obj.state.array[i].id, onClick: obj.delete }, "delete");
		tds[tds.length] = React.createElement("td", null, a);
		trs[trs.length] = React.createElement("tr", null, tds);
	}
	var tbody = React.createElement("tbody", null, trs);
	return React.createElement("table", { class: "table table-bordered table-striped table-hover" }, [thead, tbody]);
}

function renderForm(obj) {
	var span = React.createElement("span", { class: "input-group-addon" }, "Name");
	var text = React.createElement("input", { class: "form-control", type: "text", name: "name", onChange: obj.change, autocomplete: "off", required: 1 });
	var divs = [React.createElement("div", { class: "input-group input-group-lg" }, [span, text])];
	divs[divs.length] = React.createElement("br");
	span = React.createElement("span", { class: "input-group-addon" }, "Address");
	text = React.createElement("input", { class: "form-control", type: "text", name: "addr", onChange: obj.change, autocomplete: "off", required: 1 });
	divs[divs.length] = React.createElement("div", { class: "input-group input-group-lg" }, [span, text]);
	divs[divs.length] = React.createElement("br");
	span = React.createElement("span", { class: "input-group-addon" }, "Zipcode");
	text = React.createElement("input", { class: "form-control", type: "text", name: "zip", onChange: obj.change, autocomplete: "off", required: 1 });
	var span2 = React.createElement("span", { class: "input-group-btn" }, React.createElement("button", { class: "btn btn-primary", type: "submit" }, "Add Customer"));
	divs[divs.length] = React.createElement("div", { class: "input-group input-group-lg" }, [span, text, span2]);
	var form = React.createElement("form", { onSubmit: obj.submit }, divs);
	return form;
}

function setProgressBars(percent) {
	var progs = document.getElementsByClassName("progress-bar");
	for (var i = 0; i < progs.length; i++) {
		if (100 == percent) {
			progs[i].style.width = '0';
		} else {
			progs[i].style.width = percent + '%';
		}
	}
}

var Customers = function (_React$Component) {
	_inherits(Customers, _React$Component);

	function Customers(props) {
		_classCallCheck(this, Customers);

		var _this = _possibleConstructorReturn(this, (Customers.__proto__ || Object.getPrototypeOf(Customers)).call(this, props));

		_this.state = {
			array: [],
			form: { name: "", addr: "", zip: "" }
		};
		_this.submit = _this.submit.bind(_this);
		_this.change = _this.change.bind(_this);
		_this.delete = _this.delete.bind(_this);
		return _this;
	}

	_createClass(Customers, [{
		key: "componentDidMount",
		value: function componentDidMount() {
			var t = this;
			ajax2({
				verb: "GET",
				path: "/api/customers",
				onprogress: function onprogress(percent) {
					setProgressBars(percent);
				}
			}).then(function (res) {
				t.setState({ array: JSON.parse(res), form: t.state.form });
			}).catch(function (err) {
				console.log(err);
			});
		}
	}, {
		key: "delete",
		value: function _delete(event) {
			var t = this;
			var custid = parseInt(event.target.getAttribute("custid"));
			ajax2({
				verb: "DELETE",
				path: "/api/customers/" + custid,
				onprogress: function onprogress(percent) {
					setProgressBars(percent);
				}
			}).then(function (res) {
				var deleted = JSON.parse(res);
				for (var i = 0; i < t.state.array.length; i++) { // could use array.filter() instead
					if (t.state.array[i].id == deleted.id) {
						t.state.array.splice(i, 1);
						break;
					}
				}
				t.setState(t.state);
			}).catch(function (err) {
				console.log(err);
			});
		}
	}, {
		key: "submit",
		value: function submit(event) {
			var t = this;
			ajax2({
				verb: "POST",
				path: "/api/customers",
				data: t.state.form,
				onprogress: function onprogress(percent) {
					setProgressBars(percent);
				}
			}).then(function (res) {
				t.state.array[t.state.array.length] = JSON.parse(res);
				t.setState(t.state);
				document.forms[0].reset();
			}).catch(function (err) {
				console.log(err);
			});
			event.preventDefault();
		}
	}, {
		key: "change",
		value: function change(event) {
			var form = event.target.form;
			this.setState({ array: this.state.array, form: { name: form.name.value, addr: form.addr.value, zip: form.zip.value } });
		}
	}, {
		key: "render",
		value: function render() {
			return React.createElement("div", { class: "container" }, [renderTable(this), renderForm(this), React.createElement("div", { class: "progress" }, React.createElement("div", { class: "progress-bar progress-bar-striped", role: "progressbar" }))]);
		}
	}]);

	return Customers;
}(React.Component);

ReactDOM.render(React.createElement(Customers), document.getElementById("root"));

Here is the single HTML page that loads the JavaScript and CSS files.

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<meta http-equiv="X-UA-Compatible" content="ie=edge">
		<title>CUSTOMERS REACT EXAMPLE</title>
		<link rel="stylesheet" href="bootstrap.min.css">
	</head>

	<body>
		<div class="container">
			<h1>REACT EXAMPLE</h1>
		</div>
		<div id="root">
		</div>
		<script src="ajax2.js"></script>
		<script src="react.min.js"></script>
		<script src="customers.js"></script> <!--customers.modern.js not compatible with IE11-->
	</body>
</html>

Here is the Node.js server that serves static files and implements the REST API endpoints used by the above client code. The customer data is stored in a simple array.

// server.js (Node.js server)
const fs = require("fs");
const http = require("http");
const port = process.env.PORT || 8000;
const server = http.createServer().listen(port);
console.log(`listening on port ${port}`);

var mime = {};
mime[".html"] = "text/html";
mime[".css"] = "text/css";
mime[".js"] = "application/javascript";
mime[".json"] = "application/json; charset=UTF-8";
mime[".png"] = "image/png";
mime[".jpg"] = "image/jpeg";
mime[".ico"] = "image/x-icon";
mime[".txt"] = "text/plain";

/*
customer = {
	id: 0, name: "", addr: "", zip: ""
};
*/
var customers = [];

function createCustomer(customer) {
	var i = customers.length;
	customers[i] = { id: i, name: customer.name, addr: customer.addr, zip: customer.zip };
	return customers[i];
}

function readCustomers() {
	copy = [];
	customers.forEach(e => {
		if(e)
			copy.push(e);
	});
	return copy;
}

function readCustomer(i) {
	if(i < customers.length && i >= 0 && customers[i])
		return { id: i, name: customers[i].name, addr: customers[i].addr, zip: customers[i].zip };
	return null;
}

function updateCustomer(i, customer) {
	if(i < customers.length && i >= 0 && customers[i]) {
		customers[i].name = customer.name;
		customers[i].addr = customer.addr;
		customers[i].zip = customer.zip;
		return customers[i];
	}
	return null; // return empty object to client
}

function deleteCustomer(i) {
	if(i < customers.length && i >= 0 && customers[i]) {
		var customer = customers[i];
		customers[i] = null;
		return customer;
	}
	return null;
}

server.on("request", function (request, response) {
	console.log(request.method + " " + request.url);
	if(request.method == "GET") {
		var path = request.url.substring(1, request.url.length).toLowerCase();
		if(path.length == 0)
			path = "index.html";
		if(path.substring(0, 4) == "api/") // GET CUSTOMER(S)
		{
			path = path.split("/");
			if(path.length >= 3 && path[1] == "customers" && !isNaN(path[2])) {
				var i = path[2];
				if(i < customers.length && i >= 0 && customers[i]) {
					response.writeHead(200, {"Content-Type":mime[".json"]});
					response.end(JSON.stringify(readCustomer(i)));
					return;
				}
			}
			else if(path.length >= 2 && path[1] == "customers") {
				response.writeHead(200, {"Content-Type":mime[".json"]});
				response.end(JSON.stringify(readCustomers()));
				return;
			}
			response.writeHead(404, {"Content-Type":mime[".txt"]});
			response.end(`404 - path ${request.url} not found`);				
		}
		else // RETURN A STATIC HTML FILE
		{
			fs.readFile(path, function (err, data) {
				if(!err) {
					var ext = path.substring(path.lastIndexOf('.'), path.length);
					response.writeHead(200, {"Content-Type":(mime[ext]||mime[".txt"])});
					response.end(data.toString("utf8"));
				}
				else {
					response.writeHead(404, {"Content-Type":mime[".txt"]});
					response.end(`404 - path ${request.url} not found`);				
				}
			});
		}
	}
	else if(request.method == "POST") { // CREATE CUSTOMER
		var body = "";
		request.on("data", function (data) {
			body += data.toString();
		});
		request.on("end", function () {
			console.log(body);
			try {
				const path = request.url.toLowerCase().split("/");
				if(path.length >= 3 && path[1] == "api" && path[2] == "customers") { // check for the end point
					var cust = JSON.parse(body);
					if(cust.name && cust.addr && cust.zip) {
						response.writeHead(200, {"Content-Type":mime[".json"]});
						response.end(JSON.stringify(createCustomer(cust)));
						return;
					}
				}
			}
			catch {}
			response.writeHead(400, {"Content-Type":mime[".txt"]}); // BAD REQUEST
			response.end("400 - CLIENT ERROR: BAD REQUEST");
		});
	}
	else if(request.method == "PUT") { // UPDATE CUSTOMER
		const path = request.url.toLowerCase().split("/");
		var body = "";
		request.on("data", function (data) {
			body += data.toString();
		});
		request.on("end", function () {
			console.log(body);
			try {
				if(path.length >= 4 && path[1] == "api" && path[2] == "customers" && !isNaN(path[3]) && path[3] < customers.length && path[3] >= 0 && customers[path[3]]) {
					const cust = JSON.parse(body);
					if(cust.name && cust.addr && cust.zip) {
						response.writeHead(200, {"Content-Type":mime[".json"]});
						response.end(JSON.stringify(updateCustomer(path[3], cust)));
						return;
					}
					else {
						response.writeHead(400, {"Content-Type":mime[".txt"]}); // BAD REQUEST
						response.end("400 - CLIENT ERROR: BAD REQUEST");
					}
				}
				else {
					response.writeHead(404, {"Content-Type":mime[".txt"]});
					response.end(`404 - path ${request.url} not found`);				
				}
			}
			catch { // catch when the client send non-JSON data
				response.writeHead(400, {"Content-Type":mime[".txt"]}); // BAD REQUEST
				response.end("400 - CLIENT ERROR: BAD REQUEST");
			}
		});
	}
	else if(request.method == "DELETE") { // DELETE CUSTOMER
		const path = request.url.toLowerCase().split("/");
		if(path.length >= 4 && path[1] == "api" && path[2] == "customers" && !isNaN(path[3]) && path[3] < customers.length && path[3] >= 0 && customers[path[3]]) {
			response.writeHead(200, {"Content-Type":mime[".json"]});
			response.end(JSON.stringify(deleteCustomer(path[3])));
			return;
		}
		response.writeHead(404, {"Content-Type":mime[".txt"]});
		response.end(`404 - path ${request.url} not found`);				
	}
	else {
		response.writeHead(405, {"Content-Type":mime[".txt"]});
		response.end(`method ${request.method} not allowed`);
	}
});

Minified Bootstrap and React Files

These files are required for the above code to succeed and can be found in REACTAJAX2.zip.

Coding Video

https://youtu.be/_Xfg5Wb5Ssg