DEV Community

Tobias Mesquita for Quasar Framework Brasil

Posted on

1

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.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

👋 Kindness is contagious

If this article connected with you, consider tapping ❤️ or leaving a brief comment to share your thoughts!

Okay