Child component does not re-render when props change in store - vue.js

I am using Vue-js with require-js. I am trying to get data from vuex store into my cart component and render a component for each item in the store. But when I trigger a mutation from my body component to change the store, the data is being changed and the props of my cart component change, but the UI does not re-render.
This is my store:
state: {
users: {
user1: {
item: { date:null }
}
}
}
mutations: { setDate:function(state,payload){
var newState = state.users;
newState[user][item].date = payload.date
state.users = Object.assign({},newState)
} }
This is my cart component:
<template>
<div v-if="activeStep==1">
<p> Service Time: {{service.ServiceTime}} Min.</p>
<p>Date: {{service.date || 'Not Selected'}} </p>
<p>Time: {{service.time || 'Not Selected'}} </p>
<p>Prefered Staff: {{service.staff}} </p>
</div>
</template>
<script>
define(['Vue','vuex'],function(Vue,vuex){
return {
template: template,
computed: vuex.mapState(['activeStep']),
props: ['service'],
}
})
</script>
This is the parent of my cart component:
<template>
<div class="cart-user-body" >
<div class="cart-service" v-for="(service,key,index) in users[user]" :key="index">
<div class="cart-service-body">
<service-book-details :service="service"></service-book-details>
</div>
</div>
</div>
</template>
<script>
define([
'Vue','vuex','vue!./serviceBookDetails'
], function(Vue,vuex,serviceBookDetails) {
return {
template:template,
components: {
'service-book-details': serviceBookDetails
},
props: ['user'],
computed: vuex.mapState(['users']),
}
});
</script>
This is how I am triggering the mutation from my body component:
addDate(e) {
var payload = {
date: moment(e, "DD/MM/YYYY").format("Do MMMM YYYY"),
id: this.$data.class,
name: this.username
};
this.$store.commit("setDate", payload);
},
I even tried using Vue.set(state,'users',newState) but the UI does not re-render.
I have checked the Vue dev tools and I see that upon triggering the mutation, the props of my cart component have updated but it does not show on the UI.
If I try using getters, the key to the object does not exist as my store does not have the required data until user interacts with UI and adds data. And my cart component is always showing since the start so it shows me an error saying cant read property item of undefined.
Am I doing anything wrong or is there a different way to make it work.

You can't use array indexing for setting values with Vue. It is a restriction caused by Javascript.
This will not be reactive if user and or item did not exist when you created your store.
newState[user][item].date =
Instead, you need to use:
Vue.set(object, key, value)
In your case, you first need to ensure you set user and item with that method before assigning to date.

Related

Parent component updates a child component v-for list, the new list is not rendered in the viewport (vue.js)

My app structure is as follows. The Parent app has an editable form, with a child component list placed at the side. The child component is a list of students in a table.
I'm trying to update a child component list. The child component uses a 'v-for', the list is generated through a web service call using Axios.
In my parent component, I am editing a students name, but the students new name is not reflected in the List that I have on screen.
Example:
Notice on the left the parent form has the updated name now stored in the DB. However, the list (child component) remains unchanged.
I have tried a few things such as using props, ref etc. I am starting to think that my app architecture may be incorrect.
Does anyone know how I might go about solving this issue.
Sections of the code below. You may understand that I am a novice at Vue.
Assistance much appreciated.
// Child component
<component>
..
<tr v-for="student in Students.slice().reverse()" :key="student._id">
..
</component>
export default {
env: '',
// list: this.Students,
props: {
inputData: Boolean,
},
data() {
return {
Students: [],
};
},
created() {
// AXIOS web call...
},
};
// Parent component
import List from "./components/students/listTerms";
export default {
name: "App",
components: {
Header,
Footer,
List,
},
};
// Implementation
<List />
I think that it is better to use vuex for this case and make changes with mutations. Because when you change an object in the data array, it is not overwritten. reactivity doesn't work that way read more about it here
If your list component doesn't make a fresh API call each time the form is submitted, the data won't reflect the changes. However, making a separate request each time doesn't make much sense when the component is a child of the form component.
To utilise Vue's reactivity and prevent overhead, it would be best to use props.
As a simplified example:
// Child component
<template>
...
<tr v-for="student in [...students].reverse()" :key="student._id">
...
</template>
<script>
export default {
props: {
students: Array,
},
};
</script>
// Parent component
<template>
<div>
<form #submit.prevent="submitForm">
<input v-model="studentData.name" />
<input type="submit" value="SUBMIT" />
</form>
<List :students="students" />
</div>
</template>
<script>
import List from "./components/students/listTerms";
export default {
name: "App",
components: {
List,
},
data() {
return {
students: [],
studentData: {
name: ''
}
}
},
methods: {
submitForm() {
this.$axios.post('/endpoint', this.studentData).then(() => {
this.students.push({ ...this.studentData });
}).catch(err => {
console.error(err)
})
}
}
};
</script>
Working example.
This ensures data that isn't stored successfully won't be displayed and data that is stored successfully reflects in the child component.

Vue - how can i update the default value of an input text field?

I'm loading a form component more times in the same page, that's because i have more forms for different tasks, so each form has different parameters.
Html page:
<div id="app">
<myForm formType="buy"></myForm>
<myForm formType="sell"></myForm>
<myForm formType="submit"></myForm>
<refreshAmount></refreshAmount>
</div>
And this is the form component:
<template>
<div>
<div v-if="formType=='buy'">
<form #submit.prevent="formSubmit()">
<input type="text" class="form-control" value="testetete" v-bind:value="amount">
<button v-if="side==='buy'" class="btn btn-primary" style="width: 100%">BUY</button>
<p>Available amount: {{$store.getters.amount}}</p>
</form>
</div>
...
</div>
</template>
<script>
export default {
props:{
formType:{
type:String,
default:'buy'
},
},
mounted() {
console.log('mounted')
},
data() {
return {
amount: this.$store.getters.amount
}
},
methods: {
...
}
}
</script>
Then i have the following store:
<script>
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export const store = new Vuex.Store({
state: {
amount: 0
},
mutations: {
refreshAmount(state) {
fetch('SOME-URL')
.then(response => response.json())
.then(data => {
state.amount = 100
//state.amount = data['amount']
})
}
},
getters: {
amount: state => state.amount,
}
})
</script>
And finally, the refreshAmount component:
...
mounted() {
this.$store.commit('refreshBalance')
}
...
Basically, i need to show an amount in the form component. This amount is retrieved from my backend, and since i'm loading the form component 3 times, i would call my backend 3 times while i only need to call it once, so i decided to create the component refreshAmount that would call it once and pass it to the form components using a Vuex store.
The problem with my code is the following:
When i load the page, i'll see Available amount: 100 on all the 3 components, so that works; but in the default value of the text input form the value is 0. Why is that? Why isn't the value inside the input text field updated while <p>Available amount: {{$store.getters.amount}}</p> is updated?
Tl;dr: i'm using Vuex to set the value of a variable in my components, when i load the variable between a <p> tag the value is refreshed, while inside the input field the value of the variable stays the same.
amount is not updated because it's a data property, which only gets initialized when the component is first set up and not updated after. What you need is a computed property, which will keep track of the changes in the Vuex store. So instead of:
data() {
return {
amount: this.$store.getters.amount
}
}
you can do:
computed: {
amount() {
return this.$store.getters.amount
}
}

How can I pass a variable value from a "page" to "layout" in Nuxt JS?

I'm a beginner in VUE and donnow this one is the correct syntax. I need the variable {{name}} to be set from a page. Which means I need to change the value of the variable page to page. How can I achieve that? Help me guys.
My "Layout" Code is like below -
<template>
<div class="login-page">
<div class="col1">{{ name }}</div>
<div class="col2">
<div class="content-box">
<nuxt />
</div>
</div>
</div>
</template>
<script>
export default {
props: ['name']
}
</script>
And my "Page" code is following -
<template>
<div>Welcome</div>
</template>
<script>
export default {
layout: 'login',
data: function() {
return {
name: 'Victor'
}
}
}
</script>
this can be achieved by using the vuex module. The layout have access to the vuex store, so once a page is open, you can call a mutation to set the page name and listen the name state in the layout component.
First the Vuex module, we can add a module by creating a file in the store folder,
in this case we are creating the page module:
// page.js file in the store folder
const state = {
name: ''
}
const mutations = {
setName(state, name) {
state.name = name
}
}
const getters = {
getName: (state) => state.name
}
export default {
state,
mutations,
getters
}
Now we can use the setPageName mutation to set the pageName value once a page reach the created hook (also can be the mounted hook):
// Page.vue page
<template>
<div>Welcome</div>
</template>
<script>
export default {
layout: 'login',
created() {
this.$store.commit('page/setName', 'Hello')
},
}
</script>
And in the layout component we have the computed property pageName (or name if we want):
<template>
<div class="login-page">
<div class="col1">{{ name }}</div>
<div class="col2">
<div class="content-box">
<nuxt />
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
name() {
return this.$store.getters['page/getName']
}
}
}
</script>
And it's done!
Answer to your question in the commets:
The idea behind modules is keep the related information to some functionality in one place. I.e Let's say you want to have name, title and subtitle for each page, so the page module state variable will be:
const state = { name: '', title: '', subtitle: ''}
Each variable can be updated with a mutation, declaring:
const mutations = {
setName(state, name) {
state.name = name
},
setPageTitle(state, title) {
state.title = title
},
setPageSubtitle(state, subtitle) {
state.subtitle = subtitle
},
}
And their values can be updated from any page with:
this.$store.commit('page/setPageTitle', 'A page title')
The same if you want to read the value:
computed: {
title() {
// you can get the variable state without a getter
// ['page'] is the module name, nuxt create the module name
// using the file name page.js
return this.$store.state['page'].title
}
}
The getters are good for format or filter information.
A new module can be added anytime if required, the idea behind vuex and the modules is to have a place with the information that is required in many places through the application, in one place. I.e. the application theme information, if the user select the light or dark theme, maybe the colors can be changed. You can read more about vuex with nuxt here: https://nuxtjs.org/guide/vuex-store/ and https://vuex.vuejs.org/

Prop passed to child component is undefined in created method

I am using Vue.js 2.
I have a problem with passing value to the child component as a prop. I am trying to pass card to card-component.
In card-component I can access the prop in the Card goes here {{card}} section.
However when I try to access it in created or mounted methods it's undefined.
Parent:
<template>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<card-component :card="place.card"></card-component>
</div>
</div>
</div>
</template>
<script>
import CostComponent from './CostComponent';
import CardComponent from './CardComponent';
export default {
components: {
CostComponent, CardComponent
},
props: ['id'],
data() {
return {
place: []
}
},
created() {
axios.get('/api/places/' + this.id)
.then(response => this.place = response.data);
}
}
</script>
Child:
<template>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<ul class="list-unstyled">
Card goes here {{card}}
</ul>
</div>
</div>
</div>
</template>
<script>
import CardItemComponent from './CardItemComponent';
export default {
components: {
CardItemComponent
},
props: ['card'],
created() {
console.log(this.card); // undefined
},
mounted() {
console.log(this.card); // undefined
},
}
</script>
I did a lot of googling but none of the solutions I found have fixed my issue.
This is purely a timing issue. Here's what happens...
Your parent component is created. At this time it has an empty array assigned to place (this is also a problem but I'll get to that later). An async request is started
Your parent component creates a CardComponent instance via its template
<card-component :card="place.card"></card-component>
at this stage, place is still an empty array, therefore place.card is undefined
3. The CardComponent created hook runs, logging undefined
4. The CardComponent is mounted and its mounted hook runs (same logging result as created)
5. Your parent component is mounted
6. At some point after this, the async request resolves and changes place from an empty array to an object, presumably with a card property.
7. The new card property is passed down into your CardComponent and it reactively updates the displayed {{ card }} value in its template.
If you want to catch when the card prop data changes, you can use the beforeUpdate hook
beforeUpdate () {
console.log(this.card)
}
Demo
Vue.component('CardComponent', {
template: '<pre>card = {{ card }}</pre>',
props: ['card'],
created () {
console.log('created:', this.card)
},
mounted () {
console.log('mounted:', this.card)
},
beforeUpdate () {
console.log('beforeUpdate:', this.card)
}
})
new Vue({
el: '#app',
data: {
place: {}
},
created () {
setTimeout(() => {
this.place = { card: 'Ace of Spades' }
}, 2000)
}
})
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app">
<card-component :card="place.card" />
</div>
See https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram
If place is meant to be an object, you should not be initialising it as an array. Also, if your CardComponent relies on data being present, you may want to conditionally render it.
For example
data () {
return { place: null }
}
and
<card-component v-if="place" :card="place.card"></card-component>
then CardComponent will only be created and mounted after place has data.
Make sure you have props: true in the router file. It is a simple solution but many of us forget this.
{
path: '/path-to',
name: 'Name To',
component: Component,
props: true
}

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/