Порталы

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


Создание портала

В SkateJS порталы создаются с помощью функции portal(), которая возвращает объект с методами для монтирования и обновления содержимого. Основной принцип работы:

  1. Создание контейнера для портала в целевом месте DOM.
  2. Рендеринг шаблона компонента в этот контейнер.
  3. Обеспечение реактивности свойств компонента внутри портала.

Пример базовой структуры:

import { Component, h, portal } from 'skatejs';

class Tooltip extends Component {
  static get props() {
    return {
      text: String
    };
  }

  connectedCallback() {
    super.connectedCallback();
    this._portal = portal(document.body);
    this._portal.render(this.render());
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    this._portal.remove();
  }

  render() {
    return <div class="tooltip">{this.text}</div>;
  }
}

customElements.define('my-tooltip', Tooltip);

В этом примере содержимое Tooltip выводится непосредственно в <body>, а не внутри родительского компонента. Это обеспечивает корректное позиционирование поверх других элементов.


Поддержка реактивных свойств

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

this.text = 'Новый текст'; 
this._portal.render(this.render());

Важно всегда вызывать render() на объекте портала, чтобы изменения отражались в целевом контейнере. В отличие от стандартного рендера компонента, портал не автоматически реагирует на изменения, если не использовать его метод render.


Позиционирование порталов

Порталы часто требуют динамического позиционирования относительно исходного элемента. Это достигается через вычисление координат родителя и обновление стилей контейнера портала:

connectedCallback() {
  super.connectedCallback();
  this._portal = portal(document.body);
  this.updatePosition();
  this._portal.render(this.render());
}

updatePosition() {
  const rect = this.getBoundingClientRect();
  this._portal.node.style.position = 'absolute';
  this._portal.node.style.top = `${rect.bottom + window.scrollY}px`;
  this._portal.node.style.left = `${rect.left + window.scrollX}px`;
}

При изменении размеров или прокрутке страницы необходимо пересчитывать позицию портала, чтобы элемент оставался на нужном месте.


Удаление портала

При удалении компонента важно корректно очищать портал, чтобы избежать утечек памяти:

disconnectedCallback() {
  super.disconnectedCallback();
  if (this._portal) {
    this._portal.remove();
    this._portal = null;
  }
}

Метод remove() полностью удаляет узел портала из DOM, освобождая ресурсы.


Использование порталов с шаблонизаторами

SkateJS поддерживает JSX и шаблонные строки для порталов. Пример использования с JSX:

this._portal.render(
  <div class="modal">
    <header>Заголовок</header>
    <section>{this.content}</section>
    <footer>
      <button oncl ick={() => this.close()}>Закрыть</button>
    </footer>
  </div>
);

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


Применение порталов для модальных окон

Модальные окна — классический пример использования порталов. Основные требования:

  • Элемент должен отображаться поверх всего контента страницы.
  • Контент модального окна должен оставаться реактивным.
  • Должна быть возможность управлять закрытием окна извне.
class ModalWindow extends Component {
  static get props() {
    return {
      isOpen: Boolean
    };
  }

  connectedCallback() {
    super.connectedCallback();
    this._portal = portal(document.body);
    if (this.isOpen) {
      this._portal.render(this.render());
    }
  }

  updated() {
    if (this.isOpen) {
      this._portal.render(this.render());
    } else {
      this._portal.remove();
    }
  }

  render() {
    return <div class="modal-overlay">
      <div class="modal-content">
        {this.text}
      </div>
    </div>;
  }
}

customElements.define('modal-window', ModalWindow);

Портал позволяет модальному окну не зависеть от родительских стилей, корректно накладываться поверх других элементов и оставаться полностью реактивным.


Советы по оптимизации

  • Создавать портал один раз и повторно использовать для обновлений, чтобы не создавать лишние узлы в DOM.
  • Использовать requestAnimationFrame для обновления позиции портала при скролле для плавной анимации.
  • Оборачивать содержимое портала в контейнер с уникальным идентификатором, чтобы избежать конфликтов стилей и событий.

Итоговая структура портала в SkateJS

  1. Создание портала: this._portal = portal(targetNode).
  2. Рендеринг содержимого: this._portal.render(this.render()).
  3. Поддержка реактивности: обновлять через метод render.
  4. Позиционирование: динамически вычислять координаты.
  5. Удаление: this._portal.remove() при disconnectedCallback.

Порталы в SkateJS позволяют строить гибкие интерфейсы с независимым визуальным слоем, обеспечивая чистоту DOM и реактивность данных, сохраняя при этом высокую производительность и контроль над визуальными эффектами.