Local Data Access: I: IndexedDB
WinRT does not have any built-in database capabilities like SQL Server CE. It doesn’t provide any APIs to connect directly to a SQL Server; instead we need to use a cloud storage solution or rely on third-party options like SQLite. Cloud storage is not an ideal solution in many cases, as it requires complex data management. Also it might not be an affordable solution, as storing data in the cloud is not free in most cases. In the next three chapters we learn about local storage options like indexedDB, JET API, Application Storage, and SQLite. To start with, in this chapter we learn to use IndexedDB for storing structured data locally and build a Movie collection and Inventory app that use IndexedDB as data storage.
IndexedDB or the Indexed Database API is a nonrelational data store, designed to store structured objects in collections known as the object store. The object store holds records as key–value pairs. Each record in the object store has a single key, which can be configured to autoincrement or can be provided by the application. This key is like the primary key in a relational database table, where no two records with in an object store can be identified by the same key.
Note IndexedDB is supported by Firefox (since version 4), Internet Explorer 10, and Google Chrome (since version 11). Safari and Opera support an alternate mechanism for client-side database storage called Web SQL Database. As of November 2010, the W3C Web Applications Working Group ceased working on the Web SQL Database specification, citing lack of independent implementations.
Using IndexedDB in Windows 8 Application
Internet Explorer 10 and Windows Store apps using JavaScript support the Indexed Database API defined by the World Wide Web Consortium (W3C) Indexed Database API specification, so applications written using HTML5 and JavaScript will be able to use IndexedDB as a local storage options. The following are some of the common IndexedDB contracts.
Even though IndexedDB concepts looks similar to relational database management elements, one key difference is that there is no relational semantics, which means we cannot use joins. With this introduction we learn how to use IndexedDB as data storage by creating My Collections, a movie collection and inventory Windows Store app using HTML5 and JavaScript.
Creating the My Collections App
Many movie buffs have a huge collection of DVD and Blu-ray movies that they share with their friends and family. Sometimes keeping track of all these movies becomes a tedious process. To manage and keep track of the collection, here we build a simple app that helps to add and track movies within the collection. This app has three HTML pages.
Figure 4-1. My Collection Windows 8 app displays the movies in the collection
Getting Started
To start with, let’s create a new Windows Store Blank App (JavaScript) project and name it MyCollections (see Figure 4-2). We add two new pages to the project: Home.html and Details.html.
Figure 4-2. Visual Studio templates for JavaScript creates a Blank application with HTML, CSS, and JavaScript files
We also add a Search Contract and name it searchResults.html as shown in Figure 4-3. As mentioned before, this is the page that will be involved when we search for movies from the Windows 8 Search charm.
Figure 4-3. Adding support for the Search contract by using the Visual Studio template
Finally we add a JavaScript file, Movie.js. This is the main file and it contains the functionality that drives the entire application.
With all the files in place and after you move some files to restructure the project for better management, the final MyCollections Windows 8 app project will look like the one shown in Figure 4-4.
Figure 4-4. Visual Studio Solution Explorer displaying the MyCollections project structure
The MyCollections IndexedDB database CollectionDB will have only one object store called Movies. The Movies object store will have six key paths, as follows.
Creating the Database
We start the coding by creating the CollectionDB database as soon as the application starts at the activated event in default.js by calling a function createDB as shown in Listing 4-1.
Listing 4-1. createDB Function Is Called Inside the Activated Event
app.addEventListener("activated", function (args) {
if (args.detail.kind === activation.ActivationKind.launch) {
if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.terminated) {
// TODO: This application has been newly launched. Initialize
// your application here.
} else {
// TODO: This application has been reactivated from suspension.
// Restore application state here.
}
if (app.sessionState.history) {
nav.history = app.sessionState.history;
}
args.setPromise(WinJS.UI.processAll().then(function () {
if (nav.location) {
nav.history.current.initialPlaceholder = true;
return nav.navigate(nav.location, nav.state);
} else {
return nav.navigate(Application.navigator.home);
}
}));
//creating the indexeddb database
createDB();
}
});
The CreateDB function creates the request to open the databases. If it doesn’t exist, create it and it will immediately upgrade to version 1. At any given time with in the app only a single version of a database can exist. After it’s created, a database and its object stores can only be changed through a specialized type of transaction mode known as a versionchange. To change a database after its creation, we must open the database with a higher version number, and that is the reason to change it to version 1. This action causes the upgradeneeded event to fire and the code to create the Movies object store in onupgradeneeded.
The Movies object store is created using the IndexedDB createObjectStore function. This function gets a name for the object store and sets the key path and key generator. This object store also has indexes that hold additional information. These indexes are created using the createIndex function. Success callback is invoked on opening the database. Here we set the database context to an attribute, as shown in Listing 4-2.
Listing 4-2. Database and Table Are Created in the createDB Function
function createDB() {
// Create the request to open the database, named CollectionDB. If it doesn't exist, create it and immediately
// upgrade to version 1.
var dbRequest = window.indexedDB.open("CollectionDB", 1);
dbRequest.onupgradeneeded = function (e) {
MyCollection.db = e.target.result;
var txn = e.target.transaction;
var movieTable = MyCollection.db.createObjectStore(
"Movies"
,{
keyPath: "id"
, autoIncrement: true
});
movieTable.createIndex("title"
, "title"
, { unique: false });
movieTable.createIndex("year"
, "year"
, { unique: false });
movieTable.createIndex("image"
, "image"
, { unique: false });
movieTable.createIndex("poster"
, "poster"
, { unique: false });
movieTable.createIndex("status"
, "status"
, { unique: false });
txn.onerror = function () {
WinJS.log && WinJS.log("Database creation failed"
, "Log"
, "Status");
};
txn.oncomplete = function () {
WinJS.log && WinJS.log("Database table created"
, "Log"
, "Status");
};
};
dbRequest.onsuccess = function (e) {
MyCollection.db = e.target.result;
};
}
Creating the Movie Object in Windows 8 JavaScript
Movie.js contains a self-executing anonymous function where objects are created inside the MyCollections namespace. This object contains a property, Movie.
The Movie object is defined as a WinJS.Class using the WinJS.Class.define() method. As can be seen in Listing 4-3, this method takes various parameters that are assigned to the corresponding properties of the Movie class. The Movie class has five properties that match the IndexedDB objectstore Movie and one other propety, IsInCollection, that is used to determine whether the object is already in the collection.
Listing 4-3. Defining Movie Object in Movie.js
WinJS.Namespace.define("MyCollection", {
Movie: WinJS.Class.define(
function () {
this.title = "";
this.year = 0;
this.image = "";
this.isInCollection = false;
this.status = "";
this.poster = "";
this.id = 0;
},
{
getTitle: function () { return this.title; },
setTitle: function (newValue) { this.title = newValue; },
getImage: function () { return this.image; },
setImage: function (newValue) { this.image = newValue; },
getIsInCollection: function () { return this.isInCollection; },
setIsInCollection: function (newValue) { this.isInCollection = newValue; },
getYear: function () { return this.year; },
setYear: function (newValue) {
this.year = newValue;
},
getPoster: function () { return this.poster; },
setPoster: function (newValue) { this.poster = newValue; },
getStatus: function () { return this.status; },
setStatus: function (newValue) { this.status = newValue; },
getID: function () { return this.id; },
setID: function (newValue) {
this.id = newValue;
},
},
The Movie object also has CRUD functions that we use to add, delete, and update data to IndexedDB. Let’s look at each one of them in detail.
Saving the Movie Object
To add a movie to the movie collection, we call the saveMovie function. This function first checks to see if the movie details already exist in the IndexedDB Movie objectStore. If this information is already present, it will update the existing row; otherwise, it will add a new row to the IndexedDB Movie objectStore. To do this, first create a new transaction involving the Movie objectStore as shown in the Listing 4-4 and set the mode to readWrite and will get a handle of the Movie object store. Now that we have access to the objectstore, we can just pass a JSON object to either add or put command depending on the value of the ID parameter.
Listing 4-4. Saving Movie Object to IndexedDB Using the saveMovie Function
saveMovie: function (id, title, year, image, poster, status ) {
var txn = MyCollection.db.transaction(["Movies"], "readwrite");
var movieTable = txn.objectStore("Movies");
var saveRequest;
if (id > 0)
saveRequest = movieTable.put(
{
id: id,
title: title,
year: year,
image: image,
poster: poster,
status: status
});
else {
saveRequest = movieTable.add(
{
title: title,
year: year,
image: image,
poster: poster,
status: status
});
}
saveRequest.onsuccess = function () {
WinJS.log && WinJS.log("Movie Updated: " + this + ".", "Log", "Status");
};
saveRequest.onerror = function () {
WinJS && WinJS.log("Failed to update Movie: " + this + ".", "Log", "error");
};
}
Note In IndexedDB, objectStore.add() is used to add an object to the store and objectStore.put() is used to update an object.
Deleting the Movie Object
The deleteMovie function deletes a row from the Movie objectStore. Just like saveMovie, we start a transaction, reference the object store that contains the Movie object, and issue a delete command with the unique ID of our object (see Listing 4-5).
Listing 4-5. Deleting Movie Object From IndexedDB Using the deleteMovie Function
deleteMovie: function ( id ) {
var txn = MyCollection.db.transaction(["Movies"], "readwrite");
var movieTable = txn.objectStore("Movies");
var deleteRequest = movieTable.delete(id)
deleteRequest.onsuccess = function () {
WinJS.log && WinJS.log("Movie Deleted: " + this + ".", "Log", "Status");
};
deleteRequest.onerror = function () {
WinJS && WinJS.log("Failed to delete Movie: " + this + ".", "Log", "error");
};
}
Retrieving Movie Details
Movie details are retrieved either from the database that is part of the user’s collection or from the Rotten Tomatoes database using the REST API. These two actions are perfomaed within the JavaScript functions loadFromDB and loadSearchResult. These functions in turn call the buildMovie function to build a Movie object. The buildMovie function checks the model passed in as a parameter, creates a new Movie object, tries to set its values from the model passed, and returns a bondable object with the help of the WinJS.Binding.as() method (see Listing 4-6).
Listing 4-6. buildMovie Function Creates a Movie Object From the Model
buildMovie: function (model) {
var newMovie = new MyCollection.Movie();
if (model.hasOwnProperty("title")) {
newMovie.setTitle(model.title);
}
if (model.hasOwnProperty("year")) {
newMovie.setYear(model.year);
}
if (model.hasOwnProperty("movieId")) {
newMovie.setID(model.id);
newMovie.setIsInCollection(true);
}
if (model.hasOwnProperty("status")) {
newMovie.setStatus(model.status);
}
if (model.hasOwnProperty("thumbnail")) {
newMovie.setImage(model.thumbnail);
}
if (model.hasOwnProperty("poster")) {
newMovie.setPoster(model.poster);
}
//only if the request from rottentomatoes
if (model.hasOwnProperty("posters")) {
newMovie.setImage(model.posters.thumbnail);
newMovie.setPoster(model.posters.detailed);
}
return new WinJS.Binding.as(newMovie);
}
The loadFromDB function, shown in Listing 4-7, takes string as a parameter and returns an array of Movie objects that matches with the title of the movie stores in the IndexedDB Movie objectStore. Like the saveMovie and deleteMovie methods, we first create a new transaction involving the Movie objectStore and set the mode to read-only, as only the data is retrieved here. Then we open a cursor to iterate over records in the Movie object store. The results are passed through to the success callback on the cursor, where we render the result. The callback is fired only once per result, and will call continue to keep iterating across the data on the result object. A JSON object is constructed out of the result and is passed to the buildMovie function to return a WinJS.Binding object that is finally added to the array.
Listing 4-7. Searches the indexedDB and fill the results to an array
loadFromDB: function (searchText) {
var collection = new Array();
var txn = MyCollection.db.transaction(["Movies"], "readonly");
var movieCursorRequest = txn.objectStore("Movies").openCursor();
movieCursorRequest.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
var data = cursor.value;
if (data.title.indexOf(searchText) > -1) {
var movieData = {
movieId: data.id
, title: data.title
, year: data.year
, thumbnail: data.image
, poster: data.poster
, status: data.status
};
var newMovie = MyCollection.Movie.buildMovie(movieData);
collection.push(newMovie);
}
cursor.continue();
}
};
return collection;
}
Like loadFromDB, the loadSearchResult function shown in Listing 4-8 takes search text as a parameter and returns an array of Movie objects. This function performs two actions. First it queries the Rotten Tomatoes movie database using a public API and loads the result to an array. Next it calls loadFromDB and adds the matching movie object to the existing array. Now the array has objects that match the search result from the IndexedDB Movie object store and also from the Rotten Tomatoes search results.
Listing 4-8. Search the Rotten Tomatoes Database and Add the Results to an Array
loadSearchResult: function (searchText) {
var searchUrl = " http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=XXXXXXXXXXXXXf8&page_limit=10&q= " + searchText;
return WinJS.xhr({ url: searchUrl }).then(
function (result) {
var result = window.JSON.parse(result.responseText).movies;
var collection = new Array();
if (result) {
result.forEach(function (newObject) {
var newMovie = MyCollection.Movie.buildMovieFromRottentomatoes(newObject);
collection.push(newMovie);
});
var txn = MyCollection.db.transaction(["Movies"], "readonly");
var movieCursorRequest = txn.objectStore("Movies").openCursor();
movieCursorRequest.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
var data = cursor.value;
if (data.title.indexOf(searchText) > -1) {
var movieData = {
movieId: data.id
, title: data.title
, year: data.year
, thumbnail: data.image
, poster: data.poster
, status: data.status
};
var newMovie = MyCollection.Movie.buildMovie(movieData);
collection.push(newMovie);
}
cursor.continue();
}
};
}
return collection;
});
}
Designing the App Start Page
Home.html is the start page of this app (see Figure 4-1). It displays the Movies in our IndexedDB collection in a grid layout using the WinJS.UI.ListView element by binding to a Movies Collection in Home.js. We also define an item template that contains the markup to display the details of each movie (see Listing 4-9).
Listing 4-9. Home.html Page Includes a ListView With Item Template to Display Movie Details
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>homePage</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.1.0/js/base.js"></script>
<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
<link href="/css/default.css" rel="stylesheet" />
<link href="/pages/home/home.css" rel="stylesheet" />
<script src="/pages/home/home.js"></script>
</head>
<body>
<!-- The content that will be loaded and displayed. -->
<div id="dbItemtemplate"
class="itemtemplate"
data-win-control="WinJS.Binding.Template">
<div class="item">
<img
class="item-image"
src="#"
data-win-bind="src: image; alt: title" />
<div class="item-content">
<h3
class="item-title win-type-x-small win-type-ellipsis"
data-win-bind="innerHTML: title"></h3>
<h4
class="item-subtitle win-type-x-small win-type-ellipsis"
data-win-bind="innerHTML: year"></h4>
<h4
class="item-subtitle win-type-x-small win-type-ellipsis"
data-win-bind="innerHTML: status"></h4>
</div>
</div>
</div>
<div class="fragment homepage">
<header
aria-label="Header content"
role="banner">
<button
class="win-backbutton"
aria-label="Back"
disabled type="button"></button>
<h1 class="titlearea win-type-ellipsis">
<span class="pagetitle">My Collections!</span>
</h1>
</header>
<section aria-label="Main content" role="main">
<div
id="listView"
class="resultslist win-selectionstylefilled"
aria-label="Movies in my collection"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemTemplate: select('#dbItemtemplate'),
}">
}"></div>
</section>
</div>
</body>
</html>
Home.js
Home.js is where we write additional code that provides interactivity for our home.html page. This script file only implements the ready function. This function is called at the time of the page load. Inside this function we iterate through the IndexedDB Movies object store and store it into an array and will bind that array to the ListView, as shown in Listing 4-10. This page also has an itemInvoked function that is attached to the ListView and is called when an item is selected from the ListView. Once called, this function navigates the user to the MovieDetail.html (see Listing 4-11) using the WinJS.Navigation.navigate function. This function takes the detail page location and selected movie object as parameters.
Listing 4-10. Movies in the Collection Are Bound to the ListView Element
(function () {
"use strict";
WinJS.UI.Pages.define("/pages/home/home.html", {
// This function is called whenever a user navigates to this page. It
// populates the page elements with the app's data.
ready: function (element, options) {
var listView = element.querySelector(".resultslist").winControl;
var tapBehavior = listView.tapBehavior;
listView.tapBehavior = tapBehavior;
listView.oniteminvoked = this._itemInvoked;
var collection = new Array();
var txn = MyCollection.db.transaction(["Movies"], "readonly");
var movieCursorRequest = txn.objectStore("Movies").openCursor();
movieCursorRequest.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
var data = cursor.value;
var movieData = {
movieId: data.id
, title: data.title
, year: data.year
, thumbnail: data.image
, poster: data.poster
, status: data.status
};
var newMovie = MyCollection.Movie.buildMovie(movieData);
collection.push(newMovie);
cursor.continue();
}
else {
listView.itemDataSource = new WinJS.Binding.List(collection).dataSource;
}
}
},
_itemInvoked: function (args) {
args.detail.itemPromise.done(function itemInvoked(item) {
WinJS.Navigation.navigate("/pages/details/movieDetail.html"
, { movieDetail: item.data });
});
},
});
})();
Designing the Movie Detail Page
The MovieDetail.html page is redirected either from the home or search result page and displays the details of the selected movies from its previous page. MovieDetail.html also provides an option to add or edit the movie object. The markup of this page contains an HTML element that is bound to the properties of the Movie object using the WinJS data-win-bind property. This page also has two app bar buttons that allow us to save or delete the Movie object, as shown in Figure 4-5.
Listing 4-11. MovieDetail.html With HTML Elements to Display Selected Movie Details
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>movieDetail</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.1.0/js/base.js"></script>
<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
<link href="/pages/details/movieDetail.css" rel="stylesheet" />
<script src="/pages/details/movieDetail.js"></script>
</head>
<body>
<div class="movieDetail fragment">
<header
aria-label="Header content"
role="banner">
<button
class="win-backbutton"
aria-label="Back"
disabled
type="button" />
<h1
class="titlearea win-type-ellipsis">
<span class="pagetitle">My Collection</span>
</h1>
</header>
<section
aria-label="Main content"
role="main">
<div
id="divDetail"
class="detailView">
<h3 id="title">Edit Movie</h3>
<br />
<!--Movie Image-->
<img
src="#"
data-win-bind="src: poster; alt: title" />
<!--Movie Title-->
<label>Title</label>
<input
id="txtTitle"
type="text"
data-win-bind="value: title Binding.Mode.twoway" />
<br>
<!--Movie Release Year-->
<label>Year</label>
<input
id="txtYear"
type="text"
data-win-bind="value: year Binding.Mode.twoway" />
<br>
<!--Movie Status-->
<label>Status</label>
<select
id="status"
data-win-bind="selected: status; value: status Binding.Mode.twoway">
<option value="Avaliable">Avaliable</option>
<option value="Lend Out">Lend Out</option>
<option value="Rented">Borrowed</option>
</select>
<br />
</div>
</section>
</div>
<!--App bar-->
<div
data-win-control="WinJS.UI.AppBar"
class="appBar"
id="appBar">
<!--Save Movie Button-->
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{id:'saveButton', label:'Save', icon:'save',section:'global'}"/>
<!--Delete Movie Button-->
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{id:'deleteButton', label:'Delete', icon:'delete',section:'global'}"/>
</div>
</body>
</html>
Figure 4-5. The My Collections app displaying movie details along with the app bar for saving and deleting
MovieDetail.js
Here we bind the Movie object that is passed on to this page to the div element inside the page ready function. This function, shown in Listing 4-12, also sets the event handler for the save and delete button which in turn calls the saveMovie (see Listing 4-4) and deleteMovie (see Listing 4-5) functions in Movie.js.
Listing 4-12. Binds the Movie Object to the div Element for Adding or Deleting
WinJS.UI.Pages.define("/pages/details/movieDetail.html", {
// This function is called whenever a user navigates to this page. It
// populates the page elements with the app's data.
ready: function (element, options) {
// TODO: Initialize the page here.
movieDetail = options.movieDetail;
var src = WinJS.Binding.as(movieDetail);
var form = document.getElementById("divDetail");
WinJS.Binding.processAll(form, src);
document.getElementById("saveButton")
.addEventListener("click", doClickSave, false);
document.getElementById("deleteButton")
.addEventListener("click", doClickDelete, false);
if (movieDetail.fromSearch == true) {
document.getElementById("deleteButton").disabled = true;
document.getElementById("title").innerText = "Add to collection";
}
}
});
function doClickSave() {
MyCollection.Movie.saveMovie(movieDetail.id, movieDetail.title, movieDetail.year, movieDetail.image, movieDetail.poster, movieDetail.status);
WinJS.Navigation.back();
}
function doClickDelete() {
MyCollection.Movie.deleteMovie(movieDetail.id);
WinJS.Navigation.back();
}
Even though we bound the elements to the Movie object properties, the changes we make to the element are not reflected in the Movie object as WinJS doesn’t support two-way binding, but adding this functionality is quite easy thanks to the WinJS.Binding.initializer function. This function gets involved when binding is created. WinJS.Binding.initializer give access to both source and target objects and their properties, which allows them to subscribe to the target element’s events and push data to the source object, as shown in Listing 4-13.
Listing 4-13. Defining Two-Way Binding Using WinJS.Binding.initializer
WinJS.Namespace.define("Binding.Mode", {
twoway: WinJS.Binding.initializer(function (source, sourceProps, dest, destProps) {
WinJS.Binding.defaultBind(source, sourceProps, dest, destProps);
dest.onchange = function () {
var d = dest[destProps[0]];
var s = source[sourceProps[0]];
if (s !== d) source[sourceProps[0]] = d;
}
})
});
Once defined, we just need to apply the initializer to binding like the one shown in Listing 4-14.
Listing 4-14. Enabling Two-Way Binding in the HTML Element
<input type="text" data-win-bind="value: title Binding.Mode.twoway" />
Searching for Movies
The searchResults.html page is invoked when we do the app-level search using the Search charm. This page displays the matching results in a ListView element (see Listing 4-15). The result is shown in Figure 4-6. Displaying movie information in ListView is very similar to Home.html with one exception: Here we use two item templates, one for displaying movie details from the Rotten Tomatoes database and another from the IndexedDB that is dynamically switched using the JavaScript code in searchResult.js as shown in Listing 4-16.
Listing 4-15. Search Page Displays Results in a ListView When Invoked From Search Charm
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="ms-design-extensionType" content="Search" />
<title>Search Contract</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.1.0/js/base.js"></script>
<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
<link href="/css/default.css" rel="stylesheet" />
<link href="/pages/search/searchResults.css" rel="stylesheet" />
<script src="/js/data.js"></script>
</head>
<body>
<!-- This template is used to display each item in the ListView declared
below. -->
<!--ItemTemplate to display search from Rotten Tomatoes database -->
<div
id="onlineItemtemplate"
class="itemtemplate"
data-win-control="WinJS.Binding.Template">
<div class="item">
<img
class="item-image"
src="#" data-win-bind="src: image; alt: title" />
<div class="item-content">
<h3
class="item-title win-type-x-small win-type-ellipsis"
data-win-bind="innerHTML: title searchResults.title" />
<h4
class="item-subtitle win-type-x-small win-type-ellipsis"
data-win-bind="innerHTML: year searchResults.text" />
</div>
</div>
</div>
<!--ItemTemplate to display search from IndexedDB -->
<div id="dbItemtemplate"
class="itemtemplate"
data-win-control="WinJS.Binding.Template">
<div class="item">
<img
class="item-image"
src="#" data-win-bind="src: poster; alt: title" />
<div class="item-content">
<h3
class="item-title win-type-x-small win-type-ellipsis"
data-win-bind="innerHTML: title searchResults.title" />
<h4
class="item-subtitle win-type-x-small win-type-ellipsis"
data-win-bind="innerHTML: year searchResults.text" />
<h4
class="item-subtitle win-type-x-small win-type-ellipsis"
data-win-bind="innerHTML: status searchResults.text" />
</div>
</div>
</div>
<!-- The content that will be loaded and displayed. -->
<div class="searchResults fragment">
<!--Page Header-->
<header
aria-label="Header content"
role="banner">
<button
class="win-backbutton"
aria-label="Back"
disabled
type="button" />
<div class="titlearea">
<h1 class="pagetitle win-type-ellipsis" />
<h2 class="pagesubtitle win-type-ellipsis" />
</div>
</header>
<section
aria-label="Main content"
role="main">
<div
class="resultsmessage win-type-x-large">
No results match your search.
</div>
<!--Filter section-->
<div class="filterarea">
<ul class="filterbar"></ul>
<select class="filterselect" />
</div>
<!--ListView-->
<div
id="searchListView"
class="resultslist win-selectionstylefilled"
aria-label="Search results"
data-win-control="WinJS.UI.ListView" />
</section>
</div>
</body>
</html>
Figure 4-6. My Collections app displaying search results
The searchResult.js page does basically three things: gets the result data and binds it to the ListView, dynamically switches the template, and finally generates filters to the page so the user can filter the result by online and in collection.
Dynamic template change (see Listing 4-16) can be achieved by assigning the function itemTemplateFunction to the ListLiew itemTemplate property. This function returns a DOM element depending on the value of the Movie object isInCollection property.
Listing 4-16. Dynamically Switching the Template
ready: function (element, options) {
var listView = element.querySelector(".resultslist").winControl;
// listView.itemTemplate = element.querySelector(".itemtemplate");
listView.oniteminvoked = this._itemInvoked;
listView.itemTemplate = itemTemplateFunction;
this._handleQuery(element, options);
listView.element.focus();
}
function itemTemplateFunction(itemPromise) {
return itemPromise.then(function (item) {
var itemTemplate = document.getElementById("onlineItemtemplate");
if (item.data.isInCollection) {
itemTemplate = document.getElementById("dbItemtemplate");
};
var container = document.createElement("div");
itemTemplate.winControl.render(item.data, container);
return container;
});
}
Getting the Data
When we add the search contract page, Visual Studio includes the necessary code to fulfill the minimum requirements of the Search contract automatically. There are two functions in searchResults.js that are of interest to us, _handleQuery and _searchData (see Listing 4-17). The _handleQuery in turn calls the function _searchData. In the _searchData function we populate a WinJS.Binding.List with the search data by calling the methods loadFromDB and loadSearchResult. Apart from these two functions, we also call _generateFilters and the _populateFilterBar functions.
Listing 4-17. Handling Search Query and Loading the Result in an Array
_handleQuery: function (element, args) {
var originalResults;
this._lastSearch = args.queryText;
WinJS.Namespace.define("searchResults"
, {
markText: WinJS.Binding.converter(this._markText.bind(this))
});
this._initializeLayout(element.querySelector(".resultslist").winControl
, Windows.UI.ViewManagement.ApplicationView.value);
this._generateFilters();
this._searchData(args.queryText, element, this);
},
// This function populates a WinJS.Binding.List with search results for the
// provided query.
_searchData: function (queryText, element, object) {
var originalResults;
originalResults = MyCollection.Movie.loadFromDB(queryText);
MyCollection.Movie.loadSearchResult(queryText).done(
function (result) {
for (var i = 0; i < result.length; i++)
{
originalResults.push(result[i]);
}
originalResults = new WinJS.Binding.List(originalResults);
if (originalResults.length === 0) {
document.querySelector('.filterarea').style.display = "none";
} else {
document.querySelector('.resultsmessage').style.display = "none";
}
object._populateFilterBar(element, originalResults);
object._applyFilter(object._filters[0], originalResults);
return originalResults;
});
}
});
The filters are created in the _generateFilters function. Depending on the value of the isInCollection property, we add two filters for the search result, as shown in Listing 4-18: one to showcase the Rotten Tomatoes matches and the other to show the matches from the Indexed database.
Listing 4-18. Creating Filters for the Search Result
generateFilters: function () {
this._filters = [];
this._filters.push(
{
results: null
, text: "All"
, predicate: function (item) {
return true;
}
});
this._filters.push(
{
results: null
, text: "Online"
, predicate: function (item) {
return item.isInCollection === true;
}
});
this._filters.push(
{
results: null
, text: "In My Collection"
, predicate: function (item) {
return item.isInCollection !== true;
}
});
}
Now with all the codes in place, when we run the My Collections app, at first it shows an empty Start screen, but we can search for movies using the Search charm and add them to our movie collection. Once they are added, the Start screen will look like the one shown in Figure 4-1.
Ideas for Improvement
The My Collections app can be worked on and improved to make it a fully functional inventory app. Here are some of the features that can be added:
Conclusion
In this chapter we learned to use IndexedDB as a local storage option by creating a Windows 8 JavaScript app. As we saw, the IndexedDB API is a simple but powerful option for storing data locally, even though it is a little bit different from the relational database.
There are also some IndexedDB wrappers like IDBWrapper (https://github.com/jensarps/IDBWrapper) that can be used to ease the use of IndexedDB. In the next chapter, we continue to explore local storage options by learning to use Jet API and Application Storage.
3.133.141.219