VueJS vue-router passing a value to a route - vue.js

In VueJS 2 with vue-router 2 I am using a parent view with subcomponents like this:
WidgetContainer.vue with route /widget_container/:
<template>
<component :is="activeComponent"></component>
</template>
<script>
import WidgetA from './components/WidgetA'
import WidgetB from './components/WidgetB'
export default {
name: 'WidgetContainer',
components: {
WidgetA,
WidgetB
},
data () {
return {
activeComponent: 'widget-a'
}
}
}
</script>
In WidgetA I have the option of selecting a widget id
<template>
// v-for list logic here..
<router-link :to="{ path: '/widget_container/' + widget.id }"><span>{{widget.name}} </span></router-link>
</template>
<script>
export default {
name: 'WidgetA',
data() {
return {
widgets: [
{ id: 1,
name: 'blue-widget'
}]}}
routes.js:
export default new Router({
routes: [
{
path: '/widget_container',
component: WidgetContaner
},
{
path: '/widget_container/:id?',
redirect: to => {
const { params } = to
if (params.id) {
return '/widget_contaner/:id'
} else {
return '/widget_container'
}
}
}]})
From the WidgetContainer if the route is /widget_container/1 (where '1' is the id selected in WidgetA) I want to render WidgetB, but I cant work out:
1) how to pass the selected widget id into the router-link in WidgetA
2) How to know in WidgetContainer the the route is /widget_contaner/1 instead of /widget_container/ and render WidgetB accordingly.
Any ideas?

You can pass data to parent using by emitting event, you can see more details around here and here.
Once the data is change, you can watch over it and update the variable which has stored your widget.
Another option, if communication between components become unmanageable over time is to use some central state management, like vuex, more details can be found here.

Wouldn't it be easier and more scallable to use Vuex for that?
Just commit id to store and than navigate ?

Related

Passing data from route to router-view body

I have a single page application with the structure below.
|- App.vue
|- + Views
| |- Page.vue
|- + Components
| |- Slider.vue
EDIT 1: Solution thanks to #gengar.value
I solved the issue by passing params from Page.vue with
methods: {
emitIndex: function (index) {
this.$router.push({
name: "visualization",
params: { imgCat: "visualization", imgIndex: index },
});
},
}
App.vue containing the router-view container that is routing the Page.vue
Slider.vue is a component of App.vue
I want to pass index of clicked image and whole images data from Page.vue to App.vue then to Slider.vue in order to achieve decoupling Slider from Page for reusability purposes.
How can I pass user selected index from Page.vue too App.vue
I have tried to use, params, props and emit but failed.
Sample Page.vue
<template>
<div v-for="(item, index) in 3" :key="index"></div>
</template>
<script>
export default ({
data() {
return {
urls: ['url1', 'url2', 'url3']
}
}
})
</script>
Thanks in advance
EDIT 1: Solution thanks to #gengar.value
Problem solved by pushing params to router via Page.vue and listening it from Slider.vue as follows:
Page.vue
methods: {
passIndex: function (index) {
this.$router.push({
name: "visualization",
params: { imgCat: "visualization", imgIndex: index },
});
},
}
Slider.vue
watch: {
"$route.params.imgCat": function (val) {
this.state = val;
},
"$route.params.imgIndex": function (newVal) {
if (newVal != -1) this.imgState = newVal;
this.$router.push({ params: { imgIndex: -1 } });
}
My solution is a little bit complicated, but quite native since I only used Props and Emit.
You want to pass value between brother components, so you could simply try below:
App.vue
<template>
<div>
<Page :data="data" #syncData="syncData" />
<Slider :data="data" />
</div>
<template>
<script>
import Page from './Views/Page.vue'
import Slider from './Components/Slider.vue'
export default ({
components: {
Page,
Silder
},
data() {
return {
data: [] // init data in the parent component
}
},
methods: {
syncData(updatedImages) {
this.data = updatedImages
}
}
})
</script>
Page.vue
<template>
<div></div>
<template>
<script>
export default ({
props: {
data: { type: Array, default: () => [] }
},
methods: {
onSelectImage(images) {
this.$emit('syncData', images) // update selected data to App.vue
}
}
})
</script>
Slider.vue
<template>
<div></div>
<template>
<script>
export default ({
props: {
data: { type: Array, default: () => [] }
},
watch: {
data: {
handler(val) {
// when Page.vue emits updated data to App.vue,
// App.vue will pass data to Slider.vue
// and you could receive the updated data 'val' here
},
deep: true
}
}
})
</script>
Update: Sorry I misunderstood before. If you are using vue-router components and assigned different paths (eg. '/page' and '/slider'), you can use
this.$router.push({ path: '/path', query: selectedImage })
in Page.vue and get url query in Slider.vue.
Alternative methods could be using Cookie.js or sessionStorage (not pretty tho). Also you could try Vuex if the specific condition suits you.

Vuex state not being initialised before router-view component being rendered - undefined error

I am relatively new to vue and have run into a small issue. I am rendering a component that depends on the state stored in vuex. I load this information in from a json file in the main part of the app. It all works fine if I always land on the root (index.html) of the app when it loads up. However, if I refresh the app from a page that is dynamically generated from the router I hit an error:
[Vue warn]: Error in render: "TypeError: Cannot read property 'name' of undefined"
found in
---> <Room>
<RoomsOverview>
<Root>
As far as I can tell what is happening is that that the component is trying to access the state in vuex but it has not been initialised. Here is the component (Room.vue):
<template>
<div id="room">
<h2>{{ roomName }}</h2>
<div v-for="device in deviceList" v-bind:key="deviceList.name">
{{ device.name }} - {{ device.function}}
<svg-gauge v-bind:g-value="device.value" v-bind:g-min="0" v-bind:g-max="50" v-bind:g-decplace="1" g-units="℃">
<template v-slot:title>
Temperature
</template>
</svg-gauge>
</div>
</div>
</template>
<script>
module.exports = {
name: 'room',
/** Load external component files
* Make sure there is no leading / in the name
* To load from the common folder use like: 'common/component-name.vue' */
components: {
'svg-gauge': httpVueLoader('components/DisplayGauge.vue'),
}, // --- End of components --- //
data() {
return {
};
},
computed: {
roomName() {
// return this.$route.params.roomId;
return this.$store.getters['rooms/getRoomById'](this.$route.params.roomId);
},
deviceList() {
return this.$store.getters['rooms/getDevicesinRoom'](this.$route.params.roomId);
},
},
}
</script>
The error is triggered by the line
return this.$store.getters['rooms/getRoomById'](this.$route.params.roomId);
This tries to access the current state in the getter:
getRoomById: (state) => (id) => {
return state.rooms.find(room => room.id === id).name; // Needs fixing!
},
but it seems that the array:
// Initial state
const stateInitial = {
rooms: [],
};
has not been initialised under these circumstances. The initialisation occurs in the main entry point to the app in index.js in the mounted hook
// Load data from node-red into state
vueApp.$store.dispatch('rooms/loadRooms')
Where loadRooms uses axios to get the data. This works as expected if I arrive at the root of the site (http://192.168.0.136:1880/uibuilderadvanced/#/) but not if I arrive at a link such as (http://192.168.0.136:1880/uibuilderadvanced/#/rooms/office). I suspect it is all down to the order of things happening and my brain has not quite thought things through. If anyone has any ideas as to how to catch this I would be grateful - some kind of watcher is required I think, or a v-if (but I cannot see where to put this as the Room.vue is created dynamically by the router - see below).
Thanks
Martyn
Further information:
The room component is itself generated by router-view from within a parent (RoomsOverview.vue):
<template>
<div id="rooms">
<b-alert variant="info" :show="!hasRooms">
<p>
There are no rooms available yet. Pass a message that defines a room id and device id
to the uibuilder node first. See <router-link :to="{name: 'usage_info'}">the setup information</router-link>
for instructions on how start using the interface.
</p>
</b-alert>
<router-view></router-view>
</div>
</template>
<script>
module.exports = {
name: 'RoomsOverview',
data() {
return {
};
},
computed: {
hasRooms() {
return this.$store.getters['rooms/nRooms'] > 0;
},
roomList() {
return this.$store.getters['rooms/getAllRooms'];
},
},
}
</script>
and is dependent on the router file:
const IndexView = httpVueLoader('./views/IndexView.vue');
const AdminView = httpVueLoader('./views/AdminView.vue');
export default {
routes: [
{
path: '/',
name: 'index',
components: {
default: IndexView,
menu: HeaderMenu,
},
},
{
path: '/rooms',
name: 'rooms_overview',
components: {
default: httpVueLoader('./components/RoomsOverview.vue'),
menu: HeaderMenu,
},
children: [
{
path: ':roomId',
name: 'room',
component: httpVueLoader('./components/Room.vue'),
},
],
},
{
path: '/admin',
name: 'admin',
components: {
default: AdminView,
menu: HeaderMenu,
},
children: [
{
path: 'info',
name: 'usage_info',
component: httpVueLoader('./components/UsageInformation.vue'),
}
]
},
],
};
It seems you already got where the issue is.
When you land on you main entry point, the axios call is triggered and you have all the data you need in the store. But if you land on the component itself, the axios call does not get triggered and your store is empty.
To solve you can add some conditional logic to your component, to do an axios call if the required data is undefined or empty.

Vue: props passed to router-link

I want to have 3 main parts in my webapp:
App.vue - this page only has the <router-view> tag and some general configuration + it fetches an API every second
ControlPanel.vue - this page visualizes some data that the App.vue page gets
Profile.vue - this page visualizes some data that the App.vue page gets too
Right now I set up my App.vue with the API call and it passes the data it receives to the two pages with props like the following example. As you can see when it gets mounted it starts a loop that lasts 1 second where it goes and fetches the API and then it returns it to the two routes.
<template>
<div id="app">
<div id="nav">
<router-link :to="{ name: 'control_panel', params: { APIlogs } }">Control panel</router-link>
<span> | </span>
<router-link :to="{ name: 'profile', params: { APIlogs } }">Profile</router-link>
</div>
<router-view/>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
APIlogs: '',
};
},
mounted() {
setInterval(() => this.refreshData(), 1000);
},
methods: {
refreshData() {
axios.get('http://192.168.30.65:5000/logs')
.then((response) => {
this.APIlogs = response.data;
});
},
},
};
</script>
<style>
...
</style>
On the other hand, Control Panel and Profile are fundamentally the same page and they should get the props from the "father" and use it to visualize data but right now it doesn't work. When I click on one route it shows me the value the prop has in that moment and doesn't update as the App.vue page fetches more and more data.
<template>
<div id="app">
{{APIlogs}}
</div>
</template>
<script lang="ts">
import axios from 'axios';
export default {
name: 'control-panel',
props: ['APIlogs'],
data() {
return {
};
},
mounted(){
console.log(this.APIlogs);
},
methods: {
},
};
</script>
<style>
...
</style>
Did I do something wrong? Is my implementation good enough or is it lacking in some way? Really hope someone can help me out with this one, it's really tearing me apart.
Thanks a lot in advance
EDIT
Just to give a bit more context, before having props I was calling the same exact API from both components and it seemd very inefficient to me so I switched to this method.
Also my router.ts looks like this:
import Vue from 'vue';
import Router from 'vue-router';
import ControlPanel from '../src/components/ControlPanel.vue';
import Profile from '../src/components/Profile.vue';
Vue.use(Router);
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'control_panel',
component: ControlPanel,
props: true,
},
{
path: '/profile',
name: 'profile',
component: Profile,
props: true,
},
],
});
there's no params inside your paths i.e: path: '/:apilogs'
A dynamic segment is denoted by a colon :. When a route is matched,
the value of the dynamic segments will be exposed as
this.$route.params in every component.
(source)
After a while and almost an entire afternoon wasted on this problem, I found out this article which helped me achieve my goal. I just created a file with all my api calls and I call it every time I need to fetch something. It's a way more elegant and intelligent solution I think.
An easy way to make this work is to just make your APIlogs an object. Then it would be passed by reference and any updates to it will be reflected in the other components ..
export default {
data() {
return {
APIlogs: {logs: ''},
};
},
mounted() {
setInterval(() => this.refreshData(), 1000);
},
methods: {
refreshData() {
axios.get('http://192.168.30.65:5000/logs')
.then((response) => {
this.APIlogs.logs = response.data;
});
},
},
};
<template>
<div id="app">
{{APIlogs.logs}}
</div>
</template>
PS: You should probably use clearInterval in your beforeDestroy hook.

How to use "data" or "methods" result to VueRouter prop

I have simple menu tabs router as follow:
Tab 1 | Tab 2 | Tab 3
Routes are attached with individual component to each tab as example below.
const router = new VueRouter({
routes: [
{ path: '/views/type1', component: Type1, props: { listname: value} }
]
})
Problem:
How route[] prop modify to take the value from "data" of vue. Basically,
one of the component accept property as 'Array' and I need to pull those data from service and attach to component.
routes: [
{ path: '/views/type1', component: Type1, props: { collection: *[retrieve from service and attach here]*} }
]
//collection is not able to bind from "methods" or "data" , it only accepts static data.
The props field of Vue Router does not hold properties to be passed to the rendered view component, but rather it is a Boolean flag (or a hash/map of view names to Boolean flags) that tells Vue Router to pass any route parameters (parsed from the path) as properties to the component.
For example, given the following:
route config:
{
path: '/user/:name/:uid',
component: User,
props: true
}
User component definition:
export default {
props: ['name', 'uid']
}
URL:
/user/john/123
Then, User would be rendered with name set to john and uid set to 123, equivalent to this:
<User :name="john" :uid="123" />
If you need to initialize a view with server data, you could wrap the target component (e.g., with Type1View) that you initialize after you've fetched the data. In the example below, Type1.list is bound to a local list data variable. When Type1View mounts, we fetch data from the server, and save the result in list, which also updates Type1.list.
<template>
<div>
<Type1 :list="list" />
</div>
</template>
<script>
export default {
name: 'Type1View',
data() {
return {
list: []
}
},
async mounted() {
const data = await this.fetchData();
this.list = data.list;
}
}
</script>

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