Capítulo 4. Interacciones entre componentes
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
En el Capítulo 3, profundizamos en la composición de un componente con ganchos de ciclo de vida, propiedades computadas, observadores, métodos y otras características. También conocimos la potencia de las ranuras y cómo recibir datos externos de otros componentes mediante props.
Partiendo de esa base, este capítulo te guía sobre cómo construir las interacciones entre componentes utilizando eventos personalizados y patrones proporcionar/inyectar. También presenta la API Teleport, que te permite mover elementos por el árbol DOM manteniendo su orden de aparición dentro de un componente Vue.
Componentes anidados y flujo de datos en Vue
Los componentes Vue pueden anidar otros componentes Vue en su interior. Esta característica es útil para permitir a los usuarios organizar su código en piezas más pequeñas, manejables y reutilizables en un proyecto de interfaz de usuario complejo. Llamamos componentes hijos a los elementos anidados y componente padre al componente que los contiene .
El flujo de datos en una aplicación Vue es unidireccional por defecto, lo que significa que el componente padre puede pasar datos a su componente hijo, pero no al revés. El componente padre puede pasar datos al componente hijo utilizando props
(que se explica brevemente en "Exploración de la API de opciones"), y el componente hijo puede emitir eventos de vuelta al componente padre utilizando eventos personalizados emits
. La Figura 4-1 muestra el flujo de datos entre componentes .
Pasar funciones como apoyo
A diferencia de otros frameworks, Vue no te permite pasar una función como prop al componente hijo. En su lugar, puedes vincular la función como un emisor de eventos personalizado (consulta "Comunicación entre componentes coneventos personalizados") .
Utilizar apoyos para pasar datos a componentes hijos
En forma de objeto o matriz, el campo props
de un componente Vue contiene todas las propiedades de datos disponibles que el componente puede recibir de su padre. Cada propiedad de props
es una prop del componente de destino. Para empezar a recibir datos del padre, tienes que declarar el campo props
en el objeto de opciones del componente, como se muestra en el Ejemplo 4-1.
Ejemplo 4-1. Definir accesorios en un componente
export
default
{
name
:
'ChildComponent'
,
props
:
{
name
:
String
}
}
En el Ejemplo 4-1, el componente ChildComponent
acepta una proposición name
de tipo String
. El componente padre puede entonces pasar datos al componente hijo utilizando esta proposición name
, como se muestra en el Ejemplo 4-2 .
Ejemplo 4-2. Pasar datos estáticos como accesorios a un componente hijo
<
template
>
<
ChildComponent
name
=
"Red Sweater"
/>
</
template
>
<
script
lang
=
"ts"
>
import
ChildComponent
from
'./ChildComponent.vue'
export
default
{
name
:
'ParentComponent'
,
components
:
{
ChildComponent
},
}
</
script
>
En el ejemplo anterior, ChildComponent
recibe un valor estático "Suéter rojo" como name
. Si quieres pasar y vincular una variable de datos dinámica a name
, como el primer elemento de la lista children
, puedes utilizar el atributo v-bind
, denotado por :
, como se muestra en el Ejemplo 4-3.
Ejemplo 4-3. Pasar variables dinámicas como accesorios a un componente hijo
<
template
>
<
ChildComponent
:name
=
"children[0]"
/>
</
template
>
<
script
lang
=
"ts"
>
import
ChildComponent
from
'./ChildComponent.vue'
export
default
{
//...
data
()
{
return
{
children
:
[
'Red Sweater'
,
'Blue T-Shirt'
,
'Green Hat'
]
}
}
}
</
script
>
La salida del código anterior es la misma que pasar una cadena estática, Red Sweater
, a la proposición name
.
Nota
Si la proposición name
no es del tipo String
, aún tendrás que utilizar el atributov-bind
(o :
) para pasar datos estáticos al componente hijo, como :name="true"
para Boolean
, o :name="["hello", "world"]"
para el tipo Array
.
En el Ejemplo 4-3, cada vez que cambie el valor de children[0]
, Vue actualizará también la proposición name
en ChildComponent
, y el componente hijo volverá a renderizar su contenido si es necesario.
Si tienes más de una prop en el componente hijo, puedes seguir el mismo planteamiento y pasar cada dato a la prop correspondiente. Por ejemplo, para pasar name
y price
de un producto al componente ProductComp
, puedes realizar lo siguiente(Ejemplo 4-4).
Ejemplo 4-4. Pasar varios accesorios a un componente hijo
/** components/ProductList.vue */<
template
>
<
ProductComp
:name
=
"product.name"
:price
=
"product.price"
/>
</
template
>
<
script
lang
=
"ts"
>
import
ProductComp
from
'./ProductComp.vue'
export
default
{
name
:
'ProductList'
,
components
:
{
ProductComp
},
data
()
{
return
{
product
:
{
name
:
'Red Sweater'
,
price
:
19.99
}
}
}
}
</
script
>
Y podemos definir el componente ProductComp
como en el Ejemplo 4-5.
Ejemplo 4-5. Definir varios accesorios en ProductComp
<
template
>
<
div
>
<
p
>
Product: {{ name }}</
p
>
<
p
>
Price: {{ price }}</
p
>
</
div
>
</
template
>
<
script
lang
=
"ts"
>
export
default
{
name
:
'ProductComp'
,
props
:
{
name
:
String
,
price
:
Number
}
}
</
script
>
El resultado será el siguiente:
Product: Red Sweater Price: 19.99
Alternativamente, puedes utilizar v-bind
(no :
) para pasar el objeto completo user
y que sus propiedades se vinculen a los accesorios del componente hijo correspondiente:
<
template
>
<
ProductComp
v-bind
=
"product"
/>
</
template
>
Ten en cuenta que sólo el componente hijo recibirá los accesorios declarados correspondientes. Por lo tanto, si tienes otro campo, product.description
, en el componente padre, no estará disponible para su acceso en el componente hijo.
Nota
Otra forma de declarar el componente props
es utilizar una matriz de cadenas, cada una de las cuales representa el nombre de la propiedad que acepta, como props: ["name", "price"]
. Este enfoque es práctico cuando quieres crear un prototipo de un componente rápidamente. Sin embargo, te recomiendo encarecidamente que utilices la forma de objeto de props
y que declares todos tus props con tipos, como buena práctica para la legibilidad del código y la prevención de errores.
Hemos aprendido a declarar props con tipos, pero ¿cómo validamos los datos pasados a las props del niño cuando es necesario? ¿Cómo podemos establecer un valor alternativo para una proposición cuando no se pasa ningún valor? Averigüémoslo a continuación.
Declarar tipos prop con validación y valores por defecto
En el Ejemplo 4-1, declaramos la propiedad name
como un tipo String
. Vue avisará si el componente padre pasa un valor que no sea cadena a la propiedad name
durante el tiempo de ejecución. Sin embargo, para poder disfrutar de las ventajas de la validación de tipos de Vue, debemos utilizar la sintaxis de declaración completa :
{
type
:
String
|
Number
|
Boolean
|
Array
|
Object
|
Date
|
Function
|
Symbol
,
default
?:
any
,
required
?:
boolean
,
validator
?:
(
value
:
any
)
=>
boolean
}
En el que:
-
type
es el tipo de prop. Puede ser una función del constructor (o clase personalizada) o uno de los tipos incorporados. -
default
es el valor por defecto de la proposición si no se pasa ningún valor. Para los tiposObject
,Function
yArray
, el valor por defecto debe ser una función que devuelva el valor inicial. -
required
es un valor booleano que indica si la prop es obligatoria. Sirequired
estrue
, el componente padre debe pasar un valor a la utilería. Por defecto, todas las props son opcionales. -
validator
es una función que valida el valor pasado a la prop, principalmente para depuración de desarrollo.
Podemos declarar la prop name
para que sea más específica, incluyendo un valor por defecto, como se muestra en el Ejemplo 4-6.
Ejemplo 4-6. Definir prop como una cadena con un valor por defecto
export
default
{
name
:
'ChildComponent'
,
props
:
{
name
:
{
type
:
String
,
default
:
'Child component'
}
}
}
Si el componente padre no pasa un valor, el componente hijo volverá al valor por defecto "Componente hijo" para la proposición name
.
También podemos establecer name
como prop obligatoria para el componente hijo y añadir un validador para sus datos recibidos, como se muestra en el Ejemplo 4-7.
Ejemplo 4-7. Definir el nombre como obligatorio con un validador prop
export
default
{
name
:
'ChildComponent'
,
props
:
{
name
:
{
type
:
String
,
required
:
true
,
validator
:
value
=>
value
!==
"Child component"
}
}
}
En este escenario, si el componente padre no pasa un valor a la prop name
, o el valor dado coincide con el componente hijo, Vue lanzará una advertencia en modo desarrollo(Figura 4-2) .
Nota
Para el campo default
, el tipo Function
es una función que devuelve el valor inicial de la prop. No puedes utilizarla para devolver datos al componente padre ni para provocar cambios de datos en el nivel padre.
Además de los tipos incorporados y la validación que proporciona Vue, puedes combinar un JavaScript Class
o un constructor de función y TypeScript para crear tu tipo prop personalizado. Los trataré en la siguiente sección .
Declarar objetos con comprobación de tipos personalizada
Utilizar tipos primitivos como Array
, String
, o Object
es adecuado para el caso de uso esencial. Sin embargo, a medida que crece tu aplicación, los tipos primitivos pueden ser demasiado genéricos para mantener a salvo el tipo de tu componente. Toma un PizzaComponent
con el siguiente código de plantilla:
<
template
>
<
header
>
Title: {{ pizza.title }}</
header
>
<
div
class
=
"pizza--details-wrapper"
>
<
img
:src
=
"pizza.image"
:alt
=
"pizza.title"
width
=
"300"
/>
<
p
>
Description: {{ pizza.description }}</
p
>
<
div
class
=
"pizza--inventory"
>
<
div
class
=
"pizza--inventory-stock"
>
Quantity: {{pizza.quantity}}</
div
>
<
div
class
=
"pizza--inventory-price"
>
Price: {{pizza.price}}</
div
>
</
div
>
</
div
>
</
template
>
Este componente acepta una prop obligatoria pizza
, que es un Object
que contiene algunos detalles sobre el pizza
:
export
default
{
name
:
'PizzaComponent'
,
props
:
{
pizza
:
{
type
:
Object
,
required
:
true
}
}
}
Bastante sencillo. Sin embargo, al declarar pizza
como un tipo Object
, asumimos que el padre siempre pasará el objeto adecuado con los campos apropiados (title
, image
, description
, quantity
, y price
) necesarios para que se renderice un pizza
.
Esta suposición puede dar lugar a un problema. Puesto que pizza
acepta datos del tipo Object
, cualquier componente que utilice PizzaComponent
puede pasar cualquier dato de objeto a la prop pizza
sin los campos reales necesarios para un pizza
, como en el Ejemplo 4-8.
Ejemplo 4-8. Utilizar el componente Pizza con datos erróneos
<
template
>
<
div
>
<
h2
>
Bad usage of Pizza component</
h2
>
<
pizza-component
:pizza
=
"{ name: 'Pinia', description: 'Hawaiian pizza' }"
/>
</
div
>
</
template
>
El código anterior da como resultado una representación rota de la interfaz de usuario de PizzaComponent
, donde sólo está disponible description
, y el resto de los campos están vacíos (con una imagen rota), como se muestra en la Figura 4-3.
TypeScript tampoco podrá detectar aquí la discordancia de tipos de datos, ya que realiza la comprobación de tipos según el tipo declarado de pizza
: el genérico Object
. Otro problema potencial es que pasar pizza
en el formato incorrecto de las propiedades del nido puede hacer que la aplicación se bloquee. Por tanto, para evitar estos accidentes, utilizamos declaraciones de tipo personalizadas.
Podemos definir la clase Pizza
y declarar la prop pizza
de tipo Pizza
como se muestra en el Ejemplo 4-9.
Ejemplo 4-9. Declarar un tipo personalizado Pizza
class
Pizza
{
title
:
string
;
description
:
string
;
image
:
string
;
quantity
:
number
;
price
:
number
;
constructor
(
title
:
string
,
description
:
string
,
image
:
string
,
quantity
:
number
,
price
:
number
)
{
this
.
title
=
title
this
.
description
=
description
this
.
image
=
image
this
.
quantity
=
quantity
this
.
price
=
price
}
}
export
default
{
name
:
'PizzaComponent'
,
props
:
{
pizza
:
{
type
:
Pizza
,
required
:
true
}
}
}
Alternativamente, puedes utilizar interface
o type
de TypeScript para definir tu tipo personalizado en lugar de Class
. Sin embargo, en tales escenarios, debes utilizar el tipo PropType
del paquete vue
, con la siguiente sintaxis, para mapear el tipo declarado al prop objetivo :
type
:
Object
as
PropType
<
Your
-
Custom
-
Type
>
En su lugar, reescribamos la clase Pizza
como interface
(Ejemplo 4-10).
Ejemplo 4-10. Declarar un tipo personalizado Pizza utilizando la API de interfaz de TypeScript
import
type
{
PropType
}
from
'vue'
interface
Pizza
{
title
:
string
;
description
:
string
;
image
:
string
;
quantity
:
number
;
price
:
number
;
}
export
default
{
name
:
'PizzaComponent'
,
props
:
{
pizza
:
{
type
:
Object
as
PropType
<
Pizza
>
,
required
:
true
}
}
}
Cuando utilices PizzaComponent
con un formato de datos incorrecto, TypeScript detectará y resaltará el error adecuadamente.
Nota
Vue realiza la validación de tipos durante el tiempo de ejecución, mientras que TypeScript realiza la comprobación de tipos durante el tiempo de compilación. Por lo tanto, es una buena práctica utilizar tanto la comprobación de tipos de Vue como la de TypeScript para asegurarte de que tu código está libre de errores .
Declarar objetos utilizando defineProps() y withDefaults()
Como aprendimos en "configuración" , a partir de Vue 3.x, Vue ofrece la sintaxis <script setup>
para declarar un componente funcional sin la clásica API de Opciones. Dentro de este bloque <script setup>
, puedes utilizar defineProps()
para declarar props, como se muestra en el Ejemplo 4-11 .
Ejemplo 4-11. Declaración de accesorios con defineProps()
y <script setup>
<
script
setup
>
import
{
defineProps
}
from
'vue'
const
props
=
defineProps
({
name
:
{
type
:
String
,
default
:
"Hello from the child component."
}
})
</
script
>
Gracias a TypeScript, también podemos declarar el tipo aceptado para defineProps()
por componente con validación de tipo en tiempo de compilación, como se muestra en el Ejemplo 4-12.
Ejemplo 4-12. Declaración de accesorios con defineProps()
y TypeScript type
<
script
setup
>
import
{
defineProps
}
from
'vue'
type
ChildProps
=
{
name
?:
string
}
const
props
=
defineProps
<
ChildProps
>()
</
script
>
En este caso, para declarar el valor por defecto de la proposición message
, tenemos que envolver la llamada a defineProps()
con withDefaults()
, como en el Ejemplo 4-13.
Ejemplo 4-13. Declaración de accesorios con defineProps()
y withDefaults()
import
{
defineProps
,
withDefaults
}
from
'vue'
type
ChildProps
=
{
name
?:
string
}
const
props
=
withDefaults
(
defineProps
<
ChildProps
>
(),
{
name
:
'Hello from the child component.'
})
Uso de defineProps() con TypeScript Type Checking
No podemos combinar la comprobación de tipos en tiempo de ejecución y en tiempo de compilación si utilizamos defineProps()
. Recomiendo utilizar defineProps()
en el enfoque del Ejemplo 4-11, para una mejor legibilidad y una combinación de la comprobación de tipos de Vue y TypeScript.
Hemos aprendido a declarar props para pasar datos sin procesar en un componente Vue, con comprobación de tipos y validación. A continuación, exploraremos cómo pasar funciones como emisores de eventos personalizados a un componente hijo .
Comunicación entre componentes coneventos personalizados
Vue trata los datos pasados a un componente hijo mediante props como datos de sólo lectura y sin procesar. El flujo de datos unidireccional garantiza que el componente padre es el único que puede actualizar el dato prop. A menudo queremos actualizar un dato prop específico y sincronizarlo con el componente padre. Para ello, utilizamos el campo emits
de las opciones del componente para declarar eventos personalizados .
Por ejemplo, una lista de tareas pendientes o el componente ToDoList
. Este ToDoList
utilizaráToDoItem
como componente hijo para mostrar una lista de tareas con el código del Ejemplo 4-14.
Ejemplo 4-14. ComponenteToDoList
<
template
>
<
ul
style
=
"list-style: none;"
>
<
li
v-for
=
"task in tasks"
:key
=
"task.id"
>
<
ToDoItem
:task
=
"task"
/>
</
li
>
</
ul
>
</
template
>
<
script
lang
=
"ts"
>
import
{
defineComponent
}
from
'vue'
import
ToDoItem
from
'./ToDoItem.vue'
import
type
{
Task
}
from
'./ToDoItem'
export
default
defineComponent
({
name
:
'ToDoList'
,
components
:
{
ToDoItem
},
data
()
{
return
{
tasks
:
[
{
id
:
1
,
title
:
'Learn Vue'
,
completed
:
false
},
{
id
:
2
,
title
:
'Learn TypeScript'
,
completed
:
false
},
{
id
:
3
,
title
:
'Learn Vite'
,
completed
:
false
},
]
as
Task
[]
}
}
})
</
script
>
Y ToDoItem
es un componente que recibe un atributo task
y muestra un input
como casilla de verificación para que el usuario marque la tarea como completada o no. Este elemento input
recibe task.completed
como valor inicial del atributo checked
. Veamos el Ejemplo 4-15.
Ejemplo 4-15. ComponenteToDoItem
<
template
>
<
div
>
<
input
type
=
"checkbox"
:checked
=
"task.completed"
/>
<
span
>
{{ task.title }}</
span
>
</
div
>
</
template
>
<
script
lang
=
"ts"
>
import
{
defineComponent
,
type
PropType
}
from
'vue'
export
interface
Task
{
id
:
number
;
title
:
string
;
completed
:
boolean
;
}
export
default
defineComponent
({
name
:
'ToDoItem'
,
props
:
{
task
:
{
type
:
Object
as
PropType
<
Task
>,
required
:
true
,
}
},
})
</
script
>
Cuando un usuario active esta casilla input
, queremos emitir un evento llamado task-completed-toggle
para informar del valor task.completed
de la tarea específica al componente padre. Podemos hacerlo declarando primero el evento en el campo emits
de las opciones del componente(Ejemplo 4-16).
Ejemplo 4-16. ComponenteToDoItem
con emits
/** ToDoItem.vue */
export
default
defineComponent
({
//...
emits
:
[
'task-completed-toggle'
]
})
A continuación, creamos un nuevo método onTaskCompleted
para emitir el evento task-completed-toggle
con el nuevo valor de task.completed
de la casilla de verificación y task.id
como carga útil del evento(Ejemplo 4-17).
Ejemplo 4-17. ComponenteToDoItem
con un método para emitir el evento task-completed-toggle
/** ToDoItem.vue */
export
default
defineComponent
({
//...
methods
:
{
onTaskCompleted
(
event
:
Event
)
{
this
.
$emit
(
"task-completed-toggle"
,
{
...
this
.
task
,
completed
:
(
event
.
target
as
HTMLInputElement
)
?
.
checked
,
});
},
}
})
Nota
Utilizamos defineComponent
para envolver las opciones del componente y crear un componente compatible con TypeScript. Utilizar define
Component
no es necesario para los componentes simples, pero debes utilizarlo para acceder a otras propiedades de datos de this
dentro de los métodos, ganchos o propiedades computadas de los componentes. De lo contrario, TypeScript lanzará un error .
A continuación, vinculamos el método onTaskCompleted
al evento change
del elemento input
, como se muestra en el Ejemplo 4-18.
Ejemplo 4-18. Plantilla actualizada del componenteToDoItem
<
div
>
<
input
type
=
"checkbox"
:checked
=
"task.completed"
@
change
=
"onTaskCompleted"
/>
<
span
>
{{ task.title }}</
span
>
</
div
>
Ahora, en el componente padre <ToDoList>
de ToDoItem
, podemos vincular el evento task-completed-toggle
a un método utilizando la notación @
, con la plantilla del Ejemplo 4-19.
Ejemplo 4-19. Plantilla actualizada del componenteToDoList
<
template
>
<
ul
style
=
"list-style: none;"
>
<
li
v-for
=
"task in tasks"
:key
=
"task.id"
>
<
ToDoItem
:task
=
"task"
@
task-completed-toggle
=
"onTaskCompleted"
/>
</
li
>
</
ul
>
</
template
>
El método onTaskCompleted
del componente padre <ToDoList>
recibirá la carga útil del evento task-completed-toggle
, y actualizará el valor task.completed
de la tarea específica en la matriz tasks
, como en el Ejemplo 4-20.
Ejemplo 4-20. Script del componenteToDoList
con un método para gestionar el evento task-completed-toggle
//...
export
default
{
//...
methods
:
{
onTaskCompleted
(
payload
:
{
id
:
number
;
completed
:
boolean
})
{
const
index
=
this
.
tasks
.
findIndex
(
t
=>
t
.
id
===
payload
.
id
)
if
(
index
<
0
)
return
this
.
tasks
[
index
].
completed
=
payload
.
completed
}
}
}
Estos bloques de código mostrarán la página de la Figura 4-4.
Vue actualizará los datos relacionados en ToDoList
y, en consecuencia, mostrará la instancia del componente ToDoItem
correspondiente. Puedes activar la casilla para marcar una tarea como completada. La Figura 4-5 muestra que podemos detectar el evento del componente utilizandolas Devtools de Vue.
Definir eventos personalizados con defineEmits()
De forma similar a "Declarar Props utilizando defineProps() y withDefaults()", dentro de un bloque de código <script setup>
, puedes utilizar defineEmits()
para definir eventos personalizados. La función defineEmits()
acepta el mismo tipo de parámetro de entrada que emits
acepta :
const
emits
=
defineEmits
([
'component-event'
])
A continuación, devuelve una instancia de función que podemos utilizar para invocar un evento concreto del componente:
emits
(
'component-event'
,
[...
arguments
])
Así podemos escribir la sección de script de ToDoItem
como en el Ejemplo 4-21.
Ejemplo 4-21. ComponenteToDoItem
con el evento personalizado utilizando defineEmits()
<
script
lang
=
"ts"
setup
>
//...
const
props
=
defineProps
({
task
:
{
type
:
Object
as
PropType
<
Task
>,
required
:
true
,
}
});
const
emits
=
defineEmits
([
'task-completed-toggle'
])
const
onTaskCompleted
=
(
event
:
Event
)
=>
{
emits
(
"task-completed-toggle"
,
{
id
:
props
.
task
.
id
,
completed
:
(
event
.
target
as
HTMLInputElement
)
?
.
checked
,
});
}
</
script
>
Ten en cuenta que aquí no necesitamos utilizar defineComponent
, ya que no hay ninguna instancia de this
disponible dentro del bloque de código <script setup>
.
Para mejorar la comprobación de tipos, puedes utilizar una declaración de tipo para el evento task-completed-toggle
en lugar de una cadena simple. Mejoremos la declaración emits
del Ejemplo 4-21 para utilizar el tipo EmitEvents
como se muestra en el Ejemplo 4-22.
Ejemplo 4-22. Evento personalizado utilizando defineEmits()
y declaración de sólo tipo
// Declare the emit type
type
EmitEvents
=
{
(
e
:
'task-completed-toggle'
,
task
:
Task
)
:
void
;
}
const
emits
=
defineEmits
<
EmitEvents
>
()
Este enfoque ayuda a garantizar que vinculas el método correcto al evento declarado. Como se ha visto para el evento task-complete-toggle
, cualquier declaración de evento debe seguir el mismo patrón:
(e: 'component-event', [...arguments]): void
En la sintaxis anterior, e
es el nombre del evento, y arguments
son todas las entradas pasadas al emisor del evento. En el caso del evento task-completed-toggle
, el argumento de su emisor es task
de tipo Task
.
emits
es una potente función que te permite habilitar la comunicación bidireccional entre un componente padre y uno hijo sin romper el mecanismo de flujo de datos de Vue. Sin embargo, props
y emits
sólo son beneficiosos cuando quieres una comunicación directa de datos.
Debes utilizar un enfoque diferente para pasar datos de un componente a su nieto o descendiente. En la siguiente sección, veremos cómo utilizar las API provide
y inject
para pasar datos de un componente padre a su componente hijo o nieto .
Comunicarse entre componentes conel patrón proporcionar/inyectar
Para establecer la comunicación de datos entre un componente antepasado y sus descendientes, la API provide/inject
es una opción razonable. El campo provide
pasa datos desde el ancestro, mientras que inject
se asegura de que Vue inyecte los datos proporcionados en cualquier descendiente objetivo .
Utilizar provide para pasar datos
El campo de opción del componente provide
acepta dos formatos: un objeto de datos o unafunción.
provide
puede ser un objeto que contenga datos a inyectar, en el que cada propiedad represente un tipo de dato (clave, valor). En el siguiente ejemplo, ProductList
proporciona un valor de datos, selectedIds
, con el valor [1]
a todos sus descendientes(Ejemplo 4-23).
Ejemplo 4-23. Pasar selectedIds
utilizando provide en el componente ProductList
export
default
{
name
:
'ProductList'
,
//...
provide
:
{
selectedIds
:
[
1
]
},
}
Otro tipo de formato para provide
es una función que devuelve un objeto que contiene los datos disponibles para inyectar a los descendientes. Una ventaja de este tipo de formato es que podemos acceder a la instancia this
y asignar datos dinámicos o un método del componente a los campos relevantes del objeto devuelto. A partir del Ejemplo 4-23, podemos reescribir el campo provide
como una función, tal y como se muestra en el Ejemplo 4-24.
Ejemplo 4-24. Pasar selectedIds
utilizando provide en el componente ProductList
como una función
export
default
{
//...
provide
()
{
return
{
selectedIds
:
[
1
]
}
},
//...
}
<
/script>
Nota
A diferencia de props
, puedes pasar una función y hacer que el descendiente de destino la active utilizando el campo provide
. Hacerlo así permite enviar datos de vuelta al componente padre. Sin embargo, Vue considera este enfoque un antipatrón, y debes utilizarlo con precaución.
En este punto, nuestro ProductList
pasa algunos valores de datos a su descendiente utilizandoprovide
. A continuación, debemos inyectar los valores proporcionados para que operen dentro de un descendiente.
Utilizar inject para recibir datos
Al igual que props
, el campo inject
puede aceptar una matriz de cadenas, cada una de las cuales representa la clave de datos proporcionada (inject: [selectedId]
) o un objeto.
Cuando se utiliza inject
como campo de objeto, cada una de sus propiedades es un objeto, con la clave que presenta la clave de datos local utilizada dentro del componente y las siguientespropiedades:
{ from?: string; default: any }
Aquí, from
es opcional si la clave de la propiedad es la misma que la clave proporcionada por el antepasado. Tomemos el Ejemplo 4-23 con el selectedIds
como dato proporcionado por ProductList
a sus descendientes, por ejemplo. Podemos calcular un ProductComp
que reciba los datos proporcionados, selectedIds
, de ProductList
y renombrarlo a current
SelectedIds
para utilizarlo localmente, como se muestra en el Ejemplo 4-25.
Ejemplo 4-25. Inyectar datos proporcionados en ProductComp
<
script
lang
=
'ts'
>
export
default
{
//...
inject
:
{
currentSelectedIds
:
{
from
:
'selectedIds'
,
default
:
[]
},
},
}
</
script
>
En este código, Vue tomará el valor de selectedIds
inyectado y lo asignará a un campo de datos local, currentSelectedIds
, o utilizará su valor por defecto []
si no hay ningún valor inyectado.
Dentro de la sección Componentes de la pestaña Vue de las Herramientas de desarrollo del navegador, al seleccionar el ProductComp
del árbol de componentes (el panel de la izquierda), puedes depurar la indicación del cambio de nombre de los datos inyectados (el panel de la derecha), como se muestra en la Figura 4-6.
Nota
Los ganchos equivalentes en la API de composición para provide/inject
son provide()
y inject()
, respectivamente .
Ahora ya sabemos cómo utilizar provide
y inject
para pasar datos entre componentes de forma eficaz, sin perforar props. Exploremos cómo podemos renderizar una sección de contenido específica de un elemento a otra ubicación en el DOM con el componente <Teleport>
.
API de teletransporte
Debido a restricciones de estilo, a menudo necesitamos implementar un componente que contiene elementos que Vue debería renderizar en una ubicación diferente en el DOM real para conseguir un efecto visual completo. En tales casos, solemos tener que "teletransportar" esos elementos al lugar deseado desarrollando una solución compleja, lo que conlleva un pésimo impacto en el rendimiento, consumo de tiempo, etc. Para resolver este reto del "teletransporte", Vue ofrece elcomponente <Teleport>
.
El componente <Teleport>
acepta una prop to
, que indica el contenedor de destino, ya sea el selector de consulta de un elemento o el elemento HTML deseado. Supongamos que tenemos un componente House
que tendrá una sección de Cielo y nubes que necesita que el motor Vue la teletransporte a un elemento DOM designado #sky
, como en el Ejemplo 4-26.
Ejemplo 4-26. Componente casa con Teleport
<
template
>
<
div
>
This is a house</
div
>
<
Teleport
to
=
"#sky"
>
<
div
>
Sky and clouds</
div
>
</
Teleport
>
</
template
>
En nuestro App.vue
, añadimos un elemento section
con el id de destino sky
encima del componente House
, como en el Ejemplo 4-27.
Ejemplo 4-27. Plantilla de App.vue
con el componente House
<
template
>
<
section
id
=
"sky"
/>
<
section
class
=
"wrapper"
>
<
House
/>
</
section
>
</
template
>
La Figura 4-7 muestra las salidas del código.
Cuando inspeccionas el árbol DOM utilizando la pestaña Elementos de las Herramientas de desarrollo del navegador, "Cielo y nubes" aparece como anidado dentro de <section id="sky">
en su lugar(Figura 4-8).
También puedes desactivar temporalmente el desplazamiento del contenido dentro de una instancia del componente <Teleport>
con su prop booleana disabled
. Este componente es útil cuando quieres mantener la estructura de árbol DOM, y que Vue mueva sólo el contenido deseado a la ubicación de destino cuando sea necesario. Un caso de uso cotidiano para Teleport
es un modal, que implementaremos a continuación.
Envolver ambas secciones bajo un padre
El componente de destino para el teletransporte debe existir en el DOM antes de montar <Teleport>
. En el Ejemplo 4-27, si envuelves ambas instancias de section
bajo un elemento main
, el componente <Teleport>
no funcionará como se espera. Consulta el apartado "Problema de renderización mediante teletransporte" para obtener más detalles.
Implementación de un modal con teletransporte y el elemento <diálogo>.
Un modal es una ventana de diálogo que aparece en la parte superior de una pantalla y bloquea la interacción del usuario con la página principal. El usuario debe interactuar con el modal para descartarlo y luego vuelve a la página principal .
Un modal es muy útil para mostrar notificaciones esenciales que requieren toda la atención del usuario y que sólo deben aparecer una vez.
Diseñemos un modal básico. Al igual que un diálogo, un modal debe contener los siguientes elementos(Figura 4-9):
-
Un fondo que cubre toda la pantalla donde el modal aparece en la parte superior y bloquea las interacciones del usuario con la página actual.
-
Una ventana modal que contiene el contenido del modal, incluyendo un
header
con un título y un botón de cierre, una sección de contenidomain
y una secciónfooter
con un botón de cierre predeterminado. Estas tres secciones deben ser personalizables mediante las ranuras .
Basándonos en el diseño anterior, implementamos una plantilla de componentes Modal
utilizando el elemento HTML <dialog>
en el Ejemplo 4-28.
Ejemplo 4-28. ComponenteModal
<
template
>
<
dialog
:open
=
"open"
>
<
header
>
<
slot
name
=
"m-header"
>
<
h2
>
{{ title }}
<
/
h2
>
<
button
>
X
<
/
button
>
<
/
slot
>
<
/
header
>
<
main
>
<
slot
name
=
"m-main"
/
>
<
/
main
>
<
footer
>
<
slot
name
=
"m-footer"
>
<
button
>
Close
<
/
button
>
<
/
slot
>
<
/
footer
>
<
/
dialog
>
<
/
template
>
En el código anterior, utilizamos tres secciones de ranura para que el usuario pueda personalizarlas:
La cabecera del modal (
m-header
)El contenido principal (
m-main
)El pie de página del modal (
m-footer
)
También vinculamos el atributo open
del elemento <dialog>
a una propiedad de datos local open
para controlar la visibilidad del modal (visible/oculta). Además, mostramos la propiedad title
como título predeterminado del modal . Ahora vamos a implementar las opciones del componente Modal
, que recibe dos propiedades: open
y title
, como en el Ejemplo 4-29.
Ejemplo 4-29. Añadir accesorios al componente Modal
<
script
lang
=
"ts"
>
import
{
defineComponent
}
from
'vue'
export
default
defineComponent
({
name
:
'Modal'
,
props
:
{
open
:
{
type
:
Boolean
,
default
:
false
,
},
title
:
{
type
:
String
,
default
:
'Dialog'
,
},
},
})
</
script
>
Cuando un usuario haga clic en el botón de cierre del modal o en el botón "X" de la cabecera, debe cerrarse solo. Como controlamos la visibilidad del modal mediante la propiedad open
, necesitamos emitir un evento closeDialog
con el nuevo valor de open
desde el componente Modal
al padre. Declaremos emits
y un método close
que emita el evento objetivo como en el Ejemplo 4-30 .
Ejemplo 4-30. Declarar el evento closeDialog
para que lo emita Modal
<
script
lang
=
"ts"
>
/** Modal.vue */
import
{
defineComponent
}
from
'vue'
export
default
defineComponent
(
{
name
:
'Modal'
,
//...
emits
:
[
"closeDialog"
]
,
methods
:
{
close
(
)
{
this
.
$emit
(
"closeDialog"
,
false
)
;
}
,
}
,
}
)
<
/
script
>
emits
con un evento,closeDialog
close
que emite el eventocloseDialog
con el nuevo valor deopen
comofalse
A continuación, lo vinculamos a los elementos de acción pertinentes en el elemento <dialog>
utilizando la notación @
, como se muestra en el Ejemplo 4-31.
Ejemplo 4-31. Vinculación de un receptor de eventos a eventos de clic
<
template
>
<
dialog
:open
=
"open"
>
<
header
>
<
slot
name
=
"m-header"
>
<
h2
>
{{ title }}
<
/
h2
>
<
button
@
click
=
"close"
>
X
<
/
button
>
<
/
slot
>
<
/
header
>
<
main
>
<
slot
name
=
"m-main"
/
>
<
/
main
>
<
footer
>
<
slot
name
=
"m-footer"
>
<
button
@
click
=
"close"
>
Close
<
/
button
>
<
/
slot
>
<
/
footer
>
<
/
dialog
>
<
/
template
>
@click
controlador de eventos para el botón "X" de la cabecera@click
controlador de eventos para el botón de cierre por defecto del pie de página
A continuación, tenemos que envolver el elemento dialog
con un componente <Teleport>
para moverlo fuera del árbol DOM del componente padre. También pasamos la propiedad to
al componente<Teleport>
para especificar la ubicación de destino: un elemento HTML con un id, modal
. Por último, vinculamos la proposición disabled
al valor open
del componente para asegurarnos de que Vue mueva sólo el contenido del componente modal a la ubicación deseada cuando sea visible(Ejemplo 4-32).
Ejemplo 4-32. Utilizando el componente <Teleport>
<
template
>
<
teleport
to
=
"#modal"
:disabled
=
"!open"
>
<
dialog
ref
=
"dialog"
:open
=
"open"
>
<
header
>
<
slot
name
=
"m-header"
>
<
h2
>
{{ title }}
<
/
h2
>
<
button
@
click
=
"close"
>
X
<
/
button
>
<
/
slot
>
<
/
header
>
<
main
>
<
slot
name
=
"m-main"
/
>
<
/
main
>
<
footer
>
<
slot
name
=
"m-footer"
>
<
button
@
click
=
"close"
>
Close
<
/
button
>
<
/
slot
>
<
/
footer
>
<
/
dialog
>
<
/
teleport
>
<
/
template
>
<Teleport>
componenteto
prop con la ubicación de destino con selector idmodal
disabled
prop con la condición cuando el valoropen
del componente es falso
Ahora vamos a probar nuestro componente Modal
en un WithModalComponent
añadiendo el siguiente código del Ejemplo 4-33 al WithModalComponent
.
Ejemplo 4-33. Uso del componente modal en WithModalComponent
<
template
>
<
h2
>
With Modal component</
h2
>
<
button
@
click
=
"openModal = true"
>
Open modal</
button
>
<
Modal
:open
=
"openModal"
title
=
"Hello World"
@
closeDialog
=
"toggleModal"
/>
</
template
>
<
script
lang
=
"ts"
>
import
{
defineComponent
}
from
"vue"
;
import
Modal
from
"./Modal.vue"
;
export
default
defineComponent
({
name
:
"WithModalComponent"
,
components
:
{
Modal
,
},
data
()
{
return
{
openModal
:
false
,
};
},
methods
:
{
toggleModal
(
newValue
:
boolean
)
{
this
.
openModal
=
newValue
;
},
},
});
</
script
>
Por último, añade un elemento <div>
con id modal
al elemento body
del archivo index.html
:
<
body
>
<
div
id
=
"app"
>
<
/
div
>
<
div
id
=
"modal"
>
<
/
div
>
<
script
type
=
"module"
src
=
"/src/main.ts"
>
<
/
script
>
<
/
body
>
Al hacerlo, Vue renderiza el contenido del componente Modal
en este div
con id modal
siempre que la prop open
se establezca en true
(Figura 4-10).
La Figura 4-11 muestra su aspecto en pantalla:
Y cuando el prop open
es false
, el div
con id modal
está vacío(Figura 4-12), y el modal es invisible en pantalla(Figura 4-13).
Llegados a este punto, tienes un componente modal que funciona. Sin embargo, el aspecto visual del modal no es exactamente tan bueno como queríamos; debería haber una superposición oscura sobre el contenido de la página principal cuando el modal está visible. Arreglemos este problema utilizando estilos CSS para el selector ::backdrop
en la sección <style>
del elemento modal:
<
style
scoped
>
dialog
::backdrop
{
background-color
:
rgba
(
0
,
0
,
0
,
0
.
5
);
}
</style>
Sin embargo, esto no cambiará la apariencia del fondo del modal. Este comportamiento se debe a que el navegador aplica las reglas del selector CSS ::backdrop
al diálogo sólo cuando abrimos el diálogo utilizando el método dialog.showModal()
, y no cambiando el atributo open
. Para solucionar este problema, debemos realizar las siguientes modificaciones en nuestro componente Modal
:
-
Añade una referencia directa al elemento
<dialog>
asignando un valor "diálogo" al atributoref
:<
dialog
:open
=
"open"
ref
=
"dialog"
>
<!--...-->
</
dialog
>
-
Dispara
$refs.dialog.showModal()
o$refs.dialog.close()
en el elementodialog
cada vez que el puntalopen
cambie respectivamente conwatch
:watch
:
{
open
(
newValue
)
{
const
element
=
this
.
$refs
.
dialog
as
HTMLDialogElement
;
if
(
newValue
)
{
element
.
showModal
();
}
else
{
element
.
close
();
}
},
},
-
Elimina el enlace original del atributo
open
del elemento<dialog>
:<
dialog
ref
=
"dialog"
>
<!--...-->
</
dialog
>
-
Elimina el uso del atributo
disabled
en el componente<teleport>
:<
teleport
to
=
"#modal"
>
<!--...-->
</
teleport
>
Al abrir el modal utilizando el método incorporado showModal()
, el navegador añadirá un pseudoelemento ::backdrop
al elemento real <dialog>
en el DOM, y al mover dinámicamente el contenido del elemento a la ubicación de destino se desactivará esta funcionalidad, dejando el modal sin el telón de fondo deseado.
También reposicionamos el modal al centro de la página y encima de otros elementos añadiendo las siguientes reglas CSS al selector dialog
:
dialog
{
position
:
fixed
;
z-index
:
999
;
inset
-
block
-
start
:
30%
;
inset
-
inline
-
start
:
50%
;
width
:
300px
;
margin
-
inline
-
start
:
-150px
;
}
La salida será como se muestra en la Figura 4-14 cuando el modal esté visible.
Hemos aprendido a implementar un componente reutilizable Modal
utilizando Teleport
y hemos explorado diferentes casos de uso con cada una de las características de los elementos incorporados <dialog>
. También hemos aprendido a utilizar el selector CSS ::backdrop
para dar estilo al telón de fondo del modal.
Como habrás observado, hemos establecido la ubicación de destino div
para que el modal sea hijo directo de body
, fuera del elemento de entrada <div id="app">
de la aplicación Vue. ¿Qué ocurre si queremos mover el destino del modal div
al interior del componente de entrada App.vue
de la aplicación Vue? Averigüémoslo en la siguiente sección.
Problema de renderizado con teletransporte
Para entender el problema que supone utilizar Teleport
para representar el modal dentro de un componente hijo del componente App.vue
, movamos primero el <div id="modal"></div>
de index.html
a App.vue
, después la instancia WithModalComponent
:
<
template
>
<
section
class
=
"wrapper"
>
<
WithModalComponent
/>
</
section
>
<
div
id
=
"modal"
></
div
>
</
template
>
Después de ejecutar tu aplicación, puedes ver que el navegador no renderiza el modal a pesar de las veces que haces clic en el botón Open modal
. Y la consola muestra el siguiente error
Debido al mecanismo de orden de renderizado de Vue, el padre espera a que se rendericen los hijos antes de renderizarse a sí mismo. Los hijos se muestran en el orden en que aparecen en la sección template
del padre. En este caso, WithModalComponent
se renderiza primero. Así, Vue renderiza el elemento <dialog>
y empieza a mover el contenido del componente a la ubicación de destino antes de renderizar el elemento ParentComponent
. Sin embargo, como ParentComponent
aún está esperando a que WithModalComponent
termine de renderizarse, el elemento <div id="modal">
aún no existe en el DOM. Como resultado, Vue no puede localizar la ubicación de destino y realizar el movimiento correcto, y no puede renderizar el elemento <dialog>
dentro del elemento <div id="modal">
, de ahí el error.
Una solución para eludir esta limitación es poner el elemento de destino<div id="modal">
antes de WithModalComponent
:
<
template
>
<
div
id
=
"modal"
></
div
>
<
section
class
=
"wrapper"
>
<
WithModalComponent
/>
</
section
>
</
template
>
Esta solución garantiza que el div
de destino esté disponible antes de que Vue renderice el elemento Modal
y mueva el contenido. Otro enfoque consiste en utilizar el atributo disabled
para posponer el proceso de desplazamiento del contenido de Modal
durante la renderización hasta que el usuario haga clic en el botón Open modal
. Ambas opciones tienen pros y contras, y debes elegir la que mejor se adapte a tus necesidades.
La solución más habitual es insertar el elemento de destino como hijo directo del elemento body
y aislarlo del contexto de representación de Vue.
Una ventaja importante de utilizar <Teleport>
es conseguir el máximo efecto visual (como el modo de pantalla completa, modal, barra lateral, etc.) manteniendo la estructura jerárquica del código, el aislamiento de los componentes y la legibilidad .
Resumen
En este capítulo se ha explorado el concepto de diferentes enfoques en la comunicación de componentes utilizando las funciones integradas de Vue, como props
, emits
y provide/inject
. Aprendimos a utilizar estas funciones para pasar datos y eventos entre componentes manteniendo intacto el mecanismo de flujo de datos de Vue. También aprendimos a utilizar la API Teleport para renderizar un elemento fuera del árbol DOM del componente padre manteniendo su orden de aparición en el <template>
del componente padre. <Teleport>
es beneficioso para construir componentes que requieren una visualización alineada con el elemento principal de la página, como ventanas emergentes, diálogos, modales, etc.
En el próximo capítulo, exploraremos más a fondo la API de Composición y cómo utilizarla para componer componentes Vue entre sí.
Get Aprender Vue now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.