back to top

Teleport: nuova funzione introdotta in Vue 3

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.

opzioni per la creazione di un'applicazione in vue cli

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.

messaggio di errore: impossibile trovare componente di destinazione per la funzione teleport

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.

struttura DOM con teleport in Vue 3

È 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.

disposizione elementi con teleport in Vue 3

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.

https://vimeo.com/506476662

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…

Pubblicitร