I have the following sequence happening:
Main screen
Loading screen
Results screen
On homepage, when someone clicks a button, I send them to the loading screen, thru:
this.$router.push({path: "/loading"});
And once their task finishes, they are sent to the results screen via
this.$router.push({path: "/results/xxxx"});
The problem is, usually they want to go from results back to the main screen, but when they click back, they're sent to loading again which sends them back to results, so they're stuck in an infinite loop & unable to go back to main screen.
Any ideas how to fix this? I'd ideally like if there was an option like:
this.$router.push({path: "/loading", addToHistory: false});
which would send them to the route without adding it to history.
This should have a real answer using this.$router.replace:
// On login page
// Use 'push' to go to the loading page.
// This will add the login page to the history stack.
this.$router.push({path: "/loading"});
// Wait for tasks to finish
// Use 'replace' to go to the results page.
// This will not add '/loading' to the history stack.
this.$router.replace({path: "/results/xxxx"});
For further reading the Vue Router is using History.pushState() and History.replaceState() behind the scenes.
There is a perfect way to handle this situation
You can use in-component guard to control the route in granule level
Make the following changes in your code
In main screen component
Add this beofreRouteLeave guard in component options, before leaving to 'result screen' you are setting the route to go only
through loading screen
beforeRouteLeave(to, from, next) {
if (to.path == "/result") {
next('/loading')
}
next();
},
In loading screen component
If the route go backs from result to loading then , it should not land
here and directly jump to main screen
beforeRouteEnter(to, from, next) {
if (from.path == "/result") {
next('/main')
}
next();
},
In loading screen, The beforeRouteEnter guard does NOT have access to
this, because the guard is called before the navigation is confirmed,
thus the new entering component has not even been created yet. So taking the advantage of this, you won't get the infinite calls fired when routing from results screen
In result screen component
if you use go back then it should not land in loading and directly
jump to main screen
beforeRouteLeave(to, from, next) {
if (to.path == "/loading") {
next('/')
}
next();
},
I have just created small vue application to reproduce the same issue. It works in my local as per your question. Hope it resolves your issue as well.
I guess router.replace is the way to go - but still some lines of thought (untested):
Basically on $router change it renders the loading-component until it emits load:stop, then it renders the router-view
import { Vue, Component, Watch, Prop } from "vue-property-decorator";
#Component<RouteLoader>({
render(h){
const rv = (this.$slots.default || [])
.find(
child => child.componentOptions
//#ts-ignore
&& child.componentOptions.Ctor.extendedOptions.name === "RouterView"
)
if(rv === undefined)
throw new Error("RouterView is missing - add <router-view> to default slot")
const loader = (this.$slots.default || [])
.find(
child => child.componentOptions
//#ts-ignore
&& child.componentOptions.Ctor.extendedOptions.name === this.loader
)
if(loader === undefined)
throw new Error("LoaderView is missing - add <loader-view> to default slot")
const _vm = this
const loaderNode = loader.componentOptions && h(
loader.componentOptions.Ctor,
{
on: {
// "load:start": () => this.loading = true,
"load:stop": () => _vm.loading = false
},
props: loader.componentOptions.propsData,
//#ts-ignore
attrs: loader.data.attrs
}
)
return this.loading && loaderNode || rv
}
})
export default class RouteLoader extends Vue {
loading: boolean = false
#Prop({default: "LoaderView"}) readonly loader!: string
#Watch("$route")
loads(nRoute: string, oRoute: string){
this.loading = true
}
}
#Component<Loader>({
name: "LoaderView",
async mounted(){
await console.log("async call")
this.$emit("load:stop")
// this.$destroy()
}
})
export class Loader extends Vue {}
This is a tough call considering how little we know about what's occurring in your loading route.
But...
I've never had a need to build a loading route, only ever loading component(s) that gets rendered on multiple routes during init/data gathering stage.
One argument for not having a loading route would be that a user could potentially navigate directly to this URL (accidentally) and then it seems like it wouldn't have enough context to know where to send the user or what action to take. Though this could mean that it falls through to an error route at this point. Overall, not a great experience.
Another is that if you simplify your routes, navigation between routes becomes much simpler and behaves as expected/desired without the use of $router.replace.
I understand this doesn't solve the question in the way you're asking. But I'd suggest rethinking this loading route.
App
<shell>
<router-view></router-view>
</shell>
const routes = [
{ path: '/', component: Main },
{ path: '/results', component: Results }
]
const router = new VueRouter({
routes,
})
const app = new Vue({
router
}).$mount('#app')
Shell
<div>
<header>
<nav>...</nav>
</header>
<main>
<slot></slot>
</main>
</div>
Main Page
<section>
<form>...</form>
</section>
{
methods: {
onSubmit() {
// ...
this.$router.push('/results')
}
}
}
Results Page
<section>
<error v-if="error" :error="error" />
<results v-if="!error" :loading="loading" :results="results" />
<loading v-if="loading" :percentage="loadingPercentage" />
</section>
{
components: {
error: Error,
results: Results,
},
data() {
return {
loading: false,
error: null,
results: null,
}
},
created () {
this.fetchData()
},
watch: {
'$route': 'fetchData'
},
methods: {
fetchData () {
this.error = this.results = null
this.loading = true
getResults((err, results) => {
this.loading = false
if (err) {
this.error = err.toString()
} else {
this.results = results
}
})
}
}
}
Results Component
Basically the exact same results component you already have, but if loading is true, or if results is null, however you prefer, you can create a fake dataset to iterate over and create skeleton versions, if you'd like to. Otherwise, you can just keep things the way you have it.
Another option is to use the History API.
Once you are in the Results screen, you can utilize the ReplaceState to replace the URL in history of the browser.
This can be done with the beforeEach hook of the router.
What you need to do is you must save a variable globally or in localStorage in the loading component when the data is loaded (before redirecting to the results component):
export default {
name: "results",
...
importantTask() {
// do the important work...
localStorage.setItem('DATA_LOADED', JSON.stringify(true));
this.$router.push({path: "/results/xxxx"});
}
}
And then you should check for this variable in the beforeEach hook and skip to the correct component:
// router.js...
router.beforeEach((to, from, next) => {
const dataLoaded = JSON.parse(localStorage.getItem('DATA_LOADED'));
if (to.name === "loading" && dataLoaded)
{
if (from.name === "results")
{
next({ name: "main"} );
}
if (from.name === "main")
{
next({ name: "results"} );
}
}
next();
});
Also, do remember to reset the value to false in your main component when the query button is clicked (before routing to the loading component):
export default {
name: "main",
...
queryButtonClicked() {
localStorage.setItem('DATA_LOADED', JSON.stringify(false));
this.$router.push({path: "/loading"});
}
}
Your loading screen should not be controlled by vue-router at all.
The best option is to use a modal/overlay for your loading screen, controlled by javascript. There are lots of examples around for vue. If you cant find anything then vue-bootstrap has some easy examples to implement.
Related
So I have this nuxt page /pages/:id.
In there, I do load the page content with:
content: function(){
return this.$store.state.pages.find(p => p.id === this.$route.params.id)
},
subcontent: function() {
return this.content.subcontent;
}
But I also have an action in this page to delete it. When the user clicks this button, I need to:
call the server and update the state with the result
redirect to the index: /pages
// 1
const serverCall = async () => {
const remainingPages = await mutateApi({
name: 'deletePage',
params: {id}
});
this.$store.dispatch('applications/updateState', remainingPages)
}
// 2
const redirect = () => {
this.$router.push({
path: '/pages'
});
}
Those two actions happen concurrently and I can't orchestrate those correctly:
I get an error TypeError: Cannot read property 'subcontent' of undefined, which means that the page properties are recalculated before the redirect actually happens.
I tried:
await server call then redirect
set a beforeUpdate() in the component hooks to handle redirect if this.content is empty.
delay of 0ms the server call and redirecting first
subcontent: function() {
if (!this.content.subcontent) return redirect();
return this.content.subcontent;
}
None of those worked. In all cases the current page components are recalculated first.
What worked is:
redirect();
setTimeout(() => {
serverCall();
}, 1000);
But it is obviously ugly.
Can anyone help on this?
As you hinted, using a timeout is not a good practice since you don't know how long it will take for the page to be destroyed, and thus you don't know which event will be executed first by the javascript event loop.
A good practice would be to dynamically register a 'destroyed' hook to your page, like so:
methods: {
deletePage() {
this.$once('hook:destroyed', serverCall)
redirect()
},
},
Note: you can also use the 'beforeDestroy' hook and it should work equally fine.
This is the sequence of events occurring:
serverCall() dispatches an update, modifying $store.state.pages.
content (which depends on $store.state.pages) recomputes, but $route.params.id is equal to the ID of the page just deleted, so Array.prototype.find() returns undefined.
subcontent (which depends on content) recomputes, and dereferences the undefined.
One solution is to check for the undefined before dereferencing:
export default {
computed: {
content() {...},
subcontent() {
return this.content?.subcontent
👆
// OR
return this.content && this.content.subcontent
}
}
}
demo
I have a navbar where I only show certain menu items based off the user's role.
Here is the HTML (I removed all the menu items except one to simplify):
<v-speed-dial class="speed-dial-container contextual-text-menu" v-if="user && user.emailVerified" fixed top right
direction="bottom" transition="slide-y-transition">
<v-icon v-if="checkAuthorization(['superAdmin', 'admin', 'pastor', 'member']) === true" class="mt-2"
#click="sendComponent({ component: 'Dashboard' })">$admin</v-icon>
</v-speed-dial>
My method:
async checkAuthorization(permissions) {
if (permissions.length > 0) {
const temp = await this.$store.dispatch('UserData/isAuthorized', permissions)
return temp
}
},
Vuex store:
isAuthorized({
state
}, permissions) {
const userRoles = state.roles
if (permissions && userRoles) {
const found = userRoles.some(r => permissions.includes(r))
return found
}
},
All of my console logs show the correct values but the HTML is not responding accordingly.
Example: in this line of code checkAuthorization(['superAdmin', 'admin', 'pastor', 'member']) === true I added 'member' and I am logged in as a user that ONLY has the 'member' role. When looking through the console logs everything returns true so I should see this menu item but it does not show.
As someone pointed out in the comments, checkAuthorization is an async function and will return a Promise, so you cannot check for promise === true.
That aside, I would change isAuthorized to be a vuex getter and not an action, e.g.
getters: {
// ...
isAuthorized: (state) => (permissions) => {
if (permissions && state.roles) {
return state.roles.some(r => permissions.includes(r))
}
return false;
}
}
And update checkAuthorization to not return a promise e.g.
function checkAuthorization(permissions) {
if (permissions.length > 0) {
return this.$store.getters.isAuthorized(permissions);
}
return false;
}
What I usually do :
I add another user state as Unknown and make it the default state.
In main main.js (or main.ts) I call state.initialize(), which determines user's state.
And, the key thing is to use navigation guards. Instead of checking routing guards on router-link (or url or anywhere in this step), you should define it in the router. There is a router.beforeEach function you can use, so that you can check if user is authorized to use that route, and redirect the user to 401 page if the user don't have permission.
https://router.vuejs.org/guide/advanced/navigation-guards.html
Question
Using Vue.js, how can i return 404 error in dynamic route without change url?
Part of my route.js (everything ok)
{
path: '/work/:id',
name: 'work',
component: () => import( './views/Work.vue' )
},
{
path: '*',
name: 'NotFound',
component: () => import( './views/NotFound.vue' )
}
Part of Work.vue component. Where i check if route param are in my static json, if not, open NotFound component before route enter
beforeRouteEnter (to, from, next) {
next(vm => {
vm.item = json.find(item => item.id == vm.$route.params.id)
if(!vm.item) {
next({name: NotFound})
}
})
}
The problem
When i try site.com/work/non-existent-id, the component "NotFound" open, but the url goes from site.com/work/non-existent-id to site.com
What I espect
site.com/work/non-existent-id open component "NotFound" and the url stays at site.com/work/non-existent-id
Example
https://v2.vuejs.org/v2/guide/something - return 404 error and stay in url
Routers are designed to do exactly what you are wanting them to not do. If you want to display some content on any route in your application, I would recommend creating a new component and maybe firing off an event to show whatever content you want.
Personally, I would think re-routing would be the way to go because this will allow the user to hit the back button in the browser to get back to where they came from, but I guess that all depends on your specific use case here.
I'm still using vue router3 and works for me without changing the url.
for axios.
beforeRouteEnter(to,from,next) {
axios
.get(`http://localhost:5000/api/product/${to.params.id}`)
.then((res) => {
next((vm) => {
vm.product = res.data;
next();
});
})
.catch((error) => {
next({ name: 'notFound', params: [to.path]});
});
}
I am working on a project and using Vue.js for the frontend. I have following code in the main.js file.
new Vue({ // eslint-disable-line no-new
//el: '#app',
router,
data () {
return {
friends: []
}
},
methods: {
getFriends: function () {
return this.friends;
}
},
created: function () {
this.$http.get('/user/' + this.getUserIDCookie('userID') +
'/friends').then(function (response) {
this.friends = response.data;
});
},
components: {
'nav-bar': require('./components/Navigation.vue')
},
template: `
<div id="app">
<nav-bar></nav-bar>
<router-view class="router-view"></router-view>
</div>`
}).$mount('#app');
In one of the pages(for ex. when the page is redirected to localhost/#/user/1/details, I am retrieving the friends' list from main.js like below:
<script type="text/babel">
export default {
name: 'profile',
data: function () {
return {
user: {},
friends: []
}
},
methods: {
// Some methods
},
created: function () {
this.friends = this.$root.getFriends();
}
}
</script>
The problem arises when I refresh the current page. After page refresh, this.friends is null/undefined because this.$root.getFriends() is returning null/undefined. I can move it to user component, but I want to keep it in main.js so that GET call is used once and data will be available to the whole application.
Any input regarding how to solve this issue would be great. I am using Vue 2.0.1
Really, what you want to do, is pass the data the component needs as props.
The dirt simple easiest way to do it is this.
<router-view class="router-view" :friends="friends"></router-view>
And in your profile component,
export default {
props:["friends"],
name: 'profile',
data: function () {
return {
user: {},
friends: []
}
},
methods: {
// Some methods
}
}
If you want to get more sophisticated, the later versions of VueRouter allow you to pass properties to routes in several ways.
Finally, there's always Vuex or some other state management tool if your application gets complex enough.
The problem is that when you refresh the page, the whole app reloads, which includes the get, which is asynchronous. The router figures out that it needs to render details, so that component loads, and calls getFriends, but the asynchronous get hasn't finished.
You could work around this by saving and pulling the Promise from the get, but Bert's answer is correct: the Vue Way is to send data as props, not to have children pull it from parents.
I would like to add a loading animation to my website since it's loading quite a bit when entering the website. It is built in ReactJS & NodeJS, so I need to know specifically with ReactJS how to add a loading animation when initially entering the site and also when there is any loading time when rendering a new component.
So is there a way to let people on my website already, although it's not fully loaded, so I can add a loading page with some CSS3 animation as a loading screen.
The question is not really how to make a loading animation. It's more about how to integrate it into ReactJS.
Thank you very much.
Since ReactJS virtual DOM is pretty fast, I assume the biggest load time is due to asynchronous calls. You might be running async code in one of the React lifecycle event (e.g. componentWillMount).
Your application looks empty in the time that it takes for the HTTP call. To create a loader you need to keep the state of your async code.
Example without using Redux
We will have three different states in our app:
REQUEST: while the data is requested but has not loaded yet.
SUCCESS: The data returned successfully. No error occurred.
FAILURE: The async code failed with an error.
While we are in the request state we need to render the spinner. Once the data is back from the server, we change the state of the app to SUCCESS which trigger the component re-render, in which we render the listings.
import React from 'react'
import axios from 'axios'
const REQUEST = 'REQUEST'
const SUCCESS = 'SUCCESS'
const FAILURE = 'FAILURE'
export default class Listings extends React.Component {
constructor(props) {
super(props)
this.state = {status: REQUEST, listings: []}
}
componentDidMount() {
axios.get('/api/listing/12345')
.then(function (response) {
this.setState({listing: response.payload, status: SUCCESS})
})
.catch(function (error) {
this.setState({listing: [], status: FAILURE})
})
}
renderSpinner() {
return ('Loading...')
}
renderListing(listing, idx) {
return (
<div key={idx}>
{listing.name}
</div>
)
}
renderListings() {
return this.state.listing.map(this.renderListing)
}
render() {
return this.state.status == REQUEST ? this.renderSpinner() : this.renderListings()
}
}
Example using Redux
You can pretty much do the similar thing using Redux and Thunk middleware.
Thunk middleware allows us to send actions that are functions. Therefore, it allows us to run an async code. Here we are doing the same thing that we did in the previous example: we keep track of the state of asynchronous code.
export default function promiseMiddleware() {
return (next) => (action) => {
const {promise, type, ...rest} = action
if (!promise) return next(action)
const REQUEST = type + '_REQUEST'
const SUCCESS = type + '_SUCCESS'
const FAILURE = type + '_FAILURE'
next({...rest, type: REQUEST})
return promise
.then(result => {
next({...rest, result, type: SUCCESS})
return true
})
.catch(error => {
if (DEBUG) {
console.error(error)
console.log(error.stack)
}
next({...rest, error, type: FAILURE})
return false
})
}
}