![](https://habrastorage.org/webt/es/wd/ka/eswdkavwzb12runf9unmttdcsf0.jpeg)
В моём случае, так как я много работаю с popper.js, я нашёл библиотеку tippy.js, написанную тем же разработчиком. Для меня такая библиотека выглядела как идеальное решение задачи. Библиотека tippy.js обладает обширным набором возможностей. С её помощью можно создавать и всплывающие подсказки (элементы tooltip), и многие другие элементы. Эти элементы можно настраивать с помощью тем, они быстры, строго типизированы, обеспечивают доступность контента и отличаются многими другими полезными возможностями.
Я начал работу с создания директивы-обёртки для tippy.js:
@Directive({ selector: '[tooltip]' })
export class TooltipDirective {
private instance: Instance;
private _content: string;
get content() {
return this._content;
}
@Input('tooltip') set content(content: string) {
this._content = content;
if (this.instance) this.instance.setContent(content);
}
constructor(private host: ElementRef<Element>, private zone: NgZone) {}
ngAfterViewInit() {
this.zone.runOutsideAngular(() => {
this.instance = tippy(this.host.nativeElement, {
content: this.content,
});
});
}
Всплывающую подсказку создают, вызывая функцию
tippy
и передавая ей элементы host
и content
. Кроме того, мы вызываем tippy
за пределами Angular Zone, так как нам не нужно, чтобы события, регистрируемые tippy
, приводили бы к запуску цикла обнаружения изменений.Теперь воспользуемся всплывающей подсказкой в большом списке из 700 элементов:
@Component({
selector: 'my-app',
template: `
<ul>
<li *ngFor="let item of data" [tooltip]="item.label">
{{ item.label }}
</li>
</ul>
`
})
export class AppComponent {
data = Array.from({ length: 700 }, (_, i) => ({
id: i,
label: `Value ${i}`,
}));
}
Всё работает так, как ожидается. Каждый элемент выводит всплывающую подсказку. Но мы можем решить эту задачу лучше. В нашем случае создано 700 экземпляров
tippy
. А для каждого элемента средствами tippy.js было добавлено 4 прослушивателя событий. Это означает, что мы зарегистрировали 2800 прослушивателей (700*4).Для того чтобы увидеть это своими глазами, можно воспользоваться методом
getEventListeners
в консоли инструментов разработчика Chrome. Конструкция вида getEventListeners(element)
возвращает сведения о прослушивателях событий, зарегистрированных для заданного элемента.![](https://habrastorage.org/getpro/habr/post_images/c85/ea9/5b6/c85ea95b6a8f60bc535c212f5cce21cd.png)
Сводные данные обо всех прослушивателях событий
Если оставить код в таком виде, это может подействовать на потребление памяти приложением и на время его первого рендеринга. Особенно это касается вывода страницы на мобильных устройствах. Поразмыслим над этим. Нужно ли создавать экземпляры
tippy
для элементов, которые не выводятся в области просмотра? Нет, не нужно.Воспользуемся API
IntersectionObserver
для того чтобы отложить включение поддержки всплывающих подсказок до момента появления элемента на экране. Если вы не знакомы с API IntersectionObserver
— взгляните на документацию. Создадим для
IntersectionObserver
обёртку, представленную наблюдаемым объектом:const hasSupport = 'IntersectionObserver' in window;
export function inView(
element: Element,
options: IntersectionObserverInit = {
root: null,
threshold: 0.5,
}
) {
return new Observable((subscriber) => {
if (!hasSupport) {
subscriber.next(true);
subscriber.complete();
}
const observer = new IntersectionObserver(([entry]) => {
subscriber.next(entry.isIntersecting);
}, options);
observer.observe(element);
return () => observer.disconnect();
});
}
Мы создали наблюдаемый объект, который сообщает подписчикам о моменте пересечения элемента с заданной областью. Кроме того, тут мы проверяем поддержку
IntersectionObserver
браузером. Если браузер не поддерживает IntersectionObserver
— мы просто выдаём true
и завершаем работу. Пользователи IE сами виноваты в своих страданиях.Теперь наблюдаемый объект
inView
мы можем использовать в директиве, реализующей функционал всплывающей подсказки:@Directive({ selector: '[tooltip]' })
export class TooltipDirective {
...
ngAfterViewInit() {
// Не забудьте отписаться
inView(this.host.nativeElement).subscribe((inView) => {
if (inView && !this.instance) {
this.zone.runOutsideAngular(() => {
this.instance = tippy(this.host.nativeElement, {
content: this.content,
});
});
} else if (this.instance) {
this.instance.destroy();
this.instance = null;
}
});
}
}
Снова запустим код для анализа количества прослушивателей событий.
![](https://habrastorage.org/getpro/habr/post_images/14e/b0d/bc7/14eb0dbc72735f1086763e4fcc4527e2.png)
Сводные данные обо всех прослушивателях событий после доработки проекта
Отлично. Теперь мы создаём всплывающие подсказки только для видимых элементов.
Поищем ответы на пару вопросов, связанных с новым решением.
Почему бы нам не воспользоваться для решения этой задачи виртуальным скроллингом? Виртуальный скроллинг нельзя использовать в любых ситуациях. И, кроме того, библиотека Angular Material кэширует шаблон, в результате соответствующие данные будут продолжать занимать память.
А как насчёт делегирования событий? Для этого нужно самостоятельно реализовывать дополнительные механизмы, в Angular нет универсального способа решения этой задачи.
Итоги
Здесь мы поговорили о том, как откладывать применение функционала директив. Это позволяет приложению быстрее загружаться и потреблять меньше памяти. Пример со всплывающей подсказкой — это лишь один из многих случаев, в которых может применяться подобная техника. Уверен, вы найдёте немало способов её использования в собственных проектах.
А как вы решили бы задачу по выводу большого списка элементов, каждый из которых нужно оснастить всплывающей подсказкой?
Напоминаем, что у нас продолжается конкурс прогнозов, в котором можно выиграть новенький iPhone. Еще есть время ворваться в него, и сделать максимально точный прогноз по злободневным величинам.
![](https://habrastorage.org/webt/a_/bs/aa/a_bsaactpbr8fltzymtkhqbw1d4.png)
barba
Есть такой приём, называется event delegation. Он использовался еще во времена jQuery. Позволяет избежать лишних обработчиков событий, а также сложностей с тем, чтобы эти обработчики навешивать и убирать. Один обработчик сразу на все несколько тысяч элементов.
Xuxicheta
да и в ангуляре тоже можно, вот пример stackblitz.com/edit/angular-ivy-ctk18j
Видимо речь о том, что в Ангуляре нельзя из event.target получить экземпляр компонента, в котором произошло событие. В моем примере для этого используется data-аттрибут, который сохраняет индекс компонента, и по этому индексу забирается инстанс из QueryList. И нельзя сказать что это «универсальный способ».
barba
Хммм… Фреймворки точно делают чтобы было проще писать приложения?
Xuxicheta
вопрос в сложности приложения.
Фреймворк позволяет прибегать к однообразным паттернам и удобно разделять функционал.
Эта задача, если ее представить в независимом, рафинированном виде при помощи фреймворка решается сильно громоздко, но в составе сложного приложения такой код будет уже предпочтительней js-лапше на эвентах.
Ну и нельзя отрицать что конкретно этот кейс не очень хорошо ложится на ангуляр и ts.
Если бы, допустим, забить на типизацию, можно было бы сделать ссылку на инстанс полем хтмл-элемента и получать из таргета напрямую.
А еще я могу оформить все это дело в виде либы, и в живом коде (там, где логика) все это будет спрятано под короткую запись в шаблоне.
barba
Я скорее к тому, что если фреймворк для решения довольно простой задачи вынуждает использовать намного более сложный путь с кучей оверинжениринга (а навешивание и удаление хэндлеров событий при помощи intersection observer вместо того, чтобы сделать делегирование выглядит крайним овериженирингом), то что-то идет не так.
Xuxicheta
а как делегировать mouseenter? it doesn't bubble.
Можно было конечно через mouseover сделать, не факт что оно работает с tippy.
Но честно говоря не вижу разницы. Директива пишется один раз и потом можно забыть как оно устроено.
Druu
Это не задача — это решение. И если это решение плохо реализуется во фреймворке — значит, просто, это решение считается плохим.