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
}
}
//...
Related
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
Hi I have list like this:
<tr v-for="(post, index) in posts" v-bind:index="index">
<td>{{ post.rut }}</td>
<td>{{ post.names }} {{ post.father_lastname }} {{ post.mother_lastname }}</td>
<td>
<input type="number" class="form-control" id="exampleInputEmail1" v-bind:value="post.employee_id" #input="form.amount[post.employee_id]" placeholder="Ingresa el monto">
</td>
</tr>
I defined in v-bind:value="" a initial value for every input of the list, then I need to send that data with axios but when I do that it does not send anything I mean I can not catch the vale for every input why? because it displays the value.. my axios is:
onSubmit(e) {
this.loading = true; //the loading begin
e.preventDefault();
let currentObj = this;
const config = {
headers: { 'content-type': 'multipart/form-data' }
}
let formData = new FormData();
formData.append('amounts', JSON.stringify(this.form.amount));
axios.post('/api/payroll_management/store?api_token='+App.apiToken, formData, config)
.then(function (response) {
currentObj.success = response.data.success;
})
.catch(function (error) {
console.log(error);
});
}
so I wonder how can I get the data from the inputs? if it returns empty this.form.amount
Thanks
Since each post has an amount value that gets changed with the <input>, it is easier to have that value be part of the post item itself.
This is done using v-model="post.amount" (see documentation) on the <input> of each post.
This way, there is a single place where the amount value is and where it gets updated.
Then when you submit the form, you can get the an array of these amount values by using a computed property (see documentation).
For better understanding what is happening, I highly recommend going through VueJS's documentation, since it's very readable and explains everything quite well.
Now, bringing it all together, have a look at this example:
new Vue({
el: "#app",
data: {
posts: [
{
rut: "A",
names: "Name Name",
father_lastname: "Lastname",
mother_lastname: "Lastname2",
employee_id: 5,
amount: 5, // Default value here
},
{
rut: "B",
names: "Name Name",
father_lastname: "Lastname",
mother_lastname: "Lastname2",
employee_id: 2,
amount: 2, // Default value here
},
],
},
computed: {
// Make form data a computed object.
form: function() {
// Get only the amount values from the posts.
// The inputs will update those automatically,
// so these will change as well.
let amount = this.posts.map((post, idx) => {
return parseInt(post.amount); // parseInt() is optional here
});
return {
amount: amount,
// ... other stuff
};
},
},
methods: {
getFormData: function() {
console.log( JSON.stringify(this.form.amount) );
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<h2>Posts:</h2>
<table>
<tr v-for="(post, index) in posts" v-bind:index="index">
<td>{{ post.rut }}</td>
<td>{{ post.names }} {{ post.father_lastname }} {{ post.mother_lastname }}</td>
<td>
<input type="number" class="form-control" v-model="post.amount">
</td>
</tr>
</table>
<br/>
<button v-on:click="getFormData()">Console log form data</button>
</div>
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>
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>
I have a button that adds new field with the use of array.push. I was able to fetch the right data while using a REST client (Insomnia). I'm aware of how to display the data to a single form using response => this.item = response.data, but how do I loop the response.data to display all the data entered in the array.push fields?
Please see my code below.
Component.vue
<template>
<v-btn #click="addRow()">Add New</v-btn>
</template>
<table class="table">
<tbody>
<tr v-for="(row, index) in books" :key="row.id">
<td>
<base-select
v-model="row.book_id"
:items="books"
item-text="title"
item-value="id"
label="Sample Books"
/>
</td>
<td><v-textarea label="Title" v-model="row.title" /></td>
<td><v-textarea label="Genre" v-model="row.genre" /></td>
<td><v-text-field label="Pages" v-model="row.pages" /></td>
<td><a #click="removeRow(index);">Remove</a></td>
</tr>
</tbody>
</table>
<script>
export default {
data: ()=> ({
books: [],
}),
created () {
this.getBooks ()
},
methods: {
addRow () {
this.books.push({
book_id: '',
title: '',
genre: '',
pages: '',
});
},
getBooks () {
axios.get('/api/books', {
params: { id: this.$route.params.id }
})
.then(response => this.books = // Display all data to each field in row (example there are 3 records existing with the id of params.id) )
.catch(error => console.log(error))
},
removeRow (index) {
this.buttons.splice(index, 1);
},
}
}
</script>
Screnshot
I have 5 data in the database and it reflects the number of rows, but the fields are empty.