CHAPTER 5

image

Running TypeScript in a Browser

All modern web browsers—on desktops, game consoles, tablets and smart phones—include JavaScript interpreters, making JavaScript the most ubiquitous programming language in history.

—David Flanagan

Although there are many different environments you might target with your TypeScript program, one of the widest categories of runtime will surely be the web browser. This chapter introduces the general design of the web browser before introducing practical examples for interacting with web pages, making asynchronous requests to the web server, storing data on the user’s local machine, and accessing hardware sensors. At the end of the chapter there is information about modularizing your program and loading modules on demand.

Image Note  Some of the features described in this chapter are experimental and have limited browser support. To find out which browsers support any specific feature, visit the “Can I use” project by Deveria (http://caniuse.com/, 2014).

The Anatomy of a Web Browser

Web browsers have quickly evolved from the simple document displays of the 1990s to fully fledged application environments and 3D gaming displays today. The reliance on plugins, applets, and downloads is diminishing fast as video, audio, and gaming all join documents, images, and applications inside of the web browser.

It is worth knowing a little about web browsers if your program is going to rely on them to work, but if the details of browsers and the history of some of the important features aren’t causing a general feeling of excitement or if you already know everything there is to know about browsers, feel free to skip to the next section, which is more hands-on. If you’d like to know a bit more about how web browsers work read on.

Web browsers are typically made up of several components as shown in Figure 5-1.

  • User interface
  • Browser engine
  • Rendering engine
  • Widget engine
  • JavaScript interpreter
  • Networking
  • Storage

9781430267911_Fig05-01.jpg

Figure 5-1. Web browser components

The user interface includes all the buttons and text boxes that appear on all web browser windows, for example, the address bar, back and forward buttons, and the refresh button. The browser engine and rendering engine handle the content display, which takes up the main area of the web browser’s display. The widget engine supplies common user controls to the user interface and to the rendering engine, such as text inputs, drop-down lists, and buttons.

To display a web page, the browser engine relies on the rendering engine to display the HTML along with the appropriate styles defined in the cascading style sheets (CSS) or by the user if they are overriding page styles. The rendering engine relies on networking to fetch resources such as the web page, stylesheets, JavaScript files, and images. The widget engine is used whenever a user interaction component is needed, such as a text box. The JavaScript interpreter runs the downloaded JavaScript, which in turn may access storage, networking, and any other available application programming interfaces (APIs).

On the whole, the user interface, browser engine, rendering engine, and widget engine do a grand job and you don’t need to know all of the minute details; the one exception to this is a process known as reflows, which can affect the perceived performance of your program.

Reflows

Each time the layout of the web page is changed by JavaScript or CSS, the layout is flagged as being invalid, but isn’t immediately updated. The reflow, which recalculates the size and position of all elements in the document, would typically occur just before drawing the page. Additional reflows can be triggered if the JavaScript code requests the size or position of an element when the layout has the invalid flag. This additional reflow is needed to ensure the information supplied for the size or position is up to date.

Listing 5-1 shows a function that has a typical reflow problem, invalidating the layout on two occasions and causing two reflows. Each time a value is set on the document that can affect the layout; the layout is flagged as being invalid. Each time a value is retrieved from the document when the layout is invalid, the reflow is triggered. Although the example in the listing results in two reflows, if the mistake is repeated, it can result in many more. Reflows slow down your program and the page, which needs to wait for its turn to rerender.

Listing 5-1. Triggering multiple reflows

var image = document.getElementById('mainImage'),
var container = document.getElementById('content'),

function updateSizes() {
    // Flags the layout as invalid
    image.style.width = '50%';

    // Causes a reflow to get the value
    var imageHeight = image.offsetHeight;

    // Flags the layout as invalid
    container.classList.add('highlight'),

    // Causes a reflow to get the value
    var containerHeight = container.offsetHeight;

    return {
        'imageHeight': imageHeight,
        'containerHeight': containerHeight
    };
}

var result = updateSizes();

Multiple reflows can be avoided by performing the layout-invalidating operations before attempting to retrieve any values from the document, as shown in Listing 5-2. By grouping all of the operations that invalidate the layout at the start of the function and before any operations that require a reflow, we reduce the total number of reflows required during the function.

Listing 5-2. Triggering a single reflow

var image = document.getElementById('mainImage'),
var container = document.getElementById('content'),

function updateSizes() {
    // Operations that invalidate the layout
    image.style.width = '50%';
    container.classList.add('highlight'),

    // Operations that require a reflow
    var imageHeight = image.offsetHeight;
    var containerHeight = container.offsetHeight;

    return {
        'imageHeight': imageHeight,
        'containerHeight': containerHeight
    };
}

var result = updateSizes();

The only situation that can prevent you from avoiding multiple reflows is one in which you need to obtain a measurement after making a change. For example, finding the width of an element after you have changed its contents and then using the width to reposition the element, which cannot be done without a reflow. You can still carefully plan your operations to reduce the overall number of reflows to the minimum number possible.

The Interesting Components

The JavaScript interpreter along with the network and storage APIs are the most interesting components of a web browser when it comes to TypeScript. Each is described in more detail in the following sections.

The JavaScript Interpreter

The JavaScript interpreter, or JavaScript engine as it is also known, has a lot of work to do. Not only does it parse and execute the JavaScript program; it must manage objects and memory, work the event loop, and handle interactions with APIs such as storage, network, and sensors.

One of the things that make JavaScript programming in the browser so interesting (and at times frustrating) is that you will encounter many different JavaScript interpreters. In some rare cases, you may even encounter no interpreter and your program won’t run. Having to support a number of interpreters can increase the amount of testing you need to perform as you will need to check that your program works in each web browser. However, there are upsides to the plethora of interpreters. One of the upsides is that browser vendors all want to be able to claim that their particular implementation of a JavaScript engine is the fastest and as a result interpreters have become many times faster as they each fight for the top spot.

The main things to watch for when relying on all of these different interpreters to run your program are

  • They may only support an older version of the ECMAScript standard.
  • They are allowed to implement additional features that are not part of the ECMAScript specification.
  • They all run different code at different speeds especially on different operating systems.
  • At some point in time you will encounter an end user who has JavaScript switched off entirely.

Image Note  The vast majority of browsers currently support ECMAScript version 5 as well as various parts of the ECMAScript 6 standard. Some older browsers are still stuck on the much older ECMAScript 3 standard, so you’ll need to know whether you need to support these browsers as it will restrict the language features you can use in JavaScript (and to a lesser extent TypeScript, see Chapter 1).

A Brief History of Networking

The evolution of networking in the web browser can be tracked through several stages. The earliest mechanism for updating a part of the web page, without replacing the entire document, was to use a frameset. Framesets were a proposal for the HTML 3.0 specification. Websites would typically have a three-part frameset with individual frames for the header, navigation, and content. When a link was selected in the navigation frame, the content frame would be replaced with a new web page without reloading the header or the navigation. Framesets had the dual purpose of allowing parts of the display to update independently and also allowed re-usable widgets such as headers and navigation to be included without server-side processing.

A major problem with framesets was that the web address for the page was not updated as the user navigated because the user was still viewing the frameset, no matter which pages were being displayed in the frames within the frameset. When users bookmarked a page or shared a link to a page, it would not lead them back to the display they navigated to, but instead simply displayed the landing page for the website. In addition, framesets caused various problems for screen readers and text browsers.

The replacement for framesets was inline frames (the iframe element). Inline frames were placed inside the body of another document and could be updated independently.

But what do frames have to do with networking? In the days before networking in JavaScript, enterprising and creative programmers would use frames to give the appearance of live updates. For example, a hidden iframe pointing to a server-generated web page would be refreshed using a timer every ten seconds. Once the page has loaded, JavaScript would be used to grab new data from the iframe and update parts of the visible page based on the hidden page in the iframe. The architecture of this mechanism is shown in Figure 5-2.

9781430267911_Fig05-02.jpg

Figure 5-2. Updating a web page by refreshing a second page in an iframe

It was this creative use of frames and inline frames to transfer data from the server to a web page that inspired the invention of XMLHTTP communication and later the standardized XmlHttpRequest. These asynchronous requests were revolutionary because of the part they played in enabling web-based applications. There are various complications with using asynchronous requests, which are detailed later in this chapter, but their importance cannot be overstated.

The latest networking technology to hit the web browser is web sockets, which provides a persistent full-duplex communication between the browser and the server. This allows simultaneous communication in both directions. Web sockets are also discussed in more detail later in this chapter.

Storing Data on the Client

For a long time, the only storage available to a JavaScript program was a few miserly kilobytes in a cookie that could disappear without warning at any time. Many browsers offer a setting that clears all cookies each time the browser is closed. At best, cookies could be used to store a token to keep a user logged in for a time, and this was really its only major usefulness to a web application.

In modern browsers, there are many options for storage on the user’s machine from simple key/value local storage to NoSQL indexed databases. Even the initial limit of a few megabytes can be increased with the user’s permission. Concrete examples of storage options are explained later in this chapter.

The ability to store a reasonable amount of data on the user’s machine allows caching of data locally. This can speed up your program and reduce the number of round trips to the server. It also allows your web application to run offline and synchronize with the server the next time a connection is available.

The Document Object Model

The Document Object Model, or DOM, is a web browser interface for interacting with HTML and XML documents. The interface allows you to find elements, obtain and update information about their contents, and attributes and listen to user events. If you are interacting with a web page from your program, you are using the DOM.

All of the examples in this section use the HTML document in Listing 5-3.

Listing 5-3. HTML document for DOM examples

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Running in a Browser</title>
    <link rel="stylesheet" href="app.css" type="text/css" />
</head>
<body>
    <h1>Running in a Browser</h1>

    <div id="content"></div>
    <script data-main="app" src="/Scripts/require.js"></script>
</body>
</html>

The document is an HTML5 web page with a heading and a division with an id of “content”. The aim of the following examples is to obtain a reference to this element, make changes to it, and listen to events generated within it.

Finding Elements

One of the most common interactions with the DOM is finding an element within the document. There are several ways to get an element as shown in Listing 5-4. Using document.getElementById has long been the standard method of obtaining an element on the web page and in TypeScript this returns an object of the HTMLElement type. Although this is a common way to find elements, it specifically only obtains elements based on their id attribute.

Listing 5-4. Finding DOM elements

// HTMLElement
var a = document.getElementById('content'),

// Element
var b = document.querySelector('#content'),

// HTMLDivElement (due to type assertion)
var c = <HTMLDivElement> document.querySelector('#content'),

The traditional alternative to document.getElementById has been document.getElementsByTagName. Whereas obtaining elements based on the id is too specific; finding them by tag name is typically too general. For this reason, the document.querySelector and document.querySelectorAll methods were introduced in the Selectors API specification, allowing CSS query selectors to be used to find elements. When there are multiple possible matches, document.querySelector returns the first matching element, whereas document.querySelectorAll returns all matching elements.

When you obtain elements using getElementById it will return the general HTMLElement type. Using querySelector will get you the even more general Element type. The TypeScript compiler is not able to determine the exact kind of element that is returned. If you want to use members from a specific type of element, you can use a type assertion to tell the compiler which element type to expect. This doesn’t guarantee that the type will be correct at runtime; it just gives you the right autocompletion information and type checking.

Type assertions are not required when you use document.getElementsByTagName because TypeScript uses specialized overload signatures to return the correct type based on the tag name you supply. This is shown in Listing 5-5, where the NodeList is returned with elements of the HTMLDivElement type automatically.

Listing 5-5. Getting elements by HTML tag

// NodeListOf<HTMLDivElement>
var elements = document.getElementsByTagName('div'),

// HTMLDivElement
var a = elements[0];

The final type you will come across is the NodeList returned from the document.querySelectorAll method, in which each element is of type Node, rather than Element or HTMLElement as shown in Listing 5-6. Despite this, you can still use a type assertion to work with the specialized HTML element of your choice.

Listing 5-6. Getting elements using CSS selectors

// NodeList
var elements = document.querySelectorAll('#content'),

// Node
var a = elements[0];

// HTMLDivElement
var b = <HTMLDivElement> elements[0];

Image Note  You may have noticed that the various methods of finding elements in the DOM all return different types of objects and different collections. Be prepared for another variation when the DOM4 specification, van Kesteren and Gregor (2014), gains adoption—as the query and queryAll methods return the single Element type and Elements collection type, respectively.

Changing Elements

Once you have located an element, or elements, that you want to change, there are several options available to you to update the contents of each element.

Listing 5-7 shows a simple replacement of the entire contents of the element by supplying a new string of HTML. The existing contents of the element will be discarded in favor of the string you supply. There are downsides to this approach; not only does this involve hard-coding HTML strings in your program, but also there can be security risks if you use this method to insert user-generated or third-party content. On the positive side, this is simplest way to completely replace the entire contents of an element.

Listing 5-7. Updating the element’s HTML

var element = <HTMLDivElement> document.querySelector('#content'),

element.innerHTML = '<span>Hello World</span>';

In many cases, rather than replacing the entire contents of an element, you will want to add to the element without losing all of the existing contents. Listing 5-8 shows multiple additions to the content division, which results in all of the new elements being appended. The listing also shows the use of the document.createElement method to generate elements, rather than using strings.

Listing 5-8. Using appendChild

var element = <HTMLDivElement> document.querySelector('#content'),

var newElement1 = document.createElement('div'),
newElement1.textContent = 'Hello World';

element.appendChild(newElement1);

var newElement2 = document.createElement('div'),
newElement2.textContent = 'Greetings Earth';

element.appendChild(newElement2);

When you use element.appendChild, the newest element appears last. To add the newest element to the top of the element, you can use the element.insertBefore method as shown in Listing 5-9. The first argument passed to insertBefore is the new element and the second argument is the element used to position the new element. In the example the current first child element is used to ensure the new element appears first, but you can use the same method to place a new element anywhere in the DOM.

Listing 5-9. Using insertBefore

var newElement2 = document.createElement('div'),
newElement2.textContent = 'Greetings Earth';

element.insertBefore(newElement2, element.firstChild);

If you plan to create a nested set of elements to add to the page, it is more efficient to construct the whole hierarchy before adding it to the DOM. This will ensure you only invalidate the layout once, which in turn ensures the page is only redrawn once to reflect your changes.

Events

There are many different ways of attaching event listeners, with some browsers lagging behind the standards-compliant method of adding listeners. The addEventListener method is the standards-compliant way to add an event listener for a DOM event, but some browsers still rely on the attachEvent method (which also requires that the event name is prefixed with 'on').

To solve the problems of cross-browser compatibility, Remy Sharp created an addEvent method that not only eases the browser differences, but also allows collections of elements to be passed as an argument, not just single elements. Listing 5-10 is an adapted version of Remy’s original script with the addition of type information for the method.

Listing 5-10. Cross-browser enhanced events

var addEvent: (elem: any, eventName: string, callback: Function) => void = (function () {
    if (document.addEventListener) {
        return function (elem, eventName, callback) {
            if (elem && elem.addEventListener) {
                // Handles a single element
                elem.addEventListener(eventName, callback, false);
            } else if (elem && elem.length) {
                // Handles a collection of elements (recursively)
                for (var i = 0; i < elem.length; i++) {
                    addEvent(elem[i], eventName, callback);
                }
            }
        };
    } else {
        return function (elem, eventName, callback) {
            if (elem && elem.attachEvent) {
                // Handles a single element
                elem.attachEvent('on' + eventName, function () {
                    return callback.call(elem, window.event);
                });
            } else if (elem && elem.length) {
                // Handles a collection of elements (recursively)
                for (var i = 0; i < elem.length; i++) {
                    addEvent(elem[i], eventName, callback);
                }
            }
        };
    }
})();

export = addEvent;

The two major branches of the addEvent method handle the browser differences, with a check inside each branch that handles either a single element of a collection of elements. When all browsers support the addEventListener method, the second half of the method will become redundant.

This addEvent method will be used wherever events are needed in this chapter.

Frameworks and Libraries

There are many frameworks and libraries that can help with all of these DOM interactions. A select few are described below, although there are many more to choose from. The incredible selection of libraries is summed up neatly by Martin Beeby.

If you pick a noun and add .js or .io, you’ll probably get a library.

—Martin Beeby

Despite a sometimes overwhelming range of libraries, the high quality ones tend to float to the top thanks to a discerning and vocal community. Most of the available libraries can be added to your program using your preferred package manager, such as NuGet in Visual Studio, or NPM if you are using NodeJS, or you can just download the scripts and add them manually. For a third-party library that is written in plain JavaScript, you can usually find a matching type definition within the Definitely Typed project repository on GitHub:

https://github.com/borisyankov/DefinitelyTyped

Figure 5-3 shows the installation of RequireJS using the NuGet package manager screen within Visual Studio. The type definition file is also listed in the search results. The package manager will install the library as well as any dependencies and perform any project configuration required.

9781430267911_Fig05-03.jpg

Figure 5-3. Installing libraries and type definitions using NuGet

Figure 5-4 shows the local installation of KnockoutJS using Node Package Manager and the command npm install [library-name]. This will install the library and any dependencies in the local project within a folder named node_modules.

9781430267911_Fig05-04.jpg

Figure 5-4. Installing libraries with Node Package Manager

The ability to find and change elements on a web page becomes more powerful when you combine this feature with real-time data from your server. The next section covers making background requests to a web server to save and retrieve information without reloading the entire web page.

Network

Since its invention towards the end of the 1990s, AJAX has dominated the networking requirements for JavaScript in the web browser. Despite its dominance, there have been some newer entrants into the networking space that are useful for browser-based applications. This section introduces the three major techniques for communicating from the browser; allowing you to pick and choose the methods that best serve your program.

AJAX

AJAX stands for asynchronous JavaScript and XML. This is a poor name because XML is not the only format used for data, and it may not even be the most common format. An AJAX request is initiated using JavaScript in the browser. The request is sent to the server, which sends an HTTP response that can include a body in plain text, JSON, HTML, XML or even a custom format.

The HTTP request and response occur asynchronously, which means it doesn’t block the JavaScript event loop described in Chapter 4.

HTTP Get

Listing 5-11 shows a simple Ajax class with a single public method for performing HTTP GET requests. The method creates a new XMLHttpRequest object, which is the standard way to make AJAX requests. A callback is then attached to the onreadystatechange event on the request. This is called for each of the states that a request transitions to, but normally you will be primarily interested in the completed state. The potential states are

  • 0—Uninitialized
  • 1—Set up, but not sent
  • 2—Sent
  • 3—In flight
  • 4—Complete

The Ajax class in Listing 5-11 only executes the callback when the status is 4 (Complete) and passes the HTTP status code and the response text to the callback function. The HTTP status code could potentially be any of the codes described in the HTTP specification maintained by the W3C (1999).

The open method accepts the HTTP verb for the request and the URL. The third parameter sets whether the request is asynchronous. Finally, with the state change listener attached and with the request set up with a HTTP verb and URL, the send method can be used to begin the request.

Listing 5-11. HTTP Get method

class Ajax {
    private READY_STATUS_CODE = 4;

    private isCompleted(request: XMLHttpRequest) {
        return request.readyState === this.READY_STATUS_CODE;
    }

    httpGet(url: string, callback: (status: number, response: string) => any) {
        // Create a request
        var request = new XMLHttpRequest();

        // Attach an event listener
        request.onreadystatechange = () => {
            var completed = this.isCompleted(request);
            if (completed) {
                callback(request.status, request.responseText);
            }
        };

        // Specify the HTTP verb and URL
        request.open('GET', url, true);

        // Send the request
        request.send();
    }
}

export = Ajax;

Image Note  You should always make your AJAX requests asynchronous and use a callback to execute dependent code. Although making a request synchronous appears convenient, you will tie up the event loop for a long period of time and your application will appear unresponsive.

HTTP Post

The example code in Listing 5-12 is an httpPost method that can be added to the Ajax class from Listing 5-9. As well as changing the HTTP verb to 'POST', the content type request header is added and the data are sent in the request body. The data in this example must formatted as key/value pairs, for example 'type=5&size=4'. To send a JSON string containing the data, you would have to set the content type to 'application/json'.

Listing 5-12. HTTP Post method

httpPost(url: string, data: string, callback: (status: number, response: string) => any) {
    var request = new XMLHttpRequest();

    request.onreadystatechange = () => {
        var completed = this.isCompleted(request);
        if (completed) {
            callback(request.status, request.responseText);
        }
    };

    request.open('POST', url, true);
    request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'),
    request.send(data);
}

You can send different data formats by specifying the appropriate Content-type, for example, application/json or application/xml, and by passing the data in the appropriate serialized format. You are only limited by what your server-side program accepts.

You can call the Ajax class whenever you need to make an HTTP request and an example call is shown in Listing 5-13. You could also extend the Ajax class to handle other HTTP requests, such as PUT and DELETE.

Listing 5-13. Using the Ajax class

import Ajax = require('./Scripts/Ajax'),

var ajax = new Ajax();

// The function to execute when the response is received
function onGetResponse(status: number, data: string) {
    document.getElementById('content').innerHTML = data;
}

// Making a GET request
ajax.httpGet('Data.html', onGetResponse);

If you attempt to make an AJAX request to a different domain, you will find that the request is blocked by a cross-origin security feature in modern browsers. You will encounter this even across subdomains on the same website or between HTTP and HTTPS pages. In cases where you want to enable cross-origin request sharing (CORS), and if the server supports it, you can add an additional header to your AJAX request, as shown in Listing 5-14. This header causes a preflight OPTIONS request to be sent to ask if the server will accept the actual request, which follows if the server confirms that it will accept the cross-origin communication.

Listing 5-14. Allowing CORS, client side

request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'),

Although server configuration is beyond the scope of this chapter, for a server to support CORS, it must accept and respond to the preflight OPTIONS request that is issued before the actual cross-origin request with an Access-Control-Allow-Origin response header. This header indicates the domains that the server is willing to communicate with. This acts as a handshake between the client and server to verify that the cross-domain communication can proceed.

WebSockets

One of the most common uses of AJAX has been to poll a server to check for updates. One particular implementation of this is long polling; the AJAX request is made, but the server delays responding to the request until it has an update to send. Long-polling implementations must deal with timeout issues and concurrent request limits. Long polling can also cause problems on some servers where the number of clients waiting for a response can tie up a large number of request threads.

The WebSocket specification solves this problem by establishing a persistent two-way communication channel between the server and client that can be used to send messages in either direction. This means you can send messages at any time without having to re-establish a connection and you can receive messages in the same way. Listing 5-15 is a simple example of establishing communication with a server using the ws:// protocol, listening for messages, and sending a message to the server.

Listing 5-15. Establishing a WebSocket connection

var webSocket = new WebSocket('ws://localhost:8080/WS'),

webSocket.onmessage = (message: MessageEvent) => {
    // Log message from server
    console.log(message.data);
}

webSocket.send('Message To Server'),

When you are finished with a WebSocket connection, you can end the communication by calling webSocket.close(). If you want to learn more about web sockets, you can read The Definitive Guide to HTML5 WebSockets by Wang, Salim, and Moskovits (Apress, 2013).

Real-Time Communications

The next evolution in network communications is real-time peer-to-peer audio and video streaming. The WebRTC specification being drafted by the W3C (2013) allows streaming between browsers without the need for browser plugins or additional installed software. Although the specification currently has limited support, the potential for the technology is incredible. Video and audio calls would be possible between browsers without the need for a communication provider in the middle.

WebRTC is supported in several browsers in an experimental state, with most browsers offering the feature using a prefixed version that is subject to change. To use WebRTC in TypeScript you will need to extend the library definitions to include these transitional browser implementations.

A full implementation of WebRTC is outside of the scope of this book, but Listing 5-16 shows the additional definitions required to support the getUserMedia API, which gives access to the user’s video and audio stream after obtaining the user’s permission.

Listing 5-16. Interface extensions for transitional getUserMedia

interface GetUserMedia {
    (options: {}, success: Function, error: Function): any;
}

interface HTMLVideoElement {
    mozSrcObject: any;
}

interface Window {
    URL: any;
    webkitURL: any;
}

interface Navigator {
    getMedia: GetUserMedia;
    getUserMedia: GetUserMedia;
    webkitGetUserMedia: GetUserMedia;
    mozGetUserMedia: GetUserMedia;
    msGetUserMedia: GetUserMedia;
}

navigator.getMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);

These definitions are not comprehensive, but they allow your program to use the key features of this API to obtain a stream and display it within an HTML video element. The final line of code in this example condenses all of the potential prefixed versions of the feature into a single navigator property. Listing 5-17 is a working example of displaying video within a web page.

Listing 5-17. Displaying a video stream

var video = document.createElement('video'),
document.body.appendChild(video);

function videoObtained(stream) {
    if (navigator.mozGetUserMedia) {
        video.mozSrcObject = stream;
    } else {
        var vendorURL = window.URL || window.webkitURL;
        video.src = vendorURL.createObjectURL(stream);
    }
    video.play();
}

navigator.getMedia({ video: true, audio: false },
    videoObtained,
    (err) => console.log(err)
);

The getMedia method contains whichever version of getUserMedia is supported in the browser and accepts arguments for options, a success callback and an error callback. The success callback in the example is the videoObtained function, which adds the obtained stream to the video element and plays the video. The result of this script is usually the pleased face of a programmer being shown back to them on the web page.

Obtaining video and audio is the first step towards establishing a peer-to-peer stream and if you are interested in this technology there are entire books dedicated to this fascinating subject. Despite being a part of the WebRTC specification, the getUserMedia API has other potential uses outside of peer-to-peer communication. You may want to grab an image from the video stream to use in your program, or even use the stream in a more traditional manner to send to a server.

Networking provides the tools you need to communicate from the local browser to a server or remote peer. The next section covers storing data locally, which can allow your program to continue to work even when the network is unavailable.

Storage

Storage on the user’s machine has come a long way since cookies, with their size limitations and terrible API. Depending on what you need, there are several available storage options with different lifespans, soft limits, and APIs that you can use to keep hold of data locally.

Both session storage and local storage have an identical API, but they offer different life-spans. However, IndexedDB offers a more advanced data storage mechanism. All three of these storage APIs are described in the following.

Session Storage

Session storage is attached to a page session. A page session starts when a page is opened and continues even if the page is reloaded or restored within a browser tab. Opening the same page in a separate tab or browser window results in a new page session.

Listing 5-18 shows how simple the session storage API is, allowing a simple key/value pair to be stored with the setItem method. Both the key and the value must be strings, so objects would need to be serialized to a string to be stored.

Listing 5-18. Session storage

var storageKey = 'Example';

// null the first time, 'Stored value' each subsequent time
console.log(sessionStorage.getItem(storageKey));

sessionStorage.setItem(storageKey, 'Stored value'),

To demonstrate the life-span of this storage mechanism, the getItem method is called before the item is set; when the page first loads, the null value is logged, but on subsequent refreshes the stored value is logged. If you open the page in a new tab, once again the null value will be logged. If you view the page, visit an entirely separate page in the same tab, then load the original page once again, you’ll see that the value has been retained. The session remains as long as the tab is open, even if other pages are loaded in the tab—the browser may even support the resumption of the session after a restart.

Listing 5-19 shows the methods for removing an item based on its key and for clearing all items from the session storage for the page. These methods follow the same scope and life cycle as the other session storage methods described earlier.

Listing 5-19. Removing and clearing session storage

// Remove an item using a key
sessionStorage.removeItem(storageKey);

// Clear all items
sessionStorage.clear();

Local Storage

The local storage API is identical to the session storage API, but the storage persists until it is deleted by the user or cleared for security reasons. Local storage can also be accessed from multiple pages on the same domain as well as in multiple browsers and tabs.

Because local storage items are shared across pages, tabs, and browsers it can be used to store a cache of data to reduce network traffic. It also can be used to store user-entered data while there is no connection or to store data that never needs to be transmitted, such as temporary application state.

Listing 5-20 describes a script that stores a value including the current date and time in local storage. An event listener is attached to the storage event, which should fire whenever a change is made in another tab or window.

Listing 5-20. Local storage and events

var storageKey = 'Example';

localStorage.setItem(storageKey, 'Stored value ' + Date.now());

addEvent(window, 'storage', (event: StorageEvent) => {
    console.log(event.key +
        ' "' + event.oldValue + '" changed to "' + event.newValue + '"'),
});

If you run this script in multiple browser tabs, each tab will log the change in local storage except for the tab that initiated the change. This allows you to keep all of the tabs updated with changes in data made in any other tab.

Storage Restrictions

For both session storage and local storage, browsers are likely to follow a series of restrictions and configurations described in the Web Storage specification, once again maintained by the W3C (2014).

Browsers are likely to limit the amount of storage available to a page initially to prevent malicious attempts to exhaust the user’s disk space. The limit applies across subdomains and, when reached, will cause a prompt to be shown to the user asking for permission to increase the allocated storage space. The recommended limit for storage is five megabytes before the user is prompted for permission.

To protect user privacy, browsers are likely to prevent third-party access to storage. This means that you will only be able to access storage on the same domain as it was stored. Browsers can clear out storage based on user preferences (e.g., every time the browser is closed or when it reaches a certain age) and there will also be options available to the user to view and clear storage as well as white list or blacklist sites. It may even be possible for blacklisted sites to be shared across a community, allowing automatic blocking of storage for a domain based on a number of users blacklisting it.

For security reasons, you should avoid using storage from a shared domain as storage would be available to other users of the shared domain. You cannot restrict access to storage by path. For example, the same storage could be accessed by both of the following paths:

IndexedDB

Although session storage and local storage are simple and convenient ways to store small amounts of data in a key/value store, IndexedDB allows much larger volumes of data to be stored in a structured way that allows fast searches using indexes.

IndexedDB is designed to work asynchronously, which means you supply a callback to each method on the API that executes when the operation has completed. A synchronous version of IndexedDB has a specification, but currently no browsers implement this style of the API. It is generally preferable to use asynchronous APIs to avoid blocking the event loop from running on the main thread, so learning to use the asynchronous version of IndexedDB is worth the additional effort.

The IndexedDB API is demonstrated using the Product class shown in Listing 5-21. The Product class has two public properties for productId and name. The productId will be used as the key for items stored in the database.

Listing 5-21. Product.ts

class Product {
    constructor(public productId: number, public name: string) {

    }
}

export = Product;

Listing 5-22 shows an empty ProductDatabase class. This will be expanded to perform database operations such as storing, retrieving, and deleting products. This class will also reduce the dependency on the IndexedDB API in the program code.

Listing 5-22. Empty ProductDatabase.ts

import Product = require('./Product'),

class ProductDatabase {
    constructor(private name: string, private version: number) {
    }
}

export = ProductDatabase;

The ProductDatabase constructor takes the database name and the version number. The version number is used to detect if the database stored locally needs to be upgraded to a new version. Each time you change the schema, you should increment the version number. The version number must be an integer, even though there is no native integer type in JavaScript or TypeScript.

Upgrade Required

A database upgrade is determined by comparing the local version number with the version number in your program. If the program version number is larger than the local version number, an onupgradeneeded event is triggered. The event is also fired if there is no local database. You can specify a method to be executed in the event of an upgrade that handles the schema changes and adds any required data.

Listing 5-23 contains an updated constructor for the ProductDatabase class that issues a request to open the database and adds a listener to the onupgradeneeded event. If an upgrade is needed, the update method is called.

Listing 5-23. ProductDatabase supporting upgrades

import Product = require('./Product'),

class ProductDatabase {

    constructor(private name: string, private version: number) {
        var openDatabaseRequest = indexedDB.open(this.name, this.version);
        openDatabaseRequest.onupgradeneeded = this.upgrade;
    }

    upgrade(event: any) {
        var db = event.target.result;

        // The keyPath specifies the property that contains the id
        var objectStore = db.createObjectStore("products", { keyPath: 'productId' });

        objectStore.createIndex('name', 'name', { unique: false });

        // Example static data
        var products = [
            new Product(1, 'My first product'),
            new Product(2, 'My second product'),
            new Product(3, 'My third product')
        ];

        // Add records
        var productStore = db.transaction('products', 'readwrite').objectStore('products'),
        for (var i = 0; i < products.length; i++) {
            productStore.add(products[i]);
        }
    }
}

export = ProductDatabase;

The update method in this example uses createObjectStore to create a products table. The options argument specifies a keyPath, which tells the database that the objects stored will have a productId property that should be used as the unique key. You can opt to have a key automatically created for you by passing the autoIncrement option with a value of true instead of passing the keyPath property.

The createIndex method adds an index to the name property to make searches by name faster. It is possible to make an index unique, although in the example duplicates have been allowed by setting unique to false. Attempting to create a unique index will fail if the database already contains duplicates.

Finally, a transaction is created on the products object store and used to add products to the database. This step is useful if you need to seed the database with static data.

Listing 5-24 shows the code that instantiates an instance of the ProductDatabase class. Although the constructor assigns the event handler for the onupgradeneeded event, the constructor will complete before the event fires.

Listing 5-24. Instantiating a ProductDatabase

import ProductDatabase = require('./Scripts/ProductDatabase'),

var versionNumber = 1;

var db = new ProductDatabase('ExampleDatabase', versionNumber);

Querying the Database

Because IndexedDB is designed to work asynchronously, some operations seem to require more effort than you might expect. Despite this, it is worth taking advantage of asynchrony—even if the synchronous versions of these operations eventually get implemented by a browser.

Listing 5-25 shows a getProduct method for the ProductDatabase class, which handles the database opening request, transactions, and queries. This allows calling code to simply pass the productId and a callback to process the result.

Listing 5-25. getProduct method

getProduct(productId: number, callback: (result: Product) => void) {
    // Open the database
    var openDatabaseRequest = indexedDB.open(this.name, this.version);

    openDatabaseRequest.onsuccess = () => {
        // The database is open
        var db = openDatabaseRequest.result;

        // Start a transaction on the products store
        var productStore = db.transaction(['products']).objectStore('products'),

        // Request the query
        var query = productStore.get(productId);
        query.onsuccess = () => {
            callback(query.result);
        };
    };
}

The getProduct method creates a request to open the database, supplies a callback to create a transaction, and runs the query when the connection has opened successfully. You can also supply a callback to be executed onerror, which will be called if the database could not be opened. The query request is also supplied with a callback that is passed the result of the query.

To use the product database, Listing 5-26 contains a simple HTML page for the user to enter a product ID and view the result obtained from the database.

Listing 5-26. HTML page

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>IndexedDB</title>
    <link rel="stylesheet" href="app.css" type="text/css" />
</head>
<body>
    <h1>IndexedDB</h1>
    <div>
        <label>Product Id: <input type="number" id="productId" /></label>
    </div>
    <div id="content"></div>
    <script data-main="app" src="/Scripts/require.js"></script>
</body>
</html>

The code to collect the data entered by the user and call the ProductDatabase class is shown in Listing 5-27. The product ID entered into the input is collected using the keyup event and is passed to the getProduct method, along with a callback that displays the result on the web page if there is a matching record.

Listing 5-27. Calling getProduct

import addEvent = require('./Scripts/AddEvent'),
import Product = require('./Scripts/Product'),
import ProductDatabase = require('./Scripts/ProductDatabase'),

var db = new ProductDatabase('ExampleDatabase', 1);

// Wait for entry in the productId input
addEvent(document.getElementById('productId'), 'keyup', function () {
    // Get the id entered by the user, convert to number
    var productId = +this.value;

    // Search the database with the id
    db.getProduct(productId, (product) => {
        document.getElementById('content').innerHTML = product ?
        'The result for product id: ' + product.productId + ' is: ' + product.name :
        'No result';
    });
});

Running this example will confirm that despite some of the code appearing a little complex, the retrieval of records is blisteringly fast because no network round trip is required. The data is also available offline, which means your program can continue to work without a connection.

Adding a New Record

Adding a new record to the database is slightly simpler than obtaining a record with a query, as shown in the previous section, because adding a record requires one less callback. The general pattern is the same, as shown in Listing 5-28, requesting a connection and starting a transaction inside the success callback.

Listing 5-28. addProduct method

addProduct(product: Product) {
    // Open the database
    var openDatabaseRequest = indexedDB.open(this.name, this.version);

    openDatabaseRequest.onsuccess = () => {
        // The database is open
        var db = openDatabaseRequest.result;

        // Start a transaction on the products store
        var productStore = db.transaction('products', 'readwrite').objectStore('products'),

        // Add the product
        productStore.add(product);
    };
}

The product is then stored using the add method, which takes in the product object and automatically finds the productId property to use as the unique key as per the database configuration in Listing 5-23.

The code to call the addProduct method is shown in Listing 5-29. Because the ProductDatabase class has handled the connection request, all the calling code needs to do is supply the new product that is to be stored.

Listing 5-29. Calling addProduct

import Product = require('./Scripts/Product'),
import ProductDatabase = require('./Scripts/ProductDatabase'),

var db = new ProductDatabase('ExampleDatabase', 1);

var newProduct = new Product(4, 'Newly added product'),

db.addProduct(newProduct);

Because the database is available offline, it is possible to store records without a network connection and then later synchronize them to the server when a connection is available. You could use a holding table for the records to synchronize, or flag records to show whether they are synchronized.

Deleting a Record

The method for deleting a record from the database is shown in Listing 5-30. The unique key is used to identify the record to be removed. Once again there is the need to open the database and open a transaction on the product store.

Listing 5-30. deleteProduct method

deleteProduct(productId: number) {
    // Open the database
    var openDatabaseRequest = indexedDB.open(this.name, this.version);

    openDatabaseRequest.onsuccess = (event: any) => {
        // The database is open
        var db = openDatabaseRequest.result;

        // Start a transaction on the products store
        var productStore = db.transaction('products', 'readwrite').objectStore('products'),

        // Add the product
        var deleteRequest = productStore.delete(productId);
    };
}

The calling code to delete a product is shown in Listing 5-31, which is as simple as calling deleteProduct with the unique key for the product.

Listing 5-31. Calling deleteProduct

import Product = require('./Scripts/Product'),
import ProductDatabase = require('./Scripts/ProductDatabase'),

var db = new ProductDatabase('ExampleDatabase', 1);

db.deleteProduct(4);

IDBRequest Interface

The IDBRequest is prevalent in the IndexedDB model. Any request you create against the database supports this interface whether it is indexedDB.open, objectStore.get, objectStore.add, or objectStore.delete.

The beauty of this convention is that you can add a listener to any of these operations to handle both success and error events. Within the event handler you can access the original request object, which contains the following information:

  • result—the result of the request, if available
  • error—the error message, if available
  • source—the index or object store, if applicable to the request
  • transaction—the transaction for the request, if the request is within a transaction; you can undo the changes in the transaction by calling transaction.abort()
  • readyState—either pending or done

In all of these examples, event handlers could have been supplied as shown in Listing 5-32. If you are writing a robust program that uses IndexedDB you should use these events to ensure that database operations are successful and to detect any errors.

Listing 5-32. IDBRequest convention

var deleteRequest = productStore.delete(productId);

deleteRequest.onsuccess = () => {
    console.log('Deleted OK'),
}

deleteRequest.onerror = () => {
    console.log('Failed to delete: ' + deleteRequest.error.name);
}

The examples in this section have all used the TypeScript arrow function syntax. This is not to preserve the meaning of the this keyword, but to reduce the noise of the many nested function declarations that would otherwise be present in the code.

Storage Roundup

This section introduced several options for storage within the browser. Although this has involved a great many examples, it really only described the most common aspects of the storage mechanisms that you may use.

Whatever storage mechanism you use, you cannot guarantee that the data you store will persist long term. All of the storage specifications describe instances where the data may be deleted, including when the user opts to clear it manually. For this reason, any storage supplied by the browser should be treated as potentially volatile.

Another consideration when using browser storage is that many users have different devices that they may use to access your browser-based application. Therefore, synchronization with your server will be required if you want their experience to persist across these devices.

Geolocation

The geolocation API provides a single mechanism for obtaining the user’s location no matter whether the user’s device supports location using the global position system or network-based inference to determine the actual location.

You can only obtain the user’s location if they grant your application permission to access the information, so you will need to supply a fallback mechanism to handle denied requests as well as older browsers and failed lookups. The usual mechanism for obtaining a location when geolocation fails is to allow the user to enter a search term to find their location.

Listing 5-33 shows a one-off location lookup using the getCurrentPosition method. If the request is approved and succeeds the success callback will be called, with an argument containing the position information. The position object contains latitude and longitude and can also contain additional data about altitude, direction, and speed, if available. The output of Listing 5-33 assumes the user is located at the foot of the London Eye.

Listing 5-33. Geolocation getCurrentPosition

function success(pos: Position) {
    console.log('You are here: Lat=' + pos.coords.latitude +
        ' Long=' + pos.coords.longitude +
        ' Altitude=' + pos.coords.altitude +
        ' (Accuracy=' + pos.coords.altitudeAccuracy + ')' +
        ' Heading=' + pos.coords.heading +
        ' Speed=' + pos.coords.speed);
}

navigator.geolocation.getCurrentPosition(success);

// You are here: Lat = 51.5033 Long = 0.1197
// Altitude = 15 (Accuracy = 0)
// Heading = 0 Speed = 0

As well as obtaining a single reading of the user’s position, you can watch the position for changes using the watchPosition method. Listing 5-34 reuses the success callback function from the previous example to listen to changes in the user’s location. The output from this example assumes the user has travelled quickly between the top of the London Eye and the top of The Gherkin in one second, causing a speed of 3,379 meters per second to be registered. The heading is represented by degrees with north being 0, east being 90, south 180, and west 270 degrees.

Listing 5-34. Geolocation watchPosition

function success(pos: Position) {
    console.log('You are here: Lat=' + pos.coords.latitude +
        ' Long=' + pos.coords.longitude +
        ' Altitude=' + pos.coords.altitude +
        ' (Accuracy=' + pos.coords.altitudeAccuracy + ')' +
        ' Heading=' + pos.coords.heading +
        ' Speed=' + pos.coords.speed);
}

var watch = navigator.geolocation.watchPosition(success);

// You are here: Lat = 51.5033 Long = 0.1197
// Altitude = 135 (Accuracy = 15)
// Heading = 0 Speed = 0

// You are here: Lat = 51.5144 Long = 0.0803
// Altitude = 180 (Accuracy = 15)
// Heading = 60 Speed = 3379

If you want to stop tracking the user’s location, you can call the clearWatch method, passing in a reference to the original watchPostion request to end listening to changes in location. The code in Listing 5-35 ends the watch from the previous example.

Listing 5-35. Clearing a watch

navigator.geolocation.clearWatch(watch);

In cases where you need to know that the request for the user’s location has been denied or failed, you can pass an additional callback to be called if the request fails. Listing 5-36 shows an updated call to watchPosition that passes the additional error function. You can call getCurrentPosition with an error callback too.

Listing 5-36. Failing to obtain the location

function success() {
    console.log('Okay'),
}

function error() {
    console.log('Position information not available.'),
}

var watch = navigator.geolocation.watchPosition(success, error);

Geolocation is commonly used to customize a page based on the user’s current location or to store the location as metadata when the user performs an action such as posting a message. Once the user has granted permission for your website, the browser may store this to avoid prompting the user every time they use the web application. The default behavior in most browsers is to remember the permission for pages served over a secure connection but not for unsecure pages.

Sensors

There are several APIs already published for working with sensors from within a browser. This is thanks in part to organizations such as Mozilla and Nokia (among others) pushing for features for smart phones, and the traction HTML, CSS, and JavaScript have on mobile platforms. (The entire user interface of Firefox OS is written in web technologies, as are all of the apps.)

Despite being influenced by mobile devices, the standards for these APIs are being published via the World Wide Web Consortium (W3C), which means they live alongside the existing web standards and can be implemented in browsers regardless of whether the device is mobile. There are likely to be more APIs published than the selection covered in this section, but you will notice from the examples given in the following that there is a distinct pattern to the implementation of sensor APIs.

Many of the APIs featured in this section were originally part of a general System Information API that was proposed by the W3C (2014), but the editors decided to work on individual specifications for each API to speed up the process of writing the standards. For example, a disagreement on the Vibration API could have delayed the Battery Status API if both were part of the same specification.

Battery Status

To get autocompletion and type checking for the battery status API, you will need to supply a type definition containing two interfaces. These interfaces are shown in Listing 5-37. The BatteryManager interface contains the properties and events that make up the battery status API. The Navigator interface extends the existing interface in the TypeScript library to add the battery property.

Listing 5-37. Type definitions for battery status

interface BatteryManager {
    charging: boolean;
    chargingTime: number;
    dischargingTime: number;
    level: number;
    onchargingchange: () => any;
    onchargingtimechange: () => any;
    ondischargingtimechange: () => any;
    onlevelchange: () => any;
}

interface Navigator {
    battery: BatteryManager;
    mozBattery: BatteryManager;
    webkitBattery: BatteryManager;
}

To obtain information from the battery API, you first need to detect the presence of the feature before calling the properties on the battery manager. Listing 5-38 is a complete example using the battery manager to display information on a web page.

Listing 5-38. Battery status

var battery = (<any>navigator).battery || (<any>navigator).mozBattery || 
(<any>navigator).webkitBattery;

if (battery) {
    var output = document.getElementById('content'),

    function updateBatteryStatus() {
        // Gets the battery charge level
        var charge = Math.floor(battery.level * 100) + '%';

        // Detects whether the battery is charging
        var charging = battery.charging ? ' charging' : ' discharging';

        // Gets the time remaining based on charging or discharging
        var timeLeft = battery.charging ?
            ' (' + Math.floor(battery.chargingTime / 60) + ' mins)' :
            ' (' + Math.floor(battery.dischargingTime / 60) + ' mins)';

        output.innerHTML = charge + timeLeft + charging;
    }

    // Update the display when plugged in or unplugged
    battery.onchargingchange = updateBatteryStatus;

    // Update the display when the charging time changes
    battery.onchargingtimechange = updateBatteryStatus;

    // Update the display when the discharging time changes
    battery.ondischargingtimechange = updateBatteryStatus;

    // Update the display when the battery level changes
    battery.onlevelchange = updateBatteryStatus;
}

The battery level is supplied as a value between 0 and 1.0, so you can obtain the percentage charge by multiplying this value by 100. All of the times given in the battery information are supplied in seconds, which you can convert into minutes or hours as required. The charging flag indicates whether the battery is currently connected to a power source.

There are four events that you can subscribe to that allow you to detect a change in battery status. You may be interested in just one or a combination. For example, although the most likely case for using this API is to display the information obtained as shown in the example, you could use the onchargingchange event to sound an alarm if a device is taken off charge, either to warn the user or as a rudimentary security mechanism that detects the device is being stolen. You could also use the battery information to be sensitive to low battery situations—perhaps by throttling your application when the battery is below 20%.

Proximity Sensor

The proximity sensor is a very simple API that determines whether the user is very close to the device. Typically, the sensor is located at the top of a mobile phone, near the phone speaker. When the user holds the phone to their ear, the API detects that something is close to the speaker. When the phone is moved away, the device detects that the user is no longer near.

The primary purpose of this sensor is to hide the screen and disable touch when the user is speaking on the phone and then redisplay the screen when the user moves the phone away from their ear. Despite the humble purpose of the proximity sensor, you may determine a more innovative purpose for it in your program.

The proximity API allows for two different kinds of event: a user proximity event that supplies a property to state whether the user is near, and a device proximity event that supplies a measurement within a range. The device proximity event information will differ based on the specific implementation.

Listing 5-39. Proximity events

import addEvent = require('./AddEvent'),

interface ProximityEvent {
    min: number;
    max: number;
    value: number;
    near: boolean;
}

var output = document.getElementById('content'),

function sensorChange(proximity: ProximityEvent) {
    var distance =
        (proximity.value ? proximity.value + ' ' : '') +
        (proximity.near ? 'near' : 'far'),

    output.innerHTML = distance;
}

// Near or far
addEvent(window, 'userproximity', sensorChange);

// Measurement within a range
addEvent(window, 'deviceproximity', sensorChange);

Unlike the battery sensor, which supplies a manager with properties that can be tested at any time, the proximity API is based on the userproximity and deviceproximity events, which pass an event argument containing the data. If the sensor is not available or the API is not supported on the device, these events will never fire; otherwise the event handler will be called whenever there is a change in the proximity status.

Light Sensor

The ambient light sensor supplies a single reading that represents the current ambient light as measured in lux units. Lux units represent one lumen per square meter, which is a reasonable representation of light intensity as seen by the human eye. A full moon on a clear night can supply up to one lux of light. Office lighting typically ranges from 300 to 500 lux, while a television studio might use 1,000 lux. Direct sunlight can achieve a range from 32,000 to 100,000 lux.

The light sensor API has a devicelight event, which supplies a single value as shown in Listing 5-40.

Listing 5-40. Ambient light sensor

import addEvent = require('./AddEvent'),

interface DeviceLightEvent {
    value: number;
}

var output = document.getElementById('content'),

function sensorChange(data: DeviceLightEvent) {
    output.innerHTML = 'Ambient light reading: ' + data.value;
}

addEvent(window, 'devicelight', sensorChange);

Although the devicelight event in the example supplies the greatest level of granularity, there is also a lightlevel event that returns the more abstract enum values dim, normal, or bright depending on the ambient light.

Motion and Orientation

The motion and orientation API is already contained within the TypeScript standard library, so no additional types need to be declared on top of the existing DeviceMotionEvent type.

The example in Listing 5-41 obtains the motion, measured as the acceleration in meters per second squared and the rotation measured in degrees.

Listing 5-41. Motion and orientation

import addEvent = require('./AddEvent'),

var output = document.getElementById('content'),

function sensorChange(event: DeviceMotionEvent) {
    var motion = event.acceleration;
    var rotation = event.rotationRate;

    output.innerHTML = '<p>Motion :<br />' +
            motion.x + '<br />' +
            motion.y + '<br />' +
            motion.z + '</p>' +
        '<p>Rotation:<br />' +
            rotation.alpha + '<br />' +
            rotation.beta + '<br />' +
            rotation.gamma + '</p>';
}

addEvent(window, 'devicemotion', sensorChange);

The acceleration property is normalized to remove the effects of gravity. This normalization can only take place on devices that have a gyroscope. In the absence of a gyroscope, an additional property named accelerationIncludingGravity is available, which includes an additional measurement of 9.81 on the axis currently facing up/down (or spread between multiple axes if the device is at an angle where no single axis is pointing directly up/down). For example, if the device was flat on its back with the screen facing up, you would get the following values:

  • acceleration: { x: 0, y: 0, z: 0 }
  • accelerationIncludingGravity: { x: 0, y: 0, z: 9.81 }

Temperature, Noise, and Humidity

As you may have noticed in the previous examples, where the sensor supplies a single value, there is a distinct pattern to the way you use the API. In particular, you can update the code in Listing 5-42 to work for light, temperature, noise, or humidity sensor APIs simply by changing the sensorApiName variable.

Listing 5-42. The device API pattern

import addEvent = require('./AddEvent'),

var sensorApiName = 'devicetemperature';

var output = document.getElementById('content'),

addEvent(window, sensorApiName, (data) => {
    output.innerHTML = sensorApiName + ' ' + data.value;
});

The sensorApiName in this example can be changed to any of the following event names and any future event names that follow this implementation pattern.

  • devicehumidity—the value will be the percentage humidity.
  • devicelight—the value is the ambient light in lux.
  • devicenoise—the value is the noise level in dBA.
  • devicetemperature—the value is the temperature in degrees Celsius.

Sensor Roundup

The device sensor APIs show how the lines between web page, web application, and native device are gradually eroding. The battery status information works almost universally—on laptops, tablets, and smart phones—anywhere a browser supports the API. The other APIs are gaining similar levels of adoption, depending mainly on whether a specific device has the hardware required to obtain a reading.

The pattern used throughout the APIs—listening for a specific event triggered on the window object—means that you don’t even need to test the feature before using it. If the API is not available, the event will simply never fire.

Simply put, sensors can be used to supply measurements to the user, but with a little creativity they could be used to provide interesting user interactions, adaptive interfaces, or inventive games. Perhaps you’ll choose to change the theme of the page based on the ambient light, control page elements using motion or rotation, or even log the quality of the user’s sleep using a combination of light, motion, and noise sensors.

Web Workers

JavaScript was designed to run an event loop on a single thread, and this is the model you should typically follow. If you come across a situation that calls for additional threads, you can use web workers. Web workers allow scripts to run on a background thread, which has a separate global context and can communicate back to the task that spawned the thread using events.

To create a new worker, the code to run on a background thread must be contained in a separate JavaScript file. The code in Listing 5-43 shows the code in worker.ts, which will be compiled into the worker.js file that will be spawned on a background thread.

Listing 5-43. worker.ts

declare function postMessage(message: any): void;

var id = 0;

self.setInterval(() => {
    id++;
    var message = {
        'id': id,
        'message': 'Message sent at ' + Date.now()
    };

    postMessage(message);
}, 1000);

The setInterval method in this example is not called on window but on self. This reflects the fact that the worker runs in a separate context with its own scope. The postMessage event is the mechanism for sending information back to the main thread from the worker and any object passed to or from a worker is copied not shared.

The code to create the worker and listen for messages is shown in Listing 5-44. The worker is instantiated with the path to the JavaScript file that contains the worker code. The workerMessageReceived function is attached to the message event and is called whenever the worker posts a message.

Listing 5-44. Creating and using a web worker

import addEvent = require('./AddEvent'),

var worker = new Worker('/Scripts/worker.js'),

function workerMessageReceived(event) {
    var response = event.data;

    console.log('(' + response.id + ') ' + response.message);
};

addEvent(worker, 'message', workerMessageReceived);

If you run this example enough times, you will encounter a frailty in this implementation: the worker starts to run immediately in the background, which means it may start posting messages before the message event handler has been added. This problem would never occur normally in JavaScript as the main thread is not available to process other items in the event loop until a function completes.

If you need to avoid the race condition that can occur when setting up a worker, you can wrap the code inside of the worker in a function and post a message to tell the worker that you have set up the event listener and are ready for it to begin processing. The updated worker code is shown in Listing 5-45. The original setInterval call is wrapped in a function, which is called when the worker receives a start message.

Listing 5-45. Worker that waits for a start signal

declare function postMessage(message: any): void;

var id = 0;

function start() {
    self.setInterval(() => {
        id++;
        var message = {
            'id': id,
            'message': 'Message sent at ' + Date.now()
        };

        postMessage(message);
    }, 1000);
}

self.onmessage = (event) => {
    if (event.data === 'Start') {
        start();
    } else {
        console.log(event.data);
    }
}

When the worker is created, it will no longer run the messaging code until it receives the 'Start' message. Passing the start message to the worker uses the same postMessage mechanism that the worker uses to communicate back to the main thread. By placing the start message after adding the event handler, the race condition is prevented.

Listing 5-46. Signalling the worker to start

import addEvent = require('./AddEvent'),

var worker = new Worker('/Scripts/worker.js'),

function workerMessageReceived(event) {
    var response = event.data;

    console.log('(' + response.id + ') ' + response.message);
};

addEvent(worker, 'message', workerMessageReceived);

worker.postMessage('Start'),

Web workers provide a simple mechanism for processing code on a background thread along with a pattern for safely passing messages between threads. Despite the simplicity, if you find yourself routinely spinning up web workers, you may be using them for the wrong reasons, especially given that long-running operations typically follow the callback pattern without requiring web workers.

If you do find yourself performing a long-running process or calculation, a web worker can allow the event loop to continue processing on the main thread while the number crunching happens in the background.

Packaging Your Program

This section takes a break from the practical examples of interesting APIs to discuss how to package your TypeScript program.

When you switch to TypeScript from JavaScript, it is tempting to transfer your existing packaging strategy to your TypeScript program. It is common to see people switching to TypeScript, using internal modules to organize their program, and adding a build step to combine the code into a single file and minify it before it is included in the final program. This strategy works for programs up to a certain size, but if your program continues to grow, this method of packaging your program cannot scale indefinitely. This is why TypeScript has first-class support for module loading.

If you organize your program using external modules rather than internal modules, you can use a module loader to fetch dependencies as they are needed, loading just the part of the program that you need. This on-demand loading means that, although your program may be hundreds of thousands of lines of code, you can load just the components you need to perform the current operation and load additional modules if (and when) needed.

When you are certain your program will remain small, the bundling and minification strategy may be the right choice, but you can still write your program using external modules and use a tool such as the RequireJS optimizer to combine the output without limiting your future options.

Summary

This chapter has been an epic dash through some diverse but interesting web browser features, from the browser itself to the many APIs that allow you to create interesting and inventive applications. Although there is a lot of information about a large number of features, you can always return to this chapter later to refresh your memory.

Key Points

  • By avoiding unnecessary reflows, your program will appear more responsive.
  • There are multiple methods for finding elements on a web page. Each of them returns different types, although you can use a type assertion to change that type.
  • Constructing a nested set of elements before adding them to the page can be more efficient than adding each in turn.
  • AJAX allows asynchronous calls to the server and allows data in many different formats.
  • WebSockets offer persistent connections with two-way communication and WebRTC allows real-time audio and video streams.
  • You can store data on the local computer using session storage, local storage, or IndexedDB. However, there is no guarantee the data won’t be removed.
  • You can get the user’s location with their permission, and the browser will use the most accurate available method of finding the location.
  • There are a number of sensors that you can access, and they all have a similar implementation pattern.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.31.163