Учебный прототип трехшагового онбординга для AG Grid и правого сайдбара заметок. Демонстрационный сценарий построен вокруг планирования путешествий и не привязан к реальному корпоративному продукту.
Навигация, регистрация anchors, backdrop, hotkeys и жизненный цикл тура реализованы библиотекой ngx-ui-tour-tui-hint.
- Angular 19.2.6;
- Taiga UI 4.69.0;
- AG Grid 34.2.0;
ngx-ui-tour-tui-hint8.0.0.
Версия ngx-ui-tour-tui-hint 8 совместима с Angular 19 и Taiga UI 4.
Общую механику тура предоставляет библиотека. Feature-сервис хранит бизнес-контекст и связывает шаги с действиями приложения.
ГДЕ показать -> [tourAnchor] и anchorId
ЧТО показать -> custom tour-step-template
В КАКОМ порядке -> массив IStepOption
ЧТО сделать -> feature-сервис перед tour.next()
flowchart LR
Screen[Экран или таблица]
Feature[Feature tour service]
Tour[TourService]
Anchor[tourAnchor directive]
Hint[TuiHint]
UI[Sidebar, dialog или вкладка]
Screen -->|"start context"| Feature
Feature -->|"initialize / start"| Tour
Tour -->|"anchorId"| Anchor
Anchor --> Hint
Feature -->|"business action"| UI
Feature -->|"next"| Tour
ngx-ui-tour-tui-hint берет на себя:
- хранение текущего шага;
- поиск anchor по
anchorId; - переходы между шагами;
- ожидание шагов с
isAsync; - позиционирование
TuiHint; - backdrop;
- блокировку прокрутки;
- события
start$,stepShow$,end$; - hotkeys и завершение тура.
TravelNotesTourService отвечает за сценарий заметок:
- описывает три шага;
- хранит выбранный
TravelPlanDto; - определяет, какая кнопка в AG Grid получает первый anchor;
- открывает sidebar перед переходом ко второму шагу;
- запускает и завершает
TourService; - сохраняет
mutedвlocalStorage.
sequenceDiagram
participant Grid as AG Grid
participant Feature as TravelNotesTourService
participant Tour as TourService
participant Sidebar as Sidebar
participant Storage as localStorage
Grid->>Feature: prepare(travelPlan)
Feature-->>Grid: выбранная строка получает tourAnchor
Grid->>Feature: start(travelPlan)
Feature->>Tour: start()
Tour-->>Grid: показать первый hint
Grid->>Feature: next()
Feature->>Sidebar: open(travelPlan)
Feature->>Tour: next()
Tour-->>Sidebar: дождаться async anchor и показать второй hint
Sidebar->>Feature: next()
Feature->>Tour: next()
Tour-->>Sidebar: показать третий hint
Tour-->>Feature: end$
Feature->>Storage: сохранить muted
Anchor и step имеют разные идентификаторы:
export const FEATURE_TOUR_ANCHORS = {
trigger: 'feature-trigger',
details: 'feature-details',
} as const;
export const FEATURE_TOUR_STEPS = {
trigger: 'feature-trigger-step',
details: 'feature-details-step',
} as const;anchorIdуказывает, возле какого DOM-элемента показать hint;stepIdидентифицирует сам шаг и помогает выполнять feature-логику вnext().
const STEPS: IStepOption[] = [
{
stepId: FEATURE_TOUR_STEPS.trigger,
anchorId: FEATURE_TOUR_ANCHORS.trigger,
placement: 'right',
enableBackdrop: true,
},
{
stepId: FEATURE_TOUR_STEPS.details,
anchorId: FEATURE_TOUR_ANCHORS.details,
placement: 'left',
enableBackdrop: true,
isAsync: true,
},
];isAsync: true нужен, когда anchor еще не существует на момент запуска тура, например находится внутри sidebar, dialog или лениво созданной вкладки.
Feature-сервис инициализирует библиотеку один раз:
this.tour.initialize(STEPS, {
disablePageScrolling: true,
showProgress: false,
stepDimensions: {
width: 'min(29rem, calc(100vw - 1rem))',
maxWidth: 'calc(100vw - 1rem)',
},
});<tour-step-template> позволяет заменить стандартное содержимое шага собственным Angular-компонентом.
<tour-step-template>
<ng-template let-step="step">
<feature-tour-template [step]="step" />
</ng-template>
</tour-step-template>Компонент template получает текущий IStepOption и по stepId показывает нужный заголовок, preview и кнопку.
Компонент верхнего уровня должен импортировать TourTuiHintModule и содержать <tour-step-template>.
@Component({
imports: [TourTuiHintModule, FeatureTourTemplateComponent],
providers: [provideFeatureTour()],
})
export class FeaturePageComponent {}export const FEATURE_TOUR_ANCHORS = {
trigger: 'feature-trigger',
tabs: 'feature-tabs',
action: 'feature-action',
} as const;
export const FEATURE_TOUR_STEPS = {
trigger: 'feature-trigger-step',
tabs: 'feature-tabs-step',
action: 'feature-action-step',
} as const;Создайте массив IStepOption[] в правильном порядке. Для anchors, которые появятся позже, добавьте isAsync: true.
@Injectable()
export class FeatureTourService {
private readonly localStorage = inject(WA_LOCAL_STORAGE);
private readonly tour = inject(TourService);
private readonly target = signal<Entity | null>(null);
public constructor() {
this.tour.initialize(STEPS, {
disablePageScrolling: true,
showProgress: false,
});
this.tour.end$
.pipe(takeUntilDestroyed(inject(DestroyRef)))
.subscribe(() => {
this.localStorage.setItem('@onboarding.feature-name.v1', 'muted');
});
}
public prepare(entity: Entity): void {
this.target.set(entity);
}
public start(entity: Entity): boolean {
if (this.localStorage.getItem('@onboarding.feature-name.v1') === 'muted') {
return false;
}
this.target.set(entity);
this.tour.start();
return true;
}
public isTarget(entity: Entity): boolean {
return this.target()?.id === entity.id;
}
public next(): void {
if (this.tour.currentStep?.stepId === FEATURE_TOUR_STEPS.trigger) {
const entity = this.target();
if (entity) {
this.openSidebar(entity);
}
}
this.tour.next();
}
private openSidebar(entity: Entity): void {
// Бизнес-действие feature-слоя
}
}prepare() полезен для повторяющихся элементов: он заранее выбирает строку, которая должна зарегистрировать первый anchor, еще до вызова tour.start().
Для единственного элемента:
<div [tourAnchor]="anchors.tabs">
Target второго шага
</div>Для повторяющихся элементов только выбранная строка должна получить anchorId:
<button
[tourAnchor]="featureTour?.isTarget(entity) ? anchors.trigger : ''"
type="button"
>
Открыть
</button>Пустая строка означает, что directive не регистрирует anchor для этой строки.
@Component({
selector: 'feature-tour-template',
template: `
@switch (step().stepId) {
@case (steps.trigger) {
<h3>Первый шаг</h3>
}
@case (steps.tabs) {
<h3>Второй шаг</h3>
}
}
<button type="button" (click)="featureTour.next()">
Далее
</button>
`,
})
export class FeatureTourTemplateComponent {
protected readonly featureTour = inject(FEATURE_TOUR);
protected readonly steps = FEATURE_TOUR_STEPS;
public readonly step = input.required<IStepOption>();
}Для последнего шага вызовите tour.end() вместо next().
Для обычного компонента target должен существовать в DOM. Для AG Grid также нужно показать колонку и привести строку в viewport.
выбрать entity
-> prepare(entity)
-> дождаться renderer
-> показать колонку
-> привести строку в viewport
-> start(entity)
В демо prepare() вызывается заранее, а start() запускается после firstDataRendered.
Provider возвращает feature-сервис или null:
export const FEATURE_TOUR = new InjectionToken<FeatureTourService | null>(
'[FEATURE_TOUR]: FeatureTourService',
);
export function provideFeatureTour(): Provider {
return [
FeatureTourService,
{
provide: FEATURE_TOUR,
useFactory: () =>
inject(APP_CONFIG).features?.enableOnboarding
? inject(FeatureTourService)
: null,
},
];
}При выключенном флаге [tourAnchor] должен получить пустую строку.
Библиотека может дождаться регистрации anchor, но sidebar, dialog или вкладку должен открыть feature-сервис.
Правильный порядок:
feature action
-> DOM следующего anchor создается
-> tour.next()
-> библиотека показывает следующий шаг
Для виртуализированной таблицы feature-сервис явно хранит выбранную entity. Не следует назначать один anchorId всем строкам и надеяться, что библиотека выберет нужную.
В демо tour.end$ записывает:
@onboarding.travel-notes.v1 = muted
Чтобы показать значительно измененный тур повторно, увеличьте версию ключа. Для локальной проверки удалите ключ вручную.
Если пользователь закрывает sidebar во время активного тура, завершите TourService, чтобы hint не остался привязанным к уничтоженному anchor.
if (this.tour.currentStep) {
this.tour.end();
}Проверьте:
tour.initialize()был вызван;- первый
anchorIdзарегистрирован в DOM; - выбранный renderer получил непустой
[tourAnchor]; - колонка видима;
- строка находится в viewport;
- muted-state отсутствует.
Проверьте:
- у шага указано
isAsync: true; - feature-сервис открыл sidebar или dialog;
- DOM содержит элемент с нужным
anchorId; tour.next()вызывается после feature-действия.
localStorage.removeItem('@onboarding.travel-notes.v1');
location.reload();До bootstrap приложение загружает src/config.json и предоставляет конфигурацию через APP_CONFIG.
{
"features": {
"enableOnboarding": true
}
}provideTravelNotesTour() предоставляет TravelNotesTourService при включенном флаге и null при выключенном.
npm ci
npm startОткройте http://localhost:4200.
Чтобы отключить онбординг, установите enableOnboarding: false в src/config.json и перезапустите приложение.
- Таблица содержит вымышленные маршруты, страны, сезоны, длительность и статус планирования.
- Первый шаг привязан к иконке заметок выбранного маршрута.
Далееоткрывает sidebar и переводит библиотечный tour на второй шаг.- Второй шаг объясняет категории
Все,ПодготовкаиВпечатления. - Третий шаг показывает, как добавить важную заметку в чек-лист подготовки.
- Крестик или
Понятнозавершают tour и сохраняют muted-state. - Прозрачный backdrop блокирует интерфейс под текущим шагом.
npm run buildДля GitHub Pages:
npm run build:pagesРезультат находится в dist/agrid-taiga-onboarding-ngx-tour/browser.
CI устанавливает зависимости через npm ci с использованием закоммиченного package-lock.json.