Why is my vue3 component not updating when I use a method? - vue.js

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!

Related

Trigger method on all components inside of a v-for

I have an audio player component that needs to send a message to all other players to pause themselves when it is clicked. However, there's a variable amount of components all inside of a v-for loop. It looks something like this:
<template>
<AudioPlayer v-for="song in songs" :key="song.id" #play="pauseOthers" />
</template>
<script>
import { defineComponent, ref } from 'vue';
import AudioPlayer from '#/components/AudioPlayer.vue';
export default defineComponent({
setup(){
var songs = [{id: "song1"},{id: "song2"}]
const pauseOthers = () => {
//this is the part that I need to figure out
}
return { songs, pauseOthers }
}
})
</script>
The components's script looks like
<script>
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup(){
const pause = () => {
console.log("Paused!");
}
return { pause }
}
})
</script>
Ideally this could avoid using third-party packages, but it's fine if there really is no easy way.
Basically what you are going to want to do is create a a ref on the component which contains the v-for loop (i.e. the parent component) that is something like const currentlyPlayingPlayerIndex = ref(-1) then provide that as a prop to your AudioPlayer component. In AudioPlayer you use a watcher that watches for the value of that prop to change, if the index of that player (order in the v-for list) matches the value of currentlyPlayingPlayerIndex then take whatever action is required to play the audio. If it doesn't match, take whatever action is required to pause the audio. This will ensure that only one player is ever playing at once.
Within the player component the "pause" button should emit an event that sets the value of currentlyPlayingPlayerIndex to -1 or null or something that is outside the bounds of the range of the indexes. The "play" button should emit an event to the parent that updates the value of currentlyPlayingPlayerIndex to the index of the player component that contained the clicked button.
In summary, don't manage the "playing" state on the level of player but instead on their parent.
I agree with #WillD, you should manage those events from the parent or you will overcomplicate it.
Example:
<!-- Parent -->
<template>
<AudioPlayer v-for="song in songs" :key="song.id" :song-id="song.id" #play="pauseOthers" />
</template>
<script lang="ts" setup>
const songs = [
{ id: "song1" },
{ id: "song2" },
{ id: "song3" },
{ id: "song4" },
{ id: "song5" }
];
const pauseOthers = (songId: string) => {
const songsToStop = songs.filter(song => song.id !== songId);
console.log("songs to stop:", songsToStop.map(song => song.id).join(" "));
};
</script>
<!-- Audioplayer children component -->
<template>
<button block #click="emit('play', props.songId)">
something
</button>
</template>
<script lang="ts" setup>
const props = defineProps({
songId: {
type: String,
required: true
}
});
const emit = defineEmits(["play"]);
</script>
Note that all those props and emits are just created for being able to reproduce an example, you probably won't need them in your final code.

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.

Vue 3 pass reactive object to component with two way binding

I have an issue in the two way binding of a reactive component in vue 3 using the composition API.
The setup:
The parent calling code is:
<template>
<h1>{{ message.test }}</h1>
<Message v-model="message" />
</template>
<script>
import Message from '#/components/Message.vue';
import { reactive } from 'vue';
export default {
name: 'Home',
components: { Message },
setup() {
const message = reactive({ test: '123' });
return {
message
};
}
};
</script>
The child component code is:
<template>
<label>
<input v-model="message" type="text" />
</label>
</template>
<script>
import { computed } from 'vue';
export default {
props: {
messageObj: {
type: Object,
default: () => {},
},
},
emits: ['update:messageObj'],
setup(props, { emit }) {
const message = computed({
get: () => props.messageObj.test,
set: (value) => emit('update:messageObj', value),
});
return {
message,
};
},
};
</script>
The problem:
When the component is loaded, the default value from the object is shown in the input field.
This is as it should be, however, when I update the value in the input box the H1 in the parent view is not getting updated with the new input box value.
I have searched through the stackoverflow board and google but have not found any hint as to what needs to be done to make the object reactive.
I read through the reactivity documentation but still have not found any solution for my issue.
For testing I have changed message to be a ref and using this single ref value the data remains reactive and everything is working as expected.
Any pointers on what can be the issue with the reactive object not updating?
Here
<div id="app">
<h1>{{ message.test }}</h1>
<child v-model="message"></child>
</div>
const { createApp, reactive, computed } = Vue;
// -------------------------------------------------------------- child
const child = {
template: `<input v-model="message.test" type="text" />`,
props: {
modelValue: {
type: Object,
default: () => ({}),
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
});
return { message };
}
};
// ------------------------------------------------------------- parent
createApp({
components: { child },
setup() {
const message = reactive({ test: 'Karamazov' });
return { message };
}
}).mount('#app');
Solution and observations:
In the parent view which is calling the component you can use v-model and add a parameter to that v-model if you need to pass only one of the values in the object.
<template>
<h1>{{ message.test }}</h1>
<!-- <h1>{{ message }}</h1> -->
<Message v-model:test="message" />
</template>
<script>
import Message from '#/components/Message.vue';
import { reactive } from 'vue';
export default {
name: 'Home',
components: { Message },
setup() {
const message = reactive({ test: '123' });
return {
message
};
}
};
</script>
In the receiving component you then register the parameter of the object that was passed in props as an object.
<template>
<label>
<input v-model="message.test" type="text" />
</label>
</template>
<script>
import { computed } from 'vue';
export default {
props: {
test: {
type: Object,
default: () => {}
},
},
emits: ['update:test'],
setup(props, { emit }) {
const message = computed({
get: () => props.test,
set: (value) => emit('update:test', value),
});
return {
message,
};
},
};
</script>
If you need to pass the whole object you need to use as a prop in the component the name modelValue.
Change in parent compared to previous code:
<template>
<h1>{{ message.test }}</h1>
<!-- <h1>{{ message }}</h1> -->
<Message v-model="message" />
</template>
Code of the component:
<template>
<label>
<input v-model="message.test" type="text" />
</label>
</template>
<script>
import { computed } from 'vue';
export default {
props: {
modelValue: {
type: Object,
default: () => {}
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
return {
message,
};
},
};
</script>
Should be pretty straight forward, and no computed is needed. See example below.
The messageObj was replaced with message in the child component for the emit to work (which would break due to case sensitivity in this demo)
const app = Vue.createApp({
setup() {
const message = Vue.reactive({ test: '123' , foo: "bark"});
return {
message,
};
}
})
app.component('Message', {
props: {
message: {
type: Object,
default: () => {},
},
},
emits: ['update:message'],
setup(props, { emit }) {
const message = props.message;
return { message };
},
template: document.querySelector('#t_child')
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.2/dist/vue.global.prod.js"></script>
<fieldset>
<div id="app">
<h1>{{ message.test }} || {{ message.foo }}</h1>
<fieldset><Message v-model:message="message"/></fieldset>
</div>
</fieldset>
<template id="t_child">
<label>
<h4>{{message}}</h4>
<input v-model="message.test" type="text" />
<input v-model="message.foo" type="text" />
</label>
</template>
Your initial problem is quite simple. In Vue 3 v-model defaults to to a prop called modelValue and emits come from update:modelValue. Other answers here have assumed that in their solutions but not directly addressed it.
You can either rename your messageObj prop to use the default prop OR use the multi-model features in Vue 3:
<Message v-model:messageObj="message" />
However our problems run deeper.
All (current) answers will work but aren't quite correct. They all fail the idiomatic "One-way Data Flow" rule.
Consider this JSFiddle, modified from this answer.
const child = {
template: `<input v-model="message.test" type="text" />`,
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue,
// No set() ?
});
return { message };
}
}
In this example, the child component never 'emits' - yet the data is still updating in the parent component. This violates the "One-way" rule. Data must be propagated from child components using only emits and not via prop proxies.
The problem in here is that props.modelValue is reactive when arrives in the child component. One can verify this with the isReactive() helper. When it's passed through the computed() it retains that reactiveness and will continue to proxy updates through itself into the parent component.
A solution:
JSFiddle here
const { createApp, ref, computed } = Vue;
const child = {
template: `<input v-model="message" type="text" />`,
props: {
modelValue: {
type: Object,
default: () => ({}),
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue.test,
set: (test) => emit('update:modelValue', ({...props.modelValue, test })),
});
return { message };
}
};
createApp({
components: { child },
setup() {
const message = ref({ test: 'Karamazov' });
return { message };
}
}).mount('#app');
The solution is three parts:
The computed getter must not return the proxy object from the parent component. Once this happens you're in danger of violating the "one-way" rule [note 1]. In this example props.modelValue.test is a string so we're safe.
The computed setter must emit the whole object, but again it must not be a reactive type. So we clone the modelValue using spread and include in the updated test field. This can also be achieved with Object.assign({}, props.modelValue, {test}) [note 2].
The message variable in the parent component cannot be a reactive() and must be a ref(). When the v-model receives the newly emitted object the message variable is clobbered and no longer reactive [note 3]. Even with refs the props.modelValue will still fully reactive when it arrives in the child component, so the cloning steps are still important.
Alternatively:
I should also mention that values from computed() are not deeply reactive. As in, setting values on a computed object will not trigger the computed setter.
An alternate solution for passing the whole object through to your template:
setup(props, { emit }) {
const message = reactive({...props.modelValue});
watch(message, message => emit('update:modelValue', ({...message})));
return { message };
}
In this, the whole message object will emit whenever the .test field is updated. E.g. <input v-model="message.test" />. This still obeys the "one-way" data rule because emits are the only way data is given to parent component.
Reasoning:
"One-way" data flow is important [4]. Consider this:
<child :modelValue="message"></child>
On a first (and a sensible) glance, this appears to pass data into 'child' but not out of 'child'. But, given a reactive object that is not handled by the child correctly, this will emit changes into my own component.
Observing this code I don't expect this behaviour so it's very important that the child component gets it right.
Notes:
[1]: Testing violations of the "one-way" rule are surprisingly simple. Remove any emit and if the parent receives updates - you've broken it. Or replacing v-model with v-bind also works.
[2]: Object.assign() and {...} spread are indeed different. But shouldn't affect our uses here.
[3]: I haven't found any clear documentation about this behaviour regarding reactive() and v-model. If anyone wants to chime in, that'd be great.
[4]: The Vue docs stress the importance of one-way bind. Evan himself (creator of Vue) even provides examples about how to use v-model with objects (in Vue 2, but the principles still apply).
I feel it's also important to note later in the same thread Evan suggests objects that are nested more than 1-level are considered misuse of v-model.
It turns out that 2 way binding of object properties with Vue 3 is even easier than demonstrated in any of the previous answers.
Parent Code (App.vue):
<script setup>
import Controller from './components/Controller.vue';
import { reactive } from 'vue';
const object1 = reactive({name: "Bruce", age: 38});
const object2 = reactive({name: "Alex", age: 6});
</script>
<template>
<div>
{{object1}}<br/>
{{object2}}
<Controller :my-object="object1"/>
<Controller :my-object="object2"/>
</div>
</template>
Component code (Controller.vue):
<script setup>
import { computed } from 'vue'
const props = defineProps({
myObject: {
type: Object,
default: () => {}
}
})
const name = computed({
get () {
return props.myObject.name
},
set (value) {
props.myObject.name = value
}
})
const age = computed({
get () {
return props.myObject.age
},
set (value) {
props.myObject.age = parseInt(value)
}
})
</script>
<template>
<div>
<input v-model="name"/><br/>
<input v-model="age" type="number"/>
</div>
</template>
Explanation:
The <component :my-object="object1" /> syntax uses a : to tell Vue that we are passing an object (object1), rather than a string to the component and assigning it to property myObject. It turns out that when the child component receives this property, its reactivity is still intact. Therefore, as long as we don't mutate myObject itself, but instead only modify its properties, there is no need to emit any events or even pass it with as a property called v-model (we can call the property whatever we want). Instead the javascript proxy that the reactive keyword creates will do all the work tracking the changes and re-rendering it.
Some testing reveals that it is even possible to add new properties to the object or change deep properties and still maintain reactivity.
I am just a beginner with Vue, so there may be reasons why using this method are an anti-pattern, with unintended future consequences...

How to access child's props from wrapper component in Vue?

I have a bunch of Vue components on the marketing website. There are stateful and functional (aka stateless) components. For example badge and button. I need to create a wrapper component for a testing purpose, that takes any component, renders that component but additionally adds a table below with props-values, passed along with child component. For instance:
<XButton hello="hi" :some-other-prop="2" label="Hey">You Will Never Appear</XButton>
will be rendered to a Button tag with the information below:
<button>Such text with label Hey</button>
some-other-prop | 2
label | Hey
Button.js
Vue.component('XButton', {
name: 'XButton',
functional: true,
props: {
label: {
type: String,
default: 'Default text',
},
},
render(createElement, context) {
return createElement('button', context.data, 'Such text with label ' + context.props.label);
},
});
Wrapper.vue
<template>
<div>
<slot/>
</div>
</template>
<script>
export default {
functional: true,
props: {
listPropsOfDoubleNestedChild: {
type: Boolean,
default: false
}
},
render(createElement, context) {
const vNode= context.props.listPropsOfDoubleNestedChild
? context.children[0].children // or even get props of double nested child
: context.children[0];
if (vNode.componentOptions) {
console.log("Props of nested stateful component", vNode.componentOptions.propsData);
} else {
console.log("Props of nested stateless component are not reachable thru componentOptions", vNode.componentOptions);
}
return null;
}
</script>
App.vue
<template>
<Wrapper>
<XButton hello="hi" :some-other-prop="2" label="Hey">You Will Never Appear</XButton>
</Wrapper>
</template>
So, I can only get passed props of the stateful component.
Some screenshots:
Stateful component:
Functional component:
In React it is very simple to accomplish with React.Children.only(props.children).props and what about Vue?

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.