Developing Ionic apps entirely in the browser - Mocking SQLite native plugin -- Words (1758)

Fully developing Ionic apps on the browser - Mocking SQLite native plugin

Recently I was developing a mobile app with Ionic ,which makes use of native SQLite Cordova plugin ,but due to some problems ,I couldn't continue testing on my real Android device ,so instead of buying a new device why not mocking the SQLite plugin to entirely develop the app on the browser .

Thanks to Ionic Native 3.x ,we can now mock native plugins to provide a fake class that has the same API as the native wrapper for a specific Cordova plugin . You may use simple mocks which only return some developer supplied values or you can build fully working mocks that port the functionality of a specific native plugin to the browser by taking advantage of an equivalent browser API or some JavaScript library .

So lets get started by creating a new Ionic 2 / Ionic 3 project and then install the Ionic native SQLite wrapper .

Creating a new Ionic and SQLite project


Open your terminal/command prompt and type the following :

ionic start native-sqlite-mock blank 
Please note that I'm using the Ionic CLI v3.If you didn't upgrade yet ,you can follow this tutorial or just use the equivalent CLI commands .

Once your project is fully created ,navigate inside of it and type the following ,to install Ionic native SQLite wrapper .

We don't need to add a Cordova platform or add the Cordova SQLite plugin since we are not going to develop on the real device for now .
cd native-sqlite-mock 
npm install --save @ionic-native/sqlite

After installing the native plugin ,open src/app/app.module.ts and add it to the list of providers .

import { SQLite } from '@ionic-native/sqlite';

@NgModule({
declarations: [
    MyApp
    ],
imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp)
],
bootstrap: [IonicApp],
entryComponents: [
    MyApp  
    ],
providers: [
    StatusBar,
    SplashScreen,
    SQLite 
    ]
})
export class AppModule {} 

Creating a SQLite database


Open src/pages/home/home.ts then

  • Inject the SQLite plugin .
  • Declare an object of type SQLiteObject ,an array to store a set of invoices or whatever and a counter of type number .
  • on ionViewDidEnter life cycle event ,create a SQLite database as you normally do .
@Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
public database: SQLiteObject;
public invoices: Array<Object>;  
public counter : number = 0;
constructor(public navCtrl: NavController,private sqlite : SQLite) {
}
ionViewDidEnter(){
    console.log('view did enter');
        this.sqlite.create({name: "data.db", location: "default"}).then((db : SQLiteObject) => {
                this.database = db;
                //this.createTable();
            }, (error) => {
                console.log("ERROR: ", error);
        });    

}
}

Execute Ionic serve to serve your app and then visit http://localhost:8100 .

If you look on the console ,you will see a message saying

cordova is not available 

That is because ,your are in the browser environment not in a real device .

So you can't call the create() method to create a SQLite database ,lets change that !

Mocking the SQLite plugin


We want to use the SQLite API methods when developing on the browser without having to change anything when we switch back to a real device ,so we need to create a mock which has the same API .The most used methods of SQlite plugin are :

The create() method for creating a SQLite database .

The executeSql() method to execute SQL statement on the created database .

So we need a mock that at least provides these two methods .

Open src/app/app.module.ts and add the following TypeScript class

import { SQLite  , SQLiteDatabaseConfig , SQLiteObject } from '@ionic-native/sqlite';

class SQLiteMock {
public create(config: SQLiteDatabaseConfig): Promise<SQLiteObject> {

    return new Promise((resolve,reject)=>{
    resolve(new SQLiteObject(new Object()));
    });
}
} 

Then change the list of providers to use this mock class

providers: [
    StatusBar,
    SplashScreen,
    {provide: SQLite, useClass: SQLiteMock},
    {provide: ErrorHandler, useClass: IonicErrorHandler}
]
})

Congratulations ,you have created a basic mock for SQLite plugin .Now if you serve your app you won't get

Cordova Not Available 

Since we are using the create() method of our mock class instead of the original one .

But this mock doesn't actually create any database for us ,so we can't simulate the same environment as if we are developing on a real device ,lets change that .

Since the browser has no supported way to use SQLite ,we will use a JavaScript library which provides an

in memory SQLite database i.e the database and all its content will live in memory not in the disk .

So go ahead grab the library and include it with a script tag in index.html of your app ,before Ionic files .

<script src="assets/sql.js"></script>
<script src="build/polyfills.js"></script>
<script src="build/main.js"></script>

Since this is a JavaScript library ,add the following statement in app.module.ts to be able to use it

declare var SQL;

Now lets implement the create() method of our mock class

class SQLiteMock {
    public create(config: SQLiteDatabaseConfig): Promise<SQLiteObject> {

        //since this is an in memory database we can ignore the config parameters 

        var db = new SQL.Database();

        return new Promise((resolve,reject)=>{
            resolve(new SQLiteObject(db));
        });
    }
}

Until now we are using the original SQLiteObject but we need to use our own so we can provide an implementation of executeSql() method .

So go ahead remove the import statement of SQLiteObject and create a new class which has a constructor that takes a database object we have in the create() method .

The executeSql() method needs to return a promise that can be either rejected with an error string or resolved with a payload :

      var payload = {
        rows: {
          item: function(i) {
            return rows[i];
          },
          length: rows.length
        },
        rowsAffected: this._objectInstance.getRowsModified() || 0,
        insertId: this._objectInstance.insertId || void 0
      }; 

And here is the full class

class SQLiteObject{
    _objectInstance: any;

    constructor(_objectInstance: any){
        this._objectInstance = _objectInstance;
    };

    executeSql(statement: string, params: any): Promise<any>{

        return new Promise((resolve,reject)=>{
            try {
                var st = this._objectInstance.prepare(statement,params);
                var rows :Array<any> = [] ;
                while(st.step()) { 
                    var row = st.getAsObject();
                    rows.push(row)
                }
                var payload = {
                    rows: {
                    item: function(i) {
                        return rows[i];
                    },
                    length: rows.length
                    },
                    rowsAffected: this._objectInstance.getRowsModified() || 0,
                    insertId: this._objectInstance.insertId || void 0
                };  
                resolve(payload);
            } catch(e){
                reject(e);
            }
        });
    };

}
Both methods create() and executeSql() and any other implemented method should have the exact same parameters and return types as the original ones .So we can get the same behavior ,when we swap back the original plugin on the real device .
You can look at the Ionic Native wrapper of SQLite and also the SQLite Cordova plugin to get the exact signatures and return types of these methods .

Now head to HomePage and add these methods .

Creating a SQL table

Add a method createTable to create an invoices table

public createTable(){
    this.database.executeSql('create table if not exists invoices(name VARCHAR(32))', {})
        .then(() => {
            console.log('Table Invoice created !');

        })
        .catch(e => console.log(e));    
}

Inserting into table

Next add a method to insert some invoices into created table

public counter : number = 0;
public insertInvoice(){
    var c = 'INV' + this.counter; 
    this.database.executeSql("INSERT INTO invoices (name) VALUES (?)", [c]).then((data) => {
            console.log("INSERTED: ");
            this.counter++;
            this.showInvoices();
        }, (error) => {
            console.log("ERROR: " + JSON.stringify(error.err));
        });    
}

List table data

Create showInvoices method which list all inserted invoices

public invoices: Array<Object>;  
public showInvoices(){
    this.database.executeSql("SELECT * FROM invoices", []).then((data) => {
            this.invoices = [];
            if(data.rows.length > 0) {
                for(var i = 0 ; i < data.rows.length ; i++) {
                    this.invoices.push({ name: data.rows.item(i).name });
                }
            }
        }, (error) => {
            console.log("ERROR: " + JSON.stringify(error));
        });    
}

Next change ionViewDidEnter life cycle event to look like

ionViewDidEnter(){
    console.log('view did enter');
        this.sqlite.create({name: "data.db", location: "default"}).then((db : SQLiteObject) => {
                this.database = db;
                this.createTable();
            }, (error) => {
                console.log("ERROR: ", error);
        });    
}
Don't forget to inject the SQLite provider via HomePage constructor .

Open home.html ,add a button to trigger the insert invoice method and a list to show the invoices in our SQLite database

<ion-header>
<ion-navbar>
    <ion-title>
    SQLite Mock demo 
    </ion-title>
</ion-navbar>
</ion-header>

<ion-content padding>
    <button ion-button (click)="insertInvoice()">Insert Invoice</button>
    <ion-list>
    <ion-item *ngFor="let invoice of invoices">

    </ion-item>
</ion-list>
</ion-content>

Saving the SQLite database


Since we are using an in memory SQLite database ,when our app reloads the content of the database gets lost . This is not a problem because we are just testing but to avoid reentering our test data every time the app reloads lets save the SQLite database and reload it back when the app reloads .

Fortunately for us ,our JavaScript library has an export method to export the in memory database to an Array of bytes .

You can use either the local storage or the indexdb database on the browser to save the in memory database every time executeSql() is executed .

So change executeSql() to look like this

executeSql(statement: string, params: any): Promise<any>{

  return new Promise((resolve,reject)=>{
    try {
      var st = this._objectInstance.prepare(statement,params);
      var rows :Array<any> = [] ;
      while(st.step()) { 
          var row = st.getAsObject();
          rows.push(row);
      }
      var payload = {
        rows: {
          item: function(i) {
            return rows[i];
          },
          length: rows.length
        },
        rowsAffected: this._objectInstance.getRowsModified() || 0,
        insertId: this._objectInstance.insertId || void 0
      };  

      //save database after each sql query 

      var arr : ArrayBuffer = this._objectInstance.export();
      localStorage.setItem("database",String(arr));
      resolve(payload);
    } catch(e){
      reject(e);
    }
  });
};

Then change the create method of SQLiteMock to

class SQLiteMock {
public create(config: SQLiteDatabaseConfig): Promise<SQLiteObject> {
    var db;
    var storeddb = localStorage.getItem("database");

    var arr = storeddb.split(',');
    if(storeddb)
    {
        db = new SQL.Database(arr);
    }
    else
    {
        db = new SQL.Database();
    }

    return new Promise((resolve,reject)=>{
        resolve(new SQLiteObject(db));
    });
}
}

The method looks for a database item in the local storage if it exists pass it as a parameter to SQL.Database(). The database is saved as a string so we need to parse it back to an array of bytes .

Conclusion


So now you can develop Ionic apps which make use of the native SQLite plugin entirely in the browser thanks to Ionic Native 3.x+ mocks .


I'm a web developer and technical writer. Passionate about modern JavaScript technologies and currently trying to develop all kind of apps (Web, Mobile and Desktop) with JavaScript.