image


Наверное, каждому фронтенд-разработчику доводилось делать разного рода выпадайки или всплывающие подсказки. И почти всегда настает момент, когда такую штуку надо отобразить внутри элемента с overflow: hidden. Настал такой момент и в SmartProgress.


Мы на SmartProgress используем React для разработки интерфейсов и нам очень хотелось найти react-way решение. На помощь нам спешат порталы.



Портал — компонент, который рендерит свое содержимое в другую часть DOM, например в конец <body>. Такое поведение позволяет отображать элементы за пределами блоков с, например, overflow: hidden, но при этом минимально менять дерево компонентов.


Обычно порталы используют для модальных окон (что тоже весьма удобно), но мы немного модифицируем идею и приспособим её для наших нужд. Нам нужно поведение похожее на блок с position: absolute и margin-top/margin-left. Назовем этот компонент RelativePortal.


Удобно определять интерфейс компонента и только потом описывать его реализацию.
Например, мы имеем такой код:


<div className="calendarLink">
  <a className="calendarLink__trigger">Выбрать дату</a>
  {isOpen && <CalendarDropdown />}
</div>

При использовании портала, код изменится на такой:


<div className="calendarLink">
  <a className="calendarLink__trigger">Выбрать дату</a>
  <RelativePortal left="0" top="0">
    {isOpen && <CalendarDropdown />}
  </RelativePortal>
</div>

Теперь можно приступить к делу. Каждый шаг я буду иллюстрировать примером на jsfiddle. Вот начальное состояние, при котором видно проблему — https://jsfiddle.net/Sunify/1k18wxm1/1/.


Как сделать портал (осторожно — ES6!)


Метод render у портала возвращает null, так мы ничего не рендерим в месте вызова компонента. Вместо этого мы будем использовать ReactDOM.render в одном из lifecycle-методов компонента, например в componentDidUpdate.


class RelativePortal extends React.Component {
  ...

  // Возвращаем null чтобы ничего не рендерить на месте вызова компонента
  render() {
    return null;
  }

  // А тут мы рендерим в наш портал
  componentDidUpdate() {
    ReactDOM.render(
      <div {...this.props}>{this.props.children}</div>,
      this.node
    );
  }

  ...
}

Полдела сделано, но сейчас наша выпадайка отображается внизу страницы (https://jsfiddle.net/Sunify/kr8hehca/) — надо это исправить!


class RelativePortal extends React.Component {
  ...
  // Рендерим инлайновый элемент, чтобы React.findDOMNode(this) не возвращал null
  render() {
    return <span />;
  }

  // Добавляем обработчик на событие ресайза для обновления координат
  componentDidMount() {
    this.handleResize = () => {
      const rect = React.findDOMNode(this).getBoundingClientRect();
      const left = window.scrollX + rect.left;
      const top = window.scrollY + rect.top;

      if(top !== this.state.top || left !== this.state.left) {
        this.setState({ left, top });
      }
    };
    window.addEventListener('resize', this.handleResize);
    this.handleResize();
  }

  // А тут мы рендерим в портал и позиционируем наш элемент по правильным координатам
  componentDidUpdate() {
    ReactDOM.render(
      <div
        {...this.props}
        style={{
          position: 'absolute',
          top: this.state.top + this.props.top,
          left: this.state.left + this.props.left
        }}
      >
        {this.props.children}
      </div>,
      this.node
    );
  }
  ...
}

Теперь выпадайка отображается где нам и нужно было и у нас есть универсальный компонент для такого рода задач.
https://jsfiddle.net/Sunify/4fmdugrr/


У такого метода есть очевидные достоинства: он работает, у него минимальный интерфейс и он универсален — мы можем завернуть в RelativePortal что угодно.


Но у него есть и существенный недостаток — мы теряем каскад css. У нас не наследуются шрифты, цвета и т.д. Не работает :hover — состояние наведения приходится хранить в состоянии компонента. Например, так — https://jsfiddle.net/Sunify/nz7wyee3. Для нас это не критично, поэтому такое решение нас устраивает.


Мы активно используем такой компонент и разместили его в npm. Пользуйтесь!

Поделиться с друзьями
-->

Комментарии (17)


  1. reactoranime
    21.07.2016 11:24

    В какой момент собственно вы проверяете overflow:hidden и заменяете this.node в компоненте? Ведь он должен быть document.body, а в нормальных условиях как обычно родитель где используется компонент. Тема довольно интересная.


    1. Sunify
      21.07.2016 11:25
      +1

      Не уверен, что правильно понял — поправьте если что. Мы не проверяем есть ли overflow у родителей, но идея интересная, можно поисследовать.


      1. reactoranime
        21.07.2016 12:23

        немного вышел из контекста, в самом начале статьи была речь об overflow:hidden, собственно выходит портал должен динамично понимать куда ему вставляться ( в body, или в родителя ), также интересно что можно сделать с CSS классами, которые меняют отображения компонентов внутри себя, если в этом случае он встанет в body.


        1. Sunify
          21.07.2016 12:56
          +1

          Портал просто рендерит в body, например. Захотите вы его открывать или нет — это уже другой вопрос. Как и условия, при которых он нужен. Мы сейчас всегда рендерим через порталы, т. к. это намного проще.


          1. reactoranime
            21.07.2016 14:38

            В целом понятно, идея хорошая, из минусов при таком подходе будет что контекст родителей (css стилей, Js) не будет накладываться на портал, но этого в данном случае и не требуется.


  1. ilinsky
    21.07.2016 14:27

    Из минусов можно добавить, что серверный рендеринг скорее всего не работает.


    1. Sunify
      21.07.2016 14:45
      +2

      Да, точно. Но обычно такие элементы скрыты в итоге это не настолько критично (мы недавно завели серверный рендеринг и проблем пока не наблюдаем).


  1. sindes255
    21.07.2016 19:11
    -3

    Такой подход противоречит идеологии ReactJS, лучше использовать возможности фреймворка, например брать модальные окна из стора.


    1. Sunify
      21.07.2016 19:13
      +2

      Что конкретно в этой подходе противоречит идеологии реакта? Как по вашему должна решаться эта проблема в рамках вашего представления об идеологии реакта?

      Сторы, экшены и прочий флакс к реакту имеют опосредованное отношение.


    1. VasilioRuzanni
      22.07.2016 07:41
      +3

      Что это, блин, вообще значит — «брать модальные окна из стора»?


      1. Finesse
        26.07.2016 01:27

        Магазин плагинов для ReactJS? Других идей нет.


  1. raveclassic
    21.07.2016 19:16

    Буквально один в один 3 дня назад статья была, писал там, что лучше не ReactDOM.render, а ReactDOM.unstable_renderComponentIntoSubtree, так как сохранится контекст.


    1. Sunify
      21.07.2016 19:17
      +1

      Спасибо за замечание, обновим модуль.


      1. raveclassic
        22.07.2016 00:40
        +1

        Я не хотел показаться грубым, если что :)
        Мы в команде, в итоге, чтобы не городить велосипедов, взяли реализацию на хорошей поддержке — react-overlays


        1. VasilioRuzanni
          22.07.2016 11:13
          +1

          Есть еще отличный https://github.com/tajo/react-portal, если нужен только «портал».
          Впрочем, оба используют ReactDOM.unstable_renderComponentIntoSubtree, в целом похожи, а с react-overlays идут еще несколько ништяков в виде более высокоуровневых компонентов.


  1. hose314
    22.07.2016 16:22

    По хорошему наверное в componentWillUnmout нужен removeEventListener?


    1. Sunify
      22.07.2016 16:50

      Да, и ноду удалить тоже нужно. Примеры на jsfiddle более подробные