DEV Community

Cover image for Bonus: Solving Problems in Vue with Web Components
Benny Powers 🇮🇱🇨🇦
Benny Powers 🇮🇱🇨🇦

Posted on

Bonus: Solving Problems in Vue with Web Components

This is a bonus post in a series I'm working covering web components.

  1. Part 1: The Standards
  2. Part 2: The Polyfills
  3. Part 3: Vanilla Components

Part 4, on the Polymer library, is on the way. While we're waiting, check out this neat problem a student approached me with that we can solve with web standards:

They were using a library to render a WebGL globe inside a Vue component. They wanted to generate a set of markers and then track which markers were opened and which were closed. The WebGL library provided some APIs for attaching a string of innerHTML to each marker's popup, but didn't expose any APIs to track open, close, or click events.

I had a bit of a devilish thought 😈. If we can't add behaviour to the library popups, but we can add HTML, what if we added HTML that encapsulates its own behaviour?

🎩 Web Components to the Rescue!! 👨‍💻

Defining <popup-tooltip>

What we needed was an HTML element that fires an event every time it's containing popup opens or closes. The WebGL lib used style="visibility: visible" to open and close popups, so we'll create an element that uses MutationObserver to observe it's own parents.

class PopupTooltip extends HTMLElement {
  constructor() {
    super();
    this.observerCallback = this.observerCallback.bind(this);
    this.attachShadow({mode: 'open'});
    this.shadowRoot.appendChild(document.createElement('slot'));
    this.observer = new MutationObserver(this.observerCallback);
  }

  connectedCallback() {
    // HACK: WebGL library toggles style.visibility on it's own
    // generated DOM to hide and show tooltips.
    const libraryContainer = this.parentElement.parentElement.parentElement;
    const config = { attributes: true, subtree: false, children: false };
    this.observer.observe(libraryContainer, config);
  }

  observerCallback([{target}]) {
    const visible = target.style.visibility === 'visible';
    const type = 'popup-' + visible ? 'opened' : 'closed';
    const bubbles = true;
    const composed = true;
    const detail = this;
    this.dispatchEvent(new CustomEvent(type, { bubbles, composed, detail }));
  }
}

customElements.define('popup-tooltip', PopupTooltip);
Enter fullscreen mode Exit fullscreen mode

Connecting to the Vue Wrapper

So now we have a <popup-tooltip> element which will fire a popup-opened or popup-closed event any time it's container's visibility is toggled by the WebGL library. We set up listener's in the private DOM of the wrapping Vue Component:

<!-- WebGL lib instanciates on '#earth' -->
<div id="earth" @popup-opened="onPopupOpened" @popup-closed="onPopupClosed"></div>
Enter fullscreen mode Exit fullscreen mode

Creating Each Popup

Then when we instantiated the WebGL lib and passed our data, we set up the markers to display a <popup-tooltip> element in it's tooltip content.

geoMarkers.forEach(marker => {
  const location = marker.latLng;
  const glMarker = new WebGLLib.popup({/*...*/});
  // NB: popupHTML is **just HTML**, there's no framework or library here.
  const popupHTML = `<popup-tooltip data-location="${location}">${marker.title}</popup-tooltip>`;
  // `bindPopup` is a method on our WebGL library's marker API.
  glMarker.bindPopup(popupHTML, config);
})
Enter fullscreen mode Exit fullscreen mode

Profit!

The last thing we had to do was track which popups were opened and which closed.

onPopupOpened({target: {dataset: {location}}}) {
  const [lat, lng] = location.split(',');
  console.log(`opened: lat: ${lat} lng: ${lng}`);
}
Enter fullscreen mode Exit fullscreen mode

You don't need to give up your frameworks to use web components. You can use them anywhere you can use HTML and JavaScript. That's precisely what made web-components a win here: our GL library didn't take Vue components as input, it took a string of HTML.

See you in a few days for part 4 on the Polymer library.

Would you like a one-on-one mentoring session on any of the topics covered here? Contact me on Codementor

Discussion (0)