Использование с Angular

Библиотека 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
  });
}

Жизненный цикл Angular и запуск тура

Важно учитывать, что 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 на ранних этапах.


Работа с ViewChild и динамическими элементами

При использовании 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: 'Основное меню'
      }
    }
  ]);
}

Обработка маршрутов (Angular Router)

При использовании роутинга необходимо учитывать переходы между страницами:

import { Router } from '@angular/router';

constructor(private router: Router) {}

goToPageAndStartTour() {
  this.router.navigate(['/dashboard']).then(() => {
    setTimeout(() => {
      this.startTour();
    });
  });
}

Задержка требуется для полной отрисовки нового компонента.


Конфигурация шагов с Angular-спецификой

Пример сложного тура:

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();
}

Кастомизация через CSS

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

Driver.js предоставляет события, которые удобно использовать в Angular:

this.driver = new Driver({
  onReset: () => {
    console.log('Тур завершен');
  },
  onNext: () => {
    console.log('Следующий шаг');
  }
});

Lazy-loaded модули

В Angular с ленивой загрузкой модулей важно инициализировать тур внутри конкретного модуля:

ngAfterViewInit() {
  if (this.route.snapshot.routeConfig?.path === 'profile') {
    this.startTour();
  }
}

Частые проблемы и решения

Элемент не найден

  • Причина: Angular ещё не отрисовал DOM
  • Решение: использовать ngAfterViewInit или setTimeout

Тур запускается до загрузки данных

  • Решение: запускать после получения данных:
this.api.getData().subscribe(() => {
  this.startTour();
});

Конфликт с Angular change detection

  • В редких случаях помогает NgZone:
constructor(private zone: NgZone) {}

this.zone.runOutsideAngular(() => {
  this.startTour();
});

Оптимизация производительности

  • Не создавать новый экземпляр Driver при каждом запуске
  • Переиспользовать сервис
  • Минимизировать количество шагов
  • Избегать сложных DOM-запросов

Структурирование туров

В крупных проектах шаги выносятся в отдельные конфигурационные файлы:

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();
}

Совмещение с Angular Material

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();
}

Расширенные сценарии

Пошаговая навигация по страницам

  1. Запуск тура на первой странице
  2. Переход через Router
  3. Продолжение тура

Реализация требует хранения текущего шага:

localStorage.setItem('tourStep', '2');

Безопасность и UX

  • Не перекрывать критически важные элементы
  • Ограничивать частоту показа тура
  • Давать возможность закрытия
  • Использовать понятные описания

Архитектурные рекомендации

  • Вынос логики в сервис
  • Разделение туров по модулям
  • Использование конфигурационных файлов
  • Минимизация зависимости от конкретной разметки

Пример полной интеграции

@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();
  }
}