Metro Walkthrough: Creating a Task List with a ListView and IndexedDB
- by Stephen.Walther
The goal of this blog entry is to describe how you can work with data in a Metro style application written with JavaScript. In particular, we create a super simple Task List application which enables you to create and delete tasks.
Here’s a video which demonstrates how the Task List application works:
In order to build this application, I had to take advantage of several features of the WinJS library and technologies including:
IndexedDB – The Task List application stores data in an IndexedDB database.
HTML5 Form Validation – The Task List application uses HTML5 validation to ensure that a required field has a value.
ListView Control – The Task List application displays the tasks retrieved from the IndexedDB database in a WinJS ListView control.
Creating the IndexedDB Database
The Task List application stores all of its data in an IndexedDB database named TasksDB. This database is opened/created with the following code:
var db;
var req = window.msIndexedDB.open("TasksDB", 1);
req.onerror = function () {
console.log("Could not open database");
};
req.onupgradeneeded = function (evt) {
var newDB = evt.target.result;
newDB.createObjectStore("tasks", { keyPath: "id", autoIncrement:true });
};
The msIndexedDB.open() method accepts two parameters: the name of the database to open and the version of the database to open. If a database with a matching version already exists, then calling the msIndexedDB.open() method opens a connection to the existing database. If the database does not exist then the upgradeneeded event is raised.
You handle the upgradeneeded event to create a new database. In the code above, the upgradeneeded event handler creates an object store named “tasks” (An object store roughly corresponds to a database table). When you add items to the tasks object store then each item gets an id property with an auto-incremented value automatically.
The code above also includes an error event handler. If the IndexedDB database cannot be opened or created, for whatever reason, then an error message is written to the Visual Studio JavaScript Console window.
Displaying a List of Tasks
The TaskList application retrieves its list of tasks from the tasks object store, which we created above, and displays the list of tasks in a ListView control. Here is how the ListView control is declared:
<div id="tasksListView"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemDataSource: TaskList.tasks.dataSource,
itemTemplate: select('#taskTemplate'),
tapBehavior: 'toggleSelect',
selectionMode: 'multi',
layout: { type: WinJS.UI.ListLayout }
}">
</div>
The ListView control is bound to the TaskList.tasks.dataSource data source. The TaskList.tasks.dataSource is created with the following code:
// Create the data source
var tasks = new WinJS.Binding.List();
// Open the database
var db;
var req = window.msIndexedDB.open("TasksDB", 1);
req.onerror = function () {
console.log("Could not open database");
};
req.onupgradeneeded = function (evt) {
var newDB = evt.target.result;
newDB.createObjectStore("tasks", { keyPath: "id", autoIncrement:true });
};
// Load the data source with data from the database
req.onsuccess = function () {
db = req.result;
var tran = db.transaction("tasks");
tran.objectStore("tasks").openCursor().onsuccess = function(event) {
var cursor = event.target.result;
tasks.dataSource.beginEdits();
if (cursor) {
tasks.dataSource.insertAtEnd(null, cursor.value);
cursor.continue();
} else {
tasks.dataSource.endEdits();
};
};
};
// Expose the data source and functions
WinJS.Namespace.define("TaskList", {
tasks: tasks
});
Notice the success event handler. This handler is called when a database is successfully opened/created. In the code above, all of the items from the tasks object store are retrieved into a cursor and added to a WinJS.Binding.List object named tasks.
Because the ListView control is bound to the WinJS.Binding.List object, copying the tasks from the object store into the WinJS.Binding.List object causes the tasks to appear in the ListView:
Adding a New Task
You add a new task in the Task List application by entering the title of a new task into an HTML form and clicking the Add button. Here’s the markup for creating the form:
<form id="addTaskForm">
<input id="newTaskTitle" title="New Task" required />
<button>Add</button>
</form>
Notice that the INPUT element includes a required attribute. In a Metro application, you can take advantage of HTML5 Validation to validate form fields. If you don’t enter a value for the newTaskTitle field then the following validation error message is displayed:
For a brief introduction to HTML5 validation, see my previous blog entry:
http://stephenwalther.com/blog/archive/2012/03/13/html5-form-validation.aspx
When you click the Add button, the form is submitted and the form submit event is raised. The following code is executed in the default.js file:
// Handle Add Task
document.getElementById("addTaskForm").addEventListener("submit", function (evt) {
evt.preventDefault();
var newTaskTitle = document.getElementById("newTaskTitle");
TaskList.addTask({ title: newTaskTitle.value });
newTaskTitle.value = "";
});
The code above retrieves the title of the new task and calls the addTask() method in the tasks.js file. Here’s the code for the addTask() method which is responsible for actually adding the new task to the IndexedDB database:
// Add a new task
function addTask(taskToAdd) {
var transaction = db.transaction("tasks", IDBTransaction.READ_WRITE);
var addRequest = transaction.objectStore("tasks").add(taskToAdd);
addRequest.onsuccess = function (evt) {
taskToAdd.id = evt.target.result;
tasks.dataSource.insertAtEnd(null, taskToAdd);
}
}
The code above does two things. First, it adds the new task to the tasks object store in the IndexedDB database. Second, it adds the new task to the data source bound to the ListView. The dataSource.insertAtEnd() method is called to add the new task to the data source so the new task will appear in the ListView (with a nice little animation).
Deleting Existing Tasks
The Task List application enables you to select one or more tasks by clicking or tapping on one or more tasks in the ListView. When you click the Delete button, the selected tasks are removed from both the IndexedDB database and the ListView.
For example, in the following screenshot, two tasks are selected. The selected tasks appear with a teal background and a checkmark:
When you click the Delete button, the following code in the default.js file is executed:
// Handle Delete Tasks
document.getElementById("btnDeleteTasks").addEventListener("click", function (evt) {
tasksListView.winControl.selection.getItems().then(function(items) {
items.forEach(function (item) {
TaskList.deleteTask(item);
});
});
});
The selected tasks are retrieved with the TaskList selection.getItem() method. In the code above, the deleteTask() method is called for each of the selected tasks.
Here’s the code for the deleteTask() method:
// Delete an existing task
function deleteTask(listViewItem) {
// Database key != ListView key
var dbKey = listViewItem.data.id;
var listViewKey = listViewItem.key;
// Remove item from db and, if success, remove item from ListView
var transaction = db.transaction("tasks", IDBTransaction.READ_WRITE);
var deleteRequest = transaction.objectStore("tasks").delete(dbKey);
deleteRequest.onsuccess = function () {
tasks.dataSource.remove(listViewKey);
}
}
This code does two things: it deletes the existing task from the database and removes the existing task from the ListView. In both cases, the right task is removed by using the key associated with the task. However, the task key is different in the case of the database and in the case of the ListView.
In the case of the database, the task key is the value of the task id property. In the case of the ListView, on the other hand, the task key is auto-generated by the ListView.
When the task is removed from the ListView, an animation is used to collapse the tasks which appear above and below the task which was removed.
The Complete Code
Above, I did a lot of jumping around between different files in the application and I left out sections of code. For the sake of completeness, I want to include the entire code here: the default.html, default.js, and tasks.js files.
Here are the contents of the default.html file. This file contains the UI for the Task List application:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Task List</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.0.6/css/ui-dark.css" rel="stylesheet">
<script src="//Microsoft.WinJS.0.6/js/base.js"></script>
<script src="//Microsoft.WinJS.0.6/js/ui.js"></script>
<!-- TaskList references -->
<link href="/css/default.css" rel="stylesheet">
<script src="/js/default.js"></script>
<script type="text/javascript" src="js/tasks.js"></script>
<style type="text/css">
body {
font-size: x-large;
}
form {
display: inline;
}
#appContainer {
margin: 20px;
width: 600px;
}
.win-container {
padding: 10px;
}
</style>
</head>
<body>
<div>
<!-- Templates -->
<div id="taskTemplate"
data-win-control="WinJS.Binding.Template">
<div>
<span data-win-bind="innerText:title"></span>
</div>
</div>
<h1>Super Task List</h1>
<div id="appContainer">
<form id="addTaskForm">
<input id="newTaskTitle" title="New Task" required />
<button>Add</button>
</form>
<button id="btnDeleteTasks">Delete</button>
<div id="tasksListView"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemDataSource: TaskList.tasks.dataSource,
itemTemplate: select('#taskTemplate'),
tapBehavior: 'toggleSelect',
selectionMode: 'multi',
layout: { type: WinJS.UI.ListLayout }
}">
</div>
</div>
</div>
</body>
</html>
Here is the code for the default.js file. This code wires up the Add Task form and Delete button:
(function () {
"use strict";
var app = WinJS.Application;
app.onactivated = function (eventObject) {
if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {
WinJS.UI.processAll().then(function () {
// Get reference to Tasks ListView
var tasksListView = document.getElementById("tasksListView");
// Handle Add Task
document.getElementById("addTaskForm").addEventListener("submit", function (evt) {
evt.preventDefault();
var newTaskTitle = document.getElementById("newTaskTitle");
TaskList.addTask({ title: newTaskTitle.value });
newTaskTitle.value = "";
});
// Handle Delete Tasks
document.getElementById("btnDeleteTasks").addEventListener("click", function (evt) {
tasksListView.winControl.selection.getItems().then(function(items) {
items.forEach(function (item) {
TaskList.deleteTask(item);
});
});
});
});
}
};
app.start();
})();
Finally, here is the tasks.js file. This file contains all of the code for opening, creating, and interacting with IndexedDB:
(function () {
"use strict";
// Create the data source
var tasks = new WinJS.Binding.List();
// Open the database
var db;
var req = window.msIndexedDB.open("TasksDB", 1);
req.onerror = function () {
console.log("Could not open database");
};
req.onupgradeneeded = function (evt) {
var newDB = evt.target.result;
newDB.createObjectStore("tasks", { keyPath: "id", autoIncrement:true });
};
// Load the data source with data from the database
req.onsuccess = function () {
db = req.result;
var tran = db.transaction("tasks");
tran.objectStore("tasks").openCursor().onsuccess = function(event) {
var cursor = event.target.result;
tasks.dataSource.beginEdits();
if (cursor) {
tasks.dataSource.insertAtEnd(null, cursor.value);
cursor.continue();
} else {
tasks.dataSource.endEdits();
};
};
};
// Add a new task
function addTask(taskToAdd) {
var transaction = db.transaction("tasks", IDBTransaction.READ_WRITE);
var addRequest = transaction.objectStore("tasks").add(taskToAdd);
addRequest.onsuccess = function (evt) {
taskToAdd.id = evt.target.result;
tasks.dataSource.insertAtEnd(null, taskToAdd);
}
}
// Delete an existing task
function deleteTask(listViewItem) {
// Database key != ListView key
var dbKey = listViewItem.data.id;
var listViewKey = listViewItem.key;
// Remove item from db and, if success, remove item from ListView
var transaction = db.transaction("tasks", IDBTransaction.READ_WRITE);
var deleteRequest = transaction.objectStore("tasks").delete(dbKey);
deleteRequest.onsuccess = function () {
tasks.dataSource.remove(listViewKey);
}
}
// Expose the data source and functions
WinJS.Namespace.define("TaskList", {
tasks: tasks,
addTask: addTask,
deleteTask: deleteTask
});
})();
Summary
I wrote this blog entry because I wanted to create a walkthrough of building a simple database-driven application. In particular, I wanted to demonstrate how you can use a ListView control with an IndexedDB database to store and retrieve database data.