Интерактивные подсказки, реализованные с помощью Intro.js, часто зависят от текущего состояния DOM. При изменении структуры страницы — динамической подгрузке контента, изменении видимости элементов, рендеринге компонентов — заранее заданные шаги могут устаревать. Это приводит к некорректному позиционированию подсказок, ошибкам или пропуску шагов. Для предотвращения подобных проблем требуется механизм обновления конфигурации шагов.
Основные сценарии, при которых требуется пересборка шагов:
Если шаг ссылается на элемент, которого ещё нет в DOM, Intro.js не сможет корректно его обработать.
Intro.js не отслеживает изменения DOM автоматически. При изменении
структуры необходимо вручную обновить шаги через метод
setOptions().
const intro = introJs();
function buildSteps() {
return [
{
element: document.querySelector('#step1'),
intro: 'Первый шаг'
},
{
element: document.querySelector('#step2'),
intro: 'Второй шаг'
}
];
}
intro.setOptions({
steps: buildSteps()
});
После обновления можно заново запустить или продолжить тур:
intro.start();
Перед добавлением шага важно убедиться, что элемент существует:
function safeStep(selector, text) {
const el = document.querySelector(selector);
return el ? { element: el, intro: text } : null;
}
const steps = [
safeStep('#step1', 'Шаг 1'),
safeStep('#step2', 'Шаг 2')
].filter(Boolean);
intro.setOptions({ steps });
Это предотвращает ошибки и исключает “пустые” шаги.
Если DOM изменяется во время активного тура, можно:
const currentStep = intro._currentStep;
intro.exit();
intro.setOptions({
steps: buildSteps()
});
intro.goToStep(currentStep + 1).start();
Важно учитывать, что _currentStep — внутреннее свойство,
неофициальное API. Его использование требует осторожности.
Для автоматического реагирования на изменения DOM применяется
MutationObserver.
const observer = new MutationObserver(() => {
intro.setOptions({
steps: buildSteps()
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
Этот подход полезен при:
Однако частые обновления могут негативно влиять на производительность, поэтому рекомендуется добавлять дебаунс.
let timeout;
const observer = new MutationObserver(() => {
clearTimeout(timeout);
timeout = setTimeout(() => {
intro.setOptions({
steps: buildSteps()
});
}, 300);
});
Это предотвращает множественные пересчёты при серии быстрых изменений.
Если элемент появляется с задержкой, используется ожидание:
function waitForElement(selector, callback) {
const interval = setInterval(() => {
const el = document.querySelector(selector);
if (el) {
clearInterval(interval);
callback(el);
}
}, 100);
}
waitForElement('#dynamic', (el) => {
intro.addStep({
element: el,
intro: 'Динамический элемент'
});
});
Intro.js позволяет модифицировать список шагов:
intro.addStep({
element: document.querySelector('#new'),
intro: 'Новый шаг'
});
Удаление напрямую не предусмотрено, но можно пересобрать массив шагов:
const steps = intro._options.steps.filter(step => step.element !== '#old');
intro.setOptions({ steps });
Даже если элемент существует, он может быть скрыт
(display: none или visibility: hidden). Перед
добавлением шага:
function isVisible(el) {
return el.offsetParent !== null;
}
Использование:
const el = document.querySelector('#step');
if (el && isVisible(el)) {
steps.push({ element: el, intro: 'Текст' });
}
Обновление шагов после рендера:
useEffect(() => {
intro.setOptions({
steps: buildSteps()
});
}, [state]);
watch(() => state.value, () => {
nextTick(() => {
intro.setOptions({
steps: buildSteps()
});
});
});
ngAfterViewInit() {
this.intro.setOptions({
steps: this.buildSteps()
});
}
Intro.js предоставляет события:
intro.onbeforechange((targetElement) => {
if (!document.body.contains(targetElement)) {
intro.nextStep();
}
});
intro.onafterchange((targetElement) => {
// дополнительная логика
});
Это позволяет адаптироваться к изменениям DOM прямо во время тура.
Все шаги формируются в одном месте:
function buildSteps() {
const steps = [];
const el1 = document.querySelector('#a');
if (el1) steps.push({ element: el1, intro: 'A' });
const el2 = document.querySelector('#b');
if (el2) steps.push({ element: el2, intro: 'B' });
return steps;
}
<div data-intro="Описание" data-step="1"></div>
intro.setOptions({
steps: [...document.querySelectorAll('[data-intro]')].map(el => ({
element: el,
intro: el.dataset.intro
}))
});
Такой подход автоматически адаптируется к DOM.
Распространённые проблемы:
Решение:
intro.onbeforechange((el) => {
if (!el || !document.body.contains(el)) {
intro.nextStep();
}
});
При переходах между “страницами” в SPA:
router.afterEach(() => {
setTimeout(() => {
intro.setOptions({
steps: buildSteps()
});
intro.start();
}, 300);
});
Для сложных интерфейсов рекомендуется:
Пример:
const stepConfig = [
{
selector: '#a',
text: 'A'
}
];
function buildSteps() {
return stepConfig
.map(cfg => {
const el = document.querySelector(cfg.selector);
return el ? { element: el, intro: cfg.text } : null;
})
.filter(Boolean);
}
При динамическом добавлении шагов порядок может нарушаться. Для управления используется явная нумерация:
steps.sort((a, b) => a.step - b.step);
Динамическое обновление шагов — критически важный аспект при работе с современными интерфейсами, где DOM постоянно изменяется. Без этого механизма интерактивные подсказки теряют точность и надёжность.