Порталы в React.js
Наверное, каждому фронтенд-разработчику доводилось делать разного рода выпадайки или всплывающие подсказки. И почти всегда настает момент, когда такую штуку надо отобразить внутри элемента с overflow: hidden
. Настал такой момент и в SmartProgress.
Мы на SmartProgress используем React для разработки интерфейсов и нам очень хотелось найти react-way решение. На помощь нам спешат порталы.
Портал — компонент, который рендерит свое содержимое в другую часть DOM, например в конец . Такое поведение позволяет отображать элементы за пределами блоков с, например,
overflow: hidden
, но при этом минимально менять дерево компонентов.
Обычно порталы используют для модальных окон (что тоже весьма удобно), но мы немного модифицируем идею и приспособим её для наших нужд. Нам нужно поведение похожее на блок с position: absolute и margin-top/margin-left. Назовем этот компонент RelativePortal.
Удобно определять интерфейс компонента и только потом описывать его реализацию.
Например, мы имеем такой код:
Выбрать дату
{isOpen && }
При использовании портала, код изменится на такой:
Выбрать дату
{isOpen && }
Теперь можно приступить к делу. Каждый шаг я буду иллюстрировать примером на 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(
{this.props.children},
this.node
);
}
...
}
Полдела сделано, но сейчас наша выпадайка отображается внизу страницы (https://jsfiddle.net/Sunify/kr8hehca/) — надо это исправить!
class RelativePortal extends React.Component {
...
// Рендерим инлайновый элемент, чтобы React.findDOMNode(this) не возвращал null
render() {
return ;
}
// Добавляем обработчик на событие ресайза для обновления координат
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(
{this.props.children}
,
this.node
);
}
...
}
Теперь выпадайка отображается где нам и нужно было и у нас есть универсальный компонент для такого рода задач.
https://jsfiddle.net/Sunify/4fmdugrr/
У такого метода есть очевидные достоинства: он работает, у него минимальный интерфейс и он универсален — мы можем завернуть в RelativePortal что угодно.
Но у него есть и существенный недостаток — мы теряем каскад css. У нас не наследуются шрифты, цвета и т.д. Не работает : hover — состояние наведения приходится хранить в состоянии компонента. Например, так — https://jsfiddle.net/Sunify/nz7wyee3. Для нас это не критично, поэтому такое решение нас устраивает.
Мы активно используем такой компонент и разместили его в npm. Пользуйтесь!
Комментарии (2)
21 июля 2016 в 11:24
0↑
↓
В какой момент собственно вы проверяете overflow: hidden и заменяете this.node в компоненте? Ведь он должен быть document.body, а в нормальных условиях как обычно родитель где используется компонент. Тема довольно интересная.21 июля 2016 в 11:25
+1↑
↓
Не уверен, что правильно понял — поправьте если что. Мы не проверяем есть ли overflow у родителей, но идея интересная, можно поисследовать.