Sort titles in Array of objects with VueJS - vue.js

I am trying to sort my array of objects. I have managed to use the console to return all the titles but struggling now to perform the actual sort. I am using a select menu to switch between relevancy (default) and to sort alphabetically.
Any help is greatly appreciated
<select v-model="sortatoz" v-on:change="sortItems($event)">
<option disabled value="">Select</option>
<option value="alphabetically">Alphabetically</option>
<option value="relevance">Relevance</option>
</select>
methods: {
sortItems: function sortItems(event) {
this.sortby = event.target.value;
if (this.sortatoz === "alphabetically") {
Array.from(
document.querySelectorAll("div > h3.title")
).map((x) => x.innerText);
??What should I add here to sort???
} else {
Array.from(
document.querySelectorAll("div > h3.title")
).map((x) => x.innerText);
???
}
},

Step 1: Create HTML template with select options and table to display data with sorting functionality.
<div id="app">
<select v-model="sortatoz" #change="sortItems">
<option disabled value="">Select</option>
<option value="alphabetically">Alphabetically</option>
<option value="relevance">Relevance</option>
</select>
<table border="1">
<thead>
<tr>
<th>Title</th>
<th>Authur</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in sortedItems" :key="index">
<td>{{ item.title }}</td>
<td>{{ item.authur }}</td>
</tr>
</tbody>
</table>
</div>
Step 2: Define your model data. I created a sample model data with books title with Authur names
data() {
return {
sortatoz: "",
listTitles: [
{
title: "Cabbages and Kings",
authur: "O. Henry",
},
{
title: "Alien Corn",
authur: "Sidney Howard",
},
{
title: "The Little Foxes",
authur: "Lillian Hellman",
},
{
title: "A Time of Gifts",
authur: "Patrick Leigh Fermor",
},
{
title: "Ego Dominus Tuus",
authur: "W. B. Yeats",
},
{
title: "Antic Hay",
authur: "Aldous Huxley",
},
],
};
},
Step 3: Define your computed lifecycle. When ever changes happened in the model data automatically computed function will get updated.
computed: {
sortedItems() {
return this.listTitles;
},
},
Step 4: Define #change event for select options
methods: {
sortItems() {
if (this.sortatoz === "alphabetically") {
return this.listTitles.sort((a, b) => (a.title > b.title ? 1 : -1));
} else {
return this.listTitles.sort((a, b) => (a.title > b.title ? -1 : 1));
}
},
},
DEMO

Related

Standard "check-all" functionality in table

Here's a part of my grid (CRUD) component:
<template>
<table class="MyComponent table">
<thead>
<tr>
<th width="30px">
<b-form-checkbox v-model="allChecked" />
</th>
</tr>
</thead>
<tbody>
<tr v-for="(record, index) in records" :key="index">
<td width="30px">
<b-form-checkbox :value="record['id']" v-model="checkedRows" />
</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
name: "MyComponent",
components: {
},
props: ['config'],
data() {
return {
records: [{
id: 1
}, {
id: 2
}, {
id: 3
}, {
id: 4
}, {
id: 5
}, {
id: 6
}],
checkedRows: []
}
},
computed: {
allChecked: {
get() {
return this.records.length == this.checkedRows.length
},
set(v) {
if(v) {
this.checkedRows = [];
for(var i in this.records) {
this.checkedRows.push(this.records[i]['id'])
}
}
else {
this.checkedRows = [];
}
}
}
}
};
</script>
As you can see, I would like to achive a standard, widely used functionality: The user can check multiple rows and do some operation with the selected rows. The problem is with the "check all" checkbox on the top of the table. When I check all, then I remove the tick from only one checkbox below, it unchecks all the checkboxes on page.
I understand why its happening: When I remove a tick from on of the checkboxes below, the "this.records.length == this.checkedRows.length" condition will no longer be true, so the "allChecked" computed variable will be set to false, therefore the top checkbox will set to unchecked. The problem is: when the top checkbox will be unchecked, then all of the checkboxes will be unchecked as well, because of the "set" part of the computed variable.
Is there a clean way to solve this problem in Vue?
I'm not sure what you want to do with the checked rows, but maybe this will be better:
<b-form-checkbox :value="record['id']" v-model="record.checked" />
Then add to your objects in records a checked property.
records: [
{
id: 1,
checked: false
},
...
]
and if you need a list of checked records you might do a computed property:
computed: {
checkedRecords() {
return this.records.filter(record => record.checked);
}
}
and for checking-unchecking all you just iterate over all records:
<b-form-checkbox #change="clickedAll" />
methods: {
clickedAll(value) {
this.records = this.records.map(record => {
record.checked = value
return record
}
}
}
OK, meanwhile I solved the problem. Here's my solution. Thanks #Eggon for your help, you gave the idea to use the #change method.
<template>
<table class="MyComponent table">
<thead>
<tr>
<th width="30px">
<b-form-checkbox v-model="allChecked" #change="checkAll" />
</th>
</tr>
</thead>
<tbody>
<tr v-for="(record, index) in records" :key="index">
<td width="30px">
<b-form-checkbox :value="record['id']" v-model="checkedRows" />
</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
name: "MyComponent",
components: {
},
props: ['config'],
data() {
return {
records: [{
id: 1
}, {
id: 2
}, {
id: 3
}, {
id: 4
}, {
id: 5
}, {
id: 6
}],
checkedRows: []
}
},
methods: {
checkAll(value) {
if(!value) {
this.checkedRows = [];
return ;
}
var newCheckedRows = [];
for(var i in this.records) {
newCheckedRows.push(this.records[i].id)
}
this.checkedRows = newCheckedRows;
}
},
computed: {
allChecked: {
get() {
return this.records.length == this.checkedRows.length
},
set() {
}
}
}
};
</script>

Change v-model value without changin the actual data

So i've this data
data: () => ({
products: [
{ id: 1, name: "Prod 1", price: 2, stock: 5 },
{ id: 2, name: "Prod 2", price: 3, stock: 6 }
]
})
Template
<table>
<thead>
<tr>
<th>id</th>
<th>name</th>
</tr>
</thead>
<tbody>
<tr v-for="product in products" :key="product.id">
<td>{{ product.id }}</td>
<td>{{ product.name }}</td>
<td>
<input
type="text"
class="form-control"
v-model="product.price"
#paste.prevent
/>
</td>
<td>
<input
type="text"
class="form-control"
maxlength="999"
v-model="product.stock"
#paste.prevent
#keypress="onlyNumber($event)"
#input="handleInputStock($event.target.value)"
#blur="updateStock($event.target.value, product.id)"
/>
</td>
</tr>
</tbody>
</table>
So what I want is that when the user hit delete/backspace from the stock input field the value cannot be empty (blank) or it must be greater than or equal to zero. but without changing the products.stock value. this is because I need the product.stock value to compare with the changed value (stock input field) before sending to the server. So if stock value is equal to product.stock don't send to server otherwise send and update stock value.
so here's what i've done so far.
prevent the stock value empty but not working
handleInputStock(value) {
return +value.replace(/[^0-9]/g, "");
},
update stock
updateStock(stock, productId) {
const productStock = this.products.find(product => product.id == productId).stock;
if (!(stock == productStock)) {
// do ajax
}
},
onlyNumber
onlyNumber(e) {
const charCode = e.which ? e.which : event.keyCode;
if (charCode > 31 && (charCode < 48 || charCode > 57)) {
e.preventDefault();
}
},
Personally this feels like a higher level question to which your flow of product editing needs tweaking. Here is what I can think of:
User enters all the information.
User hits submit button.
Check whether of not the stock count is empty or 0.
Return an error message if it is.
Submit and update otherwise.
It might be worth looking into vuelidate that handles such validation in JavaScript. Meanwhile, we are also coming up with a tool called CRUDS DS (a WIP) that handles such situation with ease.
The best way is to create a ProductComponent and watch every product separately inside its own component, as shown below:
Product.vue
<ProductComponent
v-for="product in products"
:product="product"
:key="product.id" />
ProductComponent.vue
<template>
<tr>
<td>{{ product.name }}</td>
<td>
<input
type="text"
class="form-control"
v-model="product.price"
#paste.prevent
/>
</td>
<td>
<input
type="text"
class="form-control"
maxlength="999"
v-model="product.stock"
#paste.prevent
#keypress="onlyNumber($event)"
#blur="updateStock($event.target.value, product.id)"
/>
</td>
</tr>
</template>
<script>
export default {
props: {
product: {
type: Object,
default: {},
},
},
data: () => ({ actual_stock: "" })
// this is for handle stock cannot be empty or GTE:0
// also you dont need handleInputStock anymore
watch: {
product: {
handler(val) {
this.actual_stock = val.stock;
},
immediate: true,
},
"product.stock": function (newVal, oldVal) {
this.product.stock = +newVal;
},
},
methods: {
updateStock(stock, productId) {
if (!(stock == this.actual_stock)) {
// do ajax
}
}
}
}
</script>
If you want to handle it on parent side, you may use $emit to send an event upwards.
Can we have two versions of products? One for the server, one for v-models.
var server_products = [
{ id: 1, name: "Prod 1", stock: 5 },
{ id: 2, name: "Prod 2", stock: 6 }
]
//...
data: () => ({
products = server_products
})
updateStock(stock, productId) {
server_products.forEach((product) => {
if(product.id === productId && stock !== product.stock){
product.stock = stock
// do ajax
}
})
},
//...
If not than you can use vue's watch property so vue finds changes to the array for you.
//...
data: () => ({
products: [
{ id: 1, name: "Prod 1", stock: 5 },
{ id: 2, name: "Prod 2", stock: 6 }
]
}),
watch: {
'products': {
handler: function(newValue) {
// do ajax
},
deep: true
}
}
//...

Creating a button dynamically in vuejs table

I am trying to create a table in vuejs for a personal project (I dont want to use a existing table) and I am facing probably a newbie problem.
I am trying to insert on my last column some buttons, but I dont know why, the grid is rendering my element tag instead of the element himself.
May someone explain to me why this does not work ? And, how can I create this feature ?
Fiddle: https://jsfiddle.net/y7830cvd/1/
<div id="app">
<div>
<vue-grid :rows="gridData" :title="nTitle"></vue-grid>
</div>
</div>
Vue.component('vue-grid', {
props: ['rows', 'title'],
template: `<div>
<h2>{{title}}</h2>
<div class="table-wrapper">
<table class="fl-table">
<thead>
<tr>
<th v-for="col in columns" :key="col.id" v-on:click="sortTable(col)">{{col}}</th>
</tr>
</thead>
<tbody v-if="rows.length > 0">
<tr v-for="row in rows" :key="row.id">
<td v-for="col in columns" :key="col.id">{{row[col]}}</td>
</tr>
</tbody>
</table>
</div>
</div>`,
computed: {
columns: function columns() {
if (this.rows.length == 0) {
return []
}
return Object.keys(this.rows[0])
}
},
sortTable(col) {
this.rows.sort(function(a, b) {
if (a[col] > b[col]) {
return 1
} else if (a[col] < b[col]) {
return -1
}
return 0
})
},
methods: {
formatter(row, column) {
return row.address
},
filterTag(value, row) {
return row.tag === value
},
filterHandler(value, row, column) {
const property = column['property']
return row[property] === value
}
}
});
var app = new Vue({
el: '#app',
data(){
return {
gridData: [
{"id" : 1, "name": "firstValue", "something": "wha the fox say?","options" : "<button>Add</button>" },
{"id" : 1, "name": "firstValue", "something": "uauu uauu uauu?"},
{"id" : 1, "name": "firstValue", "something": "The cow goes mu?"}
],
nTitle: "Hello There!"
}},
})
Try v-html:
<td v-for="col in columns" :key="col.id">
<span v-if="col == 'options'" v-html="row[col]"></span>
<span v-else>{{row[col]}}</span>
</td>
Something you should consider (source documentation - link above):
Updates the element’s innerHTML. Note that the contents are inserted
as plain HTML - they will not be compiled as Vue templates. If you
find yourself trying to compose templates using v-html, try to rethink
the solution by using components instead.

Vue.js How to recalculate the displayed data after changing the select-option?

The task - when changing the unit of measurement, consider the total weight of the products.
Now the data in the array is changing, but new data is displayed on the screen only when click on other input fields
There is a table with data from the array, with the fields: Product name, net weight, quantity.
There is a select in the table with a choice of unit of measure - kg, grams, in order to understand in which units the "net weight" is set
for each value there is a conversion coefficient conversion_num from a separate array
Now, when changing the unit of measure, the unitChange function is launched, which changes the data in the array, but the changes are not displayed on the screen.
I tried with computed function, it work same.
What am I doing wrong?
HTML:
<tr v-for="(comp, i) in st">
<td>{{comp.name}}</td>
<td>
<input v-model.number="comp.weight_netto">
</td>
<td>
<select #change="unitChange($event.target.value, i)" v-model="comp.unit_id">
<option v-for="val in ul" :value="val.unit_id" v-text="val.name" ></option>
</select>
</td>
<td>
<input v-model.number="comp.quantity">
</td>
<td>{{itemqty(i)}}</td>
</tr>
JS:
methods: {
unitChange(value, i){
for (var j=0; j<this.ul.length; j++){
if (this.ul[j].unit_id==value){
this.st[i].conversion_num=this.ul[j].conversion_num;
break;
}
}
},
itemqty(i){
return (this.st[i].weight_netto*this.st[i].quantity)/this.st[i].conversion_num;
}
}
Don't mutate your products!
You'll end up with rounding errors: changing from one unit to another and back to initial will result in a different value from initial, because of the two roundings.
And it also makes things more difficult for you as everywhere you want to use the product you have to take into account its current unit.
A much cleaner solution is to leave the weight and unit unchanged (in the unit of your choice) and use a method to parse the displayed value, based on currently selected unit.
Your products remain unchanged (neither weight values or weight units change).
Proof of concept:
Vue.config.productionTip = false;
Vue.config.devtools = false;
new Vue({
el: '#app',
data: () => ({
products: [
{ id: 1, name: 'one', weight: 20},
{ id: 2, name: 'two', weight: 17}
],
units: [
{label: 'kgs'},
{label: 'lbs', factor: 2.20462},
{label: 'oz', factor: 35.274}
],
currentUnit: {label: 'kgs'}
}),
methods: {
convertToUnit(weight) {
return (Math.round(weight * (this.currentUnit.factor || 1) * 100) / 100) +
this.currentUnit.label
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
Current unit:
<select v-model="currentUnit">
<option v-for="(unit, key) in units" :value="unit" :key="key">{{ unit.label }}</option>
</select>
<div v-for="(product, k) in products" :key="k">
name: <span v-text="product.name"></span>,
weight: <span v-text="convertToUnit(product.weight)"></span>
</div>
</div>
You should use Vue.set
Also some things can be refactored
Example
const table = Vue.component('product-table', {
data() {
return {
products: [{
name: 'one',
weight_netto: 1.23,
quantity: 4,
conversion_num: .420,
unit_id: 1
},{
name: 'two',
weight_netto: 3.21,
quantity: 5,
conversion_num: .69,
unit_id: 2
}],
units: [{
unit_id: 1,
conversion_num: .420,
name: 'kg'
},
{
unit_id: 2,
conversion_num: .69,
name: 'lb'
}
]
}
},
template: `
<table>
<tbody>
<tr :key="product.name" v-for="product in products">
<td>{{product.name}}</td>
<td>
<input v-model.number="product.weight_netto">
</td>
<td>
<select #change="unitChange($event.target.value, product)" v-model="product.unit_id">
<option :key="unit.unit_id" v-for="unit in units" :value="unit.unit_id" v-text="unit.name" ></option>
</select>
</td>
<td>
<input v-model.number="product.quantity">
</td>
<td>{{itemqty(product)}}</td>
</tr>
</tbody>
</table>`,
methods: {
unitChange(unit_id, product) {
unit_id = Number(unit_id);
const { conversion_num } = this.units.find(pr => pr.unit_id === unit_id);
Vue.set(product, 'conversion_num', conversion_num);
},
itemqty({ weight_netto, quantity, conversion_num}) {
return ((weight_netto * quantity) / conversion_num).toFixed(2);
}
}
})
new Vue({
el: '#container'
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="container">
<product-table>
</product-table>
</div>

Vuetify datatable watch does not trigger

The function "change sort" executed on header item click does not trigger watch, which fetches updated data from server. On the other hand, if I try to execute fetchRecords method at the end of changeSort method, watch gets triggered multiple times
I need to use Vuetify datatables with server side pagination and sorting, and templates for header and items. I implemented code similarly to Vuetify examples: "Paginate and sort server-side" and "Slot: items and headers" from documentation https://vuetifyjs.com/en/components/data-tables#api.
<template>
<v-card class="table-container">
<v-card-title>
<v-text-field
v-model="searchField"
#blur="fetchRecords()"
#keyup.enter="fetchRecords()"
/>
</v-card-title>
<v-data-table
:headers="headers"
:items="applications.applications"
:loading="paginationLoading"
:pagination.sync="pagination"
:total-items="pagination.totalItems"
no-data-text="No results"
>
<template v-slot:headers="props">
<tr>
<th colspan="4">Dane kandydata</th>
<th colspan="4">Dane polecającego</th>
<th colspan="2">Inne</th>
</tr>
<tr>
<th
v-for="header in props.headers"
:key="header.value"
:class="['column',
header.sortable !== false ? 'sortable' : '' ,
pagination.descending ? 'desc' : 'asc',
header.value === pagination.sortBy ? 'active' : '']"
#click="changeSort(header.value, header.sortable)"
>
{{ header.text }}
<v-icon v-if="header.sortable !== false" small>fas fa-sort-up</v-icon>
</th>
</tr>
</template>
<template v-slot:items="props">
<td>{{ props.item.candidateName }}</td>
<td>{{ props.item.candidateSurname }}</td>
<td>{{ props.item.candidateEmail }}</td>
<td>{{ props.item.candidatePhone }}</td>
<td>{{ props.item.referrerName }}</td>
<td>{{ props.item.referrerSurname }}</td>
<td>{{ props.item.referrerEmail }}</td>
<td>{{ props.item.referrerPhone }}</td>
<td class="text-md-center">
<div>
<v-icon>check_circle_outline</v-icon>
</div>
</td>
<td class="text-md-center">
<div>
<v-icon>check_circle_outline</v-icon>
</div>
</td>
</template>
<v-alert v-slot:no-results :value="true" color="error" icon="warning">
Your search for "{{ searchField }}" found no results.
</v-alert>
</v-data-table>
</v-card>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
data () {
return {
announcementId: '',
announcementTitle: '',
searchField: '',
headers: [
{ text: 'Imię', value: 'candidateName' },
{ text: 'Nazwisko', value: 'candidateSurname' },
{ text: 'Email', value: 'candidateEmail', sortable: false },
{ text: 'Telefon', value: 'candidatePhone' },
{ text: 'Imię', value: 'referrerName' },
{ text: 'Nazwisko', value: 'referrerSurname' },
{ text: 'Email', value: 'referrerEmail', sortable: false },
{ text: 'Telefon', value: 'referrerPhone' },
{ text: 'Status', value: 'processed' },
{ text: 'Akcje', value: 'actions', sortable: false },
],
pagination: {
page: 1,
rowsPerPage: 10,
totalItems: 10,
sortBy: '',
descending: false,
},
paginationLoading: false
}
},
computed: {
...mapState([
'applications',
])
},
watch: {
pagination: {
handler(newVal, oldVal){
if(newVal != oldVal){
this.fetchRecords()
}
},
deep: true
}
},
methods: {
...mapActions([
'getApplications',
]),
fetchRecords () {
this.paginationLoading = true
this.getApplications({
announcementId: this.announcementId,
pagination: this.pagination,
search: this.searchField
})
.then(
response => {
this.paginationLoading = false
if(this.pagination.totalItems != response.totalItems) this.pagination.totalItems = response.totalItems
}
)
.catch(
err => {
console.log(err);
}
)
},
changeSort (columnValue, sortable) {
if(sortable === false){
return
}
if (this.pagination.sortBy === columnValue) {
this.pagination.descending = !this.pagination.descending
console.log(this.pagination.descending);
} else {
this.pagination.sortBy = columnValue
this.pagination.descending = false
}
},
editTableItem(item){
console.log(item);
},
onClearSearch() {
this.searchField = ''
this.fetchRecords()
},
}
}
</script>
You should create new object when changing pagination object. Here I use ES6 syntax:
changeSort (columnValue, sortable) {
if(sortable === false){
return
}
if (this.pagination.sortBy === columnValue) {
this.pagination = {
...this.pagination,
descending: !this.pagination.descending
}
console.log(this.pagination.descending);
} else {
this.pagination = {
...this.pagination,
sortBy: columnValue,
descending: false
}
}
}