Jeff McAleer
Jeff McAleer

Reputation: 364

How to load an External SVG into Vue as an Object

I need to load an SVG file into a Vue template. It must be loaded in such a way that I can access the internal classes with js and css, so presumably I'm looking for an <object> tag and not an <img> tag.

The SVG is located on an external server, not a part of my project. Vue-Svg-Loader works just fine if I have the svg as part of my project, but doesn't seem to work when the SVG isn't available until runtime.

I've tried the following

<template>
    <div ref="floorplan" id="floorplan-frame">
        <object
            type="image/svg+xml"
            id="floorplan"
            :data="svgPath"
        ></object>
    </div>
</template>

<script>
export default {
    data() {
        return {
            svgPath: 'http://test-mc4/floorplan.svg',
        };
    },
};
</script>

Unfortunately it doesn't work. If I replace the <object> tag with <img :src="svgPath" /> it does show the SVG, but as a static image where the internal css classes are not available. It does show me that my path is correct and the file is actually available, but it doesn't explain why the object tag is just empty when I use it.

I've searched extensively and I can figure out how to load it as an Object if it's internal, or how to load it External as long as it's an image. I just can't seem to figure out how to do both.

Upvotes: 2

Views: 2271

Answers (1)

kdau
kdau

Reputation: 2099

In order to access the elements within an <object>, you'd need to wait until it was loaded (load event) and then access the child SVG document by its contentDocument property.

Unfortunately, this won't work in your case because the SVG files are coming from a different origin. The same-origin policy will block access to the contentDocument. Here is an example, which also fails (logs null) because a data: URL is a different origin:

const svgPath = 'data:image/svg+xml;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDIwIDIwIj4NCiAgICA8Y2lyY2xlIGN4PSIxMCIgY3k9IjEwIiByPSIxMCIgZmlsbD0icmVkIi8+DQo8L3N2Zz4=';

const app = new Vue({
  el: '#app',
  data: {
    svgPath,
  },
  methods: {
    svgLoaded() {
      setTimeout(() => {
        console.log(this.$refs.object.contentDocument);
      }, 1000);
    }
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <object
    ref="object"
    :data="svgPath"
    width="100"
    height="100"
    v-on:load="svgLoaded"
  ></object>
</div>

The only way to load an SVG from a different origin and then access its internal structure with your JS and CSS would be to fetch it and then load it into your component as v-html. Note that this opens significant XSS vulnerabilities; you should be wary of this option and only use it with external servers you trust. In any case, here's a working example:

const svgPath = 'data:image/svg+xml;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDIwIDIwIj4NCiAgICA8Y2lyY2xlIGN4PSIxMCIgY3k9IjEwIiByPSIxMCIgZmlsbD0icmVkIi8+DQo8L3N2Zz4=';

const app = new Vue({
  el: '#app',
  data: {
    svgData: '',
  },
  async mounted() {
    const svgResponse = await fetch(svgPath);
    this.svgData = await svgResponse.text();
    await Vue.nextTick();

    // SVG is present in the DOM at this point
    const svg = this.$refs.drawing.firstElementChild;
    console.log(svg.outerHTML);

    // DOM manipulations can be performed
    const circle = svg.querySelector('circle');
    circle.setAttribute('fill', 'blue');
    circle.setAttribute('r', '6');
  }
});
.drawing {
  width: 100px;
  height: 100px;
}

/* SVG can be styled as part of the main document */
.drawing circle {
  stroke: cyan;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <div
    class="drawing"
    ref="drawing"
    v-html="svgData"
  ></div>
</div>

Upvotes: 4

Related Questions