How to write a singleton service for a Vue3 component javascript

Viewed 571

I have a tooltip control I've written that works very nicely in Vue 3, but I need a mechanism to fire off to all other instances to tell them to close. There are delays on close, so I'm occasionally getting two tooltips to show up at the same time.

This method, which was a crutch I've used in the past, is not allowed by the compiler / build tools. I can full well understand why, but I don't know the right way:

tooltipManager: function() {
  if (!window.TooltipManager) {
    function tooltipManager() { 
     
     let _data = {
        tooltipIndex: 0,
        callbacks: {}
      };

      return {
  
        register: function (callback) {
          let id = "tooltip_" + _data.tooltipIndex;
          _data.tooltipIndex++;
          _data.callbacks[id] = callback;
          return id;
        },

        closeOpenPopups: function (id) {
          Object.keys(_data.callbacks).forEach(key => {
            if (id !== key) {
              _data.callbacks[key]();
            }
          });
        },

        destroy: function (id) {
          delete _data.callbacks[id];
        }
      
      };

    }

    window.TooltipManager = tooltipManager();
  }
  return window.TooltipManager()
},

The first thing I tried but didn't work was a service which I imported:

export default class TooltipManager {

  constructor() {
    if(! TooltipManager.instance){
      this._data = {
        tooltipIndex: 0,
        callbacks: {}
      };
    } 
  }

  register (callback) {
    let id = "tooltip_" + this._data.tooltipIndex;
    this._data.tooltipIndex++;
    this._data.callbacks[id] = callback;
    return id;
  }

  closeOpenPopups(id) {
    Object.keys(this._data.callbacks).forEach(key => {
      if (id !== key) {
        this._data.callbacks[key]();
      }
    });
  }

  destroy(id) {
    delete this._data.callbacks[id];
  }

}
1 Answers

Ok, I was close with the first service. It should be written this way, and I'm going to leave my console.logs in that confirmed that it is indeed a singleton even though it is running on different tooltips.

class TooltipManager {

  constructor() {
    if(! TooltipManager.instance){
      this._data = {
        tooltipIndex: 0,
        callbacks: {}
      };
      console.log("got new instance");
    } else {
      console.log("got old instance");
    }
  }

  register (callback) {
    let id = "tooltip_" + this._data.tooltipIndex;
    this._data.tooltipIndex++;
    this._data.callbacks[id] = callback;
    console.log("registered key: " + id);
    return id;
  }

  closeOpenPopups(id) {
    Object.keys(this._data.callbacks).forEach(key => {
      if (id !== key) {
        console.log("closed: " + key);
        this._data.callbacks[key]();
      }
    });
  }

  destroy(id) {
    delete this._data.callbacks[id];
  }

}

export default new TooltipManager();

I got the following from console.logs:

got new instance
TooltipManager.js:19 registered key: tooltip_0
TooltipManager.js:19 registered key: tooltip_1
TooltipManager.js:19 registered key: tooltip_2
TooltipManager.js:19 registered key: tooltip_3
TooltipManager.js:26 closed: tooltip_0
TooltipManager.js:26 closed: tooltip_1
TooltipManager.js:26 closed: tooltip_2
TooltipManager.js:26 closed: tooltip_3

And indeed it solved the problem of ghost tooltips when one pops up before the other closes with a delay to prevent bounce.

In the tooltip tool I wrote, which I will later post here as an example of how easy Vue3 Teleport makes something like this to write. I want to test it a little longer before I show it off.

I just need to:

mounted() {
   this.data.tooltipId = TooltipManager.register(this.forceHide);

And, which also shows some state data I use to keep track of this:

methods: {
   forceHide: function() {
     if (this.data.isDisplayed) {
       this.data.style = '{top: -1000px, left: -1000px}';
     }
     this.data.hideRequested = false;
     this.data.showRequested = false;
     this.data.isDisplayed = false;
  },

Now the next thing maybe using Vuex for this, but I may leave this in as an alternative method so it's not dependent on it.

Related