Author avatar

ryanlogsdon

Ionic 2 + LokiJS + LocalForage (Progressive Web App, no-SQL db, and long-term storage)

ryanlogsdon

  • Jan 10, 2019
  • 9 Min read
  • 8,679 Views
  • Jan 10, 2019
  • 9 Min read
  • 8,679 Views
Front-End JavaScript

Summary

In this guide, we'll build an Ionic 2 app using a LokiJS database with LocalForage for persistent storage. My app won't be your ordinary database-related app, though. Here are my app requirements:

  1. a no-SQL database
  2. long-term data persistence
  3. simple, legible code and as few adapters as possible
  4. platform agnostic

The Main Reasons I Opted for LokiJS

LokiJS offers some distinct advantages.

  • simple, familiar JavaScript objects
  • good documentation on lokijs.org
  • in-memory architecture
  • ability to store full DB as a JSON token (awesome for small DBs!)
  • microscopic footprint

My Environment in Ionic 2

Ionic 2 is growing and maturing quickly. Here's the environment I used to create this tutorial.

Cordova CLI: 6.1.1

Ionic Framework Version: 2.0.0-beta.9

Ionic CLI Version: 2.0.0-beta.25

Ionic App Lib Version: 2.0.0-beta.15

ios-deploy version: 1.8.6

ios-sim version: 5.0.8

OS: Mac OS X El Capitan

Node Version: v5.7.1

Xcode version: Xcode 7.3 Build version 7D175

Generating the Initial Code Base

In your terminal, type the commands

1ionic start LokiDB blank --v2
2cd LokiDB
3ionic platform add ios
4ionic platform add android
5npm install lokijs
6npm install localforage
bash

If your Ionic Framework version is older than beta 9, you'll need add "--ts" to the first command:

1ionic start LokiDB blank --v2 --ts
bash

These commands will build our skeleton app. All of our hacking will take place in app > pages > home > home.html and home.ts.

Adding LokiJS without Persistence

  1. In home.ts, just under the import statements, add

    1declare var require: any;
    2var loki = require('lokijs');
    ts
  2. Inside the HomePage class, we need to declare 2 objects: one for our database and one for its collection of documents

    1db: any;            // LokiJS database
    2robots: any;        // our DB's document collection object
    ts
  3. Let's set up these objects inside the constructor

    1this.db = new loki('robotsOnTV');
    2this.robots = this.db.addCollection('robots');
    ts
  4. Next, we'll insert a few documents (for those who aren't used to no-SQL databases, a document is just an object held by the database). We're using JSON-style insertion because LokiJS receives the data as JSON. Don't worry about creating TypeScript interfaces because they will only increase the amount of code we need to write.

    1this.robots.insert({ name: 'Bender', tvShow: 'Futurama' });
    2this.robots.insert({ name: 'Rosie', tvShow: 'The Jetsons' });
    3this.robots.insert({ name: 'K1', tvShow: 'Dr. Who' });
    ts
  5. The final thing to do in the TS file is to add a helper function. We want the HTML file to display these results, but *ngFor will not iterate over custom data types. As a result, we're going to write a simple, generic object-to-Array function:

    1convert2Array(val) {
    2    return Array.from(val);
    3}
    ts

    This is how your home.ts should look:

    1import {Component} from "@angular/core";
    2import {NavController} from 'ionic-angular';
    3
    4declare var require: any;
    5var loki = require('lokijs');
    6
    7@Component({
    8    templateUrl: 'build/pages/home/home.html'
    9})
    10
    11export class HomePage {
    12   db: any;                        // LokiJS database
    13   robots: any;                    // our DB's document collection object
    14
    15    constructor(private navController: NavController) {
    16        this.db = new loki('robotsOnTV');
    17        this.robots = this.db.addCollection('robots');
    18        
    19        this.robots.insert({ name: 'Bender', tvShow: 'Futurama' });
    20        this.robots.insert({ name: 'Rosie', tvShow: 'The Jetsons' });
    21        this.robots.insert({ name: 'K1', tvShow: 'Dr. Who' });
    22    }
    23    
    24    convert2Array(val) {
    25        return Array.from(val);
    26    }
    27}
    ts
  6. Lastly, let's get the HTML ready. Delete everything inside the <ion-content> tag of home.html, and replace it with this:

    1<!-- list all database elements -->
    2<ion-card *ngFor="let robot of convert2Array(robots.data)">
    3    <ion-card-header>
    4        {{robot.name}}
    5    </ion-card-header>
    6    <ion-card-content>
    7        {{robot.tvShow}}
    8    </ion-card-content>
    9</ion-card>
    html

Adding interactive elements to our LokiJS database

  1. Inside home.ts, add 2 variables for user input. Let's call them robotName and robotTVShow.

    1robotName: string;
    2robotTVShow: string;
    ts
  2. We'll add in support to insert and delete from the database as well:

    1addDocument() {
    2    if (!this.robotName || !this.robotTVShow) {
    3        console.log("field is blank...");
    4        return;
    5    }
    6
    7    this.robots.insert({ name: this.robotName, tvShow: this.robotTVShow });
    8
    9    // LokiJS is not zero-indexed, so the final element is at <length>, not <length - 1>
    10    console.log("inserted document: " + this.robots.get(length));
    11    console.log("robots.data.length: " + this.robots.data.length);
    12}
    13
    14deleteDocument($event, robot) {
    15    console.log("robot to delete: name = " + robot.name + ", TV show = ", robot.tvShow);
    16
    17    // $loki is the document's index in the collection
    18    console.log("targeting document at collection index: " + robot.$loki);
    19    this.robots.remove(robot.$loki);
    20}
    ts
  3. Let's add one more card to home.html.

    1<!-- add items to LokiJS database -->
    2<ion-card>
    3    <ion-card-content>
    4        <ion-list>
    5            <ion-item>
    6                <ion-label floating>Robot Name</ion-label>
    7                <ion-input clearInput [(ngModel)]="robotName"></ion-input>
    8            </ion-item>
    9            <ion-item>
    10                <ion-label floating>Which TV Show?</ion-label>
    11                <ion-input type="text" [(ngModel)]="robotTVShow"></ion-input>
    12            </ion-item>
    13        </ion-list>
    14    </ion-card-content>
    15    <ion-card-content>
    16        <button (click)="addDocument()">Add</button>
    17    </ion-card-content>
    18</ion-card>
    html
  4. Finally, we need to allow for document deletion. Let's change the original card so that we have a Delete button:

    1<!-- list all database elements -->
    2<ion-card *ngFor="let robot of convert2Array(robots.data)">
    3    <ion-card-header>
    4        {{robot.name}}
    5    </ion-card-header>
    6    <ion-card-content>
    7        {{robot.tvShow}}
    8        <button (click)="deleteDocument($event, robot)">Delete</button>
    9    </ion-card-content>
    10</ion-card>
    html

Adding LocalForage for Long-term Storage

We're going to allow for saving to file and importing from that file. For more info on how LocalForage prioritizes storage, see http://mozilla.github.io/localForage/

  1. home.ts needs a localForage object. Add this just below your var loki = ... code:

    1var localforage = require('localforage');
    ts
  2. Add in functions for saving the database and retrieving it. LocalForage uses key-value maps, and since we're only interested in saving a single value (the entire database), we'll hard-code our key as storeKey.

    1saveAll() {
    2    localforage.setItem('storeKey', JSON.stringify(this.db)).then(function (value) {
    3        console.log('database successfully saved');
    4    }).catch(function(err) {
    5        console.log('error while saving: ' + err);
    6    });
    7}
    8
    9importAll() {
    10    var self = this;
    11    localforage.getItem('storeKey').then(function(value) {
    12        console.log('the full database has been retrieved');
    13        self.db.loadJSON(value);
    14        self.robots = self.db.getCollection('robots');        // slight hack! we're manually reconnecting the collection variable
    15    }).catch(function(err) {
    16        console.log('error importing database: ' + err);
    17    });
    18}
    ts
  3. In home.html, we're going to hook up the new storage functions to 2 new buttons. Next to our "Add" button, include these:
    1<button (click)="saveAll()">Save All</button>
    2<button (click)="importAll()">Import All</button>
    html

That's all it takes to build a persistent, no-SQL database in Ionic 2!

I hope you found this tutorial informative and enjoyable. Thank you for reading!