KABBOUCHI / vue-tippy

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

Singleton composition API not working with conditional content #312

Open mattbryson opened 2 weeks ago

mattbryson commented 2 weeks ago

I'm trying to group a few Buttons using the useSingleton approach.

This works when the Buttons are present when the component mounts, but if they are conditionally added later, they just use the default Tippy settings, not the singleton settings.

See a working demo here

It does work if you use the <tippy> and <tippy-singleton> components, but not if you mix the v-tippy directive, and the useSingleton composition API as per the example https://vue-tippy.netlify.app/flavor/composition-api#example-1-1

I have tried calling useSingleton after they are added, have tried delaying adding them to the instances array etc etc but nothing I do appears to work.

Any ideas on how to get this working?

mattbryson commented 1 week ago

I found what was going on. When using the directive, it uses the mounted lifecycle hook, but that is for the parent component, NOT the element that the directive is on.

So for elements added after mounted they are not added to singleton group.

For now I have a patch to get it working for anyone with this issue, which I can make into a pull request at some point.

Add this file to your project.

import { DirectiveHook, ObjectDirective, Ref, ref, watchEffect } from 'vue';

import { CreateSingletonProps, Instance } from 'tippy.js';
import { directive, useSingleton as orig_useSingleton } from 'vue-tippy';

/**
 * Custom useSingleton function to keep a reference to the instances it manages.
 * Tippy.js does not expose this (that I can see), so we ned to to hold on to it
 * externally, and then re apply it when it changes
 */
function useSingleton(opts?:Partial<CreateSingletonProps>) {

  const instances:Ref<Instance[]> = ref([]);
  const data = orig_useSingleton(instances, opts);

  watchEffect(() => {
    if(data.singleton.value) {
      data.singleton.value.setInstances( instances.value );
    }
  });

  return {
    ...data,
    instances
  };
}

// Clone the exisitng Tippy directive, and swap the mounted for created....
const vTippyPatched:ObjectDirective & { orig_mounted?:DirectiveHook<any, null, any> } = {...directive};
vTippyPatched.orig_mounted = vTippyPatched.mounted; 
delete vTippyPatched.mounted; 

vTippyPatched.created = (el, binding, vnode, prevVNode) => {  

    const opts = typeof binding.value === "string" ? { content: binding.value } : binding.value || {}

    // call the orig mounted logic
    if(vTippyPatched.orig_mounted) {
      vTippyPatched.orig_mounted(el, binding, vnode, prevVNode);
      // if we have a singleton options, append this to its instance list
      if(el._tippy && opts.singleton) {
        opts.singleton.instances.value.push(el._tippy);
      }
  }
}

export { useSingleton, vTippyPatched };

To replace the current v-tippy directive with this one, you can do the following....

import { vTippyPatched } from '@app/directives/tippyPatch';

.....

app.use(VueTippy, {directive: 'tippy-orig'});
// Patch the Tippy directive to support singleton
app.directive('tippy', vTippyPatched);

And then to use it, do the following....


<script>

import { useSingleton } from '@app/directives/tippyPatch';

const singleton = useSingleton({moveTransition: 'transform 0.2s ease-out', delay:[1000,500]});

</script>

<template>
    <button  v-tippy="{content:'One', singleton}" >One</button>
    <button  v-tippy="{content:'Two', singleton}" >Two</button>
    <button  v-tippy="{content:'Three', singleton}" >Three</button>
</template>