How to Create NW.js Crud App Using PouchDB

Usually, we need to save data when we are using and developing applications. In node-webkit, we can implement different standards and interfaces to persist in our data. From standardized database systems, IndexedDB is the preferred one.

IndexedDB is a client-side NoSQL database system. It is also used for saving text/binary/blob files. On the other hand, it is a low-level library. So using IndexedDB could be a complex thing.

Therefore I’m using PouchDB as a wrapper library. It has a very simple api and great documentation.

I develop a sample NW.js crud application. To persist the application data I will use PouchDB.

What is PouchDB?

PouchDB is an open-source javascript database. It is designed to work seamlessly with Apache CouchDB. 

When it is needed you can sync your offline database easily to the online database. Therefore your app works seamlessly both offline and online.

PouchDB is an open-source, cross-browser lightweight, easy to learn library. I’m developing NW.js crud application using PouchDB.

Sample Crud App

Our sample nw.js crud app is used to manage our person table. It has listing, adding new person, updating and removing person functionalities.

Our Person model contains only 4 fields.

I’m using Bulma as a css framework. It has some very nice features. Bulma does not use any javascript library. It has only css classes to enable responsive layout and variable of ui components (button, panel, window, tab, etc.)

Bulma is a modular framework and it is very easy to use.

Finished Project:

Create The Person Database

To be able to save your data, first of all, you should create the database:

var db = new PouchDB('persondb');

With this statement, you create a persondb database. If there is already a persondb, PouchDB only opens the database.

You can see the database with chrome developer tools (F12 in windows).

chrome developer tools
Chrome DevTools

Listing

To list documents we are using PouchDB allDocs method. It fetches all the documents.

db.allDocs({
  include_docs: true,
  attachments: true
}).then(function (result) {
  // handle result
}).catch(function (err) {
  console.log(err);
});

You should send include_docs and attachments parameters as true. Otherwise, allDocs method returns only the _id and _rev values, not the other fields (name, surname, age).

Create New Person

db.put({
  _id: '12',
  name: 'John',
  surname: 'Doe',
  age: 23
}).then(function (response) {
  // handle response
}).catch(function (err) {
  console.log(err);
});

The put method is used to create a new person document.

Delete Person

The remove method is used to delete the person document. Firstly you find the person document with the get method. After then you send the id parameter which points the intended person document.

db.get(myid).then(function(doc) {
   return db.remove(doc);
}).then(function (result) {
   // handle result
}).catch(function (err) {
   console.log(err);
}); 

Update Person

Updating a document is different than creating. In both cases, we are using the put method. But when you update the person you should send the _rev parameter.

If you do not provide the _rev parameter it throws a ‘Document update conflict’ error.

db.get(personId).then(function(doc) {
    return db.put({
        _id: personId,
        _rev: doc._rev,
        name: name,
        surname: surname,
        age: age
    });
    }).then(function(response) {
        window.opener.listAllPersons();
    }).catch(function (err) {
        alert('error occured', err);
    });
}

I have created two html files, one for listing person documents and deleting. And another file for creating and modifying the person documents.

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Person List</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
  </head>
  <body>
  <section class="section">
    <div class="container">
      <h1 class="title">
        NW.js Crud App using PouchDB
      </h1>

      <div>
          <div class="field has-addons">
              <a class="button is-primary" id='addPersonBtn'>
                  <span class="icon is-small">
                    <i class="fas fa-plus"></i>
                  </span>
                  <span>Add</span>
              </a>

              <a class="button" id='refreshBtn'>
                  <span class="icon is-small">
                    <i class="fas fa-sync"></i>
                  </span>
                  <span>Refresh</span>
              </a>              
          </div>

          <table id="personlist" class="table is-bordered">
              <thead>
                  <tr>
                      <th>Id</th>
                      <th>Name</th>
                      <th>Surname</th>
                      <th>Age</th>
                      <th></th>
                      <th></th>
                  </tr>
              </thead>
              <tbody>
                <tr>
                </tr>                  
              </tbody>
              <tfoot>

              </tfoot>
          </table>
      </div>
    </div>
  </section>
  <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
  <script src="js/pouchdb-7.1.1.min.js"></script>
  <script src="js/app.js"></script>
  </body>
</html>

app.js

(function () {
    'use strict';
    
    // indexeddb  
    var db = new PouchDB('persondb');

    var addPersonButton = document.getElementById('addPersonBtn');

    addPersonButton.addEventListener('click', openPersonForm);

    var refreshButton = document.getElementById('refreshBtn');

    refreshButton.addEventListener('click', function () {
        listAllPersons();
    });    
   
    function openPersonForm()
    {
        nw.Window.open('personform.html', 
        {
            width: 500,
            height: 550
        }, 
        function(win) {});
    }

    function openPersonFormForUpdate(myid)
    {
        db.get(myid).then(function (doc){
            if (doc) {
                var name = doc.name;
                var surname = doc.surname;
                var age = doc.age;
                
                var url = `personform.html?id=${myid}&name=${name}&age=${age}&surname=${surname}`;

                nw.Window.open(url, 
                {
                    width: 500,
                    height: 550
                }, 
                function(win) {});
            }
        });
    }        
   
    function openPersonForm()
    {
        nw.Window.open('personform.html', 
        {
            width: 500,
            height: 550
        }, 
        function(win) {});
    }    

    function removePerson(myid)
    {
        console.log('removed:' + myid);
        db.get(myid).then(function(doc) {
            return db.remove(doc);
          }).then(function (result) {
            // handle result
            listAllPersons();
          }).catch(function (err) {
            console.log(err);
        });        
    }

    function listAllPersons() {
        db.allDocs({
            include_docs: true,
            attachments: true
        }).then(function (result) {
    
            var docs = result.rows;
    
            var tableRef = document.getElementById('personlist').getElementsByTagName('tbody')[0];

            while(tableRef.hasChildNodes())
            {
                tableRef.removeChild(tableRef.firstChild);
            }
    
            docs.forEach(function(element) {
                // Insert a row in the table at the last row
                var newRow   = tableRef.insertRow();
                
                // Insert a cell in the row at index 0
                var newCell  = newRow.insertCell(0);
                
                var myid = element.doc._id;
    
                // Append a text node to the cell
                var newText  = document.createTextNode(myid);
                newCell.appendChild(newText);
    
                // Insert a cell in the row at index 0
                newCell  = newRow.insertCell(1);
                
                var myname = element.doc.name;
    
                // Append a text node to the cell
                newText  = document.createTextNode(myname);
                newCell.appendChild(newText); 
                
                // Insert a cell in the row at index 0
                newCell  = newRow.insertCell(2);
                
                var mysurname = element.doc.surname;
    
                // Append a text node to the cell
                newText  = document.createTextNode(mysurname);
                newCell.appendChild(newText);
                
                // Insert a cell in the row at index 0
                newCell  = newRow.insertCell(3);
                
                var myage = element.doc.age;
    
                // Append a text node to the cell
                newText  = document.createTextNode(myage);
                newCell.appendChild(newText);             
                

                
                // Insert a cell in the row at index 0
                newCell  = newRow.insertCell(4);
                
                var anchorUpdate = document.createElement("a");
                anchorUpdate.className = 'button';
                anchorUpdate.onclick = function () {
                    openPersonFormForUpdate(myid);
                }

                var spanIcon = document.createElement("span");
                spanIcon.className = 'icon is-small';

                var icon = document.createElement("i");
                icon.className = "fas fa-pen";

                spanIcon.appendChild(icon);

                anchorUpdate.appendChild(spanIcon);

                newCell.appendChild(anchorUpdate);

                //

                newCell  = newRow.insertCell(5);

                var anchorRemove = document.createElement("a");
                anchorRemove.className = 'button';
                anchorRemove.onclick = function () {
                    removePerson(myid);
                }

                spanIcon = document.createElement("span");
                spanIcon.className = 'icon is-small';

                icon = document.createElement("i");
                icon.className = "fas fa-trash";

                spanIcon.appendChild(icon);

                anchorRemove.appendChild(spanIcon);

                newCell.appendChild(anchorRemove);

            });
        }).catch(function (err) {
            console.log(err);
        });        
    }

    window.listAllPersons = listAllPersons;

    // If there isn't any person in database, add two test person.
    db.allDocs({
        include_docs: true,
        attachments: true
    }).then(function (result) {
        if (result.total_rows === 0) {
            db.put({
                _id: '1',
                name: 'guest',
                surname: 'guest',
                age: '21'
            }).then(function (response) {
                // handle response
            }).catch(function (err) {
                console.log(err);
            });

            db.put({
                _id: '2',
                name: 'guest1',
                surname: 'guest1',
                age: '30'
            }).then(function (response) {
                // handle response
            }).catch(function (err) {
                console.log(err);
            });    
        }

        listAllPersons();
    });     

  })();

To create or update Person you open a new window (personform.html and corresponding javascript file).

personform.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Add Person</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
  </head>
  <body>
  <section class="section">
    <div class="container">
      <h1 class="title">
        Person
      </h1>

      <div>
        <div class="field">
                <label class="label">Id</label>
                <div class="control">
                    <input class="input" type="text" placeholder="Id" id='person_id'>
                </div>
            </div>          
        <div class="field">
            <label class="label">Name</label>
            <div class="control">
                <input class="input" type="text" placeholder="Name" id='person_name'>
            </div>
        </div>
        <div class="field">
            <label class="label">Surname</label>
            <div class="control">
                <input class="input" type="text" placeholder="Surname" id='person_surname'>
            </div>
        </div>
        <div class="field">
            <label class="label">Age</label>
            <div class="control">
                <input class="input" type="text" placeholder="Age" id='person_age'>
            </div>
        </div>
        <div class="field is-grouped">
                <div class="control">
                  <button class="button is-link" id="savePersonBtn">Save</button>
                </div>

                <div class="control">
                    <button class="button is-link" id="closeBtn">Close</button>
                  </div>                
        </div>                    
      </div>
    </div>
  </section>
  <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
  <script src="js/pouchdb-7.1.1.min.js"></script>
  <script src="js/personform.js"></script>
  </body>
</html>

personform.js:

(function () {
    'use strict';

    let insertMode = true;
    
    var persondb = document.getElementById('persondb');
  
    // indexeddb  
    var db = new PouchDB('persondb');

    var saveNewPerson = document.getElementById('savePersonBtn');

    saveNewPerson.addEventListener('click', addUpdatePerson);

    var closeWindowBtn = document.getElementById('closeBtn');

    closeWindowBtn.addEventListener('click', function () {
      window.close()
    });    

    var url = window.location;

    let searchParams = new URLSearchParams(url.search);

    var id = searchParams.get('id');

    if (id) {
      insertMode = false;
      var name = searchParams.get('name');
      var surname = searchParams.get('surname');
      var age = searchParams.get('age');      

      console.log(`id=${id} name=${name} surname=${surname} age=${age}`);

      document.getElementById('person_id').value = id;
      document.getElementById('person_name').value = name;
      document.getElementById('person_surname').value = surname;
      document.getElementById('person_age').value = age;
    }

    function addUpdatePerson()
    {
        let personId = document.getElementById('person_id').value;
        let name = document.getElementById('person_name').value;
        let surname = document.getElementById('person_surname').value;
        let age = document.getElementById('person_age').value;

        if (!insertMode) {
          db.get(personId).then(function(doc) {
            return db.put({
              _id: personId,
              _rev: doc._rev,
              name: name,
              surname: surname,
              age: age
            });
          }).then(function(response) {
            window.opener.listAllPersons();
          }).catch(function (err) {
            alert('error occured', err);
          });
        }
        else {
          db.put({
              _id: personId,
              name: name,
              surname: surname,
              age: age
            }).then(function (response) {
              // handle response
              window.opener.listAllPersons();              

            }).catch(function (err) {
              alert('error', err);
            });
        }    
    }  

  })();

Conclusion

You can see the complete project on github.

Thanks to PouchDB you have created an embedded database to persist NW.js application data. PouchDB makes using IndexedDB database api a breeze. Documentation is also easy to understand and implement.

Related: How to build an NW.js App using Create React App.

Leave a Comment