How to load an External SVG into Vue as an Object - vue.js

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.

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>

Related

How to render html partial in vue 3 router

In my apps I have a couple of pages that show some embedded forms...
The forms come from jotform, and are embedded with a js script like this
<section>
<script type="text/javascript" src="https://form.jotform.com/jsform/123124124"</script>
</section>
I cannot find a way to load this inside a component, so I'm trying to use a simple HTML partial. Is there a way to do this?
I try also with a component but it doesn't work
<script>
export default {
data: () => ({
}),
mounted() {
const scripts = [
"https://form.jotform.com/jsform/123124124124"
];
scripts.forEach(script => {
let tag = document.head.querySelector(`[src="${ script }"`);
if (!tag) {
tag = document.createElement("script");
tag.setAttribute("src", script);
tag.setAttribute("type", 'text/javascript');
console.log(document.body);
setTimeout(function() {
document.body.appendChild(tag);
}, 500)
}
});
}
}
</script>
<template>
<main class="main">
<h1 class="visuallyhidden">Funding Request</h1>
<section class="funding">
</section>
</main>
</template>
As suggested here you can try to use the iFrame version: https://www.jotform.com/help/148-getting-the-form-iframe-code/
FYI: including an embed script within a dynamic vue component is not an easy thing (as the JotForm support explaned here), so you might try to go for the iFrame version if it works for you :)

Use existing CMS output as data for Single File Components (Vue.js)

I have access to the templates that CMS uses to generate pages. This CMS is very primitive. The output is pieces of HTML.
I want to use this data in Single File Components. Most importantly, this data should not be rendered by the browser. I figured that wrapping the CMS output into a noscript tag would work. Then I just parse the string from the noscript to get HTML.
This method is pretty dirty and it does not use the power of Vue.js templates. I'm wondering if there is a better way?
CMS template:
<noscript id="cms-output">
<!-- HTML generated by CMS -->
</noscript>
Main JavaScript file:
import Vue from 'vue'
import App from './app.vue'
const cmsOutput = document.getElementById('cms-output')
const parser = new DOMParser()
Vue.prototype.$cms = parser.parseFromString(cmsOutput.innerHTML, 'text/html')
Vue.config.productionTip = false
new Vue({ render: (h) => h(App) }).$mount('#app')
Single File Component:
<template>
<div v-html="content"></div>
</template>
<script>
export default {
computed: {
content: function() {
const contentElement = this.$cms.querySelector('.content')
// contentElement manipulations here (working with descendants, CSS classes, etc)
return contentElement.outerHTML
}
}
}
</script>
You can use DOM-injected HTML (or even JavaScript strings) as a template in your SFC but you'll need to enable Vue's runtime compiler. Add the following to the project's vue.config.js:
module.exports = {
runtimeCompiler: true
}
Wrap the content of your HTML output in an x-template:
<script type="text/x-template" id="cms-output">
...
</script>
In your SFC, don't use <template></template> tags. Instead, use the template option in your component (this is what the runtime compiler is needed for):
<script>
export default {
template: '#cms-output'
}
</script>
Now you can use the template just as if it were defined in the SFC, with directives, mustache syntax, etc.
EDIT (based on feedback)
There's nothing unique or complex about this if I understand correctly. Use a normal component / template. Since the output isn't ready to be used as a template then there is no choice but to parse it. You could load it from AJAX instead of embedding it as in your question but either way works. Your component could look something like this:
export default {
data() {
return {
data1: '',
data2: '',
dataN: ''
}
},
created() {
const contentElement = this.$cms.querySelector('.content');
const arrayOfData = parseTheContent(contentElement);
this.data1 = arrayOfData[1];
this.data2 = arrayOfData[2];
...
this.dataN = arrayOfData[100];
}
}
And you'd use a standard template:
<template>
<div>
Some stuff {{ data1 }}. Some more stuff {{ data2 }}.<br />
{{ dataN }}
</div>
</template>

Failed to use external js file in vue js, tried in mounted to append in head but its not working

I am trying to use an HTML admin template into vue cli. There are some js file in that template. When I trying in vue js they are working after refresh but when I am going one component to another they are not working.
For example: jquery.min.js etc.
mounted () {
let jqueryscript = document.createElement('script')
jqueryscript.setAttribute('src', '/js/jquery-2.1.1.min.js')
document.body.appendChild(jqueryscript) }
This should do the trick. Keep in mind, this will fail if the URL you are providing in the src=%url% does not exist, or is unreachable (at least on CodePen)..
Full Link:
https://codepen.io/oze4/pen/pBzexp?editors=1010
HTML:
<body>
<div id="app">
<h1>Wait for 2 seconds!</h1>
<br />
<h1>You will receive an alert.</h1>
<br />
<h1 style="color: red;">Click "Run" to run again!</h1>
</div>
</body>
JS/Vue:
new Vue({
el: "#app",
mounted() {
setTimeout(() => {
var newScript = document.createElement("script");
newScript.setAttribute('src', 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js');
var message = document.createElement("script");
var alert = document.createTextNode("alert('Added jQuery Script Tag!');");
message.appendChild(alert);
document.body.appendChild(newScript);
document.body.appendChild(message);
}, 2000);
}
});
Success!

Vue fallback to asset images

I have a bunch of entities I want to display on a page in a Vue 3 application. Each entity should have an image URL. If an entity does not have such a URL, I want to display a default image that's stored in /src/assets/images/default.png
So I've created a computed property that returns the image URL, either entity.url or the URL above.
Unfortunately, this doesn't work, instead of getting the default image I'm getting no image, and the Vue dev webserver returns index.html. This is because Vue's handling of static assets doesn't work when the static asset URL is returned in runtime.
The image is shown properly if I hard-code the URL #/assets/images/default.png, but not if I return this as a string (dropping the # makes no difference).
How can I make Vue handle static assets that are referenced dynamically?
I'd use v-if/v-else to show the appropriate image based on whether entity.url exists ..
<img v-if="entity.url" :src="entity.url" />
<img v-else src="#/assets/images/default.png" />
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
imageData:{ imgUrl:'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f9/Wiktionary_small.svg/350px-Wiktionary_small.svg.png',
}
},
methods: {
getUrlData(data) {
if (data && data.imgUrl) {
return data.imgUrl
}
else {
return "#/assets/images/default.png"
}
}
}
})
<div id="app">
<p>{{ message }}</p>
<img :src="getUrlData(imageData)">
</div>
Try this should work in your case! and let me know if you are getting any errors
In Vue 3, you can put an #error event on the image, so if the URL returns a 404, you can execute arbitrary logic:
<script setup>
import { ref } from 'vue';
const isProfileImageBroken = ref(false);
const handleBrokenImage = () => {
console.log('image broke');
isProfileImageBroken.value = true;
};
</script>
<template>
<i v-if="!auth.user.image || isProfileImageBroken" class="icon-user icon-lg"></i>
<img
v-if="auth.user.image && !isProfileImageBroken"
:src="auth.user.image"
class="w-6 h-6 rounded-full object-cover"
#error="handleBrokenImage"
>
</template>

How to compute styles on <body> or <html> using vue.js?

I am using vuejs style bindings to render changes dynamically as the styles are computed.
This works great for everything within the scope of my Vue instance but how can I compute styles for body or html tags?
This used to be possible when you could bind the vue instance to but vue no longer lets you do it.
I want to dynamically update the background color of using my computed variables in vue.
edit: added code snippet to demonstrate
var app = new Vue({
el: '#app',
data: {
color: '#666666'
},
computed: {
backgroundColor: function() {
return {
'background-color': this.color
}
}
},
methods: {
toggleBackground: function() {
if(this.color=='#666666'){
this.color = '#BDBDBD'
} else {
this.color = '#666666'
}
}
}
})
<script src="https://vuejs.org/js/vue.min.js"></script>
<html>
<body>
<div id="app" :style="backgroundColor">
<div>
lots of content...
</div>
<button #click="toggleBackground"> Click to toggle </button>
</div>
</body>
</html>
If you really need to style body itself, you'll need to do it with plain JavaScript in a watcher. A simple example is below.
You should (not something I've tried, but I'm hypothesizing) be able to defeat overscrolling effects by making body and your outer container non-scrolling. Put a scrollable container inside that. When it overscrolls, it will show your outer container, right?
The reasons for not binding to body are here (for React, but applies to Vue).
What’s the problem with ? Everybody updates it! Some people have
non-[Vue] code that attaches modals to it. Google Font Loader will
happily put elements into body for a fraction of second, and
your app will break horribly and inexplicably if it tries to update
something on the top level during that time. Do you really know what
all your third party scripts are doing? What about ads or that social
network SDK?
new Vue({
el: '#app',
data: {
isRed: false
},
watch: {
isRed() {
document.querySelector('body').style.backgroundColor = this.isRed ? 'red' : null;
}
}
});
#app {
background-color: white;
margin: 3rem;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<input type="checkbox" v-model="isRed">
</div>
I think I found better solution than using jQuery/querySelector
You can add tag style right in your Vue template.
And add v-if on this, smth like that:
<style v-if="true">
body {
background: green;
}
</style>
Thus you can use computed/methods in this v-if and DOM always will update when you need.
Hope this will help someone ;)
UPD:
Using tag "style" in templates is not best idea, but you can create v-style component, then everything will be fine:
Use style tags inside vuejs template and update from data model
My snippet:
Vue.component('v-style', {
render: function (createElement) {
return createElement('style', this.$slots.default)
}
});
new Vue({
el: '#app',
data: {
isRed: false,
color: 'yellow',
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input type="checkbox" v-model="isRed">
<v-style v-if="isRed">
body {
background: red; /*one more benefit - you can write "background: {{color}};" (computed)*/
}
</v-style>
</div>