DEV Community

loading...
Cover image for Quasar - Store Model Bind Pattern - Part I

Quasar - Store Model Bind Pattern - Part I

Tobias Mesquita
・4 min read

1. The problem

A common problem when you're starting with the Quasar and your SSR mode, is that you are forced to use preFetch and dedicated store modules for each Page. So even single SFC like that, can become somewhat verbose and tedious.

src/pages/Person/Index.vue.*

<template>
  <div class="row q-col-gutter-sm">
    <q-input class="col col-6" label="Name" v-model="name" />
    <q-input class="col col-6" label="Surname" v-model="surname" />
  </div>
</template>
<script src="./Index.vue.js"></script>
export default {
  data () {
    return {
      name: '',
      surname: ''
    }
  },
  mounted () {
    let { id } = this.$route.params
    // query the person somewhere
  }
}

can become something verbose:

src/store/person.js

export default {
  namespaced: true,
  state () {
    return {
      name: '',
      surname: ''
    }
  },
  mutations: {
    name (state, value) { state.name = value },
    surname (state, value) { state.surname = value }
  },
  actions: {
    initialize ({ context }) {
      // query the person somewhere
    }
  }
}

src/pages/Person/Index.vue.*

<template>
  <div class="row q-col-gutter-sm">
    <q-input class="col col-6" label="Name" v-model="name" />
    <q-input class="col col-6" label="Surname" v-model="surname" />
  </div>
</template>
<script src="./Index.vue.js"></script>
import Module from 'src/store/person'
import { mapActions } from 'vuex'
const moduleName = 'person'
export default {
  preFetch ({ store, currentRoute }) {
    store.registerModule(moduleName, Module)
    return store.dispatch(`${moduleName}/initialize`, currentRoute.params.id)
  },
  mounted () {
    if (!this.$store.state[moduleName]) {
      this.$store.registerModule(moduleName, Module, { preserveState: true })
      this.$store.dispatch(`${moduleName}/initialize`, this.$route.params.id)
    }
  },
  destroyed () {
    this.$store.unregisterModule(moduleName)
  },
  computed: {
    name: {
      get () { return this.$store.state[moduleName].name },
      set (value) { this.$store.commit(`${moduleName}/name`, value) }
    },
    surname: {
      get () { return this.$store.state[moduleName].name },
      set (value) { this.$store.commit(`${moduleName}/name`, value) }
    }
  }
}

So, if you want to manage your fields (rename, create or remove), instead of edit your data hook, you'll need to edit the state, the mutation name, the mutation itself, the computed (hook) names, the computed gets and the computed sets.

2. Utilities belt

We'll need to create some utility methods, to map the states, mutations and computed properties.

src/utils/mapper.js

import Vue from 'vue'

export function createMutations (Model) {
  const keys = Object.keys(new Model())
  const mutations = keys.reduce((mutations, key) => {
    mutations[key] = function (state, value) {
      Vue.set(state, key, value)
    }
    return mutations
  }, {})
  return mutations
}

export const mapState = function (module, properties) {
  var props = {}
  if (Array.isArray(properties)) {
    properties.forEach(property => {
      props[property] = {
        get () {
          return this.$store.state[module][property]
        },
        set (value) {
          this.$store.commit(`${module}/${property}`, value)
        }
      }
    })
  } else {
    Object.keys(properties).forEach(key => {
      var property = properties[key]
      props[key] = {
        get () { return this.$store.state[module][property] },
        set (value) { this.$store.commit(`${module}/${property}`, value) }
      }
    })
  }
  return props
}

export const mapGetters = function (module, properties) {
  var props = {}
  if (Array.isArray(properties)) {
    properties.forEach(property => {
      props[property] = {
        get () {
          return this.$store.getters[`${module}/${property}`]
        },
        set (value) {
          this.$store.commit(`${module}/${property}`, value)
        }
      }
    })
  } else {
    Object.keys(properties).forEach(key => {
      var property = properties[key]
      props[key] = {
        get () { return this.$store.getters[`${module}/${property}`] },
        set (value) { this.$store.commit(`${module}/${property}`, value) }
      }
    })
  }
  return props
}

The createMutations will map the fields of an Object to an Object structured like the store mutations.

The mapState has a signature similar to the original vuex's mapState, but that also will map both state and mutation to a computed property.

The mapGetters has a signature similar to the original vuex's mapGetters, but that also will map both getter and mutation to a computed property.

3. Proprosed Solution - Store Model Bind Pattern

Now, instead of defining our data structure directly in the store's state, we'll create a Class Model that will hold.

src/models/person.js

export default class Person {
  name = ''
  surname = ''
}

Now, let's update our store.:

src/store/person.js

import Model from 'src/store/person'
export default {
  namespaced: true,
  state () {
    return new Model()
  },
  mutations: {
    ...createMutations(Model)
  },
  actions: {
    initialize ({ context }) {
      // query the person somewhere
    }
  }
}

If you give a further look on the above store, you'll notice that is pretty generic, so we can now use that to scaffolding the stores that we'll create in the future.

Now, we need to update the page itself:

src/pages/Person/Index.vue.js

import Module from 'src/store/person'
import Model from 'src/models/person'
import { mapState } from 'src/utils/mapper'

const moduleName = 'person'
const keys = Object.keys(new Model())

export default {
  preFetch ({ store, currentRoute }) {
    store.registerModule(moduleName, Module)
    return store.dispatch(`${moduleName}/initialize`, currentRoute.params.id)
  },
  mounted () {
    if (!this.$store.state[moduleName]) {
      this.$store.registerModule(moduleName, Module, { preserveState: true })
      this.$store.dispatch(`${moduleName}/initialize`, this.$route.params.id)
    }
  },
  destroyed () {
    this.$store.unregisterModule(moduleName)
  },
  computed: {
    ...mapState(moduleName, keys)
  }
}

The Page still looks pretty verbose when compared to the previous version, but like the store, that Page is very generic, so we can use that for scaffolding the others Pages.

Now, if we need to edit our data structure, we won't need to do multiple edits in 2 files. We just need to edit the Class Model.

In the next article, we'll talk about a very special case, collections, a.k.a arrays.

Discussion (0)