I use vue-spinner and I have a project on codesandbox.
Main page is Home.vue with links on other components:
<router-link to="/">Home</router-link>
<router-link to="/helloworld">Hello World</router-link>
<router-link to="/bigimg">Big Img</router-link>
main.js:
import Vue from "vue";
import App from "./App";
import router from "./router";
Vue.config.productionTip = false;
/* eslint-disable no-new */
new Vue({
el: "#app",
router,
components: { App },
template: "<App/>"
});
router.beforeEach((to, from, next) => {
//loading = true;
next();
});
I want to make loader like on fishtripr.com website (vue website).
Question: How I can make vue website loader and How can make loader (vue-spinner) when moving from page to page (like NProgress.start() & NProgress.done() )?
In my opinion the best way is to have a data property loading that is set to true (you can do this with a mixin) and then once the server has responded with the actual data you dismiss the loader by setting this.loading = false. I don't think you can put this logic inside the global router.beforeEach because it does not have access to the component. You would need to put the logic in the instance beforeRouteUpdate, which i don't think is ideal.
You need a mix of router.beforeEach and of manually updating the variable as Justin mentioned. Probably something like vuex makes sense in this case since many components will be changing the data.
In your vuex store
mutations: {
resetLoading(state){
state.isLoading = true
},
// This needs to be called on each component individually
finishedLoading(state){
state.isLoading = false
},
..
Something like this for the router
router.beforeEach((to, from, next) => {
store.commit("resetLoading")
next()
})
And then in your component
..
created(){
var self = this
this.$store.dispatch("fetchData").then(function(){
self.$store.commit("finishedLoading")
})
},
..
Related
First I'm making the GET request and creating the Vue instance:
main.js
import Vue from 'vue';
import App from './App.vue';
let getData = () => { // currently unused - need to get promise return value into components
let url = '';
return fetch(url)
.then(response => {
return response.json();
})
.then(data => {
return data;
}
}
new Vue({
render: h => h(App)
}).$mount('#app');
Then I'm creating the top-level component that attaches to my index.html:
App.Vue
<template>
<div id="app">
<Badge></Badge>
<Card></Card>
</div>
</template>
<script>
import Badge from './components/Badge.vue';
import Card from './components/Card.vue';
export default {
name: 'app',
//props: ['data'], // unused, possible solution?
components: {
Badge,
Card
}
}
</script>
And then Badge.vue and Card.vue are both components that (need to) display different data from the fetch in main.js
I've tried using props to pass data from main.js -> App.vue -> Card.vue but I wasn't able to figure out how to do that with this code in main.js:
new Vue({
render: h => h(App)
}).$mount('#app');
I suspect render may be my problem; I'm using it from an example I followed in a tutorial - I'm pretty sure it's for the live webserver I'm using when I run vue-cli-service serve so maybe this is as simple as doing things differently to send props to App.vue
However, it seems like passing data through props this way is a bad idea and that I should be doing things differently, I just don't know what that would be, so I'm hoping there may be a more elegant solution. I'm only making a single ajax call which hopefully simplifies things, but it seems like using props this way can get too messy if I start adding more components.
You need to move getData to the App.vue
App.vue:
<template>
<div id="app">
<Badge></Badge>
<Card></Card>
</div>
</template>
<script>
import Badge from './components/Badge.vue';
import Card from './components/Card.vue';
export default {
name: 'app',
data: () => ({ // To store data in App.vue
someData: []
}),
components: {
Badge,
Card
},
created: {
this.getData(); // Call getData function when the component is created
},
methods: {
getData() {
// Making reference to component instance since we can't access `this` inside arrow function
const self = this;
let url = "";
fetch(url)
.then(response => {
return response.json();
})
.then(data => {
self.someData = data;
}
}
}
}
</script>
If you need to share data between multiple components, then i highly recommend you to use Vuex instead.
I'm building a Movie website to practice on VueJS. During app initialization, I get a list of movie genres from 3rd-party API. Since this list is needed in several components of the app, I manage and store it via Vuex, like so:
main.js:
new Vue({
router,
store,
vuetify,
render: h => h(App),
created () {
this.$store.dispatch('getGenreList')
}
}).$mount('#app')
Vuex's index.js:
export default new Vuex.Store({
state: {
genres: []
},
mutations: {
setGenreList (state, payload) {
state.genres = payload
}
},
actions: {
async getGenreList ({ commit }) {
try {
const response = await api.getGenreList() // axios call defined in api.js
commit('setGenreList', response)
} catch (error) {
console.log(error)
}
}
}
})
Now, in my Home view, I want to retrieve a list of movies for each genres, something like this:
Home.vue:
<script>
import { mapState } from 'vuex'
import api from '../api/api'
export default {
name: 'home',
data () {
return {
movies: null
}
},
computed: {
...mapState({
sections: state => state.genres
})
},
async mounted () {
const moviesArray = await Promise.all(
this.sections.map(section => {
return api.getMoviesByGenre(section.id)
})
)
this.movies = moviesArray
}
}
</script>
The issue here is that, on initial load, sections===[] since genres list hasn't been loaded yet. If I navigate to another view and come back, sections holds an array of genres objects as expected.
Question: How can I properly wait on sections to be loaded with genres? (since the getGenreList action isn't called from that component, I can't use this method)
I was thinking in implementing the movie list retrieval in a Watcher on sections instead of in mounted() but not sure if it's the right approach.
Yep, it is right approach, that's what watchers are for.
But if you only can... try to do actions like this one inside one component family. (parent passing props to children, controlling it);
You can read this article, about vuex - https://markus.oberlehner.net/blog/should-i-store-this-data-in-vuex/.
It will maybe clarify this idea. Just simply don't store in vuex everything, cause sometimes it' does not make sense
https://v2.vuejs.org/v2/api/#watch - for this one preferably you should use immedaite flag on watcher and delete mounted. Watcher with immedaite flag is kinda, watcher + created at once
On my main page I have dropdowns that show v-show=show by clicking on the link #click = "show=!show" and I want to set show=false when I change the route. Please advise me on how to realize this thing.
Setup a watcher on the $route in your component like this:
watch:{
$route (to, from){
this.show = false;
}
}
This observes for route changes and when changed ,sets show to false
If you are using v2.2.0 then there is one more option available to detect changes in $routes.
To react to params changes in the same component, you can watch the $route object:
const User = {
template: '...',
watch: {
'$route' (to, from) {
// react to route changes...
}
}
}
Or, use the beforeRouteUpdate guard introduced in 2.2:
const User = {
template: '...',
beforeRouteUpdate (to, from, next) {
// react to route changes...
// don't forget to call next()
}
}
Reference: https://router.vuejs.org/en/essentials/dynamic-matching.html
Just in case anyone is looking for how to do it in Typescript, here is the solution:
#Watch('$route', { immediate: true, deep: true })
onUrlChange(newVal: Route) {
// Some action
}
And yes as mentioned by #Coops below, please do not forget to include :
import { Watch } from 'vue-property-decorator';
Edit:
Alcalyn made a very good point of using Route type instead of using any:
import { Watch } from 'vue-property-decorator';
import { Route } from 'vue-router';
Watcher with the deep option didn't work for me.
Instead, I use updated() lifecycle hook which gets executed everytime the component's data changes.
Just use it like you do with mounted().
mounted() {
/* to be executed when mounted */
},
updated() {
console.log(this.$route)
}
For your reference, visit the documentation.
UPDATE
As stated by #CHANist, router.listen no longer works, I don't know from which version it stopped working, but the good news (as also stated by #CHANist) is we can use:
this.$router.history.listen((newLocation) => {console.log(newLocation);})
OLD Response
The above responses are the better, but just for completeness, when you are in a component you can access the history object inside the VueRouter with:
this.$router.history.
That means we can listen to changes with:
this.$router.listen((newLocation) => {console.log(newLocation);})
I think this is mainly useful when used along with this.$router.currentRoute.path
You can check what I am talking about placing a debugger
instruction in your code and begin playing with the Chrome DevTools Console.
import { useRouter } from "vue-router";
const router = useRouter();
router.afterEach((to, from) => { });
Using Vue3 and the composition API you can do
<script setup lang="ts">
import { watch } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
// do a `console.log(route)` to see route attributes (fullPath, hash, params, path...)
watch(
() => route.fullPath,
async () => {
console.log("route fullPath updated", route.fullPath);
}
);
</script>
References and examples here: https://router.vuejs.org/guide/advanced/composition-api.html#vue-router-and-the-composition-api
Another solution for typescript user:
import Vue from "vue";
import Component from "vue-class-component";
#Component({
beforeRouteLeave(to, from, next) {
// incase if you want to access `this`
// const self = this as any;
next();
}
})
export default class ComponentName extends Vue {}
using Vue Router is an alternative way, use the beforeRouteLeave after methods in your component like this:
<template>
<button #click="ShowMethod">DisplayButton</button>
</template>
<script>
data() {
return { show: true };
},
methods: {
ShowMethod() {
this.show = false;
}
},
beforeRouteLeave(to, from, next) {
this.show = false;
next();
}
</script>
according to VueJs documentation, it's called Navigation Guards check the link below:
Navigation Guards
The leave guard is usually used to prevent the user from accidentally
leaving the route with unsaved edits. The navigation can be canceled
by calling
In-Component Guards:
beforeRouteEnter
beforeRouteUpdate
beforeRouteLeave
beforeRouteLeave(to, from, next) {
// called when the route that renders this component is about to
// be navigated away from.
// has access to `this` component instance.
}
look at the below link for more information:
In-Component Guards
you can use the beforeEach event which allows any function to occur when the route is changing, just don't forget to call the next() function to proceed next operation, basically it has the same job as the backend expressJS middleWare.
router.beforeEach((to, from, next) => {
store.commit('setError', null); //in this example on each route I'm removing the error noted from the old route
document.title = `${to.meta.title} | HartWork`; //on each route I'm adding a prefix to document title.
next(); //calling next to proceed next functions and operations
})
I hope you doing well,
in vue3 and script setup this work is too easy:
watch(route, () => { fetch()})
be careful you must import before
import { watch } from 'vue';
import { useRoute } from 'vue-router';
and define use route :
const route = useRoute()
I have a really interesting problem at hand.
I am currently working on a Admin-Interface for a headless CMS and I want to dynamically load the components for viewing the content of different parts.
I know that it is rather easy to accomplish wit React Router since v4, but I don't want to rely on Routes to handle this to keep the URL as clean as possible.
Here is what I am trying to do:
I want to render the basic layout of the UI (in which I load the
different sub-components when clicking the navigation-links)
Inside the async componentWillMount() function of this basic layout I want to import() said components using await import()
Ideally those should be stored to the state inside a property called this.state.mainComponents
I will then pass down the names of the components to the navigation component along with a function which will change the currently displayed component in the parents state
I tried several different approaches, but did not manage to get it to work until now.
This is what I have now:
async componentDidMount() {
const mainComponents = {
Dashboard: await import('../_mainComponents/Dashboard').then(
({ Dashboard }) => {
return Dashboard;
},
),
};
const componentNames = [];
for (let p in mainComponents) {
componentNames.push(p);
}
this.setState({
mainComponents: {
views: mainComponents,
names: componentNames,
},
currentComponent: mainComponents.Dashboard,
currentTitle: 'Dashboard',
});
}
I would really appreciate any help you could give me on this.
EDIT:
So according to the answer of #Mosè Raguzzini I implemented react-loadable again and tried to store the const inside my state like this:
const LoadableDashboard = Loadable({
loader: () => import('../_mainComponents/Dashboard'),
loading: Loading
});
const mainComponents = {
Dashboard: LoadableDashboard
};
And inside my constructor function I am trying to pass this Object into the state like this:
const componentNames = [];
for (let p in mainComponents) {
componentNames.push(p);
}
this.state = {
mainComponents: {
views: mainComponents,
names: componentNames,
},
currentComponent: null,
currentTitle: null,
loading: false,
};
It is not working when trying to output the Components stored inside the this.state.mainComponents.views object inside to the render function.
When using <Dashboard /> it is working as expected, but this is not what I am trying to accomplish here since I don't want to add every component to the render function in a massive switch ... case.
Does anybody of you have an idea how I can accomplish this?
I found a solution to my problem and wanted to share it with all of those who will come after me searching for an appropiate answer.
Here is what I did:
import Loadable from 'react-loadable';
import Loading from '../_helperComponents/Loading';
// Define the async-loadable components here and store them inside of the mainComponent object
const LoadableDashboard = Loadable({
loader: () => import('../_mainComponents/Dashboard'),
loading: Loading,
});
const LoadableUserOverview = Loadable({
loader: () => import('../_mainComponents/UserOverview'),
loading: Loading,
});
const mainComponents = {
Dashboard: <LoadableDashboard />,
User: <LoadableUserOverview />,
};
I do not store the components inside my state (it would have been possible but goes against the guidelines as I've found out by researching state even further) :D
With this object I can simply call the corresponding component by outputting it in the render() function like this:
render() {
return (
<div>
{mainComponents[this.state.currentComponent]}
</div>
);
}
I hope I could help someone with this.
Happy coding!
I suggest to take a look to react-loadable:
import Loadable from 'react-loadable';
import Loading from './my-loading-component';
const LoadableComponent = Loadable({
loader: () => import('./my-component'),
loading: Loading,
});
export default class App extends React.Component {
render() {
return <LoadableComponent/>;
}
}
It provides the feature you need in a clean style.
I have a pretty simple set of react components:
container that hooks into redux and handles the actions, store subscriptions, etc
list which displays a list of my items
new which is a form to add a new item to the list
I have some react-router routes as follows:
<Route name='products' path='products' handler={ProductsContainer}>
<Route name='productNew' path='new' handler={ProductNew} />
<DefaultRoute handler={ProductsList} />
</Route>
so that either the list or the form are shown but not both.
What I'd like to do is to have the application re-route back to the list once a new item has been successfully added.
My solution so far is to have a .then() after the async dispatch:
dispatch(actions.addProduct(product)
.then(this.transitionTo('products'))
)
Is this the correct way to do this or should I fire another action somehow to trigger the route change?
If you don't want to use a more complete solution like Redux Router, you can use Redux History Transitions which lets you write code like this:
export function login() {
return {
type: LOGGED_IN,
payload: {
userId: 123
}
meta: {
transition: (state, action) => ({
path: `/logged-in/${action.payload.userId}`,
query: {
some: 'queryParam'
},
state: {
some: 'state'
}
})
}
};
}
This is similar to what you suggested but a tiny bit more sophisticated. It still uses the same history library under the hood so it's compatible with React Router.
I ended up creating a super simple middleware that roughtly looks like that:
import history from "../routes/history";
export default store => next => action => {
if ( ! action.redirect ) return next(action);
history.replaceState(null, action.redirect);
}
So from there you just need to make sure that your successful actions have a redirect property. Also note, this middleware does not trigger next(). This is on purpose as a route transition should be the end of the action chain.
For those that are using a middleware API layer to abstract their usage of something like isomorphic-fetch, and also happen to be using redux-thunk, you can simply chain off your dispatch Promise inside of your actions, like so:
import { push } from 'react-router-redux';
const USER_ID = // imported from JWT;
function fetchUser(primaryKey, opts) {
// this prepares object for the API middleware
}
// this can be called from your container
export function updateUser(payload, redirectUrl) {
var opts = {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
};
return (dispatch) => {
return dispatch(fetchUser(USER_ID, opts))
.then((action) => {
if (action.type === ActionTypes.USER_SUCCESS) {
dispatch(push(redirectUrl));
}
});
};
}
This reduces the need for adding libraries into your code as suggested here, and also nicely co-locates your actions with their redirects as done in redux-history-transitions.
Here is what my store looks like:
import { createStore, applyMiddleware } from 'redux';
import rootReducer from '../reducers';
import thunk from 'redux-thunk';
import api from '../middleware/api';
import { routerMiddleware } from 'react-router-redux';
export default function configureStore(initialState, browserHistory) {
const store = createStore(
rootReducer,
initialState,
applyMiddleware(thunk, api, routerMiddleware(browserHistory))
);
return store;
}
I know I am little late in the party as react-navigation is already included in the react-native documentation, but still it can be useful for the user who have used/using Navigator api in their apps.
what I tried is little hackish, I am saving navigator instance in object as soon as renderScene happens-
renderScene(route, navigator) {
const Component = Routes[route.Name]
api.updateNavigator(navigator); //will allow us to access navigator anywhere within the app
return <Component route={route} navigator={navigator} data={route.data}/>
}
my api file is something lke this
'use strict';
//this file includes all my global functions
import React, {Component} from 'react';
import {Linking, Alert, NetInfo, Platform} from 'react-native';
var api = {
navigator,
isAndroid(){
return (Platform.OS === 'android');
},
updateNavigator(_navigator){
if(_navigator)
this.navigator = _navigator;
},
}
module.exports = api;
now in your actions you can simply call
api.navigator.push({Name:'routeName',
data:WHATEVER_YOU_WANTED_TO_PASS)
you just need to import your api from the module.
If you're using react-redux and react-router, then I think this link provides a great solution.
Here's the steps I used:
Pass in a react-router history prop to your component, either by rendering your component inside a react-router <Route/> component or by creating a Higher Order Component using withRouter.
Next, create the route you want to redirect to (I called mine to).
Third, call your redux action with both history and to.
Finally, when you want to redirect (e.g., when your redux action resolves), call history.push(to).