Vayamos rápido al grano sobre el tema de componentes en Vue.js. Por ahora, sólo necesitas saber que todos los componentes en Vue son esencialmente una extensión a la instancia de Vue.

// Instancia Vue
new Vue({
  el: '#some-element',
  // options
})

Utilizando el entorno ofrecido por la herramienta vue-cli, vamos a utilizar los archivos iniciales para ver como trabajamos con los components. Veamos el siguiente ejemplo:

import Vue from 'vue'
import App from './App.vue'

// Extendemos las instancia para crear los componentes
Vue.component('comp1', {
  template:'<div>Component 1</div>' 
}) 

Vue.component('comp2', { 
  template:'<div>Component 2</div>' 
}) 

Vue.component('comp3', { 
  template:'<div>Component 3</div>' 
}) 

// Instancia de Vue 
new Vue({ 
  el: '#app', 
  render: h => h(App) 
})


En el ejemplo anterior hemos creados algunos componentes haciendo uso de la instancia de Vue. Veamos como trabajamos con la data en estos componentes.

El objeto data debe ser una función.

Muchas de las opciones disponibles en la instancia de Vue se pueden utilizar de igual forma en en los componentes. Una excepción a esta funcionalidad es el objeto data. Cuando vemos muchos de los ejemplos de Vue, el objeto data usualmente es un objeto sencillo similar a algo así:

new Vue({
  el: '#greeting-panel',
  data: {
    input: 'Bienvenidos al Matrix'
  }
})

Pero cuando trabajamos con componentes, debemos declarar el objeto data como una función. Esto debido a que al momento de crear los componentes esto se traduce en generar varias instancias del objeto Vue. Si utilizamos el objeto sin ser declarado como una función, estaremos compartiendo el objecto data a través de todas las instancias. Y esto no es algo que querramos en nuestro proyecto. Veamos los ejemplos que se discuten en la documentación de Vue:

// Ejemplo tomando data como un objeto 
var data = { counter: 0 }
Vue.component('simple-counter', {
  template: '<button v-on:click="counter += 1">{{ counter }}</button>',
  // Aunque data es técnicamente una función, la función devuelve
  // el mismo objeto a través de cada componente. 
  // Esto sucede por referencia.
  data: function () {
    return data
  }
})
new Vue({
  el: '#example-2'
})

El resultado sería algo como esto:

Si por el contrario utilizamos el objeto data como un función, que es como se debe hacer en los componentes, cada vez que creamos una instancia esta devolverá una copia fresca e independiente de nuestro objeto.

data: function () {
  return {
    counter: 0
  }
}

Comunicación entre componentes

El beneficio real de los componentes se encuentra en poder utilizarlos como piezas lógicas para poder ensamblar nuestro proyecto. La analogía clásica de las piezas Lego representa exactamente la idea de forma clara. Un factor crítico es entender la comunicación entre componentes. Esta comunicación ocurre utilizando el atributo props y los custom events. Los atributos tipo props envían información desde el componente padre al componente hijo, y el evento custom es la respuesta del componente hijo hacia el componente padre.

Props

Los componentes poseen un alcance aislado. La documentación es enfática en recalcar que no debes modificar directamente la data (mutar el objeto data) que maneja el componente padre. Incurrir en esta práctica puede causar la perdida del control del flujo de la data en tu aplicación. Para este propósito utilizamos los props. Con los props podemos mover data desde el componente padre a los componentes hijos.

Vue.component('child', {
  // Declaración del props llamado message
  props: ['message'],
  // just like data, the prop can be used inside templates
  // and is also made available in the vm as this.message
  template: '<span>{{ message }}</span>'
})

En el siguiente ejemplo tenemos el código de nuestro componente padre (que es el componente App.vue) comunicándose a través del props llamado message definido en el componente hijo.

<template>
  <div id="app">
    <comp3 :message="messageParent"></comp3>
  </div>
</template>

<script>
export default {
  name: 'app',
  data () {
    return {
      messageParent: 'Welcome to the Matrix'
    }
  }
}
</script>

Una gran ventaja de usar props en Vue es que puedes ejecutar cierto tipo de validación a la data que estará procesando la propiedad. Veamos el ejemplo que encontramos en la documentación:

Vue.component('example', {
  props: {
    // verificación de tipado (`null` significa any type)
    propA: Number,
    // multiples opciones
    propB: [String, Number],
    // se require que el valor sea de tipo string
    propC: {
      type: String,
      required: true
    },
    // valor tipo numérico con valor for default
    propD: {
      type: Number,
      default: 100
    },
    // object/array defaults debe ser el retorno de una
    // función tipo factory
    propE: {
      type: Object,
      default: function () {
        return { message: 'hello' }
      }
    },
    // funcion para realizar un custom validator 
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
})

Custom Events

Hemos aprendido que los componentes se comunican desde el padre al hijo usando props, pero para comunicarnos entre hijo a padre utilizamos el sistema de custom events. La implementación trabaja de la siguiente manera:

  • Escuchar (listen) eventos usando $on(eventName)
  • Activar (trigger) un evento usando $emit(eventName)

Veamos el siguiente ejemplo:

<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>

Aquí vemos como utilizando el atributo v-on creamos una referencia al método increment y lo aplicamos al componente button-counter. El código de javascript usando Vue es el siguiente:

Vue.component('button-counter', {
  template: '<button v-on:click="increment">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    increment: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
})
new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})

El ejemplo en acción:

Aquí vemos como el componente button-counter actualiza el parámetro total definido en el componente padre.

Conclusión

Solamente hemos tocado el tema de manera superficial. El potencial de los componentes es mucho más abarcador de lo que aquí hemos discutido. Lo interesante de los componentes es la utilización de diferentes tecnologías que juntas logran de manera estandarizada la construcción de webapps de fácil mantenimiento y extensibilidad.