Библиотека Driver.js используется для создания интерактивных туров по интерфейсу. В Angular она применяется как обычная сторонняя зависимость, однако требует учета особенностей архитектуры фреймворка: компонентной структуры, жизненного цикла и системы обнаружения изменений.
Установка выполняется через пакетный менеджер:
npm install driver.js
Подключение стилей обязательно, так как библиотека не работает корректно без CSS:
"styles": [
"node_modules/driver.js/dist/driver.css"
]
Импорт в Angular-компоненте:
import Driver from 'driver.js';
import 'driver.js/dist/driver.css';
Создание экземпляра Driver обычно происходит внутри компонента:
export class AppComponent {
driver = new Driver({
animate: true,
opacity: 0.75,
padding: 10,
allowClose: true
});
}
Важно учитывать, что DOM должен быть полностью отрисован. Поэтому
запуск тура выполняется не в constructor, а в
ngAfterViewInit:
import { AfterViewInit } from '@angular/core';
export class AppComponent implements AfterViewInit {
ngAfterViewInit(): void {
this.startTour();
}
startTour() {
this.driver.defineSteps([
{
element: '#step1',
popover: {
title: 'Первый шаг',
description: 'Описание первого шага'
}
}
]);
this.driver.start();
}
}
Причина — Angular асинхронно рендерит шаблон, и элементы могут отсутствовать в DOM на ранних этапах.
При использовании Angular-шаблонов часто применяются
@ViewChild:
@ViewChild('buttonRef') button!: ElementRef;
Шаги можно задавать через ссылки:
this.driver.defineSteps([
{
element: this.button.nativeElement,
popover: {
title: 'Кнопка',
description: 'Описание кнопки'
}
}
]);
Для динамически появляющихся элементов (например, через
*ngIf) требуется дождаться их появления:
setTimeout(() => {
this.startTour();
});
В больших приложениях логика тура выносится в сервис:
import { Injectable } from '@angular/core';
import Driver from 'driver.js';
@Injectable({
providedIn: 'root'
})
export class TourService {
private driver = new Driver();
start(steps: any[]) {
this.driver.defineSteps(steps);
this.driver.start();
}
stop() {
this.driver.reset();
}
}
Использование в компоненте:
constructor(private tourService: TourService) {}
startTour() {
this.tourService.start([
{
element: '#nav',
popover: {
title: 'Навигация',
description: 'Основное меню'
}
}
]);
}
При использовании роутинга необходимо учитывать переходы между страницами:
import { Router } from '@angular/router';
constructor(private router: Router) {}
goToPageAndStartTour() {
this.router.navigate(['/dashboard']).then(() => {
setTimeout(() => {
this.startTour();
});
});
}
Задержка требуется для полной отрисовки нового компонента.
Пример сложного тура:
this.driver.defineSteps([
{
element: '#header',
popover: {
title: 'Шапка',
description: 'Основная информация'
}
},
{
element: '#menu',
popover: {
title: 'Меню',
description: 'Навигация по приложению',
position: 'right'
}
},
{
element: '#content',
popover: {
title: 'Контент',
description: 'Основная область'
}
}
]);
Driver.js не имеет встроенной интеграции с Angular state management, но легко комбинируется с NgRx или сервисами:
if (!this.userService.hasSeenTour()) {
this.startTour();
this.userService.markTourAsSeen();
}
Angular использует инкапсуляцию стилей, поэтому глобальные стили
Driver.js лучше задавать в styles.css:
.driver-popover {
border-radius: 8px;
font-family: Arial, sans-serif;
}
.driver-highlighted-element {
box-shadow: 0 0 10px rgba(0,0,0,0.5);
}
Если используется ViewEncapsulation, может потребоваться
::ng-deep:
::ng-deep .driver-popover {
background-color: #333;
color: #fff;
}
Driver.js предоставляет события, которые удобно использовать в Angular:
this.driver = new Driver({
onReset: () => {
console.log('Тур завершен');
},
onNext: () => {
console.log('Следующий шаг');
}
});
В Angular с ленивой загрузкой модулей важно инициализировать тур внутри конкретного модуля:
ngAfterViewInit() {
if (this.route.snapshot.routeConfig?.path === 'profile') {
this.startTour();
}
}
Элемент не найден
ngAfterViewInit или
setTimeoutТур запускается до загрузки данных
this.api.getData().subscribe(() => {
this.startTour();
});
Конфликт с Angular change detection
NgZone:constructor(private zone: NgZone) {}
this.zone.runOutsideAngular(() => {
this.startTour();
});
В крупных проектах шаги выносятся в отдельные конфигурационные файлы:
export const dashboardTour = [
{
element: '#stats',
popover: {
title: 'Статистика',
description: 'Основные показатели'
}
}
];
Подключение:
import { dashboardTour } from './tours/dashboard-tour';
this.tourService.start(dashboardTour);
При тестировании Angular-приложений (например, с Jasmine или Cypress) туры обычно отключаются:
if (environment.production) {
this.startTour();
}
или через флаг:
if (!this.isTestEnvironment) {
this.startTour();
}
Driver.js корректно работает с компонентами Angular Material, однако важно учитывать overlay-элементы (диалоги, меню):
{
element: '.mat-dialog-container',
popover: {
title: 'Диалог',
description: 'Окно взаимодействия'
}
}
Иногда требуется задержка, чтобы overlay появился:
setTimeout(() => {
this.startTour();
}, 300);
Состояние можно сохранять в LocalStorage:
localStorage.setItem('tourCompleted', 'true');
Проверка:
if (!localStorage.getItem('tourCompleted')) {
this.startTour();
}
Пошаговая навигация по страницам
Реализация требует хранения текущего шага:
localStorage.setItem('tourStep', '2');
@Injectable({ providedIn: 'root' })
export class TourService {
private driver = new Driver({
animate: true,
opacity: 0.7
});
startDashboardTour() {
this.driver.defineSteps([
{
element: '#dashboard',
popover: {
title: 'Dashboard',
description: 'Главная панель'
}
}
]);
this.driver.start();
}
}
@Component({...})
export class DashboardComponent implements AfterViewInit {
constructor(private tour: TourService) {}
ngAfterViewInit() {
this.tour.startDashboardTour();
}
}