How to track changed Valued from array of objects in vuejs - javascript

I have an addRows array which contains a group of objects.
These objects are added dynamically when using click + button.
Then they are pushed to the addRows array; after that, the user fills these objects.
Every object has many input values such as price and quantity.
I need to change the price when the quantity changes, but the problem is if the value was added before, the returned changed item is empty.
This happens because the new value was added before in the old values array.
As shown in the image third value same first so the array returned empty value of third item
I tried foreach and for also map but I'm facing the same problem.
computed: {
originalDistValue(a , b) {
return this.addRows.map(i => {
return i.dist
})
}
}, // close computed
watch: {
originalDistValue(a , b) {
let newVal = a.filter( obj => {
return b.indexOf(obj) == -1;
})
let element = this.addRows.filter( i => {
return i.dist == newVal;
})
deep: true
console.log(newVal)
for(let i in element) {
element[i].cit = 1;
}
}
}

At first create a child component for each row
Pass each row to component
Handle the single object inside the component instead of handling an array of objects in parent
See this example:
Vue.component('child', {
props: {
value: { type: Object, default: () => ({}) }
},
data: function() {
return {
selfValue: null
}
},
watch: {
value: {
handler(value) {
this.selfValue = value
},
immediate: true,
deep: true
},
selfVaue: {
handler(value) {
// you can also validate value then emit it
this.$emit('input', value)
},
deep: true
},
'selfValue.quantity'() {
this.selfValue.price = 0
}
},
template: `
<div class="d-flex justify-content-between">
<label>
Title:
<input type="text" v-model="selfValue.title" class="form-control" />
</label>
<label>
Quantity:
<select v-model="selfValue.quantity" class="form-control">
<option value="A">A</option>
<option value="B">B</option>
</select>
</label>
<label>
Price:
<input type="tel" v-model="selfValue.price" class="form-control" />
</label>
</div>
`
})
new Vue({
el: '#app',
data: {
rows: []
},
methods: {
add() {
this.rows.push({ title: '', quantity: '', price: 0 })
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<div id="app" class="container">
<h2 class="mb-3">Rows:</h2>
<div v-for="row,i in rows">
<child v-model="row"></child>
<hr class="mt-1 mb-1" />
</div>
<button #click="add" class="mb-3">
ADD
</button>
<pre>{{ rows }}</pre>
</div>

Related

Updating a data property in Vuejs based on an input event

I have a search input that filters data. The filtered data is meant to re-render my page so that the original property(shifts) will now have the filtered data and this means any change in the search input should update my data object. The snippet below works perfectly so that when I monitor the logged variable filteredData I can see its returning the expected data. When I type on the search input the data is filtered as expected and when I clear the search, the object contains the data in the original shifts property.
The issue occurs when I assign this filtered data to my original object i.e. this.shifts=filteredData. When I do this, the shifts property will contain the filtered data but when I clear the search the previously filtered data is what will still be assigned meaning that the shifts property is not being properly updated. Not sure why this is happening.
new Vue({
el: "#app",
data: {
search_input: '',
shifts: {"John":[{"user_id":193,"shift_hours":0}],"Rose":[{"user_id":194,"shift_hours":0}]},
},
methods: {
searchInput() {
const filteredData = Object.keys(this.shifts).reduce(
(obj, key) => {
if (
key.toLowerCase().includes(this.search_input.toLowerCase())
) {
obj[key] = this.shifts[key];
}
return obj;
},
{}
);
console.log(filteredData);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input v-model="search_input" #input.prevent="searchInput" type="text"/>
</div>
I've appended an additional snippet below to showcase the issue I'm facing:
new Vue({
el: "#app",
data: {
search_input: '',
shifts: {"John":[{"user_id":193,"shift_hours":0}],"Rose":[{"user_id":194,"shift_hours":0}]},
},
methods: {
searchInput() {
const filteredData = Object.keys(this.shifts).reduce(
(obj, key) => {
if (key.toLowerCase().includes(this.search_input.toLowerCase())) {
obj[key] = this.shifts[key];
}
return obj;
},
{}
);
this.shifts = filteredData;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input v-model="search_input" #input.prevent="searchInput" type="text" />
<pre>{{ shifts }}</pre>
</div>
Data needs to be a function, and you can use computed property for filtering:
new Vue({
el: "#app",
data() {
return {
search_input: '',
shifts: {"John": [{"user_id": 193, "shift_hours": 0}], "Rose": [{"user_id": 194, "shift_hours": 0}]},
}
},
computed: {
searchInput() {
return Object.keys(this.shifts).reduce((obj, key) => {
if (key.toLowerCase().includes(this.search_input.toLowerCase())) {
obj[key] = this.shifts[key]
}
return obj
}, {} )
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input v-model="search_input" type="text" />
<pre>{{ searchInput }}</pre>
</div>

I've created multiple filters functions and each one does its own job right, however, I'm not sure how to chain them so they all work together

I'm trying to create filtering functionality which would allow me to filter cards from a collectible card game based on several criteria. I've already created 6 functions which filter based on a single criteria like card name, card cost or card rarity. These functions work fine and do their job, however, right now I can only use one of them at a time.
What I am trying to do is combine or chain these functions so that they are all taken into account before returning the final array with cards. I'm wondering if there's any easy way to do that?
Right now I have this:
<template>
<div class="cards">
<div class="cards-list">
<div class="card" v-for='card in filteredByCost' #click='specificCard(card.cardCode)'>
<div class="card-image">
<img class='responsive-image' :src='"../assets/cards/" + card.cardCode + ".png"' alt="">
</div>
</div>
</div>
</div>
</template>
<script>
import cards from '../assets/cards/set1-en_us.json'
import router from '../router'
export default {
data() {
return {
cards: cards,
search: '',
regions: ['Demacia', 'Noxus'],
cost: [7],
attack: [3, 5],
health: [4, 7],
rarity: ['Champion']
}
},
methods: {
specificCard(cardCode){
router.push({ name: 'specificCard', params: { cardCode: cardCode } })
}
},
computed: {
filteredByName(){
return this.cards.filter((card) => {
return card.name.match(this.search)
})
},
filteredByRegion(){
return this.cards.filter((card) => {
return this.regions.includes(card.region)
})
},
filteredByCost(){
return this.cards.filter((card) => {
return this.cost.includes(card.cost)
})
},
filteredByRarity(){
return this.cards.filter((card) => {
return this.rarity.includes(card.rarity)
})
},
filteredByAttack(){
return this.cards.filter((card) => {
return this.attack.includes(card.attack)
})
},
filteredByHealth(){
return this.cards.filter((card) => {
return this.health.includes(card.health)
})
},
}
}
</script>
Information
Place all your filter methods in the methods attribute on the vue instance
Create a way to enable/disable the filters
Create a computed property that looks at your #2 in this list and applies the proper filters accordingly
My rough example
new Vue({
el: "#app",
data () {
return {
filterEnabler: {
search: false,
sort: false
},
formInputs: {
searchText: ''
},
entries: [
'vue',
'react',
'angular',
'svelte'
]
}
},
computed: {
filteredEntries () {
let { entries, filterEnabler } = this
entries = entries.slice(0)
if (filterEnabler.search) entries = this.searchFilter(entries)
if (filterEnabler.sort) entries = this.sortFilter(entries)
return entries
}
},
methods: {
searchFilter (entries) {
return entries.filter(entry => entry.indexOf(this.formInputs.searchText) !== -1)
},
sortFilter (entries) {
return entries.sort()
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="filters">
<div class="search-filter">
<input type="checkbox" v-model="filterEnabler.search" /> Search Filter
<div v-if="filterEnabler.search">
<input placeholder="type here" type="text" v-model="formInputs.searchText" />
</div>
</div>
<div class="sort-filter">
<input type="checkbox" v-model="filterEnabler.sort" /> Sort Filter
</div>
</div>
<ul>
<li v-for="entry in filteredEntries" :key="entry">{{entry}}</li>
</ul>
</div>
In my example, you can see this I have 2 filters, search and sort - when one of their filterEnablers is toggeled true, it will apply all the enabled filters to the data and then return a new, separate array (very important, try not to mutate your source of truth, in my case, that's entries)
Hope this helps!

Dynamics inputs, the v-model update all values in v-for

I try this following code with Vue.js 2:
<div id="app">
<div v-for="(item, index) in items">
<div>
<input type="text" v-model="items[index].message">
<input type="text" v-model="items[index].surface">
</div>
</div>
<button #click="addNewfield">Add</button>
</div>
var app = new Vue({
el: '#app',
data: {
item: {
message: 'test',
surface: 45
},
items: [],
},
mounted() {
this.items.push(this.item)
},
methods: {
addNewfield() {
this.items.push(this.item);
}
}
})
The goal is to create news input when user click on Add button. I tried different ways like :
<input type="text" v-model="item.message">
But it doesn't work. If you write in "message" input, all "message" inputs will be updated.
How can I only updated the concerned value ?
Thanks for help !
This is happening because objects in Javascript are stored by reference. This means that every time you push this.item onto the array, it's adding a reference to the exact same object as the last.
You can avoid this by creating a new object each time:
methods: {
addNewfield() {
const obj = {
message: 'test',
surface: 45
}
this.items.push(obj);
}
}
Another option would be to clone the original object each time like:
methods: {
addNewfield() {
const clone = Object.assign({}, this.item);
this.items.push(clone);
}
}

Remove dynamically created Vue Components

I followed the solution to create dynamic form elements here:
Dynamically adding different components in Vue
Works great, my next conundrum is now I want to remove form elements if the user adds too many.
The way it works is the user creates a "set" which is defined as a group of inputs. So each set could be for a different person, or place, etc.
Here is my JSfiddle https://jsfiddle.net/61x784uv/
Html
<div id="component-pii-input" v-for="field in fields" v-bind:is="field.type" :key="field.id">
</div>
<button id='button-add-pii-component' v-on:click="addFormElement('pii-entry-field')">Add Set</button>
</div>
Javascript
Vue.component('pii-entry-field', {
data: function () {
return {
fields: [],
count: 0,
}
},
methods: {
addFormElement: function(type) {
this.fields.push({
'type': type,
id: this.count++
});
},
},
template: ` <div class='pii-field'><div>
<component v-for="field in fields" v-bind:is="field.type":key="field.id"></component>
</div>
<button id='button-add-pii-input' v-on:click="addFormElement('pii-input-field')">Add Input</button>
<hr>
</div>`,
})
Vue.component('pii-input-field', {
data: function () {
return {
}
},
template: ` <div>
<select>
<option disabled>Select Classification</option>
<option>Name</option>
<option>Address</option>
<option>Email</option>
<option>Phone</option>
<option>Medical</option>
<option>Financial</option>
</select>
<div>
<input type="text" placeholder="Input">
</div>
<button v-on:click="removeFormElement">Remove Input</button></span>
</div>`,
})
var app = new Vue({
el: '#app',
data: {
fields: [],
count: 0,
},
methods: {
addFormElement: function(type) {
this.fields.push({
'type': type,
id: this.count++
});
},
}
});
Here is a working fiddle: https://jsfiddle.net/e12hbLcr/
Walkthrough:
You need to give the component some kind of id to know what component should be removed. You can do this with props.
<component v-for="field in fields" v-bind:is="field.type" :id="field.id" :key="field.id">
Now create the function in the child-component and edit the button so it sends the id.
<button v-on:click="removeFormElement(id)">Remove Input&lt/button>
Remember in Vue that props go down (parent -> child) and events up (child-parent). So now we need to tell the parent that this button was clicked and an id was sent.
removeFormElement(id) {
console.log('sending message up to remove id', id)
this.$emit('remove', id)
}
Tell the parent component to listen to that event and attach a function to that event.
<component v-for="field in fields" v-bind:is="field.type" :id="field.id" #remove="removeFormElement" :key="field.id">
Note that the # is same as v-on:
Finally remove that item from the fields array.
removeFormElement(id) {
console.log('removing form element', id)
const index = this.fields.findIndex(f => f.id === id)
this.fields.splice(index,1)
}
You should probably move these remove buttons into a <slot> of the component so you could pass in the scoped id.
But if you can't, you could $emit removal event on the $parent of the individual components, passing the id of the item to remove.
Vue.component('pii-entry-field', {
data() {
return {
fields: [],
count: 0,
}
},
mounted() {
this.$on('removeFormElement', this.removeFormElement);
},
methods: {
addFormElement(type) {
this.fields.push({
'type': type,
id: this.count++
});
},
removeFormElement(id) {
const index = this.fields.findIndex(f => f.id === id);
this.fields.splice(index, 1);
}
},
template: `
<div class='pii-field'>
<component v-for="field in fields" v-bind:is="field.type" :key="field.id"></component>
<button id='button-add-pii-input' v-on:click="addFormElement('pii-input-field')">Add Input</button>
<hr>
</div>`,
})
Vue.component('pii-input-field', {
data() {
return {
}
},
methods: {
removeFormElement() {
const id = this.$vnode.key;
this.$parent.$emit('removeFormElement', id);
}
},
template: `
<div>
<select>
<option disabled>Select Classification</option>
<option>Name</option>
<option>Address</option>
<option>Email</option>
<option>Phone</option>
<option>Medical</option>
<option>Financial</option>
</select>
<div>
<input type="text" placeholder="Input" />
</div>
<button v-on:click="removeFormElement">Remove Input</button>
</div>`,
})
var app = new Vue({
el: '#app',
data() {
return {
fields: [],
count: 0,
}
},
methods: {
addFormElement(type) {
this.fields.push({
'type': type,
id: this.count++
});
},
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div id="component-pii-input" v-for="field in fields" v-bind:is="field.type" :key="field.id">
</div>
<button id="button-add-pii-component" v-on:click="addFormElement('pii-entry-field')" class="uk-button uk-button-primary uk-width-1-1 uk-margin-small">Add Set</button>
</div>

Vue2: Avoid mutating a prop directly inside component

I'm gettig this "Avoid mutating a prop directly", when checking if the persons input should get an invalid class because its empty.
<script type="text/x-template" id="srx">
<input type="number" name="persons" id="persons" value="" v-model="persons" :class="{invalid: !persons}">
</script>
<div id="app">
{{stepText}}
<sr-el :persons="1"></sr-el>
<button v-if="currentStep > 1" type="button" v-on:click="previous()">prev</button>
<button v-if="currentStep < 6" type="button" :disabled="currentStepIsValid()" v-on:click="next()">
next</button>
</div>
Vue.component('sr-el', {
template: '#srx',
props: ['persons'],
})
var App = window.App = new Vue({
el: '#app',
data: {
persons: '1'
currentStep: 1,
},
methods: {
currentStepIsValid: function() {
if (this.currentStep == 1) {
this.stepText = "Step 1;
return !(this.persons > 0);
}
},
previous: function() {
this.currentStep = this.currentStep - 1;
// prev slide
},
next: function() {
this.currentStep = this.currentStep + 1;
// next slide
}
}
})
You're getting that warning because you are binding persons to the input in the template via v-model. Changing the input's value will thus change the value of persons, which means the prop is getting mutated directly in your sr-el component.
You should set a new property equal to persons and pass that to v-model instead:
Vue.component('sr-el', {
template: '#srx',
props: ['persons'],
data: function() {
return {
inputVal: this.persons,
}
}
})
<input v-model="inputVal" ... />

Categories