Quasar Framework - a SSR+PWA app with dynamic data.
Tags: vue, quasarframework, ssr, couchdb
Published: 2019-09-24 19:53:07

# Table Of Contents
  * [1 Introduction](#1-introduction)
  * [2 CouchDb](#2-couchdb)
  * [3 Quasar Project](#3-quasar-project)
  * [4 Preparing](#4-preparing)
    * [4.1 Utility Belt App Extension](#41-utility-belt-app-extension)
    * [4.2 Installing dependencies](#42-installing-dependencies)
    * [4.3 Setup](#43-setup)
  * [5 Configuring PouchdDb](#5-configuring-pouchddb)
    * [5.1 Creating a Boot File](#51-creating-a-Boot-File)
    * [5.2 Installing the PouchDb plugins](#52-installing-the-pouchdb-plugins)
    * [5.3 Configuring your database schema](#53-configuring-your-database-schema)
    * [5.4 Seeding the database](#54-seeding-the-database)
    * [5.5 Sync the database](#55-sync-the-database)
    * [5.6 How your project would look like?](#56-how-your-project-would-look-like)
  * [6 CouchDb](#6-couchdb)
    * [6.1 Accessing the CouchDb from the App](#61-accessing-the-couchDb-from-the-app)
      * [6.1.1 Alternative 1 - Configuring the CORS](#611-alternative-1-configuring-the-cors)
      * [6.1.2 Alternative 2 - Reverse Proxy](#612-alternative-2-reverse-proxy)
      * [6.1.3 Silver Bullet](#613-silver-bullet)
    * [6.2 Testing the Access](#62-testing-the-access)
  * [7 Centralized Data](#7-centralized-cata)
    * [7.1 Store](#71-store)
    * [7.2 Emitting Events](#72-emitting-events)
    * [7.3 Explanation](#73-explanation)
  * [8 Setting the Framework](#8-setting-the-framework)
  * [9 Listing the People](#9-listing-the-people)
    * [9.1 Configuring the Route](#91-configuring-the-route)
    * [9.2 Creating a View](#92-creating-a-view)
    * [9.3 Adding a State Container and an Empty Page](#93-adding-a-state-container-and-an-empty-page)
    * [9.4 Listening for Changes](#94-listening-for-changes)
    * [9.5 Table and Columns](#95-table-and-columns)
    * [9.6 Actions](#96-actions)
    * [9.7 Screenshots](#97-screenshots)
  * [10 Editing a Person](#10-editing-a-person)
    * [10.1 Creating a View](#101-creating-a-view)
    * [10.2 Adding a State Container and an Empty Page](#102-adding-a-state-container-and-an-empty-page)
    * [10.3 Listening for Changes](#103-listening-for-changes)
    * [10.4 Data Sources](#104-data-sources)
    * [10.5 Actions](#105-actions)
    * [10.6 Screenshots](#106-screenshots)
  * [11 Wrapping the PouchDB instance with a Worker](#11-wrapping-the-pouchdb-instance-with-a-worker)
    * [11.1 Web Worker](#111-web-worker)
    * [11.2 Shared Worker](#112-shared-worker)
    * [11.3 Service Worker](#113-service-worker)
    * [11.4 Silver Bullet](#114-silver-bullet)
  * [12 Syncing when the App is closed](#12-syncing-when-the-app-is-closed)
  * [13 Repository](#13-repository)

## 1 Introduction

We'll build an SSR app that will manage a small CRUD, but the whole CRUD will work offline. To be able of to do that, we'll use PouchDB to persist everything at the client's browser. Then, on the server side, we'll directly query the CouchDB.

We'll use a Quasar app extension that will help us to create the stores and the pages we'll need. If you want to read more about app extensions, check the follow link: [Quasar - Utility Belt App Extension to speedup the development of SSR and offline first apps.](https://dev.to/tobymosque/quasar-utility-belt-app-extension-to-speedup-the-development-of-ssr-and-offline-first-apps-175h)

## 2 CouchDb

Our first step, is to install a CouchDb Instance. Go to [CouchDb Home Page](http://couchdb.apache.org) and follow the instructions.

The exact step-by-steps to install CouchDB will depend of your OS. If you're on `Windows`, it will be as simple as `next > next > finish` wizard. If you're on `Linux`, you'll need to execute some commands in your terminal. That will take some time, but you should be used to it.

To check if everything is working as expected, you would access: http://localhost:5984/_utils, a page like the below one will appear.

![Fauxton](https://thepracticaldev.s3.amazonaws.com/i/yz7qhdwfg6fi5dyvxput.png)

## 3 Quasar Project

First of all, I really recommend you to use `yarn` to manage your local packages and `npm` for the global ones, but you're free to use your preferred package manager.

Our first step is make sure the `@quasar/cli` is installed and `up-to-date`, so even if you already have the cli installed, please run the follow command.

```bash
$ npm i -g @quasar/cli@latest
```

![Update the Quasar CLI](https://thepracticaldev.s3.amazonaws.com/i/94t9jz2u5n91n3pq2ccd.png)

We can now create a new project, run the following command:

```bash
$ quasar create quasar-offline
```

here is what I selected:

```
? Project name (internal usage for dev) quasar-offline
? Project product name (official name; must start with a letter if you will build mobile apps) Quasar App
? Project description A Quasar Framework app
? Author Tobias de Abreu Mesquita <tobias.mesquita@gmail.com>
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)ESLint, Vuex, Axios, Vue-i18n
? Pick an ESLint preset Standard
? Cordova id (disregard if not building mobile apps) org.cordova.quasar.app
? Should we run `npm install` for you after the project has been created? (recommended) yarn
```

Besides the Vuex feature, you aren't bound to any of those options, so feel free to select what you might already do normally.

![Create a new project](https://thepracticaldev.s3.amazonaws.com/i/5lj7qkoa89nycguddkkc.png)

## 4 Preparing

### 4.1 Utility Belt App Extension

```bash
$ quasar ext add "@toby.mosque/utils"
```

### 4.2 Installing dependencies

Since we're planning to use the PouchDB to persist everything at the client-side, we need to install the required packages.

```bash
$ yarn add pouchdb pouchdb-find relational-pouch worker-pouch
```

![Install depedencies](https://thepracticaldev.s3.amazonaws.com/i/bwicvjtcyl8jlvzc084w.png)

### 4.3 Setup

We need to do a few small changes in the project (ok, we'll do a workaround/macgyver).

Edit your `./babel.config.js` to look like:

```js
module.exports = {
  presets: [
    '@quasar/babel-preset-app'
  ]
}
```

Open your `./quasar.conf.js` and extend the webpack with the follow line:

```js
cfg.resolve.alias['pouchdb-promise'] = path.join(__dirname, '/node_modules/pouchdb-promise/lib/index.js')
```

Here a simplified view of the `./quasar.conf.js`.

```js
const path = require('path')
module.exports = function (ctx) {
  return {
    build: {
      extendWebpack (cfg) {
        cfg.resolve.alias['pouchdb-promise'] = path.join(__dirname, '/node_modules/pouchdb-promise/lib/index.js')
      }
    }
  }
}
```

## 5 Configuring PouchdDb

### 5.1 Creating a Boot File

Following the Quasar's philosophy, in order to configure anything, you would create a boot with that single responsibility.

```bash
$ quasar new boot pouchdb/index
```

You need to register the boot file in the `./quasar.conf.js`

```js
const path = require('path')
module.exports = function (ctx) {
  return {
    boot: [
      'i18n',
      'axios',
      'pouchdb/index'
    ]
  }
}
```

### 5.2 Installing the PouchDb plugins

We'll install the pouchdb's plugins in a separated file:

Create `./src/boot/pouchdb/setup.js` and modify it to look like this:
```js
import PouchDB from 'pouchdb'
import RelationalPouch from 'relational-pouch'
import PouchDbFind from 'pouchdb-find'
import WorkerPouch from 'worker-pouch'

PouchDB.adapter('worker', WorkerPouch)
PouchDB.plugin(RelationalPouch)
PouchDB.plugin(PouchDbFind)

export default PouchDB
```

Now, edit the `./src/boot/pouchdb/index.js`
```js
import PouchDB from './setup'

class Database {
  local = void 0
  remote = void 0
  syncHandler = void 0
  async configure ({ isSSR }) {
    if (isSSR) {
      this.local = new PouchDB('http://localhost:5984/master/')
    } else {
      this.local = new PouchDB('db')
      this.remote = new PouchDB('http://localhost:5984/master/')
    }
  }
}

const db = new Database()
export default async ({ Vue, ssrContext }) => {
  await db.configure({ isSSR: !!ssrContext })
  Vue.prototype.$db = db
}

export { db }
```

What are we doing here? We need a slightly different behavior when the code is running at the client-side when compared to the server-side. 

When at the server-side, the app will query the CouchDb instance directly.
When at the client-side, the app will rely only on the local database and sync whenever a connection is available.

### 5.3 Configuring your database schema

One of the common mistakes what devs do when starting with `PouchDb`/`CouchDb`, is create a table for each doc type (based on personal experience), but soon they will figure out that this isn't a good idea. Each database needs a dedicated connection in order to sync properly.

To solve that problem, we will persist everything in a single table. Personally, I believe it is easy to think about the data in a relational way, so we'll use a PouchDB plugin to abstract that: [relational-pouch](https://github.com/pouchdb-community/relational-pouch)

We already registered the plugin in the previous step, but we still need to configure the database schema. Again, we'll do that in a separate file:

Create `./src/boot/pouchdb/create.js` and modify it to look like this:
```js
import PouchDB from './setup'

export default function (name, options) {
  let db = options !== void 0 ? new PouchDB(name, options) : new PouchDB(name)
  db.setSchema([
    {
      singular: 'person',
      plural: 'people',
      relations: {
        company: { belongsTo: { type: 'company', options: { async: true } } },
        job: { belongsTo: { type: 'job', options: { async: true } } }
      }
    },
    {
      singular: 'company',
      plural: 'companies',
      relations: {
        people: { hasMany: { type: 'person', options: { async: true, queryInverse: 'person' } } }
      }
    },
    {
      singular: 'job',
      plural: 'jobs',
      relations: {
        people: { hasMany: { type: 'person', options: { async: true, queryInverse: 'person' } } }
      }
    }
  ])
  return db
}
```

One more time, edit the `./src/boot/pouchdb/index.js`

```js
import create from './create'

class Database {
  local = void 0
  remote = void 0
  syncHandler = void 0
  async configure ({ isSSR }) {
    if (isSSR) {
      this.local = create('http://localhost:5984/master/')
    } else {
      this.local = create('db')
      this.remote = create('http://localhost:5984/master/')
    }
  }
}

const db = new Database()
export default async ({ Vue, ssrContext }) => {
  await db.configure({ isSSR: !!ssrContext })
  Vue.prototype.$db = db
}

export { db }
```

### 5.4 Seeding the database

Now, let's seed our database with some data. We'll do that only at the server-side. And again, we'll do that in a separate file:

In order to generate our data (for this article), we'll use [FakerJS](https://github.com/marak/Faker.js/)

```bash
yarn add faker
```

Create `./src/boot/pouchdb/seed.js` and modify it to look like this:
```js
import uuid from '@toby.mosque/utils'
import faker from 'faker'

export default async function (db) {
  var { people: dbpeople } = await db.rel.find('person', { limit: 1 })
  if (dbpeople && dbpeople.length > 0) {
    return
  }

  faker.locale = 'en_US'
  let companies = []
  for (let i = 0; i < 5; i++) {
    let company = {}
    company.id = uuid.comb()
    company.name = faker.company.companyName()
    companies.push(company)
  }

  let jobs = []
  for (let i = 0; i < 10; i++) {
    let job = {}
    job.id = uuid.comb()
    job.name = faker.name.jobTitle()
    jobs.push(job)
  }

  let people = []
  for (let i = 0; i < 100; i++) {
    let companyIndex = Math.floor(Math.random() * Math.floor(5))
    let jobIndex = Math.floor(Math.random() * Math.floor(10))
    let company = companies[companyIndex]
    let job = jobs[jobIndex]
    let person = {}
    person.id = uuid.comb()
    person.firstName = faker.name.firstName()
    person.lastName = faker.name.lastName()
    person.email = faker.internet.email()
    person.company = company.id
    person.job = job.id
    people.push(person)
  }

  for (let company of companies) {
    await db.rel.save('company', company)
  }

  for (let job of jobs) {
    await db.rel.save('job', job)
  }

  for (let person of people) {
    await db.rel.save('person', person)
  }
}
```

Now call the seed when the boot is running at the server-side:
```js
import create from './create'
import seed from './seed'

class Database {
  local = void 0
  remote = void 0
  syncHandler = void 0
  async configure ({ isSSR }) {
    if (isSSR) {
      this.local = create('http://localhost:5984/master/')
      await seed(this.local)
    } else {
      this.local = create('db')
      this.remote = create('http://localhost:5984/master/')
    }
  }
}

const db = new Database()
export default async ({ Vue, ssrContext }) => {
  await db.configure({ isSSR: !!ssrContext })
  Vue.prototype.$db = db
}

export { db }
```

### 5.5 Sync the database

Finally, we need to sync the data between the remote and the local databases.

When the app starts, before anything, we will try to do a complete replication. To make that task more clear, we'll wrap the replication method inside a promise:

```js
async replicate ({ source, target }) {
  return new Promise((resolve, reject) => {
    source.replicate.to(target).on('complete', resolve).on('error', reject)
  })
}
```

We'll verify if the app is online and try to do a complete replication (remember, the client has to be online for this action). If something goes wrong, it is because the client is offline or the CouchDB, but that wouldn't prevent the user from accessing the system.

```js
if (navigator.onLine) {
  try {
    await this.replicate({ source: this.remote, target: this.local })
    await this.replicate({ source: this.local, target: this.remote })
  } catch (err) {

  }
}
```

After that, we'll start the live replication and track any changes.

```js
this.syncHandler = this.local.sync(this.remote, {
  live: true,
  retry: true
})
this.local.changes({
  since: 'now',
  live: true,
  include_docs: true
}).on('change', onChange)
```

Now your boot file would look like this:

```js
import create from './create'
import seed from './seed'

class Database {
  local = void 0
  remote = void 0
  syncHandler = void 0
  async configure ({ isSSR, onChange }) {
    if (isSSR) {
      this.local = create('http://localhost:5984/master/')
      await seed(this.local)
    } else {
      this.local = create('db')
      this.remote = create('http://localhost:5984/master/')
      if (navigator.onLine) {
        try {
          await this.replicate({ source: this.remote, target: this.local })
          await this.replicate({ source: this.local, target: this.remote })
        } catch (err) {

        }
      }
      this.syncHandler = this.local.sync(this.remote, {
        live: true,
        retry: true
      })
      this.local.changes({
        since: 'now',
        live: true,
        include_docs: true
      }).on('change', onChange)
    }
  }
  async replicate ({ source, target }) {
    return new Promise((resolve, reject) => {
      source.replicate.to(target).on('complete', resolve).on('error', reject)
    })
  }
}

const db = new Database()
export default async ({ Vue, ssrContext }) => {
  await db.configure({
    isSSR: !!ssrContext,
    onChange (change) {
      console.log(change)
    }
  })
  if (!ssrContext) {
    var { people } = await db.rel.find('person')
    console.log(people)
  }
  Vue.prototype.$db = db
}

export { db }
```

### 5.6 How your project would look like?

![Project Overview](https://thepracticaldev.s3.amazonaws.com/i/3qx7zy64mg1lgec5camk.png)

## 6 CouchDb

### 6.1 Accessing the CouchDb from the App

If you try to run your app, you'll notice than CouchDB is refusing any connection from the client-side. Here you have two options; configure your app to act as a reverse proxy of the CouchDB, or configure the CORS of your CouchDb instance.

#### 6.1.1 Alternative 1 - Configuring the CORS

Open the Fauxton (http://localhost:5984/_utils), go into the configurations, CORS, and enable it.

![Enable CORS](https://thepracticaldev.s3.amazonaws.com/i/84oimjod2xqe4fvz5owh.png)

#### 6.1.2 Alternative 2 - Reverse Proxy

Install the follow package

```bash
yarn add --dev http-proxy-middleware
```

Edit your `./src-ssr/extention.js` to look like this:
```js
var proxy = require('http-proxy-middleware')
module.exports.extendApp = function ({ app, ssr }) {
  app.use(
    '/db',
    proxy({
      target: 'http://localhost:5984',
      changeOrigin: true,
      pathRewrite: { '^/db': '/' }
    })
  )
}
```

![SSR Setup](https://thepracticaldev.s3.amazonaws.com/i/q6mvvbt85das1fncaki1.png)

Edit your boot file:
```js
if (isSSR) {
  this.local = create('http://localhost:5984/master/')
  await seed(this.local)
} else {
  this.local = create('db')
  // you can't use a relative path here
  this.remote = create(`${location.protocol}//${location.host}/db/master/`)
}
```

#### 6.1.3 Silver Bullet

You don't know what alternative to pick? Use the reverse proxy, since that will give to you more freedom.

### 6.2 Testing the Access

Run your app:

```bash
$ quasar dev -m ssr
```

![Quasar App](https://thepracticaldev.s3.amazonaws.com/i/yd9awnwcvb425612lgu4.png)

Now check your console. If you see a list with 100 persons, everything is running as expected.

## 7 Centralized Data

### 7.1 Store

Since this is an SSR app, we don't want to query the whole database at the server-side, but would be a good idea to query the domain entities. We'll handle the job and company entities as being our domain entities (since they are used in all routes).

Our first step, is create a store (using Vuex) to hold the both collections:

**src/store/database.js**
```js
import { factory } from '@toby.mosque/utils'
import { db } from 'src/boot/pouchdb'
const { store } = factory

const options = {
  model: class PeopleModel {
    companies = []
    jobs = []
  },
  collections: [
    { single: 'company', plural: 'companies', id: 'id' },
    { single: 'job', plural: 'jobs', id: 'id' }
  ]
}

export default store({
  options,
  actions: {
    async initialize ({ commit }) {
      let { companies } = await db.local.rel.find('company')
      let { jobs } = await db.local.rel.find('job')
      commit('companies', companies)
      commit('jobs', jobs) 
    }
  }
})
```

**src/store/index.js**
```js
import Vue from 'vue'
import Vuex from 'vuex'

import database from './database'

Vue.use(Vuex)

export default function () {
  const Store = new Vuex.Store({
    modules: {
      database
    },
    strict: process.env.DEV
  })

  return Store
}
```

### 7.2 Emitting Events

Since our data is being synced with a remote database in real-time, the CRUD operations will happen outside of our store. Because of that, we need to track them and emit events to update our centralized store every time that happens.

In order to do that, we need to modify the boot file: `./src/boot/pouchdb/index.js`

```js
// ...

const db = new Database()
export default async ({ Vue, store, router, ssrContext }) => {
  await db.configure({
    isSSR: !!ssrContext,
    onChange (change) {
      let { data, _id, _rev, _deleted } = change.doc
      let parsed = db.local.rel.parseDocID(_id)
      let event = events[parsed.type]

      if (_deleted) {
        router.app.$emit(parsed.type, { id: parsed.id, _deleted })
        router.app.$emit(parsed.id, { _deleted })
        if (event) {
          store.dispatch(event.delete, parsed.id)
        }
      } else {
        data.id = parsed.id
        data.rev = _rev
        router.app.$emit(parsed.type, data)
        router.app.$emit(parsed.id, data)
        if (event) {
          store.dispatch(event.save, data)
        }
      }
    }
  })
  await store.dispatch('database/initialize')
  Vue.prototype.$db = db
}

export { db }
```

### 7.3 Explanation

let's imagine that someone updated a person, in that case the change object will look like:

```js
{
  id: person_2_016d0c65-670c-1d7d-9b96-f3ef340aa681,
  seq: ...,
  changes: [{ ... }, { ... }],
  doc: {
    "_id": "person_2_016d0c65-670c-1d7d-9b96-f3ef340aa681",
    "_rev": "2-0acd99b71f352cca4c780c90d5c23608",
    "data": {
      "firstName": "Mylene",
      "lastName": "Schmitt",
      "email": "Coby83@gmail.com",
      "company": "016d0c65-670a-8add-b10f-e9802d05c93a",
      "job": "016d0c65-670b-37bf-7d79-b23daf00fe58"
    }
  }
}
```

In order to properly index the docs, the relational-pouch plugin modifies the id before of save, appending the type of doc and the type of the key (2 means the key is a string). sWe need break it down in order to get the type of the doc and your id.

```js
let _id = 'person_2_016d0c65-670c-1d7d-9b96-f3ef340aa681'
let parsed = db.local.rel.parseDocID(_id)
console.log(parsed)
// { id: '016d0c65-670c-1d7d-9b96-f3ef340aa681', type: 'person'}
```

Now, we will emit 2 events to inform the app that some document got updated.

1. The first one, is meant to inform components who hold a collection of records, the event name is the type.
2. The second one, is meant to inform components who hold the details of a specific record, the event name is the record id (that is unique across the app).

```js
if (_deleted) {
  router.app.$emit('person', { id: '016d0c65-670c-1d7d-9b96-f3ef340aa681', _deleted: true })
  router.app.$emit('016d0c65-670c-1d7d-9b96-f3ef340aa681', { _deleted: true })
} else {
  data.id = parsed.id
  data.rev = _rev
  router.app.$emit('person', data)
  router.app.$emit('016d0c65-670c-1d7d-9b96-f3ef340aa681', data)
}
```

Our last step, is update the centralized store. We will dispatch an action that will update the store:

```js
if (_deleted) {
  if (event) {
    store.dispatch('database/deletePerson', parsed.id)
  }
} else {
  if (event) {
    store.dispatch('database/saveOrUpdatePerson', data)
  }
}
```

## 8 Setting the Framework

Let's configure the framework to use the preFetch and auto discovery the components. Set the `config > preFetch` to `true` and `config > framework > all` to `'auto'`. Here a simplified view of the `./quasar.conf.js`

```js
const path = require('path')
module.exports = function (ctx) {
  return {
    build: {
      preFetch: true,
      framework: {
        all: 'auto',
        plugins: [...]
      }
    }
  }
}
```

## 9 Listing the People

We already have some data working and the syncing process is configured. Let's create some pages. But first, we need to update the `src/router/routes.js` file to look like.:

### 9.1 Configuring the Route

```js
const routes = [
  {
    path: '/',
    component: () => import('layouts/MyLayout.vue'),
    children: [
      { path: '', redirect: '/people/' },
      { path: 'people/', component: () => import('pages/People/Index.vue') },
      { path: 'people/:id', component: () => import('pages/Person/Index.vue') }
    ]
  }
]

// Always leave this as last one
if (process.env.MODE !== 'ssr') {
  routes.push({
    path: '*',
    component: () => import('pages/Error404.vue')
  })
}

export default routes
```

### 9.2 Creating a View

Now, create the `src/pages/People/Index.vue` file to look like this:

```html
<template>
  <q-page class="q-pa-md">
    <q-table title="People" :data="people" :columns="columns" row-key="id" >
      <template v-slot:top-left>
        <q-btn color="positive" icon="edit" label="create" to="/people/create" />
      </template>
      <template v-slot:body-cell-actions="props">
        <q-td class="q-gutter-x-sm">
          <q-btn round outline color="primary" icon="edit" :to="'/people/' + props.value" />
          <q-btn round outline color="negative" icon="delete" @click="remove(props.row)" />
        </q-td>
      </template>
    </q-table>
  </q-page>
</template>

<style>
</style>

<script src="./Index.vue.js">
</script>
```

### 9.3 Adding a State Container and an Empty Page

We need to create `src/pages/People/Index.vue.js`. Out first step will be create a `state container` and an empty page:

```js
import { factory } from '@toby.mosque/utils'
import { db } from 'src/boot/pouchdb'
import { mapGetters, mapActions } from 'vuex'
const { page, store } = factory

const moduleName = 'people'
const options = {
  model: class PeopleModel {
    people = []
  },
  collections: [
    { single: 'person', plural: 'people', id: 'id' }
  ]
}

const storeModule = store({
  options,
  actions: {
    async initialize ({ commit }, { route }) {
      let { people } = await db.local.rel.find('person')
      commit('people', people)
    },
    async remove (context, person) {
      await db.local.rel.del('person', { id: person.id, rev: person.rev })
    }
  }
})

export default page({
  name: 'PeoplePage',
  options,
  moduleName,
  storeModule,
  mounted () { ... },
  destroyed () { ... },
  data () { ... },
  computed: { ... },
  methods: {
    ...mapActions(moduleName, { __remove: 'remove' }),
    ...
  }
})
```

If you're worried that the `remove` action didn't `commit` anything, that is intentional. Since we'll be listening for changes, as soon a person gets deleted (no matter who, where and/or when), it will be reflected at the state container.

### 9.4 Listening for Changes

In order to listen for any changes at the people collection, we'll need to update the mounted and destroyed hooks, and enable/disable some events listeners.

```js
export default page({
  ...
  mounted () {
    let self = this
    if (!this.listener) {
      this.listener = entity => {
        if (entity._deleted) {
          self.deletePerson(entity.id)
        } else {
          self.saveOrUpdatePerson(entity)
        }
      }
      this.$root.$on('person', this.listener)
    }
  },
  destroyed () {
    if (this.listener) {
      this.$root.$off('person', this.listener)
    }
  }
  ...
})
```

Doing this, every time when a person gets created, updated or deleted, the state container will be updated, regardless of the origin of the modification.

### 9.5 Table and Columns

Since we're using a table to display the people, we will need to configure our columns, six in total (`firstName`, `lastName`, `email`, `job`, `company`, `actions`).

But, the `job` and `company` fields didn't hold the descriptions, but ids, we'll need to map them to your respective descriptions. We'll need to edit the `computed` properties to look like:

```js
export default page({
  ...
  computed:  {
    ...mapGetters('database', ['jobById', 'companyById'])
  }
  ...
})
```

Now, we'll create the columns definitions inside the `data` hook

```js
export default page({
  ...
  data () {
    let self = this
    return {
      columns: [
        { name: 'firstName', field: 'firstName', label: 'First Name', sortable: true, required: true, align: 'left' },
        { name: 'lastName', field: 'lastName', label: 'Last Name', sortable: true, required: true, align: 'left' },
        { name: 'email', field: 'email', label: 'Email', sortable: true, required: true, align: 'left' },
        {
          name: 'job',
          label: 'Job',
          sortable: true,
          required: true,
          field (row) { return self.jobById(row.job).name },
          align: 'left'
        },
        {
          name: 'company',
          label: 'Company',
          sortable: true,
          required: true,
          field (row) { return self.companyById(row.company).name },
          align: 'left'
        },
        { name: 'actions', field: 'id', label: 'Actions', sortable: false, required: true, align: 'center' }
      ]
    }
  },
  ...
})
```

### 9.6 Actions

It's time to configure our actions. To be exact, our unique action: delete a person. We'll edit our methods hook to look like this:

```js
export default page({
  ...
  methods: {
    ...mapActions(moduleName, { __remove: 'remove' }),
    remove (row) {
      this.$q.dialog({
        color: 'warning',
        title: 'Delete',
        message: `Do u wanna delete ${row.firstName} ${row.lastName}`,
        cancel: true
      }).onOk(async () => {
        try {
          await this.__remove(row)
          this.$q.notify({
            color: 'positive',
            message: 'successfully deleted'
          })
        } catch (err) {
          console.error(err)
          this.$q.notify({
            color: 'negative',
            message: 'failed at delete'
          })
        }
      })
    }
  }
})
```

### 9.7 Screenshots

![Project Overview](https://thepracticaldev.s3.amazonaws.com/i/t15bugw8czglwfdddfoh.png)

![App Preview](https://thepracticaldev.s3.amazonaws.com/i/2jzmnfr6r2rfxx7r00wr.png)

## 10 Editing a Person

### 10.1 Creating a View

Create the `src/pages/Person/Index.vue` file, and edit it to look like this:

```html
<template>
  <q-page class="q-pa-md">
    <q-card class="full-width">
      <q-card-section>
        Person
      </q-card-section>
      <q-separator />
      <q-card-section class="q-gutter-y-sm">
        <q-input v-model="firstName" label="First Name" outlined />
        <q-input v-model="lastName" label="Last Name" outlined />
        <q-input v-model="email" label="Email" type="email" outlined />
        <q-select v-model="company" label="Company" map-options emit-value option-value="id" option-label="name" outlined :options="companies" />
        <q-select v-model="job" label="Job" map-options emit-value option-value="id" option-label="name" outlined :options="jobs" />
      </q-card-section>
      <q-separator />
      <q-card-actions class="row q-px-md q-col-gutter-x-sm">
        <div class="col col-4">
          <q-btn class="full-width" color="grey-6" label="return" to="/people/" />
        </div>
        <div class="col col-8">
          <q-btn class="full-width" color="positive" label="save" @click="save" />
        </div>
      </q-card-actions>
    </q-card>
  </q-page>
</template>

<style>
</style>

<script src="./Index.vue.js">
</script>
```

### 10.2 Adding a State Container and an Empty Page

We need to create `src/pages/Person/Index.vue.js`, our first step will be create a `state container` and a empty page:

```js
import { factory, store as storeUtils, uuid } from '@toby.mosque/utils'
import { db } from 'src/boot/pouchdb'
import { mapActions } from 'vuex'
const { mapState } = storeUtils
const { page, store } = factory

const options = {
  model: class PersonModel {
    id = ''
    rev = ''
    firstName = ''
    lastName = ''
    email = ''
    job = ''
    company = ''
  }
}

const moduleName = 'person'
const storeModule = store({
  options,
  actions: {
    async initialize ({ dispatch, commit }, { route }) {
      let person = await dispatch('personById', route.params.id)
      commit('id', person.id || uuid.comb())
      commit('rev', person.rev)
      commit('firstName', person.firstName)
      commit('lastName', person.lastName)
      commit('email', person.email)
      commit('job', person.job)
      commit('company', person.company)
    },
    async personById (context, id) {
      let { people } = await db.local.rel.find('person', id)
      let person = people && people.length > 0 ? people[0] : {}
      return person
    },
    async save ({ state }) {
      let current = { ...state }
      delete current['@@']
      await db.local.rel.save('person', current)
    }
  }
})

export default page({
  name: 'PersonPage',
  options,
  moduleName,
  storeModule,
  mounted () { ... },
  destroyed () { ... },
  computed: { ... },
  methods: {
    ...mapActions(moduleName, { __save: 'save', initialize: 'initialize' }),
    ...
  }
})
```

Again, don't worry with the `save`. The lack of a `commit` is intentional, since we'll be listening for changes. As soon as the current person gets modified (no matter who, where and/or when) the page will be notified.

### 10.3 Listening for Changes

In order to listen for any changes to the current person, we'll need to update the mounted and destroyed hooks, and enable/disable some event listeners. 

But unlike what we did before, we'll only notify the application and let the user decide what they want to do.

```js
export default page({
  ...
  mounted () {
    if (this.rev && !this.listener) {
      this.listener = entity => {
        if (entity._deleted) {
          // if that person got deleted, the unique option to the user is leave that page.
          this.$q.dialog({
            parent: this,
            color: 'warning',
            title: 'Deleted',
            message: 'Someone deleted this person'
          }).onDismiss(() => {
            this.$router.push('/people/')
          })
        } else {
          // if that person got update, the user will be able to keep the changes or discard them.
          this.$q.dialog({
            parent: this,
            color: 'warning',
            title: 'Deleted',
            cancel: 'No',
            ok: 'yes',
            message: 'Someone updated this person. do u wanna refresh the fields?'
          }).onOk(() => {
            this.initialize({ route: this.$route })
          }).onCancel(() => {
            this.rev = entity.rev
          })
        }
      }
      this.$root.$on(this.id, this.listener)
    }
  },
  destroyed () {
    if (this.rev && this.listener) {
      this.$root.$off(this.id, this.listener)
    }
  },
  ...
})
```

Doing this, every time the current person gets updated or deleted, the user will then be notified, regardless of the origin of the modification.

### 10.4 Data Sources

Like before, the `job` and `company` fields didn't hold the descriptions, but ids. But now we need the entire collection of `jobs` and `companies` in order to fetch the `QSelect` options.:

```js
export default page({
  ...
  computed: {
    ...mapState('database', ['jobs', 'companies'])
  },
  ...
})
```

### 10.5 Actions

Now, it's the time to write our save method. We'll edit our methods hook to look like:

```js
export default page({
  ...
  methods: {
    ...mapActions(moduleName, { __save: 'save', initialize: 'initialize' }),
    async save () {
      try {
        await this.__save()
        this.$q.notify({
          color: 'positive',
          message: 'successfully saved'
        })
        this.$router.push('/people/')
      } catch (err) {
        this.$q.notify({
          color: 'negative',
          message: 'failure at save'
        })
      }
    }
  }
})
```

### 10.6 Screenshots

![Project Overview](https://thepracticaldev.s3.amazonaws.com/i/24793jokpwys9n21lc8m.png)
![Create](https://thepracticaldev.s3.amazonaws.com/i/zbwyqlm9ne0fhz8bp9il.png)
![On Update](https://thepracticaldev.s3.amazonaws.com/i/yi1r5fbol8af5ii4c5fr.png)
![Confirm Delete](https://thepracticaldev.s3.amazonaws.com/i/s42ne6lhfzb8nflt1771.png)
![On Delete](https://thepracticaldev.s3.amazonaws.com/i/wqd8x83sreint9kta8pr.png)

## 11 Wrapping the PouchDB instance with a Worker

Until now, all DB operations are being made in the main thread, that includes queries, updates, deletes, sync, etc.

If you have a large database and you're creating or updating documents often, your UI can suffer from constant blocking, that will result in a poor user experience.

Anyway, I really recommend you move any DB operations to a separate thread. to achieve that you'll need this package:

```
yarn add worker-pouch
```

### 11.1 Web Worker

This is the basic setup. Your first step is to verify if the `worker adapter` is configured. Just open the `src/boot/pouchdb/setup.js` and look for:

```js
import PouchDB from 'pouchdb'
import WorkerPouch from 'worker-pouch'

PouchDB.adapter('worker', WorkerPouch)
export default PouchDB
```

Our second step, is to configure the local database to use the `worker adapter`. Just open `src/boot/pouchdb/input.js` and replace:

```js
async configure ({ isSSR, onChange }) {
  if (isSSR) {
    // ...
  } else {
    this.local = create('db')
    // ...
  }
}
```

with

```js
async configure ({ isSSR, onChange }) {
  if (isSSR) {
    // ...
  } else {
    this.local = create('db', { adapter: 'worker' })
    // ...
  }
}
```

Done, for now, all our DB operations are now in a separated worker thread.

### 11.2 Shared Worker

The biggest problem with the synchronous process is if you had multiple browser tabs opened, they will all access a single instance of the LocalStorage. If you update a document in one of the tabs, the others tabs will not be notified.

If you want all of your tabs notified, you'll need to use a `SharedWorker`. In this case, you'll have only one worker for all the tabs.

TODO: waiting https://github.com/GoogleChromeLabs/worker-plugin/pull/42 to be merged.

### 11.3 Service Worker

Besides the name of this article, until now our app isn't a PWA. Let's change that. Open the `./quasar.conf.js` and set the `ssr > pwa` to `true`.

```js
const path = require('path')
module.exports = function (ctx) {
  return {
    ssr: {
      pwa: true
    }
  }
}
```

Now, the workbox is configured and our app has a Service Worker, but we haven't great control over it, anyway we can change that. Open your ./quasar.conf.js and configure your pwa > workboxPluginMode to be InjectManifest:

```js
const path = require('path')
module.exports = function (ctx) {
  return {
    pwa: {
      workboxPluginMode: 'InjectManifest'
    }
  }
}
```

Now, we need to edit the `./src-pwa/custom-service-worker.js` to look like this:

```js
/*
 * This file (which will be your service worker)
 * is picked up by the build system ONLY if
 * quasar.conf > pwa > workboxPluginMode is set to "InjectManifest"
 */
/*eslint-disable*/
workbox.core.setCacheNameDetails({prefix: "pouchdb-offline"})

self.skipWaiting()
self.__precacheManifest = [].concat(self.__precacheManifest || [])
workbox.precaching.precacheAndRoute(self.__precacheManifest, {
  "directoryIndex": "/"
})
workbox.routing.registerRoute("/", new workbox.strategies.NetworkFirst(), 'GET')
workbox.routing.registerRoute(/^http/, new workbox.strategies.NetworkFirst(), 'GET')

self.addEventListener('activate', function(event) {
  event.waitUntil(self.clients.claim())
})
```

In order to move the DB operations into the `Service Worker`, we need to configure the webpack, so it'll be able to transpile some dependencies.

```
yarn add --dev serviceworker-webpack-plugin
```

Edit `./quasar.conf.js` one more time:

```js
const path = require('path')
module.exports = function (ctx) {
  return {
    build: {
      extendWebpack (cfg, { isServer }) {
        cfg.resolve.alias['pouchdb-promise'] = path.join(__dirname, '/node_modules/pouchdb-promise/lib/index.js')
        cfg.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /node_modules/,
          options: {
            formatter: require('eslint').CLIEngine.getFormatter('stylish')
          }
        })

        if (!isServer) {
          const worker = new ServiceWorkerWebpackPlugin({
            entry: path.join(__dirname, 'src-pwa/pouchdb-service-worker.js'),
            filename: 'pouchdb-service-worker.js'
          })
          cfg.plugins = cfg.plugins || []
          cfg.plugins.push(worker)
        }
      }
    }
  }
}
```

Now, create the `./src-pwa/pouchdb-service-worker.js` and edit your content to be like:

```js
/*eslint-disable*/
let registerWorkerPouch = require('worker-pouch/worker')
let PouchDB = require('pouchdb')

PouchDB = PouchDB.default && !PouchDB.plugin ? PouchDB.default : PouchDB
registerWorkerPouch = registerWorkerPouch.default && !registerWorkerPouch.call ? registerWorkerPouch.default : registerWorkerPouch

self.registerWorkerPouch = registerWorkerPouch
self.PouchDB = PouchDB
```

Finally, modify the `./src-pwa/custom-service-worker.js` in order to import the worker-pouch related scripts and register them:

```js
/*
 * This file (which will be your service worker)
 * is picked up by the build system ONLY if
 * quasar.conf > pwa > workboxPluginMode is set to "InjectManifest"
 */
/*eslint-disable*/
importScripts(`pouchdb-service-worker.js`)
workbox.core.setCacheNameDetails({prefix: "pouchdb-offline"})

self.skipWaiting()
self.__precacheManifest = [].concat(self.__precacheManifest || [])
workbox.precaching.precacheAndRoute(self.__precacheManifest, {
  "directoryIndex": "/"
})
workbox.routing.registerRoute("/", new workbox.strategies.NetworkFirst(), 'GET')
workbox.routing.registerRoute(/^http/, new workbox.strategies.NetworkFirst(), 'GET')

registerWorkerPouch(self, PouchDB)
self.addEventListener('activate', function(event) {
  event.waitUntil(self.clients.claim())
})
```

We need to modify our `./src/boot/pouchdb/index.js` so the local `pouchdb` instance points to the `Service Worker`:

```js
async configure ({ isSSR, onChange }) {
  if (isSSR) {
    // ...
  } else {
    if ('serviceWorker' in navigator) {
      if (!navigator.serviceWorker.controller) {
        await new Promise(resolve => {
          navigator.serviceWorker.addEventListener('controllerchange', resolve, { once: true })
        })
      }
      this.local = create('db', {
        adapter: 'worker',
        worker () {
          return navigator.serviceWorker
        }
      })
    } else {
      this.local = create('db', { adapter: 'worker' })
    }
    // ...
  }
}
```

If you check your network tab, it should now look like:

![Network Tab](https://thepracticaldev.s3.amazonaws.com/i/ncqyp91tauia2rsuwpqe.png)

### 11.4 Silver Bullet

You don't know what worker to pick? Use the `SharedWorker`, since that didn't have drawbacks over the `DedicatedWorker` and the `ServiceWorker` will not stay active after the app is closed.

## 12 Syncing when the App is closed

**That is just a Overview**

The `Service Worker` will stay active only while the app is open. Even if we move the DB operations to run inside the `Service Worker` the sync will stop as soon the app is closed.

To let the DB be synced even when the app is closed, we'll need to turn our server in a push-server using the [web-push](https://www.npmjs.com/package/web-push), after that, we need to sign the clients to the push server.

After the push is configured, we can configure a cron job to send a push periodically (like each 30 minutes), and the client will start the sync process every time it receives a notification.

## 13 Repository

You can check the final project here:
https://gitlab.com/TobyMosque/quasar-couchdb-offline