I am losing state in a multiselect component due to unwanted rerendering. My data is structured like this:
form: [
{ key: 0, value: null },
{ key: 1, value: null },
...
]
Each form value generates a multiselect in my template, but if I add a new value (e.g. this.form.push({ key: this.form.length, value: null });), every multiselect in the template is rerendered. This unwanted change of internal state results in the loss of visual feedback on the select boxes.
I've tried setting :key to prevent rerenders, but that hasn't worked. Any recommendations?
The multiselect component is vueform/multiselect. Here is a jsfiddle that shows the behavior: https://jsfiddle.net/libertie/sey1t4mb/31
Inline Example:
const app = Vue.createApp({
data: () => ({
form: [
{ key: 0, value: null }
]
}),
methods: {
add() {
this.form.push({ key: this.form.length, value: null });
},
async fetchRecords(query) {
const response = await axios.get(API_URL)
.then(response => {
if (Array.isArray(response.data.results)) {
return response.data.results;
}
return [];
});
return await response;
}
}
});
<div v-for="field in form" :key="field.key">
<Multiselect
v-model="field.value"
:filter-results="false"
:options="async (query) => await fetchRecords(query)"
searchable
/>
</div>
<button
#click="add"
type="button"
>Add field</button>
Related
I am using buefy taginput in my form, everything works as expect filter using ontype event.
only problem here is that i can see data in taginput box on when i focus, its getting selected too, but when i type its not getting filtered. for 10-15 items its not a problem but when its 1000+ items, it will be difficult. i don't know whats the problem here.
here is code so far.
<template>
<b-field label="Popular Destinations">
<b-taginput
v-model="form.popularSubDests"
:data="allSubDests"
autocomplete
:allow-new="allowNew"
:open-on-focus="openOnFocus"
field="SubDestName"
icon="label"
placeholder="Add Cities"
#typing="getFilteredSubdest"
>
</b-taginput>
</b-field>
</template>
<script>
export default {
data() {
return {
openOnFocus: false,
isSelectOnly: false,
allowNew: false,
allSubDestinations: [],
form: {
popularSubDests: [
{
SubDestName: null,
SubDestId: null,
},
],
},
}
},
computed: {
allSubDests() {
return this.allSubDestinations.map((item) => ({
SubDestId: item.id,
SubDestName: item.subdestname,
}))
},
},
methods: {
getFilteredSubdest(text) {
this.allSubDests = this.allSubDests.filter((option) => {
return option.SubDestName.toString().toLowerCase().indexOf(text.toLowerCase()) >= 0
})
},
},
async asyncdata({ route }) {
let { data: allSubDest } = await axios.get(`process.env.FETCHSUBDEST`)
return {
allSubDestinations: allSubDest.results,
}
},
}
</script>
I use Vue3 with Quasar2 and I have a problem during mutation: the DOM isn't updated.
The code consists of a parent component, a list of child components (rendered by v-for). The data comes from a Vuex store via computed property.
The child components only represent data and emit an 'edit' event with the id of the represented data.
On the 'edit' event the parent component fills properties with the actual data a presents a dialog.
When the dialog closes the parent calls an action to store the data.
The action (and the following mutation) are executed normally. The Vuex store contains the correct data.
But the list isn't updated :(
If I add a new element to the data list (via push) then the list will be updated.
Cleared sources are the following:
Parent.vue:
<template>
<q-page class="flex flex-center" :key="mykey">
<q-list padding>
<child-component
v-for="d in data"
v-bind:key="d.id"
v-bind:data="data"
v-on:edit-time="onEdit"
>
</program>
</q-list>
<!-- EDITOR DIALOG -->
<q-dialog v-model="editing" #hide="onEditDone">
....
</q-dialog>
<script>
....
export default defineComponent({
name: "PageIndex",
created: function () {
this.loadData();
},
data: function () {
return {
editing: false,
....
},
};
},
components: { Child },
methods: {
...mapActions(["loadData", "updateData"]),
onEdit(id) {
...
this.editing = true; // <= show dialog
},
onEditDone() {
try {
...
this.updateData(data); // <== Vuex Action which updates an elem of the data list
} catch (e) {
...
}
},
},
computed: {
...mapState({
data: state => state.data
}),
}
});
</script>
state.js:
export default {
data: [],
}
getters.js:
const getters = {
data: (state) => {
return state.data;
},
};
export default getters;
mutations.js:
updateData: (state, data) => {
var idx = state.data.findIndex((element) => element.id == data.id);
state.data.splice(idx, 1, data);
},
actions.js:
async updateData(context, data) {
try {
await axios.post(`api/data?id=${data.id}`, data, {
headers: {
"Content-Type": "application/json",
},
});
context.commit("updateData", data)
} catch (err) {
console.log(err);
}
},
I have an array of objects, each has a URL that is being loaded in this file:
<template>
<div>
<img :key="id" :src="img" alt="" class="image box" #click="cardClicked" />
</div>
</template>
<script lang="ts">
export default {
props: ["id", "value", "type", "owner", "imgURL"],
data() {
return {
img: require(`./../assets/${this.imgURL}.png`)
};
},
methods: {
cardClicked() {
this.$store.commit("addCardToPlayer", {
id: this.id,
type: this.type,
value: this.value,
owner: this.owner
});
}
}
};
</script>
In the Store mutation I preform filtering and while filtering I add the card to a another player, like so:
addCardToPlayer(state, clickedCard) {
const owner = clickedCard.owner;
const type = clickedCard.type;
const currPlayer = state.currentPlayerName;
if (clickedCard.owner === "deck") {
state.cardOwners[owner].cards[type] = state.cardOwners[owner].cards[
type
].filter(card => {
if (card.id === clickedCard.id) {
state.cardOwners[currPlayer].cards[type].push(card);
return false;
} else return true;
});
}
},
When clicking a card to remove, I see the card being added to the player and displayed correctly, and the number of cards displayed after removal is correct.
But
The card that was removed still shows.
What have I tried:
Forcing to re-render using:
cardClicked() {
this.$store.commit("addCardToPlayer", {
id: this.id,
type: this.type,
value: this.value,
owner: this.owner
});
this.$forceUpdate();
}
Making different components have key, and trying to change the key to cause a re-render:
data() {
return {
componentKey: 0,
};
},
methods: {
forceRerender() {
this.componentKey += 1;
}
}
Tried changing how I change the array by creating a new state.
EDIT: Tried using computed. i get an error:
TypeError: Cannot read property 'imgURL' of undefined
computed: {
getImg: () => {
return require(`./../assets/${this.imgURL}.png`);
}}
EDIT: I wrote the computed function as arrow function, which doesn't preserve context.
SOLVED, by using
computed: {
getImg() {
return require(`./../assets/${this.imgURL}.png`);
}}
How can I make the images update after click (remove).
Thanks.
SOLVED, by using
computed: {
getImg() {
return require(`./../assets/${this.imgURL}.png`);
}}
I'm trying to implement handsontable. As per my requirement, I want to re-render handsontable from changing a dropdown value, but on dropdown selection, the handsontable does not update properly. Below is my code:
Handsontable.vue:
<template>
<div id="hot-preview">
<HotTable :settings="settings" :ref="referenceId"></HotTable>
<div></div>
</div>
</template>
<script>
import { HotTable } from '#handsontable-pro/vue';
export default {
components: {
HotTable
},
props: ['settings', 'referenceId'],
}
</script>
<style>
#hot-preview {
max-width: 1050px;
height: 400px;
overflow: hidden;
}
</style>
Parent component:
<template>
<div id="provisioning-app">
<v-container grid-list-xl fluid>
<v-select
:items="selectList"
item-text="elementName"
item-value="elementName"
label="Standard"
v-model="selected"></v-select>
<handsontable :settings.sync="settings" :referenceId="referenceId"></handsontable>
</v-container>
</div>
</template>
<script>
import Handsontable from '#/components/Handsontable';
import PrevisioningService from '#/services/api/PrevisioningService';
export default {
components: {
Handsontable
},
data: () => ({
selectList: [],
selectApp: [],
selectedOption: '',
referenceId: 'provision-table',
}),
created(){
PrevisioningService.getProvisioningList(this.$session.get('userId'), this.$session.get('customerId')).then(response => {
this.provisioningList = response;
});
},
beforeUpdate() {
this.provisioningApp = this.getProvisioningAppList;
},
computed: {
settings () {
return {
data: this.getSelectApp,
colHeaders: ["Data Uploaded on", "Duration in Minutes", "Start Time", "Shift","Description","Next Day Spill Over", "Site Name"],
columns: [
{type: 'text'},
{type: 'text'},
{type: 'text'},
{type: 'text'},
{type: 'text'},
{type: 'text'},
{type: 'text'}
],
rowHeaders: true,
dropdownMenu: true,
filters: true,
rowHeaders: true,
search: true,
columnSorting: true,
manualRowMove: true,
manualColumnMove: true,
contextMenu: true,
afterChange: function (change, source) {
alert("after change");
},
beforeUpdate: function (change, source) {
alert("before update");
}
}
},
getSelectApp () {
if(this.selectedOption !== undefined && this.selectedOption !== null && this.selectedOption !== ''){
PrevisioningService.getProvisioningAppList(this.selectedOption, this.$session.get('userId'), this.$session.get('customerId')).then(response => {
this.provisioningApp = response;
return this.provisioningApp;
});
}
}
},
method: {
getSelected () {
return this.selectedOption;
}
}
};
</script>
With the above code, my data is received successfully from the server, but I'm unable to update the data in handsontable, as shown in the following screenshots:
How do I properly render the table after the dropdown selection?
I see two issues:
handsontable appears to not handle dynamic settings (see console errors), so settings should not be a computed property. Since the only settings property that needs to be updated is settings.data, that property alone should be mutated (i.e., don't reset the value of settings).
To address this, move settings into data(), initializing settings.data to null so that it would still be reactive:
data() {
settings: {
data: null,
colHeaders: [...],
...
}
},
computed: {
// settings() { } // DELETE THIS
}
getSelectApp is a computed property that is incorrectly asynchronous (i.e., in this case, it fetches data and handles the response later). A computed property cannot be asynchronous, so this computed property actually returns undefined. While there is a return call inside the computed property, the return does not set the value of the computed property because it's inside a Promise callback:
PrevisioningService.getProvisioningAppList(/*...*/).then(response => {
this.provisioningApp = response;
return this.provisioningApp; // DOES NOT SET COMPUTED PROPERTY VALUE
});
Also note the side effect from this.provisioningApp = response. It doesn't seem this.provisionApp is needed in this code in any case, so it should be removed as clean-up.
It seems the intention of this computed property is to update settings.data based on the value of the selected option. To accomplish that, you would have to use a watcher on selectedOption, which would change settings.data.
watch: {
selectedOption(val) {
PrevisioningService.getProvisioningAppList(/*...*/).then(response => {
this.settings.data = response;
});
}
},
demo
So I have a simple template like so:
<resume-index>
<div v-for="resume in resumes">
<resume-update inline-template :resume.sync="resume" v-cloak>
//...my forms etc
<resume-update>
</div>
<resume-index>
Now, inside the resume-updatecomponent I am trying to update the prop on the inside so on the outside it doesn't get overwritten, my code is like so;
import Multiselect from "vue-multiselect";
import __ from 'lodash';
export default {
name: 'resume-update',
props: ['resume'],
components: {
Multiselect
},
data: () => ({
form: {
name: '',
level: '',
salary: '',
experience: '',
education: [],
employment: []
},
submitted: {
form: false,
destroy: false,
restore: false
},
errors: []
}),
methods: {
update(e) {
this.submitted.form = true;
axios.put(e.target.action, this.form).then(response => {
this.resume = response.data.data
this.submitted.form = false;
}).catch(error => {
if (error.response) {
this.errors = error.response.data.errors;
}
this.submitted.form = false;
});
},
destroy() {
this.submitted.destroy = true;
axios.delete(this.resume.routes.destroy).then(response => {
this.resume = response.data.data;
this.submitted.destroy = false;
}).catch(error => {
this.submitted.destroy = false;
})
},
restore() {
this.submitted.restore = true;
axios.post(this.resume.routes.restore).then(response => {
this.resume = response.data.data;
this.submitted.restore = false;
}).catch(error => {
this.submitted.restore = false;
})
},
reset() {
for (const prop of Object.getOwnPropertyNames(this.form)) {
delete this.form[prop];
}
}
},
watch: {
resume: function() {
this.form = this.resume;
},
},
created() {
this.form = __.cloneDeep(this.resume);
}
}
When I submit the form and update the this.resume I get the following:
[Vue warn]: Avoid mutating a prop directly since the value will be
overwritten whenever the parent component re-renders. Instead, use a
data or computed property based on the prop's value. Prop being
mutated: "resume"
I have tried adding computed to my file, but that didn't seem to work:
computed: {
resume: function() {
return this.resume
}
}
So, how can I go about updating the prop?
One solution:
simulate v-model
As Vue Guide said:
v-model is essentially syntax sugar for updating data on user input
events, plus special care for some edge cases.
The syntax sugar will be like:
the directive=v-model will bind value, then listen input event to make change like v-bind:value="val" v-on:input="val = $event.target.value"
So the steps:
create one prop = value which you'd like to sync to parent component
inside the child component, create one data porperty=internalValue, then uses Watcher to sync latest prop=value to data property=intervalValue
if intervalValue change, emit one input event to notice parent component
Below is one simple demo:
Vue.config.productionTip = false
Vue.component('container', {
template: `<div>
<p><button #click="changeData()">{{value}}</button></p>
</div>`,
data() {
return {
internalValue: ''
}
},
props: ['value'],
mounted: function () {
this.internalValue = this.value
},
watch: {
value: function (newVal) {
this.internalValue = newVal
}
},
methods: {
changeData: function () {
this.internalValue += '#'
this.$emit('input', this.internalValue)
}
}
})
new Vue({
el: '#app',
data () {
return {
items: ['a', 'b', 'c']
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div>
<p>{{items}}
<container v-for="(item, index) in items" :key="index" v-model="items[index]">
</container>
</div>
</div>
or use other prop name instead of value (below demo use prop name=item):
Also you can use other event name instead of event name=input.
other steps are similar, but you have to $on the event then implement you own handler like below demo.
Vue.config.productionTip = false
Vue.component('container', {
template: `<div>
<p><button #click="changeData()">{{item}}</button></p>
</div>`,
data() {
return {
internalValue: ''
}
},
props: ['item'],
mounted: function () {
this.internalValue = this.item
},
watch: {
item: function (newVal) {
this.internalValue = newVal
}
},
methods: {
changeData: function () {
this.internalValue += '#'
this.$emit('input', this.internalValue)
this.$emit('test-input', this.internalValue)
}
}
})
new Vue({
el: '#app',
data () {
return {
items: ['a', 'b', 'c']
}
},
methods: {
syncChanged: function (target, index, newData) {
this.$set(target, index, newData)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div>
Event Name=input
<p>{{items}}</p>
<container v-for="(item, index) in items" :key="index" :item="item" #input="syncChanged(items, index,$event)">
</container>
</div>
<hr> Event Name=test-input
<container v-for="(item, index) in items" :key="index" :item="item" #test-input="syncChanged(items, index,$event)">
</container>
</div>
I usually use vuex to manage variables that I will be using in multiple components and like the error says, load them in the various components using the computed properties. Then use the mutations property of the store object to handle changes
In component files
computed: {
newProfile: {
get() {
return this.$store.state.newProfile;
},
set(value) {
this.$store.commit('updateNewProfile', value);
}
},
In the vuex store
state: {
newProfile: {
Name: '',
Website: '',
LoginId: -1,
AccountId: ''
}
},
mutations: {
updateNewProfile(state, profile) {
state.newProfile = profile;
}
}