XHR, Part 2
CSV and Other Data Delimiters
- A delimiter is a character or characters that define where one piece of data ends and the next one begins.
- CSV, or Comma Separated Values, uses a comma as the delimiter.
- Delimiters need to be unique, so as not to trigger false detections.
- In the case of CSV, I would not want to have a comma in a data value.
- Typically programs like Excel that export to CSV format put double quotes around commas occurring as part of data, so they can be skipped.
- I usually go with a more complicated and unique delimiter. A delimiter such as
-|-is more unique than a comma, and much less likely to occur naturally in data. - The limitations of these setups is that they depend on proper sequencing of data. Data out of sequence wrecks the output. This is part of why XML and JSON are used.
- With XML the tag sequence can vary, so sequencing within a block of tags is not important.
- With JSON the sequencing of properties also does not make a difference in retrieving the correct information.
- To break the values apart we will use the
split()method of the String object, which returns an array of the values (the delimiter is omitted). The delimiter goes between the()and is a String object.split()is not paying attention to double quotes around the delimiter, so when using this with CSV data be careful to not having any commas in the data values. - Converting the XML data from the live search example to CSV results in:
AJAX Basics,http://www.ajaxbasics.com, AJAX 123,http://www.ajax123.com, Intro to AJAX,http://www.introajax.com, About AJAX,http://www.aboutajax.com, Learning AJAX,http://www.learningajax.com, AJAX Intro,http://www.ajaxintro.com, Discovering AJAX,http://www.discoveringajax.com, AJAX Foundations,http://www.ajaxfoundations.com
and the relevant JavaScript changes:
init : function() { // disable the submit button; will appear if JS is disabled document.querySelector('#submitButton').style.display = 'none'; // send the request for the CSV data // data will be sent to the holdSearchResults() method // attach a random number to the end to break file caching this.sendRequest('searchData_csv.txt?' + Math.random(), ls.holdSearchResults); // on every key stroke call the setTimer function this.searchTextBox.addEventListener('keyup', this.setTimer, false); }, // store the CSV data in the searchData property of this object holdSearchResults : function(xhr) { const csv = xhr.responseText; ls.searchData = csv.split(','); }, displayResults : function() { // shorthands const data = ls.searchData; const results = ls.resultsArea; const box = ls.searchTextBox; results.innerHTML = ''; // if the user has deleted all the text in the query, // do not go any further // the keypresses involved in deleting the query // will trigger the function if (!box.value) { return; } // total number of possible results for subsequent loops const totalResults = ls.searchData.length; // lowercase query to eliminate mismatches due to case const queryValue = box.value.toLowerCase(); // wipe out the waiting message // flip the waitMsg flag back to false ls.waitingHolder.removeChild(ls.waitingHolder.firstChild); ls.theForm.removeChild(ls.waitingHolder); ls.waitMsg = false; // create and append the Search Results header const searchHeader = document.createElement('h1'); const searchHdrTxt = document.createTextNode('Search Results'); results.appendChild(searchHeader).appendChild(searchHdrTxt); // no results found yet ls.resultsFound = false; // since the array is arranged in sets of two values, // jump ahead 2 with each loop for (let i=0; i<totalResults; i+=2) { // pull out each article title and lowercase it const article = data[i].toLowerCase(); // if the query matches the article title // then continue and construct the result // for that item if (article.match(queryValue)) { ls.resultsFound = true; const newLink = document.createElement('a'); newLink.href = data[i+1]; const linkText = document.createTextNode(data[i]); results.appendChild(newLink).appendChild(linkText); const lineBreak = document.createElement('br'); results.appendChild(lineBreak); } } // if results flag is never flipped from false to true, // then no results were found // display the 'No results were found' message if (!ls.resultsFound) { results.innerHTML = 'No results were found.'; } },
Notifying Users of Updates
- There are a variety of ways to notify users that new data has loaded, ranging from the very subtle (a small message that fades away, a visual highlight) to the very blunt (shifting their focus, showing an
alert()message). - The following example triggers a background color change when the data has loaded. The color change is also shown whenever new data is pulled and the number of headlines has changed. This is better than always changing the color, because sometimes the data will not have changed.
- However, the change in headline quantity is not a perfect approach, because you might have removed and then added the same number of headlines between the data refreshes and the visual fade would not be triggered.
- A safer approach is to check when the file was last modified by examining the 'Last-Modified' HTTP header to see if that date has changed.
Fade Technique Example
Structure (fade_news.html)
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<title>XHR News Example using Fades</title>
<link rel="stylesheet" href="fade_news.css" />
</head>
<body>
<h1>Website Name</h1>
<div id="content">
Sample text sample text sample text
<p>Sample text sample text sample text</p>
<p>Sample text sample text sample text</p>
<p>Sample text sample text sample text</p>
<p>Sample text sample text sample text</p>
<p>Sample text sample text sample text</p>
<p>Sample text sample text sample text</p>
<p>Sample text sample text sample text</p>
<p>Sample text sample text sample text</p>
<p>Sample text sample text sample text</p>
</div>
<nav>
<ul>
<li><a href="#">Upcoming Events</a></li>
<li><a href="#">Our Products</a></li>
<li><a href="#">Our Services</a></li>
<li><a href="#">About Us</a></li>
<li><a href="#">Our Blog</a></li>
<li><a href="#">Contact Us</a></li>
</ul>
</nav>
<aside id="newsholder" aria-live="polite"></aside>
<script src="fade_news.js"></script>
</body>
</html>
Presentation (fade_news.css)
body, h1, h2 {font: 75%/1.3 verdana, sans-serif; margin: 0; padding: 0;}
h1 {font-size: 160%; padding: 5px 10px; border-bottom: 1px solid #000;}
#content {margin: 20px 200px;}
nav {position: absolute; top: 55px; left: 10px; width: 175px;}
nav ul {list-style: none; margin: 0; padding: 0;}
nav li {margin-bottom: 1em;}
nav a {display: block; border: 3px double #ccc; padding: 5px; width: 145px; text-decoration: none; color: #000;}
nav a:hover {background: #eee; color: #000; border: 3px double #000;}
#newsholder {padding: 5px; width: 170px; border: 3px double #eee; position: absolute; right: 5px; top: 55px; overflow: auto; font-size: 90%;}
#newsholder h2 {font-size: 120%; padding-bottom: 3px;}
/* background color fades for the news area */
#darker0 #newsholder {background: #ccc;}
#darker1 #newsholder {background: #999;}
#darker2 #newsholder, #darker2 #newsholder a {background: #666; color: #fff;}
#darker3 #newsholder, #darker3 #newsholder a {background: #333; color: #fff;}
#darker4 #newsholder, #darker4 #newsholder a {background: #000; color: #fff;}
Behavior (fade_news.js)
"use strict";
const news = {
// area where the data will be displayed
newsArea : document.querySelector('#newsholder'),
// references the body tag; we modify its id attribute
theBody : document.querySelector('body'),
// the JSON data
newsData : null,
// number of news headings retrieved this time
totalNewsItems : 0,
// number of news headings retrieved last time
oldNewsItems : 0,
// counter tracking the fade progress
// also controls display of background color for fade
fadeState : 0,
// interval timer for fade
fadeTimer : null,
// direction of fade
fadeDirection : 'in',
init : function() {
// send the request for the JSON data
news.sendRequest('news_data.json?' + Math.random(), this.holdNewsResults);
// set the interval for data refresh
news.newsRefresh = setInterval(news.pullNewData, 30000);
},
// pull the news data again
pullNewData : function() {
news.sendRequest('news_data.json?' + Math.random(), news.holdNewsResults);
},
// store the JSON data in the newsData property of this object
holdNewsResults : function(xhr) {
news.newsData = JSON.parse(xhr.responseText);
news.populateNews();
},
// output the news headings into the page
populateNews : function() {
this.newsArea.innerHTML = '';
const newsHeader = document.createElement('h2');
const headerTxt = document.createTextNode('News Headlines');
this.newsArea.appendChild(newsHeader).appendChild(headerTxt);
// total number of news items retrieved this time
this.totalNewsItems = this.newsData.newsItems.length;
for (let i=0; i<this.totalNewsItems; i++) {
const newPara = document.createElement('p');
const newLink = document.createElement('a');
const lineBreak = document.createElement('br');
const newsTitle = document.createTextNode(this.newsData.newsItems[i].heading);
newLink.href = this.newsData.newsItems[i].url;
newPara.appendChild(newLink).appendChild(newsTitle);
this.newsArea.appendChild(newPara);
}
// different number of headings this time around?
// if there is a difference,
// show the fade effect because the data has changed
if (this.oldNewsItems !== this.totalNewsItems) {
this.fadeTimer = setInterval(news.fade,100);
}
// old news items total
this.oldNewsItems = this.totalNewsItems;
},
// cycle through background colors for the news area
// by assigning different id values to the body
fade : function() {
if (news.fadeState >= 0 &&
news.fadeState < 5 &&
news.fadeDirection === 'in') {
news.theBody.id = 'darker' + news.fadeState;
news.fadeState += 1;
}
else if (
news.fadeState >= 0 &&
news.fadeState < 5 &&
news.fadeDirection === 'out'
) {
news.theBody.id = 'darker' + news.fadeState;
news.fadeState -= 1;
}
else if (news.fadeState === 5) {
news.fadeDirection = 'out';
news.fadeState = 4;
news.theBody.id = 'darker' + news.fadeState;
}
else {
news.theBody.id = '';
news.fadeDirection = 'in';
news.fadeState = 0;
clearInterval(news.fadeTimer);
}
},
sendRequest : function(url, func, postData) {
const xhr = new XMLHttpRequest();
if (!xhr) { return; }
const method = (postData) ? "POST" : "GET";
xhr.open(method, url, true);
if (postData) {
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
}
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) { return; }
if (xhr.status !== 200 && xhr.status !== 304) {
alert('HTTP error ' + xhr.status); return;
}
func(xhr);
}
if (xhr.readyState === 4) { return; }
xhr.send(postData);
}
}
news.init();
Data File (news_data.json)
{
"newsItems" : [
{ "heading" : "News Item 1",
"url" : "http://www.news.com/1/",
"summary" : "Sample text sample text sample text sample text" },
{ "heading" : "News Item 2",
"url" : "http://www.news.com/2/",
"summary" : "Sample text sample text sample text sample text" },
{ "heading" : "News Item 3",
"url" : "http://www.news.com/3/",
"summary" : "Sample text sample text sample text sample text" },
{ "heading" : "News Item 4",
"url" : "http://www.news.com/4/",
"summary" : "Sample text sample text sample text sample text" },
{ "heading" : "News Item 5",
"url" : "http://www.news.com/5/",
"summary" : "Sample text sample text sample text sample text" },
{ "heading" : "News Item 6",
"url" : "http://www.news.com/6/",
"summary" : "Sample text sample text sample text sample text" },
{ "heading" : "News Item 7",
"url" : "http://www.news.com/7/",
"summary" : "Sample text sample text sample text sample text" },
{ "heading" : "News Item 8",
"url" : "http://www.news.com/8/",
"summary" : "Sample text sample text sample text sample text" }
]
}
Passing Data to a Script via XHR
- In this example we will be passing data to a PHP script via XHR, then receiving a confirmation or an error message back based on what the PHP script outputs.
- The goal is to be checking user data against what is on the server as the user completes a form.
Passing Data Example 1
Structure (registration.html)
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<title>Closed Beta Registration</title>
<link rel="stylesheet" href="registration.css" />
</head>
<body>
<h1>Closed Beta Registration</h1>
<p>Usernames and passwords need to be at least 6 characters in length.</p>
<form method="post" action="">
<ul>
<li><label for="passkey">Enter Your Passkey:</label>
<input type="text" name="passkey" id="passkey" size="10" /></li>
<li><label for="username">Choose a Username:</label>
<input type="text" name="username" id="username" size="10" /></li>
<li><label for="password">Choose a Password:</label>
<input type="password" name="password" id="password" size="10" /></li>
<li><input type="submit" value="Sign Up" id="regbutton" /></li>
</ul>
</form>
<script src="registration.js"></script>
</body>
</html>
Presentation (registration.css)
body, h1 {font: 75%/1.5 verdana, sans-serif;}
h1 {font-size: 150%;}
h1, form {display: inline;}
form ul {list-style: none; margin: 0; padding: 0;}
form li {margin: 10px 0;}
input {border: 1px solid #000; padding: 3px;}
input[type="submit"] {cursor: pointer; padding: 5px;}
input[type="submit"][disabled] {cursor: not-allowed;}
label {text-align: right; padding: 3px; display: block; float: left; width: 150px; margin-right: 3px; font-weight: bold;}
#regbutton {margin-left: 159px;}
Behavior (registration.js)
"use strict";
const reg = {
// reference to the form tag
theForm : document.querySelector('form'),
// form elements
passKey : document.getElementById('passkey'),
userName : document.getElementById('username'),
passWord : document.getElementById('password'),
regButton : document.getElementById('regbutton'),
// XHR and form data
passkeyData : null,
usernmData : null,
passwordData : null,
// flags that indicate whether the submit button
// should be enabled or disabled
passkeyFlag : false,
usernmFlag : false,
init : function() {
// disable the submit button when the page loads
this.regButton.disabled = true;
// add listeners to the first two form fields and the form itself
this.passKey.addEventListener('blur', reg.checkPasskey, false);
this.userName.addEventListener('blur', reg.checkUsername, false);
this.theForm.addEventListener('submit', reg.checkPassword, false);
},
// this method is triggered when the user moves focus
// away from the passkey field
checkPasskey : function() {
if (this.value.length >= 6) {
reg.sendRequest('checkData.php?betaKey=' + this.value, reg.passKeyResults);
}
},
// this method displays the passkey error and
// confirmation messages
passKeyResults : function(xhr) {
reg.passkeyData = xhr.responseText;
// display the error or confirmation message
// remove the old message first
if (reg.passKey.parentNode.lastChild.nodeType === 3) {
reg.passKey.parentNode.removeChild(reg.passKey.parentNode.lastChild);
}
reg.passKey.parentNode.appendChild(document.createTextNode(reg.passkeyData));
// flip the flag that will help determine
// the submit button disabled/enabled status
if (reg.passkeyData.match(/Valid passkey/)) {
reg.passkeyFlag = true;
}
else {
reg.passkeyFlag = false;
}
// enable/disable the submit button
reg.enableSubmit();
},
// this method is triggered when the user
// moves focus away from the username field
checkUsername : function() {
if (this.value.length >= 6) {
reg.sendRequest('checkData.php?potentialName=' + this.value, reg.userNameResults);
}
},
userNameResults : function(xhr) {
reg.usernmData = xhr.responseText;
// display the error or confirmation message
// remove the old message first
if (reg.userName.parentNode.lastChild.nodeType === 3) {
reg.userName.parentNode.removeChild(reg.userName.parentNode.lastChild);
}
reg.userName.parentNode.appendChild(document.createTextNode(reg.usernmData));
// flip the flag that will help determine
// the submit button disabled/enabled status
if (reg.usernmData.match(/available/)) {
reg.usernmFlag = true;
}
else {
reg.usernmFlag = false;
}
// enable/disable the submit button
reg.enableSubmit();
},
// display an error message if password
// is too short and stop form submission
checkPassword: function(evt) {
if (reg.passWord.value.length < 6) {
if (reg.passWord.parentNode.lastChild.nodeType === 3) {
reg.passWord.parentNode.removeChild(reg.passWord.parentNode.lastChild);
}
reg.passWord.parentNode.appendChild(document.createTextNode(' Invalid password'));
evt.preventDefault();
}
},
// determine whether the submit button is enabled or disabled
// enabling the submit button requires both flags to be flipped to true
enableSubmit : function() {
if (reg.passkeyFlag && reg.usernmFlag) {
reg.regButton.disabled = false;
}
else {
reg.regButton.disabled = true;
}
},
sendRequest : function(url, func, postData) {
const xhr = new XMLHttpRequest();
if (!xhr) { return; }
const method = (postData) ? "POST" : "GET";
xhr.open(method, url, true);
if (postData) {
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
}
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) { return; }
if (xhr.status !== 200 && xhr.status !== 304) {
alert('HTTP error ' + xhr.status); return;
}
func(xhr);
}
if (xhr.readyState === 4) { return; }
xhr.send(postData);
}
}
reg.init();
Server-Side Script (checkData.php)
<?php
// We will be sending back regular text,
// so setting this HTTP header is essential
header("Content-type: text/plain");
// if there is a variable named betaKey, execute this code
// we pass this same script different variables at different times
// so the check is important
if (isset($_GET['betaKey'])) {
// store everything in the keys.txt data file in the allKeys variable
$allKeys = file_get_contents('keys.txt');
// if there is a match between the betaKey and the approved keys, send back
// the approval message; otherwise send back the error message
if (strstr($allKeys, $_GET['betaKey'] . ',')) {
echo ' Valid passkey entered';
}
else {
echo ' Invalid passkey';
}
}
// if there is a variable named potentialName passed, execute this code
// we pass this same script different variables at different times
// so the check is important
if (isset($_GET['potentialName'])) {
// store everything in the names.txt data file in the allNames variable
$allNames = file_get_contents('names.txt');
// if there is not a match between the potentialName
// and the existing names, report back that is is available
// otherwise send back the error message
if (strstr($allNames, $_GET['potentialName'] . ',')) {
echo ' Username taken or invalid; please try another';
}
else {
echo ' This username is available';
}
}
?>
Data File for Valid Keys (keys.txt)
AP90HI,W4MI55,UHJ731,P14BN7,YU6D3S,
The key entered must be one of the ones shown above to match.
Data File for Usernames Already Taken (names.txt)
jwithrow,testname,imatester,johndoe,janedoe,
The username cannot be one of the ones shown above - they are already taken.
See how this first passing data example renders
Saving Data on the Server via XHR
- Up to this point we have been using the
GETmethod to request data from the server. In that situation we passnullwhen we call thesend()method of the XMLHttpRequest object, because there is no message body to the request (the data we send is part of the URL requested). - In this example we will be passing data via the
POSTmethod, with the data sent as a parameter insidesend(). That data is the message body. - The specific situation is that a user is filling out a job application and we are auto-saving the data periodically to a text file on the server. The file name is
username.txt, with theusernamecoming from a hidden field that we have specified. If the user was disrupted during the job application process, we would have their data stored and could offer to have them continue from the point where they stopped.- Since this screen would be part of a larger application, we can assume that on a separate screen the user created an account and established their
username. - If this was something used in production, the username would be stored in a session and would not be part of the form data.
- Since this screen would be part of a larger application, we can assume that on a separate screen the user created an account and established their
- PHP is again used for server-side processing. The PHP code stores the data passed in a text file.
- Every 45 seconds the data is sent to the server, as long as there is some data entered. If no data is entered (we ignore the hidden field for
username) then nothing occurs when the 45 second interval elapses. - Assuming data was entered, a message about 'Auto-saving...' is displayed for 5 seconds in the top right of the window.
Passing Data Example 2
Structure (application.html)
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<title>Job Application Form</title>
<link rel="stylesheet" href="application.css" />
</head>
<body>
<h1>Job Application</h1>
<form method="post" action="">
<fieldset>
<legend>Position</legend>
<input type="hidden" name="username" value="jwithrow" />
<ul>
<li><label for="position">Position:</label>
<input type="text" name="position" id="position" size="20" /></li>
<li><label for="referral">Referred By:</label>
<input type="text" name="referral" id="referral" size="20" /></li>
</ul>
</fieldset>
<fieldset>
<legend>Reference 1</legend>
<ul>
<li><label for="ref1">Name:</label>
<input type="text" name="ref1" id="ref1" size="20" /></li>
<li><label for="email1">Email:</label>
<input type="text" name="email1" id="email1" size="20" /></li>
<li><label for="phone1">Phone:</label>
<input type="text" name="phone1" id="phone1" size="20" /></li>
</ul>
</fieldset>
<fieldset>
<legend>Reference 2</legend>
<ul>
<li><label for="ref2">Name:</label>
<input type="text" name="ref2" id="ref2" size="20" /></li>
<li><label for="email2">Email:</label>
<input type="text" name="email2" id="email2" size="20" /></li>
<li><label for="phone2">Phone:</label>
<input type="text" name="phone2" id="phone2" size="20" /></li>
</ul>
</fieldset>
<fieldset>
<legend>Reference 3</legend>
<ul>
<li><label for="ref3">Name:</label>
<input type="text" name="ref3" id="ref3" size="20" /></li>
<li><label for="email3">Email:</label>
<input type="text" name="email3" id="email3" size="20" /></li>
<li><label for="phone3">Phone:</label>
<input type="text" name="phone3" id="phone3" size="20" /></li>
</ul>
</fieldset>
<p id="submit"><input type="submit" value="Save Application" /></p>
</form>
<script src="application.js"></script>
</body>
</html>
Presentation (application.css)
body, h1 {font: 75%/1.5 verdana, sans-serif;}
h1 {font-size: 150%;}
h1, form {display: inline;}
fieldset {width: 350px; margin: 15px 0; display: block;}
legend {border: 2px solid #ccc; padding: 5px; font-weight: bold;}
form ul {list-style: none; margin: 0; padding: 0;}
form li {margin: 10px 0;}
input {border: 1px solid #000; padding: 3px;}
label {text-align: right; padding: 3px; display: block;
float: left; width: 100px; margin-right: 3px;}
#submit {width: 350px; text-align: center;}
#savingmessage {position: fixed; top: 10px; right: 10px; width: 130px;
font-weight: bold; border: 1px solid #000; padding: 5px;}
Behavior (application.js)
"use strict";
const jobApp = {
// reference to body element node
theBody : document.querySelector('body'),
// nodeList for all input elements
allInputs : document.getElementsByTagName('input'),
// count of all form elements
totalInputs : 0,
init : function() {
this.totalInputs = this.allInputs.length;
this.startTimer();
},
// every 45 seconds call the autoSave() function
startTimer : function() {
jobApp.interval = setInterval(jobApp.autoSave, 45000);
},
autoSave : function() {
// this property will hold the data sent to the server
// each time we call autoSave we clear its value
jobApp.paramData = '';
// flag that data was entered
jobApp.dataEntered = false;
// only consider text input fields
// ignore the hidden and submit inputs
// assume that no data was entered,
// unless we find something was specified
// if data is found, add it to paramData
for (let i=0; i<jobApp.totalInputs; i++) {
if (jobApp.allInputs[i].value !== '' &&
jobApp.allInputs[i].type === 'text') {
jobApp.dataEntered = true;
const thisParam = jobApp.allInputs[i].name + '=' + jobApp.allInputs[i].value + '&';
jobApp.paramData = jobApp.paramData.concat(thisParam);
}
}
// if data was entered, add the username
// to the paramData string
// trigger the createLoadingMsg() function and
// set the timer for deleteLoadingMsg()
// send the data to the PHP script via POST
if (jobApp.dataEntered) {
jobApp.paramData = jobApp.paramData.concat("username=" + jobApp.allInputs[0].value);
jobApp.createLoadingMsg();
const msgTimer = setTimeout(jobApp.deleteLoadingMsg,5000);
jobApp.sendRequest('autosave.php', jobApp.getResponse, jobApp.paramData);
}
},
// this will be an empty response if all is well
// otherwise we will get an error message
// back from the server
// use browser's Developer Tools to monitor this
getResponse : function(xhr) {
const result = xhr.responseText;
},
// create and insert the auto-saving message
createLoadingMsg : function() {
const msgHolder = document.createElement('div');
msgHolder.setAttribute('id','savingmessage');
msgHolder.setAttribute('role','status');
msgHolder.setAttribute('aria-live','polite');
msgHolder.appendChild(document.createTextNode('Auto-saving...'));
jobApp.theBody.appendChild(msgHolder);
},
// wipe the auto-saving message
deleteLoadingMsg : function() {
const theMessage = document.getElementById('savingmessage');
jobApp.theBody.removeChild(theMessage);
},
sendRequest : function(url, func, postData) {
const xhr = new XMLHttpRequest();
if (!xhr) { return; }
const method = (postData) ? "POST" : "GET";
xhr.open(method, url, true);
if (postData) {
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
}
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) { return; }
if (xhr.status !== 200 && xhr.status !== 304) {
alert('HTTP error ' + xhr.status); return;
}
func(xhr);
}
if (xhr.readyState === 4) { return; }
xhr.send(postData);
}
}
jobApp.init();
XHR2
- XHR has evolved over time and changed to a living standard approach, with new features added over time.
- One new feature is the
progressevent, useful for cases such as multi-file uploads. MDN has further details regarding the progress event.
FormData
- Another very useful addition is
FormData, which allows you to send all the data within a form to a server-side script. - The first thing you do is assign a
submitlistener to the form element viaaddEventListener(). - The function called via
addEventListeneris set up as:sampleCallerFunction : function() { const formData = new FormData(this); const xhr = new XMLHttpRequest(); xhr.open('POST', '/xhr/form-processor.php', true); xhr.onload = someGlobalObject.getTheResponseFunction; xhr.send(formData); } getTheResponseFunction : function() { // returned XHR object referenced by: this const theResponse = this.responseText; } - The above code would send all the form data to the specified destination as POST data. If you need to send file uploads as part of that data, you'll need to make some additional adjustments (such as adding a header so that the multi-part upload file data gets handled properly).
Implementation Notes for FormData
- The
onloadDOM0 event assignment determines what function/method gets the results. - The function/method receiving the data is the XHR object, so you access its properties using the
thiskeyword. - In earlier XHR examples (using XHR1), the XHR object was passed to the function/method, but in this XHR2 setup there is no passing of the XHR object.