Vue.js on render populate content dynamically via vue.router params - javascript

AS title sates, I don't so much need a solution but I don't understand why I'm getting the undesired result;
running v2 vue.js
I have a vue component in a single component file.
Basically the vue should render data (currently being imported from "excerciseModules" this is in JSON format).
IT's dynamic so based on the url path it determines what to pull out of the json and then load it in the page, but the rendering is being done prior to this, and I'm unsure why. I've created other views that conceptually do the samething and they work fine. I dont understand why this is different.
I chose the way so I didn't have to create a ton of routes but could handle the logic in one view component (this one below).
Quesiton is why is the data loading empty (it's loading using the empty "TrainingModules" on first load, and thereafter it loads "old" data.
Example url path is "https...../module1" = page loads empty
NEXT
url path is "https..../module 2" = page loads module 1
NEXT
url path is "https..../module 1" = page loads module 2
//My route
{
path: '/excercises/:type',
name: 'excercises',
props: {
},
component: () => import( /* webpackChunkName: "about" */ '../views/training/Excercises.vue')
}
<template>
<div class="relatedTraining">
<div class="white section">
<div class="row">
<div class="col s12 l3" v-for="(item, index) in trainingModules" :key="index">
<div class="card">
<div class="card-content">
<span class="card-title"> {{ item.title }}</span>
<p>{{ item.excercise }}</p>
</div>
<div class="card-action">
<router-link class="" to="/Grip">Start</router-link>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
console.log('script');
let trainingModules; //when initialized this is empty, but I would expect it to not be when the vue is rendered due to the beforeMount() in the component options. What gives?
/* eslint-disable */
let init = (params) => {
console.log('init');
console.log(trainingModules);
trainingModules = excerciseModules[params.type];
//return trainingModules
}
import { getRandom, randomImage } from '../../js/functions';
import { excerciseModules } from '../excercises/excercises_content.js'; //placeholder for JSON
export default {
name: 'excercises',
components: {
},
props: {
},
methods: {
getRandom,
randomImage,
init
},
data() {
return {
trainingModules,
}
},
beforeMount(){
console.log('before mount');
init(this.$route.params);
},
updated(){
console.log('updated');
},
mounted() {
console.log('mounted');
//console.log(trainingModules);
}
}
</script>

I can't tell you why your code is not working because it is an incomplete example but I can walk you through a minimal working example that does what you are trying to accomplish.
The first thing you want to do, is to ensure your vue-router is configured correctly.
export default new Router({
mode: "history",
routes: [
{
path: "/",
component: Hello
},
{
path: "/dynamic/:type",
component: DynamicParam,
props: true
}
]
});
Here I have a route configured that has a dynamic route matching with a parameter, often called a slug, with the name type. By using the : before the slug in the path, I tell vue-router that I want it to be a route parameter. I also set props: true because that enables the slug value to be provided to my DynamicParam component as a prop. This is very convenient.
My DynamicParam component looks like this:
<template>
<div>
<ul>
<li v-for="t in things" :key="t">{{ t }}</li>
</ul>
</div>
</template>
<script>
const collectionOfThings = {
a: ["a1", "a2", "a3"],
b: ["b1", "b2"],
c: [],
};
export default {
props: ["type"],
data() {
return {
things: [],
};
},
watch: {
type: {
handler(t) {
this.things = collectionOfThings[t];
},
immediate: true,
},
},
};
</script>
As you can see, I have a prop that matches the name of the slug available on this component. Whenever the 'slug' in the url changes, so will my prop. In order to react to those changes, I setup a watcher to call some bit of code. This is where you can make your fetch/axios/xhr call to get real data. But since you are temporarily loading data from a JSON file, I'm doing something similar to you here. I assign this data to a data value on the component whenever the watcher detects a change (or the first time because I have immediate: true set.
I created a codesandbox with a working demo of this: https://codesandbox.io/s/vue-routing-example-forked-zesye
PS: You'll find people are more receptive and eager to help when a minimal example question is created to isolate the problematic code. You can read more about that here: https://stackoverflow.com/help/minimal-reproducible-example

Related

Pass the clicked element's data by using eventbus in Vue.js

Im pretty new to Vue.js, thank you for your understanding. Im setting up a Vue project where I want to show Patients and their data. I want to tell from the beginning that Im not planning to use Vuex :)
My project has 3 layers.
Home.vue file where I import the data (patients)
Next layer is Patients.vue component where I have a for loop and output all the patients. In this case, I am getting the patient Array by using props.
And the last layer is called ViewPatient.vue view. What I want to do here is showing more details of the clicked Patient. I want to inherit for example the name to make one more call to the endpoint to retrieve some observations of the patient. For example: endpoint/patient/(theName) <-- the name should come from the previous Patients.vue component.
I tried a lot of different approaches: eventbus, dynamic router and data-attrbutes.
Home.vue
<template>
<div class="container">
<keep-alive>
<Patients :PatientsData="PatientsData" />
</keep-alive>
</div>
</template>
<script>
// # is an alias to /src
import PatientsData from "../data/messages";
import Patients from "../components/Patients.vue";
export default {
name: "home",
data() {
return {
PatientsData: PatientsData
};
},
components: {
Patients
}
};
</script>
Patients.vue (component)
<template>
<div v-if="PatientsData.length > 0">
<div class="row row-eq-height">
<div v-for="PatientData in PatientsData" class="col-12 col-sm-6 col-md-3 mb-3" :key="PatientData.content" :data-id="PatientData.content" #click.prevent="passPatientData" >
<router-link to="/patient" >
<div class="col-12 patientsTiles p-4">
<p class="patientsName">
<span>Navn</span>
{{ PatientData.content }}
</p>
<p class="patientsCPR">
<span>CPR.nr</span>
{{ PatientData.subject }}
</p>
<p class="patientsAge">
<span>Alder</span>
{{PatientData.age}}
</p>
<i :class="['fa', 'fa-star', {important: PatientData.isImportant}]"></i>
</div>
</router-link>
</div>
</div>
</div>
</template>
<script>
import router from "../main";
import { eventBus } from "../main";
export default {
props: {
PatientsData: Array,
},
data(){
return{
patientName: ""
}
},
methods: {
passPatientData() {
this.patientName = this.PatientData;
alert(this.patientName);
eventBus.$emit("passPatientData", this.patientName);
}
}
};
</script>
ViewPatient.vue (view)
<template>
<div class="container">
<h1>The Patient detail</h1>
</div>
</template>
<script>
// # is an alias to /src
import { eventBus } from "../main";
export default {
props: {
// patientId:{
// type: String
// }
},
data() {
return {
selectedPatient : ""
};
},
created() {
eventBus.$on("passPatientData", data => {
this.selectedPatient = data;
// console.log("yeaah");
})}
}
</script>
IMO, the problem is lying on the passPatientData function.
this.PatientData is empty and I dont know how to pass the clicked element's data to the empty string (this.patientName), so I can emit it to the eventbus
passPatientData() {
this.patientName = this.PatientData;
alert(this.patientName);
eventBus.$emit("passPatientData", this.patientName);
}
Here is my approach which worked for me (few changes):
#bbsimonbb, thank you for the answer, but I´m not going to use that approach, because its a bit overkill when compared to my small task.
In Patients.vue while looping patients, I have modified the click event:
Im actually passing the single element that is being clicked, which solved med a lot of time.
<div v-for="PatientData in storedPatients" class="col-12 col-sm-6 col-md-3 mb-3" :data-id="PatientData.content" #click="passPatientData(PatientData)" >
Before:
#click="passPatientData"
After:
#click="passPatientData(PatientData)"
And then in my event bus im able to "work" with the data im passing:
methods: {
passPatientData(element) {
this.patientName = element.content;
alert(this.patientName);
eventBus.$emit("passPatientData", this.patientName);
}
}
The purpose is to pass the patientName to ViewPatient.vue file by using eventbus and call a new endpoint which looks like this: endpoint/patient/(patientName) . The result of the endpoint will then be the details of the single patient that has been clicked in patients.vue
It´s working. Hope it can be useful for others that is struggling with the same issue.
You've decided not to use Vuex. That doesn't mean you shouldn't use a store. The simplest store is just an object in the data of your root Vue, passed around via provide/inject. Put your shared data in there.
This is a much simpler pattern to get your head around than an event bus. You get the shared state right out of Vue components, and because it's just an object, that does nothing but store state, you will be more in control of what you have and how it's working.
// Home.vue
export default {
name: "home",
provide: { mySharedData: this.mySharedData },
data() {
return {
mySharedData: {
patientData: {}
}
};
},
created() {
fetch("http://patientData")
.then(response.json)
.then((patientData) => this.mySharedData.patientData = patientData)
}
...
// Then, in all other components of your app...
export default {
inject: ['mysharedData'],
...
}
This way, you'll be making full use of Vue reactivity to propagate your changes. You'll need to understand how Vue makes properties reactive. In particular, you can't just assign new props in mySharedData. The simplest way is to make sure all the props are there to start with. To do it on the fly, use Vue.set().

Firestore: Unable to fetch nested documents. Why?

I'm working on a Vue (with Vuex) app, with a firebase/firestore backend, and I'm having trouble with fetching documents referenced by other documents. Specifically, I have a recipes collection (together with users and comments collections as seen in the linked photo) collection, with each contained document having, among others, addedBy and comments fields. Both are id strings (the comments field being an array of ids) of the respective documents referenced. Now, I'm not sure if this is the best way of going about it, but coming from a MongoDB background, I thought it'd be possible fetch the details of these fields like we do with MongoDB.
I have had a couple of tries but nothing seems to work. An example of this is seen in the code snippets below.
Main Recipe Component/Container (I query the DB for a specific recipe document)
<template>
<div class="recipe-detail">
<loader v-if="isLoading" message="Loading Recipe" size="huge" />
<div v-else-if="!recipe" class="no-recipe">
No such recipe in DB
</div>
<div v-else class="comments-and-similar">
<div class="comments">
<h3 class="comments-title">Comments</h3>
<comment-form />
<comment-list :comment-list="recipe.comments" />
</div>
<div class="similar-recipes">
<similar-recipes />
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import Loader from "#/components/shared/Loader";
import PostedBy from "#/components/recipes/detail/PostedBy";
import CommentForm from "#/components/forms/CommentForm";
import CommentList from "#/components/recipes/detail/CommentList";
export default {
name: "recipe-detail",
components: {
Loader,
PostedBy,
CommentForm,
CommentList,
},
data() {
return {
recipeId: this.$route.params.recipeId,
fullPath: this.$route.fullPath
};
},
computed: {
...mapGetters(["isLoading"]),
...mapGetters({ recipe: "recipes/recipe" }),
},
watch: {
"$route.params.recipeId"(id) {
this.recipeId = id;
}
},
methods: {
...mapActions({ getRecipeById: "recipes/getRecipeById" })
},
created() {
if (!this.recipe || this.recipe.id !== this.recipeId) {
this.getRecipeById(this.recipeId);
}
}
};
</script>
<style lang="scss" scoped>
</style>
Comment List Component (Here, I receive the comment id list via props)
<template>
<section class="comments">
<div v-if="commentList.length === 0">Be the first to comment on recipe</div>
<template v-else v-for="comment in commentList">
<comment :comment-id="comment" :key="comment" />
</template>
</section>
</template>
<script>
import Comment from "./Comment";
export default {
name: "comment-list",
components: {
Comment
},
props: {
commentList: {
type: Array,
required: true
}
}
};
</script>
<style lang="scss" scoped>
</style>
Comment Component
<template>
<article>
<div v-if="isLoading">Loading comment...</div>
<div v-else>{{ JSON.stringify(comment) }}</div>
</article>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
name: "comment",
props: {
commentId: {
type: String,
required: true
}
},
computed: {
...mapGetters(["isLoading"]),
...mapGetters({ comment: "recipes/comment" })
},
methods: {
...mapActions({ getCommentById: "recipes/getCommentById" })
},
created() {
this.getCommentById(this.commentId);
}
};
</script>
Now, the Comment component is where I'm having trouble. I get each individual comment id and use it to query the DB, specifically the comments collection. I actually get the comment detail body from the DB, this query wont stop and results in an infinite loop. I have to comment out the method inside created life-cycle for it to stop. I tried the same approach for the addedBy field to query for the user and got the same issue. So, what I'm I doing wrong.
DB structure
PS: I did not feel the need to include the Vuex methods (actions) in order to reduce verbosity. They work just fine sending the corresponding queries.
It looks like you're sharing an isLoading flag between all your components.
I believe what is happening is this:
You try to load a comment and isLoading is set to true.
The component recipe-detail re-renders to show the Loading Recipe message. Note that this will destroy the comment-list.
When the comment finishes loading isLoading will be set back to false.
recipe-details will re-render again, this time showing the comment-list. This will create a new set of comment components, each of which will try to load their data again. This jumps us back to step 1.
On an unrelated note, it looks like your comment component is relying on a single comment being held in the store. This might be fine when there's only one comment but when there are multiple comments they'll all load at the same time and only one of them will ultimately end up in the store.

passing props data to component which i can only access with routes

My router file
import DomainAction from './components/domainaction/DomainAction.vue'
...
{ path: '/domainaction' , component: DomainAction },
...
router link File
...
<router-link to="/domainaction" tag="li" class="list-group-item "class-active="active" exact > Domain Action</router-link>
...
From another routes going to the domainaction Route like this
...
itemAction(action,data){
if(action=='Suspend'){
this.$router.push('/domainaction')
}
}
...
My Actual DomainAction component
<template>
.......
</template>
<script>
export default {
props:['data'],
data(){
return{
.........
</script>
I want to pass data props from itemAction function .
How do i achieve this ?
I'm new to vue js .Sorry if my question is not very complete.
Well if you just need to pass a property (a value) to the component, you just need to use data passing via the router's props: https://router.vuejs.org/guide/essentials/passing-props.html#function- mode.
And receive using this.$router within the component.
Alternatively you can use the Vuex for data passing.
https://vuex.vuejs.org/
So that you can make a communication between components outside this form that was informed, both components have to be parent and child, ie, 1 calls the other inside it, so yes you can pass the values, otherwise it should be used, or the data path via route, or via vuex which is the vue status manager.
There is also the following possibility, as requested in this question.
Passing props to Vue.js components instantiated by Vue-router
Note that sosmii is right about using a Vuex to share data across components.
But if you're looking into pass soma data as params using routes, you have to change a little bit how you're defining the components routes. Using a routers params you would be able to pass props, then you can push the route and add params. See example below.
And related question:
Passing props with programmatic navigation Vue.js
const DomainActionA = {
props: ['dataProp'],
template: '<div>A data: {{dataProp}}</div>'
}
const DomainActionB = {
props: ['dataProp'],
template: '<div>B data: {{dataProp}}</div>'
}
const routes = [
{
path: '/DomainActionA/:dataProp',
name: 'domainA',
component: DomainActionA,
props: true },
{
path: '/DomainActionB/:dataProp',
name: 'domainB',
component: DomainActionB,
props: true
}
]
const router = new VueRouter({
routes
})
const app = new Vue({
router,
methods: {
goToA() {
this.$router.push({name: 'domainA', params: {dataProp: "blah"}})
},
goToB() {
this.$router.push({name: 'domainB', params: {dataProp: "bleh"}})
}
}
}).$mount('#app')
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<h1>Hello Router!</h1>
<div>
<button #click="goToA()">Go A with params: { data: "blah" }</button>
<button #click="goToB()">Go B with params: { data: "bleh" }</button>
</div>
<div>
<h2>router view</h2>
<router-view></router-view>
</div>
</div>
you should use vuex
props are designed to pass variables from a parent component to child components,
so it is unusual to use it when you want to share data between pages.
an example of right usage of prop:
parent.vue
<child-component :data="data">
<child-component :data="anotherData">
so, if you want to share variables between pages, use vuex (store pattern) instead.
similar question:
https://stackoverflow.com/a/40955110/10440108

Basic Vue help: Accessing JS object values in component

I’ve been experimenting with vue.js and I'm having difficulty accessing JS object values in components when routing.
Using this repo to experiment, https://github.com/johnayeni/filter-app-vue-js, I'm just trying to replicate a basic a “product list” and “product description” app, but I can't get it working. The repo's homepage (the SearchPage.vue component) serves as the "product list," and I'm just trying to add the "product description" component to display only one item at a time.
I've added a "description page" component (calling it "item.vue") to allow a user to click on one of the languages/frameworks that will then route to item.vue to just display that specific object's associated information (item.name, item.logo, etc.), i.e., and not display any of the other languages.
Following some tutorials, here's what I've tried:
First, I added ids to the JS objects (found in data/data.js), i.e., id:'1'.
const data = [
{
id: '1',
name: 'vue js',
logo: 'http://... .png',
stack: [ 'framework', 'frontend', 'web', 'mobile' ],
},
{
id: '2',
name: 'react js',
logo: 'http://... .png',
stack: [ 'framework', 'frontend', 'web', 'mobile' ]
},
...
];
export default data
Then, I wrapped the item.name (in ItemCard.vue) in router-link tags:
<router-link :to="'/item/'+item.id"> {{ item.name}} </router-link>
I then added a new path in router/index.js:
{
path: './item/:id',
component: item,
props: true
}
But, when that router-link is clicked I can only access the ".id" (via $route.params.id), but I can't get .name or .logo. How do I access the other values (i.e. item.name, item.logo, etc.)? I have a feeling I'm going down the wrong track here.
Thank you so much for your help.
The only reason you have access the id because it's an url param: ./item/:id.
You have a couple options here, which depends on what you're trying to accomplish:
As suggested by #dziraf, you can use vuex to create a store, which in turn would give you access to all the data at any point in your app:
export default {
computed: {
data() {
return this.$store.data;
}
}
}
Learn more here: https://vuex.vuejs.org/
As an alternative, you can just import your data, and grab the correct item by its id:
import data from './data.js';
export default {
computed: {
data() {
return data.find(d => d.id === this.$route.params.id);
}
}
}
Just depends on what you're trying to do.
I guess you just need a wrapper component that takes the desired item from the URL and renders the proper item. Let's say an ItemWrapper:
<template>
<item-card :item="item"></item-card>
</template>
<script>
import ItemCard from './ItemCard.vue';
import data from '../data/data';
export default {
components: {
ItemCard,
},
props: {
stackNameUrl: {
required: true,
type: String,
},
},
data() {
return {
item: {},
}
},
computed: {
stackName() {
return decodeURI(this.stackNameUrl);
}
},
created() {
this.item = data.find( fw => fw.name === this.stackName);
}
}
</script>
<style>
</style>
This component takes a prop which is a stack/fw name uri encoded, decodes it, finds the fw from data based on such string, and renders an ItemCard with the fw item.
For this to work we need to setup the router so /item/vue js f.i. renders ItemWrapper with 'vue js' as the stackNameUrl prop. To do so, the important bit is to set props as true:
import Vue from 'vue';
import Router from 'vue-router';
import SearchPage from '#/components/SearchPage';
import ItemWrapper from '#/components/ItemWrapper';
Vue.use(Router);
export default new Router({
routes: [
{
path: '/',
name: 'SearchPage',
component: SearchPage
},
{
path: '/item/:stackNameUrl',
name: 'ItemWrapper',
component: ItemWrapper,
props: true,
},
]
});
Now we need to modify SearchPage.vue to let the stack boxes act as links. Instead of:
<!-- iterate data -->
<item-card v-for="(item, index) in filteredData" :key="index" :item="item"></item-card>
we now place:
<template v-for="(item, index) in filteredData" >
<router-link :to="'/item/' + item.name" :key="index">
<item-card :key="index" :item="item"></item-card>
</router-link>
</template>
So now every component is placed within a link to item/name.
And voilá.
Some considerations:
the :param is key for the vue router to work. You wanted to use it to render the ItemCard itself. That could work, but you would need to retrieve the fw from data from the component created(). This ties your card component with data.js which is bad, because such component is meant to be reusable, and take an item param is much better than go grabbing data from a file in such scenario. So a ItemWrapper was created that sort of proxies the request and pick the correct framework for the card.
You should still check for cases when an user types a bad string.
Explore Vue in depth before going for vuex solutions. Vuex is great but usually leads to brittle code and shouldn't be overused.

vue-router replace parent view with subRoute

I'm using the vue-router and have a question regarding subRoutes. I would like to set up my routes so the main routes are listings and the subRoutes are things like edit/add/etc.
I want the subRoute components to replace the <router-view> of the parent route. The way I understand the documentation and from what I've tested, it looks like I should define another <router-view> in the parent components template for the subRoute to render into but then the user-list would remain visible.
Example routes:
'/users': {
name: 'user-list',
component(resolve) {
require(['./components/users.vue'], resolve)
},
subRoutes: {
'/add': {
name: 'add-user',
component(resolve) {
require(['./components/users_add.vue'], resolve)
}
}
}
}
Main router view:
<!-- main router view -->
<div id="app">
<router-view></router-view>
</div>
User list:
<template>
<a v-link="{ name: 'add-user' }">Add</a>
<ul>
<li>{{ name }}</li>
</ul>
</template>
Add user:
<template>
<div>
<a v-link="{ name: 'user-list' }">back</a>
<input type="text" v-model="name">
</div>
</template>
When I click on "Add", I want to be filled with the add-user template. Is this possible?
Also, can I establish a parent-child relationship between the user-list and add-user components? I would like to be able to pass props (list of users) to the add component and dispatch events back up to the user-list.
Thanks!
It sounds like those edit/add routes should not be subRoutes, simple as that.
just because the path makes it seem like nesting doesn't mean you have to actually nest them.
'/users': {
name: 'user-list',
component(resolve) {
require(['./components/users.vue'], resolve)
}
},
'users/add': {
name: 'add-user',
component(resolve) {
require(['./components/users_add.vue'], resolve)
}
}
So I played around with this for quite a bit and finally figured out how this can be achieved. The trick is to make the listing a subRoute too and have the root-level route just offer a <router-view> container for all child components to render into. Then, move the "list loading" stuff to the parent component and pass the list to the list-component as a prop. You can then tell the parent to reload the list via events.
That's it for now, it works like a charm. Only drawback is that I have another component that I didn't really need. I'll follow up with an example when I find the time.
edit
Added an example as requested. Please note: this code is from 2016 - it may not work with current Vue versions. They changed the event system so parent/child communication works differently. It is now considered bad practice to communicate in both directions (at least the way I'm doing it here). Also, these days I would solve this differently and give each route it's own module in the store.
That said, here's the example I would have added back in 2016:
Let's stick with the users example - we have a page with which you can list/add/edit users.
Here's the route definition:
'/users': {
name: 'users',
component(resolve) {
// this is what I meant with "root-level" route, it acts as a parent to the sub routes
require(['./pages/users.vue'], resolve)
},
subRoutes: {
'/': {
name: 'user-list',
component(resolve) {
require(['./pages/users/list.vue'], resolve)
}
},
'/add': {
name: 'user-add',
component(resolve) {
require(['./pages/users/add.vue'], resolve)
}
},
// etc.
}
},
users.vue
<template>
<div>
<router-view :users="users"></router-view>
</div>
</template>
<script>
/**
* This component acts as a parent and sort of state-storage for
* all user child components. It's the route that's loaded at /users.
* the list component is the actual component that is shown though, but since
* that's a sibling of the other child components, we need this.
*/
export default {
events: {
/**
* Update users when child says so
*/
updateUsers() {
this.loadUsers();
}
},
data() {
return {
users: []
}
},
route: {
data() {
// load users from server
if (!this.users.length) {
this.loadUsers();
}
}
},
methods: {
loadUsers() {
// load the users from an API or something
this.users = response.data;
},
}
}
</script>
./pages/users/list.vue
<template>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>
<script>
export default {
props: ['users'],
// the rest of the component
}
</script>

Categories

Resources