Implementación de Inventario de Objetos de Personaje en Vue.js

Clase 24 de 27Curso Avanzado de Vue.js 2

Actualmente nuestra vista de Hero tiene un bloque de cabecera, otro de atributos (stats) del personaje y el último, con las habilidades.

Lo siguiente que vamos a hacer es mostrar los items del personaje. La API de items nos devuelve un objeto que tiene este formato:

{ "mainHand": { "id":"P6_Unique_Scythe1H_04", "name":"Jesseth Skullscythe", "icon":"p6_unique_scythe1h_04_demonhunter_male", "displayColor":"green", "tooltipParams":"/item/jesseth-skullscythe-P6_Unique_Scythe1H_04", "requiredLevel":70, "itemLevel":1, "stackSizeMax":0, "accountBound":true, "flavorText":"This assembly of sharpened bones will make you a true artist in combat.", "typeName":"Ancient Set Scythe", "type": { "twoHanded":false, "id":"scythe1h" }, "armor":0, "damage":"1682-2259 Damage\n1.38 Attacks per Second", "dps":"2,715.3", "attacksPerSecond":1.3779999, "minDamage":1682, "maxDamage":2259, "slots":"mainHand", "attributes": { "primary": [ "+1433-1798 Damage", "+998 Intelligence", "+899 Vitality", "Increases Attack Speed by 6%" ], "secondary": [ "+19 Maximum Essence", "Monster kills grant +254 experience." ] }, "openSockets":0, "gems": [ { "item": { "id":"x1_Emerald_10", "slug":"flawless-royal-emerald", "name":"Flawless Royal Emerald", "icon":"x1_emerald_10_demonhunter_male", "path":"item/flawless-royal-emerald-x1_Emerald_10" }, "attributes": [ "Critical Hit Damage Increased by 130.0%" ], "isGem":true, "isJewel":false } ], "seasonRequiredToDrop":-1, "isSeasonRequiredToDrop":false } }

Este modelo de datos corresponde al slot del arma (mano principal o main hand). Tenemos 13 tipos de objetos (items) distintos, que son los siguientes: cabeza (head), cuello (neck), pecho (torso), hombros (shoulders), piernas (legs), cintura (waist), manos (hands), brazales (bracers), pies (feet), dedo izquierdo (leftFinger), dedo derecho (rightFinger), mano dominante (mainHand) y mano secundaria (offHand).

> 📗 Aquí tienes más información acerca de los objetos del juego: https://eu.diablo3.com/es/item/

Según la documentación, para obtener la imagen de un objeto, tenemos que usar esta url http://media.blizzard.com/d3/icons/items/large/ + la propiedad icon + .png.

Si quisiéramos obtener la imagen del arma principal (mainHand) tendríamos que poner lo siguiente:
http://media.blizzard.com/d3/icons/items/large/p6_unique_scythe1h_04_demonhunter_male.png

Que se corresponde con esta imagen:
mainHand

Como has podido ver, cada item tiene una gran cantidad de propiedades que podríamos usar para crear nuestra app, pero para evitar complejidad y no alargar mucho el tema, no vamos a usar todas.
Aparte de la propiedad icon, vamos a usar name, displayColor (para saber la calidad del objeto) y gems (para saber si tiene gemas o joyas).

La estructura HTML que vamos a seguir para mostrar los objetos de nuestro personaje en pantalla es la misma que se ve en este boceto, en el bloque de items:

wireframe

Un pequeño problema que podríamos tener es que nuestro personaje no tenga objetos equipados en todos los huecos posibles. Por ejemplo, puede darse el caso de que el personaje no tenga equipado el objeto de legs. Si esto pasara, se nos descuadraría la cuadrícula o se mostraría vacía. Es nuestro deber, como developers, controlar estos posibles casos.


La lista de objetos de nuestro personaje descansa en la variable items del componente /views/Hero/Index.vue. Tenemos que crear el componente de HeroItems para poder usarlo.

Dentro del directorio /HeroItems creamos nuestro componente principal Index.vue y le damos este contenido:

<template> <div> <h1>HeroItems</h1> </div> </template> <script> export default { name: 'HeroItems' } </script>

Ahora, desde el componente principal de la vista Hero, es decir, en /views/Hero/Index.vue hacemos el ritual de siempre: traer, declarar y utilizar el componente:

// Traer import HeroItems from './HeroItems/Index'
// Declarar components: { BaseLoading, HeroDetailHeader, HeroAttributes, HeroSkills, HeroItems }

Por último, lo usamos:

<template> <div class="hero-view"> <BaseLoading v-if="isLoadingHero"/> <HeroDetailHeader v-if="hero" :detail="detailHeader"/> <b-row> <b-col md="12" lg="8" order-lg="2"> <BaseLoading v-if="isLoadingItems"/> <!-- Componente de Items del personaje --> <HeroItems v-if="items" :items="items"/> </b-col> <b-col md="12" lg="4" order-lg="1"> <template v-if="hero"> <HeroAttributes :attributes="detailStats"/> <HeroSkills :skills="hero.skills"/> </template> </b-col> </b-row> </div> </template>

Debería verse así: pv-1


HeroItem

Vamos a crear el componente correspondiente a un objeto, que luego reutilizaremos para pintar todos los objetos. A nivel de API, todos los objetos del personaje son iguales, por lo que no tenemos que hacer distinciones.

Empezamos creando el fichero /Hero/HeroItems/ItemDetail.vue. Continuamos con la definición básica de nuestro componente:

<template> <div> <h1>Item</h1> </div> </template> <script> export default { name: 'ItemDetail', props: { item: { type: Object || undefined, required: true } } } </script> <style lang="stylus"> </style>

Lo siguiente que vamos a definir son las clases CSS que necesitamos para este componente. Para ello, dentro de style agregamos lo siguiente:

.d3-icon-item min-height 100px // El borde de la caja va determinar la rareza del objeto, segun el color que tenga border-top-style solid border-top-width 4px &.item-none border-color transparent &.item-green border-color #8bc34a &.item-orange border-color #ff9800 &.item-yellow border-color #ffeb3b &.item-blue border-color #03a9f4 &.item-white border-color #a0aab5

Estas clases las vamos a usar para determinar la calidad de nuestro objeto. Recuerda que el objeto puede ser blanco (normal), azul (mágico), amarillo (raro), verde (de conjunto) o naranja (legendario).

En el HTML vamos a poner lo siguiente:

<template> <!-- Clase que determina el color --> <div class="text-center bg-dark h-100 pt-3 d3-icon-item" :class="itemClassColor"> <div class="d-flex flex-column justify-content-between h-100"> <!-- Si el item tiene ID, es que tenemos la información --> <!-- Es decir, que tiene un objeto equipado. --> <template v-if="item.id"> <div> <div v-if="item" class="item-image"> <!-- Nombre del objeto --> <p class="text-muted">{{ item.name }}</p> <!-- Imagen correspondiente al objeto --> <img :src="itemUrl" :alt="item.slotName + ' ' + item.name "> </div> </div> <div> <!-- No todos los objetos tienen gemas --> <!-- Por lo tanto, si el objeto tiene gemas engarzadas --> <template v-if="itemHasGems"> <!-- Puede ser Gema o Joya --> <small>{{ gemOrJewel }}:</small> <ul class="list-inline"> <!-- Un objeto puede tener varias gemas --> <li v-for="(gem, index) in item.gems" :key="'gem-'+index" class="list-inline-item"> <!-- Componente gema --> <ItemDetailGem :gem="gem"/> </li> </ul> </template> </div> </template> <!-- En caso de que no tenga el objeto equipado --> <p v-else> <!-- Mostramos el nombre del slot y dejamos el contenido vacío --> <b-badge class="text-dark"> {{item.slotName}} </b-badge> </p> </div> </div> </template>

Con los comentarios queda bastante claro que hace cada cosa.
Ahora necesitamos crear las computed properties que estamos usando en el template. Agregamos, en nuestro bloque de JavaScript, las siguientes funciones (computadas):

computed: { // Resuelve la URL de la imagen itemUrl () { const host = 'http://media.blizzard.com/d3/icons/items/large/' return `${host}${this.item.icon}.png` }, // Comprueba si el item tiene gemas itemHasGems () { return Object.prototype.hasOwnProperty.call(this.item, 'gems') }, // Si tiene gemas, comprueba si es Gema o Joya // Puede haber varias Gemas. Solo puede haber una Joya. No puede haber joyas y gemas mezcladas gemOrJewel () { return this.item.gems[0].isGem ? 'Gems' : 'Jewel' }, // Clase CSS para saber la rareza itemClassColor () { if (Object.prototype.hasOwnProperty.call(this.item, 'displayColor')) { return `item-${this.item.displayColor}` } // Si no tiene color (es que no hay objeto equipado) return 'item-none' } }

El código completo se ve así:

<template> <!-- Clase CSS que determina el color --> <div class="text-center bg-dark h-100 pt-3 d3-icon-item" :class="itemClassColor"> <div class="d-flex flex-column justify-content-between h-100"> <!-- Si el item tiene `id`, es que tenemos la información --> <!-- Es decir, que tiene un objeto equipado. --> <template v-if="item.id"> <div> <div v-if="item" class="item-image"> <!-- Nombre del objeto --> <p class="text-muted">{{ item.name }}</p> <!-- Imagen correspondiente al objeto --> <img :src="itemUrl" :alt="item.slotName + ' ' + item.name "> </div> </div> <div> <!-- No todos los objetos tienen gemas --> <!-- Por lo tanto, si el objeto tiene gemas engarzadas --> <template v-if="itemHasGems"> <!-- Puede ser Gema o Joya --> <small>{{ gemOrJewel }}:</small> <ul class="list-inline"> <!-- Un objeto puede tener varias gemas --> <li v-for="(gem, index) in item.gems" :key="'gem-'+index" class="list-inline-item"> <!-- Componente gema --> <ItemDetailGem :gem="gem"/> </li> </ul> </template> </div> </template> <!-- En caso de que no tenga el objeto equipado --> <p v-else> <!-- Mostramos el nombre del slot y dejamos el contenido vacío --> <b-badge class="text-dark"> {{item.slotName}}</b-badge> </p> </div> </div> </template> <script> export default { name: 'ItemDetail', props: { item: { type: Object || undefined, required: true } }, computed: { itemUrl () { const host = 'http://media.blizzard.com/d3/icons/items/large/' return `${host}${this.item.icon}.png` }, itemHasGems () { return Object.prototype.hasOwnProperty.call(this.item, 'gems') }, gemOrJewel () { return this.item.gems[0].isGem ? 'Gems' : 'Jewel' }, itemClassColor () { if (Object.prototype.hasOwnProperty.call(this.item, 'displayColor')) { return `item-${this.item.displayColor}` } return 'item-none' } } } </script> <style lang="stylus"> .d3-icon-item min-height 100px border-top-style solid border-top-width 4px &.item-none border-color transparent &.item-green border-color #8bc34a &.item-orange border-color #ff9800 &.item-yellow border-color #ffeb3b &.item-blue border-color #03a9f4 &.item-white border-color #a0aab5 </style>

Para terminar, vamos a crear el componente de la gema (o joya). Creamos un nuevo archivo ItemDetailGem.vue al mismo nivel de ItemDetail.vue, es decir, dentro de /HeroItems.

El código de este componente es muy simple:

<template> <img :src="gemUrl" :alt="gem.item.name" :title="gem.item.name"> </template> <script> export default { name: 'GemSlotItem', props: { gem: { required: true, type: Object } }, computed: { gemUrl () { // Cambio de 'large' por 'small' const host = 'http://media.blizzard.com/d3/icons/items/small/' return `${host}${this.gem.item.icon}.png` } } } </script>

El único cambio que hay aquí es que en la URL de la imagen estamos usando el tamaño pequeño (small) en vez del grande (large) que usamos en los demás items.

Tenemos que importar este componente en el componente de detalle. Para ello, desde /Hero/HeroItems/ItemDetail.vue agregamos lo siguiente:

import ItemDetailGem from './ItemDetailGem'
export default { name: 'ItemDetail', components: { ItemDetailGem } // ... }

Con esto ya tenemos los componentes de items terminados. Queda llamarlos desde la vista principal y ¡💥! Deberían aparecer todos.

Desde /Hero/HeroItems/Index.vue, cargamos el componente de ItemDetail, lo declaramos y los utilizamos:

<template> <section class="hero-items mb-5"> <h2 class="font-diablo">Items</h2> <hr class="bg-white"> <!-- Grid de 3 columnas. Mostramos solo una centrada --> <b-row> <b-col cols="4" offset="4"> <ItemDetail :item="itemsData.head"/> </b-col> </b-row> <hr> <!-- Grid de 3 columnas --> <b-row> <b-col> <ItemDetail :item="itemsData.shoulders"/> </b-col> <b-col> <ItemDetail :item="itemsData.torso"/> </b-col> <b-col> <ItemDetail :item="itemsData.neck"/> </b-col> </b-row> <hr> <!-- Grid de 3 columnas --> <b-row> <b-col> <ItemDetail :item="itemsData.hands"/> </b-col> <b-col> <ItemDetail :item="itemsData.waist"/> </b-col> <b-col> <ItemDetail :item="itemsData.bracers"/> </b-col> </b-row> <hr> <!-- Grid de 3 columnas --> <b-row> <b-col> <ItemDetail :item="itemsData.leftFinger"/> </b-col> <b-col> <ItemDetail :item="itemsData.legs"/> </b-col> <b-col> <ItemDetail :item="itemsData.rightFinger"/> </b-col> </b-row> <hr> <!-- Grid de 3 columnas --> <b-row> <b-col> <ItemDetail :item="itemsData.mainHand"/> </b-col> <b-col> <ItemDetail :item="itemsData.feet"/> </b-col> <b-col> <ItemDetail :item="itemsData.offHand"/> </b-col> </b-row> </section> </template> <script> import ItemDetail from './ItemDetail' // Objeto con las keys de los 'items' del personaje const defaultItems = { head: { slotName: 'head' }, shoulders: { slotName: 'Shoulders' }, torso: { slotName: 'Torso' }, neck: { slotName: 'Neck' }, hands: { slotName: 'Hands' }, waist: { slotName: 'Waist' }, bracers: { slotName: 'Bracers' }, leftFinger: { slotName: 'Left Finger' }, legs: { slotName: 'Legs' }, rightFinger: { slotName: 'Right Finger' }, mainHand: { slotName: 'Main Hand' }, feet: { slotName: 'Feet' }, offHand: { slotName: 'Off Hand' } } export default { name: 'HeroItems', components: { ItemDetail }, props: { items: { type: Object, required: true } }, computed: { itemsData () { // Fusionar objetos: // Esto lo hacemos para mostrar el hueco vacío en caso de que ese objeto no esté equipado // Si NO hay item equipado, manda el valor de 'defaultItems' correspondiente // Si hay item equipado, manda la info del item return { ...defaultItems, ...this.items } } } } </script>

Si tienes todo correcto y sin errores deberías ver algo como esto:

full-pv

En mi caso, todos los personajes que tenía en el momento de crear este escrito estaban a niveles altos y con todos los huecos de equipación completos.
Sin embargo, rebuscando por internet, he encontrado un perfil a nivel 1, con varios huecos de items vacíos, y solo una habilidad activa. Así es como se vería:

low-level

Si has llegado hasta aquí... 🎉 ¡Enhorabuena! 🎉 Ya tienes una super aplicación con datos reales de personajes Diablo III.

congrats

Esta vista de la aplicación da mucho juego, pues la información que nos devuelve la API es muy extensa. En este curso no hemos trabajado con toda la información existente, ya que se haría demasiado complejo, pero te animo a que hagas extensiones y/o mejoras.

También te recuerdo que si quieres contribuir, ya sabes que el repositorio original siempre está abierto a mejoras y sugerencias.

En la siguiente lectura veremos cómo crear directivas personalizadas (custom directives).

Ahora, desde el componente principal de la vista Hero, es decir, en /views/Hero/Index.vue hacemos el ritual de siempre: traer, declarar y utilizar el componente:// Traerimport HeroItems from './HeroItems/Index'// Declararcomponents: { BaseLoading, HeroDetailHeader, HeroAttributes, HeroSkills, HeroItems}Por último, lo usamos:Debería verse así:HeroItemVamos a crear el componente correspondiente a un objeto, que luego reutilizaremos para pintar todos los objetos. A nivel de API, todos los objetos del personaje son iguales, por lo que no tenemos que hacer distinciones.Empezamos creando el fichero /Hero/HeroItems/ItemDetail.vue. Continuamos con la definición básica de nuestro componente:Lo siguiente que vamos a definir son las clases CSS que necesitamos para este componente. Para ello, dentro de style agregamos lo siguiente:.d3-icon-item min-height 100px // El borde de la caja va determinar la rareza del objeto, segun el color que tenga border-top-style solid border-top-width 4px &.item-none border-color transparent &.item-green border-color #8bc34a &.item-orange border-color #ff9800 &.item-yellow border-color #ffeb3b &.item-blue border-color #03a9f4 &.item-white border-color #a0aab5Estas clases las vamos a usar para determinar la calidad de nuestro objeto. Recuerda que el objeto puede ser blanco (normal), azul (mágico), amarillo (raro), verde (de conjunto) o naranja (legendario).En el HTML vamos a poner lo siguiente:Con los comentarios queda bastante claro que hace cada cosa.Ahora necesitamos crear las computed properties que estamos usando en el template. Agregamos, en nuestro bloque de JavaScript, las siguientes funciones (computadas):computed: { // Resuelve la URL de la imagen itemUrl () { const host = 'http://media.blizzard.com/d3/icons/items/large/' return `${host}${this.item.icon}.png` }, // Comprueba si el item tiene gemas itemHasGems () { return Object.prototype.hasOwnProperty.call(this.item, 'gems') }, // Si tiene gemas, comprueba si es Gema o Joya // Puede haber varias Gemas. Solo puede haber una Joya. No puede haber joyas y gemas mezcladas gemOrJewel () { return this.item.gems[0].isGem ? 'Gems' : 'Jewel' }, // Clase CSS para saber la rareza itemClassColor () { if (Object.prototype.hasOwnProperty.call(this.item, 'displayColor')) { return `item-${this.item.displayColor}` } // Si no tiene color (es que no hay objeto equipado) return 'item-none' }}El código completo se ve así:Para terminar, vamos a crear el componente de la gema (o joya). Creamos un nuevo archivo ItemDetailGem.vue al mismo nivel de ItemDetail.vue, es decir, dentro de /HeroItems.El código de este componente es muy simple:El único cambio que hay aquí es que en la URL de la imagen estamos usando el tamaño pequeño (small) en vez del grande (large) que usamos en los demás items.Tenemos que importar este componente en el componente de detalle. Para ello, desde /Hero/HeroItems/ItemDetail.vue agregamos lo siguiente:import ItemDetailGem from './ItemDetailGem'export default { name: 'ItemDetail', components: { ItemDetailGem } // ...}Con esto ya tenemos los componentes de items terminados. Queda llamarlos desde la vista principal y ¡💥! Deberían aparecer todos.Desde /Hero/HeroItems/Index.vue, cargamos el componente de ItemDetail, lo declaramos y los utilizamos:Si tienes todo correcto y sin errores deberías ver algo como esto:En mi caso, todos los personajes que tenía en el momento de crear este escrito estaban a niveles altos y con todos los huecos de equipación completos.Sin embargo, rebuscando por internet, he encontrado un perfil a nivel 1, con varios huecos de items vacíos, y solo una habilidad activa. Así es como se vería:Si has llegado hasta aquí… 🎉 ¡Enhorabuena! 🎉 Ya tienes una super aplicación con datos reales de personajes Diablo III.Esta vista de la aplicación da mucho juego, pues la información que nos devuelve la API es muy extensa. En este curso no hemos trabajado con toda la información existente, ya que se haría demasiado complejo, pero te animo a que hagas extensiones y/o mejoras.También te recuerdo que si quieres contribuir, ya sabes que el repositorio original siempre está abierto a mejoras y sugerencias.En la siguiente lectura veremos cómo crear directivas personalizadas (custom directives).","url":"https://platzi.com/cursos/avanzado-vue/objetos-del-heroe/","wordCount":40,"publisher":{"@type":"Organization","name":"Platzi INC"}}]}