Всем привет.
Не так давно закончился очередной спринт, и у меня появилось немного времени сделать для своих пользователей не самую нужную, но в то же время интересную фичу — интерактивный гайд по работе с нашим приложением.
В интернетах очень много готовых решений — все они безусловно могут подойти для этой задачи, но мы посмотрим как это сделать самостоятельно.
Архитектура данного компонента довольно проста.
У нас есть важный элемент в 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 — подумает об этом за нас.
Мы поняли как работать со слоями, мы поняли что задача сделать пошаговый гайд — не такая и сложная.
Вы можете доработать библиотеку, добавив возможность перехода между элементами, выхода из режима просмотра и ряд других угловых случаев.
Спасибо за внимание.