Jump to main content
Menu

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.';
      }
    
    },
    

    See how this CSV example renders

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" }

  ]
}

See how this fade technique example renders

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 GET method to request data from the server. In that situation we pass null when we call the send() 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 POST method, with the data sent as a parameter inside send(). 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 the username coming 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.
  • 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();

See how this second passing data example renders

XHR2

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 submit listener to the form element via addEventListener().
  • The function called via addEventListener is 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 onload DOM0 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 this keyword.
  • 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.