- Appendix A: Appendix B. Selected HTML5 APIs
- Does your Browser Support HTML5?
- Handling Differences in Browsers
- HTML5 Web Messaging API
- Web Workers API
- WebSockets API
- Offline Web Applications
- History API
- Summary
This appendix is a brief review of selected HTML5 APIs. HTML5 is just a commonly used term for a combination of HTML, JavaScript, CSS, and several new APIs that appeared during the last several years. Five years ago people were using the term Web 2.0 to define modern looking applications. These days HTML5 is almost a household name, and we’ll go along with it. But HTML5 is about the same old development in JavaScript plus latest advances in HTML and CSS.
This appendix is more of an overview of selected APIs that are included in HTML5 specification, namely Web Storage, Application Cache, IndexedDB, localStorage, Web Workers, and History API.
Note
|
To understand code samples included in this appendix you have to be familiar with JavaScript and some monitoring tools like Chrome Developer Tools. We assume that you are familiar with the materials covered in Appendix A. |
The majority of the modern Web browsers already support the current version of HTML5 specification, which will become a W3C standard in 2014. The question is if the users of your Web application have a modern browser installed on their device? There are two groups of users that will stick to the outdated browsers for some time:
-
Less technically savvy people may be afraid of installing any new software one their PCs, especially, people of the older generation. "John, after the last visit of our grandson our computer works even slower than before. Please don’t let him install these new fancy browsers here. I just need my old Internet Explorer, access to Hotmail and Facebook".
-
Business users working for large corporations, where all the installations of the software on their PCs is done by a dedicated technical support team. They say, "We have 50000 PCs in or firm. An upgrade from Internet Explorer version 8 to version 9 is a major undertaking. Internal users work with hundreds Web applications on a regular basis. They can install whatever browser they want, but if some of these applications won’t work as expected, the users will flood us with support requests we’re not qualified to resolve . Hence the strategy of using the lowest denominator browser often wins.
Often Web developers need to make both of the above groups of users happy. Take for example online banking - an old couple has to be able to use your Web application from their old PCs otherwise they will transfer their lifetime savings to a different bank which doesn’t require, say the latest version of Firefox installed.
Does it mean that enterprise Web developers shouldn’t even bother using HTML5 that’s not 100% supported? Not at all. This means that a substantial portion of their application’s code will be bloated with if-statements figuring out what this specific Web browser supports and providing several solutions that keep your application on float in any Web browser. This what makes the job of DHTML developers a lot more difficult than that of, say Java or .Net developers who know exactly the VM where their code will work. If you don’t install the Java Runtime of version 1.6 our application won’t work. As simple as that. How about asking Java developers writing applications that will work in any runtime released during the last 10 years? No, we’re not that nasty.
Do you believe it would be a good idea for Amazon or Facebook to re-write their UI in Java? Of course not unless they want to loose most of their customers who will be scared to death after seeing the message of the 20-step Java installer asking for the access to the internals of their computer. Each author of this book is a Java developer, and we love using Java… on the server side. But when it comes to the consumer facing Web applications there are better than Java choices.
The bottom line is that we have to learn how to develop Web applications that won’t require installing any new software on the user’s machines. In the Web browsers it’s DHTML or in the modern terminology it’s HTML5 stack.
In the unfortunate event of needing to support both new and old HTML and CSS implementations you can use HTML5 Boilerplate that is not a framework, but a template for creating a new HTML project that will support HTML5 and CSS3 elements but will work even in the hostile environments of the older browsers. It’s like broadcasting a TV show in HD, but letting the cavemen with the 50-year old black-and-white tubes watching it too.
HTML Boilerplate comes with a simple way to start your project pre-packaged with solutions and workarounds offered by well known gurus in the industry. Make no mistake, your code base may be larger that you wanted - for example, the initial CSS starts with 500 lines accommodating the old and new browsers, but it may be your safety net.
Tip
|
Watch this screencast by Paul Irish, a co-creator of HTML5 Boilerplate. You can also read the current version of the Getting started with HTML5 Boilerplate on Github. |
This appendix is about selected HTML APIs that we find important to understand in Web applications. But before using any of the API’s listed here you want to check if the versions of the Web browsers you have to user support these APIs. The Web site http://caniuse.com will give you the up-to-date information about all major browsers and their versions that do (or don’t) support the API in question. For example, to see which browsers support Worker API visit caniuse.com.
It’s a good practice to include in your code a line that tests if a specific API is supported. For example, if the following if-statement returns false, the Web Worker is not supported and the code should fallback to a single-threaded processing mode:
if (window.Worker) {
// create a Worker instance to execute your
// script in a separate thread
) else{
// tough luck, fallback to a single–threaded mode
}
In Chapter 1 you’ll learn about the feature-detection tool Modernizr that allows to programmatically check if any particular HTML5 API is supported by the browser being used.
if (Modernizr.Worker) {
// create a Worker instance to execute your
// script in a separate thread
)
HTML5 Web Messaging allows you to arrange for communication between different Web pages of the same Web application. More officially, it’s about "communicating between browsing contexts in HTML documents". Web messaging also allows you to work around the "same domain" policy that would result in security error if a browser’s page A has one origin (the combination of URL scheme, host name and port, e.g. http://myserver.com:8080) and tries to access property of a page B that was downloaded from another origin. But with messaging API windows downloaded from different origins can send messages to each other.
The API is pretty straightforward: if a script in the page WindowA
has a reference to WindowB
where you want to send a message, invoke the following method:
myWindowB.postMesage(someData, targetOrigin);
The object referenced by myWindowB
will receive an event object with the content of payload someData
in the event’s property data
. The targetOrigin
specifies the origin where myWindowB
was downloaded from.
Specifying a concrete URI of the destination window in targetOrigin
is the right way to do messaging. This way if a malicious site will try to intercept the message it won’t be delivered since the URI specified in targetOrigin
would be different from the malicious site’s URI. But if you’re absolutely sure that your application is operating in absolutely safe environment, you can specify "*"
as targetOrigin
.
Accordingly, myWindowB
has to define an event handler for processing of this external event message
, for example:
window.addEventListener('message', myEventHanler, false);
function myEventHandler(event){
console.log(`Received something: ` + event.data);
}
Let’s consider an example where an HTML Window creates an iFrame and needs to communicate with it. In particular, the iFrame will notify the main window that it has loaded, and the main window will acknowledge receiving of this message.
The iFrame will have two button emulating the case of some trading system with two buttons: Buy and Sell. When the use clicks on one of these iFrame’s buttons the main window has to confirm receiving of the buy or sell request. Message exchange between the window and iFrame is a snapshot from a Chrome browser where Developers Tools panel shows the output on the console after the iFrame was loaded and the user clicked on the Buy and Sell buttons.
The source code of this example is shown next. It’s just two HTML files: mainWindow.html and myFrame.html. Here’s the code of mainWindow.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>The main Window</title>
</head>
<body bgcolor="cyan">
<h1>This is Main Window </h1>
<iframe id="myFrame">
<p>Some page content goes here</p>
</iframe>
<script type="text/javascript">
var theiFrame;
function handleMessage(event) { // (1)
console.log('Main Window got the message ' +
event.data );
// Reply to the frame here
switch (event.data) { // (2)
case 'loaded':
theiFrame.contentWindow.postMessage("Hello my frame! Glad you loaded! ",
event.origin); // (3)
break;
case 'buy':
theiFrame.contentWindow.postMessage("Main Window confirms the buy request ",
event.origin);
break;
case 'sell':
theiFrame.contentWindow.postMessage("Main Window confirms the sell request. ",
event.origin);
break;
}
}
window.onload == function() { // (4)
window.addEventListener('message', handleMessage, false);
theiFrame == document.getElementById('myFrame');
theiFrame.src == "myFrame.html";
}
</script>
</body>
</html>
-
This function is an event handler for messages received from the iFrame window. The main window is the parent of iFrame, and whenever the latter will invoke
parent.postMessage()
this even handler will be engaged. -
Depending on the content of the message payload (
event.data
) respond back to the sender with acknowledgment. If the payload isloaded
, this means that the iFrame has finished loading. If it’sbuy
orsell
- this means that the corresponding button in the iFrame has been clicked. As an additional precaution, you can ensure thatevent.origin
has the expected URI before even starting processing received events. -
While this code shows how a window sends a message to an iPrame, you can send messages to any other windows as long as you have a reference to it. For example:
var myPopupWindow == window.open(...); myPopupWindow.postMessage("Hello Popup", "*");
-
On load the main window starts listening to the messages from other windows and loads the content of the iFrame.
Tip
|
To implement error processing add a handler for the window.onerror property.
|
The code of the myFrame.html comes next. This frame contains two buttons Buy and Sell, but there is no business logic to buy or sell anything. The role of these buttons is just to deliver the message to the creator of thie iFrame that it’s time to buy or sell.
<!DOCTYPE html>
<html lang="en">
<body bgcolor="white">
<h2> This is My Frame Window </h2>
<button type="buy" onclick="sendToParent('buy')">Buy</button>
<button type="sell" onclick="sendToParent('sell')">Sell</button>
<script type="text/javascript">
var senderOrigin == null;
function handleMessageInFrame(event) {
console.log(' My Frame got the message from ' +
event.origin +": " + event.data);
if (senderOrigin === null) senderOrigin == event.origin; // (1)
}
window.onload == function(){
window.addEventListener('message', handleMessageInFrame, false);
parent.postMessage('loaded', "*"); // (2)
};
function sendToParent(action){
parent.postMessage(action, senderOrigin); // (3)
}
</script>
</body>
</html>
-
When the iFrame receives the first message from the parent, store the reference to the sender’s origin.
-
Notify the parent that the iFrame is loaded. The target origin is specified as
"*"
here as an illustration of how to send messages without worrying about malicious sites-interceptors - always specify the target URI as it’s done in the functionsendToParent()
. -
Send the message to parent window when the user clicks on Buy or Sell button.
If you need to build a UI of the application from reusable components, applying messaging techniques allows you to create loosely coupled components. Say you’ve created a window for a financial trader. This window receives the data push from the server showing the latest stock prices. When a trader likes the price he may click on the Buy or Sell button to initiate a trade. The order to trade can be is implemented in a separate window and establishing inter-window communications in a loosely coupled manner is really important.
Three years ago O’Reilly has published another book written by us. That book was titled "Enterprise Development with Flex", and in particular, we’ve described there how to apply the Mediator design pattern for creating a UI where components can communication with each other by sending-receiving events from the mediator object. The Mediator pattern remains very important in developing UI using any technologies or programming languages, and importance of the HTML5 messaging can’t be underestimated.
Before and after the trader clicked on the Price Panel is an illustration from that Enterprise Flex book. The Pricing Panel on the left gets the data feed about the current prices of the IBM stock. When the user clicks on Bid or Ask panel, the Pricing Panel just sends the event with the relevant information like JSON-formatted string containing the stock symbol, price, buy or sell flag, date, etc. In this particular case the window that contained these two panels served as a mediator, In HTML5 realm, we can say that the Pricing Panel invokes parent.postMessage()
and shoots the message to the mediator (a.k.a. main window).
The Mediator receives the message and re-post it to its another child - the Order Panel that knows how to place orders to purchase stocks. The main takeaway from such design is that the Pricing and Order panels do not know about each other and are communication by sending-receiving messages to/from a mediator. Such a loosely coupled design allows reuse the same code in different applications. For example, the Pricing Panel can be reused in some portal that’s used by a company executives in a dashboard showing prices without the need to place orders. Since the Price Panel has no string attached to Order Panel, it’s easy to reuse the existing code in such a dashboard.
You’ll see a more advanced example of the inter-component communication techniques that uses Mediator Design Pattern in corresponding section of Chapter 6 of this book.
While this appendix is about selected HTML APIs, we’ve should briefly bring your attention to improvements in the HTML5 <form>
tag too.
It’s hard to imagine an enterprise Web application that is not using forms. At the very minimum the Contact Us form has to be there. A login view is yet another example of the HTML form that almost every enterprise application needs. People fill out billing and shipping forms, they answer long questionnaires while purchasing insurance policies online. HTML5 includes some very useful additions that simplify working with forms.
We’ll start with the prompts. Showing the hints or prompts right inside the input field will save you some screen space. HTML5 has a special attribute placeholder
. The text placed in this attribute will be shown inside the field until it gets the focus - then the text disappears. You’ll see the use of placeholder attribute in Chapter 1 in the logging part of our sample application.
<input id="username" name="username" type="text"
placeholder="username" autofocus/>
<input id="password" name="password"
type="password" placeholder="password"/>
Another useful attribute is autofocus
, which automatically places the focus in the field with this attribute. In the above HTML snippet the focus will be automatically placed in the field username
.
HTML5 introduces a number of new input types, and many of them have huge impact on the look and feel of the UI on mobile devices. Below are brief explanations.
If the input type is date
, in mobile devices it’ll show native looking date pickers when the focus gets into this field. In desktop computers you’ll see a little stepper icon to allow the user select the next or previous month, day, or year without typing. Besides date
you can also specify such types as datetime
, week
, month
, time
, datetime-local
.
If the input type is email
, the main view of the virtual keyboard on your smartphone will include the @ key.
If the input type is url
, the main virtual keyboard will include the buttons .com, ., and /.
The tel
type will automatically validate telephone numbers for the right format.
The color
type opens up a color picker control to select the color. After selection, the hexadecimal representation of the color becomes the value
of this input field.
The input type range
shows a slider, and you can specify its min
and max
values.
The number
type shows a numeric stepper icon on the right side of the input field.
If the type is search
, at the very minimum you’ll see a little cross on the right of this input field. It allows the user quickly clear the field. On mobile devices, bringing the focus to the search field brings up a virtual keyboard with the Search button. Consider adding the attributes placeholder
and autofocus
to the search field.
If the browser doesn’t support new input type, it’ll render it as a text field.
To validate the input values, use the required
attribute. It doesn’t include any logic, but won’t allow submitting the form until the input field marked as required
has something in it.
The pattern
attribute allows you to write a regular expression that ensures that the field contains certain symbols or words. For example, adding pattern="http:.+"
won’t consider the input data valid, unless it starts with http://
followed by one or more characters, one of which has to be period. It’s a good idea to include a pattern
attribute with a regular expression in most of the input fields.
Tip
|
If you’re not familiar with regular expressions, watch the presentation Demistifying Regular Expressions made by Lea Verou at O’Reilly Fluent conference - it’s a good primer on this topic. |
When you start a Web Browser or any other application on your computer or other device, you start a task or a process. A thread is a lighter process within another process. While JavaScript doesn’t support multi-threaded mode, HTML5 has a way to run a script as a separate thread in background.
A typical Web application has a UI part (HTML) and a processing part (JavaScript). If a user clicks on a button, which starts a JavaScript function that runs, say for a hundred mili-seconds, there won’t be any noticeable delays in user interaction. But if the JavaScript will run a couple of seconds, user experience will suffer. In some cases the Web browser will assume that the script became unresponsive and will offer the user to kill it.
Imagine an HTML5 game where a click on the button has to do some major recalculation of coordinates and repainting of multiple images in the browser’s window. Ideally, we’d like to parallelize the execution of UI interactions and background JavaScript functions as much as possible, so the user won’t notice any delays. Another example is a CPU-intensive spell checker function that find errors while the user keeps typing. Parsing JSON object is yet another candidate to be done in background. Web workers are also good at polling a server data.
In other words, use Web workers when you want to be able to run multiple parallel threads of execution within the same task. On a multi-processor computer parallel threads can run on different CPU’s.On a single-processor computer, threads will take turn in getting slices of CPU’s time. Since switching CPU cycles between threads happens fast, the user won’t notice tiny delays in each thread’s execution getting a feeling of smooth interaction.
HTML5 offers a solution for multi-threaded execution of a script with the help of the Worker
object. To start a separate thread of execution you’ll need to create an instance of a Worker
object passing it the name of the file with the script to run in a separate thread, for example:
var mySpellChecker == new Worker("spellChecker.js");
The Worker
thread runs asynchronously and can’t directly communicate with the UI components (i.e. DOM elements) of the browser. When the Worker`s script finishes execution, it can send back a message using the `postMessage()
method. Accordingly, the script that created the worker thread can listen for the event from the worker and process its responses in the event handler. Such event object will contain the data received from the worker in its property data
, for example:
var mySpellChecker == new Worker("spellChecker.js");
mySpellChecker.onmessage == function(event){
// processing the worker's response
document.getElementById('myEditorArea').textContent == event.data;
};
You can use an alternative and preferred JavaScript function addEventListener()
to assign the message handler:
var mySpellChecker == new Worker("spellChecker.js");
mySpellChecker.addEventListener("message", function(event){
// processing the worker's response
document.getElementById('myEditorArea').textContent == event.data;
});
On the other hand, the HTML page can also send any message to the worker forcing it to start performing its duties like start the spell checking process:
mySpellChecker.postMessage(wordToCheckSpelling);
The argument of postMessage()
can contain any object, and it’s being passed by value, not by reference.
Inside the worker you also need to define an event handler to process the data sent from outside. To continue the previous example the spellChecker.js will have inside the code that receives the text to check, performs the spell check, and returns the result back:
self.onmesage == function(event){
// The code that performs spell check goes here
var resultOfSpellCheck == checkSpelling(event.data);
// Send the results back to the window that listens
// for the messages from this spell checker
self.postMessage(resultOfSpellCheck);
};
If you want to run a certain code in the background repeatedly, you can create a wrapper function (e.g. doSpellCheck()
) that internally invokes postMesage()
and then gives such a wrapper to setTimeout()
or setInterval()`to run every second or so: `var timer == setTimout(doSpellCheck, 1000);
.
If an error occurs in a worker thread, your Web application will get a notification in a form of an event, and you need to provide a function handler for onerror
:
mySpellChecker.onerror == function(event){
// The error handling code goes here
};
If a window’s script creates a worker thread for its own use, we call it a dedicated worker. A window creates an event listener, which gets the messages from the worker. On the other hand, the worker can have a listener too to react to the events received from its creator.
A shared worker thread can be used by several scripts as long as they have the same origin. For example, if you want to reuse a spell checker feature in several views of your Web application, you can create a shared worker as follows:
var mySpellChecker == new SharedWorker("spellChecker.js");
Another use case is funneling all requests from multiple windows to the server through a shared worker. You can also place into a shared worker a number of reusable utility function that may be needed in several windows - such architecture can reduce or eliminate repeatable code.
One or more scripts can communicate with a shared worker, and it’s done slightly different that with the dedicated one. Communication is done through the port
property and the start()
method has to be invoked to be able to use postMessage()
first time:
var mySpellChecker == new SharedWorker("spellChecker.js");
mySpellChecker.port.addEventListener("message", function(event){
document.getElementById('myEditorArea').textContent == event.data;
});
mySpellChecker.port.start()
The event handler becomes connected to the port
property, and now you can post the message to this shared worker using the same postMessage()
method.
mySpellChecker.postMessage(wordToCheckSpelling);
Each new script that will connect to the shared worker by attaching an event handler to the port results in incrementing the number of active connections that the shared worker maintains. If the script of the shared worker will invoke port.postMessage("Hello scripts!")
, all listeners that are connected to this port will get it.
Tip
|
if a shared thread is interesting in processing the moments when a new script connects to it, add an event listener to the connect event in the code of the shared worker.
|
If a worker needs to stop communicating with the external world it can call self.close()
. The external script can kill the worker thread by calling the method terminate()
, for example:
mySpellChecker.terminate();
Tip
|
Since the script running inside the Worker thread doesn’t have access to the browser’s UI components, you can’t debug such scripts by printing messages onto browser’s console with console.log() . In Appendix A we’ve used Firefox browser for development, but now we’ll illustrate how to use Chrome Browser Developer Tools, which includes the Workers panel that can be used for debugging the code that’s launched in worker threads. You’ll see multiple examples of using Chrome Developers Tools going forward.
|
To get a more detailed coverage of Web Workers, read the O’Reilly book by Ido Green titled "Web Workers".
Tip
|
When the user switches to another page in a browser and the current Web page loses focus you may want to stop running some processes that would unnecessary use CPU cycles. To catch this moment use the Page Visibility API. |
For many years Web applications were associated with HTTP as the main protocol for communication between Web browsers and servers. HTTP is a request-response based protocol that adds hundreds of additional bytes to the application data being sent between browsers and the servers. WebSocket is not a request-response, but a bi-directional full-duplex socket-based protocol, which adds only a couple of bytes (literally) to the application data . WebSockets can become a future replacement for HTTP, but Web applications that require the near-real-time communications (e.g. financial trading applications, online games or auctions) can benefit from this protocol today. Authors of this book believe that WebSocket API is so important, that we dedicated the entire Chapter 8 of this book to this API. In this section we’ll just introduce this API very briefly.
This is how the WebSockets workflow goes:
-
A Web application tries to establish a socket connection between the client and the server using HTTP only for the initial handshake.
-
If the server supports WebSockets, it switches the communication protocol from HTTP to a socket-based protocol.
-
From this point on both client and server can send messages in both directions simultaneously (i.e. in full duplex mode).
-
This is not a request-response model as both the server and the client can initiate the data transmission which enables the real server-side push.
-
Both the server and the client can initiate disconnects too.
This is a very short description of what WebSocket API is about. We encourage you to read Chapter 8 and find the use of this great API in one of your projects.
The common misconception about Web applications is that they are useless if there is no connection to the Internet. Everyone knows that native application can be written in a way that they have everything they need installed on your device’s data storage - both the application code and the data storage. With HTML5, Web applications can be designed to be functional even when the user’s device is disconnected. The offline version of a Web application may not offer full functionality, but certain functions can still be available.
To be useful in a disconnected mode, HTML-based application needs to have access to some local storage on the device, in which case the data entered by the user in the HTML windows can be saved locally with further synchronization with the server when connection becomes available. Think of a salesman of a pharmaceutical visiting medical offices trying to sell new pills. What if connection is not available at certain point? She can still use her tablet demonstrate the marketing materials and more importantly, collect some data about this visit and save them locally. When the Internet connection becomes available again, the Web application should support automatic or manual data synchronization so the information about the salesman activity will be stored in a central database.
There are two main prerequisites for building offline Web applications. You need local storage, and you need to ensure that the server sends only raw data to the client, with no HTML markup (see Design with Offline Use in Mind). So all these server-side frameworks that prepare the data heavily sprinkled with HTML markup should not be used. For example, the front-end should be developed in HTML/JavaScript/CSS, the back end in your favorite language (Java, .Net, PHP, etc.), and the JSON-formatted data are being sent from the server to the client and back.
The business logic that supports the client’s offline functionality should be developed in JavaScript and run in the Web browser. While most of the business logic of Web applications remains on the server side, the Web client is not as thin as it used to be in legacy HTML-based applications. The client becomes fatter and it can have state.
It’s a good idea to create a data layer in your JavaScript code that will be responsible for all data communications. If the Internet connection is available, the data layer will be making requests to the server, otherwise it’ll get the data from the local storage.
First of all, application’s cache is not related to the Web browser’s cache. It’s main reason for existence is to allow creating applications that can run even if there is no Internet connection available. The user will still go to her browser and enter the URL, but the trick is that the browser will load some previously saved Web pages from the local application cache. So even if the user is not online, the application will start anyway.
If your Web application consists of multiple files, you need to specify which ones have to be present on the user’s computer in the offline mode. A file called Cache Manifest is a plain text file that lists such resources.
Storing some resources in the application cache can be a good idea not only in the disconnected mode, but also to lower the amount of code that has to be downloaded from the server each time the user starts your application. Here’s an example of the file mycache.manifest, which includes one CSS file, two JavaScript files, and one image to be stored locally on the user’s computer:
CACHE MANIFEST
/resources/css/main.css
/js/app.js
/js/customer_form.js
/resources/images/header_image.png
The manifest file has to start with the line CACHE MANIFEST and can be optionally divided into sections. The landing page of your Web application has to specify an explicit reference to the location of the manifest. If the above file is located in the document root directory of your application, the main HTML file can refer to the manifest as follows:
<!DOCTYPE html>
<html lang="en" manifest="/mycache.manifest">
...
</html>
The Web server must serve the manifest file with a MIME type "text/cache-manifest", and you need to refer to the documentation of your Web server to see how to see where to make a configuration change so all files with extension .manifest are served as "text/cache-manifest".
On each subsequent application load the browser makes a request to the server and retrieves the manifest file to see if it has been updated, in which case it reloads all previously cached files. It’s a responsibility of Web developers to modify manifest on the server if any of the cacheable resources changed.
Web browsers have a boolean
property window.navigator.onLine
, which should be used to check if there is no connection to the Internet. The HTML5 specification states that "The navigator.onLine attribute must return false if the user agent will not contact the network when the user follows links or when a script requests a remote page (or knows that such an attempt would fail), and must return true otherwise." Unfortunately, major Web browsers deal with this property differently so you need to do a thorough testing to see if it works as expected with the browser you care about.
To intercept the changes in the connectivity status, you can also assign event listeners to the online
and offline
events, for example:
window.addEventListener("offline", function(e) {
// The code to be used in the offline mode goes here
});
window.addEventListener("online", function(e) {
// The code to synchronize the data saved in the offline mode
// (if any) goes here
});
You can also add the onoffline
and ononline
event handlers to the <body>
tag of your HTML page or to the document
object. Again, test the support of these event in your browsers.
What if the browser’s support of the offline/online events is still not stable? In this you’ll have to write your own script that will periodically make an AJAX call (see Chapter 2) trying to connect to a remote server that’s always up and running, e.g. google.com. If this request fails, it’s a good indication that your application is disconnected from the Internet.
In the past, Web browsers could only store their own cache and application’s cookies on the user’s computer.
Note
|
Cookies are small files (up to 4Kb) that a Web browser would automatically save locally if the server’s HTTPResponse would include them. On the next visit of the same URL, the Web browser would send all non-expired cookies back to the browser as a part of HTTPRequest object. Cookies were used for arranging HTTP session management and couldn’t be considered a solution for setting up a local storage.
|
HTML5 offers a lot more advanced solutions for storing data locally, namely:
-
Web Storage which offers Local Storage for long-term storing data and Session Storage for storing a single session data.
-
IndexedDB: a NoSQL database that stores key-value pairs.
Note
|
There is another option worth mentioning - Web SQL Database. The specification was based on the open source SQLite database. But the work on this specification is stopped and future versions of the browsers may not support it. That’s why we are not going to discuss Web SQL Database in this book. |
Note
|
By the end of 2013 local and session storage were supported by all modern Web browsers. Web SQL database is not supported by Firefox and Internet Explorer and most likely will never be. IndexedDB is the Web storage format of the future, but Safari doesn’t support it yet, so if your main development platform is iOS, you may need to stick to Web SQL database. Consider using a polyfill for indexedDB using Web SQL API called IndexedDBShim. |
Note
|
To get the current status visit caniuse.com and search for the API you’re interested in. |
Important
|
Although Web browsers send cookies to the Web server, they don’t send there the data saved in a local storage. The saved data is used only on the user’s device. Also, the data saved in the local storage never expire. A Web application has to programmatically clean up the storage if need be, which will be illustrated below. |
With window.localStorage
or window.sessionStorage
(a.k.a. Web Storage) you can store any objects on the local disk as key-value pairs. Both objects implement the Storage
interface. The main difference between the two is that the lifespan of the former is longer. If the user reloads the page, the Web browser or restart the computer - the data saved with window.localStorage
will survive while the data saved via window.sessionStorage
won’t.
Another distinction is that the data from window.localStorage
is available for any page loaded from the same origin as the page that saved the data. In case of window.sessionStorage
, the data is available only to the window or a browser’s tab that saved it.
Saving the application state is the main use of the local storage. Coming back to a use case with the pharmaceutical salesman, in the offline mode you can save the name of the person she talked to in a particular medical office and the notes about the conversation that took place, for example:
localStorage.setItem('officeID', 123);
localStorage.setItem('contactPerson', 'Mary Lou');
localStorage.setItem('notes', 'Drop the samples of XYZin on 12/15/2013');
Accordingly, to retrieve the saved information you’d need to use the method getItem()
.
var officeID == localStorage.getItem('officeID');
var contact == localStorage.getItem('contactPerson');
var notes == localStorage.getItem('notes');
This code sample are pretty simple as they store single values. In the real life scenarios we often need to store multiple objects. What it our salesperson has visited several medical offices and needs to save the information about all these visits in the Web Store? For each visit we can create a key-value combination, where a key will include the unique id (e.g. office ID), and the value will be a JavaScript object (e.g. Visit) turned into a JSON-formatted string (see Chapter 2 for details) using JSON.stringify()
.
The following code sample illustrates how to store and retrieve the custom Visit
objects. Each visit to a medical office is represented by on instance of the Visit
object. To keep the code simple, we’ve have not included there any HTML components - its JavaScript functions get invoked and print their output on the browser’s console.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>My Today's Visits</title>
</head>
<body>
<script>
// Saving in local storage
var saveVisitInfo == function (officeVisit) {
var visitStr=JSON.stringify(officeVisit); // (1)
window.localStorage.setItem("Visit:"+ visitNo, visitStr);
window.localStorage.setItem("Visits:total", ++visitNo);
console.log("saveVisitInfo: Saved in local storage " + visitStr);
};
// Reading from local storage
var readVisitInfo == function () {
var totalVisits == window.localStorage.getItem("Visits:total");
console.log("readVisitInfo: total visits " + totalVisits);
for (var i == 0; i < totalVisits; i++) { // (2)
var visit == JSON.parse(window.localStorage.getItem("Visit:" + i));
console.log("readVisitInfo: Office " + visit.officeId +
" Spoke to " + visit.contactPerson + ": " + visit.notes);
}
};
// Removing the visit info from local storage
var removeAllVisitInfo == function (){ // (3)
var totalVisits == window.localStorage.getItem("Visits:total");
for (i == 0; i < totalVisits; i++) {
window.localStorage.removeItem("Visit:" + i);
}
window.localStorage.removeItem("Visits:total");
console.log("removeVisits: removed all visit info");
}
var visitNo == 0;
// Saving the first visit's info
var visit == { // (4)
officeId: 123,
contactPerson: "Mary Lou",
notes: "Drop the samples of XYZin on 12/15/2013"
};
saveVisitInfo(visit);
// Saving the second visit's info // (5)
visit == {
officeId: 987,
contactPerson: "John Smith",
notes: "They don't like XYZin - people die from it"
};
saveVisitInfo(visit);
// Retrieving visit info from local storage
readVisitInfo(); // (6)
// Removing all visit info from local storage
removeAllVisitInfo(); // (7)
// Retrieving visit info from local storage - should be no records
readVisitInfo(); // (8)
</script>
</body>
</html>
-
The function
saveVisitInfo()
uses JSON object to turn the visit object into a string withJSON.stringify()
, and then saves this string in the local storage. This function also increments the total number of visits and saves it in the local storage under the keyVisits:total
. -
The function
readVisitInfo()
gets the total number of visits from the local storage and then reads each visit record recreating the JavaScript object from JSON srting usingJSON.parse()
. -
The function
removeAllVisitInfo()
reads the number of visit records, removes each of them, and then removed theVisits:total
too. -
Creating and saving the first visit record
-
Creating and saving the second visit record
-
Reading saved visit info
-
Removing saved visit info. To remove the entire content that was saved for a specific origin call the method
localStorage.clear()
. -
Re-reading visit info after removal
Chrome’s console after running the Visits sample shows the output on the console of Chrome Developers Tools. Two visit records were saved in local storage, then they were retrieved and removed from the storage. Finally, the program attempted to read the value of the previously saved Visits:total
, but it’s null now - we’ve removed from the localStorage
all the records related to visits.
Tip
|
If you are interested in intercepting the moments when the content of local storage gets modified, listen to the DOM storage event, which carries the old and new values and the URL of the page whose data is being changed.
|
Tip
|
Another good example of a use case when locaStorage becomes handy is when the user is booking air tickets using more than one browser’s tab.
|
The sessionStorage
life is short - it’s only available for a Web page while the browser stays open. If the user decides to refresh the page, the sessionStorage
will survive, but opening a page in a new browser’s tab or window will create a new sessionStorage
object. Working with the session storage is pretty straightforward, for example
sessionStorage.setItem("userID","jsmith");
var userID == sessionStorage.getItem("userID");
Chrome Developer Tools include the tab Resources that allows browsing the local or session storage if a Web page uses it. For example, Browsing local storage in Chrome Developer Tools shows the storage used by cnn.com.
Both localStorage
and sessionStorage
are subject to the same-origin policy, meaning that saved data will be available only for the Web pages that came from the same host, port and via the same protocol.
Both localStorage
and sessionStorage
are browser-specific. For example, if the Web application stored the data from Firefox, the data won’t be available if the user opens the same application from Safari.
The APIs from Web Storage specification are pretty simple to use, but their major drawbacks are that they don’t give you a way to put any structure to the stored data, you always have to store strings, and the API is synchronous, which may case delays in the user interaction when your application is accessing the disk.
There is no actual limits for the size of local storage, but the browsers usually default this size to 5Mb. If the application tries to store more data that the browser permits, the `QUOTA_EXCEEDED_ERR`exception will be thrown - always use the try-catch blocks when saving data.
Even if the user’s browser allows increasing this setting (e.g. via about:config URL in Firefox), access to such data may be slow. Consider using File API or IndexedDB that will be introduced in the in the next section.
Indexed Database API (a.k.a. IndexedDB) is a solution based on the NoSql database. Like with the Storage
interface, IndexedDB stores data as key-value pairs, but it also offers transactional handling of objects. IndexedDB creates indexes of the stored objects for fast retrieval.
With Web Storage you can only store strings, and we had to do these tricks with JSON stingify()
and parse()
to give some structure to these strings. With IndexedDB you can directly store and index regular JavaScript objects.
IndexedDB allows you to accesses data asynchronously, so there won’t be UI freezes while accessing large objects on disk. You make a request to the database and define the event handlers that should process errors or result when ready. IndexedDB uses DOM events for all notifications. Success events don’t bubble, while error events do.
The users will have a feeling that the application is pretty responsive, which wouldn’t be the case if you’ll be saving several megabytes of data with Web Storage API. Similarly to Web Storage, access to the IndexedDB databases is regulated by the same origin policy.
Tip
|
In the future, Web browsers may implement synchronous IndexedDB API to be used inside Web workers. |
Since not every browser supports IndexedDB yet, you can use Modernizr (see Chapter 1) to detect if your browsers supports it. If it does, you still may need to account for the fact that browser vendors name the IndexedDB related object differently. Hence to be on the safe side, at the top of your script include the statements to account for the prefixed vendor-specific implementations of indexedDB
and related objects:
var medicalDB == {}; // just an object to store references
medicalDB.indexedDB == window.indexedDB || window.mozIndexedDB
|| window.msIndexedDB || window.webkitIndexedDB ;
if (!window.indexedDB){
// this browser doesn't support IndexedDB
} else {
medicalDB.IDBTransaction == window.IDBTransaction || window.webkitIDBTransaction;
medicalDB.IDBCursor == window.IDBCursor || window.webkitIDBCursor;
medicalDB.IDBKeyRange == window.IDBKeyRange || window.webkitIDBKeyRange;
}
In the above code snippet the IDBKeyRange
is an object that allows to restrict the range for the continuous keys while iterating through the objects. IDBTransaction
is an implementation of transaction support. The IDBCursor
is an object that represents a cursor for traversing over multiple objects in the database.
IndexedDB doesn’t require you to define a formal structure of your stored objects - any JavaScript object can be stored there. Not having a formal definition of a database scheme is an advantage comparing to the relational databases where you can’t store the data until the structure of the tables is defined.
Your Web application can have one or more databases, and each of them can contain one or more object stores. Each of the object stores will contain similar objects, e.g. one store is for salesman’s visits, while another stores upcoming promotions.
Every object that you are planning to store in the database has to have one property that plays a role similar to a primary key in a relational database. You have to decide if you want to maintain the value in this property manually, or use the the autoIncrement
option where the values to this property will be assigned automatically. Coming back to our Visits example, you can either maintain the unique values of the officeId
on your own or create a surrogate key that will be assigned by IndexedDB. The current generated number to be used as surrogate keys never decreases, and starts with the value of 1 in each object store.
Similarly to relational databases you create indexes based on the searches that you run often. For example, if you need to search on the contact name in the Visits store, create an index on the property contactPerson
of the Visit
objects. But if in relational databases creation of indexes is done for performance reasons, with IndexedDB you can’t run a query unless the index on the relevant property exists. The following code sample shows how to connect to the existing or create a new object store Visits
in a database called Medical_DB.
var request == medicalDB.indexedDB.open('Medical_DB'); // (1)
request.onsuccess == function(event) { // (2)
var myDB == request.result;
};
request.onerror == function (event) { // (3)
console.log("Can't access Medical_DB: " + event.target.errorCode);
};
request.onupgradeneeded == function(event){ // (4)
event.currentTarget.result.createObjectStore ("Visits",
{keypath: 'id', autoIncrement: true});
};
-
The browser invokes the method
open()
asynchronously requesting to establish the connection with the database. It doesn’t wait for the completion of this request, and the user can continue working with the Web page without any delays or interruptions. The methodopen()
returns an instance of theIDBRequest
object. -
When the connection is successfully obtained, the
onsuccess
function handler will be invoked. The result is available through theIDBRequest.result
property. -
Error handling is done here. The event object given to the
onerror
handler will contain the information about the error. -
The
onupgradeneeded
handler is the place to create or upgrade the storage to a new version. This is explained next.
Tip
|
There are several scenarios to consider while deciding if you need to use the autoIncrement property with the store key or not. Kristof Degrave described the article http://www.kristofdegrave.be/2012/02/indexed-db-to-provide-key-or-not-to.html[Indexed DB: To provide a key or not to provide a key.
|
In the world of traditional DBMS servers, when the database structure has to be modified, the DBA will do this upgrade, the server will be restarted, and the users will work with the new version of the database. With IndexedDB it works differently. Each database has a version, and when the new version of the database (e.g. Medical_DB) is created, the onupgradeneeded
is dispatched, which is where object store(s) are created. But if you already had object stores in the older version of the database, and they don’t need to be changed - there is no need to re-create them.
After successful connection to the database, the version number is available in IDBRequest.result.version
property. The starting version of any database is 1.
The method open()
takes a second parameter - the database version to be used. If you don’t specify the version - the latest one will be used. The following line shows how the application’s code can request connect to the version 3 of the database Medical_DB
:
var request == indexedDB.open('Medical_DB',3);
If the user’s computer has already the Medical_DB
database of one of the earlier versions (1 or 2), the onupgradeneeded
handler will be invoked. The initial creation of the database is triggered the same way - the absence of the database also falls under the "upgrade is needed" case, and the onupgradeneeded
handler has to invoke the createObjectStore()
method. If upgrade is needed, the onupgradeneeded
will be invoked before the onsuccess
event.
The following code snippet creates a new or initial version of the object store Visits
, requesting auto-generation of the surrogate keys named id
. It also creates indexes to allow search by office ID, contact name and notes. Indexes are updated automatically as soon as the Web application makes any changes to the stored data. If you wouldn’t create indexes, you’d be able to look up objects only by the value of key.
request.onupgradeneeded == function(event){ // (4)
var visitsStore ==
event.currentTarget.result.createObjectStore ("Visits",
{keypath='id',
autoIncrement: true
});
visitsStore.createIndex("officeIDindex", "officeID",
{unique: true});
visitsStore.createIndex("contactsIndex", "contactPerson",
{unique: false});
visitsStore.createIndex("notesIndex", "notes",
{unique: false});
};
Note that while creating the object store for visits, we could have used a unique property officeID
as a keypath
value by using the following syntax:
var visitsStore ==
event.currentTarget.result.createObjectStore ("Visits",
{keypath='officeID'});
The event.currentTarget.result
(as well as IDBRequest.result
) points at the instance of the IDBDatabase
object, which has a number of useful properties such as name
, that contains the name of the current database and the array objectStoreNames
, which has the names of all object stores that exist in this database. Its property version
has the database version number. If you’d like to create a new database, just call the method open()
specifying the version number that’s higher than the current one.
To remove the existing database, call the method indexedDB.deleteDatabase()
. To delete the existing object store invoke indexedDB.deleteObjectStore()
.
Warning
|
IndexedDB doesn’t offer a secure way of storing data. Anyone who has access to the user’s computer can get a hold of the data stored in IndexedDB. Do not store any sensitive data locally. Always use secure "https" protocol with your Web application. |
Transaction is a logical unit of work. Executing several database operation in one transaction guarantees that the changes will be committed to the database only if all operations finished successfully. If at least one of the operations fails, the entire transaction will be rolled back (undone). IndexDB supports three transaction modes: readonly
, readwrite
, and versionchange
.
To start any manipulations with the database you have to open a transaction in one of these modes. The readonly
transaction (the default one) allows multiple scripts to read from the database concurrently. This statement may raise a question - why would the user need a concurrent access to his local database is he’s the only user of the application on his device? The reason being that the same application can be opened in more than one tab, or spawning more than one worker thread that need to access the local database. The readonly
is the least restrictive mode and more than one script can open a readonly
transaction.
If the application needs to modify or add objects to the database, open transaction in the readwrite
mode - only one script can have it open on any particular object store. But you can have more than one readwrite
transactions open at the same time on different stores. And if the database/store/index creation or upgrade has to be done, use the versionchange
mode.
When a transaction is created, you should assign listeners to its complete
, error
, and abort
events. If the complete
event is fired, transaction is automatically commited - manual commits are not supported. If the error
event is dispatched, the entire transaction is rolled back. Calling the method abort()
will fire the abort
event and will roll back transaction too.
Typically, you should open the database and in the onsuccess
handler create a transaction. Then open a transaction by calling the method objectStore()
and perform data manipulations. In the next section you’ll see how to add objects to an object store using transactions.
The following code snippet creates the transaction that allows updates of the store Visits
(you could create a transaction for more than one store) and add two visit object by invoking the method add()
:
request.onsuccess == function(event) { // (1)
var myDB == request.result;
var visitsData == [{ // (2)
officeId: 123,
contactPerson: "Mary Lou",
notes: "Drop the samples of XYZin on 12/15/2013"
},
{
officeId: 987,
contactPerson: "John Smith",
notes: "They don't like XYZin - people die from it"
}];
var transaction == myDB.transaction(["Visits"],
"readwrite"); // (3)
transaction.oncomplete == function(event){
console.log("All visit data have been added);
}
transaction.onerror == function(event){
// transaction rolls back here
console.log("Error while adding visits");
}
var visitsStore == transaction.objectStore("Visits"); // (4)
for (var i in visitsData) {
visitsStore.add(visitsData[i]); // (5)
}
-
The database opened successfully.
-
Create a sample array of
visitsData
to illustrate adding more than one object to an object store. -
Open a transaction for updates and assign listeners for success and failure. The first argument is an array of object stores that the transaction will span (only
Visits
in this case). When all visits are added thecomplete
event is fired and transaction commits. If adding of any visit failed, theerror
event is dispatched and transaction rolls back. -
Get a reference to the object store
visits
. -
In a loop, add the data from the array
visitsData
to the object storeVisits
.
Note
|
In the above code sample, the each object that represents a visit has a property notes , which is a string. If later on you’ll decide to allow storing more than one note per visit, just turn the property notes into an array in your JavaScript - no changes in the object stores is required.
|
The method put()
allows you to update an existing object in a record store. It takes two parameters: the new object and the key of the existing object to be replaced, for example:
var putRequest == visitsStore.put({officeID: 123, contactName: "Mary Lee"}, 1);
To remove all objects from the store use the method clear()
. To delete an object specify its id:
var deleteRequest == visitsStore.delete(1);
Tip
|
You can browse the data from your IndexedDB database in Chrome Developer Tools under the tab Resources (see Browsing local storage in Chrome Developer Tools). |
IndexedDB doesn’t support SQL. You’ll be using cursors to iterate through the object store. First, you open the transaction. Then you open invoke openCursor()
on the object store. While opening the cursor you can specify optional parameters like the range of object keys you’d like to iterate and the direction of the cursor movement: IDBCursor.PREV
or IDBCursor.NEXT
. If none of the parameters is specified, the cursor will iterate all objects in the store in the ascending order. The following code snippet iterates through all Visit objects printing just contact names.
var transaction == myDB.transaction(["visits"], "readonly");
var visitsStore == transaction.objectStore("Visits");
visitsStore.openCursor().onsuccess == function(event){
var visitsCursor == event.target.result;
if (visitsCursor){
console.log("Contact name: " + visitCursor.value.contactPerson);
visitsCursor.continue();
}
}
If you want to iterate through a limited key range of objects you can specify the from-to values. The next line creates a cursor for iterating the first five objects from the store:
var visitsCursor == visitsStore.openCursor(IDBKeyRange.bound(1, 5));
You can also create a cursor on indexes - this allows working with sorted sets of objects. In one of the earlier examples we’ve created an index on officeID
. Now we can get a reference to this index and create a cursor on the specified range of sorted office IDs as in the following code snippet:
var visitsStore == transaction.objectStore("visits");
var officeIdIndex == visitsStore.index("officeID");
officeIdIndex.openCursor().onsuccess == function(event){
var officeCursor == event.target.result;
// iterate through objects here
}
To limit the range of offices to iterate through, you could open the cursor on the officeIdIndex
differently. Say you need to create a filter to iterate the offices with the numbers between 123 and 250. This is how you can open such a cursor:
officeIdIndex.openCursor(IDBKeyRange.bound(123, 250, false, true);
The false
in the third argument of bound()
means that 123 should be included in the range, and the true
in the fourth parameter excludes the object with officeID=250
from the range. The methods lowerbound()
and upperbound()
are other variations of the method bound()
- consult the online documentation for details.
If you need to fetch just one specific record, restrict the selected range to only one value using the method only()
:
contactNameIndex.openCursor(IDBKeyRange.only("Mary Lou");
Let’s bring together all of the above code snippets into one runnable HTML file. While doing this, we’ll be watching the script execution in Chrome Developer Tools panel. We’ll do it in two steps. The first version of this file will create a database of a newer version than the one that currently exists on the user’s device. Here’s the code that creates the database Medical_DB with an empty object store Visits:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>My Today's Visits With InsexedDB</title>
</head>
<body>
<script>
var medicalDB == {}; // just an object to store references
var myDB;
medicalDB.indexedDB == window.indexedDB || window.mozIndexedDB
|| window.msIndexedDB || window.webkitIndexedDB ;
if (!window.indexedDB){
// this browser doesn't support IndexedDB
} else {
medicalDB.IDBTransaction == window.IDBTransaction || window.webkitIDBTransaction;
medicalDB.IDBCursor == window.IDBCursor || window.webkitIDBCursor;
medicalDB.IDBKeyRange == window.IDBKeyRange || window.webkitIDBKeyRange;
}
var request == medicalDB.indexedDB.open('Medical_DB', 2); // (1)
request.onsuccess == function(event) {
myDB == request.result;
};
request.onerror == function (event) {
console.log("Can't access Medical_DB: " + event.target.errorCode);
};
request.onupgradeneeded == function(event){
event.currentTarget.result.createObjectStore ("Visits",
{keypath:'id', autoIncrement: true}); // (2)
};
</script>
</body>
</html>
-
This version of the code is run when the user’s computer already had a database Medical_DB: initially we’ve invoked
open()
without the second argument. Running the code specifying 2 as the version caused invocation of the callbackonupgradeneeded
even before theonsuccess
was called. -
Create an empty object store
Visits
Chrome’s console after running the Visits sample shows the screen shot from the Chrome Developer Tools at the end of processing the success
event. Note the Watch Expression section on the right. The name of the database is Medical_DB, its version number is 2, and the IDBDatabase
property objectStoreNames
shows that there is one object store named Visits.
The next version of our sample HTML file will populate the object store Visits with some data, and then will iterate through all the Visit objects and display the values of their properties on the console.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>My Today's Visits With InsexedDB</title>
</head>
<body>
<script>
var medicalDB == {}; // just an object to store references
var myDB;
medicalDB.indexedDB == window.indexedDB || window.mozIndexedDB
|| window.msIndexedDB || window.webkitIndexedDB ;
if (!window.indexedDB){
// this browser doesn't support IndexedDB
} else {
medicalDB.IDBTransaction == window.IDBTransaction || window.webkitIDBTransaction;
medicalDB.IDBCursor == window.IDBCursor || window.webkitIDBCursor;
medicalDB.IDBKeyRange == window.IDBKeyRange || window.webkitIDBKeyRange;
}
var request == medicalDB.indexedDB.open('Medical_DB', 2);
request.onsuccess == function(event) {
myDB == request.result;
var visitsData == [{
officeId: 123,
contactPerson: "Mary Lou",
notes: "Drop the samples of XYZin on 12/15/2013"
},
{
officeId: 987,
contactPerson: "John Smith",
notes: "They don't like XYZin - people die from it"
}];
var transaction == myDB.transaction(["Visits"],
"readwrite");
transaction.oncomplete == function(event){
console.log("All visit data have been added.");
readAllVisitsData(); // (1)
}
transaction.onerror == function(event){
// transaction rolls back here
console.log("Error while adding visits");
}
var visitsStore == transaction.objectStore("Visits");
visitsStore.clear(); // (2)
for (var i in visitsData) {
visitsStore.add(visitsData[i]);
}
};
request.onerror == function (event) {
console.log("Can't access Medical_DB: " + event.target.errorCode);
};
request.onupgradeneeded == function(event){
event.currentTarget.result.createObjectStore ("Visits",
{keypath:'id', autoIncrement: true});
};
function readAllVisitsData(){
var readTransaction == myDB.transaction(["Visits"], "readonly");
readTransaction.onerror == function(event){
console.log("Error while reading visits");
}
var visitsStore == readTransaction.objectStore("Visits");
visitsStore.openCursor().onsuccess == function(event){ // (3)
var visitsCursor == event.target.result;
if (visitsCursor){
console.log("Contact name: " +
visitsCursor.value.contactPerson +
", notes: " +
visitsCursor.value.notes);
visitsCursor.continue(); // (4)
}
}
}
</script>
</body>
</html>
-
After the data store is populated and transaction is commited, invoke the method to read all the objects from the Visits store.
-
Remove all the objects from the store Visits before populating it with the data from the array
VisitsData
. -
Open the cursor to iterate through all visits
-
Move the cursor’s pointer to the next object after printing the contact name and notes in the console.
Chrome’s console after reading the first Visit object shows the screenshot from Chrome Developer Tools when the debugger stopped in readAllVisitsData()
right after reading both objects from the Visits store. The console output is shown at the bottom. Note the content of the visitsCursor on the right. The cursor is moving forward (the next
direction), and the value
property points at the object at cursor. The key
value of the object is 30. It’s auto-generated, and on each run of this program you’ll see a new value since we clean the store and re-insert the objects, which generates the new keys.
This concludes our brief introduction to IndexedDB. Those of you who have experience in working with relational databases may find the querying capabilities of IndexedDB rather limited comparing to powerful relational databases like Oracle or MySQL. On the other hand, IndexedDB is pretty flexible - it allows you to store and look up any JavaScript objects without worrying about creating a database schema first. At the time of this writing there are no books dedicated to IndexedDB. For up to date information refer to IndexedDB online documentation at Mozilla Developer Network.
To put is simple, History API is about ensuring that the Back/Forward buttons on the browser toolbar can be controlled programmatically. Each Web browser has the window.history
object. The History API is not new to HTML5. The history
object has been around for many years, with methods like back()
, forward()
, and go()
. But HTML5 adds new methods pushState()
and replaceState()
, which allow to modify the browser’s address bar without reloading the Web page.
Imagine a Single Page Application (SPA) that has a navigational menu to open various views as based on the user’s interaction. Since these views represents some URLs loaded by making AJAX calls from your code, the Web browser still shows the original URL of the home page of your Web application.
The perfect user would always navigate your application using the menus and controls you provided, but what if she clicks on the Back button of the Web browser? If the navigation controls were not changing the URL in the browser’s address bar, the browser obediently will show the Web page that the user has visited before even launching your application, which is most likely not what she intended to do. History API allows to create more fine grained bookmarks that define specific state within the Web page.
Tip
|
Not writing any code that would process clicks on the Back and Forward buttons is the easiest way to frustrate your users. |
Imagine you have a customer-management application that has a URL http://myapp.com. The user clicked on the menu item Get Customers, which made an AJAX call loading the customers. You can programmatically change the URL on the browser’s address line to be http://myapp.com/customers without asking the Web browser to make a request to this URL. You do this by invoking the pushState()
method.
The browser will just remember that the current URL is http://myapp.com/customers, while the previous was http://myapp.com. So pressing the Back button would change the address back to http://myapp.com, and not some unrelated Web application. The Forward button will also behave properly as per the history chain set by your application.
The pushState()
takes three arguments (the values from the first two may be ignored by some Web browsers):
-
The application specific state to be associated with the current view of the Web page
-
The title of the current view of the Web page.
-
The suffix to be associated with the current view of the page. It’ll be added to the address bar of the browser.
<head>
<meta charset="utf-8">
<title>History API</title>
</head>
<body>
<div id="main-container">
<h1>Click on Link and watch the address bar...</h1>
<button type="button" onclick="whoWeAre()">Who we are</button>
<!--(1)-->
<button type="button" onclick="whatWeDo()">What we do</button>
</div>
<script>
function whoWeAre(){
var locationID== {locID: 123, // (2)
uri: '/whoweare'};
history.pushState(locationID,'', 'who_we_are' ); // (3)
}
function whatWeDo(){
var actionID== {actID: 123, // (4)
uri: '/whatwedo'};
history.pushState(actionID,'', 'what_we_do' ); // (5)
}
</script>
</body>
</html>
-
On a click of the button call the event handler function. Call the
pushState()
to modify the browser’s history. Some other processing like making an AJAX request to the server can be donein whoWeAre()
too. -
Prepare the custom state object to be used in server side requests. The information about who we are depends on location id.
-
Calling
pushState()
to remember the customer id, the page title is empty (not supported yet), and adding the suffix /who_we_are will serve as a path to the server-side REST request. -
Prepare the custom state object to be used in server side requests. The information about what we do depends on customer id.
-
Calling
pushState()
to remember the customer id, the page titleis empty (not supported yet), and adding the suffix /what_we_do will serve as a path to the server-side REST request.
This above sample is a simplified example and would require more code to properly form the server request, but our goal here is just to clearly illustrate the use of History API.
Testing pushState() depicts the view after the user clicked on the button Who We Are. The URL now looks as http://127.0.0.1:8020/HistoryAPI/who_we_are, but keep in mind that if you try to reload the page while this URL is shown, the browser will give you a Not Found error and rightly so. There is no resource that represents the URL that ends with who_we_are - it’s just the name of the view in the browser’s history.
Using the replaceState()
you can technically "change the history". We are talking about the browser’s history, of course.
But changing the URL when the user clicks on the Back or Forward button is just the half of the job to be done. The content of the page has to be refreshed accordingly. The browser dispatches the event window.popstate
whenever the browser’s navigation history changes either on initial page load, as a result of clicking on the Back/Forward buttons, or by invoking history.back()
or history.forward()
.
Your code has to include an event handler function that will perform the actions that must be dome whenever the application gets into the state represented by the current suffix, e.g. make a server request to retrieve the data associated with the state who_we_are. The popstate
event will contain a copy of the history’s entry state object. Let’s add the following event listener to the <script>
part of the code sample from previous section:
addEventListener('popstate',function (evt){
console.log(evt);
});
Monitoring popState with Chrome Developers Tool depicts the view of the Chrome Developers Tool when the debugger stopped in the listener of the popstate
event after the user clicked on the buttons Who We Are, then What We Do, and then the browser’s button Back. On the right hand side you can see that the event object contains the evt.state
object with the right values of locID
and uri
. In the real world scenario these values could have been used in, say AJAX call to the server to recreate the view for the location ID 123.
Tip
|
If you’ll run into a browser that doesn’t support HTML5 History API, consider using the History.js library. |
We’ve included this sidebar in this appendix, even though it’s not API. But we’re talking about HTML here and don’t want to miss this important feature of the HTML5 specification - you can add to any HTML tag any number of custom non-visible attributes as long as they start with data-
and have at least one character after the hyphen. For example, this is absolutely legal in HTML5:
<ol>
<li data-phone="212-324-6656">Mary</li>
<li data-phone="732-303-1234">Anna</li>
...
</ol>
Behind the scenes, a custom framework can find all elements that have the data-phone
attribute and generate some additional code for processing of the provided phone number. If this example doesn’t impress you, wait till Chapter 10, where you’ll learn how to use jQuery Mobile. The creators of this library use these data-
attributes in a very smart way.
In this appendix you’ve got introduced to a number of useful HTML5 APIs. You know how to check if a particular API is supported by your Web browser. But what if you are one of many enterprise developers that must use Internet Explorer of the versions earlier than 10.0? Google used to offer a nice solution: Google Chrome Frame, which was a plugin for Internet Explorer.
The users had to install Chrome Frame on their machines, and Web developers just needed to add the following line to their Web pages:
<meta http-equiv="X-UA-Compatible" content="chrome=1" />
After that the Web page rendering would be done by Chrome Frame while your Web application would run in Internet Explorer. Unfortunately, Google decided not to support the Chrome project as of January 2014. They are recommending to prompt the user of your application to upgrade the Web browser, which may not be something that the users will be willing to do. But let’s hope for the best.