[Vuejs]-Insert scopedSlot into d3 svg using foreignObject

0👍

Just in case anyone wants to implement something like this, I manage to resolve my issue.

Background

SVG has a concept of foreignObject which allows me to inject HTML inside an SVG. The next step is, somehow, to render the Vue component to HTML.

I’m using vue2, Vuetify, and d3js v6

Rendering the component

this.$scopedSlots["tooltip"]({
    event,
  }),

returns a VNode[], so using Vue.extend I create a new component, instantiate it and mount it to a div inside the foreignObject like this:

// Call the scoped slot, which returns a vnode[]
const vnodes = this.$scopedSlots["tooltip"]({
    event,
  });
const id = "RANDOM_GENERATED_ID";
// Append a foreignObject to an g
const fo = g?.append("foreignObject");
// Append a div to the foreignObject, which will be the vue component mount point
fo?.append("xhtml:div").attr("id", id);
// Create a new component which only render our vnodes inside a div
const component = Vue.extend({
    render: (h) => {
      return h(
        "div",
        {staticClass: "foreign-object-container"}
        vnodes,
      );
    },
  });
// Create a instance of this new component
const c = new component();
// I'm using vuetify, so I inject it here
c.$vuetify = this.$vuetify;
// Mount the component. This call will render the HTML
c.$mount("#" + id);
// Get the component rect 
const bbox = c?.$el.getBoundingClientRect();

// Resize the ForeignObject to match our injected component size
return fo
    ?.attr("width", bbox?.width ?? 0)
    .attr("height", bbox?.height ?? 0);

This successfully renders our component inside an SVG using d3js. At least it appears inside the DOM.

Problems

At this point, I faced 2 new problems.

Invalid component size

After rendering the Vue component inside the foreignObject it reported width equals 0. So, based on this I used the next styles:

.foreign-object-container {
  display: inline-flex;
  overflow: hidden;
}

And voilá, habemus a visible Vue component.

Scroll, ForeignObject, and the old Webkit Bug

My use case is this: The chart is responsive, so it re-renders after every container resizes (with some debounce), but to prevent deformations I set a minimum width to every element. With some screen sizes, this provokes some overflow, which inserts a scrollbar (browser behavior).

This is exactly what I want. But I’m using some Vuetify components on my scopedSlot which have a position: relative style.

Enters, an old bug on WebKit (Google Chrome, Safari, and Edge Chromium). This is better explained here and here

The solution in my case is simple. As I stated before, my foreignObject was resized to match the rendered component. So, to prevent my components to be wrongly drawn, I change my styles a little bit.

.foreign-object-container {
  display: inline-flex;
  overflow: hidden;
  position: sticky;
  position: -webkit-sticky;
}

Now, my teammates can use a generic chart with scroll support and customize some pieces of it using any Vue component (at least for now )

Leave a comment