KABBOUCHI / vue-tippy

VueJS Tooltip powered by Tippy.js
https://vue-tippy.netlify.app
MIT License
733 stars 91 forks source link

Unable to generate the tooltips components dynamically #40

Closed AlexandreBonneau closed 6 years ago

AlexandreBonneau commented 6 years ago

Starting from my simple tooltip example where a tooltip component is cloned as many times as needed, I tried modifying the underlying data in order to generate a dynamic amount of elements (and therefore tooltips components), however it seems vue-tippy stops transforming those components in tooltips.

I first tried a simple synchronous version, however while I added the async version, it broke and I can't find a way to display the tooltips anymore. Whoops! :smirk:

Then I created an asynchronous version where the tasks can either be added synchronously or asynchronously, in order to test if the tooltip components are correctly created.

As you can see, the tooltip components are correctly created, but directly displayed instead of being hidden.

Would you know if that's a bug and how to fix it?

KABBOUCHI commented 6 years ago

remove the id prop from the component

image

image

AlexandreBonneau commented 6 years ago

Thanks @KABBOUCHI, this indeed fixed it.

Just so I understand why it was bugging, why is declaring id as a prop a problem? Is there any reserved prop name we should not use? Perhaps a small mention about that limitation in the documentation could help other users?

Thank you again :+1:

KABBOUCHI commented 6 years ago

I don't know :), but you can use id as a prop if you use it inside the template

image

https://codepen.io/KABBOUCHI/pen/aYdJYE?editors=1010

I'll check it later and add it in the docs if needed

edit: because the component was expecting it as a prop not as a attribute, i guess its the correct behavior, if u disagree u can create a issue here vue

AlexandreBonneau commented 6 years ago

I'm good, thanks for your time and awesome component :)

AlexandreBonneau commented 6 years ago

Well, I tried to fix the bug is my real code based on those codepen snippets, however vue-tippy still does not use the generated components as tooltips.

I've narrowed down the cause of that to a simple thing I guess; in the createTippy() function, I modified the code to:

if (opts.html) {
  if (opts.reactive) {
    console.log('Vue-tippy: html and reactive prop detected...'); //DEBUG
    opts.html = document.querySelector(opts.html);
    console.log('opts.html:', opts.html); //DEBUG
  } else {
    if (document.querySelector(opts.html)._tipppyReferences) {
      document.querySelector(opts.html)._tipppyReferences.push(el);
    } else {
      document.querySelector(opts.html)._tipppyReferences = [el];
    }
  }
}

and whenever I generate new object on which a tooltip should be shown, I see this in the console:

opts.html: null

It looks like the v-tippy directive is called before all the components have time to be generated by Vue. Would there be a way to prevent that? Perhaps run createTippy() in a nextTick?

I'm not sure how I could create a codepen that reproduce that behavior (my code is pretty huge right now, and I'm not sure at which point the delay is great enough that it makes vue-tippy bug, sorry.

AlexandreBonneau commented 6 years ago

Ok so, I modified the directive so that createTippy() is always called on the nextTick using:

Vue.directive('tippy', {
  inserted: function inserted(el, binding, vnode) {
      Vue.nextTick(function() {
          createTippy(el, binding, vnode);
      });
  },
  unbind: function unbind(el) {
    el._tippy && el._tippy.destroy();
  },
  componentUpdated: function componentUpdated(el, binding, vnode) {
    var opts = binding.value || {};
    var oldOpts = binding.oldValue || {};

    if (el._tippy && JSON.stringify(opts) !== JSON.stringify(oldOpts)) {
        Vue.nextTick(function() {
            createTippy(el, binding, vnode);
        });
    }

That almost work!

When using that, it correctly render the first component in the list and displays it correctly as a tooltip, however the subsequent components are just always displayed, except their arrow which is displayed like it should on hover.

I'm investigating.

AlexandreBonneau commented 6 years ago

Using the nextTick hack, when I continue to add new elements that should have a tooltip component added to them, then hover over those with the mouse, I correctly see the tooltips .tippy-popper appear:

<div class="tippy-popper" role="tooltip" id="tippy-15" style="z-index: 9999;
    visibility: visible; position: absolute; top: 0px; left: 0px; will-change: transform;
    transition-duration: 350ms; transform: translate3d(164px, 1249px, 0px);"
    data-html="" tabindex="-1" x-placement="top">
    <div class="tippy-tooltip light-theme operation-theme"
        data-size="regular"
        data-animation="fade"
        data-state="visible"
        data-interactive=""
        data-template-id="#opTooltip-3-opTypeFoo"
        style="transition-duration: 250ms; top: 0px;">
        <div class="tippy-arrow" style="left: 0px;"></div>
        <div class="tippy-content"></div>
    </div>
</div>

...however only the arrows are shown since <div class="tippy-content"></div> is always empty.

Would you know what would trigger the fact that .tippy-content is empty? (while the component is generated and drawn correctly)

To be sure that using nextTick() would solve the problem that the html selector would try to select non-existent elements, I added some console.log() here:

Vue.directive('tippy', {
  inserted: function inserted(el, binding, vnode) {
      Vue.nextTick(function() {
        console.log('inserted:createTippy:', el, binding, vnode); //DEBUG
        console.log('binding.value.html:', binding.value.html); //DEBUG
        console.log('The selected element:', document.querySelector(binding.value.html));
        createTippy(el, binding, vnode);
      });
  },

and yes, I can see that the correct html is selected when the directive is 'inserted'. I'm not sure why some components are rendered in the source directly and not in a .tippy-popper element.

KABBOUCHI commented 6 years ago

I'm not sure what's the problem, if you can reproduce it I'm happy to help

AlexandreBonneau commented 6 years ago

Me neither unfortunately.

Would you know where the .tippy-content is supposed to be generated?

KABBOUCHI commented 6 years ago

https://github.com/atomiks/tippyjs/blob/b755ed52ca93976862974356b09b5811214d4c9f/src/js/utils/createPopperElement.js#L77

AlexandreBonneau commented 6 years ago

Ok so I'm not entirely sure I managed to correctly extract the faulty code from my codebase, but here is a codesandbox where adding operations to the table should also add a tooltip component, but the latter is not displayed in a tooltip.

Feel free to ping me if you want to discuss that in live (I'm on the AutoNumeric gitter if needed).

KABBOUCHI commented 6 years ago

they don't have the same id

image

I shortened the ids (removed the date from id, plz find a better way, duplicate keys may occurred)

but the problem still exists, because they don't have the same loop.

fixed by adding Vue.nextTick, please update to v2.0.9

image

image

i suggest to rewrite and merge paymentSchedule, operations and operationIcons

AlexandreBonneau commented 6 years ago

Thanks for the utterly speedy response :) I'll see if that fix the original problem and report back.

Thank you again!

AlexandreBonneau commented 6 years ago

Well, bummer, I'm back to square one since in my real code I only generated the ids with the term count and the operation type, not with the date. :cry:

With 2.0.9 I see the same problem I described 6 days ago, ie. only the very first tooltip is correctly generated, while the other components are just displayed in the html page (and when hovering over the elements that should have shown the tooltips, only the arrow is shown and in the appearing #tippy-99 element, the .tippy-content element is empty).

About the v2.0.9, to be more consistent with Vue.nextTick() usage, perhaps you can also add it to the componentUpdated() function too like so?:

componentUpdated(el, binding, vnode) {
    const opts    = binding.value || {};
    const oldOpts = binding.oldValue || {};

    if (el._tippy && (JSON.stringify(opts) !== JSON.stringify(oldOpts))) {
        Vue.nextTick(() => {
            createTippy(el, binding, vnode);
        });
    }

    if (el._tippy && opts.show) {
        el._tippy.show();
    } else {
        if (el._tippy && !opts.show && opts.trigger === 'manual') {
            el._tippy.hide();
        }
    }
}
KABBOUCHI commented 6 years ago

plz update and retry

AlexandreBonneau commented 6 years ago

Well, I already am editing the vue-tippy source (and the tippy and popper one too) to try to find why it's not working as intended. No luck so far.

With v2.0.10, I still get the same problem.

I'm having trouble using my debugger on the v-tippy directive/popper library to see what exactly is going on, because when I just console.log the ids and the html elements used for the tippy content, everything is fine.

I suspect the console is lying (as it almost always does when it comes to displaying objects and arrays). On your end, do you manage to debug (step by step) vue-tippy? (I'm using PhpStorm)

KABBOUCHI commented 6 years ago

Please make sure every loop has :key

AlexandreBonneau commented 6 years ago

I had a :key already in the components that are used as tooltips:

<operation-tooltip
        v-for="ope in operations"
        :key="`${ope.term}-${ope.type}`"
        :id="`opTooltip-${ope.term}-${ope.type}`"
/>

..however I did not on the elements on which the v-tippy directive was on.

Since I have a special configuration where the v-for is on a template (and you can't :key a template otherwise you get the <template> cannot be keyed. Place the key on real elements instead. error message, I now use (per this thread):

<template v-for="icon in operationIcons">
    <template v-if="inOperations(term.term, icon.type)">
        <svg :class="{
                 icon  : true,
                 past  : isPastOp(term.term),
                 future: isFutureOp(term.term),
             }"
             v-tippy="{
                 html       : `#opTooltip-${term.term}-${icon.type}`,
                 delay      : [150, 200],
                 theme      : 'light operation',
                 interactive: true,
                 reactive   : true,
                 sticky     : true,
             }"
             :key="`${term.term}-${icon.type}`"
        >
            <use :xlink:href="svgIcon(icon.iconName)"></use>
        </svg>
    </template>
    <template v-else>
        <svg class="icon"
             :key="`${term.term}-${icon.type}`"
        ><use :xlink:href="svgIcon(icon.iconName)"></use></svg>
    </template>
</template>

But even with those keys, only the first tooltip is correctly added. I'll keep investigating..

KABBOUCHI commented 6 years ago

remove the template tag and put v-if and v-else on the svg element

AlexandreBonneau commented 6 years ago

Done. It's a bit clearer that way indeed, unfortunately that does not fix the problem.

AlexandreBonneau commented 6 years ago

So, since I somehow can't hit any breakpoints in libraries in the node_modules directory, I had to resort to the annoyingly long console.log() way of debugging.

I've littered the dist/vue-tippy.js file with lots for console logging, and so far what I can tell is that at the end of the createPopperElement() function, the .tippy-content element is correctly set with the html node from the tooltip component (since I can see that popper.firstChild.childNodes[1].childElementCount is equal to 1).

In createTooltips(), just after initializing the Tippy element in the tippy variable, tippy.popper.firstChild.childNodes[1].childElementCount is also equal to 1. So far so good.

However if I console.log(tippy) as that same place, and since Chrome/Firefox only display a reference to the objects in the console (and not their values when they are logged), I can see that once my page is loaded, the tippy.popper.firstChild.childNodes[1].childElementCount is equal to 0 when expanding that object manually. (Note: the very first tooltip generated is the only one having a populated .tippy-content element)

The question is; what happens to those .tippy-content elements between the point where there are (correctly) generated by popper, and when Vue has finished generating the page (and somehow their content is emptied, apparently)?

I'm not sure how or what more I could inspect here. Any ideas?

KABBOUCHI commented 6 years ago

I'll close this due to inactivity.

AlexandreBonneau commented 6 years ago

For info, I still haven't fixed the problem nor found the cause of why vue-tippy refuses to correctly generate the tooltip.

Would you have more info about the last questions I asked by any chance?

KABBOUCHI commented 6 years ago

Can you reproduce the problem? so i can check the problem? or can you share ur code privately?