9

I am implementing several of Hopscotch tours in my app.

So far I have managed to make many tours without any problem but today, I am facing a challenge I could not resolve far.

My question is: how do I get a tour step target to work on dynamically generated content?

Here is the HTML:

<div class="pacote-destino-quartos-wrapper">
    <div class="pacote-destino-quartos-internal-wrapper">
        <h4>Todos os Destinos</h4>
        <div class="dynamic_nested_form_wrapper quartos_external_wrapper" data-destino="todos">
            <span class="add-child-link-wrapper">
                <a href="javascript:void(0)" class="add_child btn btn-info" data-association="quartos">
                    <i class="icon-plus icon-white"></i>
                </a>
            </span>
        </div>
    </div>
</div>

Whenever I click the link it dynamically creates a div which contains many elements; one of them is a div with a class called .quarto-config-wrapper.

If I try to make my Hopscotch tour go to this element, it does not work; my guess the dynamically created elements are not available in the DOM for manipulation.

Here is my Hopscotch steps code:

{
    title: "Adicionar um novo quarto",
    content: "content here",
    target: $('.add-child-link-wrapper')[0],
    placement: "left",
    width: 500,
    yOffset: -15,
    nextOnTargetClick: true,
    showNextButton: false
},
{
    title: "Menu de configuração do quarto",
    content: "content here",
    target: $('.quarto-config-wrapper')[0],
    placement: "left",
    width: 700,
    yOffset: -15,
    nextOnTargetClick: true,
    showNextButton: false,
    delay: 1200
}

The first step works but the second does not.

What am I doing wrong and how can I fix it?

nyedidikeke
  • 6,899
  • 7
  • 44
  • 59
Guido
  • 387
  • 2
  • 13
  • I have tried to call from the first static element up from the dinamically created... did not work – Guido Mar 12 '14 at 14:59

6 Answers6

8

I searched all over the place for a solution to this issue and this post is the one that came the closest but not quite definitive solution, so here it goes:

{ // This is the previous step
    element: "#idElement",
    title: "The Title",
    content: "The Content",
    onNext: function(tour) {
            tour.end();
            var checkExist = setInterval(function() {
                // This is the element from the next step. 
                $element = $('#idElementFromNextStep'); 

                if ($element.is(':visible')) {
                    clearInterval(checkExist);
                    tour.start(true); // True is to force the tour to start
                    tour.goTo(1); // The number is your next index (remember its base 0)
                }
            }, 100);
    },
    multipage: true, // Required
    orphan: true // Recommended
},{ // This is the step that was not working
    element: "#idElementFromNextStep",
    title: "Title of the step",
    content: "Details of the step",
    orphan: true,
}

So what this does is basically stop the tour when the next is fired, wait for the element to be added to the DOM and then restart the tour from the correct step by its index.

I borrowed a bit of code from @Luksurious. His solution kind of works (not for Bootstrap Tour though) but in any case it generates a flick when it loads the next step and goes back to the correct one.

I strongly advise against using a delay, it might seem to work on your local environment but its extremely dangerous to speculate with how long it takes a client to load.

Hope it helps someone else!

Lisandro
  • 10,543
  • 1
  • 24
  • 29
3

One hackaround I'm using is the delay field on each step. That is:

{
  title: "+Log",
  content: "Lipsum",
  target: "#log_activity_nav",
  placement: "left",
  nextOnTargetClick: true


},

{

  title: "Phone",
  content: "The bottom section is where each individual log and detail lives, sorted by importance.",
  target: "#activity_log_activity_log_brokers_attributes_0_contact_attributes_phone", // this target is part of an element that is dynamically generated
  delay: 2000, // wait 2 seconds for this element to load
  placement: "top"
},

This seems to fill my needs for now, although obviously it's not guaranteed to work depending on what you're loading.

If anyone has a more elegant solution I'm all ears!

Josiah
  • 114
  • 11
  • The Delay solution has worked for me as well... but it is definetely a "hackaround" as you said! – Guido Apr 08 '14 at 16:30
2

What I do to wait for AJAX calls is the following:

{
  title: "Title",
  content: "Content",
  target: '.triggerAjax',
  placement: 'bottom',
  onNext: function() {
    $('.triggerAjax').click();

    var checkExist = setInterval(function() {
      $element = $('.dynamicallyLoaded');

      if ($element.is(':visible')) {
        clearInterval(checkExist);

        window.hopscotch.startTour(window.hopscotch.getCurrTour(), window.hopscotch.getCurrStepNum());
      }
    }, 100);
  },
  multipage: true
},

Using multipage tells hopscotch to not continue. Then the onNext function checks if the element we want to target next is present and starts the tour again.

Of course, if you need this more often, extract it as a general function to keep your code clean and short.

Luksurious
  • 991
  • 9
  • 15
  • I think the `this` passed in to `startTour` should be `window.hopscotch.getCurrTour()`. – Jim Jan 28 '15 at 11:07
  • I up-voted this answer as it was what I was looking for but it's not quite working for me even after my above change to the line that starts the tour. I can't seem to start the second step by calling `startTour` from within the onNext function. I can if I get a reference to the tour object outside of it and then pass that in to`startTour`. – Jim Jan 28 '15 at 11:13
  • My tours are inside a bigger structure where the current tour is saved in a variable. What you could do is save the tour object in a variable `onStart: function () { window.lastTour = window.hopscotch.getCurrTour(); } ` and then access that variable from within the interval function. – Luksurious Jan 28 '15 at 15:09
1

Another solution (which works really well if the modal you are showing is another page) is to borrow from what the multi-page tour example. Essentially, on the page popping up the modal is where you would start the tour. However, on the page the modal actually loads (or as part of the dynamically generated content), you could do something like if (hopscotch.getState() == 'intro_tour:1') { hopscotch.startTour() }, which would essentially continue the tour started by the loader page. YMMV with this method, depending on what your modal loads.

Josiah
  • 114
  • 11
1

I solved this problem by having the target of a step be a getter so it queries the dom for the target when it arrives at the step itself

{ get target() { return document.querySelector('#your-step-target'); }
w0ps
  • 11
  • 4
  • This is THE answer. I have some hidden input so this is the best solution, avoiding readystate, handlers, events, bla bla bla. Clean and effective. Thanks @w0ps – MatteoPHRE Jul 20 '21 at 15:17
0

I couldn't get anyone's suggestions to work, the problem seems to be that the target attribute for the dynamically generated content is calculated at the moment the tour starts (when it is not yet on the page).

What works for me is to nest one tour inside another. Here's my tested and working code:

var tourP1 = {
  id: "tutp1",
  steps: [
    {
      title: "Step 1",
      content: "Lorem ipsum dolor.",
      target: "header",
      placement: "bottom"
    },
    {
      title: "Step 2",
      content: "Lorem ipsum dolor.",
      target: document.querySelector('#content'),
      placement: "top",
      multipage: true,
      onNext: function() {
        // Trigger dynamic content
        $('.someElement').trigger('click');
        // Wait for your target to become visible
        var checkExist = setInterval(function() {
          $element = $('.someElement').find('.myElement');
          if ($element.is(':visible')) {
            clearInterval(checkExist);
            // End this tour
            hopscotch.endTour(true);
            // Define the next tour
            var tourP2 = {
              id: "tutp2",
              steps: [
                {
                  title: "Step 3",
                  content: "Lorem ipsum dolor.",
                  target: $('.someElement').find('.myElement')[0],
                  placement: "bottom"
                }
              ]
            }
            // Start the nested tour
            hopscotch.startTour(tourP2);
          }
        }, 100);
      }
    },
    // This is needed for the onNext function in the previous step to work
    {
      title: "",
      target: "",
      placement: ""
    }
  ]
};

One caveat here is that the step number returns to 1 when the nested tour is started. A hack would be to pad the tour with some placeholder steps and start from the 'real' first step (yuck!). What we really need is a way to specify the step number in the step options.