Портали React.js

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. Користуйтеся!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.