Navegación y Visualización de Artesanos en Vue.js
Clase 21 de 27 • Curso Avanzado de Vue.js 2
Ya casi hemos terminado la vista de Profile
. Para terminar el (gran) componente de MainBlock
nos queda una última cosa y ya podríamos pasar al componente de los artesanos.
Lo que vamos a implementar ahora es que cuando hagamos click en un héroe, tanto si haces click en el rostro del hero en TopHeroe
o en HeroIco
(primera columna de la tabla), cambiemos a la ruta de detalle del héroe, es decir, a la vista Hero
.
Para refrescar contenidos, estas son las rutas que habíamos definido:
const routerOptions = [ { path: '/', name: 'Home' }, { path: '/region/:region/profile/:battleTag', name: 'Profile' }, { path: '/region/:region/profile/:battleTag/hero/:heroId', name: 'Hero' }, { path: '/about', name: 'About' }, { path: '/error', name: 'Error' }, { path: '*', redirect: { name: 'Home' } } ]
Queremos que al hacer click en la "cara" de un hero, la app cambie de ruta y cargue la vista de Hero. Tenemos dos casos en los que controlar este comportamiento, por lo tanto, para reutilizar, vamos a crear un mixin que, más tarde, lo usaremos en múltiples componentes.
Creamos nuestro mixin en la carpeta /mixins
. Como se trata de una función (method) que va a navegar a la vista del hero, podemos llamarle goToHero.js
. Va a tener este contenido:
export default { methods: { /** * Go to hero Id * @param heroId {String | Number} */ goToHero (heroId) { // Sacar los datos de la URL const { region, battleTag } = this.$route.params // Navegar a la ruta "Hero" this.$router.push({ name: 'Hero', params: { region, battleTag, heroId } }) } } }
Para poder usarlo, primero lo importamos y después lo declaramos en el componente. Vamos a usarlo en dos componentes:
-
TopHero.vue
<script> + import goToHero from '@/mixins/goToHero' import { formatNumber } from '@/filters/numeral' export default { name: 'TopHero', + mixins: [goToHero], filters: { formatNumber }, props: { hero: { type: Object, required: true } }, computed: { heroClass () { const gender = this.hero.gender === 0 ? 'male' : 'female' return `hero-${this.hero.classSlug} ${gender}` } } } </script>
- <div class="hero-portrait-wrapper mb-5 mb-sm-0"> + <div class="hero-portrait-wrapper mb-5 mb-sm-0" @click="goToHero(hero.id)">
-
HeroIco.vue
<script> + import goToHero from '@/mixins/goToHero' export default { name: 'HeroIco', + mixins: [goToHero], props: { hero: { required: true, type: Object } }, computed: { heroClassImg () { const gender = this.hero.gender === 1 ? 'female' : 'male' const hardcore = this.hero.hardcore ? 'border-danger' : '' return `hero-${this.hero.classSlug} ${gender} ${hardcore}` } } } </script>
- <div class="hero-ico d-flex align-items-center"> + <div class="hero-ico d-flex align-items-center" @click="goToHero(hero.id)">
Si pruebas a hacer click en estos elementos, debería funcionarte y cambiarte de vista.
Sin embargo, tenemos un problema que tal vez hayas visto. La interfaz no es muy intuitiva, pues cuando pasamos el ratón por encima, el cursor no cambia a pointer
👆.
Podemos solucionarlo con una clase de CSS. Vamos a crear una clase CSS en nuestro fichero global de estilos CSS /assets/css/main.styl
.
Al final de este fichero, agregamos lo siguiente:
// --------------------- // Utils // --------------------- .hover-cursor-pointer &:hover cursor pointer
Y aplicamos los cambios, es decir, agregamos la clase CSS .hover-cursor-pointer
a los dos componentes:
TopHero.vue
- <div class="hero-portrait-wrapper mb-5 mb-sm-0" @click="goToHero(hero.id)"> + <div class="hero-portrait-wrapper mb-5 mb-sm-0 hover-cursor-pointer" @click="goToHero(hero.id)">
HeroIco.vue
- <div class="hero-ico d-flex align-items-center" @click="goToHero(hero.id)"> + <div class="hero-ico d-flex align-items-center hover-cursor-pointer" @click="goToHero(hero.id)">
¡Estupendo! El componente MainBlock ya está listo (¡Por fin!). Podemos pasar a desarrollar el componente de los artesanos.
Y en cuanto terminemos con los artesanos podríamos dar por finalizada esta vista, la vista Profile.
ArtisansBlock
Este componente es también bastante sencillo: va a mostrar datos básicos de los tres artesanos (herrero, joyero y mística).
> 📗 Más info de artesanos y artesanía en Diablo III: https://eu.diablo3.com/es/game/guide/items/crafting
Para empezar, vamos a crear todos los archivos necesarios. Dentro de la carpeta /views/Profile
, al mismo nivel que /MainBlock
, creamos una nueva llamada /ArtisansBlock
. Dentro de esta creamos Index.vue
y ArtisanItem.vue
.
Ahora toca actualizar el componente vista /views/Profile/Index.vue
, y agregarle el nuevo componente de ArtisansBlock que acabamos de crear.
/views/Profile/Index.vue
En el bloque de JavaScript, importamos y habilitamos el componente (as usual):
// /views/Profile/Index.vue import setError from '@/mixins/setError' import { getApiAccount } from '@/api/search' import BaseLoading from '@/components/BaseLoading' import MainBlock from './MainBlock/Index' // Importar import ArtisansBlock from './ArtisansBlock/Index' export default { name: 'ProfileView', mixins: [ setError ], components: { BaseLoading, ArtisansBlock, // Habilitar componente MainBlock } // ... }
Usamos el componente. El HTML es el siguiente:
<template> <div class="profile-view"> <BaseLoading v-if="isLoading"/> <template v-if="profileData !== null"> <MainBlock :profile-data="profileData"/> <ArtisansBlock :artisans-data="artisansData" /> </template> </div> </template>
El componente está recibiendo artisansData
como prop. Vamos a crear una computed property que genere el objeto que necesitamos, los datos de los tres artesanos y en los dos modos (normal y hardcore):
computed: { artisansData () { return { blacksmith: this.profileData.blacksmith, blacksmithHardcore: this.profileData.blacksmithHardcore, jeweler: this.profileData.jeweler, jewelerHardcore: this.profileData.jewelerHardcore, mystic: this.profileData.mystic, mysticHardcore: this.profileData.mysticHardcore } } },
El código completo del componente /Profile/Index.vue
es este:
<template> <div class="profile-view"> <BaseLoading v-if="isLoading"/> <template v-if="profileData !== null"> <MainBlock :profile-data="profileData"/> <ArtisansBlock :artisans-data="artisansData" /> </template> </div> </template> <script> import setError from '@/mixins/setError' import { getApiAccount } from '@/api/search' import BaseLoading from '@/components/BaseLoading' import MainBlock from './MainBlock/Index' import ArtisansBlock from './ArtisansBlock/Index' export default { name: 'ProfileView', mixins: [ setError ], components: { BaseLoading, MainBlock, ArtisansBlock }, data () { return { isLoading: false, profileData: null } }, computed: { artisansData () { return { blacksmith: this.profileData.blacksmith, blacksmithHardcore: this.profileData.blacksmithHardcore, jeweler: this.profileData.jeweler, jewelerHardcore: this.profileData.jewelerHardcore, mystic: this.profileData.mystic, mysticHardcore: this.profileData.mysticHardcore } } }, created () { this.isLoading = true const { region, battleTag: account } = this.$route.params this.fetchData(region, account) }, methods: { /** * Obtener los datos de la API * Guardarlos en 'profileData' * @param region {String} * @param account {String} */ fetchData (region, account) { getApiAccount({ region, account }) .then(({ data }) => { this.profileData = data }) .catch((err) => { this.profileData = null const errObj = { routeParams: this.$route.params, message: err.message } if (err.response) { errObj.data = err.response.data errObj.status = err.response.status } this.setApiErr(errObj) this.$router.push({ name: 'Error' }) }) .finally(() => { this.isLoading = false }) } } } </script>
Ahora, para probar que funcione, podemos darle este contenido al componente de ArtisansBlock/Index.vue
<template> <div> <h1>Artesanos</h1> </div> </template> <script> export default { name: 'ArtisansBlock' } </script>
Se debería ver así:
Ahora vamos a entrar al detalle en el componente de artesanos (/ArtisansBlock/Index.vue
). Lo primero es definir las props que va a recibir:
<script> import ArtisanItem from './ArtisanItem' export default { name: 'ArtisansBlock', components: { ArtisanItem }, props: { artisansData: { type: Object, required: true } } } </script>
Además, necesitamos crear un array que agrupe los datos de los artesanos. ¿Adivinas cómo lo vamos a hacer? Una computed property, ¡correcto!
computed: { artisansInfo () { return [ { name: 'blacksmith', icon: 'hammer', emoji: '⚒', color: '#ffb74d', normal: this.artisansData.blacksmith, hardcore: this.artisansData.blacksmithHardcore }, { name: 'jeweler', icon: 'gem', emoji: '💎', color: '#4dd0e1', normal: this.artisansData.jeweler, hardcore: this.artisansData.jewelerHardcore }, { name: 'mystic', icon: 'hat-wizard', emoji: '🔮', color: '#ba68c8', normal: this.artisansData.mystic, hardcore: this.artisansData.mysticHardcore } ] } }
> 😎 Truqui: si estás en MacOS, puedes pulsar la combinación de teclas Control
+ Cmd
+ Tecla Espacio
para sacar un selector de emojis. 👇
> ¿Eres usuario de Windows? Con la combinación Win
+ .
puedes hacer lo mismo 😉
Hemos creado un array con tres items. Cada item corresponde a un artesano. Cada uno tiene los dos datos de su tipo que ha recibido de las props.
Además, le hemos dado un color, un emoji y un ícono (icon, que hace referencia a FontAwesome, la librería de íconos que ya conoces)
Ahora el HTML:
<template> <div class="artisan-list"> <hr class="bg-light my-5"> <h3 class="font-diablo mb-4">Artisans</h3> <b-row> <b-col lg="4" v-for="artisan in artisansInfo" :key="artisan.name"> <ArtisanItem :artisan="artisan"/> </b-col> </b-row> </div> </template>
El código completo de /ArtisansBlock/Index.vue
es el siguiente:
<template> <div class="artisan-list"> <hr class="bg-light my-5"> <h3 class="font-diablo mb-4">Artisans</h3> <b-row> <b-col lg="4" v-for="artisan in artisansInfo" :key="artisan.name"> <ArtisanItem :artisan="artisan"/> </b-col> </b-row> </div> </template> <script> import ArtisanItem from './ArtisanItem' export default { name: 'ArtisansBlock', components: { ArtisanItem }, props: { artisansData: { type: Object, required: true } }, computed: { artisansInfo () { return [ { name: 'blacksmith', icon: 'hammer', emoji: '⚒️', color: '#ffb74d', normal: this.artisansData.blacksmith, hardcore: this.artisansData.blacksmithHardcore }, { name: 'jeweler', icon: 'gem', emoji: '💎', color: '#4dd0e1', normal: this.artisansData.jeweler, hardcore: this.artisansData.jewelerHardcore }, { name: 'mystic', icon: 'hat-wizard', emoji: '🔮', color: '#ba68c8', normal: this.artisansData.mystic, hardcore: this.artisansData.mysticHardcore } ] } } } </script>
El objeto que hemos generado con la computed property, que es el que se va a usar para iterar y generar los tres componentes de artesanos dinámicamente, tiene este contenido:
[ { "name":"blacksmith", "icon":"hammer", "emoji":"⚒️", "color":"#ffb74d", "normal":{ "slug":"blacksmith", "level":12 }, "hardcore":{ "slug":"blacksmith", "level":0 } }, { "name":"jeweler", "icon":"gem", "emoji":"💎", "color":"#4dd0e1", "normal":{ "slug":"jeweler", "level":12 }, "hardcore":{ "slug":"jeweler", "level":0 } }, { "name":"mystic", "icon":"hat-wizard", "emoji":"🔮", "color":"#ba68c8", "normal":{ "slug":"mystic", "level":12 }, "hardcore":{ "slug":"mystic", "level":0 } } ]
ArtisanItem
Ahora nos queda terminar el componente /ArtisansBlock/ArtisanItem.vue
. Vamos a dejar definidos los bloques de JavaScript y de CSS:
<script> export default { name: 'ArtisanItem', props: { artisan: { required: true, type: Object } } } </script> <style lang="stylus" scoped> .artisan-item .icon width 80px height 80px background-color #404850 </style>
El HTML lo explicamos un poquito más, aunque es muy simple, ya verás. ¿Recuerdas que hablamos de los emojis anteriormente? En esta ocasión le estamos pasando, a través de las props, ¡emojis al componente! 😏
Usarlo es tan sencillo como esto:
- Opción uno
<template> <div class="artisan-item d-flex bg-dark p-3 mb-2 rounded"> <!-- Bloque Ícono / Emoji --> <div class="icon d-flex justify-content-center align-items-center rounded-circle mr-2"> <!-- ¡Usar Emoji! --> <span class="display-4">{{ artisan.emoji}}</span> </div> <!-- Bloque Contenido --> <div class="content"> <h5 class="font-weight-bold text-capitalize"> {{ artisan.name }} </h5> <!-- Si hay artesano normal --> <p v-if="artisan.normal.level" class="m-0 font-weight-normal"> Level {{artisan.normal.level}} (normal) </p> <!-- Si hay artesano hardcore --> <p v-if="artisan.hardcore.level" class="mb-0 font-weight-normal text-muted"> Level {{ artisan.hardcore.level }} <span class="text-danger">(hardcore)</span> </p> </div> </div> </template>
Esto es así porque puedes jugar la historia en modo normal y no tener los artesanos del modo hardcore.
Si abres el navegador, tu app se debería ver maravillosamente así:
- Opción dos
<!-- Bloque Ícono / Emoji --> <div class="icon d-flex justify-content-center align-items-center rounded-circle mr-2"> <!-- <span class="display-4">{{ artisan.emoji}}</span> --> <!-- Usar Ícono --> <font-awesome-icon :icon="artisan.icon" class="fa-2x" :style="{color: artisan.color}"/> </div>
Con esta opción se vería así:
La decisión es tuya, tú eliges con que opción te quedas. En mi caso voy a usar la opción uno, la de los emojis.
¡Maravilloso! 🎉 Hemos terminado con la vista de Profile. Ahora debemos continuar con la vista de detalle del héroe, donde cargaremos los items y los skills de nuestro personaje.
Lo vemos en el siguiente tema.