Build Web applications with HTML 5

Many new features and standards have emerged as part of HTML 5. While most of the modern browsers support these features, some browsers may not support all the features.

In this article, we will learn how to detect if an HTML5 feature is supported by a browser. We will also create sample applications using these features. We will use core web technologies like HTML, CSS, and JavaScript for these examples.

Getting started

To test our code we will need multiple browsers. It is recommended to test the application we will develop in different browsers, both on desktop and mobile, to see what features are supported by them.

Detecting capabilities

While building responsive web application, which can work on both desktop and mobile, we have consider cross-browser differences. While most of the popular browsers follow web standards, but as new features are introduced, browsers may adopt them at their own pace.

So, if we are building a web application using any of the cutting edge feature, it is always a good idea to use them as progressive enhancement. That is, provide some baseline features which works on all browsers, check if the browser support advance capabilities, and if they do, enhance the application with extra features.

In Listing 1, we will look at how to detect some of the HTML5 features.

Listing 1. Detection script
<!DOCTYPE html>
<html>

<head>
    <title>HTML5</title>
    <script type="text/javascript">
        function detectBrowserCapabilities() {
            document.getElementById("userAgent").innerHTML = navigator.userAgent;
            var hasWebWorkers = !!window.Worker;
            document.getElementById("workersFlag").innerHTML = "" + hasWebWorkers;
            var hasGeolocation = !!navigator.geolocation;
            document.getElementById("geoFlag").innerHTML = "" + hasGeolocation;
            if (hasGeolocation) {
                navigator.geolocation.getCurrentPosition(function(location) {
                    document.getElementById("geoLat").innerHTML = location.coords.latitude;
                    document.getElementById("geoLong").innerHTML = location.coords.longitude;
                });
            }
            var hasDb = !!window.openDatabase;
            document.getElementById("dbFlag").innerHTML = "" + hasDb;
            var videoElement = document.createElement("video");
            var hasVideo = !!videoElement["canPlayType"];
            var ogg = false;
            var h264 = false;
            if (hasVideo) {
                ogg = videoElement.canPlayType('video/ogg; codecs="theora, vorbis"') || "no";
                h264 = videoElement.canPlayType('video/mp4;codecs="avc1.4vStyle2E01E, mp4a.40.2"') || "no";
            }
            document.getElementById("videoFlag").innerHTML = "" + hasVideo;
            document.getElementById("h264Flag").innerHTML = "" + h264;
            document.getElementById("oggFlag").innerHTML = "" + ogg;
        }
    </script>
</head>

<body onload="detectBrowserCapabilities()">
    <div>Your browser's user-agent: <span id="userAgent">
         </span>
    </div>
    <div>Web Workers? <span id="workersFlag"></span></div>
    <div>Database? <span id="dbFlag"></span></div>
    <div>Video? <span id="videoFlag"></span></div>
    <div class="videoTypes">Can play H.264? <span id="h264Flag">
         </span>
    </div>
    <div class="videoTypes">Can play OGG? <span id="oggFlag">
         </span>
    </div>
    <div>Geolocation? <span id="geoFlag"></span></div>
    <div class="location">
        <div>Latitude: <span id="geoLat"></span></div>
        <div>Longitude: <span id="geoLong"></span></div>
    </div>
</body>

</html>

While there are a lot of features introduced as part of the HTML5 standard, in this article we will focus on four features:

  • Web workers (multi-threading)
  • Geolocation
  • Database storage
  • Native video playback

In our code, we have a function detectBrowserCapabilities which as its name suggests can detect what features our browser supports. We will call this function on page load. Under the body section of our page, we have a simple HTML structure used to display the diagnostic information that the detection function is gathering.

First, we display the browser’s user agent. This is usually a string that uniquely identifies a browser.

Next, we start detecting our features, starting with Web workers. We check for Web workers by looking for the Worker function in the global scoped window object. We use the double negation (!!) to get a boolean value. If the Worker function does not exist, then window.Worker will evaluate to undefined, which is a “falsy” value in JavaScript. If we put a single negation in front of it, it will evaluate to true, so a double negation will evaluate to false. After testing for the value, the script prints the evaluation to the screen by modifying the DOM structure.

The next feature to test for is geolocation. The double negation technique is used again, but this time we check for an object called geolocation that should be a property of the navigator object. If it is present, then we use it to get the current location using the geolocation object’s getCurrentPosition function. Because getting the location could take a some time, the getCurrentPosition function is an asynchronous function and takes a callback function as a parameter. In this case, we use a closure for the callback that simply displays the location fields by writing the latitude and longitude to the DOM.

The next step is to check for database storage. We check if a function openDatabase is present in the global window object. This function is used for creating and accessing client-side databases.

Finally, we check for native video playback capabilities. We use the DOM API (createElement) to create a video element. Most modern browsers will be able to create a specialized video element. If the element created has a function canPlayType, we know that the browser supports this feature.

Even if a browser has native video playback capability, the types of videos, or the supported codecs that it can play, are not standardized. So we should check for the supported codecs in the browser. There is no standard list of codecs, but two of the most common are H.264 and Ogg Vorbis. To check for support for a particular codec, we can pass an identifying string to the canPlayType function. If the browser can support the codec, this function will return probably. If not, then it will return null. Our feature detection function simply checks against these values and displays the answer in the DOM.

After testing out this code against some popular browsers, Listing 2 shows the aggregated results.

Listing 2. Capabilities of various browsers
#Chrome 72
Your browser's user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36
Web Workers? true
Database? true
Video? true
Can play H.264? no
Can play OGG? probably
Geolocation? true
Latitude: -37.810492599999996
Longitude: 144.9568162

#Safari 12.0.2
Your browser's user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.2 Safari/605.1.15
Web Workers? true
Database? true
Video? true
Can play H.264? probably
Can play OGG? no
Geolocation? true
Latitude: -37.810709529550515
Longitude: 144.95704678382384

#Firefox 65.0.1
Your browser's user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0
Web Workers? true
Database? false
Video? true
Can play H.264? no
Can play OGG? probably
Geolocation? true
Latitude: -37.810691030828046
Longitude: 144.95709265287786

As we can see, the latest versions of Chrome, Safari, and Firefox support all the features we are interested in. However, they support different video formats. Chrome and Firefox support OGG, and Safari supports H.264.

Listing 3 shows the aggregated results for mobile browsers on iPhone.

Listing 3. Capabilities of various mobile browsers on an iPhone
#iPhone Chrome
Your browser's user-agent: Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/72.0.3626.101 Mobile/15E148 Safari/605.1
Web Workers? true
Database? true
Video? true
Can play H.264? probably
Can play OGG? no
Geolocation? true
Latitude: -37.810755662925196
Longitude: 144.95711458020352

#iPhone Safari
Your browser's user-agent: Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1
Web Workers? true
Database? true
Video? true
Can play H.264? probably
Can play OGG? no
Geolocation? true
Latitude: -37.810755662925196
Longitude: 144.95711458020352

The results on iPhone mobile browsers are very similar to the one we saw on desktop. The only difference is that Chrome for iPhone supports H.264 instead of OGG.

Now that we know how to probe the features of the user’s browser, let’s explore a simple application that will use several of these features in combination, depending on what the user’s browser can handle. We will build an application that uses the Foursquare API to search for popular venues near a user’s location.

Building the applications of tomorrow

The application starts off by searching for what Foursquare calls venues near the user’s current location. Venues can be anything, but are typically restaurants, bars, stores, and so on. Next we will retrieve additional details about these venues.

We will be using two Foursquare APIs. One is for search and the other is for getting the details of a venue. To use these APIs, we need to create an account and generate a client ID and a client secret key that will be used to make API calls. Please refer to Foursquare API documentation for more details.

Now that we know what kind of calls can be made by the application code, let’s see how it will make those calls and use the data from Foursquare.

Using geolocation

The first call is a search. Listing 4 shows that you need two parameters: geoLat and geoLong for the latitude and longitude.

Listing 4. Calling search with location
const CLIENT_ID = "";
const CLIENT_SECRET = "";
const VERSION = "20190101";
const VENUE_API_URL = "https://api.foursquare.com/v2/venues/search";

let allVenues;
const venueSearch = (geoLat, geoLong) => {
  const xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      const json = JSON.parse(this.responseText);
      allVenues = json.response.venues;
      buildVenuesTable(allVenues)
    }
  };

  xhr.open(
    "GET",
    `${VENUE_API_URL}?ll=${geoLat},${geoLong}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&v=${VERSION}`
  );
  xhr.send(null);
};

const retrieveVenues = () => {
  if (!!navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(function(location) {
      venueSearch(location.coords.latitude, location.coords.longitude);
    });
  }
};

The code above checks for the geolocation capability of the browser. If it is present, the code gets the location and calls the venueSearch function with the latitude and longitude. It uses a closure for the callback function, parses the JSON data from Foursquare, and passes an array of venue objects to a function called buildVenuesTable, as shown below.

Listing 5. Building UI from venues
const buildVenueRow = venue => {
  const row = document.createElement("tr");

  const nameCell = document.createElement("td");
  nameCell.appendChild(document.createTextNode(venue.name));
  row.appendChild(nameCell);

  const addressCell = document.createElement("td");
  addressCell.appendChild(
    document.createTextNode(venue.location.formattedAddress)
  );
  row.appendChild(addressCell);

  const distanceCell = document.createElement("td");
  distanceCell.appendChild(document.createTextNode(venue.location.distance));
  row.appendChild(distanceCell);

  return row;
};

const buildVenueRows = venues => {
  const body = document.createElement("tbody");

  venues.forEach(venue => {
    body.appendChild(buildVenueRow(venue));
  });

  return body;
};

const buildTableHeader = () => {
  const header = document.createElement("thead");
  const nameHeader = document.createElement("td");
  nameHeader.appendChild(document.createTextNode("Venue Name"));
  header.appendChild(nameHeader);

  const addressHeader = document.createElement("td");
  addressHeader.appendChild(document.createTextNode("Address"));
  header.appendChild(addressHeader);

  const distanceHeader = document.createElement("td");
  distanceHeader.appendChild(document.createTextNode("Distance (m)"));
  header.appendChild(distanceHeader);

  return header;
};

const buildVenuesTable = venues => {
  const venueTable = document.createElement("table");
  venueTable.appendChild(buildTableHeader());
  venueTable.appendChild(buildVenueRows(venues));
  document.getElementById("searchResults").appendChild(venueTable);
  if (!!window.openDatabase) {
    document.getElementById("saveBtn").style.display = "block";
  }
};

The code in Listing 5 is primarily DOM code for creating a data table with the venue information in it. In the last two lines, we detect if the browser supports a database, and if it does, then we enable a Save button that the user can click to save all of this venue data to a local database. The next section discusses how this is done.

You can learn more about using the HTML5 geolocation feature in this tutorial, “Create mobile web applications with HTML5 and geolocation APIs.”

Structured storage

Listing 5 in the previous section demonstrates the classic progressive enhancement strategy. The example tests for database support. If it is found, then the code adds a UI element that adds a new feature to the application that makes use of this feature. In this case, it enables a button. Clicking on the button calls the function saveAll, which is shown in Listing 6.

Listing 6. Saving to the database
let db = {};

function saveAll() {
 db = window.openDatabase("venueDb123", "1.0", "Venue Database", 1000000);
 db.transaction(function(txn) {
   txn.executeSql("CREATE TABLE IF NOT EXISTS venue (id, name, address, geolat, geolong);");
 });
 allVenues.forEach(saveVenue);
}

function saveVenue(venue) {
 // check if we already have the venue
 db.transaction(function(txn) {
   txn.executeSql("SELECT name FROM venue WHERE id = ?", [venue.id], function(
     t,
     results
   ) {
     if (results.rows.length == 1 && results.rows.item(0)["name"]) {
       console.log("Already have venue id=" + venue.id);
     } else {
       insertVenue(venue);
     }
   });
 });
}

function insertVenue(venue) {
 db.transaction(function(txn) {
   txn.executeSql(
     "INSERT INTO venue (id, name, address, geolat, geolong) " +
       " VALUES (?, ?, ?, ?, ?);",
     [
       venue.id,
       venue.name,
       venue.location.formattedAddress,
       venue.location.lat,
       venue.location.lng
     ],
     null,
     (tx, error) => {
       console.log("Error occured:", error);
     }
   );
 });
}

To save the venue data to the database, we start by creating a venue table where we will store all the venues. This is fairly standard SQL syntax for creating a table. SQL execution is done asynchronously. The transaction function is called, and a callback function is passed to it. The callback function gets a transaction object that it can use to execute SQL. The executeSQL function takes an SQL string, then optionally a parameter list, plus success and error handler functions. Notice that we have specific to only create a table if it does not exists.

Once the table is created, we insert each venue in the table one by one by invoking the saveVenue function. This function first checks to see if the venue has already exists in the table. Here we use the success handler and call the insertVenue function is the venue has not been saved in local database. The insertVenue function then inserts the venue is the table, storing its name, address, and geolocation coordinates (latitude and longitude).

We have learned how to use progressively use local database supported by the browser. The next section explores how to use Web workers.

You can learn more about using the HTML5 local storage feature in this tutorial, “Create mobile web applications with HTML5 and local storage.”

Background processing with Web workers

Going back to Listing 4, let’s make a slight modification. As shown in Listing 7 below, we will check for Web worker support. If the browser supports web workers, we will use the feature to retrieve additional information for each venues.

const venueSearch = (geoLat, geoLong) => {
  const xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      const json = JSON.parse(this.responseText);
      allVenues = json.response.venues;
      buildVenuesTable(allVenues)
      if (!!window.Worker){
                var worker = new Worker("details.js");
                worker.onmessage =(message) => {
                    const likes = message.data;
          console('Additional venue data retrieved', likes);
                };
                worker.postMessage(allVenues);
            }

    }
  };

  xhr.open(
    "GET",
    `${VENUE_API_URL}?ll=${geoLat},${geoLong}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&v=${VERSION}`
  );
  xhr.send(null);
};

In the code above, we use the same technique as before to detect if the browser supports Web workers. If Web workers are supported, then we create a new worker. To create a new worker, we need the URL to an external script that the worker will execute, in this case it is the details.js file. When the worker finishes its work, it will send a message back to the main thread using onmessage handler. For this example, we are simply logging the data we receive from the worker thread. Finally, to initiate the worker, we call the postMessage function with some data for it to work on. We are passing in all of the venues retrieved from Foursquare.

Listing 8 shows the contents of details.js, which is the script that will be executed by the worker.

Listing 8. The worker’s script, details.js
const likes = {};

const CLIENT_ID = "";
const CLIENT_SECRET = "";
const VERSION = "20190101";
const VENUE_DETAILS_API_URL = "https://api.foursquare.com/v2/venues/";

onmessage = function(message) {
  const venues = message.data;
  venues.forEach(venue => {
    const xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
      if (this.readyState == 4 && this.status == 200) {
        const venueDetails = JSON.parse(this.responseText);
        likes[venue.id] = venueDetails.likes.count;
      }
    };
    xhr.open(
      "GET",
      `${VENUE_DETAILS_API_URL}${
        venue.id
      }?client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&v=${VERSION}`,
     true
    );
    xhr.send(null);
  });
  postMessage(likes);
};

The details script iterates over each of the venues and make an HTTP call to get the venue details using Foursquare API. Notice when using the XMLHttpRequest open function, we are passing a third parameter with value true. This is to make the call synchronous. This is to show that it is ok to make synchronous calls in Web worker since we are not on the main UI thread and this is not going to freeze up the main application.

On receiving the API response, we extract the likes count from the venue details and collect all of these likes object to pass back to the main UI thread using postMessage function. This will invoke the onmessage callback on the main thread.

We again use the progressive enhancement to provide user with list of venues and then if the browser supports Web workers use the background thread to retrieve additional details instead of making the user wait.

You can learn more about using the HTML5 Web workers feature in this tutorial, “Speed up your mobile web applications with HTML5 and Web workers.”

Summary

This article covered some of the popular HTML5 capabilities of modern browsers. We learned how to detect the features and to progressively add them to our application. Most of the features are already widely supported in popular browsers on both desktop and mobile. Using these features and techniques we can build innovative web applications.