Vue js component template not updating with data - vue.js

I have a weird issue in some of my Vue js components, let me explain. I only render my component template after data has been initialised like so:
<template>
<div>
<div v-if='!isLoading'>
<!-- content -->
</div>
<div v-else>...</div>
</div>
</template>
In the created method of this component, I get some data from the store and set isLoading to false like so.
data() {
return {
variable: null,
isLoading: true,
}
},
created() {
this.variable = this.$store.getters['someModule/someGetter']
this.isLoading = false
}
Here's where the weird behaviour happens. Even though I updated the isLoading variable to false in the created method, the component template is not updating.
When I log the isLoading variable to the console at the end of the created method, it logs false, like i set it. But when I check the isLoading variable in the Vue js tools, it's still set to true...
Lets say this components is rendered in '/content'. This weird behaviour happens when I change routes from '/' to '/content'. When I refresh the app on the '/content' route, this doesn't happen. When I go from '/' to '/other-content' and then to '/content' it also doesn't happen.
Any ideas on why this happens would be greatly appreciated.
Thanks is advance and have a nice day!

There are subtle differences between mounted and created in your case since you want to manipulate the DOM, you should use mounted lifecycle hook.
This answer would expound on the differences between the two lifecycle methods.

This is a working example of what you're trying to do: https://codesandbox.io/s/blissful-field-kjufc?file=/src/App.vue
The interesting part of the code is here:
async created() {
const response = await fetch("https://jsonplaceholder.typicode.com/photos");
const json = await response.json();
console.log("done loading the data");
if (json) this.isLoading = false;
},
You can go to your network tab and select "slow 3G" to have an emulated slow connection. That way, you will see that the VueJS logo is not displayed until we have fetched all the 5000 photos.
If it's not helping, we definitely need more details, like vue devtools debugging or a reproduction.

Related

Recommended way of waiting on an Apollo query before rendering the next page?

When using the Apollo module in a Nuxt app, the default behavior when changing routes is to render the new page immediately, before data has been fetched via Apollo.
This results in some pretty janky rendering experiences where the page does a partial render and very soon after completes rendering with data from the server, making everything on the page shift due to the changing size of components that now have data. This looks pretty bad because the data actually comes back fairly quickly, so it would be fine to wait for the data to return before rendering the new route.
What's the recommended way of waiting on the Apollo queries on a page (and its subcomponents) to complete before rendering the page?
(There's a related question that's not specific to Nuxt, but I'm not sure how to translate the recommendation to a Nuxt app.)
I'd love to see a code example of using beforeRouteEnter to fetch data via Apollo and only entering the route once the data is fetched.
Haven't used this module before, but it should be like any other async action you want to perform before page rendering in Nuxt.
It only depends if you want to pre-fill the store:
https://github.com/nuxt-community/apollo-module#nuxtserverinit
https://nuxtjs.org/guide/vuex-store/#the-nuxtserverinit-action
or only one page:
https://github.com/nuxt-community/apollo-module#asyncdatafetch-method-of-page-component
https://nuxtjs.org/guide/async-data
You can use async/await or promises if you have more than one request before page should be rendered.
When async actions are finished, Nuxt starts rendering the page. This works for SSR and if you navigate to pages on the client (nuxtServerInit will only fire once when real request is made, not when navigating on client side).
Side note: beforeRouteEnter is usually used, to validate params and check if the route is allowed.
did you try disabling the prefetch?
prefetch: false
The best approach is to use the loading attribute:
<template>
<div v-if="!this.$apollo.loading">
Your product: {{product}}
</div>
</template>
<script>
export default {
name: "Product",
apollo: {
product: {
query: productQuery,
variables() {
return {
productId: this.productId
}
}
}
}
}
</script>
I'm unfamiliar with Apollo, but I think this is what you are looking for:
// Router.js
beforeRouteEnter(to, from, next)
{
executeSomeApolloPromise().then((data) => {
// The promise has now been complete; continue to the component.
next((vm) => {
// You have access here to the component instance via `vm`.
// Note that `beforeRouteEnter` is the only guard that has this.
vm.someApolloData = data;
});
});
}
See https://router.vuejs.org/guide/advanced/navigation-guards.html#per-route-guard

How do I stop my vue component from rendering until a call completes

I've googled this but I can't find any specific solution. Basically I have a vue component that depends on an init call and I want it to stop rendering until the call completes, at which point I want the component to render. Seems simple but unless I'm missing something I can't find any lifecycle method that does that.
You can use v-if for that purpose
<template>
<div v-if="loaded"></div>
</template>
<script>
export default {
name: 'TestComponent',
data: function () {
return {
loaded: false
}
},
created() {
callExternalServices().then(() => {
this.loaded = true
})
}
}
</script>
It will render an empty component until loaded == true
Basically you make an init call in the created or mounted lifecycle method and you initialize a data object with the response of the call. If you don't change data during the call there is no reason for vue to render anything.
Maybe you can provide a jsfiddle that show exactly your problem.

How would a WebTorrent VueJS component could be build?

Hy there. Given webtorrent.io I would like to build a VueJS component that shows loading magnet files, and status and also when downloads finishes triggers players.
Is the Vuex Store a good place to keep a list of active download queues and a stream o data?
All that is possible with webtorrent.
var WebTorrent = require('webtorrent')
var client = new WebTorrent()
var magnetURI = 'magnet: ...'
client.add(magnetURI, { path: '/path/to/folder' }, function (torrent) {
torrent.on('done', function () {
console.log('torrent download finished')
})
})
How could I structure that architecture/pattern with VueJS? Any insights, apreciated
Thanks!
I will answer cause I found a solution. Then if the community wants this to be deleted, its all right.
Basically what I did is wait for a Component to be mounted.
Inside AComponent.Vue
<template>
<keep-alive>
<!-- HTML binding webtorrent client data -->
</keep-alive>
</template>
<script>
export default {
mounted() {
var client = WebTorrent()
}
}
</script>
Why this achieve what I was looking for ? :
keep alive keep that parts of the DOM alive, when i render another template and come back to this one (as if the download queue is in a widget, modal, or another view that needs to keep alive).
mounted, its part of the component lifecycle hooks (https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram), and happens once the DOM has been maniuplated, window exists (needed by WebTorrent), and the component has been rendered.

vuejs component created wait for data to load first

I have an App.vue component where I want to load the currently logged in user. I also want to redirect the user when he\she tries to go the login route and the currently logged in user is already saved in context (he's already signed in).
// inside App.vue component
created() {
AuthService.getCurrentUser().then((user) => {
this.user = user;
});
}
I have a check in the created method of the login component for whether the currentUser is setted, but then when the user tries to go to the login page it might be possible that the the request for the current user is not finished.
My question is:
How do I wait for the data to load before the App.vue component loads?
I saw something like this:
waitForData: true,
data(transition) {
return AuthService.getCurrentUser().then(currentUser => {
transition.next({ currentUser });
});
}
which doesn't actually wait for the data to be loaded and the component loads anyway.
Edit: I'm aware of beforeRouteEnter but this is App.vue component which is a parent component of all components and not a route specific component.
I ran into a very similar problem and solved by adding a v-if on the element that wrapped the child component that depends on the loaded data. And then of course have an data property to control that v-if. If you don't have a wrapping element you can always use a template element to do the wrapping.
This is not the Vue way of doing things.
Use VUEX to store your currentUser object
Set up the Vuex getters in App.vue's computed section. Your template will be updated dynamically once the data is ready.
See the mapGetters section in this page. It works very well with the computed mechanism.
You can also use v-if in your relevant component, so that the component won't be created before your data is ready in VUEX.
If using vue-router, you can use the beforeRouteEnter guard to load data async, as described here: https://router.vuejs.org/en/advanced/data-fetching.html
One way I can think of is when the user is loaed in the App.vue component to check the current path and if it's \login to redirect.
Something like this:
created() {
AuthService.getCurrentUser()
.then(user => this.setCurrentUser(user))
.then(() => {
const { path } = this.$router.currentRoute;
if (path === '/login') {
this.$router.push('/');
}
});

Vue-multiselect inconsistent reactive options

So I'm building an application using Laravel Spark, and therefore taking the opportunity to learn some Vue.js while I'm at it.
It's taken longer for me to get my head around it than I would have liked but I have nearly got Vue-multiselect working for a group of options, the selected options of which are retrieved via a get request and then updated.
The way in which I've got this far may well be far from the best, so bear with me, but it only seems to load the selected options ~60% of the time. To be clear - there are never any warnings/errors logged in the console, and if I check the network tab the requests to get the Tutor's instruments are always successfully returning the same result...
I've declared a global array ready:
var vm = new Vue({
data: {
tutorinstruments: []
}
});
My main component then makes the request and updates the variable:
getTutor() {
this.$http.get('/get/tutor')
.then(response => {
this.tutor = response.data;
this.updateTutor();
});
},
updateTutor() {
this.updateTutorProfileForm.profile = this.tutor.profile;
vm.tutorinstruments = this.tutor.instruments;
},
My custom multiselect from Vue-multiselect then fetches all available instruments and updates the available instruments, and those that are selected:
getInstruments() {
this.$http.get('/get/instruments')
.then(response => {
this.instruments = response.data;
this.updateInstruments();
});
},
updateInstruments() {
this.options = this.instruments;
this.selected = vm.tutorinstruments;
},
The available options are always there.
Here's a YouTube link to how it looks if you refresh the page over and over
I'm open to any suggestions and welcome some help please!
Your global array var vm = new Vue({...}) is a separate Vue instance, which lives outside your main Vue instance that handles the user interface.
This is the reason you are using both this and vm in your components. In your methods, this points to the Vue instance that handles the user interface, while vm points to your global array that you initialized outside the Vue instance.
Please check this guide page once more: https://v2.vuejs.org/v2/guide/instance.html
If you look at the lifecycle diagram that initializes all the Vue features, you will notice that it mentions Vue instance in a lot of places. These features (reactivity, data binding, etc.) are designed to operate within a Vue instance, and not across multiple instances. It may work once in a while when the timing is right, but not guaranteed to work.
To resolve this issue, you can redesign your app to have a single Vue instance to handle the user interface and also data.
Ideally I would expect your tutorinstruments to be loaded in a code that initializes your app (using mounted hook in the root component), and get stored in a Vuex state. Once you have the data in your Vuex state, it can be accessed by all the components.
Vuex ref: https://vuex.vuejs.org/en/intro.html
Hope it helps! I understand I haven't given you a direct solution to your question. Maybe we can wait for a more direct answer if you are not able to restructure your app into a single Vue instance.
What Mani wrote is 100% correct, the reason I'm going to chime in is because I just got done building a very large scale project with PHP and Vue and I feel like I'm in a good position to give you some advice / things I learned in the process of building out a PHP (server side) website but adding in Vue (client side) to the mix for the front end templating.
This may be a bit larger than the scope of your multiselect question, but I'll give you a solid start on that as well.
First you need to decide which one of them is going to be doing the routing (when users come to a page who is handling the traffic) in your web app because that will determine the way you want to go about using Vue. Let's say for the sake of discussion you decide to authenticate (if you have logins) with PHP but your going to handle the routing with Vue on the front end. In this instance your going to want to for sure have one main Vue instance and more or less set up something similar to this example from Vue Router pretending that the HTML file is your PHP index.php in the web root, this should end up being the only .php file you need as far as templating goes and I had it handle all of the header meta and footer copyright stuff, in the body you basically just want one div with the ID app.
Then you just use the vue router and the routes to load in your vue components (one for each page or category of page works easily) for all your pages. Bonus points if you look up and figure using a dynamic component in your main app.vue to lazy load in the page component based on the route so your bundle stays small.
*hint you also need a polyfill with babel to do this
template
<Component :is="dynamicComponent"/>
script
components: {
Account: () => import('./Account/Account.vue'),
FourOhFour: () => import('../FourOhFour.vue')
},
computed: {
dynamicComponent() {
return this.$route.name;
}
},
Now that we are here we can deal with your multiselect issue (this also basically will help you to understand an easy way to load any component for Vue you find online into your site). In one of your page components you load when someone visits a route lets say /tutor (also I went and passed my authentication information from PHP into my routes by localizing it then using props, meta fields, and router guards, its all in that documention so I'll leave that to you if you want to explore) on tutor.vue we will call that your page component is where you want to call in multiselect. Also at this point we are still connected to our main Vue instance so if you want to reference it or your router from tutor.vue you can just use the Vue API for almost anything subbing out Vue or vm for this. But the neat thing is in your main JS file / modules you add to it outside Vue you can still use the API to reference your main Vue instance with Vue after you have loaded the main instance and do whatever you want just like you were inside a component more or less.
This is the way I would handle adding in external components from this point, wrapping them in another component you control and making them a child of your page component. Here is a very simple example with multiselect pretend the parent is tutor.vue.
Also I have a global event bus running, thought you might like the idea
https://alligator.io/vuejs/global-event-bus/
tutor.vue
<template>
<div
id="user-profile"
class="account-content container m-top m-bottom"
>
<select-input
:saved-value="musicPreviouslySelected"
:options="musicTypeOptions"
:placeholder="'Choose an your music thing...'"
#selected="musicThingChanged($event)"
/>
</div>
</template>
<script>
import SelectInput from './SelectInput';
import EventBus from './lib/eventBus';
export default {
components: {
SelectInput
},
data() {
return {
profileLoading: true,
isFullPage: false,
isModalActive: false,
slackId: null,
isActive: false,
isAdmin: false,
rep: {
id: null,
status: '',
started: '',
email: '',
first_name: '',
},
musicTypeOptions: []
};
},
created() {
if (org.admin) {
this.isAdmin = true;
}
this.rep.id = parseInt(this.$route.params.id);
this.fetchData();
},
mounted() {
EventBus.$on('profile-changed', () => {
// Do something because something happened somewhere else client side.
});
},
methods: {
fetchData() {
// use axios or whatever to fetch some data from the server and PHP to
// load into the page component so say we are getting the musicTypeOptions
// which will be in our selectbox.
},
musicThingChanged(event) {
// We have our new selection "event" from multiselect so do something
}
}
};
</script>
this is our child Multiselect wrapper SelectInput.vue
<template>
<multiselect
v-model="value"
:options="options"
:placeholder="placeholder"
label="label"
track-by="value"
#input="inputChanged" />
</template>
<script>
import Multiselect from 'vue-multiselect';
export default {
components: { Multiselect },
props: {
options: {
type: [Array],
default() {
return [];
}
},
savedValue: {
type: [Array],
default() {
return [];
}
},
placeholder: {
type: [String],
default: 'Select Option...'
}
},
data() {
return {
value: null
};
},
mounted() {
this.value = this.savedValue;
},
methods: {
inputChanged(selected) {
this.$emit('selected', selected.value);
}
}
};
</script>
<style scoped>
#import '../../../../../node_modules/vue-multiselect/dist/vue-multiselect.min.css';
</style>
Now you can insure you are manging the lifecycle of your page and what data you have when, you can wait until you get musicTypeOptions before it will be passed to SelectInput component which will in turn set up Multiselect or any other component and then handle passing the data back via this.$emit('hihiwhatever') which gets picked up by #hihiwhatever on the component in the template which calls back to a function and now you are on your way to do whatever with the new selection and pass different data to SelectInput and MultiSelect will stay in sync always.
Now for my last advice, from experience. Resist the temptation because you read about it 650 times a day and it seems like the right thing to do and use Vuex in a setup like this. You have PHP and a database already, use it just like Vuex would be used if you were making is in Node.js, which you are not you have a perfectly awesome PHP server side storage, trying to manage data in Vuex on the front end, while also having data managed by PHP and database server side is going to end in disaster as soon as you start having multiple users logged in messing with the Vuex data, which came from PHP server side you will not be able to keep a single point of truth. If you don't have a server side DB yes Vuex it up, but save yourself a headache and wait to try it until you are using Node.js 100%.
If you want to manage some data client side longer than the lifecycle of a page view use something like https://github.com/gruns/ImmortalDB it has served me very well.
Sorry this turned into a blog post haha, but I hope it helps someone save themselves a few weeks.