Quasar - Utility Belt App Extension to speedup the development of SSR and offline first apps.
Tags: quasar, quasarframework, vue, appextension
Published: 2019-09-10 23:45:23

# Table Of Contents
  * [1 Introduction](#1-introduction)
  * [2 Installing](#2-installing)
  * [3 Timer](#3-timer)
    * [3.1 Sleep](#31-sleep)
  * [4 UUID](#4-uuid)
    * [4.1 comb](#41-comb)
      * [4.1.1 parameters](#411-parameters)
    * [4.2 extractDate](#42-extractdate)
      * [4.2.1 parameters](#421-parameters)
    * [4.3 Demos](#43-demos)
      * [4.3.1 Basic Usage](#431-basic-usage)
      * [4.3.2 Sorting](#432-sorting)
  * [5 Store](#5-store)
    * [5.1 mapStoreMutations](#51-mapstoremutations)
      * [5.1.1 parameters](#511-parameters)
      * [5.1.2 Demos](#512-demos)
        * [5.1.2.1 Basic Usage](#5121-basic-usage)
    * [5.2 mapStoreCollections](#52-mapsStorecollections)
      * [5.2.1 parameters](#521-parameters)
      * [5.2.1.1 Collection info object](#5211-collection-info-object)
      * [5.2.2 Demos](#522-demos)
        * [5.2.2.1 Basic Usage](#5221-basic-usage)
      * [5.2.3 Scenario without the helper](#523-scenario-without-the-helper)
    * [5.3 mapState](#53-mapstate)
      * [5.3.1 Parameters](#531-parameters)
      * [5.3.2 Demos](#532-demos)
        * [5.3.2.1 Basic Usage](#5321-basic-usage)
  * [6 Factories](#6-factories)
    * [6.1 component](#61-component)
      * [6.1.1 Parameters](#611-parameters)
        * [6.1.1.1 Options](#6111-options)
      * [6.1.2 Demos](#612-demos)
        * [6.1.2.1 Basic Usage](#6121-basic-usage)
        * [6.1.2.2 More Reusable](#6122-more-reusable)
    * [6.2 Store](#62-store)
      * [6.2.1 Parameters](#621-parameters)
        * [6.2.1.1 options](#6211-options)
      * [6.2.2 Demos](#622-demos)
        * [6.2.2.1 Basic Usage](#6221-basic-usage)
    * [6.3 Page](#63-page)
      * [6.3.1 Parameters](#631-parameters)
        * [6.3.1.1 options](#6311-options)
      * [6.3.2 Demos](#632-demos)
        * [6.3.2.1 Basic Usage](#6321-basic-usage)
    * [6.4 Page and Store](#64-page-and-store)
      * [6.4.1 Demos](#641-demos)
        * [6.4.1.1 Basic Usage](#6411-basic-usage)

## 1 Introduction

I developed a small app extension with the goal to speedup the development/prototyping of the SSR and offline first apps. I'll try to explain all the helpers, how to use them and what problem they try to solve.

## 2 Installing

```js
quasar ext add "@toby.mosque/utils"
```

Remember, the full package name is `@toby.mosque/quasar-app-extension-utils`, but for convenience, the extension will register `@toby.mosque/utils` as an alias to `@toby.mosque/quasar-app-extension-utils/src/utils`.

## 3 Timer

### 3.1 Sleep

`time.sleep` is as simple as:

```js
export default function (delay) {
  return new Promise(resolve => setTimeout(resolve, delay))
}
```

the goal is to enable the dev to suspend an async method for sometime.

```js
import { timer } from '@toby.mosque/utils'

const timestamp = function (date) {
  date = date || new Date()
  return JSON.stringify({ time: date.getTime(), iso: date.toISOString() })
}

export default async (context) => {
  console.log('start: ', timestamp())
  await timer.sleep(5000)
  console.log('end: ', timestamp())
}
```

outputs:

```
start:  {"time":1567290656567,"iso":"2019-08-31T22:30:56.567Z"}
end:    {"time":1567290661570,"iso":"2019-08-31T22:31:01.570Z"}
```

## 4 UUID

A common issue that happens when you use UUID/GUID as primary key, is the order of insertion of your records/docs becomes unpredictable, that can potentially slow down the insertion of new records/docs, or even make the `order by id` a problem to the UX, since the records/docs will look like it's been shuffled around.

### 4.1 comb

The `comb` method is inspired by the C# lib [RT.Comb](https://github.com/richardtallent/RT.Comb), but I implemented only the `Postgre` provider, that replaces the first 6 bytes of the UUID with the current date.

#### 4.1.1 parameters

|order|name|required|default|description|
|---|---|---|---|---|
|1|date|false|new Date()|date used to create the COMB|

### 4.2 extractDate

The `extractDate` method allows the dev to retrieve the date used to create the COMB.

#### 4.2.1 parameters

|order|name|required|default|description|
|---|---|---|---|---|
|1|comb|true|   |comb to have date component exracted|

### 4.3 Demos

#### 4.3.1 Basic Usage

```js
// import something here
import { uuid } from '@toby.mosque/utils'

// "async" is optional
export default async (context) => {
  let date = new Date(2019, 8, 1, 17, 25, 40, 123)
  let comba = uuid.comb()
  let combb = uuid.comb(date)
  console.log('comb a: ', comba)
  console.log('comb b: ', combb)
  console.log('date a: ', timestamp(uuid.extractDate(comba)))
  console.log('date b: ', timestamp(uuid.extractDate(combb)))
}
```

outputs

```
comb a:  016ce9d4-cb8f-4b88-b049-8569e9bec009
comb b:  016cee81-321b-f5ee-92ab-fdafda9612fe
date a:  {"time":1567291132815,"iso":"2019-08-31T22:38:52.815Z"}
date b:  {"time":1567369540123,"iso":"2019-09-01T20:25:40.123Z"}
```

#### 4.3.2 Sorting

```js
import { uuid, timer } from '@toby.mosque/utils'

// "async" is optional
export default async (context) => {
  let combs = []
  for (let i = 1; i <= 10; i++) {
    await timer.sleep(1)
    combs.push({ index: i, value: uuid.comb() })
  }

  combs.sort((a, b) => a.value > b.value ? 1 : -1)
  console.log('ascending:')
  for (let item of combs) {
    console.log(JSON.stringify(item))
  }
  combs.sort((a, b) => a.value < b.value ? 1 : -1)
  console.log('descending: ')
  for (let item of combs) {
    console.log(JSON.stringify(item))
  }
}
```

outputs

```
ascending:
{"index":1,"value":"016ce9dc-9422-be1b-d70f-8769fa62380c"}
{"index":2,"value":"016ce9dc-94e1-40e4-5a16-7db2c0fb109d"}
{"index":3,"value":"016ce9dc-94e3-1316-8633-23694b929895"}
{"index":4,"value":"016ce9dc-94e5-04e3-d043-2b147af04f19"}
{"index":5,"value":"016ce9dc-98ca-8307-1ea1-54a488a1195c"}
{"index":6,"value":"016ce9dc-9cb2-468b-23d6-3bbe47c70539"}
{"index":7,"value":"016ce9dc-a099-fc93-8edf-68f3c00b4fcc"}
{"index":8,"value":"016ce9dc-a482-5f80-b40d-a64b0e8a23a9"}
{"index":9,"value":"016ce9dc-a86a-d6c5-1359-ce0c7b9d053e"}
{"index":10,"value":"016ce9dc-abc4-7d81-e27b-9f1868a48be8"}
descending: 
{"index":10,"value":"016ce9dc-abc4-7d81-e27b-9f1868a48be8"}
{"index":9,"value":"016ce9dc-a86a-d6c5-1359-ce0c7b9d053e"}
{"index":8,"value":"016ce9dc-a482-5f80-b40d-a64b0e8a23a9"}
{"index":7,"value":"016ce9dc-a099-fc93-8edf-68f3c00b4fcc"}
{"index":6,"value":"016ce9dc-9cb2-468b-23d6-3bbe47c70539"}
{"index":5,"value":"016ce9dc-98ca-8307-1ea1-54a488a1195c"}
{"index":4,"value":"016ce9dc-94e5-04e3-d043-2b147af04f19"}
{"index":3,"value":"016ce9dc-94e3-1316-8633-23694b929895"}
{"index":2,"value":"016ce9dc-94e1-40e4-5a16-7db2c0fb109d"}
{"index":1,"value":"016ce9dc-9422-be1b-d70f-8769fa62380c"}
```

## 5 Store

If you're developing an SSR or offline first app, you're probably overusing Vuex modules; and I know, you don't have much choice here. So let's try make parts of the module definition more reusable.

### 5.1 mapStoreMutations

`mapStoreMutations` maps all classes fields to a mutations-like object.

#### 5.1.1 parameters

|order|name|required|default|description|
|---|---|---|---|---|
|1|model|true|   |class used to model the mutations object|

#### 5.1.2 Demos

##### 5.1.2.1 Basic Usage

```js
import { store } from '@toby.mosque/utils'
const { mapStoreMutations } = store

class Model {
  text = ''
  number = 0
  collection = []
}

const storeModule = {
  namespaced: true,
  state () {
    return new Model()
  },
  mutations: {
    ...mapStoreMutations(Model)
  }
}
console.log(storeModule)
export default storeModule
export { Model }
```

Expected Output

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/z0e2vphbp4mo50u75d64.png)

#### 5.1.3 Scenario without the helper

This is what you would need to write to create the above module without the using the `mapStoreMutations` helper

```js
const storeModule = {
  namespaced: true,
  state () {
    return {
      text: '',
      number: 0,
      collection: []
    }
  },
  mutations: {
    text (state, value) { state.text = value },
    number (state, value) { state.number = value },
    collection (state, value) { state.collection = value }
  }
}
console.log(storeModule)
export default storeModule
```

### 5.2 mapStoreCollections

Here the goal is create `mutations` (create, update, delete), `actions` (upsert, delete) and `getters` (index, getById) related to array fields.

#### 5.2.1 parameters

|order|name|required|default|description|
|---|---|---|---|---|
|1|collections|true|   |an array of objects that describes your collection|

#### 5.2.1.1 Collection info object

|field|required|ddescription|
|---|---|---|---|---|
|single|true|the single form of the collection (item, person, job)|
|plural|true|the plural form of the collection (list, people, jobs), would be the same as in the state|
|id|true|the name of the id field of the object in the collection|

#### 5.2.2 Demos

##### 5.2.2.1 Basic Usage

```js
import { store } from '@toby.mosque/utils'
const { mapStoreMutations, mapStoreCollections } = store

class Model {
  text = ''
  number = 0
  collection = []
}

const collection = mapStoreCollections([
  { single: 'item', plural: 'collection', id: 'id' }
])

const storeModule = {
  namespaced: true,
  state () {
    return new Model()
  },
  mutations: {
    ...mapStoreMutations(Model),
    ...collection.mutations
  },
  actions: {
    ...collection.actions
  },
  getters: {
    ...collection.getters
  }
}
console.log(storeModule)
export default storeModule
export { Model }
```

Expected Output

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/yvk78wwfreq4rl0ln0ek.png)

#### 5.2.3 Scenario without the helper

This is what you would need to write in order to create the above module without the help of the `mapStoreMutations` and `mapStoreCollections` helpers

```js
import Vue from 'vue'

const storeModule = {
  namespaced: true,
  state () {
    return {
      text: '',
      number: 0,
      collection: []
    }
  },
  mutations: {
    text (state, value) { state.text = value },
    number (state, value) { state.number = value },
    collection (state, value) { state.collection = value },
    updateItem (state, { index, item }) { Vue.set(state.collection, index, item) },
    createItem (state, item) { state.collection.push(item) },
    deleteItem (state, index) { Vue.delete(state.collection, index) }
  },
  actions: {
    deleteItem ({ commit, getters }, id) {
      let index = getters.collectionIndex.get(id)
      if (index !== void 0) {
        commit('deleteItem', index)
      }
    },
    saveOrUpdateItem ({ commit, getters }, item) {
      let index = getters.collectionIndex.get(item.id)
      if (index !== void 0) {
        commit('updateItem', { index, item })
      } else {
        commit('createItem', item)
      }
    }
  },
  getters: {
    collectionIndex (state) {
      return state.collection.reduce((map, item, index) => {
        map.set(item.id, index)
        return map
      }, new Map())
    },
    itemById (state, getters) {
      return (id) => {
        let index = getters.collectionIndex.get(id)
        return index !== void 0 ? state.collection[index] : void 0
      }
    }
  }
}
console.log(storeModule)
export default storeModule
```

### 5.3 mapState

I don't know if you noticed, but our store now has a pattern, where the state fields and mutations names have the same name and that has a reason.

We need both to have the same name, in order to map them to a computed property, to be exact, the state will be mapped to a `get`, and the mutation to a `set`.

#### 5.3.1 Parameters

The parameters will be very much like the Vuex mapState's parameters.

|order|name|required|default|description|
|---|---|---|---|---|
|1|moduleName|true|   |the module name|
|2|fields|true|   |fields can be an array of strings or a object where the keys and values are strings. e.g: `['text', number, 'list']` or `{ text: 'text', number: 'number', collection: 'list' }`|

#### 5.3.2 Demos

##### 5.3.2.1 Basic Usage

```js
import { store, uuid, timer } from '@toby.mosque/utils'
import { Model } from 'src/store/test'
import { mapActions, mapGetters } from 'vuex'
const { mapState } = store

export default {
  name: 'TestPage',
  computed: {
    ...mapState('test', Object.keys(new Model())),
    ...mapGetters('test', ['itemById'])
  },
  methods: {
    ...mapActions('test', ['saveOrUpdateItem', 'deleteItem']),
    remove (id) {
      let item = this.itemById(id)
      this.$q.dialog({
        parent: this,
        message: `delete ${item.index}:${item.date}?`
      }).onOk(() => {
        this.deleteItem(id)
      })
    }
  },
  async mounted () {
    this.text = 'Hello World'
    this.number = 123
    for (let i = 1; i <= 10; i++) {
      await timer.sleep(1)
      let comb = uuid.comb()
      this.saveOrUpdateItem({ id: comb, index: i, date: uuid.extract(comb).toISOString() })
    }
  }
}
```
```html
<template>
  <q-page class="q-pa-md q-gutter-y-md">
    <q-input type="text" v-model="text" />
    <q-input type="number" v-model.number="number" />
    <q-markup-table>
      <thead>
        <tr>
          <th class="text-left">Id</th>
          <th class="text-left">Index</th>
          <th class="text-left">Date</th>
          <th class="text-left">Actions</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in collection" :key="item.id">
          <td class="text-left">{{item.id}}</td>
          <td class="text-left">{{item.index}}</td>
          <td class="text-left">{{item.date}}</td>
          <td class="text-left">
            <q-btn icon="delete" label="Delete" @click="remove(item.id)" />
          </td>
        </tr>
      </tbody>
    </q-markup-table>
  </q-page>
</template>

<style>
</style>

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

Expected Output

console

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/43p1xsnr8lq4nd288u5x.png)

mounted page

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/fzuicamtqegakbom9jt3.png)

getting record/doc by your id

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/b3rjq2vfgqcvkx6r9j6d.png)

item removed

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/wvdx3a8reegqvsctqla4.png)

## 6 Factories

the primary goal here, is wrapping components or generate the whole store and/or pages.

### 6.1 component

the `factory.component` objective is wrapper a other component and allow the dev to modify them at the render time, or even setup your properties, slots, etc.

#### 6.1.1 Parameters

|order|name|required|default|description|
|---|---|---|---|---|
|1|options|true|   |options used to generate the component|

##### 6.1.1.1 Options

|field|required|description|
|---|---|---|---|---|
|name|true|component's name|
|component|true|component to be wrapped|
|render|false|callback function that is called everytime the component is rendered: `render ({ self, options }) { }`|
|render|false|callback function that is called when the component is ready to be returned: `setup({ component }) { }`|
|factories|false|array of objects with a render and/or setup field|

#### 6.1.2 Demos

##### 6.1.2.1 Basic Usage

We'll inject the reverse property in the QInput and when that property is set as `true` the label will be inverted. Beside that, we'll set the dark and outlined property to true. The QMarkupTable will also have the dark property changed to `true`

```js
// import something here
import { factory } from '@toby.mosque/utils'
import { QInput, QMarkupTable } from 'quasar'
const { component } = factory

// "async" is optional
export default async ({ Vue }) => {
  let input = component({
    name: QInput.options.name,
    component: QInput,
    // eslint-disable-next-line vue/require-render-return
    render ({ self, options }) {
      options.props.dark = true
      options.props.outlined = true
      if (options.props.label && options.props.reverse) {
        options.props.label = options.props.label.split('').reverse().join('')
      }
    },
    setup ({ component }) {
      component.props.reverse = Boolean
    }
  })

  let markupTable = component({
    name: QMarkupTable.options.name,
    component: QMarkupTable,
    // eslint-disable-next-line vue/require-render-return
    render ({ self, options }) {
      options.props.dark = true
    }
  })

  Vue.component('q-input', input)
  Vue.component('q-markup-table', markupTable)
}
```

Expected Output

```html
<q-input type="text" label="Text" reverse v-model="text" />
<q-input type="number" label="Number" v-model.number="number" />
```

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/qenz0ertiikaq4fbjl85.png)

##### 6.1.2.2 More Reusable

Ok, we had only 2 components and we already had a lot of boilerplate. We can improve that:

```js
import { factory } from '@toby.mosque/utils'
import { QInput, QSelect, QField, QMarkupTable, QList, QItem } from 'quasar'
const { component } = factory
const factories = {}

factories.common = {
  render ({ self, options }) {
    options.props.dark = true
  }
}

factories.field = {
  render ({ self, options }) {
    options.props.outlined = true
    if (options.props.label && options.props.reverse) {
      options.props.label = options.props.label.split('').reverse().join('')
    }
  },
  setup ({ component }) {
    component.props.reverse = Boolean
  }
}

const fields = [
  { name: 'q-input', component: QInput },
  { name: 'q-select', component: QSelect },
  { name: 'q-field', component: QField }
]
const layout = [
  { name: 'q-markup-table', component: QMarkupTable },
  { name: 'q-list', component: QList },
  { name: 'q-item', component: QItem }
]

// "async" is optional
export default async ({ Vue }) => {
  for (let item of fields) {
    let branded = component({
      name: item.component.options.name,
      component: item.component,
      factories: [ factories.common, factories.field ]
    })
    Vue.component(item.name, branded)
  }

  for (let item of layout) {
    let branded = component({
      name: item.component.options.name,
      component: item.component,
      factories: [ factories.common ]
    })
    Vue.component(item.name, branded)
  }
}
```

Now, we're rebranding `QInput`, `QSelect`, `QField`, `QMarkupTable`, `QList` and `QItem`

Expected Output

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/eh8gelsgfhhr2ootjgu6.png)

### 6.2 Store

`factory.store` combines `store.mapStoreMutations` and `store.mapStoreCollections`.

#### 6.2.1 Parameters

|order|name|required|default|description|
|---|---|---|---|---|
|1|options|true|   |options used to generate the store|
|2|state|false|   |module's state, that will be merged intro the final module|
|3|mutations|false|   |module's mutations, that will be merged intro the final module|
|4|actions|false|   |module's actions, that will be merged intro the final module|
|5|getters|false|   |module's getters, that will be merged intro the final module|
##### 6.2.1.1 options

|field|required|description|
|---|---|---|---|---|
|model|false|class used to model the state and mutations|
|collections|false|an array of objects that describes your collection (see `store.mapStoreMutations` for more details)|

#### 6.2.2 Demos

##### 6.2.2.1 Basic Usage

```js
import { factory } from '@toby.mosque/utils'
const { store } = factory

const options = {
  model: class Model {
    text = ''
    number = 0
    collection = []
  },
  collections: [
    { single: 'item', plural: 'collection', id: 'id' }
  ]
}

const storeModule = store({
  options
})
console.log(storeModule)
export default storeModule
export { options }
```

Expected Output

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/zdz57c0etwz171u9cf12.png)

### 6.3 Page

`factory.page` will expect the same options as `factory.store` and will map the `state`, `mutations`, `actions` and `getters` generated by `factory.store` to the page.

#### 6.3.1 Parameters

|order|name|required|default|description|
|---|---|---|---|---|
|1|options|true|   |options used to generate the page|
|2|moduleName|true|   |the name of the module|
|3|storeModule|false|   |if not null, it'll be registered in the `preFetch` or in the `mounted` hook, and removed in the `destroyed` hook.|
|4|*|false|   |any other property will be copied to the page as is|

##### 6.3.1.1 options

|field|required|description|
|---|---|---|---|---|
|model|false|class used to modeling the state and mutations|
|collections|false|an array of objects that describes the your collection (see `store.mapStoreMutations` for more details)|

#### 6.3.2 Demos

##### 6.3.2.1 Basic Usage

```js
import { factory, uuid, timer } from '@toby.mosque/utils'
import { options } from 'src/store/test'
const { page } = factory

let testPage = page({
  name: 'TestPage',
  options,
  moduleName: 'test',
  methods: {
    remove (id) {
      let item = this.itemById(id)
      this.$q.dialog({
        parent: this,
        message: `delete ${item.index}:${item.date}?`
      }).onOk(() => {
        this.deleteItem(id)
      })
    }
  },
  async mounted () {
    this.text = 'Hello World'
    this.number = 123
    for (let i = 1; i <= 10; i++) {
      await timer.sleep(1)
      let comb = uuid.comb()
      this.saveOrUpdateItem({ id: comb, index: i, date: uuid.extract(comb).toISOString() })
    }
  }
})

console.log(testPage)
export default testPage
```

Expected Output

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/05gbrn6r30ewtaegige0.png)

### 6.4 Page and Store 

Let's combine `factory.page` with `factory.store` and see what happens.

#### 6.4.1 Demos

##### 6.4.1.1 Basic Usage

Pay particular attention to the initialize action, it receives the route as an argument and is called as soon as the store is registered (perfect place to load data intro the state).

```js
import { factory, uuid, timer } from '@toby.mosque/utils'
const { store, page } = factory

const options = {
  model: class Model {
    text = ''
    number = 0
    collection = []
  },
  collections: [
    { single: 'item', plural: 'collection', id: 'id' }
  ]
}

const storeModule = store({
  options,
  actions: {
    async initialize ({ commit, dispatch }, { route }) {
      commit('text', 'Hello World')
      commit('number', 123)
      this.number = 123
      for (let i = 1; i <= 10; i++) {
        await timer.sleep(1)
        let comb = uuid.comb()
        await dispatch('saveOrUpdateItem', { id: comb, index: i, date: uuid.extract(comb).toISOString() })
      }
    }
  }
})

let testPage = page({
  name: 'TestPage',
  options,
  moduleName: 'test',
  storeModule,
  methods: {
    remove (id) {
      let item = this.itemById(id)
      this.$q.dialog({
        parent: this,
        message: `delete ${item.index}:${item.date}?`
      }).onOk(() => {
        this.deleteItem(id)
      })
    }
  }
})

console.log(testPage, storeModule)
export default testPage

```

Expected Output

console - without initialize action

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/hh7yj9z91sb0fs25zpsn.png)

console - with initialize action

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/3odpwmw8ihrf94itsufp.png)

rendered page

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/j8ci6zlg6hynf6ohtjbu.png)