Обновление шагов при изменении DOM

Интерактивные подсказки, реализованные с помощью Intro.js, часто зависят от текущего состояния DOM. При изменении структуры страницы — динамической подгрузке контента, изменении видимости элементов, рендеринге компонентов — заранее заданные шаги могут устаревать. Это приводит к некорректному позиционированию подсказок, ошибкам или пропуску шагов. Для предотвращения подобных проблем требуется механизм обновления конфигурации шагов.


Причины необходимости обновления шагов

Основные сценарии, при которых требуется пересборка шагов:

  • Асинхронная загрузка данных (AJAX, fetch)
  • Рендеринг компонентов (React, Vue, Angular)
  • Условное отображение элементов (toggle, tabs, accordion)
  • Изменение структуры DOM через JavaScript
  • Переходы между состояниями интерфейса без перезагрузки страницы (SPA)

Если шаг ссылается на элемент, которого ещё нет в 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 изменяется во время активного тура, можно:

  1. Остановить текущий тур
  2. Обновить шаги
  3. Запустить заново с нужного шага
const currentStep = intro._currentStep;

intro.exit();

intro.setOptions({
  steps: buildSteps()
});

intro.goToStep(currentStep + 1).start();

Важно учитывать, что _currentStep — внутреннее свойство, неофициальное API. Его использование требует осторожности.


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

Для автоматического реагирования на изменения DOM применяется MutationObserver.

const observer = new MutationObserver(() => {
  intro.setOptions({
    steps: buildSteps()
  });
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

Этот подход полезен при:

  • динамическом добавлении элементов
  • ленивой загрузке контента
  • работе с SPA

Однако частые обновления могут негативно влиять на производительность, поэтому рекомендуется добавлять дебаунс.


Дебаунс обновлений

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: 'Текст' });
}

Интеграция с фреймворками

React

Обновление шагов после рендера:

useEffect(() => {
  intro.setOptions({
    steps: buildSteps()
  });
}, [state]);

Vue

watch(() => state.value, () => {
  nextTick(() => {
    intro.setOptions({
      steps: buildSteps()
    });
  });
});

Angular

ngAfterViewInit() {
  this.intro.setOptions({
    steps: this.buildSteps()
  });
}

Синхронизация с событиями Intro.js

Intro.js предоставляет события:

intro.onbeforechange((targetElement) => {
  if (!document.body.contains(targetElement)) {
    intro.nextStep();
  }
});
intro.onafterchange((targetElement) => {
  // дополнительная логика
});

Это позволяет адаптироваться к изменениям DOM прямо во время тура.


Стратегии построения шагов

1. Централизованная функция

Все шаги формируются в одном месте:

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

2. Декларативный подход через data-атрибуты

<div data-intro="Описание" data-step="1"></div>
intro.setOptions({
  steps: [...document.querySelectorAll('[data-intro]')].map(el => ({
    element: el,
    intro: el.dataset.intro
  }))
});

Такой подход автоматически адаптируется к DOM.


Обработка ошибок

Распространённые проблемы:

  • элемент удалён до показа шага
  • элемент перекрыт другим слоем
  • элемент вне viewport

Решение:

intro.onbeforechange((el) => {
  if (!el || !document.body.contains(el)) {
    intro.nextStep();
  }
});

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

  • минимизировать частоту пересборки шагов
  • избегать глобальных наблюдателей без фильтрации
  • использовать ленивую инициализацию
  • кешировать селекторы при возможности

Поведение при SPA-навигации

При переходах между “страницами” в SPA:

  1. Останавливать текущий тур
  2. Дождаться рендера нового состояния
  3. Пересобрать шаги
  4. Запустить тур заново
router.afterEach(() => {
  setTimeout(() => {
    intro.setOptions({
      steps: buildSteps()
    });
    intro.start();
  }, 300);
});

Поддержание актуальности состояния

Для сложных интерфейсов рекомендуется:

  • хранить конфигурацию шагов отдельно от DOM
  • использовать функции-резолверы элементов
  • избегать жёсткой привязки к статическим селекторам

Пример:

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
  • необходимо проверять существование и видимость элементов
  • обновление должно происходить после изменений интерфейса
  • предпочтителен централизованный механизм генерации шагов
  • автоматическое отслеживание через MutationObserver требует оптимизации

Динамическое обновление шагов — критически важный аспект при работе с современными интерфейсами, где DOM постоянно изменяется. Без этого механизма интерактивные подсказки теряют точность и надёжность.