Vue Chart.js doesn't get initialized in Vue Tab - vue.js

I've got a component with two vue-tabs, with two instances of vue-chart-js in each of it. Though they get initialized without errors, a chart in the unactive tab returns nothing, when I try to extract an image from it via document.querySelector('#mySecondChart').toDataURL(). Only when I click that tab and make it active, this chart transforms into image. The chart in the default active tab transforms into image without errors. Here's the code:
<template>
<div>
<vue-tabs>
<v-tab>
<my-first-chart-component/>
</v-tab>
<v-tab>
<my-second-chart-component/>
</v-tab>
</vue-tabs>
<button #click="extractCharts">Extract charts</button>
</div>
</template>
<script>
// imports omitted
export default {
name: 'MyParentComponent',
// data and props omitted
methods: {
extractCharts() {
let charts = document.querySelectorAll('chart')
let firstChart = charts[0].toDataURL();
let secondChart = charts[1].toDataURL();
console.log(`${firstChart} \n\n\n ${secondChart}`)
}
}
}
</script>
When I click the button without going to second tab, my method outputs the DOMString of the first chart and nothing of the second chart. Only when I visit the second tab first, and then click the button, my method returns both stringified charts. Is there any way to forcefully get second chart rendered without even activating the tab containing it?

After searching through documentation, I've discovered that the reason of the problem lies within Chart.js itself -- if the parent element containing the chart has display: none or is hidden in some other ways, the canvas rendered in it gets the height and width properties equal to 0. This may be avoided if during chart instance's initialization pass to its options the following parameters:
options: {
// other options
responsive: false,
maintainAspectRatio: true
}
Then the canvas element bound to the chart instance would keep width and height properties passed to it in markup (i.e. <canvas id="myChart" width="1137" height: "447"></canvas>) and it's display property will remain with the value of block even if the parent element is hidden.

This worked for me.
Put the code inside mounted hook.
var canvas = document.getElementById('line-chart');
canvas.removeAttribute('width');
canvas.removeAttribute('height');
canvas.setAttribute('style', 'display: block; width: 100% !important; height: auto !important;');

Related

Insert scopedSlot into d3 svg using foreignObject

I'm creating a chart component using d3js and Vue (v2). In some parts, I want to support custom user content using scoped slots (in this case, custom tooltips)
<my-chart>
<template slot="tooltip" slot-scope="{ data }">
<span>{{ data }}</span>
</template>
</my-chart>
But I'm struggling to render this using d3js on a vuejs component render function. I'm trying to do something like:
g?.append("foreignObject").html(
this.$scopedSlots["tooltip"]({
event,
}),
);
Obviously, the html method isn't appropriated. I can't find anything like this online. All other examples use the component template to insert the foreignObject and Vue component on the SVG. Nothing using d3js
EDIT
As user I refer to developers. This code is for a lib.
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 )

VueJS - toggle background image of list item on hover

I'm relatively new to Vue and I'm wondering what's wrong with my component that my isHover variable (prop?) isn't working to change the background on mouseover.
<template>
<div class="list-wrap" v-if="gridItems">
<div
class="list-itme"
v-for="(item, index) in gridItems"
:key="index"
#click.stop="setCurrentLocation(location)"
>
<a
#mouseover="mouseOver(index)"
#mouseleave="mouseLeave(index)"
:style="{
background: isHover
? `url(${item.location_image.thumbnails.large.url})`
: `url(${item.location_image.thumbnails.largeHover.url})`
}"
>
{{ item.location_name }}
{{ isHover }}
</a>
</div>
</div>
</template>
<script>
export default {
name: "GridItems",
computed: mapState(["filters", "GridItems"]),
methods: {
mouseOver(index) {
this.item[index].isHover = true;
},
mouseLeave(index) {
this.item[index].isHover = false;
}
},
data() {
return {
isHover: false
};
}
};
</script>
background: isHover
? `url(${item.location_image.thumbnails.large.url})`
: `url(${item.location_image.thumbnails.largeHover.url})`
The isHover above references the data property of the component.
Your mouseOver() and mouseLeave() methods are assigning a property also called isHover on this.item[index]. These are two completely different properties. Where are you getting this.item from? I don't see any props or it being declared as a data attribute.
Edit
You could have a isHover property on the gridItem. Therefore instead of passing index as an argument into the mouse event methods you can actually pass item. Then just set item.isHover = true. On the style binding you can just check against item.isHover.
Which means you don't need the "other" isHover data property on the component.
There are a few things to consider in your code, the isHover variable which is being used to change the background of your elements is a data property, but in your mouseOver and mouseLeave you are trying to change the isHover property from an element on an array called item which is not declared in the code you posted. Another thing to notice is that it is not necessary to return anything on your mouseOver and mouseLeave methods.
As I understand, the expected behavior of your code is to change the background color of the item you are hovering with your cursor. A couple of suggestions, you should use class binding instead of adding inline styles to your template elements, also you could pass the item instead of the index on your mouseover and mouseleave handlers. Another thing to mention is that I would only recommend doing this if for some reason you need the isHover property on your item for something else, otherwise you should just use CSS :hover to achieve this. I made a small demo so you can take a look on what you can do to make your code work: codepen
Edit
To change the image when hovering over an item you should be using the isHover property of that particular item instead of the component's data property isHover which you are currently using to try to change the image url. I updated my codepen.

Scrolling to v-expansion-panel opening

I'm trying to build a mobile small application using v-expansion-panels to display a list.
The idea is that when the user adds a new item in such list it will open the new panel and scroll down to such new panel.
I found a goTo() method in the $vuetify variable, unfortunatly the v-expansion-panels transition (the "opening") take some time and the goTo() won't completely scroll down because of the scrollbar height changes.
So from my understanding I need to detect the end of the transition (enter/afterEnter hook).
Per the vuetifyjs documentation, I could hope to have a "transition" property on my component. (Which isn't the case anyway). But such property is only a string so I can't hook into it.
My other idea is to, somehow, find the transition component and hook into it. Unfortunatly I have trouble understanding el/vnode and the way vuejs is building is tree like the vue-devtool show and I can't get the transition component. When debugging (in the enter hook callback of the transition) it is like the component/el/vnode has a parent but isn't the child of anybody.
Is there a way to do what I'm looking for?
Here is a jsfiddler of what I currently do: https://jsfiddle.net/kdgn80sb/
Basically it is the method I'm defining in the Vue:
methods: {
newAlarm: function() {
const newAlarmPanelIndex = this.alarms.length - 1;
this.alarms.push({title: "New line"});
this.activePanelIndex = newAlarmPanelIndex;
// TODO:
this.$vuetify.goTo(this.$refs.alarmPanels[newAlarmPanelIndex]);
}
}
Firstly you should open manually panel and then use goTo with a timeout.
It works for me, just give some time to a panel to open.
<v-expansion-panels v-model="alarmPanels">
<v-expansion-panel>
<div id="example" />
</v-expansion-panel>
</v-expansion-panels>
this.alarmPanels.push(0); // Use index of expansion-panel
setTimeout(() => {
this.$vuetify.goTo(`#${resultId}`);
}, 500);

Vue.js: prevent user clicking on element while it is being transitioned with Vue <transition>

I'm working on a Vue.js project, and when I click on an element, I'm using the Vue transition tag to fade it out. The problem is that as the element is in the process of being faded out, it is still clickable, which in my application can cause issues.
My question is: how can I make an element unclickable during a transition, so that users don't click it multiple times before the transition finishes?
I've already tried applying a css class with point-events: none; to the element right when the transition starts, but it didn't stop clicks during transition.
Example:
<transition name="fade">
<div v-if="shouldShow" #click="doSomeAction">Example text</div>
</transition>
(where doSomeAction sets shouldShow to false).
Vue has event modifiers that might help with that. The specific one which might be helpful to you is #click.once. If you add this to the click event the user will only be able to click it once. Documentation for it is here.
If you are using Vue.js 2.6+ you can do it with ease. In this minor realse Dynamic directive arguments was added, so you can conditionally bind desired event name, or in you case disable it (passing null).
Dynamic argument values are expected to be strings. However, it would
be convenient if we allow null as a special value that explicitly
indicates that the binding should be removed. Any other non-string
values are likely mistakes and will trigger a warning.
Reference.
// using computed property
<transition name="fade">
<div v-if="shouldShow" #[event]="doSomeAction">Example text</div>
</transition>
export default {
data() {
return {
shouldShow: true
}
},
computed: {
event() {
return this.shouldShow ? "click" : null;
}
}
}
// using object
<transition name="fade">
<div v-if="shouldShow" v-on="{ [shouldShow ? 'click' : null]: doSomeAction }">Example text</div>
</transition>
Update
If you also need to ensure that users can immediately click "through" the element that is being faded out to items behind it, you can add a class with pointer-events: none; to the element, and then do this:
this.$nextTick(() => {
this.shouldShow = false;
});
This will make sure the fade doesn't happen until the class has been added. this.$nextTick is a Vue function that waits for the dom to update (which in this case is adding the pointer-events class) before running a callback: https://v2.vuejs.org/v2/api/#Vue-nextTick
Note that pointer-events: none; doesn't work on some very old browsers (IE < 10)

swiper doesn't work on page load

I imported the code from https://github.com/nolimits4web/Swiper/blob/master/demos/23-thumbs-gallery.html
But the first problem I had was that the slider didn't show so I fixed that with adding min-height: 250px; to the div class .swiper-slide(this is the only thing I changed), my new problem is that the slider doesn't work.
When I resize the browser the slider suddenly works, I can't find what is causing the problem.
You can watch the slider at nielsvt.remvoo.com and then section portfolio, the slider will be visible at the bottom of the page
I had a similar issue. I simply fixed it by initializing the swiper once the page loads.
useEffect(()=> {
swiper.init()
}, [])
On the swiper section definition put the observer property true.
const swiper = new Swiper(swiperIdentifier, {
...
observer: true,
});