vuejs where in the code/lifecycle should data retrieval be triggered - vue.js

I am working on a vuejs SPA.
I have a view that shows a list of items and another view that shows details for a specific Item.
when I click the item I switch views using:
this.$router.push('/item/' + event.ItemId );
The data is managed using vuex modules.
I would like to allow some temporary display while the item details are being retried (i.e. not to block the rendering of the item details view which should know on its own to indicate that it is still awaiting data).
And I would also have to consider that it should work if the URL is changed (I think I read that there is an issue with the view not being reloaded/recreated when only the item id would change in the URL.
Where would be the appropriate place (code/lifecycle) to trigger the (async) retrieval of the data required for rendering the item details view?

I would like to allow some temporary display while the item details are being retried (i.e. not to block the rendering of the item details view which should know on its own to indicate that it is still awaiting data).
One way to achieve this, is to define a state variable, named e.g. isLoading, in the data context of the Vue component. This variable would then be true while the data is retrieved asynchronously. In the template, you can use v-if to display a spinner while loading, and displaying the content after that.
If you are retrieving the data multiple times (refreshing the view), I would move the retrieving code into a method, e.g. called loadData. In the mounted section of the Vue component you then can just initially call this method once.
Here is some example code:
<template>
<div>
<button #click="loadData" :disabled="isLoading">Refresh</button>
<div class="item" v-if="!isLoading">
{{ item }}
</div>
<div class="spinner" v-else>
Loading...
</div>
</div>
</template>
<script>
import HttpService from '#/services/HttpService';
export default {
name: 'item-details',
data () {
return {
isLoading: false,
item: {}
};
},
methods: {
loadData () {
this.isLoading = true;
HttpService.loadData().then(response => {
this.item = response.data;
this.isLoading = false;
}, () => {
this.item = {};
this.isLoading = false;
});
}
},
mounted () {
this.loadData();
}
};
</script>
And I would also have to consider that it should work if the URL is changed (I think I read that there is an issue with the view not being reloaded/recreated when only the item id would change in the URL.
This issue you mentioned occurs if you are not using the HTML5 history mode, but an anchor (#) in the URL instead. If you are just changing the part after the anchor in the URL, the page is not actually refreshed by the browser. The Vue component won't be reloaded in this case and the state is still old. There are basically two ways around this:
You are switching from anchors in the URL to a real URL with the HTML5 history mode, supported by the Vue Router. This requires some back-end configuration, though. The browser then does not have this faulty behavior, because there is no anchor. It will reload the page on every manual URL change.
You can watch the $route object to get notified on every route change. Depending on if the user is changing the part after the anchor, or before, the behavior is different (it also depends where the cursor is, when you hit enter). If the part after the anchor is changed (your actual Vue route), only the component is notified. Otherwise, a full page refresh is made. Here's some example code:
// ...inside a Vue component
watch: {
'$route' (to, from) {
this.loadData();
}
}

Related

How do I implement APIs in a search bar on VueJS?

I'm working on a university project and I'm stuck trying to implement a searchbar that takes Game names from an API (https://api.rawg.io/api/games) and, after clicking on a game, redirects the user to a page with the specific game URL. Now, I wish I was at least able to do the first part.
I have created a searchbar.vue component and copy-pasted the code from the official Vuetify library:
(https://github.com/vuetifyjs/vuetify/blob/master/packages/docs/src/examples/autocompletes/simple/api.vue)
However, if I change the API from the one they provide as an example to the one I need (rawg), it does not work anymore and I am not sure why.
How can I make my searchbar work?
I'm providing a link to codesandbox where I'm currently working, in case it's needed:
(https://codesandbox.io/s/searchbar-mtjr2?file=/src/components/searchbar.vue)
I will do my best to give more information about the issue if needed.
Thank you very much in advance for even considering this post.
I believe all the problems with the search bar have been solved: Demo here
I added comments to the code so it should be clear how it works.
In searchbar.vue:
<template>
<v-autocomplete
clearable
:loading="isLoading"
:items="games"
:search-input.sync="search"
hide-details
item-text="name"
item-value="id"
label="Search for a Game..."
solo-inverted
></v-autocomplete>
</template>
<script>
import _ from "lodash";
export default {
data: () => ({
games: [], // this is where all the game data will be stored
isLoading: true, // variable to determine if the results are still being fetched from the API
search: null // this is where the query will be stored
}),
methods: {
getGames(params = "") {
// this function will fetch game data from https://api.rawg.io/api/games with whatever api parameters you pass to the parameter `params`
this.axios
.get("https://api.rawg.io/api/games" + params)
.then(resp => {
// fetch data
let tempGames = [...this.games.slice(0), ...resp.data.results]; //copy of this.games + new data
this.games = _.uniqBy(tempGames, "id"); // remove any duplicates
this.isLoading = false; // now that the data is in the array `games`, we can set isLoading to false
})
.catch(e => {
// code to run if there was an error
console.error(e); // display error message
});
},
searchGames(query) {
// this function will call getGames() with the search query formatted as an API parameter (?search=YOUR_QUERY)
let searchQuery = encodeURI("?search=" + query); // URI encode the query so it is able to be fetched properly
this.getGames(searchQuery);
}
},
watch: {
search: _.debounce(function(query) {
// debounce with a a value of 250 will allow this function to be every 250 milliseconds at most. So if the user is typing continually, it won't run until the user stops.
this.searchGames(query);
}, 250)
},
created() {
this.getGames(); // load the first 20 games initally
}
};
</script>
This will:
Load the first 20 games
When you enter a query and stop typing, it will perform a request to https://api.rawg.io/api/games?search=YOUR_QUERY, and add the results to the games array. (So each time you search for something, the saved games array increases. That means that if you search for the same game twice, while it will still search online with https://api.rawg.io/api/games?search=YOUR_QUERY, the game will have been already in the games array from the first time, so it will be loaded immediately.)
v-autocomplete filters and displays the results.
This method will work better on faster connections, as the results are loaded faster. So if you have a slow connection, it may take time for the results to load. Unfortunately that's something that can't be worked around (although you could load a lot of data beforehand, but there's no guarantee that what you load is what the user will search for)
The graphical problem with the navbar was resolved by changing the parent <div> in App.vue to <v-app>, like so:
Before:
<template>
<div id="app">
<nav-bar></nav-bar>
...
</div>
</template>
After:
<template>
<v-app id="app">
<nav-bar></nav-bar>
...
</v-app>
</template>
I also added lodash as a dependency for debouncing (waiting until the user stops typing) and removing duplicates.
Here's the codepen I was working on.
Do let me know if you have any further questions.

Why in my nuxt-link doesn't reload page with same url?

If I’m on a page with the URL 'http://localhost:8080/item' and I’m clicking on the same link on this page, then the page does not reload.
I need to make that if I click on the same link, the page will reload.
My link:
<nuxt-link :to="/item">
Any insight will be welcome. Thanks!
Use key, something like:
<router-view :key="$route.params.yourCustomParam"/>
Also you can use something like:
<router-link :to="{ params: { yourCustomParam: Data.now } }" replace>link</router-link>
Remember to is passed router.push() and it accept an object also. Doing that, it is more declarative and controllable. I'm using this to decide if the page of component should be rerendered since they will based on id params obtained from URL entry, and my child component can still using nesting .
I recently tried to solve a similar issue and to overcome this I used Vuex with :key (ref).
Firstly, in your store you need a state property such as:
export const state = () => ({
componentUpdates: {
item: 0,
//can add more as needed
}
})
In general, you could use only one property across the app if you prefer it that way. Just remember that later on, the key value needs to be unique - that is in the case if you used this property for two or more components within one page, for example. In this case, you could do something like this :key="$store.getters.getComponentUpdates.item+'uniqueString'"
then a getter:
export const getters = {
getComponentUpdates(state) {
return state.updateComponent;
}
}
finally a mutatation:
export const mutations = {
updateComponent(state, payload) {
return state.componentUpdates[payload.update]++
}
}
Now we can utilise the reactive :key wherever needed.
But first in your nuxt-link lets add an event to trigger the mutation, note the usage of #click.native to trigger the click event:
<nuxt-link #click.native="$store.commit('updateComponent', { update: 'item'})" :to="/item">
Now in the item page, for example. Let's imagine there is a component that needs to be updated. In this case we would add :key to it:
<my-item :key="$store.getters.getComponentUpdates.item" />
That is it. As you can see this solution utilises the benefits of nuxt-link but also allows us to selectively update only parts of our page that need updates (we could update the entire page this way as well if needed).
In case if you needed to trigger the logic from mounted or initial load in general, then you could use computed property and :key to your div container, right inside the <template> of your page.
Add :key to the div:
<template>
<div :key="$store.getters.getComponentUpdates.item"></div>
</template>
Create computed property:
computed: {
updateItemPage() {
//run your initial instructions here as if you were doing it in mounted then return the getter
this.initialLoadMethod()
return this.$store.getters.getComponentUpdates.item
}
}
The final touch, which is not crucial but can be implemented in order to reset the state property:
export const mutations = {
updateComponent(state, payload) {
return state.componentUpdates[payload.update] >= 10
? state.componentUpdates[payload.update] = 0
: state.componentUpdates[payload.update]++
}
}

Trying to get vue.js to render something conditionally based on a method in created()

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.

How can I destroy a cached Vue component from keep-alive?

My Vue app has a dynamic tabs mechanism.
Users can create as many tabs as the want on the fly, each tab having its own state (eg "Pages").
I am using the <keep-alive> component to cache the different pages.
<keep-alive include="page">
<router-view :key="$route.params.id" />
</keep-alive>
But users can also "close" individual tab. As pages tend to store a lot of datas, I would like to delete the according page component from the cache, as the user close the tab.
How can I programmatically destroy a cached component inside keep-alive ?
You can call this.$destroy() before user close the tab and delete all of data and event binding in that one.
If you don't mind losing the state when a tab is added/removed, then you can try these:
Use v-if and turn off the keep-alive component and turn it back on in
nextTick
Use v-bind on the include list, and remove "page" and add it
back in nextTick
<keep-alive :include="cachedViews">
<router-view :key="key" />
</keep-alive>
cachedViews is the array of the route component name
First when create a tab, cachedViews push the cached route name, when you switch the opened tab, the current route is cached.
Second when close the tab, cachedViews pop the cached route name, the route
component will destroyed.
There is no built-in function in keep-alive which allows you to clear a specific component from the cache.
However, you can clear the cache from the VNode directly inside the component you want to destroy by calling this function :
import Vue, { VNode } from 'vue'
interface KeepAlive extends Vue {
cache: { [key: string]: VNode }
keys: string[]
}
export default Vue.extend({
name: 'PageToDestroy',
...
methods: {
// Make sure you are not on this page anymore before calling it
clearPageFromKeepAlive() {
const myKey = this.$vnode.key as string
const keepAlive = this.$vnode.parent?.componentInstance as KeepAlive
delete keepAlive.cache[myKey]
keepAlive.keys = keepAlive.keys.filter((k) => k !== myKey)
this.$destroy()
}
},
})
For me, it doesn't cause any memory leaks and the component is not in the Vue.js devtools anymore.
based on the answer of #feasin, here is the setup I am using
template
<router-view v-slot="{ Component }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="$route.fullPath" />
</keep-alive>
</router-view>
script
import { ref, inject, watch } from "vue";
export default {
components: { CustomRouterLink },
setup() {
const cachedViewsDefault = ["Page1", "Page1", "Page3"];
var cachedViews = ref([]);
const auth = inject("auth");
// check whether user is logged in (REACTIVE!)
const isSignedIn = auth.isSignedIn;
// set the initial cache state
if (isSignedIn.value) {
cachedViews.value = cachedViewsDefault;
}
// clear the cache state
watch(isSignedIn, () => {
if (!isSignedIn.value) {
cachedViews.value = [];
} else {
cachedViews.value = cachedViewsDefault;
}
});
return {
cachedViews,
};
},
};
First I set the initial cached views value based on whether the user is logged in or not.. After the user logs-out I simply set the array value to an empty array.
When the user logs back in - I push the default array keys back into the array.
This example of course does not provide the login/logout functionality, it is only meant as a POC to to the solution proposed by the #feasin (which seems like a good approach to me)
Edit 19.01.2022
I now understand the shortcomings of such approach. It does not allow to gradually destroy a certain component. Given that we have a component named Item and it's path is Item/{id} - there is currently no native way (in Vuejs3) to remove, let's say a cached item with Id = 2. Follow up on this issue on the Github: https://github.com/vuejs/rfcs/discussions/283
Edit 20-21.01.2022
Note that you have to use the computed function for inclusion list. Otherwise the component will not ever be unmounted.
Here is the fiddle with the problem: https://jsfiddle.net/7f2d4c0t/4/
Here's fiddle with the fix: https://jsfiddle.net/mvj2z3pL/
return {
cachedViews: computed(() => Array.from(cachedViews.value)),
}

vuejs - Keep Form data when browser back

I do not speak English, I'm using google translator.
I am having a problem with the back button after a form submit, I use vuejs to display content and manipulate / validate a form, clicking the submit button is redirected to another action. if I click back in the browser vuejs reloads the previous page, so I lose the data of my form, how do I keep the data of a form without using a , since it is a submit button? I believe that the tag does not suit me in this case (I tested it and it did not work).
Simply you can use keep-alive in Vue js to cache states on back action.
For more information:
https://v2.vuejs.org/v2/api/#keep-alive
keep alive support in Vue2 and also Vue3, but the syntax is different. Also you can use in Nuxt.
For better performance you should limit components to be caches by "max" and "include" props.
you can use in layout or only wrap components:
Wrap in layout:
<keep-alive>
<router-view :key="$route.fullPath"></router-view>
</keep-alive>
Wrap components to cache:
<keep-alive>
<product-list></product-list>
</keep-alive>
Nuxt:
<template>
<div>
<Nuxt keep-alive :keep-alive-props="{ exclude: ['modal'],max:3 }" />
</div>
</template>
** Keep alive should be in the same layout(if you use keep alive in layout).
** If you destroy keep alive cache, the caching will not work anymore.
** You can control cache with "key" props. For same "key" component will show from cache.
** Keep alive only cache states. So for keep scroll, you should store last position as state and set scroll position on activated hook.
** Lifecycle doesn't work on cache mode and you should use activated and deactivated hooks.
The HTML5 History API seems made for this.
When formdata is changed save it in the history with:
window.history.replaceState(state, "");
And when (re)loading the page load from history with
window.history.state
Or look into window.addEventListener("popstate", function(){...})
E.g. in my listview I use:
const methods = {
saveStateToHistory() {
var state = window.history.state || {};
if( ! state[this.id]) {
state[this.id] = {};
}
state[this.id].currentPage = this.currentPage;
state[this.id].currentPerPage = this.currentPerPage;
state[this.id].collFilter = this.collFilter;
window.history.replaceState(state, "");
},
};
const watch = {
currentPage(newdata) {
this.saveStateToHistory();
},
currentPerPage(newdata) {
this.saveStateToHistory();
},
collFilter(newdata) {
this.saveStateToHistory();
},
};
function created() {
var state = window.history.state || {};
var myState = state[this.id];
if(myState) {
this.currentPage = myState.currentPage;
this.currentPerPage = myState.currentPerPage;
this.collFilter = myState.collFilter;
}
};
This keeps the current page and selected filters after someone folows a link from the list-view and then uses the browser back-button to go back to the list-view.
I'm using state[this.id] to allow for history state from different components in the same page.