Thursday, October 22, 2020

PWA with live data using IndexDB for Sync using Simple Vanilla JavaScript

In previous example we had tried to use Firbase as the DB. Firebase provide us many inbuild functionality that help us to perform PWA sync offline. In depth it uses IndexDB of browser to perform the same operation. Now lets take an example in which we will use the indexdb api to perform the same.

We are going to use the plain vanilla java script to perform all the operation of install, activate, fetch , open Indexdb.

Scenario we are going to capture here are
1- User comes online and access the site. User fill the form and submit the data
Two case can happen
A:- Internet is there and user is successful to call W/S and submit data.
B:- If the internet stop then his form data will be stored in the IndexDB and then automatically when the internet come the data is submitted from Indexdb to W/S call.

2- User is offline and as PWA is implemented he fill the form.
A:- After the user submit the form its data will be stored inside the Indexdb and then automatically when the internet come the data is submitted from Indexdb to W/S call.

Lets start the code now …

first we will create a html that will have only one text field that will be used to store in Indexdb or send to W/S.

1- index.html:-

<!DOCTYPE html>
<html>

<head>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">

    <style>
        body {
            background-color: #f5f5f5;
        }

        .explainer {
            padding-top: 5px;
            padding-bottom: 5px;
        }

        .explainer p {
            font-weight: 50;
            color: blue;
        }
    </style>
</head>

<body>
    <div class="container">
        <div class="col-md-6 col-md-offset-3">
            <div class="text-center">
                <div class="explainer">
                    <h1 class="h1 mb-3 font-weight-normal">Plain Vanilla JS Background Sync using PWA Concept</h1>

                    <p>Scenario we are going to capture here are <br>
                        1- User comes online and access the site. User fill the form and submit the data
                        Two case can happen
                        <br>
                        A:- Internet is there and user is successful to call W/S and submit data.
                        <br>
                        B:- If the internet stop then his form data will be stored in the IndexDB and then automatically
                        when the internet come the data is submitted from Indexdb to W/S call.
                        <br>
                        2- User is offline and as PWA is implemented he fill the form.
                        <br>
                        A:- After the user submit the form its data will be stored inside the Indexdb and then
                        automatically when the internet come the data is submitted from Indexdb to W/S call.
                    </p>
                </div>

                <form class="form-horizontal">
                    <div class="form-group">
                        <label for="firstname" class="col-md-3 control-label">First Name: </label>
                        <div class="col-md-9">
                            <input class="form-control" type="text" name="firstname" id="firstname"
                                placeholder="Enter the First Name">
                        </div>
                    </div>

                    <button class="btn btn-lg btn-primary btn-block " id="submitForm">Call Back End</button>
                </form>
            </div>
        </div>
    </div>

    <script src="./index.js"></script>
</body>

</html>


2- Index.js

const url = 'https://jsonplaceholder.typicode.com/posts';
//Calling the method that are executed onload of the html that inbuild call index.js
window.addEventListener('load', () => {
  //register service worker
  registerSW();
  //Initializing IndexDB
  initializeDB();
  //syncButton();
  //send the data to url if online
  checkIndexedDB();
});

//register the sw.js with the browser. In this step we will check if the browser allows us to use the PWA functionality. If not we will show error to the end user.
async function registerSW() {
  if ('serviceWorker' in navigator) {
    try {
      //await navigator.serviceWorker.register('./sw.js');
      console.log('Inside registerSW:');
      await navigator.serviceWorker.register('./sw.js')
        .then(function () {
          return navigator.serviceWorker.ready
        })
        .then(function (registration) {
          document.getElementById('submitForm').addEventListener('click', (event) => {
            event.preventDefault();
            //If user online the save data.
            saveData().then(function () {
              if (registration.sync) {
                //registring the sync process with name
                registration.sync.register('back-sync')
                  .catch(function (err) {
                    return err;
                  })
              } else {
                // sync isn't there so fallback
                console.log('Inside registerSW checkInternet:');
                checkInternet();
              }
            });
          })
        })

    } catch (e) {
      console.log('SW registration failed');
    }
  } else {
    //If the browser did not support the PWA then also our application should work when the user in online.
    document.getElementById('submitForm').addEventListener('click', (event) => {
      //below will keep the data on the screen filled even  if the something goes wrong in later case.
      event.preventDefault();
      //Calling Save Data to call the W/S.
      saveData().then(function () {
        checkInternet();
      });
    })
  }
}

/* async function syncButton() {
  try {
    await navigator.serviceWorker.ready.then(function (swRegistration) {
      return swRegistration.sync.register('myFirstSync');
    });
  } catch (e) {
    console.log('SW registration failed');
  }
}
 */
function initializeDB() {
  console.log('Inside initializeDB:');
  var newindexDB = window.indexedDB.open('browserindexDB');

  newindexDB.onupgradeneeded = function (event) {
    var db = event.target.result;

    var newIndexdbObjStore = db.createObjectStore("newIndexdbObjStore", { autoIncrement: true });
    newIndexdbObjStore.createIndex("firstName", "firstName", { unique: false });

  }
}

//Createing indexdb that will store the value while make the call to W/S.
function checkIndexedDB() {
  console.log('Inside checkIndexedDB:');
  if (navigator.onLine) {
    console.log('Inside navigator.onLine:');
    var newindexDB = window.indexedDB.open('browserindexDB');
    newindexDB.onsuccess = function (event) {
      this.result.transaction("newIndexdbObjStore").objectStore("newIndexdbObjStore").getAll().onsuccess = function (event) {
        window.fetch(url, {
          method: 'POST',
          body: JSON.stringify(event.target.result),
          mode: 'no-cors',
          headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json'
          }
        }).then(function (rez) {
          return rez.text();
        }).then(function (response) {
          newindexDB.result.transaction("newIndexdbObjStore", "readwrite")
            .objectStore("newIndexdbObjStore")
            .clear();
        }).catch(function (err) {
          console.log('err ', err);
        })
      };
    };
  }
}

//Use to save th data in side Indexdb
function saveData() {
  console.log('Inside saveData:');
  return new Promise(function (resolve, reject) {
    var tmpObj = {
      firstName: document.getElementById('firstname').value
    };

    var myDB = window.indexedDB.open('browserindexDB');

    myDB.onsuccess = function (event) {
      var objStore = this.result.transaction('newIndexdbObjStore', 'readwrite').objectStore('newIndexdbObjStore');
      objStore.add(tmpObj);
      resolve();
    }

    myDB.onerror = function (err) {
      reject(err);
    }
  })
}
//Fetching the data from indexdb
function fetchData() {
  console.log('Inside fetchData:');
  return new Promise(function (resolve, reject) {
    var myDB = window.indexedDB.open('browserindexDB');

    myDB.onsuccess = function (event) {
      this.result.transaction("newIndexdbObjStore").objectStore("newIndexdbObjStore").getAll().onsuccess = function (event) {
        resolve(event.target.result);
      };
    };

    myDB.onerror = function (err) {
      reject(err);
    }
  })
}

//Sending the data to W/S call.
function sendData() {
  console.log('Inside sendData:');
  fetchData().then(function (response) {
    var postObj = {
      method: 'POST',
      body: JSON.stringify(response),
      headers: {
        'Content-Type': 'application/json'
      }
    };

    // send request
    return window.fetch(url, postObj)
  })
    .then(clearData)
    .catch(function (err) {
      console.log(err);
    });
}

//Clear the data from the indexdb once we get the network or making a call when network in online.
function clearData() {
  console.log('Inside clearData:');
  return new Promise(function (resolve, reject) {
    var db = window.indexedDB.open('browserindexDB');
    db.onsuccess = function (event) {
      db.transaction("browserindexDB", "readwrite")
        .objectStore("newIndexdbObjStore")
        .clear();

      resolve();
    }

    db.onerror = function (err) {
      reject(err);
    }
  })
}

function checkInternet() {
  console.log('Inside checkInternet:');
  event.preventDefault();
  if (navigator.onLine) {
    sendData();
  } else {
    alert("You are offline! When your internet returns, we'll finish up your request.");
  }
}
//Sending the request to W/S if the user is online.
window.addEventListener('online', function () {
  console.log('Inside online:');
  if (!navigator.serviceWorker && !window.SyncManager) {
    fetchData().then(function (response) {
      if (response.length > 0) {
        return sendData();
      }
    });
  }
});

window.addEventListener('offline', function () {
  console.log('Inside offline:');
  alert('You have lost internet access!');
});

3- sw.js

const url = 'https://jsonplaceholder.typicode.com/posts';
//Define the cache name. By this name of the cache data is fetch when no internet is there.
const cacheName = 'siddhucache.1.0';
//This is the array that define the files that need to be cache.
const staticAssets = [
    './',
    './index.html',
    './index.js',
    'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css'
];

//Install :- This will be executed if site is accessed fresh or the version of the SW change. This is called immediately 
// Once the browser detect the new version of SW for registration. 
self.addEventListener('install', async e => {
    console.log('install:');
    const cache = await caches.open(cacheName);
    await cache.addAll(staticAssets);
    //below line of code will tell start working as soon as request is made. 
    return self.skipWaiting();
});

self.addEventListener('activate', function (event) {
    console.log('activate:');
    /*     self.clients.claim(); */
    caches.match(event.request)
        .then(function (cachedFiles) {
            if (cachedFiles) {
                return cachedFiles;
            } else {
                return fetch(event.request);
            }
        })
        .catch(function (err) {
            console.log('err in fetch ', err);
        })
});

//Fetch:- Here we do following things
//Step A:- Frist check if the internet is online. If online then we will make a call to server using internet and cache the new data.
//Step B:- If no internet then show the result from the cache to the end users.
self.addEventListener('fetch', async e => {
    const req = e.request;
    const url = new URL(req.url);

    if (url.origin === location.origin) {
        //below line indicate if the requested url is same and no internet then fetch the date from the cache
        e.respondWith(displayFirstCache(req));
    } else {
        //below line indicate else make a call to the internet and fetch the new data.
        e.respondWith(callNetworkFirstAndThenDoCache(req));
    }
});

//This fuction will be used to take the data from the cache defined in cacheName
async function displayFirstCache(req) {
    //Opening the cachee
    const cache = await caches.open(cacheName);
    //Matching the request
    const cached = await cache.match(req);
    //if same then return the cached else call the internet using fetch method.
    return cached || fetch(req);
}

//This fuction will be used to take the data from the internet and fill the new values in the cache defined in cacheName
async function callNetworkFirstAndThenDoCache(req) {
    const cache = await caches.open(cacheName);
    try {
        //calling internet and taking new data.
        const fresh = await fetch(req);
        //deleting the old cahce and pusing the new data.
        await cache.put(req, fresh.clone());
        return fresh;
    } catch (e) {
        //if no internet then use the old data.
        const cached = await cache.match(req);
        return cached;
    }
}
//Calling the sync method when the use come on line
self.addEventListener('sync', function (event) {
    console.log('firing: sync');
    if (event.tag == 'back-sync') {
        event.waitUntil(syncIt());
    }
});

function syncIt() {
    console.log('Inside syncIt:');
    return getIndexedDB()
        .then(sendToServer)
        .catch(function (err) {
            return err;
        })
}

//Getting the handle of indexdb
function getIndexedDB() {
    console.log('Inside getIndexedDB:');
    return new Promise(function (resolve, reject) {
        var db = indexedDB.open('browserindexDB');
        db.onsuccess = function (event) {
            this.result.transaction("newIndexdbObjStore").objectStore("newIndexdbObjStore").getAll().onsuccess = function (event) {
                resolve(event.target.result);
            }
        }
        db.onerror = function (err) {
            reject(err);
        }
    });
}

//Makeing call to W/S.
function sendToServer(response) {
    console.log('Inside sendToServer:');
    return fetch(url, {
        method: 'POST',
        body: JSON.stringify(response),
        headers: {
            'Content-Type': 'application/json'
        }
    }).then(function (rez2) {
        console.log('Sended data', rez2);
        var myDB = indexedDB.open('browserindexDB');
        myDB.onsuccess = function (event) {
            var objStore = this.result.transaction('newIndexdbObjStore', 'readwrite').objectStore('newIndexdbObjStore');
            objStore.clear();
        }

        myDB.onerror = function (err) {
            reject(err);
        }
        return rez2.text();
    }).catch(function (err) {
        console.log('Err data', err);
        return err;
    })
}

Now let see the scenario

1- When the user in Online.

Entering the data and click on back end call button

2- Now lets go to offline by clicking on application –> Offline checkbpox

Now enter the data and click on back end call button

Now lets come to online by unchecking the application –> Offline checkbpox. We can see immediately a W/S call is made and data store inside the indexDb is deleted.

You can download the source code from

https://github.com/shdhumale/pwasyncvanillajs.git

Monday, October 19, 2020

PWA with live data from firebase for Sync

 For this example we will take our base project which has already integrated our PWA with notification created in blog

Source code :- https://github.com/shdhumale/complete-pwa-using-workbox-notification-fallout

Above example is running with concept such as
1- Precaching [Precaching Static Files Caching]:-
2- Offline page to show or 404 page not found:
3- Runtime caching
4- Strategies and also implementing different strategies.
5- Notification

In this example we are going to look into Sync behaviour for PWA i.e. it will address following concerns

1- If the use goes offline and fill the data and submit. This submitted data will be hold by IndexDB of the browser and when the user come online it will be inserted into the persistant system like DB.
2- If the data is upated at the DB end then it will be refelected once the user come online on the screen to the user.

Here we are going to use the Firebase DB. It has many advantage

1- We can easily integrate it with our application
2- Support Sync operation efficiently. We it handle indexdb of browser automatically i.e. we did need to write the code for the same.
3- It has the capacity to load our prepared appicatoin on the Firebase cloud server with https certification
4- We can easily test the application live behaviour from the mobile device as it is not live on the internet.

So first lets create our DB in Firstore. For that login to Firebase and follow below gien step.
Note:- Firestore is NOSQL DB i.e. it deals with collection, document etc.

1- Login to Firebase
2- Click on Go to Console

After this Firebae wil;l create the project for you.

Now go to project overview and click on this icon

Copy the whole code this code will be used for connection to firestore.

Add this code inour index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- The core Firebase JS SDK is always required and must be listed first -->
 
<!-- TODO: Add SDKs for Firebase products that you want to use
 
<script>
    // Your web app's Firebase configuration
    // For Firebase JS SDK v7.20.0 and later, measurementId is optional
    var firebaseConfig = {
        apiKey: "AIzaSyAU_ScbpPpEkiHPZbzZj31_5Hi8NYHPkVs",
        authDomain: "siddhu-pwa.firebaseapp.com",
        databaseURL: "https://siddhu-pwa.firebaseio.com",
        projectId: "siddhu-pwa",
        storageBucket: "siddhu-pwa.appspot.com",
        messagingSenderId: "38363144416",
        appId: "1:38363144416:web:3a54a37ac815762dddf085",
        measurementId: "G-D6LRB9KK6Y"
    };
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
    firebase.analytics();
    const db = firebase.firestore();
</script>

Also create a db.js in js folder and add following line in it

// real-time listener
db.collection('users').onSnapshot(snapshot => {
console.log("-----------------------", snapshot.docChanges());
snapshot.docChanges().forEach(change => {
console.log(change);
console.log(change, change.doc.data(), change.doc.id);
if (change.type === 'added') {
userAdded(change.doc.data(), change.doc.id);
}
});
});

And in ui.js add this method
// render User data
const userAdded = (data, id) => {
const html = `user thumb ${data.userName}
${data.userAddress}

delete_outline

`;
users.innerHTML += html;

};

now perform this two command

npm run build
npm run start

we will be able to see the firebase data on the screen.

Now to show the offline DB data add these line in the uil.js

Now We can also see the data in offline as we are now storing the data in indexdb as shown below.

And this is done by this simple belwo line of code in our db.js
// enable offline data
db.enablePersistence()
.catch(function (err) {
if (err.code == 'failed-precondition') {
// probably multible tabs open at once
console.log('persistance failed');
} else if (err.code == 'unimplemented') {
// lack of browser support for the feature
console.log('persistance not available');
}
});

Now as soon as we go offline and add new record in the DB and hten again come online and do the refresh onthe screen we will be abel to see the new record on the screen.

Now lets perform add operation from the screen to the firebase.

Add following line in your db.js

// add new user
const form = document.querySelector('form');
form.addEventListener('submit', evt => {
evt.preventDefault();

1
2
3
4
5
6
7
8
9
10
const user = {
    userName: form.title.value,
    userAddress: form.address.value
};
 
db.collection('users').add(user)
    .catch(err => console.log(err));
 
form.title.value = '';
form.address.value = '';

});

Now again execute below command

npm run build
npm run start

Now lets create the data offline and we can check no data is created in DB but it is is created in IndexDB

Now lets go online and we will see the newly created data is also created in Firebasd DB.

Now lets perform the last delete operation
Add following line in db.js

// real-time listener
db.collection('users').onSnapshot(snapshot => {
console.log("-----------------------", snapshot.docChanges());
snapshot.docChanges().forEach(change => {
console.log(change);
console.log(change, change.doc.data(), change.doc.id);
if (change.type === 'added') {
userAdded(change.doc.data(), change.doc.id);
}
if (change.type === 'removed') {
// remove the document data from the web page
userRemoved(change.doc.id);
}
});
});

// remove a user
const recipeContainer = document.querySelector('.users');
recipeContainer.addEventListener('click', evt => {
if(evt.target.tagName === 'I'){
const id = evt.target.getAttribute('data-id');
//console.log(id);
db.collection('users').doc(id).delete();
}
})

And add this line in ui.js ..Even though if you do not add below line in ui.js it will remove the item from the list once you refreshed the data.

// remove user
const userRemoved
= (id) => {
const user = document.querySelector(.user[data-id=${id}]);
user.remove();
};

Now again execute below command

npm run build
npm run start

Download source:-
https://github.com/shdhumale/pwawithlivedatafirebase.git