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 *