Single Page Apps con Vue.js y Flask: Gestión de estados con Vuex

    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 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 gettersobjetos.

    El stateobjeto 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 stateobjeto 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 actionsobjeto 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 mutationsobjeto 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 stateobjeto. Cuando se comete una mutación, cualquier componente que haga referencia a los datos ahora reactivos en el stateobjeto se actualiza con los nuevos valores, lo que hace que la interfaz de usuario se actualice y vuelva a representar sus elementos.

    El gettersobjeto también contiene métodos, pero en este caso sirven para acceder a los statedatos 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 storemódulo recién creado. Luego, en el objeto de opciones donde Vuese crea una instancia de la instancia de nivel superior, agrego la importación storecomo 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 stateobjeto 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 apimó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 stateobjeto 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 fetchSurveysfunció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é mapStatepara mapear la surveysmatriz que reside en el stateobjeto a una propiedad calculada también llamada surveys. mapStatees simplemente una función que mantiene una referencia a una propiedad específica del stateobjeto ( state.surveysen este caso), y si esa propiedad está mutada, un componente que usa mapStatereaccionará a ese cambio y actualizará cualquier IU que esté vinculada a esos datos.

    En el componente Inicio, agregué la nueva surveyspropiedad calculada. Además, en el beforeMountmétodo loadSurveysactivo el envío de la acción de tienda. Dado que ahora hay una propiedad calculada llamada surveys, debería eliminar la surveyspropiedad 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.surveysmatriz de la tienda desde el componente usando en this.$store.state.surveyslugar 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:8080como 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 :idpunta 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 fetchSurveycomo 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 statepropiedad 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 fetchSurveydentro del módulo de la tienda. Lo uso fetchSurveyen un nuevo método de acción llamado loadSurveyque luego comete una mutación en otro método nuevo dentro del mutationsobjeto 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 fetchSurveymé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 currentSurveypropiedad al stateobjeto. Finalmente, defino una mutación llamada setSurveyen el mutationsobjeto, que agrega un choicecampo 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 beforeMountmétodo para enviar la loadSurveyacción y el mapeo state.currentSurveya una propiedad calculada llamada survey. Luego puedo eliminar la surveypropiedad 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/2me 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.choicevalor al que se hace referencia dentro de la state.currentQuestionpropiedad 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-modelen 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 gety setdentro de ella. Esto proporciona v-modelun 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 selectedChoicees en realidad una propiedad de objeto en lugar de una función como las demás. La getfunción trabaja junto con la currentQuestionpropiedad de datos para devolver el choicevalor de la pregunta que se está viendo actualmente. La set(value)porción recibe el nuevo valor que se alimenta del v-modelenlace de datos bidireccional y confirma una mutación de almacenamiento llamada setChoice. A la setChoicemutación se le pasa una carga útil de objeto que contiene el valor idde la pregunta que se actualizará junto con el nuevo value.

    Agrego la setChoicemutació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 saveSurveyResponsefunció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 actionsmétodos del módulo store / index.js, necesito agregar un nuevo método llamado addSurveyResponse, que llamará a la saveSurveyResponsefunció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 handleSubmitmétodo para enviar este método de acción en lugar de llamar directamente saveSurveyResponseasí:

    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 steppropiedad de datos que determina qué pestaña debe estar activa. steppor 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 stepdetermina qué pestaña debe tener la is-activeclase, sino que también impulsa la visualización y ocultación de divsesa interfaz de usuario para agregar nombre, pregunta y revisión antes de enviarla.

    Comienzo con el nombre de la interfaz de usuario, divque simplemente contiene una entrada de texto vinculada a una namepropiedad 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 questionpropiedad 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 questiony 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 choicesmatriz. La choicesmatriz 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 choicesmatriz 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 saveQuestionmétodo he introducido un nuevo tema. Tenga en cuenta que estoy haciendo uso de otro método adjunto a la Vueinstancia 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 questiony 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-ondirectiva 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 appendQuestionmétodo al componente NewSurvey junto con una questionspropiedad 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/#/surveysluego 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 removeQuestiony submitSurveypara 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 questionsmatriz en la propiedad de datos que actualiza reactivamente la lista de preguntas que componen la interfaz de usuario anterior. El submitSurveymétodo envía un método de acción que se agregará pronto submitNewSurveyy 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 submitNewSurveymétodo de acción y la función AJAX simulada correspondiente para realizar una publicación falsa en el servidor. En el actionsobjeto 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.

    .

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *