Aplicaciones de una sola página con Vue.js y Flask: administración de estado con Vuex

Gracias por acompañarme en la tercera publicación sobre el uso de Vue.js y Flask para el desarrollo web completo. El tema principal de esta publicación será sobre el uso de vuex para administrar...

Gestión de estado con Vuex

Gracias por acompañarme en la tercera publicación sobre el uso de Vue.js y Flask para el desarrollo web completo. El tema principal de esta publicación será el uso de vuex para administrar el estado en nuestra aplicación. Para presentar vuex, demostraré cómo refactorizar los componentes Inicio y Encuesta de la publicación anterior para utilizar vuex, y también desarrollaré la capacidad de agregar nuevas encuestas utilizando el patrón vuex.

El código de esta publicación está en un repositorio en mi cuenta GitHub en la rama ThirdPost.

Contenido de la serie

  1. Configuración y familiarización con VueJS
  2. Navegación por el enrutador Vue
  3. Gestión de estados con Vuex (usted está aquí)
  4. API RESTful con Flask
  5. Integración AJAX con API REST
  6. Autenticación JWT
  7. Implementación en un servidor privado virtual

Presentamos Vuex

Vuex es una biblioteca de administración de estado centralizada respaldada oficialmente por el equipo de desarrollo principal de Vue.js. Vuex proporciona un similar a un flujo, flujo de datos unidireccional, patrón que ha demostrado ser muy poderoso para soportar de moderado a grande Aplicaciones Vue.js.

Hay otras implementaciones de bibliotecas y patrones de administración de estados similares a flujos, pero vuex ha sido diseñado para funcionar específicamente con el sistema de reactividad simple y rápido de Vue.js. Esto se logra a través de una API bien diseñada que proporciona una única fuente de verdad para los datos de una aplicación como un objeto único. Además del principio de fuente única de la verdad, vuex también proporciona métodos explícitos y rastreables para operaciones asincrónicas (acciones), accesores reutilizables convenientes (captadores) y capacidades de alteración de datos (mutaciones).

Para usar vuex, primero tendré que instalarlo en el mismo directorio que contiene el archivo package.json de la siguiente manera:

1
$ npm install --save vuex

A continuación, agrego un nuevo directorio dentro del directorio src/ del proyecto llamado "store" y agrego un archivo index.js. Esto da como resultado la estructura del proyecto Survey-Spa que ahora se ve así (ignorando los directorios node_modules, build y config):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
├── index.html
├── package-lock.json
├── package.json
├── src
   ├── App.vue
   ├── api
      └── index.js
   ├── assets
      └── logo.png
   ├── components
      ├── Header.vue
      ├── Home.vue
      └── Survey.vue
   ├── main.js
   ├── router
      └── index.js
   └── store
       └── index.js
└── static
    └── .gitkeep

Dentro del archivo store/index.js, comienzo agregando las importaciones necesarias para los objetos Vue y Vuex, luego adjunto Vuex a Vue usando Vue.use (Vuex) similar a lo que se hizo con vue-router. Después de esto, defino cuatro objetos JavaScript eliminados: state, actions, mutations y getters.

Al final del archivo, defino un objeto final, que es una instancia del objeto Vuex.Store({}), que reúne todos los demás objetos de código auxiliar y luego se exporta.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const state = {
  // single source of data
}

const actions = {
  // asynchronous operations
}

const mutations = {
  // isolated data mutations
}

const getters = {
  // reusable data accessors
}

const store = new Vuex.Store({
  state,
  actions,
  mutations,
  getters
})

export default store

Ok, dame unos minutos para explicar el significado de los objetos state, actions, mutations y getters.

El objeto estado servirá como la única fuente de verdad donde todos los datos importantes a nivel de aplicación están contenidos dentro de la tienda. Este objeto estado contendrá datos de la encuesta a los que se puede acceder y observar cambios por parte de cualquier componente interesado en ellos, como el componente Inicio.

El objeto actions es donde definiré lo que se conoce como métodos action. Los métodos de acción se denominan "despachados" y se usan para manejar operaciones asincrónicas, como llamadas AJAX a un servicio externo o API.

El objeto mutaciones proporciona métodos que se denominan "comprometidos" y sirven como la única forma de cambiar el estado de los datos en el objeto estado. Cuando se confirma una mutación, todos los componentes que hacen referencia a los datos ahora reactivos en el objeto “estado” se actualizan con los nuevos valores, lo que hace que la interfaz de usuario actualice y vuelva a representar sus elementos.

El objeto getters también contiene métodos, pero en este caso sirven para acceder a los datos state utilizando alguna lógica para devolver información. Los captadores son útiles para reducir la duplicación de código y promover la reutilización en muchos componentes.

El último paso necesario para activar la tienda se lleva a cabo en src/main.js, donde importo el módulo store que acabo de crear. Luego, en el objeto de opciones donde se instancia la instancia de Vue de nivel superior, agrego la tienda importada como una propiedad. Esto debería verse de la siguiente manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// src/main.js

import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'

Vue.config.productionTip = false

new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})

Migración del componente de inicio a Vuex

Me gustaría comenzar a utilizar vuex en la aplicación Survey migrando la forma en que se cargan las encuestas en el componente Home para usar el patrón vuex. Para comenzar, defino e inicializo una matriz de encuestas vacía en el objeto state dentro de store/index.js. Esta será la ubicación donde residirán todos los datos de la encuesta a nivel de aplicación una vez que se hayan obtenido mediante una solicitud de AJAX.

1
2
3
4
const state = {
  // single source of data
  surveys: []
}

Ahora que las encuestas tienen un lugar donde residir, necesito crear un método de acción, loadSurveys(...), que se puede enviar desde el componente Inicio (o cualquier otro componente que requiera datos de encuestas) para manejar la solicitud asincrónica al simula la función AJAX fetchSurveys(). Para usar fetchSurveys(), primero necesito importarlo desde el módulo api y luego definir el método de acción loadSurveys(...) para manejar la solicitud.

Las acciones a menudo funcionan en conjunto con mutaciones en un patrón de realizar solicitudes AJAX asíncronas de datos a un servidor seguidas de actualizar explícitamente el objeto estado de la tienda con los datos obtenidos. Una vez que se confirma la mutación, las partes de la aplicación que usan las encuestas reconocerán que hay encuestas actualizadas a través del sistema de reactividad de Vue. Aquí la mutación que estoy definiendo se llama setSurveys(...).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import Vue from 'vue'
import Vuex from 'vuex'

// imports of AJAX functions go here
import { fetchSurveys } from '@/api'

Vue.use(Vuex)

const state = {
  // single source of data
  surveys: []
}

const actions = {
  // asynchronous operations
  loadSurveys(context) {
    return fetchSurveys()
      .then((response) => context.commit('setSurveys', { surveys: response }))
  }
}

const mutations = {
  // isolated data mutations
  setSurveys(state, payload) {
    state.surveys = payload.surveys
  }
}

Ahora que la tienda tiene la capacidad de obtener encuestas, puedo actualizar el componente Inicio y utilizar la tienda para alimentarla con datos de encuestas. De vuelta en src/components/Home.vue elimino la importación de la función fetchSurveys:

1
import { fetchSurveys } from '@/api'

y reemplácelo con una importación a la función auxiliar de vuex llamada mapState.

1
import { mapState } from 'vuex'

Usaré mapState para mapear la matriz surveys que reside en el objeto state a una propiedad calculada también llamada surveys. mapState es simplemente una función que mantiene una referencia a una propiedad específica del objeto state (state.surveys en este caso), y si esa propiedad es mutada, un componente que use mapState reaccionará a ese cambio y actualice cualquier interfaz de usuario que esté vinculada a esos datos.

En el componente Inicio, he agregado la nueva propiedad computada surveys. Además, en el método beforeMount activé el envío de la acción de almacenamiento loadSurveys. Dado que ahora hay una propiedad calculada llamada “encuestas”, debería eliminar la propiedad “encuestas” existente de la parte de datos del objeto Vue del componente. De hecho, dado que esa era la única propiedad de datos, también debería eliminar toda la propiedad de datos para mantener las cosas ordenadas, como se muestra a continuación.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<script>
import { mapState } from 'vuex'
export default {
  computed: mapState({
    surveys: state => state.surveys
  }),
  beforeMount() {
    this.$store.dispatch('loadSurveys')
  }
}
</script>

Tenga en cuenta que puedo acceder a la tienda y enviar el método de acción con la sintaxis this.$store.dispatch(...). Esto debería parecerse a la forma en que accedí a la ruta en el artículo anterior usando this.$route. Esto se debe a que tanto el vue-router como la biblioteca vuex inyectan estos objetos en la instancia de Vue como propiedades de conveniencia. También podría haber accedido a la matriz state.surveys de la tienda desde dentro del componente usando this.$store.state.surveys en lugar de usar mapState, y también puedo cometer mutaciones usando this.$store .commit.

En este punto, debería poder guardar mi proyecto y observar la misma funcionalidad en el navegador solicitando la url localhost: 8080 como se vio antes.

Migración del componente de encuesta

La siguiente tarea es migrar el componente Encuesta para utilizar la tienda de vuex para obtener la encuesta específica para participar en la realización. El flujo general para el componente Encuesta será acceder a la propiedad :id de la ruta y luego utilizar un método de acción vuex para obtener la encuesta por esa id. En lugar de llamar directamente a la función AJAX simulada fetchSurvey como se hizo anteriormente, quiero delegar eso a otro método de acción de la tienda que luego puede guardar (es decir, cometer una mutación) la encuesta obtenida en una propiedad state que nombraré currentSurvey .

Comenzando en el módulo store/index.js cambio esta línea:

1
import { fetchSurveys } from '@/api'

a

1
import { fetchSurveys, fetchSurvey } from '@/api'

Esto me da acceso a fetchSurvey dentro del módulo de tienda. Uso fetchSurvey en un nuevo método de acción llamado loadSurvey que luego comete una mutación en otro nuevo método dentro del objeto mutations llamado setCurrentSurvey.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// src/store/index.js

const actions = {
  // asynchronous operations
  loadSurveys(context) {
    // omitted for brevity
  },
  loadSurvey(context, { id }) {
    return fetchSurvey(id)
      .then((response) => context.commit('setSurvey'. { survey: response }))
  }
}

Arriba está la implementación del método de acción fetchSurvey similar al anterior fetchSurveys, excepto que se le da un parámetro de objeto adicional con una propiedad id para que se obtenga la encuesta. Para simplificar el acceso al id utilizo ES2015 desestructuración de objetos. Cuando se llama a la acción desde un componente, la sintaxis se verá así this.$store.dispatch('loadSurvey', { id: 1 }).

A continuación, agrego la propiedad currentSurvey al objeto state. Finalmente, defino una mutación llamada setSurvey en el objeto mutations, que agrega un campo choice a cada pregunta, para mantener la elección seleccionada por el encuestado, y establece el valor de currentSurvey.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const state = {
  // single source of data
  surveys: [],
  currentSurvey: {}
}

const actions = { // omitted for brevity }

const mutations = {
  // isolated data mutations
  setSurveys(state, payload) {
    state.surveys = payload.surveys
  },
  setSurvey(state, payload) {
    const nQuestions = payload.survey.questions.length
    for (let i = 0; i < nQuestions; i++) {
      payload.survey.questions[i].choice = null
    }
    state.currentSurvey = payload.survey
  }
}

En el archivo del componente Survey.vue, actualizo el método beforeMount para enviar la acción loadSurvey y asignar state.currentSurvey a una propiedad calculada llamada survey. Luego, puedo eliminar la propiedad de datos encuesta existente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script>
import { saveSurveyResponse } from '@/api'

export default {
  data() {
    return {
      currentQuestion: 0
    }
  },
  beforeMount() {
    this.$store.dispatch('loadSurvey', { id: parseInt(this.$route.params.id) })
  },
  methods: {
    // omitted for brevity
  },
  computed: {
    surveyComplete() {
      // omitted for brevity
    },
    survey() {
      return this.$store.state.currentSurvey
    }
  }
}
</script>

Guardar los archivos del proyecto y actualizar el navegador para solicitar la url localhost:8080/#/surveys/2 me da la misma interfaz de usuario nuevamente como se muestra a continuación.

Paginating survey questions{.img-responsive}

Sin embargo, todavía hay un pequeño problema. En el código de la plantilla que muestra las opciones de cada pregunta, estoy usando v-model="question.choice" para realizar un seguimiento de los cambios cuando un usuario selecciona una opción.

1
2
3
4
5
6
<div v-for="choice in question.choices" v-bind:key="choice.id">
  <label class="radio">
    <input type="radio" v-model="question.choice" :value="choice.id">
    {{ choice.text }}
  </label>
</div>

Esto da como resultado cambios en el valor question.choice al que se hace referencia dentro de la propiedad state.currentQuestion de la tienda. Este es un ejemplo de alteración incorrecta de los datos de la tienda fuera de una mutación. La documentación de vuex advierte que cualquier cambio en los datos de estado de la tienda se realice exclusivamente mediante mutaciones. Quizás se pregunte, ¿cómo puedo usar v-model en combinación con un elemento de entrada que se basa en datos obtenidos de una tienda vuex?

La respuesta a esto es usar una versión un poco más avanzada de una propiedad calculada que contiene un par definido de métodos get y set dentro de ella. Esto proporciona a v-model un mecanismo para utilizar el enlace de datos bidireccional entre la interfaz de usuario y el objeto Vue del componente. De esta forma, la propiedad calculada controla explícitamente las interacciones con los datos de la tienda. En el código de la plantilla, necesito reemplazar v-model="question.choice" con la nueva propiedad calculada como esta v-model="selectedChoice". A continuación se muestra la implementación de la propiedad computada selectedChoice.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  computed: {
    surveyComplete() {
      // omitted for brevity
    },
    survey() {
      return this.$store.state.currentSurvey
    },
    selectedChoice: {
      get() {
        const question = this.survey.questions[this.currentQuestion]
        return question.choice
      },
      set(value) {
        const question = this.survey.questions[this.currentQuestion]
        this.$store.commit('setChoice', { questionId: question.id, choice: value })
      }
    }
  }

Tenga en cuenta que en esta implementación selectedChoice es en realidad una propiedad de objeto en lugar de una función como las demás. La función get funciona junto con la propiedad de datos currentQuestion para devolver el valor choice de la pregunta que se está visualizando actualmente. La porción set(value) recibe el nuevo valor que se alimenta del enlace de datos bidireccional de v-model y comete una mutación de almacenamiento llamada setChoice. A la mutación setChoice se le pasa una carga útil de objeto que contiene el id de la pregunta que se actualizará junto con el nuevo valor.

Agrego la mutación setChoice al módulo de la tienda así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const mutations = {
  setSurveys(state, payload) {
    state.surveys = payload.surveys
  },
  setSurvey(state, payload) {
    // omitted for brevity
  },
  setChoice(state, payload) {
    const { questionId, choice } = payload
    const nQuestions = state.currentSurvey.questions.length
    for (let i = 0; i < nQuestions; i++) {
      if (state.currentSurvey.questions[i].id === questionId) {
        state.currentSurvey.questions[i].choice = choice
        break
      }
    }
  }
}

Lo último que debe migrar en el componente Encuesta es guardar las opciones de respuesta de la encuesta. Para comenzar, en Survey.vue necesito eliminar la importación de la función AJAX saveSurveyResponse

1
import { saveSurveyResponse } from '@/api'

y agréguelo como una importación en el módulo src/store/index.js así:

1
import { fetchSurveys, fetchSurvey, saveSurveyResponse } from '@/api'

Ahora, en los métodos actions del módulo store/index.js, necesito agregar un nuevo método llamado addSurveyResponse, que llamará a la función AJAX saveSurveyResponse y eventualmente la persistirá en el servidor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const actions = {
  loadSurveys(context) {
    // omitted for brevity
  },
  loadSurvey(context, { id }) {
    // omitted for brevity
  },
  addSurveyResponse(context) {
    return saveSurveyResponse(context.state.currentSurvey)
  }
}

De vuelta en el archivo del componente Survey.vue, necesito actualizar el método handleSubmit para enviar este método de acción en lugar de llamar directamente a saveSurveyResponse así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
methods: {
    goToNextQuestion() {
      // omitted for brevity
    },
    goToPreviousQuestion() {
      // omitted for brevity
    },
    handleSubmit() {
      this.$store.dispatch('addSurveyResponse')
        .then(() => this.$router.push('/'))
    }
}

Agregar la capacidad de crear nuevas encuestas

El resto de esta publicación se dedicará a desarrollar la funcionalidad para crear una nueva encuesta completa con su nombre, preguntas y opciones para cada pregunta.

Para comenzar, necesitaré agregar un archivo de componente llamado NewSurvey.vue dentro del directorio de componentes. A continuación, querré importarlo y agregar una nueva ruta en el módulo router/index.js así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// other import omitted for brevity
import NewSurvey from '@/components/NewSurvey'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    }, {
      path: '/surveys/:id',
      name: 'Survey',
      component: Survey
    }, {
      path: '/surveys',
      name: 'NewSurvey',
      component: NewSurvey
    }
  ]
})

Dentro del archivo Header.vue, necesito agregar un enlace de navegación para poder navegar a la vista de creación.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<template>
<nav class="navbar is-light" role="navigation" aria-label="main navigation">
  <div class="navbar-menu">
    <div class="navbar-start">
      <router-link to="/" class="navbar-item">
        Home
      </router-link>
      <router-link to="/surveys" class="navbar-item">
        Create Survey
      </router-link>
    </div>
  </div>
</nav>
</template>

Ahora, en el componente NewSurvey.vue, desarrollaré la estructura básica de la interfaz de usuario de creación de encuestas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<template>
  <div>
    <section class="hero is-primary">
      <div class="hero-body">
        <div class="container has-text-centered">
          <h2 class="title">{{ name }}</h2>
        </div>
      </div>
    </section>

    <section class="section">
      <div class="container">
        <div class="tabs is-centered is-fullwidth is-large">
            <ul>
                <li :class="{'is-active': step == 'name'}" @click="step = 'name'">
                    <a>Name</a>
                </li>
                <li :class="{'is-active': step == 'questions'}" @click="step = 'questions'">
                    <a>Questions</a>
                </li>
                <li :class="{'is-active': step == 'review'}" @click="step = 'review'">
                    <a>Review</a>
                </li>
            </ul>
        </div>
        <div class="columns">
          <div class="column is-half is-offset-one-quarter">

            <div class="name" v-show="step === 'name'">
              <h2 class='is-large'>Add name</h2>
            </div>

            <div class="questions" v-show="step === 'questions'">
              <h2>Add Questions</h2>
            </div>

            <div class="review" v-show="step === 'review'">
              <h2>Review and Submit</h2>
            </div>

          </div>
        </div>
      </div>
    </section>
  </div>
</template>

<script>
export default {
  data() {
    return {
      step: 'name'
    }
  }
}
</script>

<style></style>

Basic new survey structure{.img-responsive}

Como puede ver en la captura de pantalla anterior, hay tres pestañas que activarán la visualización de los componentes de la interfaz de usuario para agregar el nombre, las preguntas y la revisión antes de guardar.

La funcionalidad que impulsa la interactividad de esta página se basa en el valor de una propiedad de datos de “paso” que determina qué pestaña debe estar activa. paso por defecto es la pestaña "nombre", pero se actualiza cuando un usuario hace clic en una de las otras pestañas. El valor de step no solo determina qué pestaña debe tener la clase is-active, sino que también impulsa la visualización y ocultación de divs que proporcionan una interfaz de usuario para agregar nombre, pregunta y revisión antes de enviar.

Comienzo con el div de la interfaz de usuario del nombre, que simplemente contiene una entrada de texto vinculada a una propiedad de datos name a través de v-model, así:

parte de la plantilla

1
2
3
4
5
6
7
8
<div class="name" v-show="step === 'name'">
  <div class="field">
    <label class="label" for="name">Survey name:</label>
    <div class="control">
      <input type="text" class="input is-large" id="name" v-model="name">
    </div>
  </div>
</div>

parte del guión

1
2
3
4
5
6
data() {
  return {
    step: 'name',
    name: ''
  }
}

La interfaz de usuario de preguntas y respuestas será un poco más complicada. Para mantener el componente NewSurvey más organizado y reducir la complejidad, agregaré un componente de archivo NewQuestion.vue para manejar la interfaz de usuario y el comportamiento necesarios para agregar nuevas preguntas junto con un número variable de respuestas.

También debo tener en cuenta que para los componentes NewSurvey y NewQuestion utilizaré el estado de nivel de componente para aislar la tienda de los nuevos datos de encuesta intermedios hasta que un usuario envíe la nueva encuesta. Una vez enviada, activaré la tienda de vuex y el patrón asociado de envío de una acción para ENVIAR la nueva encuesta al servidor y luego redirigir al componente Inicio. El componente Inicio puede recuperar todas las encuestas, incluida la nueva.

En el archivo NewQuestion.vue ahora tengo el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<template>
<div>
    <div class="field">
        <label class="label is-large">Question</label>
        <div class="control">
            <input type="text" class="input is-large" v-model="question">
        </div>
    </div>

    <div class="field">
        <div class="control">
            <a class="button is-large is-info" @click="addChoice">
                <span class="icon is-small">
                <i class="fa fa-plus-square-o fa-align-left" aria-hidden="true"></i>
                </span>
                <span>Add choice</span>
            </a>
            <a class="button is-large is-primary @click="saveQuestion">
                <span class="icon is-small">
                    <i class="fa fa-check"></i>
                </span>
                <span>Save</span>
            </a>
        </div>
    </div>

    <h2 class="label is-large" v-show="choices.length > 0">Question Choices</h2>
    <div class="field has-addons" v-for="(choice, idx) in choices" v-bind:key="idx">
      <div class="control choice">
        <input type="text" class="input is-large" v-model="choices[idx]">
      </div>
      <div class="control">
        <a class="button is-large">
          <span class="icon is-small" @click.stop="removeChoice(choice)">
            <i class="fa fa-times" aria-hidden="true"></i>
          </span>
        </a>
      </div>
    </div>
</div>
</template>

<script>
export default {
  data() {
    return {
      question: '',
      choices: []
    }
  },
  methods: {
    removeChoice(choice) {
      const idx = this.choices.findIndex(c => c === choice)
      this.choices.splice(idx, 1)
    },
    saveQuestion() {
      this.$emit('questionComplete', {
        question: this.question,
        choices: this.choices.filter(c => !!c)
      })
      this.question = ''
      this.choices = []
    },
    addChoice() {
      this.choices.push('')
    }
  }
}
</script>

<style>
.choice {
  width: 90%;
}
</style>

La mayoría de las características ya se han discutido, por lo que solo las revisaré brevemente. Para comenzar, tengo una propiedad de datos pregunta que está vinculada a una entrada de texto a través de v-model="pregunta" que proporciona un enlace de datos bidireccional entre la propiedad de datos pregunta y el elemento de entrada de la interfaz de usuario.

Debajo de la entrada de texto de la pregunta hay dos botones. Uno de los botones es para agregar una opción y contiene un detector de eventos @click="addChoice" que inserta una cadena vacía en la matriz choices. La matriz choices se usa para controlar la visualización de las entradas de texto de elección, cada una de las cuales está vinculada a su elemento respectivo de la matriz choices a través de v-model="choices[idx]". Cada entrada de texto de elección se combina con un botón que permite al usuario eliminarlo debido a la presencia del detector de eventos de clic @click="removeChoice(choice)".

La última parte de la interfaz de usuario en el componente NewQuestion para discutir es el botón Guardar. Cuando un usuario ha agregado su pregunta y el número deseado de opciones, puede hacer clic aquí para guardar la pregunta. Esto se logra a través del detector de clics @click="saveQuestion".

Sin embargo, dentro del método saveQuestion he introducido un nuevo tema. Tenga en cuenta que estoy haciendo uso de otro método adjunto a la instancia Vue del componente. Este es el método emisor de eventos this.$emit(...). Al llamar a esto, estoy transmitiendo al componente principal, NewSurvey, el evento llamado "questionComplete" y pasando junto con él un objeto de carga útil con pregunta y opciones.

De vuelta en el archivo NewSurvey.vue, querré importar este componente NewQuestion y registrarlo en la instancia de Vue del componente de esta manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<script>
import NewQuestion from '@/components/NewQuestion'

export default {
  components: { NewQuestion },
  data() {
    return {
      step: 'name',
      name: ''
    }
  }
}
</script>

Entonces puedo incluirlo en la plantilla como un elemento componente así:

1
2
3
<div class="questions" v-show="step === 'questions'">
  <new-question v-on:questionComplete="appendQuestion"/>
</div>

Tenga en cuenta que he usado la directiva v-on para escuchar el evento "questionComplete" que se emitirá desde el componente NewQuestion y registré una devolución de llamada de appendQuestion. Este es el mismo concepto que hemos visto con el detector de eventos @click="someCallbackFunction", pero esta vez es para un evento personalizado. Por cierto, podría haber usado la sintaxis @questionComplete="appendQuestion" más corta, pero pensé en agregar algo de variedad, y también es más explícito de esta manera.

El siguiente paso lógico sería agregar el método appendQuestion al componente NewSurvey junto con una propiedad de datos questions para mantener la colección de preguntas y respuestas generadas en el componente NewQuestion y enviadas a NewSurvey.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
export default {
  components: { NewQuestion },
  data() {
    return {
      step: 'name',
      name: '',
      question: []
    }
  },
  methods: {
    appendQuestion(newQuestion) {
      this.questions.push(newQuestion)
    }
  }
}

Ahora puedo guardar y actualizar por navegador a la URL localhost:8080/#/surveys y luego hacer clic en la pestaña Preguntas, agregar el texto de una pregunta y algunas opciones como se muestra a continuación.

New Survey Questions and Responses{.img-responsive}

La pestaña final para completar es la pestaña Revisar. Esta página enumerará las preguntas y las opciones, así como también ofrecerá al usuario la posibilidad de eliminarlas. Si el usuario está satisfecho, puede enviar la encuesta y la aplicación lo redirigirá al componente Inicio.

La parte de la plantilla del código para la IU de revisión es la siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div class="review" v-show="step === 'review'">
  <ul>
    <li class="question" v-for="(question, qIdx) in questions" :key="`question-${qIdx}`">
      <div class="title">
        {{ question.question }}
        <span class="icon is-medium is-pulled-right delete-question"
          @click.stop="removeQuestion(question)">
          <i class="fa fa-times" aria-hidden="true"></i>
        </span>
      </div>
      <ul>
        <li v-for="(choice , cIdx) in question.choices" :key="`choice-${cIdx}`">
          {{ cIdx + 1 }}. {{ choice }}
        </li>
      </ul>
    </li>
  </ul>

  <div class="control">
    <a class="button is-large is-primary" @click="submitSurvey">Submit</a>
  </div>

</div>

La parte del script ahora solo necesita actualizarse agregando los métodos removeQuestion y submitSurvey para manejar sus respectivos detectores de eventos de clic.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
methods: {
  appendQuestion(newQuestion) {
    this.questions.push(newQuestion)
  },
  removeQuestion(question) {
    const idx = this.questions.findIndex(q => q.question === question.question)
    this.questions.splice(idx, 1)
  },
  submitSurvey() {
    this.$store.dispatch('submitNewSurvey', {
      name: this.name,
      questions: this.questions
    }).then(() => this.$router.push('/'))
  }
}

El método removeQuestion(question) elimina la pregunta de la matriz questions en la propiedad de datos que actualiza de forma reactiva la lista de preguntas que componen la interfaz de usuario anterior. El método submitSurvey envía un método de acción que pronto se agregará submitNewSurvey y le pasa el nuevo contenido de la encuesta y luego usa this.$router.push(...) del componente para redirigir la aplicación al componente Inicio.

Ahora, lo único que debe hacer es crear el método de acción submitNewSurvey y la función AJAX simulada correspondiente para falsificar la publicación en el servidor. En el objeto actions de la tienda, agrego lo siguiente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const actions = {
  // asynchronous operations
  loadSurveys(context) {
    return fetchSurveys()
      .then((response) => context.commit('setSurveys', { surveys: response }))
  },
  loadSurvey(context, { id }) {
    return fetchSurvey(id)
      .then((response) => context.commit('setSurvey', { survey: response }))
  },
  addSurveyResponse(context) {
    return saveSurveyResponse(context.state.currentSurvey)
  },
  submitNewSurvey(context, survey) {
    return postNewSurvey(survey)
  }
}

Finalmente, en el módulo api/index.js agrego la función AJAX postNewSurvey(survey) para simular un POST a un servidor.

1
2
3
4
5
6
7
8
export function postNewSurvey(survey) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('Saving survey ...', survey)
      resolve()
    }, 300)
  })
}

Guardo todos los archivos de mi proyecto y solicito la URL localhost:8080/#/surveys. Luego, al agregar un nombre, algunas preguntas con opciones y hacer una pausa en la pestaña de revisión, veo la siguiente interfaz de usuario:

Review new survey{.img-responsive}

Recursos

¿Desea obtener más información sobre Vue.js y la creación de aplicaciones web front-end? Intente consultar algunos de los siguientes recursos para profundizar en este marco de front-end:

Conclusión

Durante esta publicación, he tratado de cubrir lo que creo que son los aspectos más importantes de un tema bastante amplio, vuex. Vuex es una adición muy poderosa a un proyecto Vue.js que brinda al desarrollador un patrón intuitivo que mejora la organización y la solidez de las aplicaciones de una sola página basadas en datos de moderadas a grandes.

Como siempre, gracias por leer y no se avergüence de comentar o criticar a continuación. r a continuación.