При работе с библиотекой Intro.js часто возникает ситуация, когда элементы интерфейса появляются не сразу: данные загружаются с сервера, компоненты рендерятся динамически, или DOM обновляется после пользовательских действий. В таких условиях стандартный запуск тура приводит к ошибкам — шаги не находят целевые элементы, позиционирование нарушается, а часть сценария пропускается.
Основная сложность заключается в том, что Intro.js ожидает наличие всех элементов на момент инициализации. При асинхронной загрузке это условие не выполняется.
Intro.js формирует шаги на основе:
element)steps)При запуске:
introJs().start();
библиотека:
Если элемент отсутствует — шаг игнорируется или вызывает некорректное поведение.
Самый базовый способ — отложить запуск тура до появления нужных элементов.
setTimeoutsetTimeout(() => {
introJs().start();
}, 1000);
Недостатки:
Более надёжный способ — проверять DOM до тех пор, пока элемент не появится.
function waitForElement(selector, callback) {
const interval = setInterval(() => {
if (document.querySelector(selector)) {
clearInterval(interval);
callback();
}
}, 100);
}
waitForElement('#async-element', () => {
introJs().start();
});
Преимущества:
Современный и эффективный способ отслеживания изменений DOM.
const observer = new MutationObserver((mutations, obs) => {
if (document.querySelector('#async-element')) {
introJs().start();
obs.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
Особенности:
Если элементы создаются асинхронно, шаги лучше формировать после их появления:
function buildSteps() {
return [
{
element: document.querySelector('#step1'),
intro: 'Первый шаг'
},
{
element: document.querySelector('#step2'),
intro: 'Второй шаг'
}
];
}
introJs().setOptions({
steps: buildSteps()
}).start();
При изменении DOM во время прохождения тура:
const intro = introJs();
intro.onbeforechange(() => {
if (!document.querySelector('#dynamic-element')) {
intro.exit();
waitForElement('#dynamic-element', () => {
intro.start();
});
}
});
Иногда элемент существует, но его содержимое подгружается позже.
onafterchangeintroJs().onafterchange(function(targetElement) {
if (targetElement.id === 'data-container') {
loadData().then(() => {
introJs().refresh();
});
}
});
Где:
function loadData() {
return fetch('/api/data')
.then(res => res.json())
.then(data => {
document.querySelector('#data-container').innerHTML = data.content;
});
}
Метод refresh():
Проблема: компоненты рендерятся после жизненного цикла.
Решение:
useEffect(() => {
if (dataLoaded) {
introJs().start();
}
}, [dataLoaded]);
watch(() => isReady, (val) => {
if (val) {
nextTick(() => {
introJs().start();
});
}
});
ngAfterViewInit() {
this.dataService.getData().subscribe(() => {
setTimeout(() => {
introJs().start();
});
});
}
Если следующий шаг зависит от асинхронного действия:
introJs().onbeforechange(function(targetElement) {
if (targetElement.id === 'step-async') {
return new Promise((resolve) => {
fetchData().then(() => {
resolve();
});
});
}
});
Важно: стандартный Intro.js не поддерживает Promise напрямую, поэтому
требуется кастомная обёртка или контроль через exit() и
start().
Шаг создаётся только в момент необходимости:
introJs().onbeforechange(function(targetElement) {
if (targetElement.id === 'placeholder') {
waitForElement('#real-element', () => {
introJs().addStep({
element: '#real-element',
intro: 'Динамический шаг'
});
introJs().nextStep();
});
}
});
Причины:
display: none)Причины:
Решение:
setTimeout(() => {
introJs().refresh();
}, 0);
Решение:
window.onload как единственной точки
запускаНа практике используется сочетание методов:
function initTour() {
waitForElement('#step1', () => {
waitForElement('#step2', () => {
introJs().setOptions({
steps: [
{ element: '#step1', intro: 'Шаг 1' },
{ element: '#step2', intro: 'Шаг 2' }
]
}).start();
});
});
}
Или через MutationObserver + refresh:
const intro = introJs();
const observer = new MutationObserver(() => {
intro.refresh();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
intro.start();
Основные хуки:
onbeforechangeonafterchangeoncompleteonexitПример:
introJs()
.onbeforechange((el) => {
if (!el) return false;
})
.onafterchange((el) => {
introJs().refresh();
})
.start();
Если элемент может исчезнуть:
introJs().onbeforechange(function(targetElement) {
if (!document.body.contains(targetElement)) {
introJs().nextStep();
}
});
refresh()Такая архитектура позволяет создавать устойчивые и предсказуемые пошаговые интерфейсы даже в полностью динамических приложениях.