Представьте модальное окно внутри глубоко вложенного компонента. Родительские элементы могут иметь overflow: hidden, z-index или transform, которые ограничивают позиционирование модального окна — оно будет обрезано или перекрыто.
Традиционное решение — рендерить модалку напрямую в <body>. Именно это делает Teleport.
<template>
<button @click="isOpen = true">Открыть</button>
<!-- Всё внутри <Teleport> будет отрендерено в <body>, -->
<!-- но логически остаётся частью этого компонента -->
<Teleport to="body">
<div v-if="isOpen" class="modal-overlay">
<div class="modal">
<h2>Модальное окно</h2>
<p>Это содержимое находится в body DOM</p>
<button @click="isOpen = false">Закрыть</button>
</div>
</div>
</Teleport>
</template>Принимает CSS-селектор или DOM-элемент:
<!-- По CSS-селектору -->
<Teleport to="#modal-container">...</Teleport>
<Teleport to=".popup-root">...</Teleport>
<!-- Динамически -->
<Teleport :to="targetElement">...</Teleport>
<!-- В head для мета-тегов -->
<Teleport to="head">
<title>Динамический заголовок</title>
</Teleport><!-- На мобильных — встроен в дерево, на десктопе — в body -->
<Teleport to="body" :disabled="isMobile">
<Sidebar />
</Teleport>При disabled=true содержимое рендерится на месте (без телепортации).
<!-- Первый уведомление -->
<Teleport to="#notifications">
<div class="toast">Файл сохранён</div>
</Teleport>
<!-- Второе уведомление от другого компонента -->
<Teleport to="#notifications">
<div class="toast">Новое сообщение</div>
</Teleport>Содержимое добавляется в целевой элемент **в порядке появления в DOM**. Первый Teleport идёт первым.
Если целевой элемент ещё не существует в момент рендера, используйте defer:
<Teleport defer to="#late-mount-target">
<MyComponent />
</Teleport>
<!-- Целевой элемент создаётся позже -->
<div id="late-mount-target"></div>**Реактивность и события сохраняются** — несмотря на другое место в DOM, компонент в Teleport:
// Внутри телепортированного компонента — всё работает:
const emit = defineEmits(['close'])
const parentData = inject('parentKey')**Стили не телепортируются** — CSS из scoped стилей применится корректно, но будьте осторожны с :global стилями.
Реализация паттерна "портал" — рендеринг контента вне текущего контейнера, аналог Teleport
// Эмулируем механику Teleport без браузера:
// компонент логически "живёт" в одном месте,
// но его вывод перенаправляется в другое.
// --- Виртуальный DOM (упрощённый) ---
class VNode {
constructor(tag, props = {}, children = []) {
this.tag = tag
this.props = props
this.children = children
}
toString(indent = 0) {
const pad = ' '.repeat(indent)
const attrs = Object.entries(this.props)
.map(([k, v]) => ` ${k}="${v}"`)
.join('')
const inner = this.children
.map(c => typeof c === 'string' ? pad + ' ' + c : c.toString(indent + 2))
.join('\n')
return `${pad}<${this.tag}${attrs}>${inner ? '\n' + inner + '\n' + pad : ''}</${this.tag}>`
}
}
// --- "Дерево DOM" ---
class DOMTree {
constructor() {
this.nodes = new Map() // id → VNode
this.root = new VNode('body', { id: 'body' })
this.nodes.set('body', this.root)
}
addContainer(id, parentId = 'body') {
const node = new VNode('div', { id })
this.nodes.set(id, node)
const parent = this.nodes.get(parentId)
if (parent) parent.children.push(node)
return node
}
getContainer(selector) {
// Простейший селектор: #id
const id = selector.startsWith('#') ? selector.slice(1) : selector
return this.nodes.get(id)
}
print() { console.log(this.root.toString()) }
}
// --- Teleport реализация ---
class Teleport {
constructor(dom, options = {}) {
this.dom = dom
this.to = options.to || 'body'
this.disabled = options.disabled || false
this.content = null
this._mountedIn = null
}
render(content, localContainer) {
this.content = content
const target = this.disabled
? localContainer
: this.dom.getContainer(this.to)
if (!target) {
throw new Error(`Teleport: целевой элемент "${this.to}" не найден`)
}
target.children.push(content)
this._mountedIn = target
console.log(
this.disabled
? `[Teleport disabled] контент добавлен в локальный контейнер`
: `[Teleport] контент телепортирован в "${this.to}"`
)
return this
}
destroy() {
if (this._mountedIn && this.content) {
const idx = this._mountedIn.children.indexOf(this.content)
if (idx !== -1) this._mountedIn.children.splice(idx, 1)
console.log('[Teleport] контент удалён из DOM при unmount компонента')
}
}
}
// --- Симуляция компонентного дерева ---
const dom = new DOMTree()
// Структура страницы
const app = dom.addContainer('app')
const header = dom.addContainer('header', 'app')
const main = dom.addContainer('main', 'app')
const deep = dom.addContainer('deep-nested', 'main')
const notifs = dom.addContainer('notifications', 'body')
console.log('=== Исходная структура ===')
dom.print()
// Компонент рендерит модалку через Teleport в body
const modal = new VNode('div', { class: 'modal' }, ['Модальное содержимое'])
const t1 = new Teleport(dom, { to: '#app' })
t1.render(modal, deep)
// Тосты в #notifications
const toast1 = new VNode('div', { class: 'toast' }, ['Файл сохранён'])
const toast2 = new VNode('div', { class: 'toast' }, ['Новое сообщение'])
new Teleport(dom, { to: '#notifications' }).render(toast1, null)
new Teleport(dom, { to: '#notifications' }).render(toast2, null)
console.log('\n=== После телепортации ===')
dom.print()
// disabled — рендер на месте
console.log('\n=== Teleport disabled ===')
const inlinePopup = new VNode('div', { class: 'popup' }, ['Встроенный попап'])
const t2 = new Teleport(dom, { to: '#notifications', disabled: true })
t2.render(inlinePopup, deep)
dom.print()
// Destroy — уничтожение компонента убирает контент
console.log('\n=== После destroy модалки ===')
t1.destroy()
dom.print()
Представьте модальное окно внутри глубоко вложенного компонента. Родительские элементы могут иметь overflow: hidden, z-index или transform, которые ограничивают позиционирование модального окна — оно будет обрезано или перекрыто.
Традиционное решение — рендерить модалку напрямую в <body>. Именно это делает Teleport.
<template>
<button @click="isOpen = true">Открыть</button>
<!-- Всё внутри <Teleport> будет отрендерено в <body>, -->
<!-- но логически остаётся частью этого компонента -->
<Teleport to="body">
<div v-if="isOpen" class="modal-overlay">
<div class="modal">
<h2>Модальное окно</h2>
<p>Это содержимое находится в body DOM</p>
<button @click="isOpen = false">Закрыть</button>
</div>
</div>
</Teleport>
</template>Принимает CSS-селектор или DOM-элемент:
<!-- По CSS-селектору -->
<Teleport to="#modal-container">...</Teleport>
<Teleport to=".popup-root">...</Teleport>
<!-- Динамически -->
<Teleport :to="targetElement">...</Teleport>
<!-- В head для мета-тегов -->
<Teleport to="head">
<title>Динамический заголовок</title>
</Teleport><!-- На мобильных — встроен в дерево, на десктопе — в body -->
<Teleport to="body" :disabled="isMobile">
<Sidebar />
</Teleport>При disabled=true содержимое рендерится на месте (без телепортации).
<!-- Первый уведомление -->
<Teleport to="#notifications">
<div class="toast">Файл сохранён</div>
</Teleport>
<!-- Второе уведомление от другого компонента -->
<Teleport to="#notifications">
<div class="toast">Новое сообщение</div>
</Teleport>Содержимое добавляется в целевой элемент **в порядке появления в DOM**. Первый Teleport идёт первым.
Если целевой элемент ещё не существует в момент рендера, используйте defer:
<Teleport defer to="#late-mount-target">
<MyComponent />
</Teleport>
<!-- Целевой элемент создаётся позже -->
<div id="late-mount-target"></div>**Реактивность и события сохраняются** — несмотря на другое место в DOM, компонент в Teleport:
// Внутри телепортированного компонента — всё работает:
const emit = defineEmits(['close'])
const parentData = inject('parentKey')**Стили не телепортируются** — CSS из scoped стилей применится корректно, но будьте осторожны с :global стилями.
Реализация паттерна "портал" — рендеринг контента вне текущего контейнера, аналог Teleport
// Эмулируем механику Teleport без браузера:
// компонент логически "живёт" в одном месте,
// но его вывод перенаправляется в другое.
// --- Виртуальный DOM (упрощённый) ---
class VNode {
constructor(tag, props = {}, children = []) {
this.tag = tag
this.props = props
this.children = children
}
toString(indent = 0) {
const pad = ' '.repeat(indent)
const attrs = Object.entries(this.props)
.map(([k, v]) => ` ${k}="${v}"`)
.join('')
const inner = this.children
.map(c => typeof c === 'string' ? pad + ' ' + c : c.toString(indent + 2))
.join('\n')
return `${pad}<${this.tag}${attrs}>${inner ? '\n' + inner + '\n' + pad : ''}</${this.tag}>`
}
}
// --- "Дерево DOM" ---
class DOMTree {
constructor() {
this.nodes = new Map() // id → VNode
this.root = new VNode('body', { id: 'body' })
this.nodes.set('body', this.root)
}
addContainer(id, parentId = 'body') {
const node = new VNode('div', { id })
this.nodes.set(id, node)
const parent = this.nodes.get(parentId)
if (parent) parent.children.push(node)
return node
}
getContainer(selector) {
// Простейший селектор: #id
const id = selector.startsWith('#') ? selector.slice(1) : selector
return this.nodes.get(id)
}
print() { console.log(this.root.toString()) }
}
// --- Teleport реализация ---
class Teleport {
constructor(dom, options = {}) {
this.dom = dom
this.to = options.to || 'body'
this.disabled = options.disabled || false
this.content = null
this._mountedIn = null
}
render(content, localContainer) {
this.content = content
const target = this.disabled
? localContainer
: this.dom.getContainer(this.to)
if (!target) {
throw new Error(`Teleport: целевой элемент "${this.to}" не найден`)
}
target.children.push(content)
this._mountedIn = target
console.log(
this.disabled
? `[Teleport disabled] контент добавлен в локальный контейнер`
: `[Teleport] контент телепортирован в "${this.to}"`
)
return this
}
destroy() {
if (this._mountedIn && this.content) {
const idx = this._mountedIn.children.indexOf(this.content)
if (idx !== -1) this._mountedIn.children.splice(idx, 1)
console.log('[Teleport] контент удалён из DOM при unmount компонента')
}
}
}
// --- Симуляция компонентного дерева ---
const dom = new DOMTree()
// Структура страницы
const app = dom.addContainer('app')
const header = dom.addContainer('header', 'app')
const main = dom.addContainer('main', 'app')
const deep = dom.addContainer('deep-nested', 'main')
const notifs = dom.addContainer('notifications', 'body')
console.log('=== Исходная структура ===')
dom.print()
// Компонент рендерит модалку через Teleport в body
const modal = new VNode('div', { class: 'modal' }, ['Модальное содержимое'])
const t1 = new Teleport(dom, { to: '#app' })
t1.render(modal, deep)
// Тосты в #notifications
const toast1 = new VNode('div', { class: 'toast' }, ['Файл сохранён'])
const toast2 = new VNode('div', { class: 'toast' }, ['Новое сообщение'])
new Teleport(dom, { to: '#notifications' }).render(toast1, null)
new Teleport(dom, { to: '#notifications' }).render(toast2, null)
console.log('\n=== После телепортации ===')
dom.print()
// disabled — рендер на месте
console.log('\n=== Teleport disabled ===')
const inlinePopup = new VNode('div', { class: 'popup' }, ['Встроенный попап'])
const t2 = new Teleport(dom, { to: '#notifications', disabled: true })
t2.render(inlinePopup, deep)
dom.print()
// Destroy — уничтожение компонента убирает контент
console.log('\n=== После destroy модалки ===')
t1.destroy()
dom.print()
Реализуй класс `Portal`, который эмулирует Teleport. Конструктор принимает `{ to, disabled }`. Метод `mount(content, localContainer)` добавляет content (строку) в целевой контейнер (если disabled — в localContainer). Контейнеры — простые объекты `{ id, children: [] }`. Метод `unmount()` удаляет content из контейнера. Метод `moveTo(newTarget)` перемещает контент в другой контейнер (убирает из старого, добавляет в новый).
В mount определи target: const target = this.disabled ? localContainer : this.to. Затем target.children.push(content). В unmount используй: const idx = this._container.children.indexOf(this._content); if (idx !== -1) this._container.children.splice(idx, 1). В moveTo: сохрани content = this._content, вызови this.unmount(), установи this.to = newTarget, вызови this.mount(content, null).
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке