AJAX and Web Sockets in Web File Uploads

September 02, 2015 in Web Development Articles

Written by Eric Greene


In the previous post, we examined the history and the technical details of HTTP-based file uploads using web browsers. In this post, we explore two modern file upload methods: AJAX and Web Sockets.

Asynchronous JavaScript and XML (AJAX) is powered by a technology built into web browsers named the XMLHttpRequest (XHR) object. The XHR object was a Microsoft invention used to support their Outlook Web Access product in 2000. This object was first available as an ActiveX object in Internet Explorer 5.

As the usefulness of XHR became apparent, it was adopted in other web browsers such as Firefox and Safari. However, those browsers did not support ActiveX (a proprietary Microsoft COM-based technology). Therefore, to use the XHR object, developers had to write specific code for each browser in order to perform AJAX calls.

Before the wide-spread adoption of the XHR object, AJAX-like calls were accomplished using hacks such as hidden iframes. With the advent of libraries, such as jQuery and Prototype, common interfaces were created for developers to perform AJAX calls. The libraries then handled the details for each specific browser implementation.

Old School Async File Uploads: Hidden iframes

The AJAX capabilities provided the XHR object fueled the Web 2.0 revolution and greatly increased the usefulness of JavaScript-enabled web sites. Nevertheless, as useful as the XHR object was, it lacked one major feature: it could not handle file uploads. To perform asynchronous file uploads, the old school hidden iframe hack with programmatic form submission was needed. The following code sample was adapted from Viral Patel’s blog post titled “Ajax Style File Uploading using Hidden iframe“.

For Hidden iframes (and the XHR uploads in the next section), a standard HTML form is used to display an HTML file input field. This field will be used to specify which file is to be uploaded. In modern browsers, it’s possible to specify multiple files through the file selection dialog box. To select multiple files, add the multiple attribute to the file input element.

<form>
  <div>
    <label for="my-file">Select File:</label>
    <input id="my-file" name="my-file" type="file" multiple>
  </div>
  <button type="button" id="upload-button">Upload</button>
</form>

Once the file(s) are selected and the upload button is clicked, the iframe needs to be created and the form needs to be submitted.

function createUploadFrame(iframeName) {
  // create iframe element with specified name, and return it
  var iframe = document.createElement("iframe");
  iframe.setAttribute("id", iframeName);
  iframe.setAttribute("name", iframeName);
  iframe.setAttribute("width", 0);
  iframe.setAttribute("height", 0);
  iframe.setAttribute("border", 0);
  iframe.setAttribute("style", "border:0px");
  return iframe;
}

Using the createUploadFrame function, a new hidden iframe is created. The form will be submitted to this iframe by setting the target attribute on the upload form. To handle the response from the server, an event handler will be registered with the load event of the iframe. This load event will run once the response from the server has been received.

function doFileUpload(uploadFormElement, formActionUrl, uploadCallbackFn) {
  var
    uploadFrameName = "iframe-upload",
    uploadFrame = createUploadFrame(uploadFrameName);
  
  // add the created hidden iframe to the DOM
  uploadFormElement.parentNode.appendChild(uploadFrame);

  function uploadResult() {
    // call the supplied callback function with the result of the
    // upload request
    uploadCallbackFn(uploadFrame.contentDocument.body.innerHTML);

    // clean up the load event listener so it does not run again
    uploadFrame.removeEventListener("load", uploadResult);
    uploadFrame.parentNode.removeChild(uploadFrame);
  };

  // when the page reloads from the form submission
  // run the passed in callback function
  uploadFrame.addEventListener("load", uploadResult);

  // submit the visible form to the hidden iframe by
  // submitting the hidden iframe, the main page is not
  // reloaded, and the submission is asynchronous
  uploadFormElement.setAttribute("target", uploadFrameName);
  uploadFormElement.setAttribute("action", formActionUrl);
  uploadFormElement.setAttribute("enctype", "multipart/form-data");
  uploadFormElement.setAttribute("method", "POST");
  uploadFormElement.submit();
}

A click of the upload button triggers the upload process. The code below demonstrates the implementation of the click event.

window.addEventListener("DOMContentLoaded", function() {
  var buttonElement = document.getElementById("upload-button");

  buttonElement.addEventListener("click", function() {

  // performs the file upload
  // 1. Create a hidden iframe
  // 2. Post the visible form to the hidden iframe
  // 3. Call the callback with the response text from the upload request
  doFileUpload(document.getElementsByTagName("form")[0], "/upload",
    function(result) {
      console.log(result);
    });
  });
});

New School Async File Uploads: XHR Level 2

In 2008, work began on the XHR Level 2 specification. Enhancements to the XHR object included progress events, cross-site requests and byte streams. In December 2011, XHR Level 1 and Level 2 were combined into a single specification. The handling of byte streams and progress events greatly enhance the process of performing asynchronous file uploads.

To perform file uploads with the XHR object, the FormData API is used. The FormData API allows the creation of forms programmatically in JavaScript. These forms are not forms displayed on the screen, instead they are in-memory forms which can be populated with many kinds of form data including files to be uploaded.

Because JavaScript in a web browser is highly sandboxed for security reasons, JavaScript cannot directly access the file system. In order for JavaScript to know which file to upload, it must access that file indirectly. In this case, the file to be uploaded will be accessed through the user selection of the file using an on-screen form. Once selected, JavaScript will access the on-screen form input file control to retrieve the file(s) selected by the user. Those files will then be added to the in-memory form data object and passed to the XHR object through the send function.

// Create a new FormData object to hold the file datavar
fd = new FormData();

// Add the data from the file input field to the FormData object
fd.append("my-file", document.getElementById("my-file").files[0]);

// Initialize a new XHR object
var xhr = new XMLHttpRequest();

// handle ready state change events
xhr.onreadystatechange = function() {

  if (xhr.readyState === 4 && xhr.status === 200) {
    // upload is complete, output result data
    console.dir(JSON.parse(xhr.responseText));
  }
};

// configure the request
xhr.open("POST", "/upload");

// makes the request and uploads the form data
xhr.send(fd);

Once the file upload is complete, the response from the server is handled the usual way.

Drag and Drop: DropBox Style

Dragging and Dropping has been a mainstay of desktop applications ever since the first graphical user interface environments 30+ years ago. With the advent of the web and vendor-specific browser capabilities, drag and drop capabilities within browsers have been possible. However, since there were no official standards, drag and drop functionality was rarely implemented in web applications. HTML5 standardized the Drag & Drop API for modern browsers. Through the API, users can drag and drop elements within the window, as well as from outside the browser window. When files from outside the browser are dropped in that window, JavaScript code can then be used to upload those files.

To get started with Drag & Drop, the drop zone needs to be specified. The id value does NOT have to be “drop-zone”, the developer merely has to identify where users are going to be allowed to drop files for uploading.

<form>
  <div id="drop-zone"></div>
</form>

Next, a few styles should be specified to help indicate to the user where they can drop the file, as well as when they have activated the drop zone by dragging a file(s) over it.

<style>
  #drop-zone {
    width:200px;
    height:200px;
    border: 2px dashed gray;
  }

  #drop-zone.active {
    border: 2px dashed red;
  }
</style>

For drag and drop file upload to work, several events need to be set up. First, the event dragenter is used “activate” the drop zone with a style to indicate to the user that they have dragged a file over the drop zone. Secondly, the dragleave event is needed to “deactivate” the drop zone should the user leave the drop zone without dropping the file. Thirdly, the dragover event is used to prevent the default browser behavior when a file is dragged over the area of the web page.

var dropZone = document.getElementById("drop-zone");

dropZone.addEventListener("dragenter", function(e) {
  this.classList.add("active");
});

dropZone.addEventListener("dragleave", function(e) {
  this.classList.remove("active");
});

dropZone.addEventListener("dragover", function(e) {
e.preventDefault();
});

Finally, the drop event is needed. The drop event will handle the actual dropping of the file, process the upload, and deactivate the drop zone. To perform the file upload, the drop event will use the same FormData object demonstrated in the earlier example, except instead of referencing a form input file field, it will refer to the file to be uploaded through the dataTransfer property of the event object.

On the dataTransfer property, there is a files list property that provides access to the files that have been dropped on the drop zone. These files are then appended to the instantiated FormData object and uploaded as previously shown with the XHR object.

dropZone.addEventListener("drop", function(e) {

  e.preventDefault();
  this.classList.remove("active");

  // create a new FormData object
  var fd = new FormData();
  // iterate over the files dragged on to the browser
  for (var x=0; x < e.dataTransfer.files.length; x++) {
    // add each file to FormData object
    fd.append("file-" + x, e.dataTransfer.files[x]);
  }

  var xhr = new XMLHttpRequest();

  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
      console.dir(JSON.parse(xhr.responseText));
    }
  };

  xhr.open("POST", "/upload");
  xhr.send(fd);
});

Tracking File Upload Progress

HTML5 provides a new element named progress which can be used to display a progress bar. The bar has two attributes, max and value. The progress bar assumes a minimum value of 0, and allows the developer to specify the maximum value. For the purposes of handling file uploads, the maximum value specified with the max attributed is the size in bytes of the file to be uploaded. The value represents the current progress toward the maximum value. For file uploads, the value is the number of bytes that have been uploaded.

<form>
  <div>
    <label for="my-file">Select File:</label>
    <input id="my-file" name="my-file" type="file">
  </div>
  <div>
    <progress id="upload-progress" class="hide-me"></progress>
  </div>
  <button type="button" id="upload-button">Upload Me!</button>
</form>

The most recent version of the XHR object provides an upload property. The upload property provides a progress event which is called multiple times during the upload process. Typically, it reports the size of the file being uploaded along with the number of bytes that have been uploaded. Using JavaScript, the code can hook into this event to retrieve the file upload progress in bytes, then use the information to update the progress bar element.

xhr.upload.onprogress = function(e)  {

  // if the file upload length is known, then show progress bar
  if (e.lengthComputable) {
    uploadProgress.classList.remove("hide-me");
    // total number of bytes being uploaded
    uploadProgress.setAttribute("max", e.total);
    // total number of bytes that have been uploaded
    uploadProgress.setAttribute("value", e.loaded);
  }
};

The code above can simply be inserted into the same code block that wires up the XHR file upload, as seen in previous examples. Full code sample link are provided at the bottom of the page.

Conclusion

The ability to perform file uploads using asynchronous techniques is nothing new, but the ability to use the XHR object, drag & drop, and even track progress, can replace and enhance the old-school approach of using Hidden iframes. HTML5 and enhancements to the XHR object have improved the usability of file uploads while simplifying the implementation details for the developer. In the next post, we will be look at using Web Sockets and Socket.IO to perform file uploads over web sockets using the FileReader and ArrayBuffer APIs available in the web browser. Truly, the web browsers of today are a feature and API rich environment for performing all kinds of useful web tasks.

Download Code Samples:

  1. XHR File
  2. Progress XHR
  3. Hidden iframe
  4. Drag and Drop

Web Page File Uploads: History and Future (Part 1)
Web Page File Uploads: Web Sockets (Part 3)


Author: Eric Greene, one of Accelebrate’s instructors.

Accelebrate offers private AngularJS training and JavaScript training for groups and instructor-led online JavaScript classes for individuals.


Written by Eric Greene

Eric Greene

Eric is a professional software developer specializing in HTML, CSS, and JavaScript technologies. He has been developing software and delivering training classes for nearly 19 years. He holds the MCSD Certification for ASP.Net Web Applications, and is a Microsoft Certified Trainer.


Learn faster

Our live, instructor-led lectures are far more effective than pre-recorded classes

Satisfaction guarantee

If your team is not 100% satisfied with your training, we do what's necessary to make it right

Learn online from anywhere

Whether you are at home or in the office, we make learning interactive and engaging

Multiple Payment Options

We accept check, ACH/EFT, major credit cards, and most purchase orders



Recent Training Locations

Alabama

Birmingham

Huntsville

Montgomery

Alaska

Anchorage

Arizona

Phoenix

Tucson

Arkansas

Fayetteville

Little Rock

California

Los Angeles

Oakland

Orange County

Sacramento

San Diego

San Francisco

San Jose

Colorado

Boulder

Colorado Springs

Denver

Connecticut

Hartford

DC

Washington

Florida

Fort Lauderdale

Jacksonville

Miami

Orlando

Tampa

Georgia

Atlanta

Augusta

Savannah

Hawaii

Honolulu

Idaho

Boise

Illinois

Chicago

Indiana

Indianapolis

Iowa

Cedar Rapids

Des Moines

Kansas

Wichita

Kentucky

Lexington

Louisville

Louisiana

New Orleans

Maine

Portland

Maryland

Annapolis

Baltimore

Frederick

Hagerstown

Massachusetts

Boston

Cambridge

Springfield

Michigan

Ann Arbor

Detroit

Grand Rapids

Minnesota

Minneapolis

Saint Paul

Mississippi

Jackson

Missouri

Kansas City

St. Louis

Nebraska

Lincoln

Omaha

Nevada

Las Vegas

Reno

New Jersey

Princeton

New Mexico

Albuquerque

New York

Albany

Buffalo

New York City

White Plains

North Carolina

Charlotte

Durham

Raleigh

Ohio

Akron

Canton

Cincinnati

Cleveland

Columbus

Dayton

Oklahoma

Oklahoma City

Tulsa

Oregon

Portland

Pennsylvania

Philadelphia

Pittsburgh

Rhode Island

Providence

South Carolina

Charleston

Columbia

Greenville

Tennessee

Knoxville

Memphis

Nashville

Texas

Austin

Dallas

El Paso

Houston

San Antonio

Utah

Salt Lake City

Virginia

Alexandria

Arlington

Norfolk

Richmond

Washington

Seattle

Tacoma

West Virginia

Charleston

Wisconsin

Madison

Milwaukee

Alberta

Calgary

Edmonton

British Columbia

Vancouver

Manitoba

Winnipeg

Nova Scotia

Halifax

Ontario

Ottawa

Toronto

Quebec

Montreal

Puerto Rico

San Juan