Skip to content

Fundamentos de los Componentes

Los componentes nos permiten dividir la interfaz de usuario en piezas independientes y reutilizables, y pensar en cada pieza de forma aislada. Es común que una aplicación se organice en un árbol de componentes anidados:

Árbol de Componentes

Esto es muy similar a cómo anidamos los elementos HTML nativos, pero Vue implementa su propio modelo de componentes que nos permite encapsular contenido y lógica personalizados en cada componente. Vue también juega muy bien con los Componentes Web nativos. Si tienes curiosidad por saber la relación entre los Componentes de Vue y los Componentes Web nativos, lee más aquí.

Definiendo un Componente

Cuando usamos un paso de compilación, normalmente definimos cada componente de Vue en un archivo dedicado usando la extensión .vue conocido como Componente de un Solo Archivo (abreviado SFC):

vue
<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

<template>
  <button @click="count++">Me has hecho clic {{ count }} veces.</button>
</template>
vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">Me has hecho clic {{ count }} veces.</button>
</template>

Cuando no se utiliza un paso de compilación, un componente de Vue puede definirse como un objeto JavaScript simple que contiene opciones específicas de Vue:

js
export default {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count++">
      Me has hecho clic {{ count }} veces.
    </button>`
}
js
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return { count }
  },
  template: `
    <button @click="count++">
      Me has hecho clic {{ count }} veces.
    </button>`
  // También puede apuntar a una plantilla dentro del DOM:
  // template: '#my-template-element'
}

La plantilla se alinea aquí como una cadena de JavaScript, que Vue compilará sobre la marcha. También puedes utilizar un selector de ID que apunte a un elemento (normalmente elementos nativos <template>). Vue utilizará su contenido como fuente de la plantilla.

El ejemplo anterior define un único componente y lo exporta como la exportación por defecto de un archivo .js, pero puedes usar exportaciones nominales para exportar múltiples componentes desde el mismo archivo.

Usando un Componente

TIP

Utilizaremos la sintaxis SFC para el resto de esta guía. Los conceptos en torno a los componentes son los mismos independientemente de si está utilizando un paso de compilación o no. La sección Ejemplos muestra el uso de componentes en ambos escenarios.

Para usar un componente hijo, necesitamos importarlo en el componente padre. Asumiendo que colocamos nuestro componente contador dentro de un archivo llamado ButtonCounter.vue, el componente será expuesto como la exportación por defecto del archivo:

vue
<script>
import ButtonCounter from './ButtonCounter.vue'

export default {
  components: {
    ButtonCounter
  }
}
</script>

<template>
  <h1>¡Aquí hay un componente hijo!</h1>
  <ButtonCounter />
</template>

Para exponer el componente importado a nuestra plantilla, necesitamos registrarlo con la opción components. El componente estará entonces disponible como una etiqueta usando la clave con la que está registrado.

vue
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>

<template>
  <h1>¡Aquí hay un componente hijo!</h1>
  <ButtonCounter />
</template>

Gracias a <script setup>, los componentes importados se ponen automáticamente a disposición de la plantilla.

También es posible registrar globalmente un componente, haciéndolo disponible para todos los componentes de una determinada aplicación sin tener que importarlo. Las ventajas y desventajas del registro global frente al local se discuten en la sección dedicada al Registro de Componentes.

Los componentes pueden ser reutilizados tantas veces como se quieras:

template
<h1>¡Aquí hay muchos componentes hijos!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />

Observa que al hacer clic en los botones, cada uno mantiene su propio "recuento" por separado. Esto se debe a que cada vez que se utiliza un componente, se crea una nueva instancia del mismo.

En los SFCs, se recomienda utilizar nombres de etiquetas PascalCase para los componentes hijos para diferenciarlos de los elementos HTML nativos. Aunque los nombres de las etiquetas HTML nativas no distinguen entre mayúsculas y minúsculas, el SFC de Vue es un formato compilado, por lo que podemos utilizar nombres de etiquetas que distinguen entre mayúsculas y minúsculas. También podemos utilizar /> para cerrar una etiqueta.

Si estás creando tus plantillas directamente en un DOM (por ejemplo, como el contenido de un elemento nativo <template>), la plantilla estará sujeto al comportamiento de análisis nativo de HTML del navegador. En estos casos, tendrá que utilizar kebab-case y etiquetas de cierre explícitas para los componentes:

template
<!-- si esta plantilla está escrita en el DOM -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>

Consulta Advertencias sobre el procesamiento de las plantillas en el DOM para más detalles.

Pasando Props

Si estamos construyendo un blog, probablemente necesitaremos un componente que represente una entrada del blog. Queremos que todas las entradas del blog compartan el mismo diseño visual, pero con diferente contenido. Dicho componente no será útil a menos que puedas pasarle datos, como el título y el contenido de la entrada específica que queremos mostrar. Es ahí donde entran los props.

Las Props son atributos personalizados que puedes registrar en un componente. Para pasar un título a nuestro componente de entrada de blog, debemos declararlo en la lista de props que este componente acepta, utilizando la opción de la macro propsdefineProps:

vue
<!-- BlogPost.vue -->
<script>
export default {
  props: ['title']
}
</script>

<template>
  <h4>{{ title }}</h4>
</template>

Cuando se pasa un valor a un atributo prop, se convierte en una propiedad de esa instancia del componente. El valor de esa propiedad es accesible dentro de la plantilla y en el contexto this del componente, como cualquier otra propiedad del componente.

vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>

<template>
  <h4>{{ title }}</h4>
</template>

defineProps es una macro en tiempo de compilación que sólo está disponible dentro de <script setup> y no necesita ser importada explícitamente. Las props declaradas se exponen automáticamente a la plantilla. defineProps también devuelve un objeto que contiene todos las props pasadas al componente, para que podamos acceder a ellas en JavaScript si es necesario:

js
const props = defineProps(['title'])
console.log(props.title)

Véase también: Escritura de las Props de Componentes

Si no está usando <script setup>, las props deben ser declaradas usando la opción props, y el objeto props será pasado a setup() como primer argumento:

js
export default {
  props: ['title'],
  setup(props) {
    console.log(props.title)
  }
}

Un componente puede tener tantas props como se quieras y, por defecto, se puede pasar cualquier valor a cualquier prop.

Una vez registrada una prop, puedes pasarle datos como un atributo personalizado, así:

template
<BlogPost title="Mi viaje con Vue" />
<BlogPost title="Blogueando con Vue" />
<BlogPost title="Por qué Vue es tan divertido" />

En una aplicación típica, sin embargo, es probable que tengas un array de posts en tu componente padre:

js
export default {
  // ...
  data() {
    return {
      posts: [
        { id: 1, title: 'Mi viaje con Vue' },
        { id: 2, title: 'Blogueando con Vue' },
        { id: 3, title: 'Por qué Vue es tan divertido' }
      ]
    }
  }
}
js
const posts = ref([
  { id: 1, title: 'Mi viaje con Vue' },
  { id: 2, title: 'Blogueando con Vue' },
  { id: 3, title: 'Por qué Vue es tan divertido' }
])

Después queremos renderizar un componente para cada uno, usando v-for:

template
<BlogPost
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
 />

Observa cómo podemos usar v-bind para pasar props dinámicos. Esto resulta especialmente útil cuando no conoces el contenido exacto que vas a renderizar con antelación.

Eso es todo lo que necesitas saber sobre los props por ahora, pero una vez que hayas terminado de leer esta página y te sientas cómodo con su contenido, te recomendamos que vuelvas más tarde para leer la guía completa sobre Props.

Escuchando los Eventos

Según vayamos desarrollando nuestro componente <BlogPost>, algunas características pueden requerir comunicarse con el padre. Por ejemplo, podemos decidir incluir una función de accesibilidad para ampliar el texto de las entradas del blog, dejando el resto de la página en su tamaño por defecto.

En el padre, podemos soportar esta característica añadiendo una propiedad de dataref postFontSize:

js
data() {
  return {
    posts: [
      /* ... */
    ],
    postFontSize: 1
  }
}
js
const posts = ref([
  /* ... */
])

const postFontSize = ref(1)

La cual se puede utilizar en la plantilla para controlar el tamaño de la fuente de todas las entradas del blog:

template
<div :style="{ fontSize: postFontSize + 'em' }">
  <BlogPost
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
   />
</div>

Ahora vamos a añadir un botón a la plantilla del componente <BlogPost>:

vue
<!-- BlogPost.vue, omitiendo <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button>Ampliar el texto</button>
  </div>
</template>

El botón de momento no hace nada; queremos que al hacer clic en el botón comunique al padre que debe ampliar el texto de todas las entradas. Para resolver este problema, las instancias de componentes proporcionan un sistema de eventos personalizado. El padre puede elegir escuchar cualquier evento en la instancia del componente hijo con v-on o @, tal como lo haríamos con un evento DOM nativo:

template
<BlogPost
  ...
  @enlarge-text="postFontSize += 0.1"
 />

Entonces el componente hijo puede emitir un evento sobre sí mismo llamando al método emit integrado, pasando el nombre del evento:

vue
<!-- BlogPost.vue, omitiendo <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Ampliar el texto</button>
  </div>
</template>

Gracias al escuchador @enlarge-text="postFontSize += 0.1", el padre recibirá el evento y actualizará el valor de postFontSize.

Opcionalmente podemos declarar eventos emitidos utilizando la opción macro emitsdefineEmits:

vue
<!-- BlogPost.vue -->
<script>
export default {
  props: ['title'],
  emits: ['enlarge-text']
}
</script>
vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>

Esto documenta todos los eventos que emite un componente y opcionalmente los valida. También permite a Vue evitar aplicarlos implícitamente como oyentes nativos al elemento raíz del componente hijo.

Al igual que defineProps, defineEmits también se puede utilizar sólo en <script setup> y no necesita ser importado. Devuelve una función emit que se puede utilizar para emitir eventos en el código JavaScript:

js
const emit = defineEmits(['enlarge-text'])

emit('enlarge-text')

Véase también: Escritura de Emits del Componente

Si no estás usando <script setup>, puedes declarar eventos emitidos usando la opción emits. Puedes acceder a la función emit como una propiedad del contexto de configuración (pasada a setup() como segundo argumento):

js
export default {
  emits: ['enlarge-text'],
  setup(props, ctx) {
    ctx.emit('enlarge-text')
  }
}

Eso es todo lo que necesitas saber sobre los eventos de componentes personalizados por ahora, pero una vez que hayas terminado de leer esta página y te sientas cómodo con su contenido, te recomendamos que vuelvas más tarde para leer la guía completa sobre Eventos Personalizados.

Distribución de Contenidos con Slots

Al igual que con los elementos HTML, a menudo es útil poder pasar contenido a un componente, de esta manera:

template
<AlertBox>
  Ha ocurrido algo malo.
</AlertBox>

Lo que podría renderizar algo como:

Esto es un error para fines de demostración

Algo malo ha ocurrido.

Esto puede lograrse utilizando el elemento personalizado <slot> de Vue:

vue
<template>
  <div class="alert-box">
    <strong>Esto es un error para fines de demostración</strong>
    <slot />
  </div>
</template>

<style scoped>
.alert-box {
  /* ... */
}
</style>

Como verás arriba, usamos el <slot> como marcador de posición donde queremos que vaya el contenido, y eso es todo. ¡Ya hemos terminado!

Esto es todo lo que necesitas saber sobre las ranuras por ahora, pero una vez que hayas terminado de leer esta página y te sientas cómodo con su contenido, te recomendamos que vuelvas más tarde para leer la guía completa sobre Slots.

Componentes Dinámicos

A veces, es útil cambiar dinámicamente entre componentes, como en una interfaz con pestañas:

Lo anterior es posible gracias al elemento <component> de Vue con el atributo especial is:

template
<!-- El componente cambia cuando cambia el currentTab -->
<component :is="currentTab"></component>
template
<!-- El componente cambia cuando cambia el currentTab -->
<component :is="tabs[currentTab]"></component>

En el ejemplo anterior, el valor pasado a :is puede contener

  • la cadena de nombre de un componente registrado, O
  • el objeto componente actual importado

También puedes utilizar el atributo "is" para crear elementos HTML normales.

Cuando se cambia entre varios componentes con <component :is="...">, un componente será desmontado cuando se cambie de lugar. Podemos forzar que los componentes inactivos permanezcan "vivos" con el componente integrado <KeepAlive>.

Advertencias sobre el procesamiento de las plantillas en el DOM

Si estás escribiendo tus plantillas de Vue directamente en el DOM, Vue tendrá que recuperar la cadena de la plantilla desde el DOM. Esto conduce a algunas advertencias debido al comportamiento de análisis nativo de HTML de los navegadores.

TIP

Debe tenerse en cuenta que las limitaciones discutidas a continuación sólo se aplican si estás escribiendo tus plantillas directamente en el DOM. NO se aplican si está usando plantillas de cadena de las siguientes fuentes:

  • Componentes de un Solo Archivo
  • Cadenas de plantillas alineadas (por ejemplo, template: '...')
  • <script type="text/x-template">

Insensibilidad a las Mayúsculas y Minúsculas

Las etiquetas HTML y los nombres de atributos no distinguen entre mayúsculas y minúsculas, por lo que los navegadores interpretarán cualquier carácter en mayúscula como en minúscula. Esto significa que cuando se utilizan plantillas en el DOM, los nombres de componentes en PascalCase y los nombres de props en camelCase o los nombres de eventos v-on deben utilizar sus equivalentes en kebab-cased (delimitados por guiones):

js
// camelCase en JavaScript
const BlogPost = {
  props: ['postTitle'],
  emits: ['updatePost'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
}
template
<!-- kebab-case en el HTML -->
<blog-post post-title="¡hola!" @update-post="onUpdatePost"></blog-post>

Etiquetas de Autocierre

Ya hemos utilizado etiquetas de autocierre para los componentes en los ejemplos de código anteriores:

template
<MyComponent />

Esto es debido a que el parser de plantillas de Vue respeta /> como una indicación para terminar cualquier etiqueta, independientemente de su tipo.

Sin embargo, en las plantillas en el DOM, debemos incluir siempre etiquetas de cierre explícitas:

template
<my-component></my-component>

Esto es debido a que la especificación HTML sólo permite omitir las etiquetas de cierre en unos pocos elementos específicos, siendo los más comunes <input> y <img>. Para los demás elementos, si omites la etiqueta de cierre, el analizador nativo de HTML pensará que nunca has terminado la etiqueta de apertura. Por ejemplo, el siguiente fragmento:

template
<my-component /> <!-- pretendemos cerrar la etiqueta aquí... -->
<span>hola</span>

se interpretará como:

template
<my-component>
  <span>hola</span>
</my-component> <!-- pero el navegador lo cerrará aquí. -->

Restricciones para la Colocación de Elementos

Algunos elementos HTML, como <ul>, <ol>, <table> y <select> tienen restricciones sobre qué elementos pueden aparecer dentro de ellos, y algunos elementos como <li>, <tr> y <option> solo pueden aparecer dentro de ciertos otros elementos.

Esto dará lugar a problemas cuando se utilicen componentes con elementos que tengan dichas restricciones. Por ejemplo:

template
<table>
  <blog-post-row></blog-post-row>
</table>

El componente personalizado <blog-post-row> será captado como contenido inválido, causando errores en la eventual salida renderizada. Podemos utilizar el atributo especial is como solución:

template
<table>
  <tr is="vue:blog-post-row"></tr>
</table>

TIP

Cuando se utiliza en elementos HTML nativos, el valor de is debe llevar el prefijo vue: para ser interpretado como un componente Vue. Esto es necesario para evitar la confusión con los elementos integrados personalizados nativos.

¡Enhorabuena! Por ahora, eso es todo lo que necesitas saber sobre el análisis de advertencias de la plantilla en el DOM y, en definitiva, el final de los Esenciales de Vue. Todavía hay más que aprender, pero primero, recomendamos tomar un descanso para que juegues con Vue tú mismo, construyas algo divertido o revises algunos de los Ejemplos si aún no lo has hecho.

Una vez que te sientas cómodo con los conocimientos que acabas de digerir, sigue con la guía para aprender más sobre los componentes en profundidad.

Fundamentos de los Componentes has loaded