Passing data from route to router-view body - vue.js

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.

Related

Can props be accessed in beforecreated of vue?

In the vue docs, the part of "beforeCreate", I read the following:
Called immediately when the instance is initialized, after props resolution, before processing other options such as data() or computed.
Does this means that I can get props in beforeCreate hooks? if so, how can I get it?
In my child component, I try like this to get the message passed by parent component, but failed.
export default {
name: 'Child',
props: ['message'],
beforeCreate() {
console.log(this.message)
}
}
Please check the following snippet, looks like your code is fine:
const app = Vue.createApp({
data() {
return {
msg: 'aaa',
};
},
})
app.component('Child', {
template: `<div>{{ message }}</div>`,
props: ['message'],
beforeCreate() {
console.log('before create: ', this.message)
}
})
app.mount('#demo')
<script src="https://unpkg.com/vue#3.2.29/dist/vue.global.prod.js"></script>
<div id="demo">
<child :message="msg"></child>
</div>

Vue 3: Wait until parent is done with data fetching to fetch child data and show loader

I'm looking for a reusable way to display a full page loader (Sidebar always visible but the loader should cover the content part of the page) till all necessary api fetches has been done.
I've got a parent component LaunchDetails wrapped in a PageLoader component
LaunchDetails.vue
<template>
<PageLoader>
<router-link :to="{ name: 'launches' }"> Back to launches </router-link>
<h1>{{ name }}</h1>
<section>
<TabMenu :links="menuLinks" />
</section>
<section>
<router-view />
</section>
</PageLoader>
</template>
<script>
import TabMenu from "#/components/general/TabMenu";
export default {
data() {
return {
menuLinks: [
{ to: { name: "launchOverview" }, display_name: "Overview" },
{ to: { name: "launchRocket" }, display_name: "Rocket" },
],
};
},
components: {
TabMenu,
},
created() {
this.$store.dispatch("launches/fetchLaunch", this.$route.params.launch_id);
},
computed: {
name() {
return this.$store.getters["launches/name"];
},
},
};
</script>
PageLoader.vue
<template>
<Spinner v-if="isLoading" full size="medium" />
<slot v-else></slot>
</template>
<script>
import Spinner from "#/components/general/Spinner.vue";
export default {
components: {
Spinner,
},
computed: {
isLoading() {
return this.$store.getters["loader/isLoading"];
},
},
};
</script>
The LaunchDetails template has another router-view. In these child pages new fetch requests are made based on data from the LaunchDetails requests.
RocketDetails.vue
<template>
<PageLoader>
<h2>Launch rocket details</h2>
<RocketCard v-if="rocket" :rocket="rocket" />
</PageLoader>
</template>
<script>
import LaunchService from "#/services/LaunchService";
import RocketCard from "#/components/rocket/RocketCard.vue";
export default {
components: {
RocketCard,
},
mounted() {
this.loadRocket();
},
data() {
return {
rocket: null,
};
},
methods: {
async loadRocket() {
const rocket_id = this.$store.getters["launches/getRocketId"];
if (rocket_id) {
const response = await LaunchService.getRocket(rocket_id);
this.rocket = response.data;
}
},
},
};
</script>
What I need is a way to fetch data in the parent component (LaunchDetails). If this data is stored in the vuex store, the child component (LaunchRocket) is getting the necessary store data and executes the fetch requests. While this is done I would like to have a full page loader or a full page loader while the parent component is loading and a loader containing the nested canvas.
At this point the vuex store is keeping track of an isLoading property, handled with axios interceptors.
All code is visible in this sandbox
(Note: In this example I could get the rocket_id from the url but this will not be the case in my project so I'm really looking for a way to get this data from the vuex store)
Im introduce your savior Suspense, this feature has been added in vue v3 but still is an experimental feature. Basically how its work you create one suspense in parent component and you can show a loading when all component in any depth of your application is resolved. Note that your components should be an async component means that it should either lazily loaded or made your setup function (composition api) an async function so it will return an async component, with this way you can fetch you data in child component and in parent show a fallback if necessary.
More info: https://vuejs.org/guide/built-ins/suspense.html#suspense
You could use Events:
var Child = Vue.component('child', {
data() {
return {
isLoading: true
}
},
template: `<div>
<span v-if="isLoading">Loading …</span>
<span v-else>Child</span>
</div>`,
created() {
this.$parent.$on('loaded', this.setLoaded);
},
methods: {
setLoaded() {
this.isLoading = false
}
}
});
var Parent = Vue.component('parent', {
components: { Child },
data() {
return {
isLoading: true
}
},
template: `<div>
Parent
<Child />
</div>`,
mounted() {
let request1 = new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
let request2 = new Promise((resolve, reject) => {
setTimeout(resolve, 2000);
});
Promise.all([ request1, request2 ]).then(() => this.$emit('loaded'))
}
});
new Vue({
components: { Parent },
el: '#app',
template: `<Parent />`
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
This may be considered an anti-pattern since it couples the parent with the child and events are considered to be sent the other way round. If you don't want to use events for that, a watched property works just fine, too. The non-parent-child event emitting was removed in Vue 3 but can be implemented using external libraries.

<b-form-input> bind value does not update on the second time

I am currently implementing a component that update parent's year[] array when year to / year[1] value is lower than year from / year[0] with <b-input> (Bootstrap Vue Library).
The year to stop updating after the second time.
Code example are as below.
Code equivalent in jsfiddle can be found here.
Parent.vue
<template>
<child :year="year" #update="update">
</template>
<script>
// Import child component here
export default {
name: 'Parent',
components: {
Child,
},
data: () => ({
year: [100, null],
}),
methods: {
update(newYear) {
this.year = newYear;
},
},
</script>
Child.vue
<template>
<div>
From <b-input :value="year[0]" />
To <b-input :value="year[1]" #change="update" />
</div>
</template>
<script>
export default {
name: 'Child',
props: {
year: {
type: Array,
required: true,
}
},
methods: {
update(yearToVal) {
const [yearFrom] = this.year;
let newYear = [...this.year];
if (yearToVal < yearFrom) {
/* Both of this update end up the same */
// newYear[1] = yearFrom;
this.$set(newYear, 1 , yearFrom);
}
this.$emit('update', newYear);
},
},
};
</script>
I had used Vue Dev Tools to check and the child is emitting data correctly to the parent.
The issue happen on the vModalValue and localValue of the <b-input> are not updating on the second time.
What am I doing wrongly or is it a Bootstrap Vue library problem?
Hiws's answer indicate that the this problem does not only happen on <b-form-input> but ordinary <input> with Vue as well.
This happen due to Vue not able to react to changes since the update is happening on child, hence when year to is lower than year from, parent will not detect any changes on the second time as the array pass to Parent.vue will always be [100,100].
The solution will be using watcher on Parent.vue's array to detect the changes, hence both eg: [100, 1] -> [100,100] are both reflected on Parent.vue and most importantly, force the component to re-render.
Without force re-rendering, [100,1] or [100,2]... will always be treated as [100,100], the same value, and Vue will not react or even update to them.
Jsfiddle equivalent solution can be found here
Code sample below:
Parent.vue
<template>
<child :year="year" #update="update">
</template>
<script>
// Import child component here
export default {
name: 'Parent',
components: {
Child,
},
data: () => ({
year: [100, null],
yearKey: 0,
}),
watch: {
year: {
handler(val) {
if (val[1] < val[0]) {
let newYear = [...val];
newYear[1] = val[0];
this.year = newYear;
// key update reference: https://michaelnthiessen.com/force-re-render/
this.yearKey += 1;
}
}
}
},
methods: {
update(newYear) {
this.year = newYear;
},
},
</script>
Child.vue
<template>
<div>
From <b-input :value="year[0]" />
To <b-input :value="year[1]" #change="update" />
</div>
</template>
<script>
export default {
name: 'Child',
props: {
year: {
type: Array,
required: true,
}
},
methods: {
update(yearToVal) {
const [yearFrom] = this.year;
let newYear = [...this.year];
newYear[1] = yearToVal;
this.$emit('update', newYear);
},
},
};
</script>

How to share data between components Vue js

Hi there I saw posts talking about this, but is not easy for me understand what I have to do to share data between components, I don't want to use event bus so can you tell me how to use props??
Component A:
<template>
<div>
<div class="container">
<fileForm></fileForm> //<--- THE COMPONENT B
</div>
</div>
</div>
</template>
<script>
export default {
name: "DashBoard",
data() {
return {
user: {},
};
},
methods: {
checkIfImLoggedIn() {
}
},
onComplete() {
},
},
mounted() {
this.checkIfImLoggedIn();
}
};
</script>
Component B:
<template>
//...
</template>
<script>
export default {
name: "FileForm",
data() {
return {
fileExtensions: ["CSV", "EXCEL"],
sharedData : {}, //<--- for example share this
};
},
methods: {}
};
</script>
If you want to share data from the parent to the child you can do:
on component A
<fileForm :something="user"></fileForm>
In component B
props: {
something: Object
}
If you want to share data from the child to the parent you have to use an event bus or vuex: https://v2.vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication
https://alligator.io/vuejs/component-communication/
You need to do some research about communication among components.
There are a lot of ways to do it ;)

Vue: How to use store with component?

//store
export default {
state: {
aboutModels: []
},
actions: {
findBy: ({commit}, about)=> {
//do getModels
var aboutModels = [{name: 'About'}] //Vue.resource('/abouts').get(about)
commit('setModels', aboutModels)
}
},
getters: {
getModels(state){
return state.aboutModels
}
},
mutations: {
setModels: (state, aboutModels)=> {
state.aboutModels = aboutModels
}
}
}
//component
import {mapActions, mapGetters} from "vuex";
export default {
name: 'About',
template: require('./about.template'),
style: require('./about.style'),
created () {
document.title = 'About'
this.findBy()
},
computed: mapGetters({
abouts: 'getModels'
}),
methods: mapActions({
findBy: 'findBy'
})
}
//view
<div class="about" v-for="about in abouts">{{about.name}}</div>
//error
vue.js:2532[Vue warn]: Cannot use v-for on stateful component root element because it renders multiple elements:
<div class="about" v-for="about in abouts">{{about.name}}</div>
vue.js:2532[Vue warn]: Multiple root nodes returned from render function. Render function should return a single root node. (found in component <About>)
You are mapping your Vuex state getters and action correctly. Your problem is something else as your error message states...
In your component template you can not use v-for directive on a root element. For example this is not allowed because your component can have multiple root elements:
<template>
<div class="about" v-for="about in abouts">{{about.name}}</div>
</template>
instead do it this way:
<template>
<div>
<div class="about" v-for="about in abouts">{{about.name}}</div>
</div>
</template>
** *fixed typo in template tag **