Vista Profile

16/27

Lectura

En esta vista vamos a trabajar con muchos componentes que a su vez tienen más componentes hijos.
Tenemos los componentes divididos en 2 grandes bloques:

  • Bloque Principal (MainBlock), que a su vez contiene:
    • Top Héroes: los tres últimos héroes del juego con los que el usuario ha jugado
    • Listado de héroes: listado restante de héroes.
    • Progreso de actos: si hemos completado la historia o campaña del juego.
    • Estadísticas generales: élites, nivel de leyenda, etc.
    • Tiempo de jugado: porcentaje de tiempo jugado por héroe.
  • Bloque Artesanos (ArtisansBlock): información de los artesanos.

En la mayoría de los casos no nos detendremos a comentar los componentes que vayamos creando, puesto que son bastante sencillos.
Reciben unas props (que serán los datos de la API) y los pintaremos en pantalla. Habrán algunos casos específicos que sí comentaremos.


Si retomamos una de las lecturas anteriores, nos habíamos quedado en que al hacer el submit del formulario en la página principal, no nos hacía el cambio de ruta porque dicha ruta no existía. Ahora la ruta ya existe y si haces el envío del formulario, debería llevarte a la vista de Profile:

profile

Los pasos a seguir son los siguientes:

  • Recuperar los datos a través de la ruta (URL): región y battleTag.
  • Llamar a la API oficial de Diablo III para que nos devuelva los datos del jugador de esa región
    • Si hay error, redirigir a la página de error y mostrar el mensaje de error
    • Si no hay error, mostrar los datos a través de los componentes que vamos a ir creando

Parámetros de Ruta

Si recuerdas, a nuestra ruta Profile la definimos de la siguiente forma:

{ path: '/region/:region/profile/:battleTag', name: 'Profile' },

En las Vue DevTools del navegador, hay una pestaña en la cual podemos ver la información de todas las rutas.
vdt-routing-2

vdt-routing-1

¿No te parece genial? Ahora, para ver los datos de la ruta (route object), no vas a tener que estar haciendo inspecciones usando console.log en los componentes. Utilizando las Developer Tools te será mucho más cómodo inspeccionar las rutas y ver su configuración.

📗 Documentación oficial del objeto enrutador (route object), con el que vamos a trabajar ahora: https://router.vuejs.org/api/#the-route-object

En la segunda imagen, en el bloque de la derecha, vemos dicho route object, con los siguientes valores:

path: "/region/eu/profile/SuperRambo-2613"
fullPath: "/region/eu/profile/SuperRambo-2613"
params: Object
  battleTag: "SuperRambo-2613"
  region: "eu"
name: "Profile"
matched: Array[1]
  0: Object
    path: "/region/:region/profile/:battleTag"

Nos vamos a fijar en el bloque params, ahí es donde están definidos los parámetros que nos interesan de nuestra ruta.
Como bien dice la documentación, desde un componente cualquiera podemos acceder a este objeto de ruta a través de this.$route.

El siguiente paso es hacer la llamada a la API del juego correspondiente pasándole los parámetros que acabamos de capturar del path de la ruta.

Para ello, en el hook created de nuestro componente, vamos a crear una función que se encargue de llamar a la API y que nos devuelva una promesa, que en caso de éxito nos devuelva los datos correspondientes y en caso de error nos devuelva un mensaje, y, si es posible, el código de error.

El endpoint al que vamos a apuntar desde nuestra web para que nos devuelva los datos, lo encontramos aquí (https://develop.battle.net/documentation/diablo-3/community-apis) en el bloque de D3 Profile API / getApiAccount:
endpoint

// views/Profile/Index.vue

export default {
  name: 'ProfileView',
  created () {
    // this.$route.params -> { region: "eu", battleTag: "SuperRambo-2613" }
    this.fetchData()
  },
  methods: {
    fetchData () {
      // Llamada API
    }
  }
}

Para hacer la llamada a la API vamos a necesitar varias cosas:

  • El token de acceso, que lo tenemos guardado en el Store de nuestra app.
  • El BattleTag del usuario.
  • La región en la cual se encuentra dicho usuario.
  • El locale o lenguaje en el que queremos que nos devuelva los datos. Esto lo dejaremos fijo, es decir, cada región tendrá asociado un locale por defecto, a través de una funcionalidad (muy sencilla) que explicaremos más abajo.

La llamada a la API de Blizzard la vamos a hacer desde el fichero llamado search.js, que hemos creado anteriormente dentro de /api. Tendrá el siguiente contenido:

// /api/search.js

// Axios para hacer la llamada HTTP
import { get } from 'axios'
// Store, donde tenemos almacenado nuestro token
import store from '../store/index'
// Útil de regiones, que nos devolverá el 'locale' por defecto correspondiente a cada región
import { locales } from '../utils/regions'

// API URL
// https://{region}.api.blizzard.com, donde {region} puede ser 'us', 'eu', 'kr' o 'tw'
const protocol = 'https://'
const host = '.api.blizzard.com/'

📗 Puedes ver la lista de locales disponibles en este enlace (https://develop.battle.net/documentation/guides/regionality-and-apis), en el apartado de Region Host List.

Continuamos, ahora sí, con el proceso de creación de nuestra función encargada de llamar a la API. La llamaremos getApiAccount, al igual que se la llama en la documentación para que haya correspondencia entre nuestras funciones y la documentación de la API.

// /api/search.js

/**
 * Returns the specified account profile.
 * GET – /d3/profile/{account}
 * Los parámetros que hemos obtenido a través de la URL
 *  - @param region {String}
 *  - @param account {String}
 * @returns {Promise}
 */
function getApiAccount ({ region, account }) {
  // Recurso de la API al que queremos acceder
  const resource = `d3/profile/${account}/`
  // API URL completa
  const API_URL = `${protocol}${region}${host}${resource}`
  // Locale
  const locale = locales[region]

  // Parámetros:
  // - Token de acceso (recuperado desde Vuex)
  // - Locale
  const params = {
    access_token: store.state.oauth.accessToken,
    locale
  }

  // Retornamos el resultado de hacer la llamada a la API, es decir, una promesa
  // que controlaremos (éxito / error) desde el componente
  return get(API_URL, { params })
}

Lo último que tenemos que hacer para poder usar esta funcionalidad desde nuestro componente Vue, es exponerla para que pueda ser importada desde fuera. Esto lo hacemos de la siguiente forma:

export {
  getApiAccount
}

El código completo de /api/search.js se vería así:

import { get } from 'axios'
import store from '../store/index'
import { locales } from '../utils/regions'

// https://{region}.api.blizzard.com, where {region} is one of us | eu | kr | tw
const protocol = 'https://'
const host = '.api.blizzard.com/'

/**
 * Returns the specified account profile.
 * GET – /d3/profile/{account}
 * @param region {String}
 * @param account {String}
 * @returns {Promise}
 */
function getApiAccount ({ region, account }) {
  const resource = `d3/profile/${account}/`
  const API_URL = `${protocol}${region}${host}${resource}`
  const locale = locales[region]

  const params = {
    access_token: store.state.oauth.accessToken,
    locale
  }

  return get(API_URL, { params })
}

export {
  getApiAccount
}

Con esto creado, ya podemos hacer la llamada a la API desde nuestro componente y controlar si sale por error o por éxito.
Volvemos al componente principal de Profile en /views/Profile/Index.vue y desde nuestro método fetchData llamamos a esta función. Recuerda que, para usarla, tienes que importarla primero.

// /Profile/Index.vue

import { getApiAccount } from '@/api/search'

export default {
  name: 'ProfileView',
  created () {
    this.fetchData()
  },
  methods: {
    fetchData () {
      // Desestructuración
			const { region, battleTag: account } = this.$route.params
      // Llamada a la API con los datos necesarios
      getApiAccount({ region, account })
        .then()
        .catch()
    }
  }
}

La llamada a getApiAccount , que recibe como parámetro un objeto con region y account como claves, nos va a devolver una promesa que puede terminar exitosamente (then) o con error (catch):

  • Éxito: guardamos los datos en una variable local al componente. Con esto ya podemos propagar los datos (props) por los componentes hijos que vayamos creando.
  • Error: guardar respuesta de error en el Store, cambiar de ruta a vista de Error y recuperar los datos.

Mixins

Lo que vamos a hacer, en caso de que nuestra llamada a la API nos devuelva un error, es guardar dicho error en el Store y hacer un cambio de ruta a la página de Error.

Esta funcionalidad de guardar datos que se mostrarán en la pág. de error, la podemos usar cada vez que llamemos a una API y nos salte un error.
Por lo tanto, en vez de duplicar código a través de varios componentes, lo que vamos a hacer es crear un mixin que contenga dicha funcionalidad. De esta forma, cuando queramos utilizar esta funcionalidad, estaremos reutilizando código en vez de duplicarlo.

📗 Documentación a mixins: https://es.vuejs.org/v2/guide/mixins.html

Para crear nuestro mixin nos vamos a la carpeta /mixins de nuestro proyecto y creamos un nuevo fichero llamado, por ejemplo, setError.js.

Este fichero va a interactuar con el Store de nuestra app, por lo que antes de continuar, necesitamos crear dicho código en el módulo Vuex correspondiente de nuestra aplicación.

Como vamos a establecer el objeto de error que nos devuelva la API, podemos crear el fichero con el nombre de error.js, dentro de nuestro directorio de Vuex: /store/modules. Deberías tener creado el fichero en esta ruta: /store/modules/error.js.

El contenido que va a tener es muy simple, una variable error que por defecto sea null y una función (mutación) que se encargue de mutar el valor de error.

// /store/modules/error.js

export default {
  namespaced: true,
  state: {
    error: null
  },
  mutations: {
    SET_ERROR (state, payload) {
      state.error = payload
    }
  }
}

Cuando tengamos un error, llamamos a SET_ERROR y le pasamos por parámetro el objeto. Cuando queramos limpiar el error, podemos pasarle null como parámetro.

Ahora queda darlo de alta en el Store de nuestra app, y para ello nos vamos una carpeta atrás /store, al fichero index.js y le añadimos el módulo que acabamos de crear.

 	import Vue from 'vue'
 	import Vuex from 'vuex'

 	import oauth from './modules/oauth'
 	import loading from './modules/loading'
+ import error from './modules/error'

 	Vue.use(Vuex)

 	export default new Vuex.Store({
   	modules: {
     	oauth,
     	loading,
+    	error
   	}
 	})

Con esto, como ya sabes, lo tenemos disponible desde cualquier parte de nuestra app. Volvamos a retomar el tema de los mixins, en concreto nuestro archivo setError.js.
Creo que el nombre del fichero te da una pista de que es lo que tenemos que hacer, ¿no?

// /mixins/setError.js

import { mapMutations } from 'vuex'

export default {
  methods: {
    ...mapMutations('error', {
      setError: 'SET_ERROR'
    }),
    /**
     * API response error.
     * @param params {Object || null} Error Object
     */
    setApiErr (params) {
      this.setError(params)
    }
  }
}

Para trabajar con las mutaciones de nuestro Store en nuestros componentes, Vuex nos ofrece un elegante método que nos permite mapear el nombre de nuestra mutación (del Store) con una función en nuestro componente.

📗 Documentación completa de mapMutations.

Para hacer uso de este método, lo primero que tenemos que hacer es importarlo desde Vuex.

Su uso es muy sencillo. En este ejemplo, mapMutations recibe 2 argumentos:

  • El primer argumento es el bloque al que hacemos referencia, en este caso, lleva por nombre error.
  • El segundo argumento es un objeto con las funciones que queremos mapear, es decir, le hemos dicho que nuestra mutación SET_ERROR, se convierta en un método de nombre setError en nuestro componente.
    De esta forma podemos usarlo como un componente local con la sintaxis de siempre this.setError().

Lo que hemos hecho es crear un mixin, es decir, código que puede ser reutilizado. Dentro de este código (que va a formar parte de un componente) estamos exponiendo un método (methods) que se llama setApiErr, que recibe un argumento por parámetro.
Lo único que hace este método es llamar a nuestra función mapeada de Vuex, que en realidad es como si estuviéramos llamando la mutación del Store SET_ERROR.

Es decir, desde un componente (en el cual tengamos importado el mixin) podemos llamar al método setApiErr(), como si fuera un método local más de nuestro componente.

Ya tenemos implementado un mixin que se comunica con el Store de nuestra aplicación. Solo nos queda usarlo.


Ahora ya tenemos nuestra funcionalidad Vuex y nuestro mixin listo para usarse. Es hora de volver a nuestra vista Profile y retomar la llamada a la API. Nos quedaría algo así:

// /views/Profile/Index.vue

// Traemos el mixin
import setError from '@/mixins/setError'
import { getApiAccount } from '@/api/search'

export default {
  name: 'ProfileView',
  // Lo damos de alta
  mixins: [
    setError
  ],
  data () {
    return {
      profileData: null
    }
  },
  created () {
    // llamada a la API
    this.fetchData()
  },
  methods: {
    fetchData () {
      const { region, battleTag: account } = this.$route.params
      getApiAccount({ region, account })
        .then(({ data }) => {
        	// Guardamos los datos en una variable local
          this.profileData = data
        })
        .catch((err) => {
          this.profileData = null
        	// Creamos el objeto error
          const errObj = {
            routeParams: this.$route.params,
            message: err.message
          }
          if (err.response) {
            errObj.data = err.response.data
            errObj.status = err.response.status
          }
        	// Hacemos uso del Mixin
	        // Guardamos el objeto en el Store
          this.setApiErr(errObj)
	        // Navegamos a la ruta de nombre 'Error'
          this.$router.push({ name: 'Error' })
        })
    }
  }
}

Si hacemos la prueba con el usuario SuperRambo#2613 y con la región EU, hacemos el envío del formulario y vamos al navegador, deberíamos ver algo como esto:

api-request
vue-data

Ya tenemos todo lo necesario para pintar nuestra pantalla de Profile. Vamos a ir creando los componentes necesarios y les iremos pasando la información necesaria en forma de props.

wireframeSegún esta imagen, vamos a dividir el contenido en 2 grandes bloques, en base a la maquetación que vamos a hacer. El bloque de artesanos va a ser un bloque (ArtisansBlock) y el resto de componentes va a ser otro bloque (MainBlock).
Dentro del bloque principal (MainBlock) vamos a posicionar nuestro contenido con CSS Grid Layout y con las clases de que nos proporciona Bootstrap.

📗 CSS Grid está fuera del alcance de este curso, pero como buen front-end developer deberías tener conocimientos para saber cómo funciona y poder entenderlo.
Si no conoces Grid, esta guía te puede ser de ayuda: https://css-tricks.com/snippets/css/complete-guide-grid/

Al navegar a la ruta Profile estamos haciendo una llamada a una API. Sería lógico mostrar un mensaje de ‘Loading’ mientras obtenemos los datos.
Seguramente recuerdes que tenemos un componente Loading que nos encaja a la perfección en este momento. Vamos a traerlo y a usarlo.

Lo primero que tenemos que hacer es importarlo desde /components. Lo segundo es darlo de alta dentro del bloque components de nuestro componente vista Profile. Por último, solo nos queda usarlo.

import BaseLoading from '@/components/BaseLoading'
components: { BaseLoading }
<BaseLoading/>

Para controlar el componente BaseLoading vamos a crear una nueva variable llamada isLoading dentro de nuestras variables. En el hook created vamos a igualar esta variable a true y cuando la llamada a la API termine, la igualaremos a false.
Veamos como queda el código completo hasta ahora.

<template>
  <div>
    <BaseLoading v-if="isLoading"/>
    <h1>Profile View</h1>
  </div>
</template>

<script>
import BaseLoading from '@/components/BaseLoading'
import setError from '@/mixins/setError'
import { getApiAccount } from '@/api/search'

export default {
  name: 'ProfileView',
  mixins: [
    setError
  ],
  components: { BaseLoading },
  data () {
    return {
      isLoading: false,
      profileData: null
    }
  },
  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'
     */
    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>

Ya tenemos el loading funcionando. Es hora de crear los componentes que muestren por pantalla datos reales de la API. ¡Lo veremos en la siguiente lectura!

Aportes 7

Preguntas 2

Ordenar por:

Los aportes, preguntas y respuestas son vitales para aprender en comunidad. Regístrate o inicia sesión para participar.

En mi caso tuve que hacer un pequeño workaround donde tengo que mandar el hash a la función getApiAccount y dentro de ella reemplazar el # por un -, además tuve que formatear el API_URL por completo, por alguna razón los params no se mandaban al ejecutar axios.get

Así quedó (una disculpa, ando usando TypeScript)

// search.ts
function getApiAccount (
  hash: string,
  { region, account }: { region:string, account: string }
) {
  const newHash = hash.replace('#', '-')
  const resource: string = `d3/profile/${account}${newHash}`

  const locale = locales[region.toLowerCase()]
  const accessToken = store.state.oauth.accessToken
  const API_URL: string = `${protocol}${region.toLowerCase()}${host}${resource}/?locale=${locale}&access_token=${accessToken}`

  return axios.get(API_URL)
}

Tengo una duda: ¿En qué momento se debe hacer una mutación con el dispatch de un action, y en qué momento no (como en el ejemplo de esta clase, donde se llama la mutación SET_ERROR a través de un método local directamente)?

Tengo unas dudas pero de JS:
1.

// no entiendo este parametro {region, account}
function getApiAccount ({ region, account }) 
export default {
  methods: {
    ...mapMutations('error', { // no entiedo los '...' del inicio de linea
      setError: 'SET_ERROR'
    }),

Gracias al que me pueda aclarar

Genial, aunque me quedan algunas dudas y algunos huecos que resolver:

1.- Funciona bien pero… funciona únicamente si cargas la ruta profile después del evento submit del formulario de home, si tu estás ya en la vista profile como primera carga de la página, aunque la URL esté bien te va a mandar a la ruta de error, estuve investigando en el código y al parecer primero se manda a llamar a la busqueda de profile y luego al get access token, realmente no entiendo por qué pasa esto, imagino que son los problemas del asincronismo, ya que en ningun momento validamos que se manden a hacer llamadas hasta que el token a este listo.

2.- En cuanto a los mixins, ¿No sería lo mismo utilizar directamente el mutation desde nuestro fetchData? Es decir, en cualquier componente que vaya a usar el setError podría usar directamente el mutation, no entiendo muy bien la funcionalidad del mixin aquí:(

3.- En cuanto al componente loading, veo que lo estamos volviendo a importar, pero realmente ese componente ya está importado en el MainLayout, ¿No sería mejor simplemente llamar al mutation SET_LOADING? Claro, hacer eso supondría destruir el componente MainLayout y cuando termine volverlo a construir pero ya con los datos, obtenidos

Me ha gustado el formato de este curso, veo el empeño de Jorge por responder y se nota la dedicación puesta en cada una de las partes del curso. Una buena forma de complementarlo hubiese sido que en alguna que otra parte importante, se hubiese hecho una explicación o introducción en video, por ejemplo, al comenzar cada módulo. De todas formas el curso está bueno y nos induce a que hagamos lecturas adicionales de la documentación relacionada. Gracias.

A mi me funciono correctamente tal y como esta en el post.

En mi caso los params no se mandaban en el request del perfil, solo es cambiar un poco el código.
Espero les sirva.

function getApiAccount({ region, account }) {
  const resource = `d3/profile/${account}`

  const locale = locales[region.toLowerCase()]
  const accessToken = store.state.oauth.accessToken
  const API_URL = `${protocol}${region.toLowerCase()}${host}${resource}/?locale=${locale}&access_token=${accessToken}`

  return get(API_URL)
}