When removing images the last image removed stays (no re-render) - vue.js

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`);
}}

Related

Is there a way to make a loop to create objects in Vue3?

<template>
<div class="container">
<div class="gameboard">
<div v-for="item in boardfields" :key="item.number">
{{ item.number }}
</div>
</div>
</div>
</template>
<script>
export default {
name: "App",
components: {},
data() {
return {
boardfields: [
{ number: 1, isclicked: false },
{ number: 2, isclicked: false },
{ number: 3, isclicked: false },
{ number: 4, isclicked: false },
{ number: 5, isclicked: false },
{ number: 6, isclicked: false },
],
};
},
As you can see I have a few similar objects in the 'boardfields' array. I have to make around 50 of those. Is there a way to create a loop that creates a certain amount of this object with a different number and pushing it to the array so I don't have to copy and paste it and manually change the numbers?
I think in JS it would be something like
var i;
for (var i = 0, i > 50, i++){
this.boardfields.push({number: i, isclicked: false});
}
I think #Boussadjra's answer is correct, but wanted to add some context.
The functional [...Array(50)].map()...etc is the popular way to go these days. You can populate the value on data definition or onmount or oncreate, there are some nuances that might be worthwhile considering.
Note that if you are using:
const initialBoard = []
for (var i = 1; i <= 50; i++) {
initialBoard.push({number: i, isclicked: false});
}
export default {
name: "App",
components: {},
data() {
return {
boardfields: initialBoard
};
},
}
The value of initialBoard is persistent. The objects are created on first run and are populating the array which is re-used. That means if you create two components, they may share the values of the objects inside the array. IMHO, this is a a side effect you want to avoid unless you explicitly looking for that functionality, even if you only use one instance of the component.
B's solution...
export default {
name: "App",
components: {},
data() {
return {
boardfields: [],
};
},
mounted() {
this.boardFields=[...Array(50)].map((_,i)=>({number: i+1, isclicked: false}))
}
}
Is safer in that regard, since it generates a new array with new objects every time it is mounted. My preference would be to use created, because it will make the data available on the first draw, but because the variable is preset to an empty array, it's not going to cause errors (like an error in your template if the variable had .length on undifined or null)
Here is an example that illustrates the difference. Not that when the component is remounted or recreated (doesn't make a difference which on here) the data is lost, but the (top) two components don't share the data., wheras the bottom two do.
const app = Vue.createApp({
data: function() {
return {
cKey: 1
}
}
})
const prepArr = [...Array(5)].map((_, i) => ({
name: 'item-' + i
}))
app.component("my-component", {
data: function() {
return {
myArr: []
}
},
created: function() {
this.myArr = [...Array(5)].map((_, i) => ({
name: 'item-' + i
}))
},
template: `<div class="h">Component Using created<ul>
<li v-for="item in myArr">{{ item.name }} <button #click="()=>{item.name = item.name +'!'}">+</button></li>
</ul></div>`
});
app.component("persistent-component", {
data: function() {
return {
myArr: prepArr
}
},
template: `<div class="h">Component using persistent<ul>
<li v-for="item in myArr">{{ item.name }} <button #click="()=>{item.name = item.name +'!'}">+</button></li>
</ul></div>`
});
app.mount('#app')
.h{display: inline-block; width: 49%;}
<script src="https://unpkg.com/vue#3.0.2/dist/vue.global.prod.js"></script>
<div id="app">
<div><button #click="()=>{cKey++}">regenerate</button></div>
<my-component :key="cKey"></my-component>
<my-component :key="cKey"></my-component>
<persistent-component :key="cKey"></persistent-component>
<persistent-component :key="cKey"></persistent-component>
</div>
You could achieve this by using [...Array(50)] which returns 50 items with undefined values then map this array to return your objects array, this is done in the mounted lifecycle hook :
export default {
name: "App",
components: {},
data() {
return {
boardfields: [],
};
},
mounted(){
this.boardFields=[...Array(50)].map((_,i)=>({number: i+1, isclicked: false}))
}
}
You can run any valid javascript code inside the <script> tag, so this will work
<script>
const initialBoard = []
for (var i = 1; i <= 50; i++) {
initialBoard.push({number: i, isclicked: false});
}
export default {
name: "App",
components: {},
data() {
return {
boardfields: initialBoard
};
},
#Daniel - thanks for the clarification. Upvote for Boussadjra Brahim's answer which is better.
Use a "factory" function to create the data if you wish to have independent boardfields per component.
<script>
const initializeBoard = () => {
const initialBoard = [];
for (var i = 1; i <= 50; i++) {
initialBoard.push({number: i, isclicked: false});
}
return initialBoard;
}
export default {
name: "App",
components: {},
data() {
return {
boardfields: initializeBoard()
};
},

Vue v-model data is from ajax undefined value

I used the vue 2. I had a data from ajax, this is my code example:
<template>
<div>
<input type="input" class="form-control" v-model="siteInfo.siteId">
<input type="input" class="form-control" v-model="siteInfo.info.name">
<input type="input" class="form-control" v-model="siteInfo.accountData.name">
</div>
</template>
<script>
export default {
name: 'Site',
data() {
return {
siteInfo: {},
/* siteInfoName: '', */
}
},
/*computed: {
siteInfoName: function() {
return siteInfo.info.name || '';
},
...
},*/
methods: {
getData() {
// do ajax get data
this.$http.post('URL', {POSTDATA}).then(response => {
/*
response example
{ body:
data: {
sitdeId: 1,
info: { name: 'test'},
accountData: { name: 'accountTest'},
}
}
*/
this.siteInfo = response.body.data;
})
}
},
mounted() {
this.getData();
}
}
</script>
I got a warring message
[Vue warn]: Error in render: "TypeError: Cannot read property 'name'
of undefined"
I can use computed to fix it, but if I had a lot model, I should
write a lot computed.
I should create a lot data for those model?
I should not use an object to bind a lot model?
Does it have another solution for this situation? Thanks your help.
Before the data loads siteInfo.info will be undefined, so you can't access name in the v-model:
v-model="siteInfo.info.name"
Likewise for siteInfo.accountData.name.
My suggestion would be to set the initial value of siteInfo to null and then put a v-if="siteInfo" on the main div. Alternatively you could put a v-if on the individual input elements that checks for siteInfo.info and siteInfo.accountData.
You may also want to consider showing alternative content, such as a load mask, while the data is loading.
Don't be worried about too many v-models - you can do an iteration on the Object - like with Object.entries().
Vue.component('list-input-element', {
props: ['siteLabel', 'siteInfo'],
template: '<div><label>{{siteLabel}}<input type="input" class="form-control" v-model="siteInfo"></label></div>'
})
new Vue({
name: 'Site',
el: '#app',
data() {
return {
siteInfo: {},
}
},
methods: {
getData() {
// using mockup data for this example
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => {
console.log(json)
this.siteInfo = json
})
// do ajax get data
/*this.$http.post('URL', {
POSTDATA
}).then(response => {
this.siteInfo = response.body.data;
})*/
}
},
mounted() {
this.getData();
}
})
div {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<list-input-element v-for="siteInfo in Object.entries(siteInfo)" :site-label="siteInfo[0]" :site-info="siteInfo[1]" />
</div>
Rounding up
So, when you do the single file template, use a computed value, and return an Object from that.
Base your v-for on that computed, and you'll have no problems.
Something like this:
<template>
<div>
<input type="input" class="form-control" v-for="infoEl in siteInfoComputed" v-model="infoEl">
</div>
</template>
<script>
export default {
name: 'Site',
data() {
return {
siteInfo: {},
}
},
computed: {
siteInfoComputed: function() {
// you could check for all the keys-values you want here, and handle
// 'undefined' problem here
// so, actually you "create" the Object here that you're going to use
let ret = {}
// checking if this.siteInfo exists
if (Object.keys(this.siteInfo).length) ret = this.siteInfo
return ret
},
},
methods: {
getData() {
// do ajax get data
this.$http.post('URL', {POSTDATA}).then(response => {
/*
response example
{ body:
data: {
sitdeId: 1,
info: { name: 'test'},
accountData: { name: 'accountTest'},
}
}
*/
this.siteInfo = response.body.data;
})
}
},
mounted() {
this.getData();
}
}
</script>

Use v-model with groupBy array, return flat array

I'm trying to set up a Vue component that takes a flat list of items in an array, groups them by a property for use in a sub-component, and emits the updated flat array.
My section component uses these grouped items in their v-model and emits the updated list. The section component is a drag-and-drop with some input fields, so items are changed under the section component and the updated list is emitted.
Here's an example of the component that takes the flat list as a prop:
<template>
<div>
<div v-for="section in template.sections" :key="section.id">
<h2>{{ section.name }}</h2>
<item-section :section="section" v-model="sectionData[section.id]"></item-section>
</div>
</div>
</template>
<script type="text/javascript">
import { groupBy } from "lodash";
import ItemSection from "#/components/Section.vue";
export default {
name: "ItemAssignment",
props: {
// All items in flat array
value: {
type: Array,
required: true,
default: () => [
/**
* {
* id: null,
* section_id: null,
* name: null
* }
*/
]
},
// Template (containing available sections)
template: {
type: Object,
default: () => {
return {
sections: [
/**
* {
* id: null,
* name: null
* }
*/
]
};
}
}
},
components: {
ItemSection
},
data() {
return {
sectionData: []
};
},
mounted() {},
computed: {
flattenedData() {
return Object.values(this.sectionData).flat();
}
},
methods: {},
watch: {
// Flat list updated
value: {
immediate: true,
deep: true,
handler(val) {
this.sectionData = groupBy(val, "section_id");
}
},
// --- Causing infinite loop ---
// flattenedData(val) {
// this.$emit("input", val);
// },
}
};
</script>
The parent of this component is basically this:
<template>
<div>
<!-- List items should be updatable here or from within the assignment component -->
<item-assignment v-model="listItems"></item-assignment>
</div>
</template>
<script type="text/javascript">
import ItemAssignment from "#/components/ItemAssignment.vue";
export default {
name: "ItemExample",
props: {
},
components: {
ItemAssignment
},
data() {
return {
listItems: []
};
},
mounted() {},
computed: {
},
methods: {
// Coming from API...
importExisting(list) {
var newList = [];
list.forEach(item => {
const newItem = {
id: null, // New record, so don't inherit ID
section_id: item.section_id,
name: item.name
};
newList.push(newItem);
});
this.listItems = newList;
}
},
watch: {
}
};
</script>
When emitting the finalized flat array, Vue goes into an infinite loop trying to re-process the list and the browser tab freezes up.
I believe the groupBy and/or Object.values(array).flat() method are stripping the reactivity out so Vue constantly thinks it's different data, thus the infinite loop.
I've tried manually looping through the items and pushing them to a temporary array, but have had the same issue.
If anyone knows a way to group and flatten these items while maintaining reactivity, I'd greatly appreciate it. Thanks!
So it makes sense why this is happening...
The groupBy function creates a new array, and since you're watching the array, the input event is triggered which causes the parent to update and pass the same value which gets triggered again in a loop.
Since you're already using lodash, you may be able to include the isEqual function that can compare the arrays
import { groupBy, isEqual } from "lodash";
import ItemSection from "#/components/Section.vue";
export default {
// ...redacted code...
watch: {
// Flat list updated
value: {
immediate: true,
deep: true,
handler(val, oldVal) {
if (!isEqual(val, oldVal))
this.sectionData = groupBy(val, "section_id");
}
},
flattenedData(val) {
this.$emit("input", val);
},
}
};
this should prevent the this.sectionData from updating if the old and new values are the same.
this could also be done in flattenedData, but would require another value to store the previous state.

Vue.js - data value does not set from prop

What do I have: two components, parent and child.
Parent
<UserName :name=user.name></UserName>
...
components: {UserName},
data() {
return {
user: {
name: '',
...
}
}
},
created() {
this.fetchUser()
console.log(this.user) //<- object as it is expected
},
methods: {
fetchUser() {
let that = this
axios.get(//...)
.then(response => {
for (let key in response.data) {
that.user[key] = response.data[key]
}
})
console.log(that.user) //<- object as it is expected
}
}
Child
<h3 v-if="!editing" #click="edit">{{ usersName }}</h3>
<div v-if="editing">
<div>
<input type="text" v-model="usersName">
</div>
</div>
...
props: {
name: {
type: String,
default: ''
},
},
data() {
return {
editing: false,
usersName: this.name,
...
}
},
Problem: even when name prop is set at child, usersName data value is empty. I've inspected Vue debug extension - same problem.
What have I tried so far (nothing helped):
1) props: ['name']
2)
props: {
name: {
type: String
},
},
3) usersName: JSON.parse(JSON.stringify(this.name))
4) <UserName :name="this.user.name"></UserName>
P. S. when I pass static value from parent to child
<UserName :name="'just a string'"></UserName>
usersName is set correctly.
I've also tried to change name prop to some foobar. I guessed name might conflict with component name exactly. But it also didn't helped.
user.name is initially empty, and later gets a value from an axios call. usersName is initialized from the prop when it is created. The value it gets is the initial, empty value. When user.name changes, that doesn't affect the already-initialized data item in the child.
You might want to use the .sync modifier along with a settable computed, or you might want to put in a watch to propagate changes from the prop into the child. Which behavior you want is not clear.
Here's an example using .sync
new Vue({
el: '#app',
data: {
user: {
name: ''
}
},
methods: {
fetchUser() {
setTimeout(() => {
this.user.name = 'Slartibartfast'
}, 800);
}
},
created() {
this.fetchUser();
},
components: {
userName: {
template: '#user-name-template',
props: {
name: {
type: String,
default: ''
}
},
computed: {
usersName: {
get() { return this.name; },
set(value) { this.$emit('update:name', value); }
}
},
data() {
return {
editing: false
}
}
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
{{user.name}}
<user-name :name.sync=user.name></user-name>
</div>
<template id="user-name-template">
<div>
<input type="text" v-model="usersName">
</div>
</template>
should be passed like this...
<UserName :name="user.name"></UserName>
if data property is still not being set, in mounted hook you could set the name property.
mounted() {
this.usersName = this.name
}
if this doesn't work then your prop is not being passed correctly.
sidenote: I typically console.log within the mounted hook to test such things.

Issue with update content data in handsontable

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