Всем привет.
Не так давно закончился очередной спринт, и у меня появилось немного времени сделать для своих пользователей не самую нужную, но в то же время интересную фичу — интерактивный гайд по работе с нашим приложением.
В интернетах очень много готовых решений — все они безусловно могут подойти для этой задачи, но мы посмотрим как это сделать самостоятельно.
Архитектура данного компонента довольно проста.
У нас есть важный элемент в DOM-дереве, о котором мы хотим что-то рассказать пользователю, например, кнопка.
Нам нужно нарисовать затемненный слой вокруг этого элемента, переключив на него внимание.
Необходимо нарисовать карточку рядом с этим элементом с важным сообщением.

Для решения этой задачи нам поможет @angular/cdk. В своей прошлой статье я уже хвалил @angular/material которая зависит от CDK, для меня она по-прежнему остается примером качественно исполненных компонентов, созданных с использованием всех возможностей фреймворка.
Такие компоненты как меню, диалоговые окна, тултипы из библиотеки @angular/material сделаны с помощью компонента из CDK — Overlay.
Простой интерфейс этого компонента позволяет быстро создать слой поверх нашего приложения, который будет самостоятельно подстраиваться под изменения размера экрана и скроллинг. Как вы уже поняли, с помощью этого компонента решить нашу задачу становиться очень просто.
Для начала установим библиотеки
После установки не забудьте добавить стили в style.css
Теперь создадим новый модуль в нашем проекте:
И сразу же cгенерируем шаблон для директивы:
Данная директива будет выполнять роль триггера для нашего будущего гайда, будет слушать нажатия на элемент и подсвечивать его на странице.
Тут мы обращаемся к сервису который был так же сгенерирован при создании библиотеки, всю основную работу будет делать он.
Для начала объявим в сервисе новое свойство
Как мы уже видели эти значения нам будет присылать директива, в массиве первый элемент это описание, а второй — DOM-элемент который мы описываем (такое решение я выбрал для простоты примера).
Обновим конструктор сервиса, добавив подписку на обновления от EventEmitter, функция attach будет получать обновления и создавать слои.
Для создания слоя нам понадобиться Overlay, Injector и NgZone.
Следующие действия можно разделить на несколько этапов:
С первым пунктов ясно, для этого мы уже объявили свойство в сервисе. PositionStrategy — отвечает за то как наш слой будет позиционироваться в DOM — дереве.
Существует несколько готовых стратегий:
Если простыми словами, то
FlexibleConnectedPositionStrategy — будет следить за конкретным элементом и в зависимости от конфигурации будет липнуть к нему при изменении размера браузера или скролле, явный пример использования выпадающие списки, меню.
GlobalPositionStrategy — как и говорится в названии, создается глобально, ему не нужен какой-либо элемент для работы, явный пример использования — модальные окна.
Добавим метод создания стратегии плавающего окна вокруг исследуемого элемента
Добавим метод создания OverlayRef
И добавим метод привязки нашего компонента к слою:
Вот так у нас выглядит компонент показывающий сообщение
Про все что приведено в листингах уже рассказал, кроме DataRef.
DataRef — это простой класс который мы добавляем в инжектор для компонентов, фактически для передачи данных для отрисовки — например описание.
Так же, в нем я решил рисовать еще один слой для затемнения и выделения элемента. В этом случае мы уже будем использовать глобальную стратегию создания слоя.
ShadowOverlayComponent — рисует компонент, и в инжекторе получает тот же токен, только с элементом, вокруг которого нужно сделать акцент.
Как я это реализовал, можно посмотреть в исходниках на github, отдельно на этом внимание акцентировать не хочется.
Только скажу, что там я рисую canvas на весь экран, рисую фигуру вокруг элемента, и заливаю контекст методом fill('evenodd');
Самое крутое, это то что @angular/cdk/overlay позволяет нам рисовать сколько угодно слоев. Они будут адаптивными и гибкими. Нам не нужно заботиться о том что изменится размер экрана, или элементы сместятся по каким то естественным причинам, overlay — подумает об этом за нас.
Мы поняли как работать со слоями, мы поняли что задача сделать пошаговый гайд — не такая и сложная.
Вы можете доработать библиотеку, добавив возможность перехода между элементами, выхода из режима просмотра и ряд других угловых случаев.
Спасибо за внимание.
Не так давно закончился очередной спринт, и у меня появилось немного времени сделать для своих пользователей не самую нужную, но в то же время интересную фичу — интерактивный гайд по работе с нашим приложением.
В интернетах очень много готовых решений — все они безусловно могут подойти для этой задачи, но мы посмотрим как это сделать самостоятельно.
Архитектура
Архитектура данного компонента довольно проста.
У нас есть важный элемент в DOM-дереве, о котором мы хотим что-то рассказать пользователю, например, кнопка.
Нам нужно нарисовать затемненный слой вокруг этого элемента, переключив на него внимание.
Необходимо нарисовать карточку рядом с этим элементом с важным сообщением.

Для решения этой задачи нам поможет @angular/cdk. В своей прошлой статье я уже хвалил @angular/material которая зависит от CDK, для меня она по-прежнему остается примером качественно исполненных компонентов, созданных с использованием всех возможностей фреймворка.
Такие компоненты как меню, диалоговые окна, тултипы из библиотеки @angular/material сделаны с помощью компонента из CDK — Overlay.
Простой интерфейс этого компонента позволяет быстро создать слой поверх нашего приложения, который будет самостоятельно подстраиваться под изменения размера экрана и скроллинг. Как вы уже поняли, с помощью этого компонента решить нашу задачу становиться очень просто.
Для начала установим библиотеки
npm i @angular/cdk @angular/material -S
После установки не забудьте добавить стили в style.css
@import '~@angular/cdk/overlay-prebuilt.css'; @import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
Теперь создадим новый модуль в нашем проекте:
ng generate library intro-lib
И сразу же cгенерируем шаблон для директивы:
ng generate directive intro-trigger
Данная директива будет выполнять роль триггера для нашего будущего гайда, будет слушать нажатия на элемент и подсвечивать его на странице.
@Directive({ selector: '[libIntroTrigger]' }) export class IntroTriggerDirective { @Input() libIntroTrigger: string; constructor(private introLibService: IntroLibService, private elementRef: ElementRef) {} @HostListener('click') showGuideMessage(): void { this.introLibService.show$.emit([this.libIntroTrigger, this.elementRef]); } }
Тут мы обращаемся к сервису который был так же сгенерирован при создании библиотеки, всю основную работу будет делать он.
Для начала объявим в сервисе новое свойство
show$ = new EventEmitter<[string, ElementRef]>();
Как мы уже видели эти значения нам будет присылать директива, в массиве первый элемент это описание, а второй — DOM-элемент который мы описываем (такое решение я выбрал для простоты примера).
@Injectable({ providedIn: 'root' }) export class IntroLibService { private overlayRef: OverlayRef; show$ = new EventEmitter<[string, ElementRef]>(); constructor(private readonly overlay: Overlay, private readonly ngZone: NgZone, private readonly injector: Injector) { this.show$.subscribe(([description, elementRef]: [string, ElementRef]) => { this.attach(elementRef, description); }); } }
Обновим конструктор сервиса, добавив подписку на обновления от EventEmitter, функция attach будет получать обновления и создавать слои.
Для создания слоя нам понадобиться Overlay, Injector и NgZone.
Следующие действия можно разделить на несколько этапов:
- Закрыть текущий оверлей (если он есть)
- Создать PositionStrategy
- Создать OverlayRef
- Создать PortalInjector
- Прикрепить компонент к слою
С первым пунктов ясно, для этого мы уже объявили свойство в сервисе. PositionStrategy — отвечает за то как наш слой будет позиционироваться в DOM — дереве.
Существует несколько готовых стратегий:
- FlexibleConnectedPositionStrategy
- GlobalPositionStrategy
Если простыми словами, то
FlexibleConnectedPositionStrategy — будет следить за конкретным элементом и в зависимости от конфигурации будет липнуть к нему при изменении размера браузера или скролле, явный пример использования выпадающие списки, меню.
GlobalPositionStrategy — как и говорится в названии, создается глобально, ему не нужен какой-либо элемент для работы, явный пример использования — модальные окна.
Добавим метод создания стратегии плавающего окна вокруг исследуемого элемента
{ ... private getPositionStrategy(elementRef: ElementRef): PositionStrategy { return this.overlay .position() .flexibleConnectedTo(elementRef) .withViewportMargin(8) // добавляем отступы от краев экрана .withGrowAfterOpen(true) // этот флаг помогает в случае если элемент может изменять размер (например, exspansion panel при открытии) .withPositions([ // задаем позиции расположения, при обновлениях позиции эта стратегия будет выбирать лучший вариант из перечисленного { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' }, { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom' }, { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' }, { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom' } ]); } ... }
Добавим метод создания OverlayRef
createOverlay(elementRef: ElementRef): OverlayRef { const config = new OverlayConfig({ positionStrategy: this.getPositionStrategy(elementRef), scrollStrategy: this.overlay.scrollStrategies.reposition() }); return this.overlay.create(config); }
И добавим метод привязки нашего компонента к слою:
attach(elementRef: ElementRef, description: string): void { if (this.overlayRef && this.overlayRef.hasAttached()) { this.overlayRef.dispose(); } this.overlayRef = this.createOverlay(elementRef); const dataRef = this.ngZone.run( () => new DataRef(this.overlay, this.injector, this.overlayRef, elementRef, description) ); // тут следует отметить, что процедуры которые вызываются внутри конструктора, нужно совершить внутри зоны, иначе CD не узнает что что-то нужно перерисовать const injector = new PortalInjector(this.injector, new WeakMap([[DATA_TOKEN, dataRef]])); dataRef.overlayRef.attach(new ComponentPortal(IntroLibComponent, null, injector)); }
Вот так у нас выглядит компонент показывающий сообщение
@Component({ selector: 'lib-intro-lib', template: ` <mat-card> <mat-card-content> {{ data.description }}</mat-card-content> </mat-card> `, styles: ['mat-card {width: 300px; margin: 32px;}'] }) export class IntroLibComponent { constructor(@Inject(DATA_TOKEN) public data: DataRef) {} }
Про все что приведено в листингах уже рассказал, кроме DataRef.
DataRef — это простой класс который мы добавляем в инжектор для компонентов, фактически для передачи данных для отрисовки — например описание.
Так же, в нем я решил рисовать еще один слой для затемнения и выделения элемента. В этом случае мы уже будем использовать глобальную стратегию создания слоя.
export class DataRef { shadowOverlayRef: OverlayRef; constructor( private overlay: Overlay, private injector: Injector, public overlayRef: OverlayRef, public elementRef: ElementRef, public description: string ) { const config = new OverlayConfig({ positionStrategy: this.overlay.position().global(), scrollStrategy: this.overlay.scrollStrategies.block() }); this.shadowOverlayRef = this.overlay.create(config); this.shadowOverlayRef.attach( new ComponentPortal( ShadowOverlayComponent, null, new PortalInjector(this.injector, new WeakMap([[DATA_TOKEN, this.elementRef]])) ) ); } }
ShadowOverlayComponent — рисует компонент, и в инжекторе получает тот же токен, только с элементом, вокруг которого нужно сделать акцент.
Как я это реализовал, можно посмотреть в исходниках на github, отдельно на этом внимание акцентировать не хочется.
Только скажу, что там я рисую canvas на весь экран, рисую фигуру вокруг элемента, и заливаю контекст методом fill('evenodd');
Итого
Самое крутое, это то что @angular/cdk/overlay позволяет нам рисовать сколько угодно слоев. Они будут адаптивными и гибкими. Нам не нужно заботиться о том что изменится размер экрана, или элементы сместятся по каким то естественным причинам, overlay — подумает об этом за нас.
Мы поняли как работать со слоями, мы поняли что задача сделать пошаговый гайд — не такая и сложная.
Вы можете доработать библиотеку, добавив возможность перехода между элементами, выхода из режима просмотра и ряд других угловых случаев.
Спасибо за внимание.
