В основе работы Shepherd.js лежит концепция пошаговых туров, однако в реальных приложениях статичная последовательность шагов оказывается недостаточной. Интерфейс может изменяться в зависимости от состояния приложения, пользовательских действий или асинхронных данных. В таких условиях требуется реактивный подход — способность тура адаптироваться к изменениям среды выполнения.
Реактивность в контексте Shepherd.js — это управление жизненным циклом шагов с учётом динамики DOM, состояния UI и логики приложения.
Тур должен учитывать текущее состояние интерфейса. Например:
Для решения этой задачи используется комбинация:
when)attachTobeforeShowPromise, show,
hide)Пример:
const tour = new Shepherd.Tour({
defaultStepOptions: {
scrollTo: true,
cancelIcon: {
enabled: true
}
}
});
tour.addStep({
id: 'dynamic-step',
text: 'Этот шаг зависит от наличия элемента',
attachTo: {
element: () => document.querySelector('.dynamic-element'),
on: 'bottom'
},
beforeShowPromise: function () {
return new Promise((resolve) => {
const interval = setInterval(() => {
if (document.querySelector('.dynamic-element')) {
clearInterval(interval);
resolve();
}
}, 100);
});
}
});
Здесь шаг не отобразится, пока элемент не появится в DOM.
Частая ситуация — элемент появляется после рендера или загрузки данных. Shepherd.js не отслеживает DOM автоматически, поэтому необходимо реализовать ожидание вручную.
Подходы:
function waitForElement(selector) {
return new Promise((resolve) => {
const element = document.querySelector(selector);
if (element) {
return resolve(element);
}
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
resolve(el);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
Использование:
beforeShowPromise: () => waitForElement('.async-element')
Не все шаги должны отображаться всегда. Например:
Для этого используется логика внутри when.show или
динамическое добавление шагов.
tour.addStep({
id: 'conditional-step',
text: 'Этот шаг показывается только при условии',
when: {
show: function () {
if (!window.user.isNew) {
tour.next();
}
}
}
});
Альтернативный подход — формировать список шагов перед запуском:
if (user.isNew) {
tour.addStep({ ... });
}
В React состояние интерфейса управляется через state и
props. Shepherd.js работает напрямую с DOM, поэтому важно
синхронизировать эти два мира.
Основные принципы:
useEffectuseRefimport { useEffect, useRef } from 'react';
import Shepherd from 'shepherd.js';
function App() {
const tourRef = useRef(null);
useEffect(() => {
tourRef.current = new Shepherd.Tour({});
tourRef.current.addStep({
text: 'Добро пожаловать',
attachTo: {
element: '.header',
on: 'bottom'
}
});
tourRef.current.start();
}, []);
return <div className="header">...</div>;
}
Если элемент появляется после асинхронного обновления:
useEffect(() => {
if (dataLoaded) {
tourRef.current.start();
}
}, [dataLoaded]);
В Vue реактивность встроена в систему. Основная задача — дождаться обновления DOM после изменения состояния.
this.$nextTick(() => {
tour.start();
});
Тур может изменяться в зависимости от действий пользователя:
tour.addStep({
id: 'click-step',
text: 'Нажмите на кнопку',
attachTo: {
element: '.btn',
on: 'bottom'
},
advanceOn: {
selector: '.btn',
event: 'click'
}
});
Это создаёт реактивное поведение: шаг автоматически завершается при действии пользователя.
Shepherd позволяет управлять шагами во время выполнения:
tour.addStep({ id: 'step1', text: 'Шаг 1' });
if (condition) {
tour.addStep({ id: 'step2', text: 'Дополнительный шаг' });
}
Удаление:
tour.removeStep('step2');
Реактивность может выражаться в изменении маршрута тура:
tour.addStep({
id: 'branching-step',
text: 'Выберите действие',
buttons: [
{
text: 'Вариант A',
action: () => tour.show('stepA')
},
{
text: 'Вариант B',
action: () => tour.show('stepB')
}
]
});
Так реализуется нелинейный сценарий.
Ключевой инструмент реактивности — beforeShowPromise. Он
позволяет задержать показ шага до выполнения асинхронной операции:
tour.addStep({
id: 'async-step',
text: 'Данные загружаются...',
beforeShowPromise: () => {
return fetch('/api/data')
.then(response => response.json())
.then(data => {
window.data = data;
});
}
});
Если элемент скрыт (display: none), Shepherd не сможет
корректно вычислить позицию.
Решения:
beforeShowPromisebeforeShowPromise: () => {
return new Promise(resolve => {
const el = document.querySelector('.hidden');
el.style.display = 'block';
resolve();
});
}
Если элемент перемещается (например, при анимации или ресайзе), необходимо обновить позицию:
window.addEventListener('resize', () => {
const step = tour.getCurrentStep();
if (step) {
step.updateStepOptions({});
}
});
Shepherd предоставляет события:
startshowhidecompletecancelПример:
tour.on('show', (event) => {
console.log('Показ шага:', event.step.id);
});
Это позволяет реагировать на изменения состояния тура.
В одностраничных приложениях шаги могут зависеть от маршрута:
tour.addStep({
id: 'route-step',
text: 'Этот шаг на другой странице',
beforeShowPromise: () => {
return new Promise((resolve) => {
router.push('/dashboard');
setTimeout(resolve, 300);
});
}
});
В сложных системах может существовать несколько туров:
Выбор тура:
if (user.isFirstVisit) {
onboardingTour.start();
} else {
featureTour.start();
}
Тур можно запускать при определённых условиях:
setTimeout(() => {
if (!user.hasSeenTour) {
tour.start();
}
}, 2000);
Если элемент не найден:
Решение:
attachTo: {
element: () => {
const el = document.querySelector('.safe');
return el || 'body';
},
on: 'center'
}
advanceOnПример:
function createStep(id, selector, text) {
return {
id,
text,
attachTo: {
element: selector,
on: 'bottom'
}
};
}
Реактивный тур в Shepherd.js строится на сочетании:
Такая архитектура позволяет создавать гибкие, устойчивые и адаптивные пользовательские сценарии, которые корректно работают даже в сложных SPA-интерфейсах.