Let's say I have a form, and can add or remove a column by a click.
I use v-for to render the vue components, when I try to use splice() to delete a specific component, it always delete the last component in the array.
I can't figure out what I did wrong here, any hint will be very appreciated.
Here is a part of my code:
the problem is occured at removePerson method.
Parent Component
<template>
<div class="form-container">
<template>
<div v-for="(child, key) in otherPersonArray" :key="key" class="form-container">
<button #click="removePerson(key)" class="close">X</button>
<component :is="child"></component>
</div>
</template>
<button #click="addPerson" >+ Add Person</button>
</div>
</template>
<script>
import otherPerson from './OtherPerson';
export default {
components: {
otherPerson
},
data() {
return {
otherPersonArray: [],
}
},
methods: {
addPerson() {
if (this.otherPersonArray.length <= 10) {
this.otherPersonArray.push(otherPerson);
}
},
removePerson(key) {
this.otherPersonArray.splice(key, 1);
},
},
}
</script>
For example, when I try to delete the component which input value is 1, it delete the component which input value is 2.
otherPerson component
<template>
<div class="form-container">
<div class="person">
<div class="row">
<div class="form-group col-6">
<label for="inventor-last-name">Lastname of Person *</label>
<div class="input-container">
<input v-model="lastName" type="text" class="form-control form-control-lg">
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
lastName: '',
}
},
}
</script>
You can use Array.prototype.filter
removePerson(key) {
this.otherPerson = this.otherPersonArray.filter((x, i) => i !== key);
}
there are couple of ways to achieve this but for now can you try these:
removePerson(key) {
this.otherPersonArray = this.otherPersonArray.filter(person => {
return person.key === key;
})
}
OR
const index = this.otherPersonArray.indexOf(ele by key); // get index by key person key
if (index > -1) {
this.otherPersonArray.splice(index, 1);
}
What I am understanding key is index here then you should follow this:
var filtered = this.otherPersonArray.filter(function(value, index){
return index !== key;
});
let me know if still it not working for you?
Here is example :
Related
I have a Vue 3 app. In this app, I need to show a list of items and allow a user to choose items in the array. Currently, my component looks like this:
MyComponent.vue
<template>
<div>
<div v-for="(item, index) in getItems()" :key="`item-${itemIndex}`">
<div class="form-check">
<input class="form-check-input" :id="`checkbox-${itemIndex}`" v-model="item.selected" />
<label class="form-check-label" :for="`checkbox-${itemIndex}`">{{ item.name }} (selected: {{ item.selected }})</label>
</div>
</div>
<button class="btn" #click="generateItems">Generate Items</button>
</div>
</template>
<script>
import { reactive } from 'vue';
export default {
data() {
return itemCount: 0
},
methods: {
generateItems() {
this.itemCount = Math.floor(Math.random() * 25) + 1;
},
getItems() {
let items = reactive([]);
for (let i=0; i<this.itemCount; i++) {
items.push({
id: (i+1),
selected: false,
name: `Item #${i+1}`
});
}
return items;
}
}
}
</script>
When I click select/deselect the checkbox, the "selected" text does not get updated. This tells me that I have not bound to the property properly. However, I'm also unsure what I'm doing wrong.
How do I bind a checkbox to the property of an object in an Array in Vue 3?
If you set a breakpoint in getItems(), or output a console.log in there, you will notice every time that you change a checkbox selection, it is getting called. This is because the v-for loop is re-rendered, and it'll call getItems() which will return it a fresh list of items with selected reset to false on everything. The one that was there before is no longer in use by anything.
To fix this, you could only call getItems() from generateItems() for example, and store that array some where - like in data, and change the v-for to iterate that rather than calling the getItems() method.
Steve is right.
Here is a fixed version: Vue SFC Playground.
<template>
<div>
<div v-for="(item, index) in items" :key="`item-${index}`">
<div class="form-check">
<input type="checkbox" class="form-check-input" :id="`checkbox-${index}`" v-model="item.selected" />
<label class="form-check-label" :for="`checkbox-${index}`">{{ item.name }} (selected: {{ item.selected }})</label>
</div>
</div>
<button class="btn" #click="generateItems">Generate Items</button>
</div>
</template>
<script>
import { reactive } from 'vue';
export default {
data() {
return { items: [] }
},
methods: {
generateItems() {
this.itemCount = Math.floor(Math.random() * 25) + 1;
this.getRndItems();
},
getRndItems() {
this.items = reactive([]);
for (let i=0; i<this.itemCount; i++) {
this.items.push({
id: (i+1),
selected: false,
name: `Item #${i+1}`
});
}
}
}
}
</script>
I have a parent component where I am doing the API call and getting the response. So what I am trying to do is pass this response as a prop to child component in Vue.
So here is my parent component and the call:
<button class="btn button col-2" #click="addToCart()">
Add to cart
</button>
addToCart: function () {
let amount = this.itemsCount !== "" ? this.itemsCount : 1;
if(this.variationId != null) {
this.warningMessage = false;
cartHelper.addToCart(this.product.id, this.variationId, amount, (response) => {
this.cartItems = response.data.attributes.items;
});
} else {
this.warningMessage = true;
}
},
So I want to pass this "this.cartItems" to the child component which is:
<template>
<div
class="dropdown-menu cart"
aria-labelledby="triggerId"
>
<div class="inner-cart">
<div v-for="item in cart" :key="item.product.id">
<div class="cart-items">
<div>
<strong>{{ item.product.name }}</strong>
<br/> {{ item.quantity }} x $45
</div>
<div>
<a class="remove" #click.prevent="removeProductFromCart(item.product)">Remove</a>
</div>
</div>
</div>
<hr/>
<div class="cart-items-total">
<span>Total: {{cartTotalPrice}}</span>
Clear Cart
</div>
<hr/>
<router-link :to="{name: 'order'}" class="btn button-secondary">Go To Cart</router-link>
</div>
</div>
</template>
<script>
export default {
computed: {
},
methods: {
}
};
</script>
So I am quite new in vue if you can help me with thi, I would be really glad.
Passing props is quite simple. If cartItems is what you wan´t to pass as a prop, you can do this:
<my-child-component :cartItems="cartItems"></my-child-component>
In this case you implemented your child as myChildComponent. You pass cartItems with :cartItems="cartItems" to it. In your child you do this:
props: {
cartItems: Object
}
Now you can use it with this.cartItems in your methods or {{cartItems}} in your themplate.
Vue.component('Child', {
template: `
<div class="">
<p>{{ childitems }}</p>
</div>
`,
props: ['childitems']
})
new Vue({
el: '#demo',
data() {
return {
items: []
}
},
methods: {
getItems() {
//your API call
setTimeout(() => {
this.items = [1, 2]
}, 2000);
}
}
})
Vue.config.productionTip = false
Vue.config.devtools = false
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<button #click="getItems">get data</button>
<Child v-if="items.length" :childitems="items" />
</div>
You can wait for response, and when you gate this.cartItems then render your child component with a v-if="this.cartItems.length" condition
I pass the value from parent template to child template under this scheme:
parentModel -> parentTemplate -> prop -> childModel -> childTemplate.
That is, when getting in a child model, I need to handle value before installing in template... but it doesn't work!
My method is similar to a kludge =(
Parent:
<template>
<section class="login-wrapper border border-light">
<form id="add-form" class="form-signin" enctype="multipart/form-data" #submit.prevent="send">
<label>
<span>Images</span>
<input type="file" id="files" ref="files" multiple #change="addFile()"/>
</label>
<button type="submit">Submit</button>
</form>
<div id="files-container">
<div v-for="(file, index) in files" class="file-listing" v-bind:key="index">
<Preview :msg="file"></Preview><!-- here I send data to the child with :msg property -->
</div>
</div>
</section>
</template>
<script>
import Preview from "../Partial/ImagePreview.vue"
export default {
name: "ProductAdd",
components: {
Preview
},
data() {
return {
files: []
}
},
methods: {
addFile() {
for (let i = 0; i < this.$refs.files.files.length; i++) {
const file = this.$refs.files.files[i]
this.files.push( file );
}
},
async send() {
/// Sending data to API
}
}
}
</script>
Child:
<template>
<section>
<span>{{ setImage(msg) }}</span><!-- This I would like to avoid -->
<img :src="image_url" alt=""/>
</section>
</template>
<script>
export default {
name: 'ImagePreview',
data: () => {
return {
image_url: ""
}
},
props: [ "msg" ],
methods: {
setImage(data) {
const reader = new FileReader();
reader.onload = (event) => {
this.image_url = event.target.result;
};
reader.readAsDataURL(data);
return null;
}
}
}
</script>
I'm so sorry for a stupid question (perhaps), but I rarely work with frontend.
Now there is such a need =)
PS: I tried using "watch" methods, it doesn't work in this case. When changing an array in the parent component, these changes are not passed to child
But its work.. I see selected image preview
const Preview = Vue.component('ImagePreview', {
data: () => {
return {
image_url: ""
}
},
template: `
<section>
<span>{{ setImage(msg) }}</span><!-- This I would like to avoid -->
<img :src="image_url" alt=""/>
</section>
`,
props: [ "msg" ],
methods: {
setImage(data) {
const reader = new FileReader();
reader.onload = (event) => {
this.image_url = event.target.result;
};
reader.readAsDataURL(data);
return null;
}
}
});
new Vue({
name: "ProductAdd",
components: {Preview},
data() {
return {
files: []
}
},
methods: {
addFile() {
for (let i = 0; i < this.$refs.files.files.length; i++) {
const file = this.$refs.files.files[i]
this.files.push( file );
}
},
async send() {
/// Sending data to API
}
}
}).$mount('#container');
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id='container'>
<section class="login-wrapper border border-light">
<form id="add-form" class="form-signin" enctype="multipart/form-data" #submit.prevent="send">
<label>
<span>Images</span>
<input type="file" id="files" ref="files" multiple #change="addFile()"/>
</label>
<button type="submit">Submit</button>
</form>
<div id="files-container">
<div v-for="(file, index) in files" class="file-listing" v-bind:key="index">
<Preview :msg="file"></Preview><!-- here I send data to the child with :msg property -->
</div>
</div>
</section>
</div>
I'm using the Google places API for my work and everything works fine. However, I have a unique scenario where I need to have more than one delivery address, hence I'll need to make Google place API works that way.
Here is my current code:
<template>
<div>
<div v-for="(search, index) in searchInput" :key="index">
<input type="text" ref="search" v-model="search.city">
</div>
<button type="button" #click="addMore">Add more</button>
</div>
</template>
<script>
export default {
name: "App",
data: () => ({
location: null,
searchInput: [
{
city: ""
}
]
}),
mounted() {
window.checkAndAttachMapScript(this.initLocationSearch);
},
methods: {
initLocationSearch() {
let autocomplete = new window.google.maps.places.Autocomplete(
this.$refs.search
);
autocomplete.addListener("place_changed", function() {
let place = autocomplete.getPlace();
if (place && place.address_components) {
console.log(place.address_components);
}
});
},
addMore() {
this.searchInput.push({ city: "" });
}
}
};
</script>
I get this error from the API: InvalidValueError: not an instance of HTMLInputElement and b.ownerDocument is undefined
How do I make the refs unique for the individual item that I add and then pass it to the initLocationSearch method? Please, any help would be appreciated.
Her is a Codesandbox demo
Given the details you provided for the complete scenario, here is how I would do it.
I would use a simple counter to keep track of the number of inputs and set its initial value to 1 and also use it to display the inputs.
<div v-for="(n, index) in this.counter" :key="index">
<input type="text" ref="search">
</div>
Then bind the Autocomplete widget to the last added input, based on the counter.
autocomplete = new window.google.maps.places.Autocomplete(
this.$refs.search[this.counter - 1]
);
And use your addMore() method to increment the counter.
addMore() {
this.counter++;
}
Use updated() for whenever a new input is added to the DOM to trigger the initLocationSearch() method.
updated() {
this.initLocationSearch();
},
On place_changed event, update the list of selected cities based on the various input values.
this.selectedCities = this.$refs["search"].map(item => {
return { city: item.value };
});
This way you always have an up to date list of cities, whatever input is changed. Of course you would need to handle emptying an input and probably other edge cases.
Complete code:
<template>
<div>
<div v-for="(n, index) in this.counter" :key="index">
<input type="text" ref="search">
</div>
<button type="button" #click="addMore">Add more</button>
<hr>Selected cities:
<ul>
<li v-for="(item, index) in this.selectedCities" :key="index">{{ item.city }}</li>
</ul>
</div>
</template>
<script>
export default {
name: "App",
data: () => ({
selectedCities: [],
counter: 1
}),
mounted() {
window.checkAndAttachMapScript(this.initLocationSearch);
},
updated() {
this.initLocationSearch();
},
methods: {
setSelectedCities() {
this.selectedCities = this.$refs["search"].map(item => {
return { city: item.value };
});
},
initLocationSearch() {
let self = this,
autocomplete = new window.google.maps.places.Autocomplete(
this.$refs.search[this.counter - 1],
{
fields: ["address_components"] // Reduce API costs by specifying fields
}
);
autocomplete.addListener("place_changed", function() {
let place = autocomplete.getPlace();
if (place && place.address_components) {
console.log(place);
self.setSelectedCities();
}
});
},
addMore() {
this.counter++;
}
}
};
</script>
CodeSandbox demo
You can dynamically bind to ref. Something like :ref="'search' + index"
<template>
<div>
<div v-for="(search, index) in searchInput" :key="index">
<input type="text" :ref="'search' + index" v-model="search.city">
</div>
<button type="button" #click="addMore">Add more</button>
</div>
</template>
Then in addMore, before creating a pushing on a new object to searchInput, grab the last index in searchInput. You can then get the newly created ref and pass that to initLocationSearch.
initLocationSearch(inputRef) {
let autocomplete = new window.google.maps.places.Autocomplete(
inputRef
);
autocomplete.addListener("place_changed", function() {
let place = autocomplete.getPlace();
if (place && place.address_components) {
console.log(place.address_components);
}
});
},
addMore() {
const lastInputRef = this.$refs['search' + this.searchInput.length - 1];
this.initLocationSearch(lastInputRef);
this.searchInput.push({ city: "" });
}
I have got it now where I can render the entire array in a random order, just cant render one element of the array. I am also having an issue in showing the entire json object instead of just the text of the quote.
here is the html:
<template>
<div>
<button v-on:click="getTeacupData">Get Teacup Data</button>
<!-- <div>{{ teacupDataList }}</div> -->
<div
v-for="teacupData in teacupDataList"
:key="teacupData.quote"
class="teacup-data"
>
<div>
<span class="quote">
{{
teacupDataList[Math.floor(Math.random() * teacupData.quote.length)]
}}</span
>
</div>
</div>
</div>
</template>
and here is the script:
<script>
import axios from 'axios'
export default {
name: 'Teacup',
data() {
return {
teacupDataList: []
}
},
methods: {
getTeacupData() {
axios.get('/teacupProph.json').then((response) => {
this.teacupDataList = response.data
})
}
}
}
</script>
Add a computed property called randomQuote as follows :
<script>
import axios from 'axios'
export default {
name: 'Teacup',
data() {
return {
teacupDataList: []
}
},
computed:{
randomQuote(){
const rand=Math.floor(Math.random() * this.teacupDataList.length)
return this.teacupDataList[rand]?this.teacupDataList[rand].quote:""
}
},
methods: {
getTeacupData() {
axios.get('/teacupProph.json').then((response) => {
this.teacupDataList = response.data
})
}
}
}
</script>
in template don't use v-for loop just call the computed property :
<template>
<div>
<button v-on:click="getTeacupData">Get Teacup Data</button>
<!-- <div>{{ teacupDataList }}</div> -->
<div>
<span class="quote">
{{
randomQuote
}}</span>
</div>
</div>
</template>
Edit
place your json file inside the components folder and call it like axios('./teacupProph.json') and fix the #:click to #click, check this code