Hovercards Using Stimulus JS

Matt Swanson has done a great job describing the ins-and-outs of using hovercards via Stimulus JS. This blog post adds to what is there contained with another feature: (i) the ability to abort / cancel a hover card, if your mouse moves away. This entails hiding the hover card, and/or aborting a fetch request, if made.

In my experience, without the ability to cancel a hover card - the experience is very, very annoying to the user.

The key is to use an AbortController.

The code will abundantly demonstrate the problem: I’ve provided comments so you can hopefully follow along:

import { Controller } from "stimulus";

export default class extends Controller {
  static targets = ["card"];
  static values = { url: String };

  connect(){    
    this.abortController = new AbortController(); // set upt the abort conttoller and signal so we can cancel the fetch method, if required
    this.signal = this.abortController.signal;    
  }

  show() {
  	// if the card already exists
  	// then we can simply hide it with a bootstrap
  	// class, instead of removing it from the DOM.
    if (this.hasCardTarget) {
      this.cardTarget.classList.remove("d-none");
    } else {   

    	// if we've already aborted the fetch
    	// then we need to create a new abort controller
    	// to handle the situation if the user decides to 
    	// hover over our target element again, and then decides against it, the abort process can be properly handled the SECOND time around.
        if (this.signal.aborted) {
          this.abortController = new AbortController();
          this.signal = this.abortController.signal;    
        }
      
      	// this is the critical line: if we decide to abort the fetch, then the fetch method needs to be aware of it somehow: we pass in the signal into the fetch method.

        fetch(this.urlValue, {signal: this.signal})
          .then((r) => r.text())
          .then((html) => {
            let fragment = document
              .createRange()
              .createContextualFragment(html);

            this.element.appendChild(fragment);})
          .catch( function(err){            
              if (err.name == 'AbortError') { // handle abort() - this is critical. We need to catch the error thrown by the abort. In our case, we don't want to do anything if we abort.
              } else {
                throw err;
              }
          });
    }
  }

  hide() {
  	// if the user decides to move their mouse away
  	// then we immediately want to hide the hover card
  	// and this is the perfect time to mark something as having been aborted, if it was not already done so.

    if (this.signal.aborted) {
    }
    else
    {
      this.abortController.abort()     
    }    

    if (this.hasCardTarget) {      
      this.cardTarget.classList.add("d-none")      
      // these are bootstrap classes which hide
      // the card when required.
    }
  }

  disconnect() {
    if (this.hasCardTarget) {
      this.cardTarget.remove();
    }
  }
}

Perhaps I will provide a gif to demonstrate, but the above code served me very well.

I hope it helps you.

Written on October 1, 2020