AngularJS IndexedDB Demo

This post is more than 2 years old.

Over the past few months I've had a series of articles (Part 1, Part 2, Part 3) discussing IndexedDB. In the last article I built a full, if rather simple, application that let you write notes. (I'm a sucker for note taking applications.) When I built the application, I intentionally did not use a framework. I tried to write nice, clear code of course, but I wanted to avoid anything that wasn't 100% necessary to demonstrate the application and IndexedDB. In the perspective of an article, I think this was the right decision to make. I wanted my readers to focus on the feature and not anything else. But I thought this would be an excellent opportunity to try AngularJS again.

For the most part, this conversion worked perfectly. This may sound lame, but I found myself grinning as I built this application. I'm a firm believer that if something makes you happy then it is probably good for you. ;)

I still find myself a bit... not confused... but slowed down by the module system and dependency injection. These are both things I grasp in general, but in AngularJS they feel a bit awkward to me. It feels like something I'll never be able to code from memory, but will need to reference older applications to remind me. I'm not saying they are wrong of course, they just don't feel natural to me yet.

On the flip side, the binding support is incredible. I love working with HTML templates and $scope. It feels incredibly powerful. Heck, being able to add an input field and use it as a filter in approximately 30 seconds was mind blowing.

One issue I ran into and I'm not convinced I created the best solution for was the async nature of IndexedDB's database open logic. AngularJS has a promises library built in and it works incredibly well for my application in general. But I needed the entire application to be bootstrapped to an async call for database startup. I got around that with two things that felt a bit like a hack.

First, my home view (get all notes) ran a call to an init function to ensure the db was already open. So consider this init():

function init() {
		var deferred = $q.defer();

		if(setUp) {
			deferred.resolve(true);
			return deferred.promise;
		}
		
		var openRequest = window.indexedDB.open("indexeddb_angular",1);
	
		openRequest.onerror = function(e) {
			console.log("Error opening db");
			console.dir(e);
			deferred.reject(e.toString());
		};

		openRequest.onupgradeneeded = function(e) {
	
			var thisDb = e.target.result;
			var objectStore;
			
			//Create Note OS
			if(!thisDb.objectStoreNames.contains("note")) {
				objectStore = thisDb.createObjectStore("note", { keyPath: "id", autoIncrement:true });
				objectStore.createIndex("titlelc", "titlelc", { unique: false });
				objectStore.createIndex("tags","tags", {unique:false,multiEntry:true});
			}
	
		};

		openRequest.onsuccess = function(e) {
			db = e.target.result;
			
			db.onerror = function(event) {
				// Generic error handler for all errors targeted at this database's
				// requests!
				deferred.reject("Database error: " + event.target.errorCode);
			};
	
			setUp=true;
			deferred.resolve(true);
		
		};	

		return deferred.promise;
	}

This logic is similar to what I had in the non-framework app but I've made use of promises and a flag to remember when I've already opened the database. This lets me then tie to init() in my getNotes logic.

	function getNotes() {
		var deferred = $q.defer();
		
		init().then(function() {

			var result = [];

			var handleResult = function(event) {  
				var cursor = event.target.result;
				if (cursor) {
					result.push({key:cursor.key, title:cursor.value.title, updated:cursor.value.updated});
					cursor.continue();
				}
			};  
			
			var transaction = db.transaction(["note"], "readonly");  
			var objectStore = transaction.objectStore("note");
            objectStore.openCursor().onsuccess = handleResult;

			transaction.oncomplete = function(event) {
				deferred.resolve(result);
			};
		
		});
		return deferred.promise;
	}

All of this worked ok - but I ran into an issue on the other pages of my application. If for example you bookmarked the edit link for a note, you would run into an error. I could have applied the same fix in my service layer (run init first), but it just felt wrong. So instead I did this in my app.js:

$rootScope.$on("$routeChangeStart", function(event,currentRoute, previousRoute){
		if(!persistanceService.ready() && $location.path() != '/home') {
			$location.path('/home');
		};

	});

The ready call was simply a wrapper to the flag variable. So yeah, this worked for me, but I still think there is (probably) a nicer solution. Anyway, if you want to check it out, just hit the Demo link below. I want to give a shoutout to Sharon DiOrio for giving me a lot of help/tips/support while I built this app.

p.s. I assume this is obvious, but I'm not really offering this up as a "Best Practices" AngularJS application. I assume I could have done about every part better. ;)

Raymond Camden's Picture

About Raymond Camden

Raymond is a developer advocate for HERE Technologies. He focuses on JavaScript, serverless and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Laurence posted on 2/11/2014 at 3:23 AM

The demo appears to be kaputt in Chrome.

Comment 2 by Raymond Camden posted on 2/11/2014 at 3:24 AM

Working for me. What error do you see in DevTools?

Comment 3 by Laurence posted on 2/11/2014 at 3:51 AM

My mistake. I assumed that because the Resources tab listed IndexedDB that the browser supported it (go figure), but when I checked the actual version, I only add v22.0.xxx. The error in the console is "TypeError: Cannot call method 'open' of undefined" not that it matters now.

Comment 4 by Raymond Camden posted on 2/11/2014 at 3:54 AM

Well, this is a great comment though. I really should have added an error for it.

Comment 5 by Raymond Camden posted on 2/11/2014 at 4:11 AM

So I have a fixed version but am having difficulty hitting my FTP server inside my corp network. I'll push up a fix later tonight. Thanks for pointing this out.

Comment 6 by Raymond Camden posted on 2/11/2014 at 8:25 AM

I pushed up a fix - let me know if it works for you. (And by fix I mean it should at least tell you that your browser isn't supported.)

Comment 7 by Laurence posted on 2/11/2014 at 8:28 PM

Looks good. The demo is showing the error message now.

Comment 8 by Jens posted on 2/25/2014 at 1:13 AM

Great article. We build a webapp with offline support (20.000 records) for mobile (websql) and desktop(indexeddb) and I would love to try this approach when I ever get around rebuilding this based on a proper framework

Comment 9 by Thiago Costa posted on 3/18/2014 at 6:54 PM

Hello. I'm from Brazil.
First, congratulations for post.

I've demand that I need synchronize Local Storage with server side, more exactly with indexeddb.
In this case, my server side is Java.
Do you know any example about it ?
I didn't find out about it.
I really appreciate all the help you'll give me. Or suggestions how to resolve this.

Other thing.. I tried to copy your demo 'AngularJS IndexedDB Demo' but It doesn't work.
It appears trouble with angular.js. Do you have code in Git repository ?

Thanks a lot.
Sorry my poor english, I've been studying for 2 months :)

Comment 10 by Raymond Camden posted on 3/19/2014 at 1:37 AM

"In this case, my server side is Java.
Do you know any example about it ?"
Nope. Sorry. In theory it isn't too complex. I know how it would be done with ColdFusion, and can imagine it in Node, and the process is relatively the same for each. The difference is just in how you build it.

"Other thing.. I tried to copy your demo 'AngularJS IndexedDB Demo' but It doesn't work.
It appears trouble with angular.js. Do you have code in Git repository ? "

Can you describe *how* it doesn't work? What error do you get exactly?

Comment 11 by Thiago Costa posted on 3/19/2014 at 4:39 PM

Thanks for answer.
I've been searching for examples, anyway thanks a lot.

When I had copied your code shows me error about javascript, but it wasn't.
But I'd seen this morning and the code source has reference to files 'partial/home.html' 'partial/edit.html' and so on.
I think it could be problem.

If necessary, I can send you an email with content in annex.
Now, The example doesn't show problem on firebug, but it doesn't work too, only blank page.

Comment 12 by Thiago Costa posted on 3/19/2014 at 5:31 PM

I forget. I'd commented where there were references to folder 'partial/xxx.html'

Comment 13 by Raymond Camden posted on 3/19/2014 at 5:37 PM

Please share the URL of where I can test the app. Please share it here though so others can see as well (and may be able to respond quicker than I can today).

Comment 14 by Raymond Camden posted on 3/19/2014 at 10:10 PM

Ah, you don't have all the files. It is a pain to make you grab all the files by hand, so I'll make a zip.

Comment 15 by Raymond Camden posted on 3/19/2014 at 10:12 PM
Comment 16 by Thiago Costa posted on 3/19/2014 at 10:23 PM

Sorry, I'm newbie yet.
Thanks for attention.

Comment 17 by laza posted on 7/21/2014 at 5:23 PM

Hi!

I'm fresh developper, how can I synchronise front db to my server db using api-service.

Thx in advance.

Comment 18 by Raymond Camden posted on 7/21/2014 at 5:45 PM

Your question is kinda big, I'll answer at a high level, but obviously it depends on your back end service too. Also, it depends on how you want to handle conflicts. Is the server the 'master' and your client side db just a slave? if so, you need to find out what changed on the server and make those changes to the client, including new content, edited content, and deleted content.

Comment 19 by Krishna posted on 2/1/2015 at 4:21 AM

Very nice tutorial. Makes for a great starter app as well. Thanks much!

Comment 20 by Andy Myers posted on 6/6/2015 at 6:38 AM

As always, just what I needed Ray. Thanks for sharing - your site is invaluable to me!

Comment 21 (In reply to #20) by Raymond Camden posted on 6/6/2015 at 12:24 PM

You are most welcome.

Comment 22 by Stephen Cooper posted on 6/24/2015 at 12:27 PM

Brilliant, thanks for sharing.

I'd read your other three posts before finding this. Promises make working with indexedDB so much easier. Shame we still have to support browsers that wont be ES6 compliant, at least Angular helps there.

There were a couple of hiccups i noticed though. Your indexedDB service doesn't always reject the promise in some of its requests, the getNote for example. Meaning if the read failed the caller's catch wont be called (Unless it throws an exception if you don't register an onerror handler, which angular will catch). Also in the save method, the add you save the getTime() of a date object whereas in the put your store the date object itself. Does indexdDB store the actual date object, or a string like localStorage?

Also on a side note of angular the methods where you call init first (getNotes for example) you don't need to create a new defered promise, simply return the promise chain you created after the init function and your controller can continue chaining the promises.

Comment 23 (In reply to #22) by Raymond Camden posted on 6/24/2015 at 7:24 PM

Yeah, I kinda get lazy w/ error handling - and I really shouldn't.

"Also in the save method, the add you save the getTime() of a date object whereas in the put your store the date object itself. Does indexdDB store the actual date object, or a string like localStorage?"

I honestly don't remember, but it smells like a bug in the put section. It should be consistent.

Good point on the init thing too!

Comment 24 by Abhishek Pandey posted on 8/23/2016 at 6:28 AM

How to implement paging in IndexedDb.

Comment 25 (In reply to #24) by Raymond Camden posted on 8/23/2016 at 2:31 PM
Comment 26 (In reply to #25) by Abhishek Pandey posted on 8/24/2016 at 4:24 AM

Dear Sir, I have used following code but it not worked.......

this.getAllHeads = function (pageSize, skipCount) {
var deferred = $q.defer();

if (db === null) {
deferred.reject("DB is not opened yet!");
}
else {
var trans = db.transaction(["M_Head"], "readwrite");
var store = trans.objectStore("M_Head");
var HeadList = []; var idx = 0;

// Get everything in the store;
var keyRange = IDBKeyRange.lowerBound(0);
var cursorRequest = store.openCursor(keyRange);

cursorRequest.onsuccess = function (e) {
debugger;
var result = e.target.result;
if (result === null || result === undefined) {
deferred.resolve(HeadList);
}
else {

if (skipCount <= idx && idx < pageSize + skipCount)
// HeadList.push(result.value);

idx++;

if (idx >= pageSize + skipCount + 1) {
return deferred.promise;
}
else {

HeadList.push(result.value);
if (result.value.Id > lastIndex) {
lastIndex = result.value.Id;
}
result.continue();
}

}
};

cursorRequest.onerror = function (e) {
// console.log(e.value);
deferred.reject("Something went wrong!!! " + e.value);
};
}

return deferred.promise;
};

Comment 27 (In reply to #26) by Raymond Camden posted on 8/24/2016 at 1:07 PM

This really isn't the place to discuss this - you may want to reply to the person on SO who suggested the cursor idea.