How To Use LokiJS For Local Storage In Your Ionic App
A few months ago I wrote a tutorial on how to use PouchDB for local storage in Ionic apps.
I've recently come across another library called LokiJS that promises fast performance because it uses an in-memory database.
In this tutorial we'll take the same app I used in the PouchDB tutorial and build it with LokiJS instead.
###What is the difference between LokiJS and PouchDB? LokiJS creates an in-memory database and allows you to query it efficiently using an API that is similar to MongoDB. You can persist the database with IndexedDB, as a JSON file or write your own adapter to store it.
One of the reasons LokiJS was created, was to offer an alternative to SQLite for Cordova apps.
LokiJS doesn't have advanced synchronization capabilities, but there is a Changes API, which keeps track of all the changes made to the local database. You can use this API to synchronize your database.
PouchDB also allows you to create a database locally and has several adapters for persisting the data, using WebSQL, IndexedDB and SQLite, but the most interesting feature is that it supports seamless synchronization with a CouchDB database.
###Which one should I use? We will create an app that only stores data locally, so we don't need the synchronization capabilities of PouchDB.
So the choice here is simple: LokiJS, because it offers faster performance since all the queries on the database are executed in-memory.
Read this article for a more detailed comparison of PouchDB and LokiJS.
###Installation We can use Bower to install LokiJS into our Ionic project.
$ bower install lokijs --save
Next, let's reference the LokiJS libraries in index.html. LokiJS has support for Angular, so we will include the JavaScript file for that as well.
<script src="lib/lokijs/src/lokijs.js"></script>
<script src="lib/lokijs/src/loki-angular.js"></script>
LokiJS automatically persists the data to localStorage, but I wouldn't trust that, because localStorage can be cleared when memory is low.
If we have a look at the adapters that come with LokiJS, the other option would be IndexedDB, but unfortunately that doesn't work on iOS devices. That's because Cordova uses UIWebView which doesn't support IndexedDB.
The other option would be to save the data as a JSON file, but at the moment LokiJS doesn't have an adapter that can save files in Cordova apps.
We can download a unofficial adapter for it though: loki-cordova-fs-adapter. It's written by Corentin Smith in ES6, but you can download the transpiled verion here, so let's reference that in index.html.
<script src="js/loki-cordova-fs-adapter.js"></script>
This adapter depends on the Cordova File plugin, so let's install that as well.
$ cordova plugin add cordova-plugin-file
Note: You can also create your own adapter to persist the data whichever way you want to.
Now that we have installed all the neccessary libraries, let's inject the lokijs
module into our app module, in app.js.
angular.module('starter', ['ionic', 'lokijs'])
###What are we going to build? Our app is going to be a birthday registration app that will have add, update, delete and read functionality.
###Create database service Let's start by creating a service to encapsulate our LokiJS calls.
angular.module('starter').factory('BirthdayService', ['$q', 'Loki', BirthdayService]);
function BirthdayService($q, Loki) {
var _db;
var _birthdays;
function initDB() {
var adapter = new LokiCordovaFSAdapter({"prefix": "loki"});
_db = new Loki('birthdaysDB',
{
autosave: true,
autosaveInterval: 1000, // 1 second
adapter: adapter
});
};
return {
initDB: initDB,
getAllBirthdays: getAllBirthdays,
addBirthday: addBirthday,
updateBirthday: updateBirthday,
deleteBirthday: deleteBirthday
};
}
There are different ways to initialize a LokiJS database, in this example we are going to configure it to autosave every second to a JSON file.
The autosave function is smart enough to know that it only needs to save if the data in the in-memory database has changed.
###Get all birthdays
Next, let's implement the getAllBirthdays
function.
function getAllBirthdays() {
return $q(function (resolve, reject) {
var options = {};
_db.loadDatabase(options, function () {
_birthdays = _db.getCollection('birthdays');
if (!_birthdays) {
_birthdays = _db.addCollection('birthdays');
}
resolve(_birthdays.data);
});
});
};
We have to load the database first with the loadDatabase
function. This function takes a callback so that we can access the collections when it's done loading.
As you can see there is an options
object passed in as the first parameter of loadDatabase
. You can leave this object empty if you're happy with the way the data is deserialized from JSON.
However, in our case the dates in JSON will not be automatically converted back to Date
objects, so we'll have to do that ourselves by defining an inflate
function.
var options = {
birthdays: {
proto: Object,
inflate: function (src, dst) {
var prop;
for (prop in src) {
if (prop === 'Date') {
dst.Date = new Date(src.Date);
} else {
dst[prop] = src[prop];
}
}
}
}
};
We're basically copying all properties from the source to the destination object, except the value of the Date
property will be converted to a Date
object.
Note: You can also set autoload when initializing the database, but then you lose the option to specify an
inflate
method.
####Add/Update/Delete a birthday All we have left to do now are the add/update/delete functions. As you can see, these are very simple.
function addBirthday(birthday) {
_birthdays.insert(birthday);
};
function updateBirthday(birthday) {
_birthdays.update(birthday);
};
function deleteBirthday(birthday) {
_birthdays.remove(birthday);
};
###Let's build the UI OK, so we have the service set up which does most of the heavy work, let's have a look at the UI.
We'll add an OverviewController, this calls the birthdayService.initDB
function, but we have to wait for the $ionicPlatform.ready
event to make sure the device is ready.
angular.module('starter').controller('OverviewController', ['$scope', '$ionicModal', '$ionicPlatform', 'BirthdayService', OverviewController]);
function OverviewController($scope, $ionicModal, $ionicPlatform, birthdayService) {
var vm = this;
$ionicPlatform.ready(function() {
// Initialize the database.
birthdayService.initDB();
// Get all birthday records from the database.
birthdayService.getAllBirthdays()
.then(function (birthdays) {
vm.birthdays = birthdays;
});
});
// Initialize the modal view.
$ionicModal.fromTemplateUrl('add-or-edit-birthday.html', {
scope: $scope,
animation: 'slide-in-up'
}).then(function(modal) {
$scope.modal = modal;
});
vm.showAddBirthdayModal = function() {
$scope.birthday = {};
$scope.action = 'Add';
$scope.isAdd = true;
$scope.modal.show();
};
vm.showEditBirthdayModal = function(birthday) {
$scope.birthday = birthday;
$scope.action = 'Edit';
$scope.isAdd = false;
$scope.modal.show();
};
$scope.saveBirthday = function() {
if ($scope.isAdd) {
birthdayService.addBirthday($scope.birthday);
} else {
birthdayService.updateBirthday($scope.birthday);
}
$scope.modal.hide();
};
$scope.deleteBirthday = function() {
birthdayService.deleteBirthday($scope.birthday);
$scope.modal.hide();
};
$scope.$on('$destroy', function() {
$scope.modal.remove();
});
return vm;
}
And this is the code in index.html
, we're using a modal dialog to display the Add Birthday and Edit Birthday views.
<body ng-app="starter">
<ion-pane ng-controller="OverviewController as vm">
<ion-header-bar class="bar-stable">
<h1 class="title">🎂 Birthdays 🎉</h1>
<div class="buttons">
<button ng-click="vm.showAddBirthdayModal()" class="button button-icon icon ion-plus"></button>
</div>
</ion-header-bar>
<ion-content>
<ion-list>
<ion-item ng-repeat="b in vm.birthdays" ng-click="vm.showEditBirthdayModal(b)">
<div style="float: left">{{ b.Name }}</div>
<div style="float: right">{{ b.Date | date:"dd MMMM yyyy" }}</div>
</ion-item>
</ion-list>
</ion-content>
</ion-pane>
<script id="add-or-edit-birthday.html" type="text/ng-template">
<ion-modal-view>
<ion-header-bar>
<h1 class="title">{{ action }} Birthday</h1>
<div class="buttons">
<button ng-hide="isAdd" ng-click="deleteBirthday()" class="button button-icon icon ion-trash-a"></button>
</div>
</ion-header-bar>
<ion-content>
<div class="list list-inset">
<label class="item item-input">
<input type="text" placeholder="Name" ng-model="birthday.Name">
</label>
<label class="item item-input">
<input type="date" placeholder="Birthday" ng-model="birthday.Date">
</label>
</div>
<div class="padding">
<button ng-click="saveBirthday()" class="button button-block button-positive activated">Save</button>
</div>
</ion-content>
</ion-modal-view>
</script>
</body>
###Final Thoughts As you can see it's very easy to use LokiJS for local storage. The only part I struggled with was figuring out which adapter to use for Ionic apps. Saving the data as a JSON file seems to work fine, I've tested it on both iOS and Android devices.
In this example we haven't explored the advanced querying capabilities of LokiJS, so I encourage you to have a look at the documentation and the links below.
###More Info
LokiJS, the idiomatic way
How to query a CSV file with Javascript and LokiJS