Where should route meta data be loaded in a Vue app? - vue.js

I'm in the process of setting up a VueJs SPA. I'm using vue-router and I'm trying to find the best solution to the following problem. I have a series of routes. Each of which needs to call an API to get the meta data for the given ID.
/industry/:id/overview
/industry/:id/top-stories
/industry/:id/top-tweets
/brand/:id/overview
/brand/:id/top-stories
/brand/:id/top-tweets
I've been looking at using created or beforeRouteEnter/beforeRouteUpdate and I'm a bit lost. Ideally, I would only fetch new data when a new /industry/:id is reached, not when navigating between pages within the same ID. Also, I'd like to avoid having to define the fetch to grab data in every page component. Also don't want to over complicate this, so my question is, Is there a standard method for tackling this issue?
Clarification:
When I say meta here, I mean data returned from an API about the given industry or brand which I pull using the ID in the route. The api call includes the name of the industry/brand which I want to have on page as soon as the page is presented to the user.

I have something similar. I tackle this using the following approach:
I use the same component for all /industry/:id Vue likes to reuse components wherever it can so if two routes (for example /industry/:id/overview and /industry/:id/top-stories) are using the same component it will stay the same.
What does change, however, is the route meta. So if you add a page key to the meta object in the route objects, and probably add a computed property called page that return this.$route.meta.page, you can use v-if attributes to conditionally render any component. So you might have something like <div v-if="page === 'overview'"></div><div v-else-if="page==='top-stories'"></div>
What this allows you to do is fetch all the data from the API during created or mounted lifecycle and store it as the state. Since the route change doesn't reload the component the state stays the same.
Here is a code example
// router.js
const Project = () =>
import(/* webpackChunkName: "projects" */ "./views/projects/_id");
export default new Router({
mode: "history",
routes: [
{
path: "/projects/:project_id/views",
name: "ViewProject",
component: Project,
meta: {
page: "views",
}
},
{
path: "/projects/:project_id/export",
name: "ExportProject",
component: Project,
meta: {
page: "exports"
}
},
{
path: "/projects/:project_id/recommendations",
name: "ProjectRecommendations",
component: Project,
meta: {
page: "recommendations"
}
},
]
});
And here is the template
<template>
<div v-if="project">
<h1>{{ project.name }}</h1>
<router-link :to="/project/someid/views">Views</router-link>
<router-link :to="/project/someid/exports">Exports</router-link>
<router-link :to="/project/someid/recommendations">Recommendations</router-link>
<ul v-if="page==='views">
<li v-for="(view, i) in project.views" :key="i">{{ views }}</div>
</ul>
<ul v-else-if="page==='exports">
<li v-for="(export, i) in project.exports" :key="i">{{ export }}</div>
</ul>
<ul v-else-if="page==='recommendations">
<li v-for="(recommendation, i) in project.recommendations" :key="i">{{ recommendation }}</div>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
project: null
}
},
computed: {
page() {
return this.$route.meta.page;
}
},
mounted() {
this.getProject()
},
methods: {
getProject() {
axios
.get(`/projects/someid`)
.then(res => this.project = res.data)
}
}
}
</script>

Related

VueRouter does not receive meta property in router-link tag

I'm trying to pass meta fields via <router-link> tag. Like this:
<router-link :to="{path: '/inlineMeta', meta: {title: 'Inline meta'}}">
Inline meta
</router-link>
I want to achieve this because I can dynamically pass metas from a backend framework directly on the tag itself. Declaring routes option in a JS file simply does not have that power.
Theoretically, any object passed via to prop should be pushed to router stack right? But in this case it doesn't seem like so.
If I declare meta in routes option, it definitely works. There is no doubt about that.
I wonder if it's possible to do so, and how would I do that?
A small fiddle to illustrate the problem. Click on URLs and notice the console. I can't get StackOverflow snippet to work properly.
JSFiddle
Vue.use(VueRouter);
const cComponent = {
data() {
return {
fetchedData: null
}
},
template: `<div>The meta is: {{$route.meta.title}}</div>`,
}
const routes = [{
path: "/inlineMeta",
component: cComponent,
},
{
path: "/routeMeta",
meta: {
title: 'Meta declared in routes'
},
component: cComponent,
}
]
const router = new VueRouter({
mode: "history",
routes,
})
router.beforeEach((to, from, next) => {
console.log("The meta title is : " + to.meta.title);
})
const app = new Vue({
el: "#app",
router,
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.14/dist/vue.js"></script>
<script src=" https://unpkg.com/vue-router#3.5.1/dist/vue-router.js"></script>
<div id="app">
<router-link :to="{path: '/inlineMeta', meta: {title: 'Inline meta'}}">Inline meta</router-link>
<router-link :to="{path: '/routeMeta'}">Meta declared in routes</router-link>
<router-view></router-view>
</div>
If you want to pass the parameters between two components, use props.
v-bind:to in <route-link> is just used for route match, so you can't use it to set the value of meta.
If you want to set the meta without declaring it in route, i think using "this.$route.meta.title="Inline meta" " in js.
like using the click event:
<li #click="setMeta">
<router-link :to="{path: '/inlineMeta'}">
Inline meta
</router-link>
</li>
and in js:
method:{
setMeta(){
this.$route.meta.title="Inline meta";
}
}
you can also use mounted() in the new component you just routed to like :
mounted(){
this.$route.meta.title="Inline meta"
}
and meta will be modified after reloading
This may work though it may looks not that elegant.

Vue.js 2 - Sharing XHR Data between Views WITHOUT using Vuex?

I'm teaching myself Vue.js 2. My task is to pull a list of posts from the Hacker News API. Upon clicking a list post, a new view is to display some of the data from that specific post.
I'm having a very tough time understanding how to get the REST API data populated in the 2nd view upon routing to the 2nd view, from the 1st view.
(I'm using vue-router and vue-resource(?), and not Vuex (it's such a small application).)
Below are the Item.vue and List.vue. From the List.vue, I'm trying to route to the Item.vue by clicking on a list item. For example, click on a list item called "Guy Has Tough Time with Vue", then open a 2nd view to display a title, score, URL, and comments of the post "Guy Has Tough Time with Vue".
List.vue (creates list, XHR request)
<template>
<div class="list-container">
<h1>List Vue</h1>
<ul class="item-list" v-for="(item, index) in this.items">
<li>
<router-link class="list-item" :to="/views">
{{index + 1}}. {{item.title}}
<div class="points">
Points: {{item.points}}
</div>
</router-link>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'List',
props:[]
data(){
return {
items: []
}
},
mounted: function(){
console.log("created");
this.fetchList();
},
methods: {
fetchList(){
this.$http.get('http://hn.algolia.com/api/v1/search?query=javascript&hitsPerPage=25').then((response) => {
this.items = response.data.hits;
})
}
}
}
Item.vue (Receives item-specific data from List.vue)
<template>
<video id="bgvid" playsinline autoplay loop>
<source src="./src/assets/rad.mp4" type="video/mp4">
<div class="item-container">
<h1>Item Vue</h1>
<div class="post-title"></div>
<div class="post-score"></div>
<div class="post-url"></div>
<ul class="post-comments">
<li class="sngl-comment">
</li>
</ul>
</div>
</video>
</template>
<script>
export default {
name: 'Item',
data(){
return {
item: {}
}
},
mounted: function(){
console.log("created");
},
methods: {
}
}
</script>
Thanks in advance!
The problem is that the first view isn't a direct descendant of the second view so you can't pass data to it through props. I actually wouldn't be using vuex for this, instead I would pass an id through the route for the specific list item and fetch that individual item by the id, as an example:
const View1 = {
template: `<div>
<ul>
<li v-for="item in list"><router-link :to="'view2/'+item.id">{{item.name}}</router-link></li>
</ul>
</div>`,
data() {
return {
list: [{
id: 1,
name: 'foo'
}, {
id: 2,
name: 'bar'
}, {
id: 3,
name: 'baz'
}]
}
}
}
const View2 = {
template: `<div>Fetch item {{id}} <br /><router-link to="/">back</router-link></div>`,
created(){
console.log('Fetch data for item '+ this.id);
},
computed:{
id(){
return this.$route.params.id
}
}
}
const routes = [{
path: '/',
component: View1
}, {
path: '/view2/:id',
component: View2
}]
const router = new VueRouter({
routes // short for `routes: routes`
})
var app = new Vue({
router
}).$mount('#app')
Here, I've set the route on View2 to be: view2/:id (see: Dynamic route matching), now in the View2 component I can access that id via this.$route.params.id (which I've put in a computed). Once I've got that id I can simply make an ajax request for the data for the specific item.
And here's the JSFiddle: https://jsfiddle.net/craig_h_411/3hwr6mcd/
Using Vuex
If you are unable to retrieve a record by the specific id for some reason and you don't want to duplicate the call, you will need to share state between non-descendant components and that's where Vuex comes in.
There is a misconception with vuex that it's complicated, but if you only want to share state between a couple of components, it's really quite simple (and it's less than 10kb in size) so there really isn't much use trying to avoid it, all you need to add to your project is:
store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = {
state:{
item: {}
},
mutations:{
setItem(state, value){
state.item = value;
}
}
}
export default new Vuex.Store(store)
Then in your app.js (or wherever you have you main Vue instance) you would do something like:
import Vue from 'vue'
import store from './store'
import App from './components/App.vue'
new Vue({
store, // this will be available in components via this.$store
render: h => h(App)
}).$mount('#app')
You will also have to make a few changes, such as committing the state before pushing to the route, adding a computed to get the state from the store and removing the id from the route as it's no longer needed.
I've updated the JSFiddle to show you how that would work: https://jsfiddle.net/craig_h_411/bvswc0kb/

How to pass data from one view to another with the vue-router

When using the vue-router with .vue files, there is no documented way to pass data from one view/component to another.
Let's take the following setup...
main.js:
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
let routes = [
{
path: '/page1',
component: require('./views/Posts.vue')
},
{
path: '/page2',
component: require('./views/EditPost.vue')
}
];
let router = new VueRouter({
routes
});
new Vue({
el: '#main',
router
});
Posts.vue:
<template>
<div>
Posts.vue passing the ID to EditPost.vue: {{ postId }}
</div>
</template>
<script>
export default {
data() {
return {
allPostsHere: // Whatever...
}
}
}
</script>
EditPost.vue:
<template>
<div>
EditPost.vue received ID from Posts.vue: {{ receivedId }}
</div>
</template>
<script>
export default {
data() {
return {
receivedId: // This is where I need the ID from Posts.vue
}
}
}
</script>
Please note: It is not possible to receive the ID directly from the EditPost.vue, because it has to be selected from Posts.vue.
Question: How can I pass the ID from one view/component to the other?
A route can only be accessed via a URL and a URL has to be something user can type into the URL bar, therefore to pass a variable from one view component to another you have to use route params.
I assume you have a list of posts in Posts component and want to change page to edit a specific post in EditPost component.
The most basic setup would be to add a link in the post list to redirect to the edit page:
<div v-for="post in posts">
{{ post.title }}
<router-link :to="'/post/' + post.id + '/edit'">Edit</router-link>
</div>
Your routes would look like this:
[
{
path: '/posts',
component: require('./views/Posts.vue'),
},
{
path: '/post/:postId/edit',
component: require('./views/EditPost.vue'),
props: true,
},
]
The props configuration option is just to inform the Router to convert route params to component props. For more information see Passing props to route components.
Then in EditPost you'd accept the id and fetch the post from server.
export default {
props: ['postId'],
data() {
return {
post: null,
}
},
mounted() {
this.fetchPost();
},
methods: {
fetchPost() {
axios.get('/api/post/' + this.postId)
.then(response => this.post = response.data);
},
},
}
After the request has been completed, EditPost has its own copy which it can further process.
Note, that on every post edit and every time you enter the post list, you'll make a request to the server which in some cases may be unnecessary, because all needed information is already in the post list and doesn't change between requests. If you want to improve performance in such cases, I'd advise integrating Vuex into your app.
If you decide to do so, the components would look very similar, except instead of fetching the post to edit via an HTTP request, you'd retrieve it from the Vuex store. See Vuex documentation for more information.
if you don't want the params appear in the URL bar,you can use window.sessionStorage, window.localStorage or vuex.
Before you leave the view, set your parameters and get it after entering the new view.
You can use a prop on the <router-view :my-id="parentStoredId"></router-view> to pass down data present in the app.vue (main component). To change the parent data you need to emit a custom event comprising the value, from the childs (Posts.vue, EditPost.vue).
Another way is the Non Parent-Child Communication.
The way I prefer is Vuex. Even if it require you to learn the usage, it will repay back when the app grows.

Navigating vuejs SPA via routes that share component does not refresh component data as expected

I have a couple routes in my vuejs SPA that I have set up using vue-router:
/create/feedback
/edit/feedback/66a0660662674061b84e8ea2fface0e4
The component for each route is the same form with a bit of smarts to change form values based on the absence or present of the ID in the route (feedbackID, in my example).
I notice that when I click from the edit route to the create route, the data in my form does not clear.
Below is the gist of my route file
import FeedbackFormView from './components/FeedbackForm.vue'
// Routes
const routes = [
{
path: '/create/feedback',
component: FeedbackFormView,
name: 'FeedbackCreate',
meta: {
description: 'Create Feedback',
}
},
{
path: '/edit/feedback/:feedbackId',
component: FeedbackFormView,
name: 'FeedbackEdit',
meta: {
description: 'Edit Feedback Form'
},
props: true
}
]
export default routes
Below is the gist of my component
<template lang="html">
<div>
<form>
<input v-model="model.someProperty">
</form>
</div>
</template>
<script>
export default {
data() => ({model: {someProperty:''}}),
props: ['feedbackId'],
created() => {
if (!this.$props['feedbackId']) {
return;
}
// otherwise do ajax call and populate model
// ... details omitted
}
}
</script>
However, if I modify my component as follows, everything works as expected
<template lang="html">
<div>
<form>
<input v-model="model.someProperty">
</form>
</div>
</template>
<script>
export default {
data() => ({model: {someProperty:''}}),
props: ['feedbackId'],
created() => {
if (!this.$props['feedbackId']) {
return;
}
// otherwise do ajax call and populate model
// ... details omitted
},
watch: {
'$route' (to, from) {
if (to.path === '/create/feedback') {
this.model = {}
}
}
}
}
</script>
Why is this? Why do I need watch?
I would have though that changing routes would be sufficient as the purpose of routing is to mimic the semantic behavior of page navigation
You have same component for different routes, when you go to edit route from the create route component is already created and mounted so the state of the component doesn't clear up.
Your component can listen to route changes using $router provided by vue-router every time the route changes the watcher is called.
For those who come this later, the following answer addresses the issue I was facing:
Vue-Router: view returning to login page after page refresh

How to get route params to work with vue-router and vuex

I'm trying to pass data from a component to $route.params.post but somewhere along the line it's failing and I'm not sure how to get it to work.
In my component I'm using router-link to go to a specific path in my routes file but it's not routing to the specified component.
// Component.vue
<router-link :to="{ path: 'replies', params: { post: postId }}">
<div class="button is-light is-small has-replies" #click=" postId = thread.no ">Replies</div>
//clicking replies will push the thread number to data and load it into the params
</router-link>
export default {
data () {
return {
postId: null
}
}
}
// ./routes/index.js
import Replies from '../components/Replies'
routes: [
{ path: '/', component: Frontpage },
{ path: '/replies/:post', component: Replies }
]
Clicking the button should open the Replies component with the route looking like /replies/# but it's just loading a blank page and ignoring the component entirely. I'm importing vuex-router-sync on my main.js, but I can't tell if that's the issue, but I'm aware it very well may be since I'm not entirely sure I'm using vuex-router-sync correctly.
You can try it like following, as postId is not a URL parameter, but part of the URL itself:
<router-link :to="'replies/'+ postId'">
<div class="button is-light is-small has-replies" #click=" postId = thread.no ">Replies</div>
//clicking replies will push the thread number to data and load it into the params
</router-link>