Gestión de estado con Vuex
Contenido
Gracias por acompañarme en la tercera publicación sobre el uso de Vue.js y Flask para el desarrollo web de pila completa. El tema principal de esta publicación será sobre el uso de vuex para administrar el estado en nuestra aplicación. Para presentar vuex, demostraré cómo refactorizar los componentes de Home y Survey 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 para esta publicación está en un repositorio en mi cuenta de GitHub en la rama ThirdPost.
Contenido de la serie
- Seup y familiarización con VueJS
- Navegando por el enrutador Vue
- Gestión de estado con Vuex (estás aquí)
- API RESTful con Flask
- Integración AJAX con API REST
- Autenticación JWT
- Implementación en un servidor privado virtual
Presentando Vuex
Vuex es una biblioteca de administración estatal centralizada oficialmente respaldada por el equipo de desarrollo principal de Vue.js. Vuex proporciona un patrón de flujo de datos unidireccional, similar a un flujo, que ha demostrado ser muy poderoso para admitir aplicaciones Vue.js de moderadas a grandes.
Hay otras implementaciones de bibliotecas y patrones de administración de estado similares a flux, pero vuex ha sido diseñado para funcionar específicamente con el sistema de reactividad rápido y simple de Vue.js. Esto se logra mediante 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 único de la fuente de la verdad, vuex también proporciona métodos explícitos y rastreables para operaciones asincrónicas (acciones), accesos reutilizables convenientes (captadores) y capacidades de alteración de datos (mutaciones).
Para usar vuex, primero necesitaré instalarlo en el mismo directorio que contiene el archivo package.json así:
$ npm install --save vuex
Luego 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):
├── 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 de manera Vue.use(Vuex)
similar a lo que se hizo con vue-router. Después de esto definir cuatro apagó objetos JavaScript: state
, actions
, mutations
, y getters
.
Al final del archivo, defino un objeto final, que es una instancia del Vuex.Store({})
objeto, que reúne todos los demás objetos de código auxiliar y luego se exporta.
// 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 state
, actions
, mutations
, y getters
objetos.
El state
objeto 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 state
objeto contendrá datos de la encuesta a los que se puede acceder y observar los cambios de cualquier componente interesado en ellos, como el componente Inicio.
El actions
objeto es donde definiré lo que se conoce como métodos de acción. Los métodos de acción se denominan “enviados” y se utilizan para manejar operaciones asincrónicas como llamadas AJAX a un servicio externo o API.
El mutations
objeto proporciona métodos a los que se hace referencia como “comprometidos” y sirven como la única forma de cambiar el estado de los datos en el state
objeto. Cuando se comete una mutación, cualquier componente que haga referencia a los datos ahora reactivos en el state
objeto se actualiza con los nuevos valores, lo que hace que la interfaz de usuario se actualice y vuelva a representar sus elementos.
El getters
objeto también contiene métodos, pero en este caso sirven para acceder a los state
datos 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 store
módulo recién creado. Luego, en el objeto de opciones donde Vue
se crea una instancia de la instancia de nivel superior, agrego la importación store
como una propiedad. Esto debería verse como sigue:
// 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 la casa a Vuex
Me gustaría comenzar a utilizar vuex en la aplicación Encuesta migrando la forma en que se cargan las encuestas en el componente Inicio para usar el patrón vuex. Para comenzar, defino e inicializo una matriz de encuestas vacía en el state
objeto dentro de store / index.js. Esta será la ubicación donde residirán todos los datos de la encuesta a nivel de la aplicación una vez que se obtengan mediante una solicitud AJAX.
const state = {
// single source of data
surveys: []
}
Ahora que las encuestas tienen un lugar para residir, necesito crear un método de acción loadSurveys(...)
, que pueda enviarse desde el componente Inicio (o cualquier otro componente que requiera datos de la encuesta) para manejar la solicitud asincrónica a la función simulada de AJAX fetchSurveys()
. Para usarlo fetchSurveys()
, primero necesito importarlo desde el api
módulo y luego definir el loadSurveys(...)
método de acción para manejar la solicitud.
Las acciones a menudo funcionan en conjunto con mutaciones en un patrón de realizar solicitudes AJAX asincrónicas de datos a un servidor, seguidas de una actualización explícita del state
objeto de la tienda con los datos obtenidos. Una vez que se ha cometido la mutación, las partes de la aplicación que utilizan las encuestas reconocerán que hay encuestas actualizadas a través del sistema de reactividad de Vue. Aquí se llama la mutación que estoy definiendo setSurveys(...)
.
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 buscar 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 fetchSurveys
función:
import { fetchSurveys } from '@/api'
y reemplácelo con una importación a la función auxiliar de vuex llamada mapState
.
import { mapState } from 'vuex'
Usaré mapState
para mapear la surveys
matriz que reside en el state
objeto a una propiedad calculada también llamada surveys
. mapState
es simplemente una función que mantiene una referencia a una propiedad específica del state
objeto ( state.surveys
en este caso), y si esa propiedad está mutada, un componente que usa mapState
reaccionará a ese cambio y actualizará cualquier IU que esté vinculada a esos datos.
En el componente Inicio, agregué la nueva surveys
propiedad calculada. Además, en el beforeMount
método loadSurveys
activo el envío de la acción de tienda. Dado que ahora hay una propiedad calculada llamada surveys
, debería eliminar la surveys
propiedad 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.
<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 verse similar a la forma en que accedí a la ruta en el artículo anterior usando this.$route
. Esto se debe a que tanto el enrutador vue como la biblioteca vuex inyectan estos objetos en la instancia de Vue como propiedades de conveniencia. También podría haber accedido a la state.surveys
matriz de la tienda desde el componente usando en this.$store.state.surveys
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.
Migrar el 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 de Encuesta será acceder a la :id
punta de la ruta y luego utilizar un método de acción vuex para buscar la encuesta por eso id
. En lugar de llamar directamente a la función simulada de AJAX fetchSurvey
como se hizo anteriormente, quiero delegar eso a otro método de acción de la tienda que luego pueda guardar (es decir, realizar una mutación) la encuesta obtenida en una state
propiedad que nombraré currentSurvey
.
Comenzando en el módulo store / index.js cambio esta línea:
import { fetchSurveys } from '@/api'
a
import { fetchSurveys, fetchSurvey } from '@/api'
Esto me da acceso fetchSurvey
dentro del módulo de la tienda. Lo uso fetchSurvey
en un nuevo método de acción llamado loadSurvey
que luego comete una mutación en otro método nuevo dentro del mutations
objeto llamado setCurrentSurvey
.
// 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 fetchSurvey
método de acción similar al anterior fetchSurveys
, excepto que se le da un parámetro de objeto adicional con una propiedad id para que la encuesta sea recuperada. Para simplificar el acceso a la identificación, utilizo la desestructuración de objetos ES2015 . 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 currentSurvey
propiedad al state
objeto. Finalmente, defino una mutación llamada setSurvey
en el mutations
objeto, que agrega un choice
campo a cada pregunta, para contener la opción seleccionada por el encuestado y establecer el valor de currentSurvey
.
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 beforeMount
método para enviar la loadSurvey
acción y el mapeo state.currentSurvey
a una propiedad calculada llamada survey
. Luego puedo eliminar la survey
propiedad de datos existente .
<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 devuelve la misma interfaz de usuario que se muestra a continuación.
Sin embargo, todavía hay un pequeño problema. En el código de plantilla que muestra las opciones de cada pregunta que estoy usando v-model="question.choice"
para realizar un seguimiento de los cambios cuando un usuario selecciona una opción.
<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 question.choice
valor al que se hace referencia dentro de la state.currentQuestion
propiedad 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. Es posible que se pregunte, ¿cómo puedo usarlo v-model
en combinación con un elemento de entrada impulsado por datos provenientes de una tienda vuex?
La respuesta a esto es utilizar una versión un poco más avanzada de una propiedad calculada que contenga un par definido de métodos get
y set
dentro de ella. Esto proporciona v-model
un mecanismo para utilizar el enlace de datos bidireccional entre la interfaz de usuario y el objeto Vue del componente. De esta manera, la propiedad calculada controla explícitamente las interacciones con los datos de la tienda. En el código de la plantilla, necesito reemplazarlo 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 calculada selectedChoice
.
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 get
función trabaja junto con la currentQuestion
propiedad de datos para devolver el choice
valor de la pregunta que se está viendo actualmente. La set(value)
porción recibe el nuevo valor que se alimenta del v-model
enlace de datos bidireccional y confirma una mutación de almacenamiento llamada setChoice
. A la setChoice
mutación se le pasa una carga útil de objeto que contiene el valor id
de la pregunta que se actualizará junto con el nuevo value
.
Agrego la setChoice
mutación al módulo de la tienda de la siguiente manera:
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 saveSurveyResponse
función AJAX
import { saveSurveyResponse } from '@/api'
y agréguelo como una importación en el módulo src / store / index.js así:
import { fetchSurveys, fetchSurvey, saveSurveyResponse } from '@/api'
Ahora, en los actions
métodos del módulo store / index.js, necesito agregar un nuevo método llamado addSurveyResponse
, que llamará a la saveSurveyResponse
función AJAX y, finalmente, la persistirá en el servidor.
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 handleSubmit
método para enviar este método de acción en lugar de llamar directamente saveSurveyResponse
así:
methods: {
goToNextQuestion() {
// omitted for brevity
},
goToPreviousQuestion() {
// omitted for brevity
},
handleSubmit() {
this.$store.dispatch('addSurveyResponse')
.then(() => this.$router.push("https://Pharos.sh.com/"))
}
}
Adición de 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 componentes 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í:
// other import omitted for brevity
import NewSurvey from '@/components/NewSurvey'
Vue.use(Router)
export default new Router({
routes: [
{
path: "https://Pharos.sh.com/",
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.
<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 para crear encuestas.
<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>
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 dicta en función del valor de una step
propiedad de datos que determina qué pestaña debe estar activa. step
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 no solo step
determina qué pestaña debe tener la is-active
clase, sino que también impulsa la visualización y ocultación de divs
esa interfaz de usuario para agregar nombre, pregunta y revisión antes de enviarla.
Comienzo con el nombre de la interfaz de usuario, div
que simplemente contiene una entrada de texto vinculada a una name
propiedad de datos a través de v-model
, así:
porción de plantilla
<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>
porción del guión
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 necesario 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 a nivel de componente para aislar la tienda de los datos de la nueva encuesta intermedia hasta que un usuario envíe la nueva encuesta. Una vez enviada, involucraré la tienda de vuex y el patrón asociado de envío de una acción para PUBLICAR la nueva encuesta al servidor y luego redirigir al componente Inicio. El componente Inicio puede buscar todas las encuestas, incluida la nueva.
En el archivo NewQuestion.vue ahora tengo el siguiente código:
<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 question
propiedad de datos que está vinculada a una entrada de texto al v-model="question"
proporcionar un enlace de datos bidireccional entre la propiedad de datos question
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 choices
matriz. La choices
matriz se utiliza 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 choices
matriz a través de v-model="choices[idx]"
. Cada entrada de texto de elección se empareja con un botón que permite al usuario eliminarlo debido a la presencia del oyente del evento de clic @click="removeChoice(choice)"
.
La última pieza de la interfaz de usuario del componente NewQuestion que se debe analizar 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 mediante el oyente de clics @click="saveQuestion"
.
Sin embargo, dentro del saveQuestion
método he introducido un nuevo tema. Tenga en cuenta que estoy haciendo uso de otro método adjunto a la Vue
instancia del componente . Este es el this.$emit(...)
método de emisor de eventos. 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 question
y choices
.
De vuelta en el archivo NewSurvey.vue, querré importar este componente NewQuestion y registrarlo en la instancia de Vue del componente de esta manera:
<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 de componente así:
<div class="questions" v-show="step === 'questions'">
<new-question v-on:questionComplete="appendQuestion"/>
</div>
Observe que he usado la v-on
directiva 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 @click="someCallbackFunction"
detector de eventos, pero esta vez es para un evento personalizado. Por cierto, podría haber usado la @questionComplete="appendQuestion"
sintaxis más corta, pero pensé que agregaría algo de variedad, y también es más explícito de esta manera.
La siguiente cosa lógica sería agregar el appendQuestion
método al componente NewSurvey junto con una questions
propiedad de datos para mantener la colección de preguntas y respuestas generadas en el componente NewQuestion y emitidas de nuevo a NewSurvey.
export default {
components: { NewQuestion },
data() {
return {
step: 'name',
name: '',
question: []
}
},
methods: {
appendQuestion(newQuestion) {
this.questions.push(newQuestion)
}
}
}
Ahora puedo guardar y actualizar mediante el navegador la URL y localhost:8080/#/surveys
luego hacer clic en la pestaña Preguntas, agregar el texto de una pregunta y algunas opciones como se muestra a continuación.
La última pestaña para completar es la pestaña Revisar. Esta página enumerará las preguntas y opciones, además de 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 interfaz de usuario de revisión es la siguiente:
<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.
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("https://Pharos.sh.com/"))
}
}
El removeQuestion(question)
método elimina la pregunta de la questions
matriz en la propiedad de datos que actualiza reactivamente la lista de preguntas que componen la interfaz de usuario anterior. El submitSurvey
método envía un método de acción que se agregará pronto submitNewSurvey
y le pasa el nuevo contenido de la encuesta y luego usa el componente this.$router.push(...)
para redirigir la aplicación al componente Inicio.
Ahora, lo único que se puede hacer es crear el submitNewSurvey
método de acción y la función AJAX simulada correspondiente para realizar una publicación falsa en el servidor. En el actions
objeto de la tienda agrego lo siguiente.
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 postNewSurvey(survey)
función AJAX para simular un POST en un servidor.
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, agregando un nombre, algunas preguntas con opciones y haciendo una pausa en la pestaña de revisión, veo la siguiente interfaz de usuario:
Conclusión
Durante esta publicación he tratado de cubrir lo que creo que son los aspectos más importantes de un tema bastante extenso, vuex. Vuex es una adición muy poderosa a un proyecto de 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 dude en comentar o criticar a continuación.
.