DEV Community

Tobias Mesquita for Quasar Framework Brasil

Posted on

QPANC - Parte 11 - Quasar - Componentes - Diferença entre SPA e SSR

QPANC são as iniciais de Quasar PostgreSQL ASP NET Core.

22 Componente de Login

Iremos utilizar o componente de Login, para demonstrar as diferenças entre um componente feito para um SPA, e um feito para um SSR, que demanda que os dados sejam hidratados.

E antes de continuamos, a escolha por dividir os componentes SFC (Single File Component) em múltiplos arquivos, é algo de cunho pessoal. você pode ler mais a respeito em Single File Components - What About Separation of Concerns?

.

Caso durante a criação do projeto, tenha optado pela não importação automática dos componentes (quasar.config.js > framework > all > true). Você precisará instalar a seguinte qautomate (como descrito no capitulo 19).

22.1 - SPA

o nosso primeiro passo, será criar os layouts, por hora iremos usar um layout clean para as atividades de autenticação (landspage, login, registro, etc) e outro para as demais paginas.

Lembrando que esta estrutura é apenas um exemplo, por mais que seja aplicável a maioria dos projetos, não encarre ela como sendo uma bala de prata.

o primeiro componente que iremos fazer, é o layout clean.
Quasar.App/src/layout/clean/index.vue

<template>
  <q-layout id="layout-clean" view="lHh Lpr lFf" class="bg-main">
    <q-page-container>
      <q-page class="row">
        <div class="col flex flex-center relative-position layout-auth">
          <img alt="Quasar logo" class="absolute-center" src="~assets/quasar-logo-full.svg">
        </div>
        <div class="col col-auto shadow-up-2 page-container relative-position bg-content">
          <div class="page-form q-pa-xl absolute-center">
            <router-view />
          </div>
        </div>
      </q-page>
    </q-page-container>
  </q-layout>
</template>

<script src="./index.js"></script>
<style src="./index.sass" lang="sass"></style>

Quasar.App/src/layout/clean/index.js

export default {
  name: 'CleanLayout'
}

Quasar.App/src/layout/clean/index.sass

#layout-clean
  .page-container
    width: 540px !important
    .page-form
      width: 100%
  @media (max-width: $breakpoint-sm-max)
    .page-container
      width: 100% !important

note que estamos usando o id do layout no primeiro nível do arquivo index.sass, isto é necessário para garantir que este estilo será aplicado apenas para este componente e os seus respectivos filhos.

Agora, vamos criar a pagina responsável pelo login em QPANC.App/src/pages/login
QPANC.App/src/pages/login/index.vue

<template>
  <div id="page-login">
    <h5 class="q-my-md">{{$t('login.title')}}</h5>
    <q-separator></q-separator>
    <q-form class="row q-col-gutter-sm">
      <div class="col col-12">
        <q-input v-model="userName" :label="$t('fields.userName')" :rules="validation.userName"></q-input>
      </div>
      <div class="col col-12">
        <q-input type="password" v-model="password" :label="$t('fields.password')" :rules="validation.password"></q-input>
      </div>
      <div class="col col-5">
        <q-btn class="full-width" flat color="primary" :label="$t('actions.forget')" @click="forget"></q-btn>
      </div>
      <div class="col col-12">
        <q-btn class="full-width" color="positive" :label="$t('actions.login')" @click="forget"></q-btn>
      </div>
    </q-form>
  </div>
</template>

<script src="./index.js"></script>
<style src="./index.sass" lang="sass"></style>

QPANC.App/src/pages/login/index.js

import validations from 'services/validations'

export default {
  name: 'LoginPage',
  data () {
    const self = this
    const validation = validations(self, {
      userName: ['required', 'email'],
      password: ['required']
    })
    return {
      userName: '',
      password: '',
      validation
    }
  },
  methods: {
    forget () {
      console.log('forget: not implemented yet')
    },
    login () {
      this.validation.resetServer()
      const isValid = await this.$refs.form.validate()
      if (isValid) {
        console.log('login: not implemented yet')
      }
    }
  }
}

QPANC.App/src/pages/login/index.sass

#page-login

No exemplo acima, o arquivo index.sass não possui nenhum estilo, por tanto ele é dispensável, podendo ser excluído.

Agora, precisamos modificar as nossas rotas em QPANC.App/src/routes

QPANC.App/src/routes/areas/clean.js

export default function (context) {
  return {
    path: '',
    component: () => import('layouts/clean/index.vue'),
    children: [
      { name: 'login', path: 'login', component: () => import('pages/login/index.vue') },
      { name: 'register', path: 'register', component: () => import('pages/register/index.vue') }
    ]
  }
}

QPANC.App/src/routes/routes.js

import clean from './areas/clean'

export default function (context) {
  const routes = [{
    path: '/',
    component: {
      render: h => h('router-view')
    },
    children: [
      {
        path: '/',
        beforeEnter (to, from, next) {
          next('/login')
        }
      },
      clean(context)
    ]
  }]

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

  return routes
}

QPANC.App/src/routes/routes.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes'

Vue.use(VueRouter)

export default function (context) {
  context.router = new VueRouter({
    scrollBehavior: () => ({ x: 0, y: 0 }),
    routes: routes(context),
    mode: process.env.VUE_ROUTER_MODE,
    base: process.env.VUE_ROUTER_BASE
  })

  return context.router
}

Originalmente, o routes.js retornava as rotas de forma direta, porém precisamos encapsular esta logica dentro de um função, para que possamos passar o contexto. Através do contexto, nós teremos acesso ao objeto router, store e ssrContext, o que nós será bastante útil durante a construção dos navigation guards.

Uma nota quanto ao component: { render: h => h('router-view') }, esta é a forma que temos para definir uma rota no vue-router que será renderizada, porém sem especificar um componente, esta técnica é especialmente útil, quando a intenção é unicamente agrupar as rotas.

Outro aspecto, é que não possuirmos uma rota na raiz da aplicação, por isto é necessário declarar uma rota com beforeEnter que redireciona a aplicação para o /login. Por hora poderíamos utilizar o redirect ou alias, mas futuramente este redirecionamento será condicionado ao fato do usuário está logado ou não. Lembrando que esta roda não será renderizada, por tanto, não é necessário utilizar o component: { render: h => h('router-view') }.

Agora, podemos acessar a aplicação:

Alt Text

22.2 - SSR

Tecnicamente, o Login do jeito que está, já está apto para uma aplicação SSR, uma vez que, ele não faz nenhuma requisição assincronia durante a sua montagem/criação.

Porém, caso o fizesse, seria necessário criar um modulo no vuex dedicado para esta pagina, e mover as propriedades reativas que estão no data para o state deste modulo.

E por fim, fazer o carregamento dos dados no state usando uma action, que seria chamada no preFetch.

Mas mesmo sem precisar, iremos converter a pagina para usar uma store, até para que todas as paginas do sistema venham a ter a mesma estrutura. O primeiro passo, será ativar o recurso do preFetch no quasar.config.js > preFetch:

QPANC.App/quasar.config.js

module.exports = function (ctx) {
  return {
    preFetch: true
  }
}

O segundo passo, é criar uma store dentro da pasta da pagina, ou seja QPANC.App/src/pages/login

QPANC.App/src/pages/login/store.js

export default {
  namespaced: true,
  state () {
    return {
      userName: '',
      password: ''
    }
  },
  mutations: {
    userName (state, value) { state.userName = value },
    password (state, value) { state.password = value }
  },
  actions: {
    async initialize ({ state }, { route, next }) {
    },
    forget ({ state }) {
      console.log('forget: not implemented yet')
    },
    login ({ state }) {
      console.log('login: not implemented yet')
    }
  }
}

Então, precisamos registrar este modulo no index.js, assim como chamar a action initialize no preFetch.

QPANC.App/src/pages/login/index.js

import validations from 'services/validations'
import pageModule from './store'

const moduleName = 'page-login'
export default {
  name: 'LoginPage',
  preFetch ({ store, currentRoute, redirect }) {
    store.registerModule(moduleName, pageModule)
    return store.dispatch(`${moduleName}/initialize`, { route: currentRoute, next: redirect })
  },
  created () {
    if (process.env.CLIENT) {
      this.$store.registerModule(moduleName, pageModule, { preserveState: true })
    }
  },
  destroyed () {
    this.$store.unregisterModule(moduleName)
  },
  data () {
    const self = this
    const validation = validations(self, {
      userName: ['required', 'email'],
      password: ['required']
    })
    return {
      validation
    }
  },
  computed: {
    userName: {
      get () { return this.$store.state[moduleName].userName },
      set (value) { this.$store.commit(`${moduleName}/userName`, value) }
    },
    password: {
      get () { return this.$store.state[moduleName].password },
      set (value) { this.$store.commit(`${moduleName}/password`, value) }
    }
  },
  methods: {
    forget () {
      this.$store.dispatch(`${moduleName}/forget`)
    },
    login () {
      this.$store.dispatch(`${moduleName}/login`)
    }
  }
}

Os hooks preFetch, created e o destroyed são usados para gerenciar o ciclo de vida do modulo do vuex, para que ele tenha um ciclo de vida semelhante ao da pagina.

A action initialize está sendo chamada do preFetch, desta forma a pagina será renderizada apenas quando o preFetch for concluído.

No computed, estamos mapeando o state e os mutations, para que seja possível utilizar o two-way bind (v-model) com o state do vuex.

O methods está servindo apenas de proxy para as actions do modulo dedicado a esta pagina.

E por fim, no data temos apenas campos de controle, utilitários, semi-estáticos, etc. Como por exemplo, o rules ou as definições das colunas de uma tabela.

Caso não deseje usar a extensão sugerida no próximo tópico, recomendo que dê uma olhada no plugin vuex-map-fields.

22.3 - SSR usando @toby-mosque/utils

O primeiro passo, é instalar a extensão '@toby.mosque/utils'

quasar ext add '@toby.mosque/utils'

altere o jsconfig.json > compilerOptions > paths para incluir o seguinte item:

QPANC.App/jsconfig.json

{
  "compilerOptions": {
    "paths": {
      "@toby.mosque/utils": [
        "node_modules/@toby.mosque/quasar-app-extension-utils/src/utils.js"
      ]
    }
  }
}

Então, altere o arquivo store.js
QPANC.App/src/pages/login/store.js

import { factory } from '@toby.mosque/utils'

class LoginPageModel {
  constructor ({
    userName = '',
    password = ''
  } = {}) {
    this.userName = userName
    this.password = password
  }
}

const options = {
  model: LoginPageModel
}

export default factory.store({
  options,
  actions: {
    async initialize ({ state }, { route, next }) {
    },
    forget ({ state }) {
      console.log('forget: not implemented yet')
    },
    login ({ state }) {
      console.log('login: not implemented yet')
    }
  }
})

export { options, LoginPageModel }

Note que as propriedades do state e os respectivos mutations foram movidos para a classe LoginPageModel. A factory factory.store irá criar o state e o mutations usando a classe LoginPageModel como referencia.

Ao usar a factory.store, a action initialize torna-se um requisito, deve ser declarada, mesmo que não faça muito (ou nada).

Note que, apesar do factory.store montar alguns states, mutations, getters e actions, você poderá declarar os seus próprios states, mutations, getters e actions, pois a store gerada pelo factory.store, será o resultado da mesclagem entre ambos.

Agora, modifique o script index.js
QPANC.App/src/pages/login/index.js

import validations from 'services/validations'
import { factory } from '@toby.mosque/utils'
import store, { options } from './store'

const moduleName = 'page-login'
export default factory.page({
  name: 'LoginPage',
  options,
  moduleName,
  storeModule: store,
  data () {
    const self = this
    const validation = validations(self, {
      userName: ['required', 'email'],
      password: ['required']
    })
    return {
      validation
    }
  },
  methods: {
    forget () {
      this.$store.dispatch(`${moduleName}/forget`)
    },
    login () {
      this.validation.resetServer()
      const isValid = await this.$refs.form.validate()
      if (isValid) {
        this.$store.dispatch(`${moduleName}/login`)
      }
    }
  }
})

Note, que não é mais necessário gerenciar o ciclo de vida do modulo (preFetch, created e destroyed), assim como invocar a action initialize, pois o factory.page irá faze-lo por você.

Outro ponto que já não é necessário, é o mapeamento do state e dos mutations no computed, pois ele também é feito pelo factory.page.

Note que, apesar da factory.page gerá os hooks preFetch, created, destroyed, computed e methods, você poderá definir os seus próprios preFetch, created, destroyed, computed e methods, pois a page gerada pelo factory.page, será o resultado da mesclagem entre ambos

Apenas um detalhe, o options, além do campo model, possui os campos collections e complexTypes. Eles são utilizados para criar/gerenciar um state do tipo Array (collections) ou Object (complexTypes), neste caso, será gerado getters e actions para acessar e manipular estes dados.

Para mais detalhes, leia: Quasar - Utility Belt App Extension to speedup the development of SSR and offline first apps.

22.4 - Considerações

A partir deste monto, estarei utilizando o @toby.mosque/utils para a construção de layouts e pages.

Um outro ponto importante a se citar, é que todos os componentes de layout e page devem ser utilizados no routes.js, assim como, o routes.js só deve declarar componentes provenientes da pasta layouts ou pages.

Isto se faz necessário, pois o hook preFetch existe apenas para os componentes que compõem a rota, pois o preFetch é construído sobre um navigation guard, desta forma, não podemos usar a factory.page para os demais componentes, ou seja, aqueles que iremos criar na pasta components e serão consumidos pelos layouts e pages

23 Exemplos avançados com a extensão @toby-mosque/utils

Caso tenha decidido em não usar a @toby-mosque/utils, você pode até ignorar este capitulo.

Porém estarei exibindo a store e a page que são geradas pela factory.store e pela factory.page, então você poderá estudar a estrutura delas, para aplicar este conhecimento nas suas próprias stores e pages

23.1 - Coleções

O primeiro exemplo, é sobre a geração de uma store que possua um Array, neste caso, iremos configurar o options collections.

models/item.js

export default class ItemModel {
  constructor ({
    id = 0,
    fieldA = '',
    fieldB = ''
  } = {}) {
    this.id = id,
    this.fieldA = fieldA
    this.fieldB = fieldB
  }
}

sample/store.js

import { factory } from '@toby.mosque/utils'
import ItemModel from 'models/item'

class SampleModel {
  constructor ({
    collectionA = [],
    collectionB = []
  } = {}) {
    this.collectionA = collectionA
    this.collectionB = collectionB
  }
}

const options = {
  model: SampleModel,
  collections: [
    { single: 'itemA', plural: 'collectionA', id: 'id', type: ItemModel },
    { single: 'itemB', plural: 'collectionB', id: 'id', type: ItemModel }
  ]
}

export default factory.store({
  options,
  actions: {
    async initialize ({ state }, { route, next }) {
    }
  }
})

export { options, LoginPageModel }

então, a partir do options, a factory.store será capaz de criar a seguinte store.:

sample/store_generated.js

import Vue from 'vue'

export default {
  namespaced: true,
  state () {
    return {
      collectionA = [],
      collectionB = []
    }
  },
  mutations: {
    collectionA (state, value) { Vue.set(state, 'collectionA', value) },
    collectionB (state, value) { Vue.set(state, 'collectionB', value) },
    createItemA (state, item) { state.collectionA.push(item) },
    updateItemA (state, { index, item }) { Vue.set(state.collectionA, index, item) },
    deleteItemA (state, index) { Vue.delete(state.collectionA, index) },
    createItemB (state, item) { state.collectionB.push(item) },
    updateItemB (state, { index, item }) { Vue.set(state.collectionB, index, item) },
    deleteItemB (state, index) { Vue.delete(state.collectionB, index) }
    setFieldAOfAnItemA  (state, { index, value }) { Vue.set(state.collectionA[index], 'fieldA', item) }
    setFieldBOfAnItemA  (state, { index, value }) { Vue.set(state.collectionA[index], 'fieldB', item) }
    setFieldAOfAnItemB  (state, { index, value }) { Vue.set(state.collectionB[index], 'fieldA', item) }
    setFieldBOfAnItemB  (state, { index, value }) { Vue.set(state.collectionB[index], 'fieldB', item) }
  },
  actions: {
    async initialize ({ state }, { route, next }) {
    },
    saveOrUpdateItemA ({ commit, getters }, item) {
      const index = getters.collectionAIndex.get(item.id)
      if (index !== undefined) {
        commit('updateItemA', { index, item })
      } else {
        commit('createItemA', item)
      }
    },
    deleteItemA ({ commit, getters }, id) {
      const index = getters.collectionAIndex.get(id)
      if (index !== undefined) {
        commit('deleteItemA', index)
      }
    },
    saveOrUpdateItemB ({ commit, getters }, item) {
      const index = getters.collectionBIndex.get(item.id)
      if (index !== undefined) {
        commit('updateItemB', { index, item })
      } else {
        commit('createItemB', item)
      }
    },
    deleteItemB ({ commit, getters }, id) {
      const index = getters.collectionBIndex.get(id)
      if (index !== undefined) {
        commit('deleteItemB', index)
      }
    },
    setFieldAOfAnItemA ({ commit, getters }, { id, value }) {
      const index = getters.collectionAIndex.get(id)
      if (index !== undefined) {
        commit('setFieldAOfAnItemA', { index, value })
      }
    },
    setFieldBOfAnItemA ({ commit, getters }, { id, value }) {
      const index = getters.collectionAIndex.get(id)
      if (index !== undefined) {
        commit('setFieldBOfAnItemA', { index, value })
      }
    },
    setFieldAOfAnItemB ({ commit, getters }, { id, value }) {
      const index = getters.collectionBIndex.get(id)
      if (index !== undefined) {
        commit('setFieldAOfAnItemB', { index, value })
      }
    },
    setFieldBOfAnItemB ({ commit, getters }, { id, value }) {
      const index = getters.collectionBIndex.get(id)
      if (index !== undefined) {
        commit('setFieldBOfAnItemB', { index, value })
      }
    }
  },
  getters: {
    collectionAIndex (state) {
      return state.collectionA.reduce((map, item, index) => {
        map.set(item.id, index)
        return map
      }, new Map())
    },
    itemAById (state, getters) {
      return function itemAById(id) {
        const index = getters.collectionAIndex.get(id)
        if (index !== undefined) {
          return state.collectionA[index]
        }
      }
    },
    collectionBIndex (state) {
      return state.collectionB.reduce((map, item, index) => {
        map.set(item.id, index)
        return map
      }, new Map())
    },
    itemBById (state, getters) {
      return function itemBById(id) {
        const index = getters.collectionBIndex.get(id)
        if (index !== undefined) {
          return state.collectionB[index]
        }
      }
    }
  }
}

Note que, o prefiro das actions pode ser configurado, o default é saveOrUpdate e delete, mas por exemplo, você pode alterar para upsert e remove.

Agora vamos a page.:

sample/index.js

import ItemASection from 'components/item-a-section/index.vue'
import ItemBSection from 'components/item-b-section/index.vue'
import { factory } from '@toby.mosque/utils'
import store, { options } from './store'

const moduleName = 'page-sample'
export default factory.page({
  name: 'SamplePage',
  options,
  moduleName,
  storeModule: store,
  data () {
    return {
      moduleName
    }
  },
  components: {
    'item-a-section': ItemASection,
    'item-b-section': ItemBSection
  }
})

A page resultante será a seguinte.:
sample/index_generated.js

import ItemASection from 'components/item-a-section/index.vue'
import ItemBSection from 'components/item-b-section/index.vue'
import store from './store'

const moduleName = 'page-sample'
export default {
  name: 'SamplePage',
  preFetch ({ store, currentRoute, redirect }) {
    store.registerModule(moduleName, pageModule)
    return store.dispatch(`${moduleName}/initialize`, { route: currentRoute, next: redirect })
  },
  created () {
    if (process.env.CLIENT) {
      this.$store.registerModule(moduleName, pageModule, { preserveState: true })
    }
  },
  destroyed () {
    this.$store.unregisterModule(moduleName)
  },
  data () {
    return {
      moduleName
    }
  },
  components: {
    'item-a-section': ItemASection,
    'item-b-section': ItemBSection
  },
  computed: {
    collectionA: {
      get () { return this.$store.state[moduleName].collectionA},
      set (value) { this.$store.commit(`${moduleName}/collectionA`, value) }
    },
    collectionB: {
      get () { return this.$store.state[moduleName].collectionB },
      set (value) { this.$store.commit(`${moduleName}/collectionB`, value) }
    },
    itemAById () {
      return this.$store.getters[`${moduleName}/itemAById`]
    },
    itemBById () {
      return this.$store.getters[`${moduleName}/itemBById`]
    },
    collectionAIndex () {
      return this.$store.getters[`${moduleName}/collectionAIndex`]
    },
    collectionBIndex () {
      return this.$store.getters[`${moduleName}/collectionBIndex`]
    },
  },
  methods: {
    saveOrUpdateItemA (item) {
      return this.$store.dispatch(`${moduleName}/saveOrUpdateItemA`, item)
    },
    deleteItemA (id) {
      return this.$store.dispatch(`${moduleName}/deleteItemA`, id)
    },
    saveOrUpdateItemB (item) {
      return this.$store.dispatch(`${moduleName}/saveOrUpdateItemB`, item)
    },
    deleteItemB (id) {
      return this.$store.dispatch(`${moduleName}/deleteItemB`, id)
    }
  }
}

E para ilustrar, um template.:

sample/index.vue

<template>
  <div>
    <q-card v-for="itemA in collectionA" :key="itemA.id">
      <item-a-section :module="moduleName" :id="itemA.id"></item-a-section>
      <q-card-actions>
        <q-btn icon="delete" @click="deleteItemA(itemA.id)" />
      </q-card-actions>
    </q-card>
    <q-card v-for="itemB in collectionB" :key="itemB.id">
      <item-b-section :module="moduleName" :id="itemB.id"></item-b-section>
      <q-card-actions>
        <q-btn icon="delete" @click="deleteItemA(itemB.id)" />
      </q-card-actions>
    </q-card>
  </div>
</template>

Antes que me pergunte, as actions/mutations com formato semelhante à setFieldBOfAnItemA foram feitas para serem utilizadas em componentes, no exemplo acima, o item-a-section e item-b-section.

Aqui a implementação do item-a-section:

components/item-a-section/index.js

import { store } from '@toby.mosque/utils'
import ItemModel from 'models/item'

const module = store.mapCollectionItemState('', { id: 'id', single: 'itemA', type: ItemModel })

export default {
  name: 'ItemAComponent',
  props: {
    uid: String,
    module: String
  },
  created () {
    module.setModuleName(this.module)
  },
  computed: {
    ...module.computed
  }
}

e por fim o template para este componente:

components/item-a-section/index.vue

<q-card-section>
  <div class="row q-col-gutter-sm">
    <q-input class="col col-12" v-model="fieldA" label="Field A" />
    <q-input class="col col-12" v-model="fieldB" label="Field B" />
  </div>
</q-card-section>

E para ilustrar, o mesmo componente, mas sem o uso do mapCollectionItemState.

components/item-a-section/index_generated.js

export default {
  name: 'ItemAComponent',
  props: {
    uid: String,
    module: String
  },
  created () {
    module.setModuleName(this.module)
  },
  computed: {
    itemAById () {
      return this.$store.getters[`${this.module}/itemAById`]
    },
    itemA () {
      return this.itemAById(this.id)
    },
    fieldA: {
      get () { return this.itemA.fieldA },
      set (value) { 
        this.$store.dispatch(`${this.module}/setFieldAOfAnItemA`, {
          id: this.id,
          value
        })
      }
    },
    fieldA: {
      get () { return this.itemA.fieldB },
      set (value) { 
        this.$store.dispatch(`${this.module}/setFieldBOfAnItemA`, {
          id: this.id,
          value
        })
      }
    }
  }
}

23.2 Tipos Complexos

Ao utilizar o @toby-mosque/utils, o ideal é utilizar objetos com apenas tipos concretos (String, Number, Date) e Array, porém as vezes precisamos utilizar tipos complexos.

sample/store.js

import { factory } from '@toby.mosque/utils'
import ItemModel from 'models/item'

class SampleModel {
  constructor ({
    itemA = new ItemModel(),
    itemB = new ItemModel()
  } = {}) {
    this.itemA = itemA
    this.itemB = itemB
  }
}

const options = {
  model: SampleModel,
  complexTypes: [
    { name: 'itemA', type: ItemModel },
    { name: 'itemB', type: ItemModel }
  ]
}

export default factory.store({
  options,
  actions: {
    async initialize ({ state }, { route, next }) {
    }
  }
})

export { options, LoginPageModel }

esta seria a store gerada pela factory.store:
sample/store.js

export default {
  options,
  state () {
    return {
      itemA: {
        id: 0,
        fieldA: '',
        fieldB: ''
      },
      itemB: {
        id: 0,
        fieldA: '',
        fieldB: ''
      }
    }
  },
  mutations: {
    itemA (state, value) { Vue.set(state, 'itemA', value) },
    itemB (state, value) { Vue.set(state, 'itemB', value) },
    setIdOfItemA (state, value) { state.itemA.id = value },
    setFieldAOfItemA (state, value) { state.itemA.fieldA = value },
    setFieldBOfItemA (state, value) { state.itemA.fieldB = value },
    setIdOfItemB (state, value) { state.itemB.id = value },
    setFieldAOfItemB (state, value) { state.itemB.fieldA = value },
    setFieldBOfItemB (state, value) { state.itemB.fieldB = value }
  },
  actions: {
    async initialize ({ state }, { route, next }) {
    }
  }
}

export { options, LoginPageModel }

Agora, vejamos a page:

sample/index.js

import { factory } from '@toby.mosque/utils'
import store, { options } from './store'

const moduleName = 'page-sample'
export default factory.page({
  name: 'SamplePage',
  options,
  moduleName,
  storeModule: store
})

E um exemplo de template:
sample/index.vue

<template>
  <div>
    <q-card>
      <q-card-section>
        Item A
      </q-card-section>
      <q-separator />
      <q-card-section>
        <div class="row q-col-gutter-sm">
          <q-input class="col col-12" v-model="fieldAOfItemA" label="Field A" />
          <q-input class="col col-12" v-model="fieldBOfItemA" label="Field B" />
        </div>
      </q-card-section>
    </q-card>
    <q-card>
      <q-card-section>
        Item B
      </q-card-section>
      <q-separator />
      <q-card-section>
        <div class="row q-col-gutter-sm">
          <q-input class="col col-12" v-model="fieldAOfItemB" label="Field A" />
          <q-input class="col col-12" v-model="fieldBOfItemB" label="Field B" />
        </div>
      </q-card-section>
    </q-card>
  </div>
</template>

e para fins de comparação, a mesma page, forem sem uso da factory.page

sample/index_generated.js

const moduleName = 'page-sample'
export default {
  name: 'SamplePage',
  preFetch ({ store, currentRoute, redirect }) {
    store.registerModule(moduleName, pageModule)
    return store.dispatch(`${moduleName}/initialize`, { route: currentRoute, next: redirect })
  },
  created () {
    if (process.env.CLIENT) {
      this.$store.registerModule(moduleName, pageModule, { preserveState: true })
    }
  },
  destroyed () {
    this.$store.unregisterModule(moduleName)
  },
  computed () {
    itemA: {
      get () { return this.$store.state[moduleName].itemA },
      set (value) { this.$store.commit(`${moduleName}/itemA`, value) }
    },
    itemB: {
      get () { return this.$store.state[moduleName].itemA },
      set (value) { this.$store.commit(`${moduleName}/itemA`, value) }
    },
    fieldAOfItemA: {
      get () { return this.itemA.fieldA },
      set (value) { this.$store.commit(`${moduleName}/setFieldAOfItemA`, value) }
    },
    fieldBOfItemA: {
      get () { return this.itemA.fieldB },
      set (value) { this.$store.commit(`${moduleName}/setFieldBOfItemA`, value) }
    },
    fieldAOfItemB: {
      get () { return this.itemB.fieldA },
      set (value) { this.$store.commit(`${moduleName}/setFieldAOfItemB`, value) }
    },
    fieldBOfItemB: {
      get () { return this.itemB.fieldB },
      set (value) { this.$store.commit(`${moduleName}/setFieldBOfItemB`, value) }
    }
  }
})

Quanto a geração e manutenção de stores e pages usando o @toby-mosque/utils, não há muito mais a ser dito.

Discussion (0)