Vue.js para principiantes lección 11: pestañas, el bus de eventos global

Hoy, en la undécima lección que concluye este tutorial de Vue Fundamentals, hablaremos sobre cómo organizar el contenido de la página de su aplicación usando pestañas. Aquí discutiremos el bus de eventos global, un mecanismo simple para transferir datos dentro de una aplicación.







Vue.js principiantes lección 1: instancia Vue

Vue.js para principiantes, lección 2: atributos de enlace

Vue.js principiantes lección 3: renderizado condicional

Vue.js principiantes lección 4: listas de renderización

Vue .js para principiantes lección 5: procesamiento de eventos

Vue.js principiantes lección 6: clases y estilos vinculantes

Vue.js principiantes lección 7: propiedades calculadas

Vue.js principiantes lección 8: componentes

Vue. js para principiantes lección 9: eventos personalizados

Vue.js para principiantes lección 10: formularios



El propósito de la lección



Queremos tener pestañas en la página de la aplicación, una de las cuales permite a los visitantes escribir reseñas de productos y la otra les permite ver reseñas existentes.



Código inicial



Así es como se ve el contenido del archivo en esta etapa del trabajo index.html:



<div id="app">
  <div class="cart">
    <p>Cart({{ cart.length }})</p>
  </div>

  <product :premium="premium" @add-to-cart="updateCart"></product>
</div>


En main.jsexiste el siguiente código:



Vue.component('product', {
  props: {
    premium: {
      type: Boolean,
      required: true
    }
  },
  template: `
  <div class="product">

    <div class="product-image">
      <img :src="image" />
    </div>

    <div class="product-info">
      <h1>{{ title }}</h1>
      <p v-if="inStock">In stock</p>
      <p v-else>Out of Stock</p>
      <p>Shipping: {{ shipping }}</p>

      <ul>
        <li v-for="(detail, index) in details" :key="index">{{ detail }}</li>
      </ul>
      <div
        class="color-box"
        v-for="(variant, index) in variants"
        :key="variant.variantId"
        :style="{ backgroundColor: variant.variantColor }"
        @mouseover="updateProduct(index)"
      ></div>

      <button
        @click="addToCart"
        :disabled="!inStock"
        :class="{ disabledButton: !inStock }"
      >
        Add to cart
      </button>

    </div>

    <div>
      <h2><font color="#3AC1EF">Reviews</font></h2>
      <p v-if="!reviews.length">There are no reviews yet.</p>
      <ul>
        <li v-for="review in reviews">
        <p>{{ review.name }}</p>
        <p>Rating: {{ review.rating }}</p>
        <p>{{ review.review }}</p>
        </li>
      </ul>
    </div>

    <product-review @review-submitted="addReview"></product-review>   
  
    </div>
  `,
  data() {
    return {
      product: 'Socks',
      brand: 'Vue Mastery',
      selectedVariant: 0,
      details: ['80% cotton', '20% polyester', 'Gender-neutral'],
      variants: [
        {
          variantId: 2234,
          variantColor: 'green',
          variantImage: './assets/vmSocks-green.jpg',
          variantQuantity: 10
        },
        {
          variantId: 2235,
          variantColor: 'blue',
          variantImage: './assets/vmSocks-blue.jpg',
          variantQuantity: 0
        }
      ],
      reviews: []
    }
  },
    methods: {
      addToCart() {
        this.$emit('add-to-cart', this.variants[this.selectedVariant].variantId);
      },
      updateProduct(index) {
        this.selectedVariant = index;
      },
      addReview(productReview) {
        this.reviews.push(productReview)
      }
    },
    computed: {
      title() {
        return this.brand + ' ' + this.product;
      },
      image() {
        return this.variants[this.selectedVariant].variantImage;
      },
      inStock() {
        return this.variants[this.selectedVariant].variantQuantity;
      },
      shipping() {
        if (this.premium) {
          return "Free";
        } else {
          return 2.99
        }
      }
    }
})

Vue.component('product-review', {
  template: `
    <form class="review-form" @submit.prevent="onSubmit">

      <p v-if="errors.length">
        <b>Please correct the following error(s):</b>
        <ul>
          <li v-for="error in errors">{{ error }}</li>
        </ul>
      </p>

      <p>
        <label for="name">Name:</label>
        <input id="name" v-model="name">
      </p>
      
      <p>
        <label for="review">Review:</label>      
        <textarea id="review" v-model="review"></textarea>
      </p>
      
      <p>
        <label for="rating">Rating:</label>
        <select id="rating" v-model.number="rating">
          <option>5</option>
          <option>4</option>
          <option>3</option>
          <option>2</option>
          <option>1</option>
        </select>
      </p>
          
      <p>
        <input type="submit" value="Submit">  
      </p>    

    </form>

  `,
  data() {
    return {
      name: null,
      review: null,
      rating: null,
      errors: []
    }
  },
  methods: {
    onSubmit() {
      if(this.name && this.review && this.rating) {
        let productReview = {
          name: this.name,
          review: this.review,
          rating: this.rating
        }
        this.$emit('review-submitted', productReview)
        this.name = null
        this.review = null
        this.rating = null
      } else {
        if(!this.name) this.errors.push("Name required.")
        if(!this.review) this.errors.push("Review required.")
        if(!this.rating) this.errors.push("Rating required.")
      }
    }
  }
})

var app = new Vue({
  el: '#app',
  data: {
    premium: true,
    cart: []
  },
  methods: {
    updateCart(id) {
      this.cart.push(id);
    }
  }
})


Así es como se ve la aplicación ahora.





Página de aplicación



Tarea



Actualmente, las reseñas y el formulario que se utiliza para enviar reseñas se muestran en la página contigua. Esta es una estructura bastante funcional. Pero se espera que aparezcan más y más reseñas en la página con el tiempo. Esto significa que será más conveniente para los usuarios interactuar con una página que, de su elección, muestre un formulario o una lista de reseñas.



La solucion del problema



Para solucionar nuestro problema, podemos agregar un sistema de pestañas a la página. Uno de ellos, con un título Reviews, mostrará reseñas. El segundo, con un título Make a Review, mostrará un formulario para enviar reseñas.



Crear un componente que implemente el sistema de pestañas



Comencemos por crear un componente product-tabs. Se mostrará en la parte inferior de la representación visual del componente product. Con el tiempo, reemplazará el código que se usa actualmente para mostrar la lista de revisiones y formularios en la página.



Vue.component('product-tabs', {
  template: `
    <div>
      <span class="tab" v-for="(tab, index) in tabs" :key="index">{{ tab }}</span>
    </div>
  `,
  data() {
    return {
      tabs: ['Reviews', 'Make a Review']      
    }
  }
})


En este momento, esto es solo un componente en blanco que finalizaremos pronto. Por ahora, analicemos brevemente lo que se presenta en este código.



Los datos del componente tienen una matriz que tabscontiene las cadenas que usamos como encabezados de pestaña. La plantilla de componente utiliza una construcción v-forque tabscrea un elemento que <span>contiene la cadena correspondiente para cada elemento de la matriz . Lo que forma este componente en esta etapa de trabajo se verá como el que se muestra a continuación.





El componente de pestañas de productos en la etapa inicial de trabajo en él



. Para lograr nuestros objetivos, necesitamos saber cuál de las pestañas está activa. Por lo tanto, agreguemos una propiedad a los datos del componenteselectedTab. Estableceremos dinámicamente el valor de esta propiedad utilizando un controlador de eventos que responde a los clics en los títulos de las pestañas:



@click="selectedTab = tab"


La propiedad se escribirán líneas correspondientes a los títulos de las pestañas.



Es decir, si el usuario hace clic en la pestaña Reviews, selectedTabse escribirá una cadena Reviews. Si hace clic en la pestaña Make a Review, se selectedTabincluirá la línea Make a Review.



Así es como se verá ahora el código completo del componente.



Vue.component('product-tabs', {
  template: `
    <div>    
      <ul>
        <span class="tab" 
              v-for="(tab, index) in tabs" 
              @click="selectedTab = tab"
        >{{ tab }}</span>
      </ul> 
    </div>
  `,
  data() {
    return {
      tabs: ['Reviews', 'Make a Review'],
      selectedTab: 'Reviews'  //    @click
    }
  }
})


Vincular una clase a una pestaña activa



Un usuario que trabaja con una interfaz que utiliza pestañas debe saber qué pestaña está activa. Puede implementar un mecanismo similar mediante el enlace de clases a los elementos que se <span>utilizan para mostrar los nombres de las pestañas:



:class="{ activeTab: selectedTab === tab }"


Aquí está el archivo CSS que define el estilo de la clase usada aquí activeTab. Así es como se ve este estilo:



.activeTab {
  color: #16C0B0;
  text-decoration: underline;
}


Y aquí está el estilo de la clase tab:



.tab {
  margin-left: 20px;
  cursor: pointer;
}


Si explica la construcción anterior en un lenguaje simple, resulta que el estilo especificado para la clase se aplica a la pestaña activeTab, en el caso de que sea selectedTabigual tab. Dado que el selectedTabnombre de la pestaña en la que el usuario acaba de hacer clic está escrito, el estilo .activeTabse aplicará específicamente a la pestaña activa.



En otras palabras, cuando el usuario haga clic en la primera pestaña, tabestará dentro Reviews, se escribirá la misma selectedTab. Como resultado, el estilo se aplicará a la primera pestaña .activeTab.



Ahora, los títulos de las pestañas en la página se verán a continuación.





El título resaltado de la pestaña activa



Parece que todo está funcionando como se esperaba en esta etapa, por lo que podemos seguir adelante.



Trabajando en la plantilla de componentes



Ahora que podemos decirle al usuario qué pestaña es la activa, podemos seguir trabajando en el componente. Es decir, estamos hablando de finalizar su plantilla, describiendo qué se mostrará exactamente en la página cuando se active cada una de las pestañas.



Pensemos en lo que debería mostrarse al usuario si hace clic en la pestaña Reviews. Esto es, por supuesto, reseñas de productos. Por lo tanto, movamos el código para mostrar revisiones de la plantilla de componente producta la plantilla de componente product-tabs, colocando este código debajo de la construcción utilizada para mostrar los encabezados de las pestañas. Así es como se verá la plantilla de componentes ahora product-tabs:



template: `
  <div>    
    <ul>
      <span class="tab"
            :class="{ activeTab: selectedTab === tab }" 
            v-for="(tab, index) in tabs" 
            @click="selectedTab = tab"
      >{{ tab }}</span>
    </ul> 
    <div>
      <p v-if="!reviews.length">There are no reviews yet.</p>
      <ul>
        <li v-for="review in reviews">
        <p>{{ review.name }}</p>
        <p>Rating: {{ review.rating }}</p>
        <p>{{ review.review }}</p>
        </li>
      </ul>
    </div>
  </div>
`


Tenga en cuenta que nos deshicimos de la etiqueta <h2><font color="#3AC1EF">porque ya no necesitamos mostrar el título Reviewssobre la lista de reseñas. En lugar de este título, se mostrará el título de la pestaña correspondiente.



Pero mover el código de la plantilla por sí solo no es suficiente para proporcionar comentarios. La matriz reviewscuyos datos se utilizan para mostrar reseñas se almacena como parte de los datos del componente product. Necesitamos pasar esta matriz al componente product-tabsusando el mecanismo de accesorios del componente . Agreguemos lo product-tabssiguiente al objeto con las opciones utilizadas durante la creación :



props: {
  reviews: {
    type: Array,
    required: false
  }
}


Pasemos una matriz reviewsde componente producta componente product-tabsutilizando la productsiguiente construcción en la plantilla :



<product-tabs :reviews="reviews"></product-tabs>


Ahora pensemos en lo que debe mostrarse en la página si el usuario hace clic en el título de la pestaña Make a Review. Este es, por supuesto, un formulario para enviar comentarios. Con el fin de preparar el proyecto para seguir trabajando en él, transfiramos el código de conexión product-reviewdel componente de la plantilla del componente producta la plantilla product-tabs. Coloquemos el siguiente código debajo del elemento <div>utilizado para mostrar la lista de reseñas:



<div>
  <product-review @review-submitted="addReview"></product-review>
</div>


Si mira la página de la aplicación ahora, encontrará que la lista de revisiones y el formulario se muestran debajo de los encabezados de las pestañas.





Una etapa intermedia de trabajo en la página



En este caso, los clics en los encabezados, aunque conducen a su selección, no afectan en modo alguno a otros elementos de la página. Además, si intenta utilizar el formulario, resulta que ha dejado de funcionar normalmente. Todas estas son consecuencias bastante esperadas de los cambios que hicimos en la aplicación. Sigamos trabajando y llevemos nuestro proyecto a un estado funcional.



Visualización condicional de elementos de página



Ahora que hemos preparado los elementos principales de la plantilla de componentes product-tabs, es hora de crear un sistema que le permitirá mostrar diferentes elementos de página según el título de la pestaña en el que el usuario hizo clic.



Los datos del componente ya tienen una propiedad selectedTab. Podemos usarlo en una directiva v-showpara renderizar condicionalmente lo que pertenece a cada una de las pestañas.



Entonces, a la etiqueta que <div>contiene el código para generar la lista de reseñas, podemos agregar la siguiente construcción:



v-show="selectedTab === 'Reviews'"


Gracias a ella, la lista de reseñas se mostrará cuando la pestaña esté activa Reviews.



De manera similar, agregaremos lo siguiente a la etiqueta que <div>contiene el código de conexión del componente product-review:



v-show="selectedTab === 'Make a Review'"


Esto hará que el formulario se muestre solo cuando la pestaña esté activa Make a Review.



Así es como se verá la plantilla de componentes ahora product-tabs:



template: `
  <div>    
    <ul>
      <span class="tab"
            :class="{ activeTab: selectedTab === tab }" 
            v-for="(tab, index) in tabs" 
            @click="selectedTab = tab"
      >{{ tab }}</span>
    </ul> 
    <div v-show="selectedTab === 'Reviews'">
      <p v-if="!reviews.length">There are no reviews yet.</p>
      <ul>
        <li v-for="review in reviews">
        <p>{{ review.name }}</p>
        <p>Rating: {{ review.rating }}</p>
        <p>{{ review.review }}</p>
        </li>
      </ul>
    </div>
    <div v-show="selectedTab === 'Make a Review'">
      <product-review @review-submitted="addReview"></product-review>
    </div>
  </div>
`


Si miras la página y haces clic en las pestañas, puedes asegurarte de que el mecanismo que hemos creado funciona correctamente.





Al hacer clic en las pestañas, se ocultan algunos elementos y se muestran otros.



Enviar comentarios a través de un formulario aún no funciona. Investiguemos el problema y solucionémoslo.



Resolver el problema al enviar comentarios



Si observa la consola de herramientas para desarrolladores del navegador ahora, verá una advertencia.





Advertencia de consola



Aparentemente, el sistema no puede detectar el métodoaddReview. ¿Lo que le sucedió?



Para responder a esta pregunta, recuerde queaddReviewes un método que se declara en un componenteproduct. Debe llamarse si el componenteproduct-review(y este es un componente hijo del componenteproduct) genera un eventoreview-submitted:



<product-review @review-submitted="addReview"></product-review>


Así es como funcionaba todo antes de transferir el fragmento de código anterior al componente product-tabs. Y ahora un componente productes un componente hijo product-tabs, y product-reviewahora no es un "hijo", un componente product, sino su "nieto".



Nuestro código ahora está diseñado para interactuar product-reviewcon el componente principal. Pero ahora ya no es un componente product. Como resultado, resulta que para que el formulario funcione correctamente, necesitamos refactorizar el código del proyecto.



Refactorización del código del proyecto



Para asegurar la comunicación de los componentes del nieto con sus "abuelos", o para establecer la comunicación entre componentes del mismo nivel, a menudo se usa un mecanismo llamado bus de eventos globales.



El Global Event Bus es un canal de comunicación que se puede utilizar para transferir información entre componentes. Y es, de hecho, solo una instancia de Vue que se crea sin pasarle un objeto con opciones. Creemos un bus de eventos:



var eventBus = new Vue()


Este código irá al nivel superior del archivo main.js.



Puede que le resulte más fácil comprender este concepto si piensa en el autobús de eventos como un autobús. Sus pasajeros son datos que unos componentes envían a otros. En nuestro caso, estamos hablando de transferir información sobre eventos generados por otros componentes a un componente. Es decir, nuestro "bus" viajará de un componente product-reviewa otro product, llevando la información que el formulario ha sido enviado y entregando los datos del formulario de product-reviewa product.



Ahora, en el componente product-review, en el método onSubmit, hay una línea como esta:



this.$emit('review-submitted', productReview)


Reemplazémoslo con el siguiente, usando en su eventBuslugar this:



eventBus.$emit('review-submitted', productReview)


Después de eso, ya no es necesario que escuche el evento del review-submittedcomponente product-review. Por lo tanto, cambiaremos el código de este componente en la plantilla del componente product-tabsa lo siguiente:



<product-review></product-review>


El productmétodo ahora se puede eliminar del componente addReview. En su lugar, usaremos la siguiente construcción:



eventBus.$on('review-submitted', productReview => {
  this.reviews.push(productReview)
})


Hablaremos a continuación sobre cómo usarlo en un componente, pero por ahora, describiremos en pocas palabras lo que sucede en él. Esta construcción indica que cuando eventBusgenera un evento review-submitted, debe tomar los datos pasados ​​en este evento (es decir, - productReview) y colocarlos en la matriz de reviewscomponentes product. De hecho, esto es muy similar a lo que se ha hecho hasta ahora en un método addReviewque ya no necesitamos. Tenga en cuenta que el fragmento de código anterior utiliza una función de flecha. Este momento es digno de una cobertura más detallada.



Razones para usar una función de flecha



Aquí estamos usando la sintaxis de la función de flecha que se introdujo en ES6. El punto es que el contexto de la función de flecha está vinculado al contexto principal. Es decir, cuando dentro de esta función usamos una palabra clave this, es equivalente a la palabra clave thisque corresponde a la entidad que contiene la función flecha.



Este código se puede reescribir sin usar las funciones de flecha, pero luego debe organizar el enlace this:



eventBus.$on('review-submitted', function (productReview) {
  this.reviews.push(productReview)
}.bind(this))


Completando el proyecto



Casi hemos alcanzado nuestra meta. Todo lo que queda por hacer es encontrar un lugar para el fragmento de código que proporcione una respuesta al evento review-submitted. Una productfunción puede convertirse en un lugar en un componente mounted:



mounted() {
  eventBus.$on('review-submitted', productReview => {
    this.reviews.push(productReview)
  })
}


¿Qué es esta función? Este es un enlace de ciclo de vida que se llama una vez después de que el componente se monta en el DOM. Ahora, después de productmontar el componente , esperará a que ocurran los eventos review-submitted. Después de que se genera un evento de este tipo, lo que se pasa en este evento se agregará a los datos del componente, es decir, - productReview.



Si ahora intenta dejar una reseña sobre el producto mediante el formulario, resulta que esta reseña se muestra donde debería estar.





La forma funciona como debería



El bus de eventos no es la mejor solución para comunicar componentes



Aunque el bus de eventos se utiliza a menudo, y aunque puede encontrarlo en varios proyectos, tenga en cuenta que está lejos de ser la mejor solución al problema de conectar componentes de la aplicación.



A medida que la aplicación crece, un sistema de gestión de estado basado en Vuex puede resultar muy útil . Es una biblioteca y un patrón de administración del estado de la aplicación.



Taller



Agregue pestañas Shippingy al proyecto Details, que, respectivamente, muestran el costo de entrega de las compras y la información sobre los bienes.





Salir



Esto es lo que aprendió en este tutorial:



  • Puede utilizar herramientas de representación condicional para organizar el mecanismo de las pestañas.
  • , Vue, .
  • — . . — Vuex.


Esperamos que después de tomar este curso de Vue, haya aprendido lo que quería y esté listo para aprender muchas más cosas nuevas e interesantes sobre este marco.



Si acaba de completar este curso, comparta sus impresiones.



Lección 1 para principiantes de Vue.js : instancia Vue

Vue.js para principiantes, lección 2: atributos de enlace

Lección 3 para principiantes de Vue.js: renderizado condicional

Lección 4 para principiantes de Vue.js: renderizado de listas

Vue .js para principiantes Lección 5: Manejo de eventos

Vue.js para principiantes, Lección 6: Clases y estilos vinculantes

Vue.js para principiantes, Lección 7: Propiedades calculadas

Vue.js para principiantes, lección 8: Componentes

Vue.js para principiantes, lección 9: eventos personalizados

Vue.js para principiantes, lección 10: Formularios






All Articles