Until now, users of web applications have only been able to use applications while connected to the Internet. When offline, web-based email, calendars, and other online tools have been unavailable, and for the most part, continue to be.
While offline, users may still access some portions of sites they have visited by accessing what is in the browser cache, but that is limited and difficult to manage. If a user gets bumped offline in the middle of a process, like writing an email or filling in a form, hitting submit can lead to a loss of all the data entered.
The HTML5 specification provides a few solutions, including local and session storage for storing data locally, and an offline application HTTP cache for ensuring applications are available even when the user is offline. HTML5 contains several features that address the challenge of building web applications that don’t lose all functionality while offline, including indexDB, offline application caching APIs, connection events, status, as well as the localStorage and sessionStorage APIs.
One thing you may want to know when implementing offline features
is if the user is indeed connected to the Internet. HTML5 defines an
onLine
property on the Navigator object so you can determine whether the
user is currently online:
var isOnline = navigator.onLine;
This will return true or false. Note that if it returns true, it could mean the user is on an intranet and does not necessarily mean the user has access to the Internet.
If you want to create web-based games that are able to compete with native games on mobile devices, you have to ensure that players can access your game even when they are not online. We want CubeeDoo players to be able to play whether they’re at home on their WiFi, camping in the Mojave desert (why enjoy nature when you could be flipping cards?), or even flying over the Pacific. Application cache enables you to create web-based applications that are accessible even when the user is not currently online.
In the past, desktop browsers have only been able to save the HTML file and associated media to a local folder. This method works for static content, but it never updates and is generally a bad user experience.
With the ubiquity of web-based applications, it is more important than ever that web applications are accessible when the user is offline. While browsers have been able to cache the components of a website, HTML5 addresses some of the difficulties of being offline with the application cache (“AppCache”) API.
AppCache allows you to specify which files should be cached and made available offline, enabling your website to work correctly when your user is offline even if they reload a page. Using the AppCache interface gives your web application the following advantages: (1) offline browsing, (2) faster reloads, and (3) reduced server load. With application cache offline browsing, your entire site can be navigable even when a user is offline.
AppCache on most mobile browsers enables the local storage of up to 5 MB (or limits you to 5 MB, depending on your perspective). Different browsers may have different limits. While users can change these limits in their browser preferences, you should always code to browser default values, unless all your users are power users.
For AppCache to work, you must include the manifest
attribute in the opening <html>
tag, the value of which is the URL of a text file listing which
resources should be cached. In your HTML file, include manifest="URL_of_manifest"
:
<!doctype HTML> <html manifest="cubeedoo.appcache"> <meta charset="utf-8"/> <title>....
With the inclusion of the manifest
attribute on
the <html>
element linking to a
valid manifest file, when a user downloads this page, the browser will
cache the files listed in the manifest file, the manifest file itself, and the current document, making
them available even when the user is offline. Even though the current
document is by default cached, it is best to list it among the cached
files in the manifest.
The document linking to the manifest file is cached by default.
So how does it work? When a browser sees a manifest
attribute, it downloads the manifest
file and attempts to cache the files listed in that manifest. If the
user opens a locally stored website when offline, it uses the already
cached files. If online, the browser accesses the cached site first, and
only then does it check to see whether updates have been made to the
cache manifest file and therefore the cache.
If changes have been made to the cache manifest file, the browser will download the entire cache before making updates to the cache in memory. The browser looks for changes in the manifest file, not the rest of the files on the server, to determine whether the cache should be refreshed. In other words, to get the cache to update, you need to edit the manifest file itself. Updating your other assets is not enough. Remember this when the “comment” is detailed in the section Updating the cache.
After loading the site from the cache, it fetches the manifest file from the server. If the manifest has changed since the page was last visited, the browser re-downloads all the assets and re-caches them. If the browser fails to re-download all of the assets, it continues to use the old cache. However, if the browser successfully downloads all the required files, it still continues to use the old cache, switching to the newer cache the next time the user accesses the site.
The .appcache file is a
text file that lists the resources the browser should
cache to enable offline access to your application. The file must
start with the following string: CACHE
MANIFEST
. The required string is then followed by a list of
files to be cached, and optional comments and section headers.
Your .appcache file should
be served with the MIME-type text/cache-manifest
. Add:
AddType text/cache-manifest .appcache
to your .htaccess file or server configuration. This used to be required, but is now optional. The manifest file is permanently stored in the browser cache. An .appcache may look something like this:
CACHE MANIFEST #version01 #files that explicitly cached CACHE: index.html css/styles.css scripts/application.js #Resources requiring connectivity NETWORK: signin.php dosomething.cgi FALLBACK: / 404.html
Note that the files listed in the cache manifest file are relative to the manifest file.
To create a comment, include a #
as the first character of the line and the
remainder of the line will be ignored.
Section headers add more control to how AppCache treats your web
files and assets. There are four possible section headers: CACHE
, FALLBACK
, SETTINGS
, and
NETWORK
, each followed by a
colon.
The files following the CACHE
header are explicitly cached. If no header is defined, or if files
are listed above the headers, those files are cached as if they
followed the CACHE
header. Note
that secure (HTTPS) files can only be cached if from the same origin
as the manifest.
The file containing the cache manifest file in the <html>
element is always added to the
cache whether or not it’s listed under the CACHE
header.
Do not list the manifest file itself, or the site may never update.
The files following the NETWORK
heading are explicitly not cached, and are
therefore only accessible when the user is online.
FALLBACK
files include paired
files: files to show, and fallback files to backfill if the
former file is not available. (If the first file in the pair is not
available, the second file listed on the line will be served.) If
included, the SETTINGS
header should be last and list the single line value
prefer-online
.
Do not list the cache.appcache file as a file to be cached in your manifest file, or your site may never update.
The browser cache is not updated or overwritten until a change is made to the
manifest file or by using applicationCache
JavaScript methods. Making
an update to a file listed in the manifest, such as your JavaScript,
CSS, or HTML, is not sufficient: a change needs to occur in the
manifest file itself.
A standard practice is to add a comment within the manifest file to force a file update. In the preceding
snippet, changing the comment #version01
to #version02
will inform the browser that the
cache should be updated. Using a timestamp instead of a version number
may be more intuitive for you. Note that what you put in the comment
is not important, only that it creates a change in the file that the
browser will detect.
The version number for our cache is basically the comment on the second line. When the user requests that web page, the browser first loads the site from the cache if the cache is present. Then it downloads the .appcache file from the server and compares it with the one in memory. If there is a change to the manifest file—such as a change in that version number—it will download the rest of the cache. This is why we add a comment. It is much easier to change a comment than to change a filename (and all the files linking to it). Changing the comment with a version number or timestamp has become the standard way of informing the browser that the manifest file should be considered updated.
Once an application is offline it remains cached until the user clears their browser’s data storage for your site, the .appcache file is modified, or the application cache is programmatically updated.
The browser first loads the site from the cache. Only then does it check to see if there are changes in the manifest file. When a change is noted, all the files listed in the manifest file are downloaded. The cache is not updated, however, until all the files are successfully retrieved from the server. If the manifest file or a resource specified in it fails to download, the entire cache update process fails—there is no risk of your user seeing a partially updated version of your web application.
You can force an update to the cache without altering the
manifest file programmatically. To explicitly update the cache, call
applicationCache.update()
. When the status is ready, swap the old cache for the
new one:
var appCache = window.applicationCache; if (appCache.status == appCache.UPDATEREADY) { appCache.swapCache(); }
The possible status values include UNCACHED
, IDLE
, CHECKING
, DOWNLOADING
, UPDATEREADY
, and OBSOLETE
. If the manifest file or a resource
specified in it fails to download, the entire cache update process
fails and the browser will keep using the old application
cache.
While these steps update the cache, they do not update what the user is currently viewing. The user will continue viewing the previously cached version of the site until the next time he or she tries to access the web application. It thus takes two loads of your site for the user to get the new content.
You can force the new site on the user by reloading the site
based on an updateready
event
handler, but having the site reload while the user is interacting with
it could be bad user experience; do so thoughtfully.
In terms of CubeeDoo, we can add all of the files to the
manifest file. We would have excluded the secure login form if we had
one. We also include a comment with the version number (or the date,
or something that makes sense to you), which we will update if we ever
make changes to any of the files listed under the CACHE:
or FALLBACK:
headers:
CACHE MANIFEST #version01 CACHE: index.html css/cubeedoo.css scripts/cubeedoo.js assets/matched.mp3 assets/notmatched.mp3 images/shapes.svg NETWORK: login.html FALLBACK: / 404.html
When we are ready to deploy our application, we will add the
manifest
attribute to our index page’s <html>
tag. But don’t do it
now. There is very little that is more annoying than
developing and testing a web application that is completely cached in
the browser:
<html lang="en-us" manifest="cubeedoo.appcache">
Browsers will also clear the cache if they are unable to find the cache manifest file on the server. Linking to a nonexistent file, thereby returning a 404 Not Found response, will cause the browser to clear the cache.
With application cache, we can get our web applications saved onto devices so they’re available offline. Application cache enables you to store the files, but sometimes you also need to store data. For example, when our CubeeDoo player is offline (and online), we want to maintain our high scores with the names, times, and scores of these overachievers along with the current game state when a player pauses the game. For these features, we could use localStorage, IndexedDB, or the deprecated Web SQL Database.
We’re going to use localStorage to pause the game and use the deprecated (and yet still pervasive) Web SQL Database to save high scores. We could have done the inverse. We can’t, however, use IndexedDB, since it is not yet supported on iOS or Android (though IE10, Blink, and Firefox added support in recent releases).
LocalStorage and sessionStorage are easy-to-use key/value stores. You may be thinking “but we have cookies, so what is the big whoop-dee-doo?” There are a lot of drawbacks to cookies, which localStorage solves.
The main uses for cookies are session management, personalization, and tracking. Server cookies are strings sent from the web server to the browser and back again with each HTTP request and response. The browser can return an unchanged cookie to the server, introducing state into an otherwise stateless HTTP transaction. Client-side cookies are JavaScript-generated strings that can be used to enable state, pass information back and forth to the server, or even simply to maintain values client-side, such as items in a shopping cart.
Browsers can store 300–400 cookies with a maximum size of 4 KB per cookie, and a limit of 20 cookies per server or domain. All cookies are sent with each HTTP request. While this automatic sending of information may be used to your favor, one of the big downsides in terms of mobile is the increased bandwidth. Also, passing cookies back and forth can be a security risk.
Whereas cookies are limited to 20 cookies at 4 KB each for a total of 80 KB per domain, the new local and session storage standards allow for more space. The size depends on the browser, but is generally in the MB rather than KB range.
LocalStorage is used for long-term storage of lots of data for a particular domain within a single browser. LocalStorage data persists after the browser or browser window is closed. LocalStorage data is accessible across all browser windows.
SessionStorage data is confined to the browser window that it was created in, and gets deleted when the session ends. SessionStorage is accessible to any page from the same origin opened in that window. If you open a window and navigate from page to page in the same site in the same window, every page navigated to within that same browser window will have access to the sessionStorage. If the user has multiple windows opened—for example, viewing your site in three separate browser windows—each browser window would have its own individual copy of the sessionStorage, but would share the same localStorage key/value pairs.
Long-term and session cookies are both sent to the server with every HTTP request. If you need to send information to the server, cookies may be the right solution. However, saving state via cookies, like many of us did before HTML5, meant sending lots of useless information to and from the server, wasting bandwidth. LocalStorage and sessionStorage both save bandwidth.
SessionStorage and localStorage both have the same five methods and a single property, as shown in Table 6-1.
Using sessionStorage and localStorage is extremely easy. It is like a regular object with a predefined name: sessionStorage and localStorage, respectively.
There is an argument as to the speed or performance of these storage APIs. While hitting the hard drive to retrieve data is slower than hitting a JSON value in the browser, hitting the hard drive on a mobile device is generally more performant than making an HTTP request.
Some websites, like http://m.bing.com, have taken advantage of localStorage to reduce the number of HTTP requests a page load makes. As briefly described in <style> and mobile performance: standards anti-pattern, they include the scripts and styles in the first hit to the server, then extract the JavaScript and CSS into separate localStorage name/value pairs. Each script has a unique identifier as the name in the name/value pair, which is stored inside a cookie.
When the user makes a request for a new page, the cookie gets sent along with the request informing the server which files are already stored in the user’s browser. The server then only sends the needed files. This reduces a page request to a single HTTP request.
While the first request may have a large file size, subsequent requests are small as all of the assets are stored locally in localStorage. While including scripts and styles within a page is an anti-pattern of performance and standards, it has been used effectively to improve the performance on some mobile web applications and sites.[40]
Data and user settings persistence is not just helpful in terms of user experience, but can also benefit web users whose data or WiFi may not be consistent, be it due to overloaded cell towers, lack of data, or the user wanting to limit their data usage.
Because application cache isn’t the panacea we are all hoping for, developers have developed their own best practices for offline application storage, generally mixing application cache with localStorage. The Financial Times has a good article explaining their process, reasoning, and code.
In CubeeDoo, we use localStorage to save state for pausing the game, and sessionStorage to store the username and the game’s default values. You can use sessionStorage or localStorage for all three, or any combination of the two. I chose to use both to demonstrate both.
We are leveraging the storage APIs to reduce the need to save state server-side. In fact, there is no server backend for CubeeDoo. Our server only needs to store and serve static files. All of the features such as high scores that generally sit on a database in the cloud are on the user’s device.
LocalStorage is used to maintain state when the game is paused. When the user pauses the game, we use the custom data attributes and the dataset API to set and get the values and locations of each card. We store the card values in localStorage, along with all the other relevant data—such as time left, current level, current score, etc.—required to continue the game where and how we left off. Had we used sessionStorage, pausing the game would have worked just as well, but the information would have been cleared when the user closed the browser window, as sessionStorage key/value pairs are cleared when a browser session is terminated.
SessionStorage is used to temporarily store the user’s name. The benefit of using sessionStorage instead of localStorage for the username is that a separate player’s username can be maintained in a second tab in the same browser. The drawback (or benefit) is that when the user closes the browser, the username (used for listing and storing high scores) is cleared.
We stored the original state of the game—the default values—with sessionStorage. It’s employed to save the default game settings when the game is initially loaded. When the user progresses through the game, the levels increase, then the time allowed per level decreases, and so on. When the user starts a new game, instead of refreshing the page, we pull the original values out of sessionStorage. In this way, starting a new game does not require a page reload to access the game settings, which may have changed during the previous game. We could have saved these variables as properties on a global object in our script, but reloading would have reset the values to the default values set in our JavaScript.
I’ve chosen to store these default values in sessionStorage because, with HTML5, I can! I used sessionStorage instead of localStorage so those values don’t maintain state between sessions.
I’ve included the following functions (among many others):
storeValue
Stores default game values.
alterValue
Updates stored default values.
pauseGame
Pauses game. Stores current state in localStorage.
playGame
Resets the game to pre-paused state, putting the cards back in their place and restarting the timer.
reset
Clears localStorage set up when game was paused, clearing the saved paused state of the game.
Note qbdoo
is the top-level
namespace for CubeeDoo, and has several properties you can
control:
1 var qbdoo = {
2 //game settings
3 currentLevel: 1,
4 currentTheme: "numbers",
5 gameDuration: 120,
6 score: 0,
7 matchedSound: 'assets/match.mp3',
8 failedMatchSound: 'assets/notmatch.mp3',
9 mute: true,
10 cardCount: 16,
11 iterations: 0,
12 iterationsPerLevel: 2,
13 possibleLevels: 3,
14 maxHighScores: 5, ...
You can set the default values such as the number of cards, iterations per level, initial duration of a round, and so on. You, as the developer, can alter any of these default values. As the user plays the game, some of these values get altered. We store the original values, and restore these values as they get altered in sessionStorage.
The storeValues()
function
stores the initial game values:
1 storeValues: function(newgame) {
2 var currentState = {};
3 //capture values for play
4 currentState.currentTheme = qbdoo.currentTheme;
5 currentState.timeLeft = qbdoo.timeLeft;
6 currentState.score = qbdoo.score;
7 currentState.cardCount = qbdoo.cardCount;
8 currentState.mute = qbdoo.mute;
9 currentState.iterations = qbdoo.iterations;
10
11 // get all the cards values and positions
12 // use dataset to get value for all the cards.
13 if (newgame == 'newgame') {
14 currentState.currentLevel = qbdoo.currentLevel;
15 currentState.score = 0;
16 currentState.gameDuration = qbdoo.gameDuration;
17 sessionStorage.setItem('defaultvalues',
JSON.stringify(currentState));
18 return;
19 } else {
20 return currentState;
21 }
22 },
The storeValues()
function is
called when the game is initialized to store the default values set in
our JavaScript file. As the user plays the game, some of these values
change. By storing these values, when the user starts a new game by
clicking on the new button, we do not need to reload the page.
Instead, the default values are captured in lines 4–9, and updated in
14–16 if the user is starting a new game (without reloading the
page).
When the function initially called, we set the values on the
locally scoped currentState
object.
In line 17, we use JSON’s stringify()
method to turn that object into a JSON string. We then save that
string in sessionStorage with the key defaultvalues
using sessionStorage’s
setItem()
method. We use the key
defaultvalues
to retrieve the value
with the getItem()
method, which we
do in our playGame()
function.
We’ve included an alterAValue()
function to update or return
the default values set with storeValues()
, should a user choose to
change settings or should the progression of the game change the
user’s settings.
23 alterAValue: function(item, value) {
24 var currentState = JSON.parse(sessionStorage.getItem('defaultvalues'));
25 if (value) {
26 currentState[item] = value;
27 } else {
28 qbdoo[item] = currentState[item];
29 }
30 sessionStorage.setItem('defaultvalues', JSON.stringify(currentState));
31 return value;
32 },
The parameter of the alterAValue()
function is the item to be set
or retrieved and an optional value for the item, if the item is to be
set. When the user changes the theme of the cards or mutes/unmutes the
audio, the item and value are sent as arguments with the function
call. The alterAValue()
function
fetches the item from sessionStorage, alters the object property
required, then re-saves the default values for the game in
sessionStorage to reflect the new value.
The function retrieves the default setting from sessionStorage
with the getItem()
method
in line 24. The return value is the JSON string we had stored in
sessionStorage with the setItem()
method earlier with the storeValues()
function. Because we stored a
JSON string, when we retrieve the value with the getItem()
method, a JSON string is returned.
We parse it with the JSON.parse()
method to define our locally scoped currentState
object.
If two values are passed to the alterAValue()
function, the first parameter
is the game property to be altered. The second parameter is the new
value of that game property. The currentState
object is updated to reflect
that change. If only one parameter is passed, the alterAValue()
function returns the value of
that game property.
We’ve included pauseGame()
and
playGame()
functions, to pause and
play the game, along with associated functions:
33 pauseGame: function(newgame) {
34
35 var currentState = {}, i, cardinfo = [];
36 if (qbdoo.game.classList.contains('paused')) {
37 qbdoo.playGame();
38 return false;
39 }
40
41 qbdoo.pauseOrPlayBoard('pause');
42 currentState = qbdoo.storeValues();
43 currentState.currentLevel = qbdoo.currentLevel;
44
45 for (i = 0; i < qbdoo.cardCount; i++) {
46 cardinfo.push(qbdoo.cards[i].dataset);
47 }
48
49 currentState.cardPositions = JSON.stringify(cardinfo);
50 localStorage.setItem('pausedgame', JSON.stringify(currentState));
51
52 qbdoo.clearAll();
53
54 },
The pauseGame()
function is
called when the pause/play button is clicked in the upper righthand
corner of our game, toggling between paused and play states. The
current state of the game—whether paused or in play—is determined by
the class on the game. In lines 36–38, we note that if the game is
already paused, as indicated by the presence of the paused
class, the playGame()
function, described in the next
section, is called. If the game is not already paused, we call the
pauseOrPlayBoard()
method in line
41 that toggles the game’s class and clears the timer interval.
To pause the game, we need to store the current state of the
game. We need to store the remaining card face values and locations as
well as the state of the game. We capture some of the stored state of
the game values with a call to the storeValues()
method in line 42, which we
described earlier. We add the current level with the currentLevel
property to the state object in
line 43.
We then iterate through all the cards, pushing the dataset
key/value pairs into the cardinfo
array in lines 45–47. We turn that array into a JSON string, adding
the card positions into the state object in line 49. We then stringify
the entire currentState
object and
store the JSON string we’ve created in the browser as the pausedgame
key’s value in line 50 in
localStorage.
In the last line, we clear the board by calling the clearAll()
method. That function clears the
cards from view by changing the value of the custom data attribute
data-value
to 0 for all cards. We
hide all of the cards that have a data-value
value of 0. We discuss CSS
selectors and how we target based on attributes in Chapter 7.
Note that we used the term classList
on line 36. The classList
object, added to all nodes
within the DOM, provides us with the ability to add, remove, toggle,
and query the existence of classes on any DOM node. classList
returns a token list of the
class attribute of the element:
node.classList.add(class)
node.classList.remove(class)
Removes the class from the node’s list of classes if it was present. If the class was not present, it does not throw an error.
node.classList.toggle(class)
Adds the class to the node’s list of classes if the class was not already present, and removes it if it was.
node.classList.contains(class)
Returns a Boolean: true
if the DOM node’s list
of classes contains a specific class; false
otherwise.
classList
has been
supported since iOS 5, Android 3, and IE10.
When the user pauses the game, the pause button becomes a play
button. This change is done with CSS based on the paused
class we added with the pauseGame()
function. When the user then
clicks on that same button again, the conditional in line 36 returns
true, calling the playGame()
function:
55 playGame: function(newgame) {
56 var cardsValues, cards, i, currentState = {};
57
58 if (newgame == 'newgame') {
59 currentState = JSON.parse(sessionStorage.getItem('defaultvalues'));
60 qbdoo.timeLeft = qbdoo.gameDuration = currentState.gameDuration;
61 } else {
62 // get state via local storage
63 currentState = JSON.parse(localStorage.getItem('pausedgame'));
64
65 if (qbdoo.game.classList.contains('paused')) {
66 qbdoo.game.classList.remove('paused');
67 }
68 qbdoo.timeLeft = currentState.timeLeft;
69 }
70 qbdoo.reset('pausedgame');
71
72 qbdoo.currentTheme = currentState.currentTheme;
73 qbdoo.mute = currentState.mute;
74 qbdoo.currentLevel = currentState.currentLevel;
75 qbdoo.score = currentState.score;
76 qbdoo.cardCount = currentState.cardCount;
77 qbdoo.iterations = currentState.iterations;
78
79 qbdoo.setupGame(currentState.cardPositions);
80 },
The playGame()
function is
called when the user restarts a game from pause and when the user
starts a new game after losing a previous game. If the user is
restarting the game, we want to continue the game from where we left
off. If we want to start a new game, we want to reset the default
values of the game. The playGame()
function handles both.
If starting a new game, with lines 58–60, the function
retrieves the default values for the game from sessionStorage with
the getItem()
method, parsing the
string and assigning it to the currentState
object. We also reset the
gameDuration
to its appropriate
value, and change the timeLeft
to
that value.
Otherwise, if the game is started from pause, we get the saved
state with card positions and values from localStorage with the
getItem()
method, passing the
pausedgame
key instead of the
defaultvalues
key, which is a
sessionStorage key, getting the pausedstate
and parsing the JSON string
into the currentState
object. The
function also changes the class of the board to drop the paused
class.
The end of the function sets the game properties based either
on the defaultvalues
captured
from sessionStorage or the paused values captured from localStorage,
before calling the setupGame
method to start the game.
In line 70, we call the reset()
function, which deletes the paused
game values stored in localStorage, using the removeItem()
method, which has as its only
argument the key name for the localStorage key/value pair we want to
delete:
81 reset: function(item) {
82 localStorage.removeItem(item);
83 }
In CubeeDoo, when we pause the game, we use custom data
attributes and a custom dataset to extract the card position and
values, the JSON.stringify()
method to turn the game object into a JSON string, and then
store that string in localStorage.
In terms of sessionStorage, the important line here is line
30. We created a sessionStorage entry with a key of defaultvalues
and a value of a JSON string
representing our current state object. Lines 6 through 25 (in the
earlier code snippet) create the
properties of that object, with lines 18 through 25 using custom
data attributes and the dataset API (covered in Chapter 2) to capture the key/value pairs
showing each card’s position and value.
When we restart the game, we get the item from localStorage:
gameState = localStorage.getItem('cubeedoo');
And then we delete the stored state from memory with:
localStorage.removeItem('cubeedoo');
Items that are stored in localStorage and sessionStorage are visible in the various debuggers, as shown in Figure 6-1.
Note that while you can see the localStorage and sessionStorage items in your debugger, some debuggers don’t automatically update the view. You may have to close your debugger and reopen to see the current state of your resources.
Our other use of sessionStorage is fairly simple as well.
If the username doesn’t exist, we prompt the user for their
username and add it both to the namespaced player
property and to sessionStorage:
if (!player || player == 'UNKNOWN') { player = qbdoo.player = prompt('Enter your name') || 'UNKNOWN'; sessionStorage.setItem('user', player); }
We assign the player name on page load with:
player: sessionStorage.getItem('user') || '',
If the user refreshes the page, the username is captured from sessionStorage. Otherwise, it defaults to blank. While we could have used cookies, this information doesn’t need to be sent back and forth to the server. I could also have used localStorage, but for the case of this book example, I wanted the “security” of when the user closes the browser and the username is deleted, as is what happens with sessionStorage.
The quirk with CubeeDoo is that whoever loses the game gets credit for the full game: if one user starts the game, and pauses it, the cards and current score status are saved in localStorage. Since the username is in sessionStorage, if the browser is closed out after a pause, any username stored in sessionStorage is lost. When a user continues on from the paused state, they can enter a name. If you don’t want a second user to continue on from where the first user left off, even if they are the same person, use sessionStorage instead of localStorage to store the paused state of the game. If you don’t want to bug the user for their username, even if they haven’t played in a month or two, use localStorage instead of sessionStorage to store the username. If you don’t care, still implement one or the other or both. This stuff is fun!
Web databases were new to HTML5, and were well supported. Actually, Web SQL Database is still well supported, especially in the mobile space. However, the specification was abandoned, and will not be supported in browsers that never supported it (IE and Firefox). Because the alternative, IndexedDB, is not ready for prime time, and Web SQL Database is almost fully supported in WebKit and Opera Mobile browsers, I’m covering it! Realize, however, that Web SQL is obsolete, and is just a stopgap until Android and iOS support IndexDB.
So, what is it? Web databases are databases that are hosted and persisted inside a user’s browser. The client-side SQL database allows for structured data storage: tables with rows and columns, not just name/value pairs. This can be used to store emails locally for an email application or for a cart in an online shopping site. The API to interact with this database is asynchronous, which ensures that the user interface doesn’t lock up (localStorage is synchronous). Because database interaction can occur in multiple browser windows at the same time, the API supports transactions.
Just like SQL, Web SQL has several methods and properties, detailed in the following sections.
An openDatabase()
method of
the window object takes four parameters: the database
name, version, display name, and database size. openDatabase()
creates a database object.
The database needs to be opened before it can be accessed. You need
to define the name, version, description, and the size of the
database:
window.openDatabase(database_name, database_version, display_name, db_size);
This method returns a reference to the database, which is referenced for all database transactions.
In CubeeDoo, the high scores can be maintained in two ways: either
localStorage or Web SQL. We check to see if Web SQL is supported,
and if so use it. If not, we set the script to use localStorage. We
fork our code based on the qbdoo.storageType
property:
storageType: (!window.openDatabase)? "WEBSQL": 'local',
Because Web SQL is currently supported but will forever be obsolete, don’t use it without first checking for support!
To maintain the high scores in a database, we need to create that database:
var dbSize = 5 * 1024 * 1024; // 5MB variable for dbSize if (!qbdoo.db) { if (window.openDatabase) { qbdoo.db = openDatabase("highscoresDB", "1.0", "Scores", dbsize); } }
The transaction()
method of
the database object takes up to three arguments: the
transaction, error, and success callback functions. transaction()
is a method of the database
object we created using the openDatabase()
method, not a window object
like the openDatabase()
method
that created the database. You pass it a SQL transaction object on
which you can use the executeSQL()
method:
db.transaction(transaction_callback, error_callback, success_callback)
The executeSQL()
method
takes one to four arguments: a SQL statement, arguments, a
SQL statement callback, and a SQL statement error callback. The SQL
statement callback gets passed the transaction object and a SQL
statement result object that gives access to the rows:
db.transaction(function(trnactn) { trnactn.executeSql('SELECT * FROM scores', [], callbackFunc, db.onError); });
In CubeeDoo, we combine the two to get and set scores:
saveHighScores:
function(score, player) {
qbdoo.db.transaction(function(tx) {
tx.executeSql("INSERT INTO highscoresTable (score, name, date)
VALUES (?, ?, ?)", [score, player, new Date()], onSuccess, qbdoo.onError);
});
function onSuccess(tx, results){
// not needed
}
},
With offline SQL database storage, you can create tables, delete rows, and basically run any SQL command that you might run on your database server. Client-side SQL database storage (Web SQL Database) is supported in Safari, Chrome, and Opera, but will never be supported in Firefox or Internet Explorer. However, since it is supported in WebKit and Opera, it can be used for mobile web applications for improved performance, with localStorage as a fallback until IndexDB is well supported.
As noted earlier, in CubeeDoo we use Web SQL to store the high
scores, with a localStorage backup in case Web SQL is not supported. I
feature detected, and set the storageType on the qbdoo
object:
storageType: (window.openDatabase)? "WEBSQL": 'local',
I then included functions to create the table, save high scores,
load the high scores, render the high scores, and to delete the scores
from the database. I included a method for sorting high scores for the
local storage scores method of retrieving high scores. I didn’t need
to sort the Web SQL scores when saving, as I can retrieve sorted
scores with SQL using ASC
or
DESC
on the column by which I want
to sort.
We have to create the table in our database with the SQL
create
statement:
createTable: function() {
var i;
qbdoo.db.transaction(function(tx) {
tx.executeSql("CREATE TABLE highscoresTable (id REAL UNIQUE, name TEXT,
score NUMBER, date DATE )", [],
function(tx) {console.log('highscore table created'); },
qbdoo.onError);
});
},
We save high scores with the SQL insert
statement, or we store in localStorage with the setItem()
method if the browser doesn’t
support Web SQL:
saveHighScores: function(score, player) {
if (qbdoo.storageType === 'local') {
localStorage.setItem("highScores", JSON.stringify(qbdoo.highScores));
} else {
qbdoo.db.transaction(function(tx) {
tx.executeSql("INSERT INTO highscoresTable (score, name, date)
VALUES (?, ?, ?)", [score, player, new Date()],
onSuccess,
qbdoo.onError);
});
function onSuccess(tx,results){
// not needed
}
}
},
We have two functions to load high scores depending on whether
we’re using localStorage or Web SQL. We use the SQL select
statement to select the high scores
from the database, sorting in descending order:
loadHighScoresLocal: function() { var scores = localStorage.getItem("highScores"); if (scores) { qbdoo.highScores = JSON.parse(scores); } if (qbdoo.storageType === 'local') { qbdoo.sortHighScores(); } }, loadHighScoresSQL: function(){ var i, item; qbdoo.db.transaction(function(tx) { tx.executeSql("SELECT score, name, date FROM highscoresTable ORDER BY score DESC", [], function(tx, result) { for (i = 0, item = null; i < result.rows.length; i++) { item = result.rows.item(i); qbdoo.highScores[i] = [item['score'], item['name'], item['date']]; } //end for }, onError); // end execute function onError(tx, error) { if (error.message.indexOf('no such table')) { qbdoo.createTable(); } else { console.log('Error: ' + error.message); } } qbdoo.renderHighScores(); }); // end transaction },
The renderHighScores()
function creates a list of the high scores:
// put the high scores on the screen
renderHighScores: function(score, player) {
var classname, highlighted = false, text = '', i;
for (i = 0; i < qbdoo.maxHighScores; i++) {
if (i < qbdoo.highScores.length) {
if (qbdoo.highScores[i][1] == player && qbdoo.highScores[i][0] == score) {
classname = ' class="current"';
} else {
classname = '';
}
text += "<li" + classname + ">" + qbdoo.highScores[i][1].toUpperCase() +
": <em>" + parseInt(qbdoo.highScores[i][0]) + "</em></li> ";
}
}
qbdoo.highscorelist.innerHTML = text;
},
The SQL drop
statement can be
used to delete the table if the user chooses to delete the scores. If
Web SQL is not supported, the reset()
function uses the localStorage removeItem()
method:
eraseScores: function() { if (qbdoo.storageType === 'local') { qbdoo.reset("highScores"); } else { qbdoo.db.transaction(function(tx) { tx.executeSql("DROP TABLE highscoresTable", [], qbdoo.createTable, qbdoo.onError); }); } qbdoo.highscorelist.innerHTML = '<li></li>'; }, onError: function(tx, error) { console.log('Error: ' + error.message); }, reset: function(item) { localStorage.removeItem(item); }
For client-side storage of structured data, we will soon have IndexedDB. IndexedDB, when finalized and supported, will provide for high-performance data searches using indexes. While DOM Storage is useful for storing smaller amounts of data, IndexedDB provides for an asynchronous solution for storing larger amounts of structured data. Since it is not currently widely supported in mobile browsers, we are not covering it here. If you prefer to use APIs that are actually moving forward as specifications, there is a polyfill to enable using IndexedDB syntax in Web SQL supporting browsers.
As support improves, I will add IndexedDB to the online chapter resources. At the time of this writing, the only support in the mobile space is in IE10, and prefixed with different syntax in BlackBerry 10.
In addition to providing for offline web applications and uniform support for media, HTML5 includes several APIs that enable developers to enhance user experience. HTML5 includes a geolocation API enabling browsers to determine user location (with user consent, of course), web workers to improve script runtime of web applications, microdata to improve the semantics of web content, cross-document messaging API should allow documents to safely communicate with each other regardless of their source domain, and ARIA, to enable developers making the rich Internet applications accessible.
Geolocation allows users to share their physical location with your application if they choose to. Especially useful in social networking, geotagging, and mapping (but applicable to any type of application), geolocation enables developers to enhance the user experience, making content, social graphs, and advertisements more relevant to the location of the user.
The browser will request the permission of the user before accessing geolocation information (see Figure 6-2). Geolocation is an opt-in feature: when your web application requests geolocation information, the browser will ask the user if permission to share geolocation information is granted via banner or alert. The user can grant permission or deny it, and optionally remember the choice on that site. If permission is granted, the geolocation information will be accessible to your scripts and any third-party scripts included in the page, letting your application determine the location of the user, and capable of updating location information as the user moves around.
Location information is approximate, garnered from IP addresses, cell towers, WiFi networks, GPS, or even getting the information through manual data entry by the user. While approximate, you’ll notice that it can be freakishly accurate.
The geolocation API does not care how the client determines location as long as the data is received in a standard way. The geolocation API is asynchronous.
To determine browser support for geolocation, use:
if (navigator.geolocation) { //geolocation is supported }
The geolocation object provides for the getCurrentPosition()
and watchCurrentPosition()
methods that
asynchronously return the user’s current location, either once or
continuously. The watchCurrentPosition()
method can be used for
active location applications such as GPS/navigation applications. For
our web application, we don’t need direction information, so the
getCurrentPosition()
method would
suit our needs, and wouldn’t drain the battery by repeatedly seeking
location information. We don’t need location information for CubeeDoo,
but we can still learn it:
if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(handle_success, handle_errors); }
If successful, the callback function returns the current position
with the coords
object
containing the more commonly used latitude
and longitude
properties, as well as the altitude
, accuracy
, altitudeAccuracy
, heading
, and speed
properties. The following script will
return the alert with the current latitude and longitude, and is
available in the online chapter
resources:
if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(handle_success,handle_errors); function handle_success(position) { alert('Latitude: ' + position.coords.latitude + '\n Longitude: ' + position.coords.latitude); } function handle_errors(err) { switch(err.code) { case err.PERMISSION_DENIED: alert("User refused to share geolocation data"); break; case err.POSITION_UNAVAILABLE: alert("Current position is unavailable"); break; case err.TIMEOUT: alert("Timed out"); break; default: alert("Unknown error"); break; } } }
If successful, both the getCurrentPosition()
and watchCurrentPosition()
methods success
callbacks will return a location object with the coords
object, with the following
properties:
position.coords.latitude
position.coords.longitude
position.coords.altitude
position.coords.accuracy
The watchCurrentPosition()
method also returns the following properties:
position.coords.heading
position.coords.speed
The properties are kind of self-explanatory. Other than the Kindle and Opera Mini, geolocation is supported everywhere, and has been for a while (since IE9 on desktop).
We don’t need location information for CubeeDoo, but we did include pinpointing current location on a map as an example in the files:
1 function getLocation() { 2 if (navigator.geolocation) { 3 navigator.geolocation.getCurrentPosition(success, error); 4 console.log('got position'); 5 } else { 6 error('not supported'); 7 } 8 } 9 function error(text) { 10 text = text || 'failed'; 11 console.log(text); 12 } 13 function success(location) { 14 var lat = location.coords.latitude; 15 var long = location.coords.longitude; 16 var url = "http://maps.google.com/maps?q=" + lat + "," + long; 17 }
The getLocation()
function
feature detects in line 2 to see if geolocation is supported by the
browser. Note that geolocation is on the navigator
object (rather than window or
document, like most method and properties we use). We get the current
position with the getCurrentPosition()
method of the
geolocation
object in line 3. If successful, the
success callback uses the returned coords
object’s latitude
and longitude
properties to add a pinpoint to a
Google map in lines 14–16. We included an error callback function on
line 9. The message is usually a timeout, permission denied, or position
unavailable if there is a failure.
All of the JavaScript, including page reflows and repaints, runs on the single UI thread that also handles repainting of user interactions, non-hardware-accelerated animations, etc. If there is a task with a heavy script load, the browser may slow to a crawl, greatly harming user experience. Web workers allow JavaScript to delegate heavy tasks to other processes so that scripts run in parallel. This enables the main thread to do the exciting UI stuff while the web worker does any heavy lifting you pass to the worker without slowing the main script thread. Web workers are useful in allowing your code to perform processor-intensive calculations without blocking the user interface thread.
Workers enable web content to run scripts in background threads, even AJAX. The worker thread can perform tasks without interfering with the user interface.
You know how sometimes web applications take a long time and you
have to wait for the hourglass or rainbow beach ball to disappear before
being able to interact with the page? Web workers are a solution.
Workers perform JavaScript on a background thread leaving the main UI
thread free to manipulate the DOM and repaint the page, if necessary.
Web workers cannot manipulate the DOM. If actions taken by the
background thread need to result in changes to the DOM, they should post
messages back to their creators to do that work. The postMessage()
method can be employed.
If your JavaScript includes some resource-intensive calculations, you can pass this to a web worker to process while the main thread continues running. Before creating a worker, ensure that the browser supports web workers. Web workers support heavy JavaScript processing that might crash a non-web-worker–supporting browser:
if (window.Worker) { //browser supports web workers }
To create a web worker, you call the Worker()
constructor, specifying the URI of a
script to execute in the worker thread. The URI is relative to the file
calling the script:
if (window.Worker) { var webWorker = new Worker('subcontractor.js'); //create it }
Communicating with the web worker is accomplished via the postMessage()
method. Once created, set the
worker’s onmessage
property to an
appropriate event handler function to receive notifications from the web
worker. You terminate a worker with the terminate()
or close()
method. The terminate()
method is immediate, stopping all current processes:
if (window.Worker) { var webWorker = new Worker('subcontractor.js'); //create it webWorker.postMessage(some_message); }
In the worker (in this case, the subcontractor.js), receive the message from the main thread and act on it:
//in the subcontractor.js file self.onmessage = function(event) { // handle the message var stuff = event.data; // and send it back to the main thread postMessage(stuff); };
Workers can use timeouts and intervals just like the main thread
can. This can be useful, for example, if you want to have your worker
thread run code periodically instead of nonstop. You can control the
worker by employing the setTimeout()
,
clearTimeout()
, setInterval()
, and clearInterval()
methods.
Worker threads have access to a global function, importScripts()
, which lets them import
scripts or libraries into their scope. It accepts as parameters zero or
more URIs of resources to import:
/* imports two scripts */ importScripts('scripts/jquery-min.js', 'application.js');
Web workers do not have access to the DOM. They don’t have access to the console either. You may also get a security error when testing locally, which can make developing with web workers a little more difficult than regular JavaScript.
In CubeeDoo, we have no intensive JavaScript that we need to run, or background AJAX processes. While we don’t employ the benefits of web workers in CubeeDoo, there is a Fibonacci sequence example in the online chapter resources. Also, we’ve included a web worker to handle our high score sort function. Sorting five numbers is certainly doable without the need of a web worker. However, if we kept the top 1,000,000 scores, sorting that many values would be a good use of a web worker (but a bad use of localStorage).
Were we to have used web workers for sorting the high scores, we would have replaced our sorting function with a web worker call:
var webWorker = new Worker('js/sort.js'); webWorker.postMessage(qbdoo.highscores); webWorker.onmessage(function(event) { qbdoo.highscores(event.data); });
The web worker script, in turn, needs to expect and accept the
highscores
via the onmessage
property, and needs to post back the
sorted scores via postMessage
:
self.onmessage = function(event) { var sortedScores = sortScores(event.data); self.postMessage(sortedScores); };
Another feature of HTML5 is microdata. While microdata will not impact your site in any visible way, it is an increasingly relevant feature when it comes to search engine optimization and data scraping.
Microdata will replace the need for microformats. Microformats are
standardized sets of vocabularies that are both human and
machine-readable. They are web page conventions used to describe common
information types including events, reviews, address book information,
and calendar events via class
attributes. Each entity, such as a person, event, or business, has its
own properties, such as name, address, and phone number.
Microdata lets you create your own vocabularies beyond HTML5 and
extend your web pages with custom semantics. Microdata uses the new to
HTML5 attributes of itemscope
, itemprop
, itemref
, and itemtype
.
The itemscope
attribute is used
to create an item, indicating that the scope of the item begins in the
opening tag in which the attribute is included, and ends at that
element’s closing tag. The itemprop
,
or item property attribute, is used to add a property to an item. If an
itemprop
has associated properties
that are not descendants of that itemprop
, you can associate those properties
with that itemprop
by using the
itemref
, or item reference attribute.
The entity that has the itemscope
attribute also accepts the itemref
attribute that takes as its value a space-separated list of IDs of
entities that should be crawled in addition to the itemprop
’s descendants.
Microdata is most useful when it is used in contexts where other authors and readers are able to cooperate to make new uses of the markup. You can create your own types of microdata or use predefined data vocabularies. Some predefined vocabularies can be found at http://www.data-vocabulary.org/.
Microformats are very similar to microdata. In fact, microdata can be viewed as an
extension of the existing microformat idea, which attempts to address
the deficiencies of microformats without the complexity of the often
preferred systems like RDFa. Instead of using the new itemscope
, itemprop
, itemtype
, etc., attributes, Microformats
repurpose the class
attribute to
provide human and machine-readable semantic meaning to
data—in essence, microdata.
In general, microformats use the class
attribute in the opening HTML tags
(often <span>
or <div>
) to assign brief, descriptive
names to entities and their properties. Unlike microdata, microformats
are not part of the HTML5 specification.
Here’s an example of a short HTML block showing my contact information for myself:
<ul> <li><img src="http://standardista.com/images/estelle.jpg" alt="photo of Estelle Weyl"/></li> <li><a href="http://www.standardista.com">Estelle Weyl</a></li> <li>1234 Main Street<br />San Francisco, CA 94114</li> <li>415.555.1212</li> </ul>
Here is the same HTML marked up with the hCard (person) microformat.
<ul id="hcard-Estelle-Weyl" class="vcard"> <li><img src="http://standardista.com/images/estelle.jpg" alt="photo of Estelle Weyl" class="photo"/></li> <li><a class="url fn" href="http://www.standardista.com">Estelle Weyl</a></li> <li class="adr"> <span class="street-address">1234 Main Street</span> <span class="locality">San Francisco</span>, <span class="region">CA</span>, <span class="postal-code">94114</span> <span class="country-name hidden">USA</span> </li> <li class="tel">415.555.1212</li> </ul>
In the first line, class="vcard"
indicates that the HTML
enclosed in the <ul>
describes a person: in this case, me. The microformat used to describe
people is called hCard but is referred to in HTML as vCard. While
confusing, it isn’t a typo.
The rest of the example describes properties of the person,
including a photo, name, address, URL, and phone, with each property
having a class attribute describing the property. For example,
fn
describes my “full name.”
Properties can contain other properties. In the example, the
property adr
encompasses all the
components of my fake address, including street address, locality,
region, and postal code. With a little CSS, we can hide elements with
class hidden
and add
a line break between street address and locality. To create your own
hCard, visit http://microformats.org/code/hcard/creator.
The same content could be written with microdata:
<ul id="hcard-Estelle-Weyl" itemscope itemtype="http://microformats.org/profile/hcard"> <li><img src="http://standardista.com/images/estelle.jpg" alt="photo of Estelle Weyl" class="photo"/></li> <li><a href="http://www.standardista.com" itemprop="fn">Estelle Weyl</a></li> <li itemprop="adr"> <span itemprop="street-address">1234 Main Street</span> <span itemprop="locality">San Francisco</span>, <span itemprop="region">CA</span>, <span itemprop="postal-code">94114</span> <span class="hidden" itemprop="country-name">USA</span> </li> <li itemprop="tel">415.555.1212</li> </ul>
Or you can combine the two:
<ul id="hcard-Estelle-Weyl" class="vcard" itemscope itemtype="http://microformats.org/profile/hcard"> <li><img src="http://standardista.com/images/estelle.jpg" alt="photo of Estelle Weyl" class="photo"/></li> <li><a class="url fn" href="http://www.standardista.com" itemprop="fn">Estelle Weyl</a></li> <li class="adr" itemprop="adr"> <span class="street-address" itemprop="street-address">1234 Main Street</span> <span class="locality" itemprop="locality">San Francisco</span>, <span class="region" itemprop="region">CA</span>, <span class="postal-code" itemprop="postal-code">94114</span> <span class="country-name hidden" itemprop="country-name">USA</span> </li> <li class="tel" itemprop="tel">415.555.1212</li> </ul>
Microdata does not alter the appearance of a document. Rather, it just enhances the semantics of that document. Search engines will not display content that is not visible to the user. Providing search engines with more detailed information, even if you don’t want that information to be seen by visitors to your page, can be helpful. To enable the Web to be a single global database, being able to parse the available data into meaningful data points is required. Microdata and microformats help make otherwise nondescript data meaningful to parsers.
Not yet well supported, the microdata DOM API provides access to the microdata
items. The document.getItems(itemType)
returns a
nodeList containing the items with the specified types, or all types
if no argument is specified. The document.getItems()
method returns a
nodeList containing all the microdata items on a page when no argument
is passed. You can specify a specific itemtype URL as the argument to
return only items of that type.
Once you’ve returned your nodeList, you can then access the
different properties with the properties
attribute:
var allMicrodata = document.getItems(); var firstItemName = allMicrodata.properties['name'][0].itemValue;
Each item is represented in the DOM by the element on which
the relevant itemscope
attribute is found.
Cross-document messaging allows documents to communicate with each other regardless of their source domain, in a way designed to protect us from cross-site scripting attacks.
Web applications often include services from several different domains. The current way of doing mash-ups has many security risks. When you include third-party JavaScript on your website, those external scripts, over which you have no control, have access to your domain’s cookies and can forge requests that appear to come from the user. Iframes are not the solution to the problem, since your document cannot communicate with the contents of an iframe embedded within the page if that iframe comes from a different domain.
The HTML5 cross-document messaging API attempts to solve both of these issues by enabling the registration of event handlers for incoming messages from other domains, and sending messages to other domains.
To verify that the message is coming from the expected domain:
window.addEventListener('message', function(e) { if (e.origin == 'http://the_domain.com') { // the origin of the message is verified. Test to see if // it's in the correct format before using }, false);
Send a message to another domain:
var theFrame = document.getElementById("myIFrame").contentWindow; theFrame.postMessage("The message", "http://www.the_domain.com");
As CubeeDoo is a small, self-contained application with limited visitors and no third-party app integration, we aren’t using any cross-document messaging or cross-origin resource sharing (CORS). If you are creating more popular applications, you’ll likely be using content delivery networks (CDN) or integrating third-party applications. For example, if we were hosting our font on a CDN, we would need to use CORS to tell Firefox and Internet Explorer that it is OK to render fonts from a different domain.
Security is a major concern in cross-domain messaging. Always check the
origin
property to ensure that messages are only
accepted from domains that you expect to receive messages from. After
you’ve confirmed that the message is coming from the expected server,
confirm that the data received is in the expected format. You should
never rely on someone else’s server not being compromised.
HTML5, just like prior versions, can be made to be completely accessible. It just requires
a little bit of planning. ARIA, or Accessible Rich Internet
Applications, is the first part of HTML5 that is supported by all modern
browsers. Most popular JavaScript libraries also provide support for
ARIA implementation. In addition to ARIA, HTML5 provides the ability to
enhance accessibility in the fact that it does not use Flash (yes, they
talk the accessibility talk, but no one has figured out how to make
Flash walk the accessibility walk) or other embedded objects. It uses
<video>
, <audio>
, <svg>
, and <canvas>
, HTML elements that, with the exception of
<canvas>
, are inherently accessible. As part of
the DOM, they are easily targetable to increase accessibility.
As we create more and more dynamic web applications, the content becomes less and less inherently accessible to differently abled users. You can use ARIA properties to provide the basic type, state, and changes created via JavaScript widgets to screen readers and other assistive technologies. For example, when checking for airfares, selecting a checkbox entitled “nonstop only” may lead to a dynamic response that updates the airfare rates without reloading the page. If a user is visually impaired, how do they know that part of the page was updated since they can’t actually see the update? On a finance page, how does a user know that the stock ticker is continually updated? The ARIA API provides for unobtrusive (and obtrusive) ways of providing such information to the user.
As previously stated, ARIA stands for Accessible Rich Internet Applications. With the proliferation of Internet applications, there has been an increase in the number of sites requiring JavaScript and that update without page refreshes. This imposes accessibility issues that weren’t addressed by Web Content Accessibility Guidelines, or WCAG 1, as those specifications were written when “sites must work without JavaScript” was a reasonable accessibility specification.
With the increase of web-based “applications” (versus “sites”) requiring JavaScript, and improved support of JavaScript in assistive technologies, new accessibility issues have emerged. ARIA attempts to handle some of those issues. Through the inclusion of roles, states, and properties, your dynamically generated content can be made accessible to assistive technologies. Additionally, static content can be made more accessible through these additional, enhanced semantic cues.
By including ARIA accessibility features on your website, you are enhancing the accessibility of your site or application. By including roles, states, and properties, ARIA enables the developer to make the code semantically richer for the assistive technology user. ARIA enables semantic description of element or widget behavior and enables information about groups and the elements within them. ARIA states and properties are accessible via the DOM.
Similar to including the title
attribute, ARIA
is purely an enhancement and will not harm your site in any way. In
other words, there is no valid reason to not include these features!
Most JavaScript libraries, such as jQuery and Dojo, already support ARIA. Modern browsers,
including IE8, support ARIA.
The easiest to include and most important properties of ARIA are
the inclusions for the role
attribute, and inclusion
of states and properties:
Only use ARIA roles, attributes, and properties when regular HTML markup does not support all of the semantics required.
Apply the ARIA role
attribute in cases where the markup needs to be semantically
enhanced and in cases where elements are being employed outside of
their semantic intent. This includes setting up relationships
between related elements (grouping).
Set the properties and initial state on dynamic and user-changing elements. States, such as “checked,” are properties that may change often. Assistive technology that supports ARIA will react to state and property changes.
Support full, usable keyboard navigation. Elements should all be able to have keyboard focus.
Make the user interface visually match the defined states and properties in browsers that support the ARIA CSS pseudoclasses.
The role
attribute enables the developer to
create semantic structure on repurposed elements. While to a sighted
user, the example of a span repurposed as a checkbox is not noticeable,
the role
attribute makes this seemingly nonsemantic
markup accessible, usable, and interoperable with assistive
technologies. Once set, however, a role
attribute’s
value should not be dynamically changed, since this will confuse the
assistive technology.
Example: your designer insists that they want the checkboxes on
your page to look a certain way. “Impossible,” you say. You know that
you can use CSS to make a span look like a checkbox. The sighted user
would never know that you weren’t using <input type="checkbox"...
, but for
accessibility concerns, you know a screen reader user will not know it’s
a checkbox. With the ARIA role
attribute included in
your code, and both a browser and screen reader that support ARIA, you
can make your repurposed span accessible with:
<span role="checkbox" aria-checked="true" tabindex="0"/>
It’s not enough to simply use role
in the preceding example. If you include
spans transformed into checkboxes, you will need to include equivalent
but unobtrusive touch, keyboard, and mouse events for each interaction.
Best practices dictate that you should use the most semantically
appropriate element for the job, so in practice you should not do
this.
There are currently over 60 roles, including:
alert | dialog | listitem | option | spinbutton |
alertdialog | directory | log | presentation | status |
application | document | main | progressbar | tab |
article | form | marquee | radio | tablist |
banner | grid | math | radiogroup | tabpanel |
button | gridcell | menu | region | textbox |
checkbox | group | menubar | row | timer |
columnheader | heading | menuitem | rowgroup | toolbar |
combobox | img | menuitemcheckbox | search | tooltip |
complementary | link | menuitemradio | scrollbar | tree |
contentinfo | list | navigation | separator | treegrid |
definition | listbox | note | slider | treeitem |
There is no need to include role
if an element is employed as intended
(you don’t have to include role="checkbox"
on <input type="checkbox"/>
). However, if
you use a span to appear and function like a checkbox, include the ARIA
role
attribute: <span role="checkbox">
. Choose the role
type from this list that is most similar to the role you are assigning
to the element you are employing in a nonsemantically correct manner.
If you are interested in learning more about WAI-ARIA, Web Accessibility Initiative—Accessible Rich Internet Applications, http://www.w3.org/WAI/intro/aria.php is the place to start.
This chapter is intended to give you an idea of the APIs that are being included in HTML5. Each of these sections merit books of their own. In fact, some, like microformats, already have their own books. In other cases, such as cross-document messaging, the issue is too nascent to write about. Please check out the online chapter resources for links to more in-depth articles on each of these topics.
[40] For more information on sessionStorage, see http://www.nczonline.net/blog/2009/07/21/introduction-to-sessionstorage/.