Вылікваты проблемы з переходам высоты у Vue для асінхронна адрадзіваных веб-компанентаў
Дазвольце дазнаць, чаму анімацыя пераходу расширэння Vue зменшваеся з 0px да 0px паўстаноўкай Web Components, і як ResizeObserver адкладае анімацыю пакуль не будзе вялічыны.
Компанент для розгортання/згортання, які можна викорыстоўваць па-разу, створаны на базе <Transition> у Vue, зазвычай виміроўвае высоту элемента ў хуке enter і анімавае його з нуля да гэтага значэння. Гэта добра працуе для стандартных элементаў і звычных компанентаў Vue, але можа моўчкі не працаваць, калі дзецявы элемент — это дынамічна завантажаны Web Component: панель з’яўляецца без жадной анімацыі. У гэтым кярыянте пояснюецца проблема часавага падбору, якая стоіць за гэтым некарэтным робачым станам, чаму польсінг ёсць толькі частковым рашэнням, і як викорыстоўваць ResizeObserver, каб пераход запускаўся роўна тады, калі контэнт мае рэальныя размеры.
Чаму анімацыя праходзіць з 0px на 0px
Падазроўваючы, што паведамленняя пра аблыканніх выкарыстоўваюцца элементам упавядомлення, які реалізаваны як спецыяльны элемент і загорнуты ў пераход розгортання:
<transition-expand>
<notification-wrapper v-if="showError" />
</transition-expand>
Калі showError прымае значэнне true, Vue дадаўае <notification-wrapper> у DOM і негайна вызывае функцыю enter пераходу. Для компаненту Vue гэта нічога не стварае, таму што яго маркап вэлічынюецца ўжо тады. Аднак багато Web Components апдаюцца асінхронна: таг падае ў DOM як порожняя, невядомая елемент, і толькі пазней, калі яго вызначэнне завантажыцца і його shadow DOM будзе адрасаваны, ён прабывае контэнт і высоту.
Рэальная парада здарэнняў выглядае так:
v-if becomes true
↓
Vue inserts the element
↓
Transition enter() runs
↓
Height is still 0px
↓
Web Component finishes rendering
↓
Actual height becomes 160px
У трэцім кроку пераход выконвае свою вызначэнную меру, калі елемент яшчэ ў форме порожней шкляпы. Ён вярна анімаваецца з 0px да вымеранага 0px, а чырвоны праз момант компанент адрасаваецца ў свою пачатковую высоту 160px без жаднага пераходу. Для пользователя выглядае так, нібы анімазія ўжо не выканалася.
Альтернатыўны спосаб зьвяртання к пыткам і яго выкалікаты
Вочывае рашэньне — чакаць, пакуль элемент не паведаміць свою высоту, перш чым яе вимерваць. Цэлкам можна адсакаваць за дапамою ціклу, який пераконтваецца аднае за раз на кожны кадр:
while (element.scrollHeight === 0) {
await new Promise(requestAnimationFrame)
}
Гэты спосаб дзейнае. Кожная ітерацыя чакае наступны кадр анімасці і знову пераконтваецца, чы не застаецца scrollHeight равным нулю. Недзеяючы момент у тым, што цікл паўтарае запиты да лэйауту на кожны кадр, пакуль чыго-небудзь не змяніцца, і якщо контэнт ніколі не павышае сваю высоту, напрыклад, таму што компонент не можа быць завантажаны, цікл будзе працаваць так далей, калі толькі элемент існуе. Кроме таго, гэта ператварае enter у асінхронную функцыю, што ускладняе розумэнне ўсіго механізму. Браузер вже мае механізм паведамлення самэй для такой ситуацыі, таму няма прычыны запытваць яго пра тое ж самае шасцудзе на секунду.
Разьеднанне вимервання і анімасці
Перш чым перайсці на варыянт з абзэрватарам, корисна незначная пераструктурацыя коду для пераходу. У стандартнай рэалізацыі функцыя enter() адначасова вимервае элемент і запускае анімацыю. Адныя як толькі вимеровка можа знадобіцца чакаць, тым лепш выдзеліць этыя два заведамення.
Сам код анімацыі застаецца такім жа, як і раней; ён проста пераводзіцца у дапаможную функцыю пад назвай animateEnter(). Новая функцыя enter() тады мае адну задачу — вяршыць выбір часу запуску:
- Калі элемент вже можна вимерваць, негайна вызваць
animateEnter(). - Інакш, чакаць пакі змогы вимерваць яго, а потым вызваць
animateEnter().
Эта переработка не змінюе працэўкі для стандартных ситуацый, а таксама дае логіке чакання чыстае, аўтаномнае месца.
Чаканне на рэальную высоту з ResizeObserver
ResizeObserver паведамляе, калі зменяецца размер элемента, і гэта самэўсёльва той сігнал, які патрэбен тут. Пасля выкарыстоўвання анімазіі новы метод enter() стае корачы:
function enter(element: HTMLElement) {
if (element.scrollHeight > 0) {
animateEnter(element)
return
}
const observer = new ResizeObserver(() => {
if (element.scrollHeight === 0)
return
observer.disconnect()
requestAnimationFrame(() => {
animateEnter(element)
})
})
observer.observe(element)
}
Разберім усё па порядку. Швайны шлях працюе з натыўнымі элементамі і вяліканутымі компонентамі: якщо scrollHeight ў паазытным значэнні, анімазія запускаецца негайна, як і раней. У працоўным разе да элемента прыкладзаецца абсерватар. Яго функцыя-адаптавальнік зноў пераканалівае высоту і вяртаецца раней, калі яна ўсё ў значэнні нуля; гэты захад важны, таму што абсерватар таксама запускаецца ў момент пачатку спостэрэння, калі элемент можа ўсё ўжо быць порожнім. Калі з’яўляецца рэальная высота, абсерватар сам сабе ад’єктуецца, таму ён не будзе продаваць сігналы праз наступныя змены размера, а анімазія запускаецца на наступны кадр анімазіі, даўаючы браузеру час адладзіць лейаут прычым пераходу.
Змянілася толькі момент, калі запускаецца анімацыя. Сама анімацыя застала недастрыжанай.
Спецыяльныя ситуацыі, якія трэба адрабатаваць у прыменні
Залежна ад таго, як викорыстоўваецца компанент, варта рассмотрзіць калькі ситуацый:
- Якщо элемент знімаецца або хаваецца раней, чым у яго з’яўляецца высота, назіральнік застаецца прыяўленым. Яго ад’ём у момент завершэння пераходу аб анулювання запобегае тым, каб назіральнік застаўся прыяўленым.
- Назіральнік реагуе на размер элемента, які ён стежыць. Якщо ваш пункт
beforeEnterзадае элементу значэннеheight: 0, пераканайцеся, што элемент у такім стане можа фактычна зменіць размер, або ж стежыце за внутрашнім контэнтам заместа таго.
done, якое пасылаецца да enter. Убедзіцеся, што гэты сигнал все ўсё надаецца пасля затрыманага запуску.Чаму падход з абзэрватаром є болей надзеяным
Параўняючы з методам польнінгу, версія ResizeObserver мае калькі прынад:
- Няма циклу па кожным кадру для перагляду лэй아уту.
- Ён справляецца з Web Components, які апдэйтуюцца асінхронна.
- Ён таксама справляецца з компанентамі Vue, які завантажаюцца з запазджэнням.
- Ён справляецца з адпраўкамі та іншым кантэнтам, размеры якога зменяюцца пасля першага атрыбутавання.
- Пераход застаецца універсальным, без веды пра тое, што ён абгружае.
Гэты пасляпэўны момант ёсць найценнейшы. Компоненту expand не трэба ведаць, чыі ўрадзік — это натыўны div, компонент Vue чы спецыяльны элемент; ён чакае, пакуль абгортаваны контэнт не падае ненульовы размер, а потым запускае анімацыю.
Заключэнне
Перадавальны падход «зважыць, а потым анімаваць» застаецца цалкам падходяным для большасці прыемоў Vue, дзе контэнт вужо распакаваны на момент выконання enter. Корэктаванне стае важлівым, калі адрасаванне ўрадзіка ёсць асінхронным і лейаут не готавы на момент, калі Vue вставляе элемент. Абщая наука для перадаўальных UI-прымітываў — не выкладвацца на тое, калі будзе готавы лейаут: уместа таго трэба реагаваць на сябеўны сігнал браузера. Цэе невялікая змена ў кодзе, якая значна павышае надзейныцю, а кожны такі крайні случай у рэальной інтеграцыі робіць компонент моцнейшым для кожнага прыему, які ўжо будзе яго викорыстоўваць.