QPANC - Parte 11 - Quasar - Componentes - Diferença entre SPA e SSR
Tags: quasar, postgres, aspnetcore, docker
Published: 2020-04-19 23:38:05

> **QPANC** são as iniciais de **Q**uasar **P**ostgreSQL **A**SP **N**ET **C**ore.

  * [Source](https://github.com/TobyMosque/qpanc)
  * [Introdução](https://dev.to/quasar-pt-br/arquiteturando-uma-aplicacao-com-quasar-postgresql-asp-net-core-e-docker-introducao-o7o)
  * [Parte I - ASP.NET - Inicializando os Projetos](https://dev.to/quasar-pt-br/qpanc-parte-1-asp-net-inicializando-os-projetos-lkm)
  * [Parte 2 - PostgreSQL](https://dev.to/quasar-pt-br/qpanc-parte-2-postgresql-51pb)
  * [Parte 3 - ASP.NET - Registrando Serviços e Lendo Variáveis de Ambiente](https://dev.to/quasar-pt-br/qpanc-parte-3-asp-net-registrando-servicos-e-lendo-variaveis-de-ambiente-3do2)
  * [Parte 4 - ASP.NET - Entity Framework e ASP.NET Core Identity](https://dev.to/quasar-pt-br/qpanc-parte-4-asp-net-entity-framework-e-asp-net-core-identity-40f9)
  * [Parte 5 - ASP.NET - Documentação Interativa com Swagger](https://dev.to/quasar-pt-br/qpanc-parte-5-asp-net-documentacao-interativa-com-swagger-4n0o)
  * [Parte 6 - ASP.NET - Regionalização](https://dev.to/quasar-pt-br/qpanc-parte-6-asp-net-regionalizacao-13p5)
  * [Parte 7 - ASP.NET - Autenticação e Autorização](https://dev.to/quasar-pt-br/qpanc-parte-7-asp-net-autenticacao-e-autorizacao-bpd)
  * [Parte 8 - ASP.NET - CORS](https://dev.to/quasar-pt-br/qpanc-parte-8-asp-net-cors-52dn)
  * [Parte 9 - Quasar - Criação e Configuração do Projeto](https://dev.to/quasar-pt-br/qpanc-parte-9-quasar-criacao-e-configuracao-do-projeto-5hbi)
  * [Parte 10 - Quasar - Configurações e Customizações](https://dev.to/quasar-pt-br/qpanc-parte-10-quasar-configuracoes-e-customizacoes-2i86)
  * [Parte 11 - Quasar - Componentes - Diferença entre SPA e SSR](https://dev.to/quasar-pt-br/qpanc-parte-11-quasar-componentes-diferenca-entre-spa-e-ssr-1a3c)
  * [Parte 12 - Quasar - Serviços](https://dev.to/quasar-pt-br/qpanc-parte-12-quasar-servicos-4e61)
  * [Parte 13 - Quasar - Regionalização e Stores](https://dev.to/quasar-pt-br/qpanc-parte-13-quasar-regionalizacao-e-stores-1kep)
  * [Parte 14 - Quasar - Consumindo a API](https://dev.to/quasar-pt-br/qpanc-parte-14-quasar-consumindo-a-api-fn6)
  * [Parte 15 - Quasar - Login](https://dev.to/quasar-pt-br/qpanc-parte-15-quasar-login-jc6)
  * [Parte 16 - Quasar - Áreas Protegidas](https://dev.to/quasar-pt-br/qpanc-parte-16-quasar-areas-protegidas-3jkn)
  * [Parte 17 - Quasar - Registro](https://dev.to/quasar-pt-br/qpanc-parte-17-quasar-registro-3b8c)
  * [Parte 18 - Docker - Maquina Virtual Linux](https://dev.to/quasar-pt-br/qpanc-parte-18-docker-maquina-virtual-linux-g5c)
  * [Parte 19 - Docker - Registro e Build](https://dev.to/quasar-pt-br/qpanc-parte-19-docker-registro-e-build-31i4)
  * [Parte 20 - Docker - Traefik e Publicação](https://dev.to/quasar-pt-br/qpanc-parte-20-docker-traefik-e-publicacao-3g3d)
  * [Demo Online](https://qpanc.tobiasmesquita.dev)

##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?](https://vuejs.org/v2/guide/single-file-components.html#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**
```html
<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**
```js
export default {
  name: 'CleanLayout'
}
```
**Quasar.App/src/layout/clean/index.sass**
```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**
```html
<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**
```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**
```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**
```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**
```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**
```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](https://dev-to-uploads.s3.amazonaws.com/i/h6zy2swl6bh4vly1zgdz.png)

###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**
```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**
```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**
```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`](https://github.com/maoberlehner/vuex-map-fields).

###22.3 - SSR usando `@toby-mosque/utils`

O primeiro passo, é instalar a extensão ['@toby.mosque/utils'](https://www.npmjs.com/package/@toby.mosque/quasar-app-extension-utils)

```
quasar ext add '@toby.mosque/utils'
```

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

**QPANC.App/jsconfig.json**
```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**
```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**
```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.](https://dev.to/tobymosque/quasar-utility-belt-app-extension-to-speedup-the-development-of-ssr-and-offline-first-apps-175h)

###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**
```js
export default class ItemModel {
  constructor ({
    id = 0,
    fieldA = '',
    fieldB = ''
  } = {}) {
    this.id = id,
    this.fieldA = fieldA
    this.fieldB = fieldB
  }
}
```

**sample/store.js**
```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**
```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**
```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**
```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**
```html
<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**
```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**
```html
<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**
```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**
```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**
```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**
```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**
```html
<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**
```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.