Search code examples
vue.jsvuejs3piniafloating-ui

How can you update a floating-vue dropdown popup in Vue3?


I am using Vue3, Pinia and the floating-vue library.

I am trying to dynamically create a popup at a position which depends on other elements that may move in the viewport. For this I place a dummy div popup-anchor to which my Dropdown Popup is attached.

When I click a specific item, to which I want to attach the popup to, the item is saved in the pinia store.

Whenever the item in the store changes I call setPosition() and show() for this Vue Component.

    selectedItemCoords (newValue, oldValue) {
      if (this.$refs.infoPopup) {
        if (newValue) {
          const pixelCoords = this.getPixelPositionOfItem()
          this.$refs.infoPopup.setPosition(pixelCoords)
          this.$refs.infoPopup.show();
        } else {
          this.$refs.infoPopup.close();
        }
      } else {
        console.log("Info Popupvalue not found")
      }
    }

This works well, if there is a delay between updates of the item in the store (i.e. the item is null in between) and I click somewhere else on the screen. However, if I click on one item after another, the popup closes and does not reopen, unless I click something which is not an item and the store value is set to null.

However, I want to popup to show up whenever showPopup=true, i.e. if the selected items changes, I just want to move the popup and update its contents.

Below is the Vue Component, which holds the dummy anchor and the floating-vue dropdown.

<template>
  <div
    id="popup-anchor"
    ref="anchor"
    class="w-1 h-1 "
    :style="{ position: 'absolute'}"
  >
    <Dropdown
      :shown="showPopup"
      :distance="1"
      :triggers="[]"
      :popper-triggers="[]"
    >
      <template #popper>
        <div
          v-if="item"
          :item="item"
          @share="share"
        >
        item.text
        </div>
        <div
          v-else
          class="loading-text"
        >
          Loading...
        </div>
      </template>
    </Dropdown>
  </div>
</template>

<script>
import { ref, computed } from 'vue';
import {Dropdown, hideAllPoppers, recomputeAllPoppers} from 'floating-vue';
import { useStore} from "@/store/store";

export default {
  components: {
    Dropdown,
  },
  props: {
    position: {
      type: Object,
      required: true,
      default: () => ({ top: '0px', left: '0px' }),
    },
  },
  setup(props, { expose }) {
    const anchor = ref(null);
    const showPopup = ref(false); 
    const storage = useStore();
    const item = computed(() => storage.item);

    // Method to toggle the visibility of the InfoPopup
    const show = () => {
      showPopup.value = true
      console.log('show', showPopup.value)
    };
    const close = () => {
      showPopup.value = false
      hideAllPoppers();
      console.log('close', showPopup.value)
    };
    const setPosition = (pixelCoords) => {
      anchor.value.style.top = `${pixelCoords[1]}px`;
      anchor.value.style.left =`${pixelCoords[0]}px`;
      recomputeAllPoppers();
    };

    const share = () => {
      console.log('share')
    };

    expose({ anchor, show, close , setPosition});

    return {
      anchor,
      show,
      close,
      share,
      showPopup,
      item,
    };
  },
};
</script>

Solution

  • After extensive debugging I found that the problem was not the position of the anchoring div, but that the popup needs to have time to close and reopen. So setting showPopup = false and then to showPopup = true in the same tick, does in fact hide the popup, but not reopen it. The solution is to introduce a delay between both actions.

    const show = () => {
      if (showPopup.value = true) {
        // if the popup is already showing we have to close it 
        // and reopen it in the next tick so it can update the position 
        close();
        nextTick(() => {
          showPopup.value = true
        });
      } else {
        showPopup.value = true
      }
    };