I've created a component in vue which wraps a vue-apexchart donut graph. As soon as the page loads and this component is loaded, the vue-apexchart animates and displays a small graph.
Now I would like to instantiate multiple of these components from a dataset side by side. Instead of the components to all load an animate at the same time, I would like a small rendering delay to give it an overall nice effect. Something like this would be nice:
<donut :items="series1"></donut>
<donut :items="series2" delay=1500></donut>
The vue-apexchart doesent support initialization delays, and as far as I can see there isn't any vue-specific official solution to delay the rendering of components.
I've tried to put a setTimeout in any of the component hooks to stall the initialization,
I´ve also tried to inject the all the graph DOM in the template element on a v-html tag in a setTimeout, but apexchart doesent notice this new dom content, and vue doesent notice the html bindings either.
I´ve created this fiddle which loads two instances of a graph:
https://jsfiddle.net/4f2zkq5c/7/
Any creative suggestions?
There are several ways you can do this, and it depends on whether you can actually modify the <animated-component> logic yourself:
1. Use VueJS's built-in <transition-group> to handle list rendering
VueJS comes with a very handy support for transitions that you can use to sequentially show your <animated-component>. You will need to use a custom animation library (like VelocityJS) and simply store the delay in the element's dataset, e.g. v-bind:data-delay="500". VueJS docs has a very good example on how to introduce staggered transitions for <transition-group>, and the example below is largely adapted from it.
You then use the beforeAppear and appear hooks to set the opacity of the individual children of the <transition-group>.
Vue.component('animated-component', {
template: '#animatedComponentTemplate',
props: {
data: {
required: true
}
}
});
new Vue({
el: '#app',
data: {
dataset: {
first: 'Hello world',
second: 'Foo bar',
third: 'Lorem ipsum'
}
},
methods: {
beforeAppear: function(el) {
el.style.opacity = 0;
},
appear: function(el, done) {
var delay = +el.dataset.delay;
setTimeout(function() {
Velocity(
el, {
opacity: 1
}, {
complete: done
}
)
}, delay)
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<transition-group name="fade" v-on:before-appear="beforeAppear" v-on:appear="appear">
<animated-component v-bind:data="dataset.first" v-bind:key="0"> </animated-component>
<animated-component v-bind:data="dataset.second" v-bind:key="1" v-bind:data-delay="500"> </animated-component>
<animated-component v-bind:data="dataset.third" v-bind:key="2" v-bind:data-delay="1000"> </animated-component>
</transition-group>
</div>
<script type="text/x-template" id="animatedComponentTemplate">
<div>
<h1>Animated Component</h1>
{{ data }}
</div>
</script>
2. Let <animated-component> handle its own rendering
In this example, you simply pass the a number to the delay property (remember to use v-bind:delay="<number>" so that you pass a number and not a string). Then, in the <animated-component>'s mounted lifecycle hook, you use a timer to toggle the visibility of the component itself.
The technique on how you want to show the initially hidden component is up to you, but here I simply apply an initial opacity of 0 and then transition it after a setTimeout.
Vue.component('animated-component', {
template: '#animatedComponentTemplate',
props: {
data: {
required: true
},
delay: {
type: Number,
default: 0
}
},
data: function() {
return {
isVisible: false
};
},
computed: {
styleObject: function() {
return {
opacity: this.isVisible ? 1 : 0
};
}
},
mounted: function() {
var that = this;
window.setTimeout(function() {
that.isVisible = true;
}, that.delay);
}
});
new Vue({
el: '#app',
data: {
dataset: {
first: 'Hello world',
second: 'Foo bar',
third: 'Lorem ipsum'
}
}
});
.animated-component {
transition: opacity 0.25s ease-in-out;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<animated-component v-bind:data="dataset.first"> </animated-component>
<animated-component v-bind:data="dataset.second" v-bind:delay="500"> </animated-component>
<animated-component v-bind:data="dataset.third" v-bind:delay="1000"> </animated-component>
</div>
<script type="text/x-template" id="animatedComponentTemplate">
<div class="animated-component" v-bind:style="styleObject">
<h1>Animated Component, delay: {{ delay }}</h1>
{{ data }}
</div>
</script>
If you have the possibility to reformat your data, you can build an array of series objects, add a show: true/false property and iterate it:
//template
<div v-for="serie in series">
<donut :items="serie.data" v-if="serie.show"></donut>
</div>
//script
data: function() {
return {
series: [
{ data: [44, 55, 41, 17, 15], show: false },
{ data: [10, 20, 30], show: false },
]
}
}
Now you can create a setTimeout function which will change the serie.show to true by incrementing the delay based on the serie index.
Then add the function on the mounted hook:
methods: {
delayedShow (serie, idx) {
let delay = 1500 * idx
setTimeout(() => {
serie.show = true
}, delay)
}
},
mounted () {
this.series.forEach((serie, idx) => {
this.delayedShow(serie, idx)
})
}
Live example
Faced the same problem with ApexCharts Pie Charts being redrawn rapidly in sequence due to data being pulled from a pinia store mutating too quickly for the chart to keep up, leading to ugly errors in the console.
I resolved the issue by using a boolean ref in a v-if="showChart" on the component and then using a setTimeout to trigger a delayed drawing of the chart:
import { ref } from "vue";
import useStore from "#/store/myChartStore";
const store = useStore();
const showChart = ref(false);
store.$subscribe((mutation, state) =>{
showChart.value = false;
setTimeout(()=> {
showChart.value = true;
}
, 100);
});
If you're not using a store, you may find another way to watch the initial availability of the chart data and then delay the rendering using that same approach.
Related
I have a DefaultLayout component with a dark mode toggle button which is its own component. One if its children (DefaultLayout's) is About.vue where I want a specific image to change its src depending on a localStorage value that can be set to either 'dark' or 'light'.
I've managed to read the localStorage value but the image does not change unless I refresh the page.
I'm new to Vue so I'm lost on how I can create a method to do this in DefaultLayout and change a variable in its child. I've tried to use an emit with no luck.
Could anyone point me in the right direction?
Yes, the local storage is for keeping data not propagate events.
The simplest way for you is to make a prop in child component and pass the value by this prop. But if you want to implement it as global variable the suggested way is by Pinia.
Below is a simple example
Vue.component('About', {
name: 'About',
template: `<div>
<div v-if="mode==='dark'">Dark</div>
<div v-else>Light</div>
</div>
`,
data() {
return {
mode: 'light',
};
},
mounted() {
this.setMode('white'); // In realtime use `this.getMode()` instead of 'white'
},
methods: {
setMode(val) {
this.mode = val;
},
getMode() {
return JSON.parse(localStorage.getItem('mode'));
}
}
});
var app = new Vue({
el: "#app",
template: `<div>
<input type="checkbox" v-model="toggler" #input="setVal" />
<About ref="about" />
</div>`,
data() {
return {
toggler: false,
};
},
methods: {
setVal() {
const mode = this.toggler === false ? 'dark' : 'light';
// localStorage.setItem('mode', mode); // In realtime uncomment this line
this.$refs.about.setMode(mode);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
</div>
I would like to access refs in a vue.js component, where the ref itself is created dynamically like so:
<style>
</style>
<template>
<div>
<lmap class="map" v-for="m in [1, 2, 3]" :ref="'map' + m"></lmap>
</div>
</template>
<script>
module.exports = {
components: {
lmap: httpVueLoader('components/base/map.vue'),
},
mounted: function(){
console.log('all refs', this.$refs);
// prints an object with 3 keys: map1, map2, map3
console.log('all ref keys', Object.keys(this.$refs));
// would expect ["map1", "map2", "map3"], prints an empty array instead
Vue.nextTick().then(() => {
console.log('map1', this.$refs["map1"]);
// would expect a DOM element, instead prints undefined
})
},
destroyed: function(){
},
methods: {
},
}
</script>
However this seems not to work (see above in the comments), and I can't figure why.
I think the problem is that you are importing the component asynchronously, with httpVueLoader, which then downloads and imports the component only when the component is rendered from the dom, therefore, the component has not yet been imported into the nextTick callback.
I suggest you put a loaded event in the map.vue component, maybe in mounted lifecycle , which will be listened to in the father, example #loaded = "showRefs"
surely when the showRefs(){ } method is invoked, you will have your refs populated ;)
Try using a template string e.g
`map${m}`
You have to wait until components have been rendered / updated. This works:
module.exports = {
data: function () {
return {
};
},
components: {
lmap: httpVueLoader('components/base/map.vue'),
},
mounted: function(){
},
destroyed: function(){
},
updated: function(){
Vue.nextTick().then(() => {
console.log('all ref keys', Object.keys(this.$refs));
console.log('map1', this.$refs['map1'][0].$el);
})
},
methods: {
},
}
I've got a working script that runs fine under Vue 1.x but it doesn't work with Vue 2.x even though I have replaced ready by mounted
<div id="app">
<div id="slider"></div>
<input id="slider-input" v-model="third" v-on:change="updateSlider"/>
<input id="slider-input" v-model="fourth" v-on:change="updateSlider"/>
</div>
Vue 1.x:
var vue = new Vue({
el: '#app',
data: {
first: 3,
second: 2,
third: 40,
fourth: 60,
slider: {
min: 0,
max: 100,
start: [50, 60],
step: 1
},
Slider: document.getElementById('slider')
},
computed: {
total: function total() {
return parseInt(this.first) * parseInt(this.second) * parseInt(this.third);
}
},
methods: {
updateSlider: function updateSlider() {
this.Slider.noUiSlider.set(this.third);
}
},
ready: function ready() {
noUiSlider.create(this.Slider, {
start: this.slider.start,
step: this.slider.step,
range: {
'min': this.slider.min,
'max': this.slider.max
}
});
}
});
Now I have replaced ready with mounted and it's still not working. Problem is, it doesn't even spit out an error message in the console.
My guess is that the <div id="slider"> in the template is being replaced with a different element instance once Vue has compiled the full template and rendered itself (after mounting). What I mean is, in the mounted hook, this.Slider and document.getElementById('slider') no longer refer to the same element (this.Slider is removed from the DOM).
There's probably no reason for Slider to be defined within the data block (it needn't be reactive), just initialize that in the mounted hook instead:
mounted() {
this.Slider = document.getElementById('slider')
noUiSlider.create(this.Slider, ...)
}
Actually, a better way would be to use ref to get an instance of the element instead of querying the DOM:
<div ref="slider"></div>
mounted() {
noUiSlider.create(this.$refs.slider, ...)
}
It was because mounted does not support functions, you had to create the functions in methods and just call it from mounted.
I have a special application where I would like to run a method on every component when it is mounted. So I could have the method as a global mixin or something and then simply do..
mounted(){
this.mySpecialMethod();
}
However, I was wondering if it is possible to simply extend Vues mounted hook so the method is always run on every component when it is mounted. I have not been able to find in info on this.
If you really want to have everything call your mounted hook, you can use a global mixin.
Below, we have the mixin myMixin that will log to console every time something is mounted or destroyed. When you run the example, you can see that every time the plus button is clicked, it runs both the component's mounted hook as well as the mixin's hook.
If you want to extend this so that it can be reusable as a library, you can create a plugin out of it.
const foo = {
template: "<div #click='onClick'>hello</div>",
mounted() {
console.log("Foo's mounted");
},
methods: {
onClick() {
console.log("click");
}
}
}
const myMixin = {
mounted() {
console.log("I've been mounted");
},
destroyed() {
console.log("I've been destroyed");
}
};
Vue.mixin(myMixin);
const app = new Vue({
el: "#app",
data() {
return {
foos: []
};
},
components: {
foo
},
methods: {
add() {
this.foos.push("fizz");
},
remove() {
this.foos.pop();
}
}
});
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app">
<button #click="add()">+</button><button #click="remove()">-</button>
<ul>
<li v-for="f in foos">
<foo></foo>
</ul>
</div>
is there any way how can I set/override slot's content from inside the component? Like
Template:
<div>
<slot></slot>
</div>
JS:
export default {
...
mounted() {
this.$slot.render("<button>OK</button>");
}
...
}
I know I can use v-html on my element to dynamically push content into component template, but I mean not just pure HTML I mean HTML with Vue directives. Like:
JS:
export default {
...
mounted() {
this.$slot.default.render('<button #click="submit">OK</button>');
},
methods: {
submit() {
// Here I want to get :)
}
}
...
}
Basically I want Vue to render (like parse and render, not like innerHTML) certain string in scope of my component and put in on certain spot in my component. The reason is I get the new content from server via AJAX.
I'm sorry but I can't still get my head around after 2 days of googling.
Thanks a lot!
According to this you can instantiate a component programmatically and insert slot content as well.
import Button from 'Button.vue'
import Vue from 'vue'
var ComponentClass = Vue.extend(Button)
var instance = new ComponentClass({
propsData: { type: 'primary' }
})
instance.$slots.default = [ 'Click me!' ]
instance.$mount() // pass nothing
this.$refs.container.appendChild(instance.$el)
I think this is your best chance: building your component on the fly.
Here in the example I use a simple placeholder (with v-cloak).
You may obtain a better result using vue-router to add your newly created component during the execution, with router.addRoutes so your app doesn't have to wait to instantiate the whole vue.
function componentFactory(slot) {
return new Promise((resolve, reject) => {
window.setTimeout(() => { // Asynchronous fetching
resolve({ // Return the actual component
template: `<div>
${slot}
</div>`,
methods: {
submit() {
console.log('hello');
}
}
});
}, 1000);
});
}
componentFactory('<button #click="submit">OK</button>') // Build the component
.then(component => {
new Vue({ // Instantiate vue
el: '#app',
components: {
builtComponent: component
}
});
});
[v-cloak], .placeholder {
display: none;
}
[v-cloak] + .placeholder {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id='app' v-cloak>
<built-component></built-component>
</div>
<div class="placeholder">
This is a placeholder, my vue app is loading...
</div>