Change ordering of a array of objects with up/down buttons - vue.js

I'm having the following issue:
I want in the frondend to get a list of items based on the key 'order':
<div id="app">
<ul>
<li v-for="item in sorted">
{{item.order}} {{item.title}}
<button #click="changeOrderDown(item)">down</button>
<button #click="changeOrderUp(item)">up</button>
</li>
</ul>
</div>
based on the JSON you see below. When you click the button I want to swap out for example order 1 -> 2 and then 2 becomes 1
items: [
{
title: "test",
order: 1
},
{
title: "test2",
order: 2
},
{
title: "test3",
order: 3
}
]
I keep getting a duplicate key error cause i first change the first key and then the second. i resolved it to update the whole object at ones. that seems to work but still it doesn't behave the correct way.
computed: {
sorted() {
function compare(a, b) {
let comparison = 0;
if (a.order > b.order) {
comparison = 1;
} else if (a.order < b.order) {
comparison = -1;
}
return comparison;
}
return this.items.sort(compare)
},
},
methods: {
changeOrderDown(currentItem) {
let temp = this.items
let old_value = parseFloat(currentItem.order)
let new_value = parseFloat(currentItem.order) + 1;
console.log(old_value, new_value)
temp.filter(o => o.order === old_value)[0].order = new_value;
temp.filter(o => o.order === new_value)[0].order = old_value;
this.items = temp;
},
changeOrderUp(currentItem) {
let temp = this.items
let old_value = parseFloat(currentItem.order)
let new_value = parseFloat(currentItem.order) - 1;
console.log(old_value, new_value)
temp.filter(o => o.order === old_value)[0].order = new_value;
temp.filter(o => o.order === new_value)[0].order = old_value;
this.items = temp;
},
}
I made a codepen down below with the code from above. this is kinda a working example but it doesn't feel right. Can someone give me a push in the right direction?
https://codepen.io/frank-derks/pen/BaQVOZV

Interesting challenge. Using my Vue 2 CLI sandbox app, I came up with functionality that doesn't require an 'order' property. Here is the component code:
<template>
<div class="swap-array-objects">
<h3>SwapArrayObjects.vue</h3>
<div class="row">
<div class="col-md-6">
<table class="table table-bordered">
<thead>
<tr>
<th>TITLE</th>
<th> </th>
<th> </th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in items" :key="index">
<td>{{ item.title }}</td>
<td>
<button class="btn btn-secondary btn-sm" #click="moveUp(index)">Up</button>
</td>
<td>
<button class="btn btn-secondary btn-sm" #click="moveDown(index)">Down</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{
title: 'title1'
},
{
title: 'title2'
},
{
title: 'title3'
},
{
title: 'title4'
},
{
title: 'title5'
}
]
}
},
methods: {
moveUp(index) {
if (index === 0) {
return;
}
let priorIndex = index - 1;
let itemCopy = {...this.items[index]};
let priorItemCopy = {...this.items[priorIndex]};
// Swap array position with prior element
this.$set(this.items, priorIndex, itemCopy);
this.$set(this.items, index, priorItemCopy);
},
moveDown(index) {
if (index === this.items.length - 1) {
return;
}
let subsequentIndex = index + 1;
let itemCopy = {...this.items[index]};
let subsequentItemCopy = {...this.items[subsequentIndex]};
// Swap array positions with subsequent element
this.$set(this.items, subsequentIndex, itemCopy);
this.$set(this.items, index, subsequentItemCopy);
}
}
}
</script>

This solution is similar to Tim's, but a bit simpler and easier to follow:
<template>
<v-app>
<ul>
<li v-for="(item, index) in items" :key="index">
{{item.order}} {{item.title}} {{index}}
<button #click="changeOrderDown(item, index)" v-if="index != items.length-1">down</button>
<button #click="changeOrderUp(item, index)" v-if="index != 0">up</button>
</li>
</ul>
</v-app>
</template>
<script>
export default {
name: 'App',
data: () => ({
items: [
{
title: "test1",
order: 1
},
{
title: "test2",
order: 2
},
{
title: "test3",
order: 3
}
]
}),
methods: {
changeOrderDown(item, index) {
// save clicked item in temporary variable
let temp = item
// move the following item to the clicked element
this.items[index] = this.items[index + 1]
// move clicked item to destination
this.items[index + 1] = temp
},
changeOrderUp(item, index) {
// save clicked item in temporary variable
let temp = item
// move the following item to the clicked element
this.items[index] = this.items[index - 1]
// move clicked item to destination
this.items[index - 1] = temp
},
}
};
</script>
<template>
<v-app>
<ul>
<li v-for="(item, index) in items" :key="index">
{{item.order}} {{item.title}} {{index}}
<button #click="changeOrderDown(item, index)" v-if="index != items.length-1">down</button>
<button #click="changeOrderUp(item, index)" v-if="index != 0">up</button>
</li>
</ul>
</v-app>
</template>
<script>
export default {
name: 'App',
data: () => ({
items: [
{
title: "test1",
order: 1
},
{
title: "test2",
order: 2
},
{
title: "test3",
order: 3
}
]
}),
methods: {
changeOrderDown(item, index) {
// save clicked item in temporary variable
let temp = item
// move the following item to the clicked element
this.items[index] = this.items[index + 1]
// move clicked item to destination
this.items[index + 1] = temp
},
changeOrderUp(item, index) {
// save clicked item in temporary variable
let temp = item
// move the following item to the clicked element
this.items[index] = this.items[index - 1]
// move clicked item to destination
this.items[index - 1] = temp
},
}
};
</script>

Related

Not able to automatically set a checkbutton as checked on a value getting updated

I have a Vue js template that contains two radio buttons that are initially unchecked. Once the widgets get rendered, I want one of the radio buttons to be checked automatically on a value being updated after retrieving it from the backend server.
Here is the code as seen below:
<template>
<div>
<CCard class="card">
<CCardHeader>{{ $t("SETTINGS.NOTIFICATIONS.HEADER") }}</CCardHeader>
<CCardBody>
<table>
<tr>
<td class="index">
<strong>
{{ $t("SETTINGS.NOTIFICATIONS.DISBURSEMENT_INDEX") }}
</strong>
<img
src="#/assets/img/blue-circle.svg"
class="info pointer-on-hover"
v-c-tooltip="{
html: true,
content: getDisbursementTooltipContent,
active: false,
placement: 'top',
}"
>
</td>
<td class="category">
<fieldset id="disbursement-group">
<input
id="whatsapp"
type="radio"
name="disbursement-group"
value="whatsapp"
#change="selectDisbursementPreference"
:checked="disbursementPreference === whatsapp"
>
<span class="text">Whatsapp</span>
</fieldset>
</td>
<td class="category">
<fieldset id="disbursement-group">
<input
id="email"
type="radio"
name="disbursement-group"
value="email"
#change="selectDisbursementPreference"
:checked="disbursementPreference === email"
>
<span class="text">Email</span>
</fieldset>
</td>
</tr>
<br>
<tr hidden>
<td class="index">
<strong>
{{ $t("SETTINGS.NOTIFICATIONS.SETTLEMENT_INDEX") }}
</strong>
<img
src="#/assets/img/blue-circle.svg"
class="info pointer-on-hover"
v-c-tooltip="{
html: true,
content: getSettlementTooltipContent,
active: false,
placement: 'bottom',
}"
>
</td>
<td class="category">
<fieldset id="settlement-group">
<input
type="radio"
name="settlement-group"
value="email"
#change="selectSettlementPreference"
:checked="settlementPreference === email"
>
<span class="text">{{ $t("SETTINGS.NOTIFICATIONS.YES") }}</span>
</fieldset>
</td>
<td>
<fieldset id="settlement-group">
<input
value="none"
type="radio"
name="settlement-group"
#change="selectSettlementPreference"
:checked="settlementPreference === none"
>
<span class="text">{{ $t("SETTINGS.NOTIFICATIONS.NO") }}</span>
</fieldset>
</td>
<td
v-if="isEmailInputVisible"
class="email-column"
>
<strong>Email</strong>
<input
type="text"
v-model="emailRecepients"
>
</td>
</tr>
</table>
</CCardBody>
</CCard>
<CButton
color="durianprimary"
class="button"
#click="savePreferences"
:disabled="isSaveButtonDisabled"
>
{{ $t("SETTINGS.NOTIFICATIONS.SAVE") }}
</CButton>
</div>
</template>
<script>
export default {
name: "Notifications",
data() {
return {
disbursementPreference: "",
whatsapp: constant.WHATSAPP,
email: constant.EMAIL,
none: constant.TEXT_NONE,
};
},
methods: {
selectDisbursementPreference(event) {
this.disbursementPreference = event.target.value;
},
setDisbursementPreference(data) {
if (!data.is_available) {
return;
}
const length = data.types.length;
for (let i = 0; i < length; i++) {
if (data.types[i].is_enabled) {
this.disbursementPreference = data.types[i].type;
return;
}
}
},
createDisbursementPreferencePayload() {
const disbursementPreferenceLength =
constant.DISBURSEMENT_PREFERENCE_TYPES.length;
const types = [];
for (let i = 0; i < disbursementPreferenceLength; i++) {
if (
constant.DISBURSEMENT_PREFERENCE_TYPES[i] ===
this.disbursementPreference
) {
types.push({
type: constant.DISBURSEMENT_PREFERENCE_TYPES[i],
is_enabled: true,
});
} else {
types.push({
type: constant.DISBURSEMENT_PREFERENCE_TYPES[i],
is_enabled: false,
});
}
}
return { is_available: true, types: types };
},
createRequestPayload() {
const disbursementPayload = this.createDisbursementPreferencePayload();
return {
disbursement: disbursementPayload,
};
},
async savePreferences() {
....
},
async getPreferences() {
....
},
},
computed: {
getDisbursementTooltipContent() {
return `${this.$t("SETTINGS.NOTIFICATIONS.DISBURSEMENT")}`;
},
getSettlementTooltipContent() {
return `${this.$t("SETTINGS.NOTIFICATIONS.SETTLEMENT")}`;
},
isEmailInputVisible() {
return this.settlementPreference === this.email;
},
getEmailRecipients() {
let recipientsString = "";
const length = this.settlementEmailReceipients.length;
for (let i = 0; i < length; i++) {
recipientsString += this.settlementEmailReceipients[i] + ";";
}
return recipientsString;
},
isSaveButtonDisabled() {
return (
this.emailRecepients.length === 0 &&
this.settlementPreference === constant.EMAIL
);
},
},
mounted() {
this.getPreferences();
},
};
</script>
I have tried to automatically set either of the check buttons with the following code pieces:
watch : {
disbursementPreference:function(val) {
console.log("watch value has been changed => ",val);
document.getElementById(val).checked = true;
},
},
and
updated() {
console.log("value that I am trying to test => ",this.disbursementPreference);
document.getElementById(this.disbursementPreference).checked = true;
},
But the thing is, while the above code captures the changes made to the disbursementPreference variable, the result still remains the same because the widgets have not been rendered yet.
Is there a way to solve the above problem? I tried window.onload but I think rendering in vue.js is somewhat different as far as I know.
Is there any other way to solve this problem? Please do let me know thanks.
P.S : I have not added all of the template code for proprietary reasons.

VueJS- Display multiple nested properties in objects

I have a multi-level navigation component in VueJS. I can retrieve and display the first 2 levels but I also need the nested property called name of the children array to be displayed. How can I get this nested property to print out in my for loop?
Here is my code:
<template>
<div class="navigation__main-menu-wrapper">
<ul>
<li
v-for="(item, index) in navItems"
:key="index">
<div class="navigation__main-menu-list-link">
<a class="mainnav-anchor"
:href="item.url">{{ item.name }}</a>
</div>
<ul
class="navigation__submenu">
<li
v-for="(subItem, index) in item.items"
:key="index">
<a
:href="subItem.url"
:title="subItem.name">
<div>
<span>{{subItem.name}}</span>
</div>
<div
v-for="(subItemChild, index) in items.children"
:key="index">
<span class="navigation__submenu-name">{{subItemChild.name}}</span>
</div>
</a>
</li>
</ul>
</li>
</ul>
</div>
</template>
<script>
export default {
data: function () {
return {
navItems: []
};
},
mounted: function () {
this.onLoadMainNavigation();
},
methods: {
onLoadMainNavigation: function () {
this.$helpers
.getApiCall("/api/", {
type: "mainnavigation",
})
.then((response) => {
const items = [];
Object.values(response.data.data).forEach((item) => {
const subItems = [];
//Check if there is a submenu
if (item.subMainNavigation) {
item.subMainNavigation.forEach((subItem, subItemChild) => {
subItems.push({
name: subItem.name,
class: subItem.class,
children: [{
name: subItemChild.name
}]
});
});
}
items.push({
name: item.name,
url: item.url,
items: subItems
});
});
this.navItems = items;
})
}
}
};
</script>
This is an exmplae of the data that is outputted
[
{
"name":"Charging solutions"
"items":[
{
"name":"By industry",
"class":"by-industry",
"children":[
{
"id":6671,
"name":"Workplaces",
"class":"workplaces"
},
{
"id":6672,
"name":"Retail & hospitality",
"class":"retail"
},
{
"id":6673,
"name":"Commercial parking",
"class":"parking"
},
{
"id":6674,
"name":"Fuel retailers",
"class":"fuel"
},
]
},
{
"name":"Products",
"class":"products",
"children":[
{
"id":204,
"name":"Public chargers",
"class":"public"
},
{
"id":206,
"name":"Accessories",
"class":"accessories"
},
{
"id":4889,
"name":"Smart charging",
"class":"smart"
}
]
}
]
}
]
Just replace items.children by subItem.children in the last nested loop :
<div v-for="(subItemChild, _index) in subItem.children" :key="_index">
<span class="navigation__submenu-name">{{subItemChild.name}}</span>
</div>
I eventually solved this by simply doing
subItems.push(subItem);

How to add new records in a table with temp values in vuejs

I have a table with records. There is an option for inline editing when you click on the field. There is an option to add new rows.
The problem is that when I click Add New 2 times and there are 2 empty rows in the table with inputs and start typing, field values for both rows are changed.
It's because I use v-model="temp.name" and isCreateMode and in my case there are several rows with this temp model, but I'm not sure how to deal with this.
temp is necessary because users can cancel editing the field. I use the same input fields for create and edit.
//in component
// other code here
computed: {
...mapState([
'editModeField',
'editedUser',
'editMode',
'createMode',
'temp',
'users'
]),
...mapGetters([
'filteredUsers'
]),
isEditable (field, user, index) {
if (this.isCreateMode(user)) {
return this.users[index] === user
}
return this.editedUser === user && this.editModeField ===
field
},
isEditMode (field, user) {
return this.editMode && this.editedUser === user &&
this.editModeField === field
},
isCreateMode (user) {
return this.createMode && !user.id
},
addUser (user, index) {
if (!user) {
user = {
name: '',
car: ''
}
this.toggleCreateMode(true)
this.createUser(user)
return
}
// this makes a request to the endpoint
this.storeUser(user, index)
},
//actions.js
createUser ({ state, commit }, user) {
commit('CREATE_USER', user)
commit('SET_TEMP_OBJECT', { name: '', car: null })
},
//mutations
CREATE_USER (state, user) {
state.users.push(user)
},
SET_TEMP_OBJECT (state, user) {
state.temp = user
},
<table>
<tr v-for="user, index in filteredUsers">
<td>{{ index + 1 }}</td>
<td>
<input
v-if="isEditable('name', user, index)"
v-model="temp.name"
v-focus="!isCreateMode(user)" />
<div v-if="isEditMode('name', user)"
#click="updateField('name', user)"></div>
<span v-if="isEditMode('name', user)"
#click="cancelUpdate('name', user)"></span>
<span
v-if="isShowMode('name', user)"
#click="editField('name', user)">{{ user.name }}</span>
</td>
<td>
<a href="javascript:void(0)"
#click="addUser()">
Add user</a>
</td>
// other columns
</table>
How about preventing the user from adding a new input unless he submits the existing one:
i can't really see how your code works since you didn't provide the temp / state of your store data but the suggested solution should be something like this :
new Vue({
el: "#app",
data: {
users: ['foo', 'bar'],
input: '',
counter: 0
},
methods: {
addUser(user) {
if (this.counter == 0) {
this.counter++
} else if (this.counter == 1 && user) {
this.counter--
this.input = ''
this.users.push(user)
} else {
alert('please fill the exisiting input')
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input v-for="n in counter" type="text" v-model="input">
<button #click="addUser(input)">addUser</button>
<ul>
<li v-for="user in users">{{user}}</li>
</ul>
</div>

select/deselect single element in v-for

I'm trying to make an option list with v-for, where you can choose only one option at a time. It works great except I can't unselect the option.
<div id="main">
<ul>
<li
v-for="l in list"
id="l.key"
#click="selectone(l.key, l.isSelected)"
v-bind:class="{ selected : l.isSelected, notselected : !l.isSelected }"
> {{ l.tec }} </li>
<ul>
</div>
The JS
new Vue({
el:"#main",
data: {
list: [
{key:"0", tec:"html", isSelected:false},
{key:"1", tec:"css", isSelected:false},
{key:"2", tec:"JS", isSelected:false},
{key:"3", tec:"Git", isSelected:false},
{key:"4", tec:"NodeJS", isSelected:false},
{key:"5", tec:"Postgres", isSelected:false}
]
},
methods: {
selectone: function(k, o) {
for( i = 0; i < this.list.length; i ++ ) {
if(this.list[i].isSelected == true ) {
this.list[i].isSelected = false
}
}
this.list[k].isSelected = !this.list[k].isSelected;
}
}
})
CSS
.selected {
background:lightpink;
}
.notselected {
background:lightblue;
}
Shouldn't my loop deactivate all options every time I click an element?
Your close. try this: (un tested)
<div id="main">
<ul>
<li
v-for="(l,index) in list"
id="l.key"
#click="selectone(l, index)"
v-bind:class="{ selected : l.isSelected, notselected : !l.isSelected }"
> {{ l.tec }} </li>
<ul>
</div>
The JS
new Vue({
el:"#main",
data: {
list: [
{key:"0", tec:"html", isSelected:false},
{key:"1", tec:"css", isSelected:false},
{key:"2", tec:"JS", isSelected:false},
{key:"3", tec:"Git", isSelected:false},
{key:"4", tec:"NodeJS", isSelected:false},
{key:"5", tec:"Postgres", isSelected:false}
]
},
methods: {
selectone:function(l, index){
for( i = 0; i < this.list.length; i ++ ) {
this.list[i].isSelected = false
}
l.isSelected = true;
}
}
}
})
to explain you were miss using the variable k in your function. that should be the entire object not the index
In selectone(), you're setting isSelected=false for all list items, and then attempting to toggle the selected list item's isSelected, which was just previously set to false (i.e., the "toggle" would always set isSelected=true for the selected item).
The loop should exclude the key of the selected item:
selectone(key) {
for (let i = 0; i < this.list.length; i++) {
if (this.list[i].key !== key) {
this.list[i].isSelected = false
}
}
// this.toggleSelection(key)
}
But the toggling code itself needs a bug-fix to properly lookup the list item. The first argument to selectone() is the key property of the list item. In order to get the item by key from the list array, you have to search the list, e.g., using Array.prototype.find():
toggleSelection(key) {
const listItem = this.list.find(item => item.key === key)
if (listItem) {
listItem.isSelected = !listItem.isSelected
}
}
new Vue({
el: '#app',
data: {
list: [
{key:"0", tec:"html", isSelected:false},
{key:"1", tec:"css", isSelected:false},
{key:"2", tec:"JS", isSelected:false},
{key:"3", tec:"Git", isSelected:false},
{key:"4", tec:"NodeJS", isSelected:false},
{key:"5", tec:"Postgres", isSelected:false}
]
},
methods: {
selectone(key) {
for (let i = 0; i < this.list.length; i++) {
if (this.list[i].key !== key) {
this.list[i].isSelected = false
}
}
this.toggleSelection(key)
},
toggleSelection(key) {
const listItem = this.list.find(item => item.key === key)
if (listItem) {
listItem.isSelected = !listItem.isSelected
}
}
}
})
.selected {
background:lightpink;
}
.notselected {
background:lightblue;
}
<script src="https://unpkg.com/vue#2.6.10"></script>
<div id="app">
<ul>
<li
v-for="l in list"
id="l.key"
#click="selectone(l.key, l.isSelected)"
v-bind:class="{ selected : l.isSelected, notselected : !l.isSelected }"
> {{ l.tec }} </li>
<ul>
</div>
Alternatively, you could track the selected index, set it in the item's click-handler, and set the class binding based on the item's index matching the selected index:
// template
<li
v-for="(l, index) in list"
id="l.key"
#click="selectedIndex = index"
v-bind:class="{ selected: index === selectedIndex, notselected: index !== selectedIndex }"
> {{ l.tec }} </li>
// script
export default {
data() {
return {
selectedIndex: -1,
...
}
}
}
new Vue({
el: '#app',
data: {
selectedIndex: -1,
list: [
{key:"0", tec:"html", isSelected:false},
{key:"1", tec:"css", isSelected:false},
{key:"2", tec:"JS", isSelected:false},
{key:"3", tec:"Git", isSelected:false},
{key:"4", tec:"NodeJS", isSelected:false},
{key:"5", tec:"Postgres", isSelected:false}
]
}
})
.selected {
background:lightpink;
}
.notselected {
background:lightblue;
}
<script src="https://unpkg.com/vue#2.6.10"></script>
<div id="app">
<ul>
<li
v-for="(l, index) in list"
id="l.key"
#click="selectedIndex = index"
v-bind:class="{ selected : index === selectedIndex, notselected : index !== selectedIndex }"
> {{ l.tec }} </li>
<ul>
</div>

v-if and v-else not working after data array change

Depending on the array 'r.meta.fields' a specific sort icon of each column needs to be shown. When the template is rendering, it is working correctly. But when the array change, the template isn't changing anymore.
<th v-for="field in r.meta.fields">
{{field.label}}
<a href="#" #click.prevent="sortField(field)">
<div class="fa fa-sort-up" v-if="field.sort_direction === 'desc'"></div>
<div class="fa fa-sort-down" v-else-if="field.sort_direction === 'asc'"></div>
<div class="fa fa-sort" v-else-if="field.sortable"></div>
</a>
What could be the problem?
You could create a mapping for the sort icons and handle the changes on click:
const vm = new Vue({
el: '#app',
data() {
const iconMap = {
sort: {
'asc': 'fa-sort-up',
'desc': 'fa-sort-down'
}
};
return {
r: {
meta: {
fields: [
{
label: 'field #1',
sortable: false,
sort_direction: 'asc',
icon: ''
},
{
label: 'field #2',
sortable: true,
sort_direction: 'desc',
icon: iconMap.sort['desc']// Initially sortable in descending order
}
]
}
},
iconMap
}
},
methods: {
sortField(field) {
let direction = (field.sort_direction === 'asc') ? 'desc' : 'asc';
let icon = this.iconMap.sort[direction] || '';
field.sort_direction = direction;
field.icon = icon;
}
}
})
Template or HTML
<div id="app">
<table>
<tr>
<th v-for="field in r.meta.fields" :key="field.label">
{{field.label}}
<a href="#"
:class="field.icon"
#click.prevent="sortField(field)"></a>
</th>
</tr>
</table>
</div>
if you are using something like
r.meta.fields = newValue
then this won't work.
you should use
Vue.set(r.meta.fields, indexOfItem, newValue)
document here: vue update array