In questa lezione ci soffermeremo su un’altra novità di Vue 3, ovvero Teleport. Si tratta di un modo per spostare un elemento o un intero sottoalbero presenti all’interno del template di un componente in una differente area del DOM.
Tale funzionalità era disponibile in Vue 2.x solo tramite il plugin Portal Vue.
A partire da Vue 3, il team di sviluppo ha voluto integrarla come funzionalità nativa del framework. Inizialmente presentata col nome di Portals
, si è optato per cambiare il nome in Teleport al fine di evitare possibili conflitti con elementi che potrebbero essere introdotti in futuro nel linguaggio HTML.
Teleport: perché è stata introdotta in Vue 3
Abbiamo già brevemente spiegato il motivo e dal nome (Teleport) si intuisce qual è il suo scopo, ma vediamo più in dettaglio come possiamo sfruttare al meglio questa nuova funzione.
Immaginiamo di realizzare un’applicazione che presenta diversi componenti, uno di questi contiene la logica per attivare una modal box che vogliamo posizionare in maniera assoluta sopra tutti gli altri elementi del DOM. Potremmo chiederci come organizzare i componenti e soprattuto in quale di questi dovrà finire il template della modal box. In particolare, se il componente che attiva quest’ultima è annidato dentro altri, può risultare complesso andare poi a posizionare la modal box tramite delle regole CSS.
Ci troviamo allora nella situazione in cui da un lato la modal box appartiene da un punto di vista logico ad un componente e deve magari accedere ad alcune delle sue proprietà o reagire a qualche evento emesso da quest’ultimo, dall’altro potremmo avere la necessità di inserire il template della modal box in un componente differente perché diversamente potrebbe diventare difficile implementare delle regole CSS per una corretta disposizione all’interno della pagina.
Per risolvere questo tipo di problemi possiamo impiegare i Teleport che consentono di inserire uno o più elementi nel template del componente al quale appartengono da un punto di vista logico per poi essere teletrasportati in una diversa area del DOM.
Così facendo risolviamo le difficoltà esposte in precedenza ed otteniamo due benifici:
- Possibilità di accedere a proprietà e metodi del componente di appartenenza
- Semplicità nel disporre un elemento in un altro punto del DOM ed applicare determinate regole CSS per definire il suo stile
Come usare Teleport in Vue 3
Senza perderci in altre parole vediamo immediatamente un esempio che inizializziamo attraverso l’ultima versione di vue-cli. Lanciamo il comando vue create teleport-demo
e scegliamo poi con i tasti freccia l’opzione per realizzare un’applicazione in Vue 3.
Creiamo nella cartella src/components
un nuovo componente TeleportDemo
che riceve in ingresso una stringa attraverso la proprietà msg
.
// src/components/TeleportDemo.vue
<template>
<h2>Componente TeleportDemo</h2>
<teleport to="#teleport-destination">
<p class="teleport-demo-msg">
Un messaggio da TeleportDemo "{{ msg }}"
</p>
</teleport>
</template>
<script>
export default {
name: 'TeleportDemo',
props: ['msg']
}
</script>
Notiamo che nel template di TeleportDemo
abbiamo usato un nuovo componente <teleport>
che richiede un attributo to
il cui valore è una stringa. Questa rappresenta un selettore (simile ai selettori CSS) valido per identificare un altro elemento del DOM. Nel caso specifico, indichiamo che l’elemento di destinazione, in cui deve essere teletrasportato il paragrafo, è identificato da un attributo id
pari a ‘teleport-destination’. In alternativa, avremmo potuto indicare una certa classe prefissando il suo nome da un punto .nome-classe (esattamente come siamo abituati fare con i selettori CSS).
Importiamo poi TeleportDemo
nel componente App
e aggiungiamo nel template di quest’ultimo anche un elemento <div>
con id
pari a ‘teleport-destination’, ovvero l’elemento in cui deve essere teletrasportato il paragrafo con classe 'teleport-demo-msg'
presente in TeleportDemo
.
<template>
<h1>Teleport Demo</h1>
<img alt="Vue logo" src="./assets/logo.png">
<TeleportDemo msg='Messaggio passato dal componente App' />
<div id="teleport-destination"></div>
</template>
<script>
import TeleportDemo from './components/TeleportDemo.vue'
export default {
name: 'App',
components: {
TeleportDemo
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
#teleport-destination {
border: 2px dashed royalblue;
padding: 2rem;
}
</style>
Se lanciamo l’applicazione e apriamo il browser ci accorgiamo però che non tutto sembra funzionare correttamente e che, almeno in questo momento, la funzione Teleport presenta comunque dei limiti più o meno significativi. Attivando infatti gli strumenti per sviluppatori, visualizziamo un avviso il quale ci segnala che non è stato possibile identificare l’elemento di destinazione del Teleport. Ci viene fatto presente che dovrebbe idealmente trovarsi al di fuori dell’albero di elementi controllato dall’applicazione Vue.
Se apriamo il file src/main.js
, notiamo che il componente base dell’applicazione è identificato da #app
.
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
Apriamo allora il file index.html
situato nella cartella public
e, dopo averlo rimosso dal template del componente App
, aggiungiamo l’elemento div
di destinazione in cui dovrà essere teletrasportato il paragrafo con classe ‘teleport-demo-msg’.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- Teleport target element -->
<div id="teleport-destination"></div>
<!-- built files will be auto injected -->
</body>
</html>
Così facendo, il paragrafo presente fra i tag <teleport>
in TeleportDemo.vue
viene trasferito nella nuova destinazione come possiamo vedere nell’immagine sottostante.
È interessante osservare che, nonostante il paragrafo sia stato teletrasportato al di fuori del componente di appartenenza, ha ancora accesso alle sue proprietà. Infatti notiamo che viene visualizzata senza problemi la stringa di testo passata a TeleportDemo
dal componente App
attraverso la prop msg
.
Il componente <teleport>
supporta anche una proprietà disabled
che se pari a true
"disabilità il portale" lasciando gli elementi specificati fra i tag <teleport>
all’interno del componente di appartenenza.
Vediamo come funziona, modificando il file TeleportDemo.vue
come mostato sotto.
<template>
<h2 class="header">Componente TeleportDemo</h2>
<teleport to="#teleport-destination" disabled>
<p class="teleport-demo-msg">Un messaggio da TeleportDemo "{{ msg }}"</p>
</teleport>
</template>
<script>
export default {
name: 'TeleportDemo',
props: ['msg']
}
</script>
<style scoped>
</style>
Per semplicità ci siamo limitati ad applicare disabled
su <teleport>
, ma avremmo anche potuto associarlo a qualche proprietà o espressione con l’ausilio della direttiva v-bind
.
Se ora apriamo nuovamente il browser, notiamo che l’elemento target è vuoto ed il paragrafo resta nel componente di appartenenza.
Teleport consente inoltre di controllare se e quando teletrasportare uno o più elementi nell’elemento target. Possiamo farlo in modo programmatico usando per esempio la direttiva v-if
associata ad una certa proprietà di un componente.
Creiamo allora un nuovo esempio. In questo caso però il componente TeleportDemo
presenta un pulsante che se cliccato attiva una notifica rappresentata da un altro componente Notification
. Attraverso la direttiva v-if
decidiamo se inserire nel DOM o meno il componente e trasferirlo quindi nell’elemento target di destinazione.
Il componente App
resta sostanzialmente invariato rispetto all’esempio precedente così come il file public/index.html
.
// file: src/App.vue
<template>
<h1>Teleport Demo</h1>
<img alt="Vue logo" src="./assets/logo.png">
<TeleportDemo msg='Messaggio passato dal componente App' />
</template>
<script>
import TeleportDemo from './components/TeleportDemo.vue';
export default {
name: 'App',
components: {
TeleportDemo
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
#teleport-target {
text-align: center;
position: absolute;
top: 1rem;
right: 1rem;
max-width: 40vw;
}
</style>
<!-- file: public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<div id="teleport-target"></div>
<!-- built files will be auto injected -->
</body>
</html>
Nel file src/components/Notification.vue
creiamo un componente che si limita a mostrare un messaggio di testo e ad emettere un evento personalizzato close
quando si preme l’apposito pulsante.
<template>
<div class="notification-wrapper">
<p class="notification-text">{{ text }}</p>
<button class="close-btn" @click="$emit('close')">Hide</button>
</div>
</template>
<script>
export default {
name: 'Notification',
props: {
text: {
type: String,
default: '🚨🚨🚨🚨🚨'
}
},
emits: ['close']
}
</script>
<style scoped>
.notification-wrapper {
background-color: hsl(0, 0%, 100%);
padding: 1rem;
border-radius: 8px;
box-shadow: 0px 4px 8px hsla(0, 0%, 0%, 0.14);
font-family:'Roboto', 'Arial', Arial, sans-serif;
}
.close-btn {
border: 2px solid transparent;
padding: 0.5rem 1rem;
border-radius: 4px;
text-transform: uppercase;
font-weight: 700;
letter-spacing: 0.1em;
background-color: transparent;
cursor: pointer;
}
.close-btn:hover {
background-color: hsl(0, 0%, 87%);
}
.close-btn:focus, .close-btn:active {
outline: none;
border: 2px solid black;
}
</style>
Infine creiamo un componente TeleportDemo
che presenta un componente <teleport>
il quale teletrasporta il componente Notification
nell’elemento HTML del file public/index.html
identificato da id ‘teleport-target’. In src/components/TeleportDemo.vue
facciamo uso della Composition API di cui abbiamo già parlato nelle precedenti lezioni.
<template>
<h2 class="header">Componente TeleportDemo</h2>
<button @click="changeNotificationStatus(true)" :disabled="isVisible">
Show Notification
</button>
<teleport to="#teleport-target">
<notification
v-if="isVisible"
:text="msg"
@close="changeNotificationStatus"
/>
</teleport>
</template>
<script>
import { ref } from "vue";
import Notification from "@/components/Notification";
export default {
name: "TeleportDemo",
props: ["msg"],
components: {
Notification,
},
setup() {
const isVisible = ref(false);
function changeNotificationStatus(visible = false) {
isVisible.value = visible;
}
return {
isVisible,
changeNotificationStatus,
};
},
};
</script>
<style scoped></style>
Affinché il componente Notification
possa essere teletrasportato, deve essere presente nel DOM. Per controllare la sua presenza o meno usiamo la direttiva v-if
pilotata dal valore di isVisible
che viene a sua volta modificato tramite la funzione changeNotificationStatus
.
Cliccando sul pulsante ‘Show Notification’, isVisible
diventa pari a true
, la notifica viene inserita nel DOM e teletrasportata nell’elemento di destinazione specificato tramite la proprietà to
di <teleport>
, se invece attiviamo il pulsante Hide
presente nel componente Notification
, viene emesso un evento personalizzato (‘close’) in risposta al quale settiamo il valore di isVisible
a false
, rimuovendo di conseguenza lo stesso componente Notification
dal DOM.
Per concludere questa lezione, evidenziamo che un certo elemento può essere la destinazione per più componenti Teleport
.
Possiamo quindi avere più componenti che nel loro template presentano uno o più componenti <teleport>
con una proprietà to
avente lo stesso valore.
Così facendo componenti differenti possono teletrasportare degli elementi nello stesso nodo di destinazione da punti diversi del DOM.
Riepilogo
In questa lezione abbiamo illustrato quella che insieme alla Composition API è sicuramente una delle novità più interessanti introdotte in Vue 3 che consente di teletrasportare uno o più componenti in una diversa area dell’applicazione nonostante siano parte di un altro componente che viene magari inserito in un differente punto del DOM. Così facendo, un componente, presente all’interno del template di un altro, ha accesso alle proprietà e ai metodi di quest’ultimo, ma diventa più semplice posizionarlo all’interno della pagina tramite delle regole CSS, soprattutto nel caso sia necessario disporre degli elementi in maniera assoluta o su un livello superiore come avviene per modal box, notifiche ecc…