Vue.js component inside an SVG element is not working - vue.js

I have a component called legend that should embed SVG elements inside a template that places the actual SVG tags:
<svg xmlns="http://www.w3.org/2000/svg" class="pie-chart" :width="outerwidth" :height="outerheight">
<legend ref="legend" :series="series" :position="positionLegend" :options="{}" ></legend>
</svg>
But the legend component does not get rendered the output is:
<div id="graphbox">
<svg xmlns="http://www.w3.org/2000/svg" width="1691" height="14" class="pie-chart">
<legend series="[object Object],[object Object],[object Object]" position="[object Object]" options="[object Object]"></legend>
</svg>
</div>
Does vue not parse within SVG tags?

Components inside <svg> should work fine but you'll need to rename legend to something else. legend is a standard HTML element name so Vue will get confused.
Also make sure that you've registered the component correctly, either locally or globally. Usually Vue will warn you if you've forgotten to register a component but in this case it won't because legend is a standard HTML element.
Here's a complete example, similar to the one in the question:
const myLegend = {
template: `<circle r="10" cx="20" cy="20" :fill="color" />`,
props: ['color']
}
new Vue({
el: '#graphbox',
components: {
myLegend
},
data () {
return {
outerheight: 50,
outerwidth: 50,
color: '#ff0000'
}
}
})
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="graphbox">
<svg :width="outerwidth" :height="outerheight">
<my-legend :color="color"></my-legend>
</svg>
</div>

Related

Tailwind color not working on dynamic classes Vue + Vite

First time Im using tailwind and I can't figure out why colors are not working. This is a fresh install of Laravel Jetstream which comes with Tailwind, Vue3, Vite & Inertia.
seems like the relevant styling is not imported from tailwind if classes are added dynamically.
Here's some basic component
<template>
<div :class="style" class="border-l-4 p-4" role="alert">
<p><slot name="headline"></slot></p>
<p class="pt-3" v-if="slots.error"><span class="font-bold">Message:</span><slot name="error"></slot></p>
<div class="my-3" v-if="slots.info"><slot name="info"></slot></div>
</div>
</template>
<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
</script>
<script>
export default {
name: 'Alert',
props: {
color: {type: String, default: 'red'}
},
computed: {
style() {
return `bg-${this.color}-100 border-${this.color}-500 text-${this.color}-700`
}
}
}
</script>
and using something like this does not have any color related styling although the class is there
<Alert color="orange" class="my-5">
<template #headline>Test</template>
</Alert>
but if the dynamic classes are also specified somewhere in the same page then everything works.
i.e.
<div class="bg-orange-100 border-orange-500 text-orange-700"></div>
<Alert color="orange" class="my-5">
<template #headline>Test</template>
</Alert>
This was relatively an easy fixed, it was mentioned in here to avoid constructing class name dynamically
https://tailwindcss.com/docs/content-configuration#dynamic-class-names
so, in the computed style I just specify full class name conditionally with all the possible values
changed from this.
style() {
return `bg-${this.color}-100 border-${this.color}-500 text-${this.color}-700`
}
to this
style() {
const styles = {
default : 'bg-cyan-100 border-cyan-500 text-cyan-700',
red : 'bg-red-100 border-red-500 text-red-700',
orange: 'bg-orange-100 border-orange-500 text-orange-700',
green: 'bg-green-100 border-green-500 text-green-700',
blue: 'bg-blue-100 border-blue-500 text-blue-700',
}
return styles[this.color] ?? styles.default
}
now everything works perfectly
The method I always use is to simply put in all the possible classes inside the file when dealing with some basic dynamic classes. I noticed that even if the classes are specified in commented lines, tailwind still import the styling of those classes when found inside any file
here's an example
<template>
<div :class="`bg-${color}-100 border-${color}-500 text-${color}-700`" class="border-l-4 p-4" role="alert">
test
</div>
</template>
<script>
/* all supported classes for color props
bg-red-100 border-red-500 text-red-700
bg-orange-100 border-orange-500 text-orange-700
bg-green-100 border-green-500 text-green-700
bg-blue-100 border-blue-500 text-blue-700
*/
export default {
name: 'Alert',
props: {
color: {type: String, default: 'red'}
}
}
</script>
So now all these would work fine
<Alert color="red"></Alert>
<Alert color="orange"></Alert>
<Alert color="green"></Alert>
<Alert color="blue"></Alert>
but this one wont have any styling as the generated classes for purple are not pre specified in any files
<Alert color="purple"></Alert>

How to pass the src attribute of the img tag to the child component through the parent component in vue

I have a parent component and a child component, and I want to pass a src attribute to the child component via defineProps to make it display the image.
Below is my parent component code:
<script setup>
import item from "./item.vue";
const items=[
{
id:0,
img:'../assets/2.png',
name:'秘塔写作猫',
des:'码文章必备工具'
},
{
id:1,
img:'../assets/2.png',
name:'AdblockPlus广告拦截',
des:'最流行的广告拦截拓展程序'
},
{
id:2,
img:'../assets/3.png',
name:'Tampermonkey油猴脚本',
des:'码文章必备工具'
},
{
id:3,
img:'../assets/4.png',
name:'蔚蓝主页',
des:'个性化简洁风格浏览器主页'
},
{
id:4,
img:'../assets/5.png',
name:'ABCD PDF',
des:'完全免费在线PDF压缩转换工具'
},
{
id:5,
img:'../assets/6.png',
name:'ASO谷歌商店工具',
des:'洞悉竞品下载、评论等核心数据'
},
]
</script>
<template>
<div id="ain">
<div id="head">
<h4>站长推荐</h4>
<a>查看全部</a>
</div>
<div id="body">
<item v-for="item in items" :key="item.id" :img="item.img" :name="item.name" :des="item.des">
</item>
</div>
</div>
</template>
Below is my subcomponent code:
<script setup>
defineProps(['img','name','des'])
</script>
<template>
<div id="item">
<div>
<img src="img">
<p>{{name}}</p>
<p id="detail">{{des}}</p>
</div>
</div>
</template>
This error is displayed in the console:GET http://127.0.0.1:5173/assets/2.png 404 (Not Found)
I think it's a problem with passing parameters, but I don't know how to modify it, thank you all.
There are two solutions
1.You can use an absolute path
2.Put your image in the public path under the root directory
It looks like you are using Vite and there are a few options to reference images:
Use import statements for images to obtain the image URL
import imgUrl from './assets/img.png'
document.getElementById('hero-img').src = imgUrl
Place images inside public folder and reference using /. For example if all images are contained inside public/img then the URL would be /img.
Try new URL(url, import.meta.url) although this does not work with SSR
Number 2) is probably the easiest if these images won't be updated.
Reference:
https://vitejs.dev/guide/assets.html

v-html receiving a prop does not render (Vue, SVG)

I'm passing a string containing a sequence of elements as a prop, which I try to render using :v-html="prop":
<template>
<svg
version="1.1"
baseProfile="full"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 26 26"
:class="sizeClass"
:height="size"
:width="size"
>
<g :v-html="sequence" />
</svg>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
name: "Avatar",
props: {
sizeClass: String,
size: String,
sequence: String,
},
});
</script>
However inspecting the result on dev tools, the html is not rendered, but appears only in the v-html attribute :
What am I missing ?
directives are bound by default they don't need binding sign : or v-bind::
<g v-html="sequence" />

Vue2-leaflet map not showing properly in BoostrapVue modal

Here's my problem - a Vue2 leaflet map does not render correctly in BootstrapVue modal.
Here's what it looks like visually (it should show just the ocean)
<template>
<div>
<b-modal size="lg" :visible="visible" #hidden="$emit('clear')" title="Event details">
<div class="foobar1">
<l-map :center="center" :zoom="13" ref="mymap">
<l-tile-layer :url="url" :attribution="attribution"></l-tile-layer>
<l-marker :lat-lng="center"></l-marker>
</l-map>
</div>
<template slot="modal-footer">
<b-btn variant="danger" #click="deleteEventLocal(event.id)">Delete</b-btn>
</template>
</b-modal>
</div>
</template>
<script>
import * as moment from "moment";
import { LMap, LMarker, LTileLayer } from "vue2-leaflet";
import { deleteEvent } from "./api";
import "vue-weather-widget/dist/css/vue-weather-widget.css";
import VueWeatherWidget from "vue-weather-widget";
export default {
data() {
return {
center: L.latLng(event.latitude, event.longitude),
url: "http://{s}.tile.osm.org/{z}/{x}/{y}.png",
attribution:
'© OpenStreetMap contributors'
};
},
props: {
visible: {
type: Boolean
},
event: {
required: true,
type: Object
}
},
methods: {
async deleteEventLocal(id) {
await deleteEvent(id);
this.$emit("refresh");
this.$emit("clear");
}
},
components: {
weather: VueWeatherWidget,
LMap,
LMarker,
LTileLayer
}
};
</script>
As you can see there aren't any CSS rules that could make the map spill outside the modal as it does. Which is weird.
I'm kinda asking this question to answer it myself as I couldn't find a solution before.
There were 3 issues because of which this was happening.
1. First - I forgot to load the leaflet css into main.js - this is why the leaflet map was somehow outside the modal.
//src/main.js
import '#babel/polyfill';
import Vue from 'vue';
import './plugins/bootstrap-vue';
import App from './App.vue';
import router from './router';
import store from './store';
//above imports not important to this answer
import 'leaflet/dist/leaflet.css'; //<--------------add this line
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app');
2. Now the map may disappear. Set a width and height on the l-map component's container. I used a class but you can use style="" etc.
<div class="foobar1"> <!-- <--- Add a class on l-map's container -->
<l-map :center="center" :zoom="13">
<l-tile-layer :url="url" :attribution="attribution"></l-tile-layer>
<l-marker :lat-lng="center"></l-marker>
</l-map>
</div>
<style lang="scss">
.foobar1 { /* <--- class we added above */
width: 100%;
height: 400px;
}
</style>
3. Now your map will render within the modal but if you move the map's view, you'll see that leaflet does not download the map's squares in time.
You will see something like this:
To fix this:
create an event handler on b-modal for the #shown event.
<b-modal
#shown="modalShown"
#hidden="$emit('clear')"
size="lg"
:visible="visible"
title="Event details"
>
I called mine modalShown.
Then, add a ref attribute to your l-map. I called mine mymap.
<l-map :center="center" :zoom="13" ref="mymap"> <!-- ref attribute added to l-map -->
<l-tile-layer :url="url" :attribution="attribution"></l-tile-layer>
<l-marker :lat-lng="center"></l-marker>
</l-map>
Then, create a modalShown method in the Vue methods for your view/component and call invalidateSize() inside.
export default {
data() {
//some data here
}
methods: {
modalShown() {
setTimeout(() => {
//mapObject is a property that is part of leaflet
this.$refs.mymap.mapObject.invalidateSize();
}, 100);
}
}
}
Now everything should be fine:
map should not spill outside the modal
map should be visible (duh)
map squares should be downloaded when within map body
Here's my full code, it contains some stuff specific to my app but overall it contains all of the code snippets above.
Addtional to Artur Tagisow answer
You can also use this approach to your parent component if your map is in child component.
export default {
data() {
//some data here
}
methods: {
modalShown() {
setTimeout(() => {
window.dispatchEvent(new Event("resize"));
}, 100);
}
}
}
For vue.js and nuxt.js developers , probably it's because of using v-show or v-if
!in your case display none happening by bootstrap modal
but dont worry the only thing u have to do is using client-only (its like ssr but for new version of js frameworks like nuxt or vue):
<client-only>
<div id="bootstrapModal">
<div id="map-wrap" style="height: 100vh">
<l-map :zoom=13 :center="[55.9464418,8.1277591]">
<l-tile-layer url="http://{s}.tile.osm.org/{z}/{x}/{y}.png"></l-tile-layer>
<l-marker :lat-lng="[55.9464418,8.1277591]"></l-marker>
</l-map>
</div>
</div>
</client-only>
ps: if still not loaded in iphone browsers it's probably because of geolocation

Custom Vue directive to omit tag but render tag's contents?

I'd like to create a custom Vue directive to omit the tag but render the tag's contents when the directive is true.
So for example, if the data for my vue instance is defined as
data:{
omitIt: true
}
And if the markup looks like this:
<div v-omit="omitIt" class="someClass">
Hello world!
</div>
When omitIt is set to false as it is above, I'd like the following rendered into the dom:
<div class="someClass">
Hello world!
</div>
But when omitIt is true I'd like only the following rendered into the dom:
Hello world!
I initially tried to solve this by doing the following (admittedly not a custom vue directive):
<template v-if="!omitIt">
<div class="someClass">
</template>
Hello world!
<template v-if="!omitIt">
</div>
</template>
The above isn't pretty but I thought perhaps it would work. But alas what gets rendered into the dom when omitIt is false is:
<div class="someClass"></div>
Hello world!
Any suggestions on how to achieve the results I'm looking for?
I thought #Nit's answer was a great and simple one and upvoted it, but it does have one flaw: a slot may not be a root element so the component will fail when the wrapper needs to be omitted. This is because slots can contain more than one element and if the slot does contain more than one, there could end up being more than one root element, which is not allowed.
I have a partial solution that renders just the first element in the slot if the component should not wrap.
Vue.component("wrapper", {
props:{
nowrap: {type: Boolean, default: false}
},
render(h){
// This will *only* render the *first* element contained in
// the default slot if `nowrap` is set. This is because a component
// *must* have a single root element
if (this.nowrap) return this.$slots.default[0]
// Otherwise, wrap the contents in a DIV and render the contents
return h('div', this.$slots.default)
}
})
Here is an example of it working.
console.clear()
Vue.component("wrapper", {
props:{
nowrap: {type: Boolean, default: false}
},
render(h){
// Log a warning if content is being omitted
const omissionMessage = "Wrapper component contains more than one root node with nowrap specified. Only the first node will be rendered."
if (this.$slots.default.length > 1 && this.nowrap)
console.warn(omissionMessage)
// This will *only* render the *first* element contained in
// the default slot if `nowrap` is set. This is because a component
// *must* have a single root element
if (this.nowrap) return this.$slots.default[0]
// Otherwise, wrap the contents in a DIV and render the contents
return h('div', this.$slots.default)
}
})
new Vue({
el: "#app"
})
.someClass{
color: blue
}
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<wrapper class="someClass">Hello World</wrapper>
<wrapper nowrap>No wrap, single root</wrapper> <br>
<wrapper nowrap>
No wrap, two roots. Paragraph is ommitted.
<p>Some other content</p>
</wrapper>
</div>
A couple notes: The component will always wrap unless you add nowrap as an attribute. Also, notice the class is added to the wrapped container without specifying it as a prop. This is because Vue automatically renders attributes that aren't specified as props on the root element of a component, unless you tell it not to.
This answer is wrong, slots cannot be used in this manner. Please see Bert's answer instead.
The easiest solution would be to create a wrapper component with slots for this purpose, passing the omitting argument as a prop.
The content distribution part becomes rather straightforward.
In the wrapper component template:
<slot v-if="omitIt"></slot>
<div v-else>
<slot></slot>
</div>
Wherever you want to use the wrapper:
<wrapper v-bind:omitIt="omitIt">
// Content
</wrapper>