vuejs - Keep Form data when browser back - vue.js

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.

Related

Vue Routing - prevent re-rendering the page?

I have News.vue and News-View.vue. When I route to news/1, it opens the News-View.vue page. The problem is that I have many search filters (category, date, etc.) and infinite scroll in News.vue. This means, that when the user gets back from News-View.vue, everything re-renders and refreshes, and the user's preferences are cleared. Is there a way to not re-render the News.vue page?
News.Vue:
beforeMount: async function () {
this.$axios.post('http://localhost/?action=news', this.filters).then((response) => {
this.results = this.results.concat(response)
})
}
P.S. I'm currently using State Management for saving the loaded results and saved scroll position and it works, but again, I was wondering is there a way to just not re-render a page, but save the current state or something...
You could try to use the keep-alive tag:
<keep-alive>
<router-view></router-view>
</keep-alive>
https://v2.vuejs.org/v2/guide/components-dynamic-async.html
https://router.vuejs.org/api/
However, you'll need to tweak it a little bit because I'm not sure you would want that behavior for your entire app?

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]++
}
}

Lazy loading data from API in Vue.js

I want to lazy load content when the user scrolls down to the bottom of the Bootstrap modal, sort of like an infinite scroll, using Vue.js. I'm fetching all my data from a action method on API call. The data coming from this API is stored in array of objects on mounted and is used in the application.
So far so good. But now I want to implement that in the initial state, only the first 10 items are fetched from the API. When the user scrolls down to the bottom of the page I want to fetch the next 10 results and so on. I've looked at the documentation of the API that I'm using, and it has support for offsetting the items I'm fetching. Though I'm not sure where to start from there. Does anyone know any good resources on this subject? Thanks a ton!
after a while i solved the problem
here is my sample project for you reading this question
Here is my solution if you are trying to avoid using an npm package. You can use it in any scrollable div in vue. And in the if statement below is where you would handle your api call to fetch more results.
<template>
<div class="scroll" #scroll="scroll">
</div>
</template>
<script>
export default{
name: "Scroll",
data(){
return{
bottom: false
}
},
methods:{
scroll(e){
const {target} = e;
if (Math.ceil(target.scrollTop) >=
target.scrollHeight - target.offsetHeight) {
//this code will run when the user scrolls to the bottom of this div so
//you could do an api call here to implement lazy loading
this.bottom = true;
}
}
}
</script>

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 where in the code/lifecycle should data retrieval be triggered

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();
}
}