# Телепорты

Узнайте как использовать телепорты на бесплатном уроке Vue School

Vue поощряет создавать пользовательские интерфейсы, инкапсулируя их и связанное с ним поведение в компоненты. Вкладывая компоненты друг в друга можно получить дерево компонентов, из которого и будет строиться пользовательский интерфейс приложения.

Но иногда случается, что часть этого шаблона логически принадлежит компоненту, хотя с технической точки зрения было бы удобнее переместить эту часть шаблона в какое-нибудь другое место в DOM или даже вне приложения Vue.

Один из частых сценариев — создание компонента, содержащего в себе полноэкранное модальное окно. В большинстве случаев, удобней когда логика модального окна внутри компонента, но позиционирование модального окна с помощью CSS становится сложной задачей, что может потребовать даже кардинальных изменений в композиции компонентов.

Рассмотрим следующую структуру HTML:

<body>
  <div id="app" style="position: relative;">
    <h3>Подсказки с помощью телепортов Vue 3</h3>
    <div>
      <modal-button></modal-button>
    </div>
  </div>
</body>
1
2
3
4
5
6
7
8

Давайте взглянем на компонент modal-button.

В нём есть button для открытия модального окна и элемент div с классом .modal, где будет располагаться содержимое модального окна, а также кнопка для его закрытия.

const app = Vue.createApp({})

app.component('modal-button', {
  template: `
    <button type="button" @click="modalOpen = true">
      Открыть полноэкранное модальное окно!
    </button>

    <div v-if="modalOpen" class="modal">
      <div>
        Информация в модальном окне!
        <button type="button" @click="modalOpen = false">
          Закрыть
        </button>
      </div>
    </div>
  `,
  data() {
    return {
      modalOpen: false
    }
  }
})

app.mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

При использовании этого компонента внутри исходной структуры HTML становится видна проблема — модальное окно отрисовывается внутри глубоко вложенного div, а его стиль position: absolute получает родителя относительно расположенного div по ссылке.

Телепорт же предоставляет прозрачный способ для управления, в каком месте DOM нужно отрисовать часть HTML, не требуя использовать глобальное состояние или разделять на два компонента.

Доработаем компонент modal-button, чтобы использовать <teleport> и скажем Vue «телепортировать часть его HTML в тег <body>».

app.component('modal-button', {
  template: `
    <button type="button" @click="modalOpen = true">
      Открыть полноэкранное модальное окно! (с помощью телепорта!)
    </button>

    <teleport to="body">
      <div v-if="modalOpen" class="modal">
        <div>
          Я телепортированное модальное окно!
          (Мой родитель "body")
          <button type="button" @click="modalOpen = false">
            Закрыть
          </button>
        </div>
      </div>
    </teleport>
  `,
  data() {
    return {
      modalOpen: false
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

Теперь, при нажатии кнопки открытия модального окна, Vue корректно отобразит его содержимое в качестве дочернего тега body.

See the Pen Телепорты Vue 3 by Vue (@Vue) on CodePen.

# Использование с компонентами

Если в <teleport> расположен компонент Vue, то он всё равно логически остаётся дочерним компонентом родителя, в котором <teleport> использован:

const app = Vue.createApp({
  template: `
    <h1>Корневой экземпляр</h1>
    <parent-component />
  `
})

app.component('parent-component', {
  template: `
    <h2>Это родительский компонент</h2>
    <teleport to="#endofbody">
      <child-component name="John" />
    </teleport>
  `
})

app.component('child-component', {
  props: ['name'],
  template: `
    <div>Привет, {{ name }}</div>
  `
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

В этом случае, child-component остаётся потомком компонента parent-component и получит входной параметр name от него, даже когда будет отрисован в другом месте.

Это также означает, что инъекции из родительского компонента будут работать как и ожидается, и что дочерний компонент будет вложен в родительский компонент во Vue Devtools, вместо того, чтобы отображаться там, куда он телепортировался фактически.

# Использование нескольких телепортов на одной цели

Подобный сценарий может произойти при переиспользовании компонента <Modal>, когда одновременно может быть несколько его активных экземпляров. В таких случаях несколько компонентов <teleport> будут монтировать своё содержимое к одному и тому же элементу. Порядок будет определяться временем добавления — в целевом элементе более поздние будут располагаться после тех монтирований, что произошли раньше.

<teleport to="#modals">
  <div>A</div>
</teleport>
<teleport to="#modals">
  <div>B</div>
</teleport>

<!-- результат -->
<div id="modals">
  <div>A</div>
  <div>B</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12

Подробнее опции компонента <teleport> можно прочитать в справочнике API.