Vue 2, Is there a lifecycle hook that actually refers to "finished rendering"? I want to put a loading screen when entering a page and it fades away and show the page content once Vue is finished loading everything, but I have tried most of the lifecycle hook but not working. Here is something I would try to do if updated refers to "finished rendering":
updated(){
this.loaded()
},
methods:{
loaded(){
var vm = this;
this.loading = false;
}
}
If there is not such a lifecycle hook, what would you suggest me to do instead? Thanks
Probably the method you're looking for is mounted(), this is fired when the vue component is ready. You can check the Vue life-cycle documentation here
so probably your Vue instance would look something like this:
var app = new Vue({
el: '#app',
/*
* When the instance is loaded up
*/
mounted: function () {
this.loaded()
},
methods: {
loaded: function () {
var vm = this
this.loading = false
}
}
})
To make sure that all child components have also been mounted, use vm.$nextTick - much cleaner than a setTimeout:
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the
// entire view has been rendered
})
}
Source: https://v2.vuejs.org/v2/api/#Options-Lifecycle-Hooks
As I cannot find that there is a specific lifecycle hook or other Vue-specified method to hook the point where everything is finished rendering, I used the basic js code as well as estimation of time needed for loading all the things with codes as following:
mounted(){
this.loaded()
},
methods:{
loaded(){
var vm = this;
setTimeout(function(){
vm.loading = false
}, 3000);
}
}
Hope there will be someone who is strong at Vue and provide a new answer to hook it more accurately.
Actually, your answer to your question is not what you have asked originally, the Vue instance is rendered in less than a second and it's not possible to use any hooks only to create userfriendly loader.
The case in using setTimeout function is the only solution for this. But the component is ready in DOM a way earlier than 3 seconds.
The best approach for this purpose is to use loader component, where you will have setTimeout in created hook and after this time it will emit event to the parent.
So inside app you need only to import the loader component itself and when the event fires, change internal boolean data property to render another component v-if
Related
am completely new to Vuejs, sorry if this is a stupid question.
This is a nuxt app and am using an IntersectionObserver and depends on the element visibility am trying to change the internal state (data). but its not reactive unless i hit the refresh in vue dev tools.
so this is my approach
async mounted(){
let options = {
root: document.querySelector('#scroll-root'),
rootMargin: '0px',
threshold: 1.0
}
const testimonialStart:any = document.querySelector('#testimonial-start')
let startObserver = new IntersectionObserver(((entries,observer)=>{
entries.forEach((entry)=>{
if(entry.isIntersecting){
this.updateTesti(false)
}
else{
this.updateTesti(true)
}
})
}), options);
startObserver.observe(testimonialStart)
}
in the methods
updateTesti(st:boolean){
this.testiPrev = st
console.log(st,'state')
},
in the data
data(){
return {
testiPrev:false,
}
}
there is no issues in the intersection observer, in the console.log am getting the boolean value as expected.
what should i need to do to get reactivity here?
temporary solution:
I found that if I add testiPrev like below inside watch am getting the reactivity.
watch: {
testiPrev: function(){
}
},
this made me to ask one more question, do we need to explicitly include all the properties inside watch to achieve reactivity, if any better way please let me know.
I have an app that has a Parent component (app.js) and 2 child sibling components ( Modal.vue and Cart.vue).
I am getting a response back from a call within Modal.vue that needs to trigger a function within Cart.vue right after. What are the best options of going about this?
I don't currently use Vuex, which I know would make this easier.
You can use refs like so:
<parent>
<modal #triggerFunc="triggerFunc"/>
<cart ref="$cartRef"/>
</parent>
...
triggerFunc(){
this.$refs.$cartRef.functionIwantToTrigger()
}
You could create an event bus:
https://www.digitalocean.com/community/tutorials/vuejs-global-event-bus
You can use Custom Events for this using the root Vue instance of the current component tree.
In Modal.vue:
Where you are getting a response back from a call, simply emit a custom event like:
this.$root.$emit('onupdate', data);
In Cart.vue:
Simply listen to this root emitted event like
var vm = new Vue({
data: {},
mounted() {
this.$root.$on('onupdate', data => {
// trigger a function within Cart.vue
this.myFunction(data)
});
}
methods: {
myFunction: function(data) {
console.log(data);
}
}
})
I created the child using:
const ComponentClass = Vue.extend(someComponent);
const instance = new ComponentClass({
propsData: { prop: this.value }
})
instance.$mount();
this.$refs.container.appendChild(instance.$el);
When this.value is updated in the parent, its value doesn't change in the child. I've tried to watch it but it didn't work.
Update:
There's an easier way to achieve this:
create a <div>
append it to your $refs.container
create a new Vue instance and .$mount() it in the div
set the div instance's data to whatever you want to bind dynamically, getting values from the parent
provide the props to the mounted component from the div's data, through render function
methods: {
addComponent() {
const div = document.createElement("div");
this.$refs.container.appendChild(div);
new Vue({
components: { Test },
data: this.$data,
render: h => h("test", {
props: {
message: this.msg
}
})
}).$mount(div);
}
}
Important note: this in this.$data refers the parent (the component which has the addComponent method), while this inside render refers new Vue()'s instance. So, the chain of reactivity is: parent.$data > new Vue().$data > new Vue().render => Test.props. I had numerous attempts at bypassing the new Vue() step and passing a Test component directly, but haven't found a way yet. I'm pretty sure it's possible, though, although the solution above achieves it in practice, because the <div> in which new Vue() renders gets replaced by its template, which is the Test component. So, in practice, Test is a direct ancestor of $refs.container. But in reality, it passes through an extra instance of Vue, used for binding.
Obviously, if you don't want to add a new child component to the container each time the method is called, you can ditch the div placeholder and simply .$mount(this.$refs.container), but by doing so you will replace the existing child each subsequent time you call the method.
See it working here: https://codesandbox.io/s/nifty-dhawan-9ed2l?file=/src/components/HelloWorld.vue
However, unlike the method below, you can't override data props of the child with values from parent dynamically. But, if you think about it, that's the way data should work, so just use props for whatever you want bound.
Initial answer:
Here's a function I've used over multiple projects, mostly for creating programmatic components for mapbox popups and markers, but also useful for creating components without actually adding them to DOM, for various purposes.
import Vue from "vue";
// import store from "./store";
export function addProgrammaticComponent(parent, component, dataFn, componentOptions) {
const ComponentClass = Vue.extend(component);
const initData = dataFn() || {};
const data = {};
const propsData = {};
const propKeys = Object.keys(ComponentClass.options.props || {});
Object.keys(initData).forEach(key => {
if (propKeys.includes(key)) {
propsData[key] = initData[key];
} else {
data[key] = initData[key];
}
});
const instance = new ComponentClass({
// store,
data,
propsData,
...componentOptions
});
instance.$mount(document.createElement("div"));
const dataSetter = data => {
Object.keys(data).forEach(key => {
instance[key] = data[key];
});
};
const unwatch = parent.$watch(dataFn || {}, dataSetter);
return {
instance,
update: () => dataSetter(dataFn ? dataFn() : {}),
dispose: () => {
unwatch();
instance.$destroy();
}
};
}
componentOptions is to provide any custom (one-off) functionality to the new instance (i.e.: mounted(), watchers, computed, store, you name it...).
I've set up a demo here: https://codesandbox.io/s/gifted-mestorf-297xx?file=/src/components/HelloWorld.vue
Notice I'm not doing the appendChild in the function purposefully, as in some cases I want to use the instance without adding it to DOM. The regular usage is:
const component = addProgrammaticComponent(this, SomeComponent, dataFn);
this.$el.appendChild(component.instance.$el);
Depending on what your dynamic component does, you might want to call .dispose() on it in parent's beforeDestroy(). If you don't, beforeDestroy() on child never gets called.
Probably the coolest part about it all is you don't actually need to append the child to the parent's DOM (it can be placed anywhere in DOM and the child will still respond to any changes of the parent, like it would if it was an actual descendant). Their "link" is programmatic, through dataFn.
Obviously, this opens the door to a bunch of potential problems, especially around destroying the parent without destroying the child. So you need be very careful and thorough about this type of cleanup. You either register each dynamic component into a property of the parent and .dispose() all of them in the parent's beforeDestroy() or give them a particular selector and sweep the entire DOM clean before destroying the parent.
Another interesting note is that in Vue 3 all of the above will no longer be necessary, as most of the core Vue functionality (reactivity, computed, hooks, listeners) is now exposed and reusable as is, so you won't have to $mount a component in order to have access to its "magic".
I have a call in my created method which has an await.
I want to know that the results of that call are loaded so that i can conditionally show/hide things in the DOM.
Right now it looks like the DOM is being rendered before that method has completed. But I though that methods in created were called before the DOM rendered?
You're correct in assuming that the created hook runs before the component mounts. However, the lifecycle hooks are not waiting for async calls to complete. If you want to wait for that call to be completed and data to load, you can do so by using a Boolean that you set to true when your data has loaded.
Your template:
<div v-if='dataLoaded'>Now you can see me.</div>
in your vue instace
export default {
data () {
return {
dataLoaded: false
}
},
created () {
loadMyData().then(data => {
// do awesome things with data
this.dataLoaded = true
})
}
}
This way you can keep your content hidden until that call has resolved. Take care with the context when you handle the ajax response. You will want to keep this as a reference to the original vue instance, so that you can set your data correctly. Arrow functions work well for that.
Good Day Fellows,
Quick summary: how can I use custom option merge strategies on an individual basis per component and not globaly?
My problem:
I am extending my components via Mixins and it is working great so far. However, while it is working great with the likes of component methods, I often need to override some lifecycle hooks, like mounted, created, etc. The catch is, Vue - by default - queues them up in an array and calls them after another. This is of course defined by Vues default merge strategies.
However in some specific cases I do need to override the hook and not have it stack. I know I can customize Vue.config.optionMergeStrategies to my liking, but I want the mergeStrategy customized on a per component basis and not applying it globably.
My naive approach on paper was to create a higher function which stores the original hooks, applies my custom strategy, calls my component body and after that restores Vues original hooks.
Let's say like this
export default function executeWithCustomMerge(fn) {
const orig = deep copy Vue.config.optionMergeStrategies;
Vue.config.optionMergeStrategies.mounted = (parent, child) => [child];
fn();
Vue.config.optionMergeStrategies = deep copy orig;
}
And here's it in action
executeWithCustomMerge(() => {
Vue.component('new-comp', {
mixins: [Vue.component("old-comp")],
},
mounted() {
//i want to override my parent thus I am using a custom merge strategy
});
});
Now, this is not going to work out because restoring the original hook strategies still apply on a global and will be reseted before most hooks on my component are being called.
I wonder what do I need to do to scope my merge strategy to a component.
I had a look at optionMergeStrategies in more detail and found this interesting quote from the docs (emphasis mine):
The merge strategy receives the value of that option defined on the parent and child instances as the first and second arguments, respectively. The context Vue instance is passed as the third argument.
So I thought it would be straightforward to implement a custom merging strategy that inspects the Vue instance and looks at its properties to decide which strategy to use. Something like this:
const mergeCreatedStrategy = Vue.config.optionMergeStrategies.created;
Vue.config.optionMergeStrategies.created = function strategy(toVal, fromVal, vm) {
if (vm.overrideCreated) {
// If the "overrideCreated" prop is set on the component, discard the mixin's created()
return [vm.created];
}
return mergeCreatedStrategy(toVal, fromVal, vm);
};
It turns out though that the 3rd argument (vm) is not set when the strategy function is called for components. It's a new bug! See https://github.com/vuejs/vue/issues/9623
So I found another way to inform the merge strategy on what it should do. Since JavaScript functions are first-class objects, they can have properties and methods just like any other object. Therefore, we can set a component's function to override its parents by setting a property on it and looking for its value in the merge strategy like so:
Vue.mixin({
created() {
this.messages.push('global mixin hook called');
}
});
const mixin = {
created() {
this.messages.push('mixin hook called');
},
};
const mergeCreatedStrategy = Vue.config.optionMergeStrategies.created;
Vue.config.optionMergeStrategies.created = function strategy(toVal, fromVal) {
if (fromVal.overrideOthers) {
// Selectively override hooks injected from mixins
return [fromVal];
}
return mergeCreatedStrategy(toVal, fromVal);
};
const app = {
el: '#app',
mixins: [mixin],
data: { messages: [] },
created() {
this.messages.push('component hook called');
},
};
// Comment or change this line to control whether the mixin created hook is applied
app.created.overrideOthers = true;
new Vue(app);
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h1>Messages from hooks</h1>
<p v-for="message in messages">{{ message }}</p>
</div>