Pull to refresh

ResizeObserver — новый мощный инструмент для работы с адаптивностью

Reading time 5 min
Views 34K
Original author: Khrystyna Skvarok
Доброго времени суток, друзья!

«Отзывчивый» является одним из стандартов веб-разработки. Существует большое количество разрешений экрана, и это количество все время увеличивается. Мы стремимся поддерживать все возможные размеры экранов с сохранением дружелюбного пользовательского интерфейса. Отличным решением данной задачи являются медиа-запросы (media-queries). Но что насчет веб-компонентов? Современная веб-разработка основана на компонентах, и нам нужен способ делать их отзывчивыми. Сегодня я хочу рассказать о ResizeObserver API, позволяющим следить (наблюдать) за изменениями размеров конкретного элемента, а не всей области просмотра (viewport), как в случае с медиа-запросами.

Немного истории


Раньше в нашем распоряжении имелись лишь медиа-запросы — решение на CSS, основанное на размере, типе и разрешении экрана медиа устройства (под медиа устройством я подразумеваю компьютер, телефон или планшет). Медиа-запросы являются достаточно гибкими и простыми в использовании. Долгое время медиа-запросы были доступны только в CSS, сейчас они также доступны в JS через window.matchMedia(mediaQueryString). Теперь мы можем проверять, с какого устройства просматривается страница, а также следить за изменением размера области просмотра (речь идет о методе MediaQueryList.addListener() — прим. пер.).

Запросы размеров элемента (Element Queries)


Чего нам не хватало, так это возможности следить за размерами отдельного элемента DOM, а не всего «вьюпорта». Разработчики сетуют на это на протяжении многих лет. Это одна из самых ожидаемых особенностей. В 2015 году даже выдвигалось предложение — запросы размеров контейнера (Container Queries):
Разработчики часто нуждаются в возможности стилизовать элементы при изменении размера их родительского контейнера, независимо от области просмотра. Запросы размеров контейнера предоставляют им такую возможность. Пример использования в CSS:
.element:media(min-width: 30em) screen {***}
Звучит здорово, но у производителей браузеров была серьезная причина отклонить это предложение — круговая зависимость (circular dependency) (когда один размер определяет другой, это приводит к бесконечной петле (infinite loop); подробнее об этом можно почитать здесь). Какие еще варианты существуют? Мы можем использовать window.resize(callback), но это «дорогое удовольствие» — callback будет вызываться каждый раз при возникновении события, и нам потребуется много вычислений для определения того, что размер нашего компонента действительно изменился…

Наблюдение за изменениями размеров элемента с помощью ResizeObserver API


Встречайте, ResizeObserver API от Chrome:
ResizeObserver API — это интерфейс слежения за изменениями размеров элемента. Это своего рода аналог события window.resize для элемента.

ResizeObserver API — это «живой черновик». Он уже реализован в Chrome, Firefox и Safari для ПК. Поддержка мобильных менее впечатляющая — только Chrome на Android и Samsung Internet. К сожалению, полноценного полифила не существует. Имеющиеся полифилы содержат некоторые ограничения (например, медленная реакция на изменение размера или отсутствие поддержки плавного перехода). Однако это не должно останавливать нас от тестирования данного API. Так давайте же сделаем это!

Пример: изменение текста при изменении размера элемента


Представим следующую ситуацию — текст внутри элемента должен меняться в зависимости от размера элемента. ResizeObserver API предоставляет два инструмента — ResizeObserver и ResizeObserverEntry. ResizeObserver используется для слежения за изменением размера элемента, а ResizeObserverEntry содержит сведения об элементе, размер которого изменился.
Код очень простой:

<h1> size </h1>
<h2> boring text </h2>

const ro = new ResizeObserver(entries => {
    for(let entry of entries){
        const width = entry.contentBoxSize
        ? entry.contentBoxSize.inlineSize
        : entry.contentRect.width

        if(entry.target.tagName === 'H1'){
            entry.target.textContent = width < 1000 'small' : 'big'
        }

        if(entry.target.tagName === 'H2' && width < 500){
            entry.target.textContent = `I won't change anymore`
            ro.unobserve(entry.target) // прекращаем наблюдение, когда ширина элемента достигла 500px
        }
    }
})

// мы можем следить за любым количеством элементов
ro.observe(document.querySelector('h1'))
ro.observe(document.querySelector('h2'))

В начале создаем объект ResizeObserver и передаем ему в качестве параметра функцию обратного вызова:

const resizeObserver = new ResizeObserver((entries, observer) => {
    for(let entry of entries){
        // логика
    }
})

Функция вызывается каждый раз, когда происходит изменение размеров одного из элементов, содержащихся в ResizeObserverEntries. Второй параметр функции обратного вызова это сам observer. Мы можем использовать его, например, для остановки слежения при выполнении определенного условия.

Callback получает массив ResizeObserverEntry. Каждая запись (entry) содержит размеры наблюдаемого элемента (target).

for(let entry of entries){
    const width = entry.contentBoxSize
    ? entry.contentBoxSize.inlineSize
    : entry.contentRect.width

    if(entry.target.tagName === 'H1'){
        entry.target.textContent = width < 1000 ? 'small' : 'big'
    }
    ...
}

У нас имеется три свойства, описывающих размеры элемента — borderBoxSize, contentBoxSize и contentRect. Они представляют блочную модель (box model) элемента, о которой мы поговорим позже. Сейчас несколько слов о поддержке. Лучше всего браузеры поддерживают contentRect, однако, судя по всему, это свойство будет признано устаревшим:
contentRect появился на предварительной стадии разработки ResizeObserver и был добавлен только в интересах текущей совместимости. Вероятно, в будущем он будет признан устаревшим.


Поэтому я бы настоятельно рекомендовала использовать contentRect совместно с bordeBoxSize и contentBoxSize. ResizeObserverSize включает в себя два свойства: inlineSize и blockSize, которые можно интерпретировать как ширину и высоту (при условии, что мы работаем в горизонтальной ориентации текста — writing-mode: horizontal).

Наблюдение за элементом


Последнее, что нужно сделать — это запустить слежение за элементом. Для этого мы вызываем ResizeObserver.observe(). Данный метод добавляет новую цель в список наблюдаемых элементов. Мы можем добавлять в этот список как один, так и сразу несколько элементов:

// resizeObserver(target, options)
ro.observe(document.querySelector('h1'))
ro.observe(document.querySelector('h2'))

Второй параметр является «опциональным». На сегодняшний день единственной доступной опцией является box, определяющей блочную модель. Возможными значениями являются content-box (по умолчанию), border-box и device-pixel-content-box (только Chrome). В одном ResizeObserver можно определить только одну блочную модель.

Для остановки наблюдения следует использовать ResizeObserver.unobserve(target). Для того, чтобы прекратить слежение за всеми элементами следует использовать ResizeObserver.disconnect().

Блочная модель


Content box — это содержимое блока без padding, border и margin. Border box включает padding и border (без margin).



Device pixels content box — это содержимое элемента в физических пикселях. Я не встречала примеров использования этой модели, но, похоже, это может пригодиться при работе с холстом (canvas). Вот интересное обсуждение данной темы на Github.

Когда «наблюдатель» (observer) узнает об изменениях?


Callback вызывается каждый раз при изменении размера целевого элемента. Вот что об этом говорится в спецификации:

  • Наблюдение (observation) срабатывает, когда наблюдаемый элемент добавляется/удаляется из DOM.
  • Наблюдение срабатывает, когда свойство display наблюдаемого элемента принимает значение none.
  • Наблюдение не работает для «незамещаемых» строчных элементов.
  • Наблюдение не работает в CSS трансформации.
  • Наблюдение работает только для элементов, рендеринг которых завершен, т.е. для элементов, чей размер не равен 0,0.

Согласно первому пункту мы можем определять изменение родительского контейнера при изменении его дочерних элементов. Отличный пример подобного использования ResizeObserver API — прокрутка окна чата вниз при добавлении нового сообщения. Пример можно посмотреть здесь.

Помните о запросах размера контейнера, о которых я упоминала ранее? О его проблеме круговой зависимости? Так вот, ResizeObserver API имеет встроенное решения для предотвращения бесконечной петли «ресайзов». Почитать об этом можно здесь.

Благодарю за внимание.

Полезные ссылки:

Спецификация
MDN
CanIUse
Первая статья от команды разработчиков
Самый популярный полифил
Tags:
Hubs:
+7
Comments 20
Comments Comments 20

Articles