El sistema de componentes es un concepto importante en Vue y en cualquier framework moderno. En lugar de separar nuestra aplicación en ficheros según el tipo de información que contienen (ficheros html, css o js) es más lógico separarla según su funcionalidad. Una página web muestra una UI donde se pueden distinguir diferentes partes. En el siguiente ejemplo tenemos:
Pues estos elementos podrían constituir diferentes componentes: nuestras aplicaciones estarán compuestas de pequeños componentes independientes y reusables en diferentes partes de nuestra aplicación o en otras aplicaciones (podemos usar el elemento de buscar en otras páginas de nuestra aplicación o incluso en otras aplicaciones). También es habitual que un componente contenga otros subcomponentes, estableciéndose relaciones padre-hijo (por ejemplo el componente tabla tiene como subcomponentes el buscador y cada una de las filas, y el componente fila tendrá un subcomponente por cada botón que queramos poner en ella).
Para saber qué debe ser un componente y que no, podemos considerar un componente como un elemento que tiene entidad propia, tanto a nivel funcional como visual, es decir, que puede ponerse en el lugar que queramos de la aplicación y se verá y funcionará correctamente. Además es algo que es muy posible que pueda aparecer en más de un lugar de la aplicación. En definitiva un componente:
El componente es un objeto con una parte de HTML donde definimos su estructura y una parte JS que le da su funcionalidad. CUando trabajemos con Single File Components (SFC) también se incluirá una parte CSS para establecer su apariencia.
Separar nuestra aplicación en componentes nos va a ofrecer muchas ventajas:
El primer paso a la hora de hacer una aplicación debe ser analizar qué componentes tendrá
En definitiva nuestra aplicación será como un árbol de componentes con la instancia principal de Vue como raíz.
Un componeteusarlo se crea con app.component
al que le pasamos 2 parámetros:
data
, methods
, …). Además tendrá una opción template
con el código HTML que se insertará donde pongamos el componente.Por ejemplo, vamos a crear un componente para mostrar cada elemento de la lista de tareas a hacer:
const app = Vue.createApp({
...
})
app.component('TodoItem', {
template: `
<li>
<input type="checkbox" v-model="elem.done">
<del v-if="elem.done">
{ { elem.title }}
</del>
<span v-else>
{ { elem.title }}
</span>
</li>`,
data: ()=>({
elem: { title: 'Cosa a hacer', done: true }
})
})
...
app.mount('#app')
NOTA: no se puede montar la apicación hasta después de haber definido los componentes.
Ahora ya podemos usar el componente en nuestro HTML:
<ul>
<todo-item></todo-item>
</ul>
Resultado:
Cosa a hacer
Podemos utilizar la etiqueta tal (<todo-item>) o usar una etiqueta estándar y poner la nuestra como valor de su atributo is:
<ul>
<li is="todo-item"></li>
</ul>
De esta forma evitamos errores de validación de HTML ya que algunos elementos sólo pueden tener determinados elementos hijos (por ejemplo los hijos de un <ul> deben ser <li> o los de un <tr> deben ser <td>).
ATENCIÓN: El nombre de un componente puede estar en PascalCase (MyComponentName) o en kebab-case (my-component-name). Lo recomendado es que en Javascript lo pongamos en PascalCase y en el HTML en kebab-case (Vue hace la traducción automáticamente). Se recomienda que el nombre de un componente tenga al menos 2 palabras para evitar que pueda llamarse como alguna futura etiqueta HTML.
NOTA: En versiones anteriores de Vue la propiedad template sólo podía tener un nodo raíz. En Vue3 esta limitación no existe aunque en dev-tools se depura más fácilmente si solo hay uno. Por ello, si tenemos más de 1 nodo los envolvemos en otra etiqueta (normalmente un <div>):
// MAL en Vue2
template: `<input id="query">
<button id="search">Buscar</button>`,
// BIEN en Vue2
template: `<div>
<input id="query">
<button id="search">Buscar</button>
</div>`,
Declarar los componentes con app.component()
en el mismo fichero JS de la instancia genera varios problemas, especialmente:
Por tanto eso puede ser adecuado para proyectos muy pequeños pero no lo es cuando estos empiezan a crecer.
La solución es guardar cada componente en un único fichero javascript (con extensión .js
), aunque cuando usemos SFC usaremos ficheros .vue
.
En el fichero exportaremos un objeto con las propiedades del componente (data, methods, …), además de la propiedad template. También podemos añadir una propiedad name donde indicar el nombre del componente.
Por ejemplo, vamos a crear un componente para el botón de eliminar todas las tareas:
// Fichero DelAllItems.js
export default {
template: `
<button @click="delAll">Vacía lista</button>
`,
methods: {
delAll() {
if (confirm('Vas a borrar la lista de tareas')) {
this.todos.splice(0)
}
}
},
}
Para poder usar un componente, en donde queramos usarlo (otro componente o la instancia raíz) debemos hacer 2 cosas:
components
// Fichero main.js
import DelAllItems from './DellAllItems.js'
Vue.createApp({
components: {
DelAllItems,
},
data() {
...
}
})
Ahora ya podemos usar el componente en nuestro HTML:
...
</form>
<del-all-items></del-all-items>
</div>
<script type="module" src="main.js"></script>
</body>
</html>
Fíjate que hay que declarar el fichero main.js como module
para que nos permita importar ficheros en él.
El navegador sustituirá la etiqueta del componente (<del-all-items>) por su template.
Haz el ejercicio del tutorial de Vue.js |
Podemos pasar parámetros a un componente añadiendo atributos a su etiqueta:
<ul>
<todo-item :todo="{ title: 'Nueva cosa', done: false }"></todo-item>
</ul>
NOTA: recuerda que si no ponemos el v-bind estaríamos pasando texto y no una variable.
El parámetro lo recibimos en el componente en una opción llamada props
:
app.component('todo-item', {
props: {
todo: String
},
template: `
<li>
<input type="checkbox" v-model="todo.done">
...`
})
Es equivalente a los parámetros que recibe una función.
Haz el ejercicio del tutorial de Vue.js |
Se pueden declarar las props recibidas como un array de string (props: ['todo']
), aunque es mejor declararlas como un objeto porque nos permitirá hacer ciertas comprobaciones (en el ejemplo anterior que se recibe un String).
IMPORTANTE: si un parámetro tiene más de 1 palabra en el HTML lo pondremos en forma kebab-case (ej.: <todo-item :todo-elem=...>
) pero en el Javascript irá en camelCase (app.component('todo-item',{ props: ['todoElem'],...})
). Vue hace la traducción automáticamente.
Resultado:
- Nueva cosa a hacer
En nuestro caso queremos un componente todo-item para cada elemento del array todos:
<ul>
<todo-item v-for="item in todos" :key="item.id" :todo="item"></todo-item>
</ul>
Resultado:
Learn JavaScript- Learn Vue
...
IMPORTANTE: al usar v-for
en un componente debemos indicarle obligatoriamente en la propiedad key
la clave de cada elemento. Si no tuviera ninguna podemos usar como clave su índice en el array como vimos al hablar de v-for:
<ul>
<todo-item v-for="(item, index) in todos" :key="index" :todo="item"></todo-item>
</ul>
Para empezar a ver el uso de componentes vamos a seguir con la aplicación de la lista de cosas que hacer pero dividiéndola en componentes.
La decisión de qué componentes crear es subjetiva pero en principio cuanto más descompongamos más posibilidades tendremos de reutilizar componentes. Nosotros haremos los siguientes componentes:
A continuación tienes la solución de cómo dividirla en componentes en un único fichero pero en su lugar lo que haremos es separar cada componente en su propio fichero.
IMPORTANTE: separar los componentes en ficheros que se importan donde vayan a usarse sólo funciona si abrimos la aplicación desde un servidor web, no desde local (sí http://…, no file://…). Si no tenéis ninguno podéis usar la extensión Live Server de Visual Studio Code para ejecutar la aplicación.
Solución
See the Pen to-do app components by Juan Segura (@juanseguravasco) on CodePen.
Pasos que he hecho: