How to update a vue js array with user inputed value - vue.js

i am creating a web app which has a table that displays information from an array i have created using vue.js. i wanted to be able to edit/update/delete the content of the array based on the needs of an admin account which would then change what the table is displaying.
my current array
var app2 = new Vue({
el: '#app',
data: {
search: '',
courses: [
{ 'topic': 'x', 'description': 'x', },
{ 'topic': 'x', 'description': 'x', },
{ 'topic': 'x', 'description': 'x', },
{ 'topic': 'x', 'description': 'x',},
{ 'topic': 'x', 'description': 'x', },
{ 'topic': 'x', 'description': 'x', },
{ 'topic': 'x', 'description': 'x', },
{ 'topic': 'x', 'description': 'x!', },
{ 'topic': 'x', 'description': 'x', },
{ 'topic': 'x', 'description': 'x!', }
]
}
i would like the "x" to be changeable based on what the user typed into the input field
<th scope="row"> 1</th>
<td>{{item.topic}}</td>
<td>{{item.description}}</td>
this isnt all the code the gist of it

It sounds like what you want is a component which you toggle to be either in a read-only mode, or in a mode which accepts input for your admin users. You can then use this in your v-for loop to bind to the array of data.
You can still use v-model with custom components to facilitate two-way binding as described here. Basically, v-model will supply the value to the component via the value property, and will listen for an input event from the component to receive updates to the value.
So, our component template could look like the below, which conditionally renders an input field if the prop edit is true, otherwise it renders a read-only span element. We bind the value of the input to a local mutable data item we initialise from the value prop v-model will pass in from the parent. On the input event as the user changes the value, it will in turn emit an event of the same name so the parent component can receive updates.
<input
v-if="edit"
type="text"
v-model="localValue"
#input="$emit('input', $event.target.value)"
class="form-control" />
<span v-else>{{ value }}</span>
With a backing script like this:
var customInputComponent = {
template: '#input-template',
props: ["edit", "value"],
data() {
return {
localValue: this.value
}
}
};
In the parent element, the usage of the component in the template would look like this, where we register the custom input component with the name appCustomInput. We use v-model to bind the component to the current item of the iteration and the property for that particular column. Finally, we bind the edit prop of the custom component to a data field editMode on the parent with :edit="editMode" (short cut for v-bind:edit="editMode").
It is important to set the :key="searchResult.id" to uniquely identify each entry, otherwise there will be problems when updating the DOM when the value changes. We can't use just the index for this, because if we remove an item from the courses array, it will no longer be correct. So, I've added a unique id property to each of the course objects.
<tr v-for="(searchResult, index) in searchCourses" :key="searchResult.id">
<th scope="row">
{{ index + 1 }}
</th>
<td>
<app-custom-input v-model="searchResult.topic" :edit="editMode" />
</td>
<td>
<app-custom-input v-model="searchResult.description" :edit="editMode" />
</td>
<td v-if="editMode">
<button
v-if="editMode"
class="btn btn-secondary"
#click="deleteRow(searchResult.id)">Delete</button>
</td>
</tr>
The backing instance code for this would look something like this:
new Vue({
el: "#app",
components: {
appCustomInput: customInputComponent
},
data: {
search: '',
editMode: false,
courses: [
{ topic: "English", description: "Universal language", id: 1 },
{ topic: "Maths", description: "Numbers etc.", id: 2 },
{ topic: "Physics", description: "Falling apples etc.", id: 3 },
{ topic: "Chemistry", description: "Periodic table etc.", id: 4 }
]
},
methods: {
deleteRow(sourceId) {
var index = this.courses.findIndex(a => a.id == sourceId);
this.courses.splice(index, 1);
}
},
computed: {
searchCourses() {
return this.courses
.filter((item) => {
return item.topic
.toLowerCase()
.includes(this.search.toLowerCase())
});
}
}
});
A complete code pen is available here for demonstration.

The {{ }} "mustache" syntax is used to display values, so the expression between the double braces will be evaluated and the output will replace it. This is a one-way process. You're most likely looking for a two-way binding which does not only change your UI if the property changs, but also change the property when a user changes it from the UI, hence the name.
Have a look at Input bindings. Where v-model is the key:
<input v-model="item.topic">

Related

How to concatenate/modify props in VueJS

I'm trying to build an application in Vue2 I have a parent component where I'm passing a data as props to child component using v-for.
Here is my parent component:
<template>
<div class="row" v-for="(item, index) in variables">
<design-arch :details="item" :selectedFields="selectedFields"></design-arch>
</div>
</template>
props:['selectedFields'],
data() {
return {
variables:[
{id: 1, role: 2, specialisation: 13, name: 'ABC - spec 1', role_name: 'ABC', spec_name: 'spec 1'},
{id: 2, role: 2, specialisation: 24, name: 'ABC - spec 2', role_name: 'ABC', spec_name: 'spec 2'},
{id: 3, role: 2, specialisation: 27, name: 'ABC - spec 3', role_name: 'ABC', spec_name: 'spec 3'},
]
}
}
and below is my child component:
<template>
<table v-if="tableData && tableData.data.length">
<thead class="">
<tr>
<th>Sr No</th>
<th>Projects Count</th>
<th>Value</th>
<th>Area</th>
</tr>
</thead>
<tbody>
<tr v-for="(item,index) in tableData.data">
<td>{{tableData.meta.from + index}}</td>
<td>{{item.projects_count}}</td>
<td>{{item.value}}</td>
<td>{{item.area}}</td>
</tr>
</tbody>
</table>
</template>
props: ['details', 'selectedFields'],
data(){
return{
loading: false,
tableData:{},
filters: '',
}
},
methods:{
fetchData(){
this.filters = this.selectedFields;
this.filters['role'] = typeof this.selectedFields['role'] !== 'undefined' ? this.selectedFields['role'] : [{id: this.details.role, name: this.details.role_name}];
this.filters['specialisation'] = typeof this.selectedFields['specialisation'] !== 'undefined' ? this.selectedFields['specialisation'] : [{id: this.details.specialisation, name: this.details.spec_name}];
this.filters['sort_by_column'] = typeof this.selectedFields['sort_by_column'] !== 'undefined' ? this.selectedFields['sort_by_column'] : { column: 'projects_count', order: 'desc'};
console.log(this.filters)
//axios call... with payload as this.filters
}
},
In above code we need to concatenate or modify the prop - selectedFields and call the API to get the data. Since each component has specific modifications, we need to re-calculate in the child component.
Currently my filters are similar in each child component and the modifications are not reflected during the Axios call.
How we can modify the props element inside the local data. So that we can have different executions.
Any better approach is appreciated. Thanks.
I would suggest adding a Vue watch on the props details. Anytime details changes it will rerun fetchData(). See Vue watch.
watch: {
details: {
immediate: true,
deep: true,
handler (oldValue, newValue) {
this.fetchData()
}
}
}

Template renders twice in vue.js

In my components template I have following code:
<div
class="program-point-container"
v-for="p in programPoints"
:key="p.id"
>
<div class="mt-6 mt-sm-3 mt-md-14 mb-3 font-weight-bold font-size-16" v-if="showDate(p.startsAt)">
{{new Date(p.startsAt) | moment('DD.MM.YYYY')}} // Humboldt Carré
</div>
</div>
My programPoints Array has 23 object´s in it. For debug reasons I console logged a string to see that the function is called 23 * 2 times.
In this method I push data into an array. And return true or false to show the div container. But since it already pushed and returned at the "first" rendering. I don't get to show that container.
showDate(datetime) {
const date = new Date(datetime).toDateString()
// check if date already been pushed
if(this.dates.includes(date)) {
console.log('false')
return false // date already been pushed
}
this.dates.push(date)
console.log('true')
return true
},
Throw away "JQuery thinking". This is NOT how Vue works. Your template is and will be rendered multiple times - simply every time some of the reactive data it uses changes...
You don't need to care about what was rendered before and what to do to update the result for a new data state. It is Vue's job. Just write your template in a "what I want to see on the screen" style and Vue will take care of the rest...
Just remove everything related to showDate - it's not needed. And read the documentation!
Update
In the Array of Objects I have a property which has datetimes. And I only want to display the date once on the first object the date appears. And ignore it for the rest. When a new date appears I want it to show it too but only for the first object and so on
I want to add an extra key to the Object which is the first to iterate and has that unique date. The others will be ignored, till another one comes with an unique date.
First rule of Fight Club...ehm Vue is: Never modify any data which are used in template during template rendering. In best case you end up with incorrect result (as you did). In worst case you end up with infinite loop...
Much better solution is to create alternative data structure that represents what you want and thanks to Vue computed properties it stays up to date even if original data changes...
const vm = new Vue({
el: "#app",
data() {
return {
programPoints: [
{ id: 1, name: 'Program A', startsAt: '2021/04/13' },
{ id: 2, name: 'Program B', startsAt: '2021/04/14' },
{ id: 3, name: 'Program C', startsAt: '2021/04/14' },
{ id: 4, name: 'Program D', startsAt: '2021/04/18' },
{ id: 5, name: 'Program E', startsAt: '2021/04/13' },
{ id: 6, name: 'Program F', startsAt: '2021/04/18' },
]
}
},
computed: {
programByDays() {
return this.programPoints.reduce((acc, item) => {
if(!acc[item.startsAt]) {
acc[item.startsAt] = []
}
acc[item.startsAt].push(item)
return acc
}, {})
}
},
mounted() {
setTimeout(() => this.programPoints.push({ id: 7, name: 'Program SPECIAL', startsAt: '2021/04/20' }), 2000)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<ul>
<li v-for="(programs, date) in programByDays" :key="date">
<p> {{ date }} </p>
<ul>
<li v-for="program in programs" :key="program.id">
{{ program.name }}
</li>
</ul
</li>
</ul>
</div>

Connect v-select with vuex: problem [object object]

I am trying to create a dropdown (v-select/q-select (using quasar)), which allows me to select from an array in my vuex-storage and then eventually save the selected item (content of it) in a variable. Currently I have no problem to access the vuex-storage, but face the problem, that the v-select expects a string and not an object.
My code looks like the following.
// vuex storage:
const state = {
savedsystems:
[
id: "1",
system: {...}
],
[
id: "2",
system: {...}
]
// example of the vuex storage out of my viewdevtools
systemsconstant: Object
savedsystems:Array[2]
0:Object
id:"first"
system:Object
7a73d702-fc28-4d15-a54c-2bb950f7a51c:Object
name:"3"
status:"defined"
88519419-8a81-48f1-a5e6-5da77291b848:Object
name:"5"
status:"not defined"
1:Object
id:"second"
system:Object
7a73d702-fc28-4d15-a54c-2bb950f7a51c:Object
name:"3"
status:"not defined"
88519419-8a81-48f1-a5e6-5da77291b848:Object
name:"9"
status:"defined"
}
// dropdown:
<q-select
outlined
dense
emit-value
:value="currentsystem"
:options="savedsystems"
label="selectsystem" />
// computed to get systems from vuex:
computed: {
savedsystems() {
return this.$store.getters['systemsconstant/getsavedsystems']
}
},
I used the following example https://codepen.io/sagalbot/pen/aJQJyp as inspiration and tried a couple of different setups stringifying resulting in nothing really.
If one would try to apply my case to a similar problem (v-select displays object Object), the mentioned formatlabel would be an object instead of a string.
Question:
How can I modify the (with a getter) imported array of objects "savedsystems", so it can be used both as label to select it and furthermore then to connect it properly to the values, so I can save the selected as a variable.
Or can I change something in my v-select, e.g. varying what comes behind :options/options?
I'd appreciate any help!
You should use the property option-label
<div id="q-app">
<div class="q-pa-md" style="max-width: 300px">
<div class="q-gutter-md">
<q-badge color="secondary" multi-line>
Model: "{{ model }}"
</q-badge>
<q-select filled v-model="model" :options="options" label="Standard" option-label="description"></q-select>
{{ model }}
</div>
</div>
</div>
JS:
new Vue({
el: '#q-app',
data () {
return {
model: null,
options: [
{
label: 'Google',
value: 'Google',
description: 'Search engine',
category: '1'
},
{
label: 'Facebook',
value: 'Facebook',
description: 'Social media',
category: '1'
},
{
label: 'Twitter',
value: 'Twitter',
description: 'Quick updates',
category: '2'
},
]
}
}
})
https://codepen.io/reijnemans/pen/bGpqJYx?editors=1010

Mutating a value in vue when the key didn't previously exist does not update the view

I have a table and a select box for each row. I want the check box to model a value in the data that doesn't actually exist, yet.
<tr v-for="item in someData">
<input type="checkbox" v-model="item.selected"></td>
<input type="checkbox" v-model="item.name"></td>
<tr>
My data when loaded from the DB looks like this:
someData: [
{'name': 'john'},
{'name': 'kate'},
{'name': 'aaron'},
]
When the user presses a Select All button it should update the selected key even if it doesn't exist (well thats the idea)
toggleSelect: function () {
this.someData.forEach(element => {
element.selected = !element.selected;
});
}
However the checkboxes don't react even though the values have been updated. To make this work I need to get the data and add the key/value manually prior to loading it into view and rendering
getDatabaseData: function () {
// some code omitted
response['data'].forEach(element => {
element["selected"] = false;
});
app.someData = response['data']
}
Am I doing it correctly? Am I right in thinking Vue won't be reactive to values that didn't exist prior to rendering?
Try this idea,
in vue component.
<input type="checkbox" v-model="selectAll"> Select All
<tr v-for="item in someData" :key="item.name">
<td>
<input type="checkbox" v-model="selected" :value="item.name">
</td>
{{ item.name }}
</tr>
script:
data() {
return {
selectAll: false,
selected: [],
someData: [{ name: "john" }, { name: "kate" }, { name: "aaron" }]
};
},
watch: {
selectAll(value) {
// validate if value is true
if (value) {
this.someData.forEach(item => {
// push unique value
if(this.items.indexOf(item.name) === -1) {
this.selected.push(item.name);
}
});
} else {
// Unselect all
this.selected = [];
}
}
}
You have a selected variable where the selected Items are located. selectAll variable to select all items and push to selected variable.
You should be using Vue.set to update the value of the selected property on your objects in order to be reactive, like this:
import Vue from 'vue';
...
toggleSelect: function () {
this.someData.forEach(element => {
Vue.set(element, 'selected', !element.selected);
});
}

VueJS v-if = array[index] is not working

I wanted to make a component which gets text-box when mouse is over the image.
Below is HTML template.
<section class="item-container" v-for="(item, index) in items">
<div class="image-box" #mouseenter="changeStatus(index)">
<img class="image" src="item.link" alt>
</div>
<div class="text-box" #mouseleave="changeStatus(index)" v-if="show[index]">
<h4>{{ item.name }}</h4>
<p>{{ item.content }}</p>
</div>
</section>
And below is app.js
new Vue({
el: '#app',
data: {
show: [false, false, false],
items: [
{
name: 'Author 1',
content: 'Content 1'
},
{
name: 'Author 2',
content: 'Content 2'
},
{
name: 'Author 3',
content: 'Content 3'
}
]
},
methods: {
changeStatus: function(index) {
this.show[index] = !this.show[index];
console.log(this.show);
console.log(this.show[index]); // console gets it as expected
}
}
});
When I execute above codes, I can find that the show property has changed. However, v-if is not updated. Can't we use array[index] for v-if or is there other reason for it?
The problem is not about v-if, it's because Vue cannot detect the changes of an array element directly, this is one of the limitation of JavaScript.
Thus, Vue provides some helper functions for this, like Vue.set
Change this this.show[index] = !this.show[index]
to Vue.set(this.show, index, !this.show[index])
then it should work.
Vue.set is not the only solution, there are several ways to accomplish this, in case you may want to know.
You can use native methods of JavaScript array, Vue will hook on these methods so it can detect the changes.
Here is the example of the usage of .splice
this.show.splice(index, 1, !this.show[index])
Another way is to replace the array entirely. If you are using ES6, you can use the spread operator to clone the array easily:
this.show[index] = !this.show[index]
this.show = [...this.show]
.map will also work because it returns a new array
this.show = this.show.map((el, i) =>
i === index ? !el : el
)
You can use a JS object instead of an array and get the same effect. In other words, replace show: [false, false, false], with show: {0:false, 1:false, 2:false},.
In component in methods you can use:
this.$set(this.show, index, !this.show[index])