Force props to update in child component after Vuex update - vuejs2

I have a child component (<BaseProjectTable>) that is re-used throughout my site that contains a Vuetify <v-data-table> component. The headers and content items for the data table are passed into the the component as props, and the item data is mapped into the parent as a mapGetter from a Vuex store. The child component contains editing functionality for each row, and I'm using a mapAction to update the API and Vuex data from there, with the idea being that since my mapGetter is reactive, it should update the data and hence the data table display. However, this is not working; I can see via dev tools that state is updated just fine, but the child component display is not.
Here is the relevant portion of the child <BaseProjectTable> component:
<template>
<div>
<v-data-table
show-expand
:headers="headers"
:items="filteredItems"
:search="search"
tem-key="sow"
#click:row="rowClick"
:hide-default-footer="disablePagination"
dense
:disable-pagination="disablePagination"
>
...
</v-data-table>
...
export default {
name: "BaseProjectTable",
props: {
headers: Array,
items: Array,
loggedInUser: Object,
title: String,
max2chars: v => v.length <= 2 || 'Input too long!',
editDialog: false,
showPracticeFilter: {
default: true,
type: Boolean
},
showSEFilter: {
default: true,
type: Boolean
},
showStatusFilter: {
default: true,
type: Boolean
},
projectType: {
type: String,
default: 'active'
},
disablePagination: {
type: Boolean,
default: true
},
},
},
methods: {
...mapActions('projects', {
saveProject: 'saveProject',
}),
save() {
// Update API and store with new data.
this.saveProject({
projectType: this.projectType,
projectData: this.editedItem
})
},
computed: {
statuses() {
return this.projectType === 'active' ? this.activeStatuses : this.oppStatuses;
},
filteredItems() {
return this.items.filter(d => {
return Object.keys(this.filters).every(f => {
return this.filters[f].length < 1 || this.filters[f].includes(d[f])
})
})
},
}
and here is the relevant code from the parent component:
<v-card>
<BaseProjectTable
:headers="headers"
:items="activeProjects"
:loggedInUser="loggedInUser"
title="Active Projects"
:disablePagination=false
></BaseProjectTable>
</v-card>
...
export default {
computed: {
...mapGetters('projects', {
activeProjects: 'activeProjects',
closedProjects: 'closedProjects',
opportunities: 'opportunities'
}),
}
}
The save() method updates the data in my Vuex store that is referenced by the activeProjects map getter (I have verified in the Vue devtools that this is true). It also shows the items value in the component itself updated in the Components tab in the dev tools. Since using mapGetters makes the data reactive, I expected that it would also update the data in my child component, but it doesn't.
Based on what I saw here, I tried the item-key property of the <v-data-table> like so:
<v-data-table
show-expand
:headers="headers"
:items="filteredItems"
:search="search"
item-key="sow"
#click:row="rowClick"
:hide-default-footer="disablePagination"
dense
:disable-pagination="disablePagination"
>
(every record using this component will have the unique sow key), but that didn't work.
The only think I could think if is how I'm editing the data in my mutation:
export const state = () => ({
active: [],
closed: [],
opportunities: [],
})
export const getters = {
activeProjects: state => state.active,
closedProjects: state => state.closed,
opportunities: state => state.opportunities,
}
export const actions = {
saveProject({ commit }, {projectType, projectData}) {
commit(types.SAVE_PROJECT, {projectType, projectData});
}
}
export const mutations = {
[types.SAVE_PROJECT](state, {projectType, projectData}) {
// Get project from state list by sow field.
const index = state[projectType].findIndex(p => p.sow === projectData.sow);
state[projectType][index] = projectData;
}
}
as compared to replacing the entire state[projectType] value.
What do I need to do to get the data table to display my updated value?

From the Vue documentation,
Vue cannot detect the following changes to an array
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g. vm.items.length = newLength
Replace what you have with
import Vue from 'vue'; // this should be at the top level
export const mutations = {
[types.SAVE_PROJECT](state, {projectType, projectData}) {
// Get project from state list by sow field.
const index = state[projectType].findIndex(p => p.sow === projectData.sow);
Vue.set(state[projectType], index, projectData)
}
}
After this, the changes to the array will be detected and the getter will work as expected.

Related

Vue child component not rerending after parent component's data value updates

I am trying to have a child component update its props that were passed from the parents at the start of the rendering. Since the value is coming from a fetch call, it takes a bit of time to get the value, so I understand that the child component will receive a 'null' variable. But once the fetch call is completed, the value is updated but the child component still has the original null value.
During my search for a solution, I found that another way was to use Vuex Stores, so I implemented it with the count variable and had a button to call a commit and later dispatch with an action function to the store to increment it's value but when the increment happens, it doesn't show the new value on the screen even though with console logs I confirmed it did change the value when the function was called.
I guess I don't fully understand how to update the value of a variable without reassigning it within it's own component or having to call a separate function manually right after I change the value of a data variable.
App.vue
<template>
<div id="app">
<div id="banner">
<div>Title</div>
</div>
<p>count: {{count}}</p> // a small test i was doing to figure out how to update data values
<button #click="update">Click </button>
<div id="content" class="container">
<CustomDropdown title="Title Test" :valueProps="values" /> // passing the data into child component
</div>
</div>
</template>
<script>
import CustomDropdown from './components/CustomDropdown.vue'
export default {
name: 'App',
components: {
CustomDropdown,
},
data() {
return {
values: null
count: this.$store.state.count
}
},
methods: {
update() {
this.$store.dispatch('increment')
}
},
async created() {
const response = await fetch("http://localhost:3000/getIds", {
method: 'GET',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json'
}
});
const data = await response.json();
this.values = data // This is when I expect the child component to rerender and show the new data. data is an array of objects
console.log("data", data, this.values) // the console log shows both variables have data
}
}
</script>
CustomDropDown.vue
<template>
<div id="dropdown-container" class="">
<b-dropdown class="outline danger" variant="outline-dark" :text="title" :disabled="disabled">
<b-dropdown-item
v-for="value in values"
:key="value.DIV_ID"
href="#">
{{value.name}}
</b-dropdown-item>
</b-dropdown>
</div>
</template>
<script>
export default {
name: 'CustomDropdown',
components: {},
props: {
title: String,
valuesProp: Array,
disabled: Boolean
},
data() {
return {
values: this.valuesProp
}
},
methods: {
},
created() {
console.log("dropdown created")
console.log(this.valuesProp) //Always undefined
}
}
</script>
store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state() {
return {
count: 0,
divisionIds: []
}
},
mutations: {
increment (state) {
console.log("count", state.count)
state.count++
}
},
actions: {
increment (state) {
console.log("count action", state.count)
state.commit('increment')
}
}
})
data in your child component CustomDropdown.vue is not reactive: therefore the value of this.values is not updated when the prop changes. If you want to alias a prop, use computed instead:
export default {
name: 'CustomDropdown',
components: {},
props: {
title: String,
valuesProp: Array,
disabled: Boolean
},
computed: {
values() {
return this.valuesProp;
}
},
created() {
console.log("dropdown created");
}
}
If you want to console log the most updated values of this.valuesProp, you will need to watch it: the same if you want for this.values.
One thing you can do is to use a v-if in your child component to only render it after you get your result from you api.
It would be something like:
<CustomDropdown title="Title Test" :valueProps="values" v-if="values"/>
This way you would make sure that your child component gets rendered only when values are available.
It would only be a bad solution if this api call took so long and you needed to display the child component data to the user before that.
Hey you can simply watch it your child component
watch: { valuesProp: function(newVal, oldVal) { // watch it if(newVal.length > 0) do something }
it will watch for the value changes and when you get your desired value you can perform whatever hope it will help you you dont need store or conditional binding for it.

how to make the nuxt child component wait until asyncData call get finish

For a form we have 2 components parent(for calling asyncdata and pass data as props to child) & child(form). I can properly fetch the props in child if I navigate using a link. But If I try to refresh the child component page it throws error as no props is passed. Found the reason to be that the parents asyncdata is not completing before the child render to sent the data in props.
Parent Component
<template>
<div>
<p>EDIT</p>
<NewListingModal :is-edit="true" :form-props="this.form" />
</div>
</template>
<script>
import NewListingModal from '#/components/NewListingModal.vue'
export default {
components: { NewListingModal },
async asyncData({ params, store }) {
const listing = await store.$db().model('listings').find(params.listing) //vuexorm call
if (typeof listing !== 'undefined') {
const convertedListing = JSON.parse(JSON.stringify(listing))
return {
name: '',
scrollable: true,
form: {names: convertedListing.names}
}
}
},
}
</script>
child component(other form data is removed to keep it understandable)
<template>
<div v-for="name in this.form.names" :key="name">
<p>{{ name }} <a #click.prevent="deleteName(name)">Delete<a /></a></p>
</div>
</template>
<script>
import Listing from '#/models/listing'
export default {
name: 'ListingModal',
props: {isEdit: {type: Boolean, default: false}, formProps: {type: Object}},
data() {
return {
name: '',
scrollable: true,
form: {names: this.formProps.names}
}
},
methods: {
addName() {
this.form.names.push(this.name)
this.name = ''
},
deleteName(name) {
const names = this.form.names
names.splice(names.indexOf(name), 1)
}
}
}
</script>
How can I make the NewListingModal component rendering wait until the asyncData completes in parent?
In my case, I used asyncData in my parent nuxt component, which fetches the data via store dispatch action, then set it to some store state key, via mutation.
Then I used validate method in my child component. Since Nuxt validate can return promises, I checked the vuex store first for fetched data. If there is none, I refetch it and return the promise instead.
In Parent component.vue
export default {
async asyncData({ params, store }) {
// Api operation which may take sometime
const {data} = await store.dispatch('fetch-my-data')
store.commit('setData', data) //This should be within above dispatch, but here for relevance
}
}
Here I am only fetching and saving to vuex store.
Child component.vue
export default {
async validate({ params, store }) {
let somedata = store.state.data //This is what you've set via parent's component mutation
return !!somedata || store.dispatch('fetch-my-data')
}
}
Here I am returning either the vuex store data (if exists), else refetch it.

Why is my vue3 component not updating when I use a method?

I have a card picker component that let's the user pick a number of cards from a deck. This is my Vue3 code:
<template>
<v-layout row justify-center>
<card3
v-for="(card, index) in cards"
:key="index"
:card="card"
:cardWidth="cardWidth"
:isSelected="contains(card, selectedCards)"
v-on="$listeners"
></card3>
</v-layout>
</template>
<script lang="ts">
import { defineComponent, ref, contains, watch, computed } from "#/common"
import Card3 from "#/components/Card3.vue"
import { Cards, Card } from "#/types"
type Props = {
selectedCards: Cards
}
export default defineComponent({
props: {
cards: {
type: Array,
default: () => []
},
selectedCards: {
type: Array,
default: () => []
},
faceUp: {
type: Boolean,
default: true
},
cardWidth: {
type: Number,
default: 16
}
},
components: {
Card3
},
setup(props: Props, context) {
const { selectedCards } = props
return {
contains
}
}
})
</script>
The component takes cards and selectedCards from the parent component. It then makes a check to see if the card is selected before passing that information on to a child component which renders the card and puts a border around selected cards.
Everything is working as expected at this point!
However, now I want to make one small refactor. I make a method isSelected and expose it to the template:
const isSelected = (card: Card) => contains(card, selectedCards)
return {
contains,
isSelected
}
I then use the method in the template instead of contains:
:isSelected="isSelected(card)"
However, the component has stopped working now! The selectedCards is updated as expected but the child component is not updating the selected cards as expected. The border indicating the selected cards is not being rerendered when I make this single change.
Can someone explain me why?
I have tried to put all this into a working example on codesandbox.io but I had to give up unfortunately.
I found the problem. The problem is that I am destructuring my props and thereby loose reactivity.
If I instead do this:
const isSelected = (card: Card) => contains(card, props.selectedCards)
or this:
const { selectedCards } = toRefs(props)
...then I (apparently) keep reactivity and it works. Talk about a gotcha!
Thanks for the help!

vuejs treeselect - delay loading does not work via vuex action

Using Vue TreeSelect Plugin to load a nested list of nodes from firebase backend. It's doc page says,
It's also possible to have root level options to be delayed loaded. If no options have been initially registered (options: null), vue-treeselect will attempt to load root options by calling loadOptions({ action, callback, instanceId }).
loadOptions (in my App.vue) dispatch vuex action_FolderNodesList, fetches (from firebase) formats (as required by vue-treeselect), and mutates the state folder_NodesList, then tries to update options this.options = this.get_FolderNodesList but this does not seems to work.
Here is the loadOptions method (in app.vue)
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
Vue errors out with Invalid prop: type check failed for prop "options". Expected Array, got String with value ""
I am not sure what am I doing wrong, why that does not work. A working Codesandbox demo
Source
App.vue
<template>
<div class="section">
<div class="columns">
<div class="column is-7">
<div class="field">
<Treeselect
:multiple="true"
:options="options"
:load-options="loadOptions"
:auto-load-root-options="false"
placeholder="Select your favourite(s)..."
v-model="value" />
<pre>{{ get_FolderNodesList }}</pre>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Treeselect from "#riophae/vue-treeselect";
import "#riophae/vue-treeselect/dist/vue-treeselect.css";
export default {
data() {
return {
value: null,
options: null,
called: false
};
},
components: {
Treeselect
},
computed: mapGetters(["get_FolderNodesList"]),
methods: {
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
}
};
</script>
Store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
folder_NodesList: ""
},
getters: {
get_FolderNodesList(state) {
return state.folder_NodesList;
}
},
mutations: {
mutate_FolderNodesList(state, payload) {
state.folder_NodesList = payload;
}
},
actions: {
action_FolderNodesList({ commit }) {
fmRef.once("value", snap => {
var testObj = snap.val();
var result = Object.keys(testObj).reduce((acc, cur) => {
acc.push({
id: cur,
label: cur,
children: recurseList(testObj[cur])
});
return acc;
}, []);
commit("mutate_FolderNodesList", result);
});
}
}
});
Any help is appreciated.
Thanks
It seems you are calling this.options which would update the entire element while only the current expanding option should be updated.
It seems loadOptions() is called with some arguments that you can use to update only the current childnode. The first argument seems to contain all the required assets so I wrote my loadTreeOptions function like this:
loadTreeOptions(node) {
// On initial load, I set the 'children' to NULL for nodes to contain children
// but inserted an 'action' string with an URL to retrieve the children
axios.get(node.parentNode.action).then(response => {
// Update current node's children
node.parentNode.children = response.data.children;
// notify tree to update structure
node.callback();
}).catch(
errors => this.onFail(errors.response.data)
);
},
Then I set :load-options="loadTreeOptions" on the <vue-treeselect> element on the page. Maybe you were only missing the callback() call which updates the structure. My installation seems simpler than yours but it works properly now.

Vue component computed not reacting

I have 2 components OperatorsList and OperatorButton.
The OperatorsList contains of course my buttons and I simply want, when I click one button, to update some data :
I emit select with the operator.id
This event is captured by OperatorList component, who calls setSelectedOperator in the store
First problem here, in Vue tools, I can see the store updated in real time on Vuex tab, but on the Components tab, the operator computed object is not updated until I click antoher node in the tree : I don't know if it's a display issue in Vue tools or a real data update issue.
However, when it's done, I have another computed property on Vue root element called selectedOperator that should return... the selected operator : its value stays always null, I can't figure out why.
Finally, on the button, I have a v-bind:class that should update when the operator.selected property is true : it never does, even though I can see the property set to true.
I just start using Vue, I'm pretty sure I do something wrong, but what ?
I got the same problems before I used Vuex, using props.
Here is my OperatorList code :
<template>
<div>
<div class="conthdr">Operator</div>
<div>
<operator-button v-for="operator in operators" :op="operator.id"
:key="operator.id" #select="selectOp"></operator-button>
</div>
</div>
</template>
<script>
import OperatorButton from './OperatorButton';
export default {
name: 'operators-list',
components : {
'operator-button': OperatorButton
},
computed : {
operators() { return this.$store.getters.operators },
selected() {
this.operators.forEach(op =>{
if (op.selected) return op;
});
return null;
},
},
methods : {
selectOp(arg) {
this.$store.commit('setSelectedOperator', arg);
}
},
}
</script>
OperatorButton code is
<template>
<span>
<button type="button" v-bind:class="{ sel: operator.selected }"
#click="$emit('select', {'id':operator.id})">
{{ operateur.name }}
</button>
</span>
</template>
<script>
export default {
name: 'operator-button',
props : ['op'],
computed : {
operator() {
return this.$store.getters.operateurById(this.op);
}
},
}
</script>
<style scoped>
.sel{
background-color : yellow;
}
</style>
and finally my app.js look like that :
window.Vue = require('vue');
import Vuex from 'vuex';
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
const store = new Vuex.Store({
state: {
periods : [],
},
mutations: {
setInitialData (state, payload) {
state.periods = payload;
},
setSelectedOperator(state, payload) {
this.getters.operateurs.forEach( op => {
op.selected = (op.id==payload.id)
})
},
},
getters : {
operators : (state) => {
if (Array.isArray(state.periods))
{
let ops = state.periods
.map( item => {
return item.operators
}).flat();
ops.forEach(op => {
// op.selected=false; //replaced after Radu Diță answer by next line :
if (ops.selected === undefined) op.selected=false;
})
return ops;
}
},
operatorById : (state, getters) => (id) => {
return getters.operators.find(operator => operator.id==id);
},
}
});
import Chrono from './components/Chrono.vue';
var app = new Vue({
el: '#app',
store,
components : { Chrono },
mounted () {
this.$store.commit('setInitialData',
JSON.parse(this.$el.attributes.initialdata.value));
},
computed: {
...mapState(['periods']),
...mapGetters(['operators', 'operatorById']),
selectedOperator(){
this.$store.getters.operators.forEach(op =>{
if (op.selected) return op;
});
return null;
}
},
});
Your getter in vuex for operators is always setting selected to false.
operators : (state) => {
if (Array.isArray(state.periods))
{
let ops = state.periods
.map( item => {
return item.operators
}).flat();
ops.forEach(op => {
op.selected=false;
})
return ops;
}
}
I'm guessing you do this for initialisation, but that's a bad place to put it, as you'll never get a selected operator from that getter. Just move it to the proper mutations. setInitialData seems like the right place.
Finally I found where my problems came from :
The $el.attributes.initialdata.value came from an API and the operator objects it contained didn't have a selected property, so I added it after data was set and it was not reactive.
I just added this property on server side before converting to JSON and sending to Vue, removed the code pointed by Radu Diță since it was now useless, and it works.