Multiple select inputs in table header with unique models - vue.js

I am receiving data from the backend that takes the following format
[
[
[ "123", "21/11/2013", "Data", "Data" ],
[ "234", "22/11/2013", "Data", "Data" ],
[ "345", "12/09/2018", "Data", "Data" ],
],
[
[ "123", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data" ],
[ "234", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data" ],
[ "345", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data" ]
]
]
Each fileData represents a table, so in the above example it should produce two tables. The data inside contains a tables rows, so each table above has two rows.
In order to achieve this I am doing something like the following.
<table class="table" v-for="(file, index) in fileData" :key="index">
<tbody>
<tr v-for="(row, index2) in file":key="index2">
<td v-for="(data, index3) in row" :key="index3">
{{ data }}
</td>
</tr>
</tbody>
</table>
This all seems to work fine. However, the data I am using does not have headers, but I need to prodive a header for each column that contains a select. As such I have added the following
<table class="table" v-for="(file, index) in fileData" :key="index">
<thead>
<tr>
<th scope="col" v-for="(col, index2) in file[index]" :key="index2">
<b-form-select v-model="options.value" :options="options"></b-form-select>
</th>
</tr>
</thead>
</table>
Once again this seems to work. My problem is that I want the user to define what a column represents, using the select. At the moment, if I select something, they all change.
I have produced this Fiddle as an example https://jsfiddle.net/mhyv62bt/1/
How can I make the selects independent, and is it also possible to remove an option once selected?
Thanks
This seems to produce the correct number of header columns for each table.
Update
I have a slightly different setup so trying to fit it in with my project. As such, I created the file THeadSelect.vue
<template id="theadselect">
<thead>
<tr>
<th
v-for="(i,index) in this.length_"
:key="index"
>
<select
v-model="headers[index]">
<option disabled value="">
Please select one
</option>
<option
v-if="headers[index]"
selected
>
{{headers[index]}}
</option>
<option
v-for="option in filteredOptions"
:key="option"
>
{{option}}
</option>
</select>
</th>
</tr>
</thead>
</template>
<script>
export default {
mounted () {
this.$emit('update:headers',
this.headers
.concat(Array.from({ length: this.length_ }, _ => ''))
.slice()
)
},
props: {
options: {
type: Array,
required: true
},
length: Number,
headers: {
type: Array,
required: true
}
},
computed: {
length_: {
get () {
return this.length || this.options.length
},
set (l) {
this.$emit('update:length', l)
}
},
filteredOptions () {
return this.options.filter(
option => !this.headers.includes(option)
)
}
}
}
</script>
I am then trying to use this within my page
<template>
<div>
<b-form
novalidate
#submit.stop.prevent=""
>
<div class="row">
<div class="col-12">
<table class="table table-bordered" v-for="(file, index) in fileData" :key="index">
<thead
is="THeadSelect"
:options="['option1', 'option2', 'option3']"
:headers.sync="headers"
></thead>
<tbody>
<tr v-for="(row, index2) in file" :key="index2">
<td v-for="(data, index3) in row" :key="index3">
{{ data }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</b-form>
</div>
</template>
<script>
import { THeadSelect } from '#/components/common/THeadSelect'
export default {
components: {
THeadSelect
},
computed: {
fileData () {
return this.$store.getters.fileData
}
},
data () {
return {
headers: [],
length: 10,
}
}
}
</script>
It is a bit messed up though. Only 3 selects are being displayed for each table. Additionally, if I select an option in table 1, it selects the same option in table 2. If you check out my original fiddle you can see the initial data I am trying to work with, so there will always be two tables.

<div id='app'>
<table class="table table-bordered" v-for="(file, index) in fileData" :key="index">
<thead>
<tr>
<th scope="col" v-for="(col, index2) in file[index]" :key="index2+index">
<b-form-select v-model="selectedValue[index+index2]" :options="options">
<template v-slot:first>
<b-form-select-option :value="null" disabled>Ignore</b-form-select-option>
</template>
</b-form-select>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index2) in file" :key="index2">
<td v-for="(data, index3) in row" :key="index3">
{{ data }}
</td>
</tr>
</tbody>
data: {
selectedValue: [],
mappedColumns: [],
selected: null,
options: [{
value: '1',
text: 'Option 1'
},
{
value: '2',
text: 'Option 2'
},
{
value: '3',
text: 'Option 3'
}
],
You are using a single value in v-model for all the dropdowns. so, once you change a single dropdown. All of them gets changed.
Try the above solution, where I declared a new array in data which is selectedValue
You can keep the data of which dropdown is selected in this array

I guess you can make this a component
The header:
Possible usage:
<thead
is="THeadSelect"
:options="header_row"
:length="length /*defaults to options.length*/"
:headers.sync="headers"
></thead>
The ComponentOptions
//*.js
const THeadSelect = {
template: "#theadselect",
mounted(){
// make sure headers is populated
this.$emit("update:headers",
this.headers
.concat(Array.from({length:this.length_}, _=>""))
.slice()
)
},
props: {
options: {
type: Array,
required: true,
},
length: Number,
headers: {
type: Array,
required: true
}
},
computed:{
length_:{
get(){
return this.length || this.options.length
},
set(l){
this.$emit("update:length",l)
}
},
filteredOptions(){
return this.options.filter(
option => !this.headers.includes(option)
)
}
}
}
The template
// *.html
<template id="theadselect">
<thead>
<tr>
<th
v-for="(i,index) in length_"
:key="index"
>
<select
v-model="headers[index]">
<option disabled value="">
Please select one
</option>
<option
v-if="headers[index]"
selected
>
{{headers[index]}}
</option>
<option
v-for="option in filteredOptions"
:key="option"
>
{{option}}
</option>
</select>
</th>
</tr>
</thead>
</template>
Example
const THeadSelect = {
template: "#theadselect",
mounted(){
// make sure headers is populated
this.$emit("update:headers",
this.headers
.concat(Array.from({length:this.length_}, _=>""))
.slice()
)
},
props: {
options: {
type: Array,
required: true,
},
length: Number,
headers: {
type: Array,
required: true
}
},
computed:{
length_:{
get(){
return this.length || this.options.length
},
set(l){
this.$emit("update:length",l)
}
},
filteredOptions(){
return this.options.filter(
option => !this.headers.includes(option)
)
}
}
}
new Vue({
components: {THeadSelect},
data(){
return {
headers: [],
length: 10
}
},
template: "#root"
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template id="theadselect">
<thead>
<tr>
<th
v-for="(i,index) in length_"
:key="index"
>
<select
v-model="headers[index]">
<option value="">
Please select one
</option>
<option
v-if="headers[index]"
selected
>
{{headers[index]}}
</option>
<option
v-for="option in filteredOptions"
:key="option"
>
{{option}}
</option>
</select>
</th>
</tr>
</thead>
</template>
<template id="root">
<div>
<table>
<caption>Sample usage with select</caption>
<thead
is="THeadSelect"
:options="['option1', 'option2', 'option3']"
:headers.sync="headers"
></thead>
<tbody>
<tr>
<td
v-for="(prop, index) in headers"
:key="prop+index"
>
{{ prop || '?'}}
</td>
</tr>
</tbody>
</table>
</div>
</template>
<table id="app"></table>
For the body one can think about the value of the headers array. Maybe put array indeces or object properties instead of currently option values
So for multiple tables one can think about:
<template id="table">
<table
v-for="(table, index) in tables"
:key="'table-'+index"
is="TableSelect"
:headers="table[0]"
:rows="table.slice(1)"
>
</table>
</template>
And for TableSelect:
const TableSelect = {
props: ["headers", "rows"],
template: "#table-select",
data(){
return {
selectedHeaders: []
}
},
computed(){
mappedRows(){
return this.rows
.map(row=> row.map(
(cell, index) => ({[headers[index]]: cell})
).reduce((obj, val) => Object.assign(obj, val))
)}
}
}
<template id="table-select">
<table>
<thead
is="THeadSelect"
:options="headers"
:headers.sync="selectedHeaders"
></thead>
<tbody>
<tr
v-for="(row, index) in mappedRows"
:key="'row-'+index"
>
<td
v-for="cell in selectedHeaders"
:key="cell+index"
>
{{cell && row[cell || ""]}}
</td>
</tr>
</tbody>
</table>
</template>
there are errors in above code but due to lazyness and missing linter on so i will let it be - as it provides the basic idea.
And a running example on codesandbox:
https://lbm8l.csb.app/

Use v-on:change and a function instead of v-model Here is the solution for individual selection
new Vue({
el: "#app",
data: {
mappedColumns: [],
selected: null,
options: [{
value: '1',
text: 'Option 1'
},
{
value: '2',
text: 'Option 2'
},
{
value: '3',
text: 'Option 3'
}
],
fileData: [
[
["123", "21/11/2013", "Data", "Data"],
["234", "22/11/2013", "Data", "Data"],
["345", "12/09/2018", "Data", "Data"],
],
[
["123", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data",
"Data"
],
["234", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data",
"Data"
],
["345", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data",
"Data"
]
]
]
},
methods: {
getSelectedItem(a, b, c) {
console.log(a, b, c);
}
}
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/tether#1.4.7/dist/css/tether.min.css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-vue#2.15.0/dist/bootstrap-vue.css">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<script src="https://unpkg.com/tether#1.4.7/dist/js/tether.min.js"></script>
<script src="https://unpkg.com/bootstrap-vue#2.15.0/dist/bootstrap-vue.js"></script>
<title>Document</title>
<style>
#app {
padding: 20px;
height: 500px;
}
</style>
</head>
<body>
<div id='app'>
<table class="table table-bordered" v-for="(file, index) in fileData" :key="index">
<thead>
<tr>
<th scope="col" v-for="(col, index2) in file[index]" :key="index2">
<b-form-select v-on:change="getSelectedItem($event,index,index2)" :options="options">
<template v-slot:first>
<b-form-select-option :value="null" disabled>Ignore</b-form-select-option>
</template>
</b-form-select>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index2) in file" :key="index2">
<td v-for="(data, index3) in row" :key="index3">
{{ data }}
</td>
</tr>
</tbody>
</table>
</div>
<script src="table.js"></script>
</body>
</html>

Related

display a foreign data in a table vuejs

I am creating a page, in which a section called product, I show data and in this I have a data called id_categoria, where instead of showing the id I want to show the name of that category in question, this data is in the table of categories and in the product table I have the id. I already managed to show the data, but not this, besides that I managed to save and edit it in question, it is only necessary in show.
When working with vuejs I saw that you have to use a v-if but I do not know how to do it, or at least the attempts I have made have been wrong.
this is the code of the table where I show the data
<div class="card-body table-responsive">
<table id="example1" class="table table-bordered table-striped">
<thead>
<tr>
<th>id_producto</th>
<th>imagen</th>
<th>código</th>
<th>producto</th>
<th>categoria</th>
<th>stock</th>
<th>precio_compra</th>
<th>precio_venta</th>
<th>fecha</th>
<th colspan="2" class="text-center">Acciones</th>
</tr>
</thead>
<tbody>
<tr v-for="pro in productos" :key="pro.id_productos">
<th>{{pro.id_productos}}</th>
<td>
<img :src="pro.imagen" class="img-circle elevation-2" alt="Product Image" width="60" />
</td>
<td>{{pro.codigo}}</td>
<td>{{pro.producto}}</td>
<td>{{pro.id_categoria}}</td>
<td>{{pro.stock}}</td>
<td>{{pro.precio_compra}}</td>
<td>{{pro.precio_venta}}</td>
<td>{{pro.fecha}}</td>
<td class="text-center">
<button #click="modificar=true; abrirModal(pro)" type="button" class="editar btn btn-primary"><i class="fa fa-pencil-alt"></i></button>
</td>
<td class="text-center">
<button #click="eliminar(pro.id_productos,pro.producto)" type="button" class="eliminar btn btn-danger" data-toggle="modal" data-target="#modalEliminar"><i class="fas fa-dumpster-fire"></i></button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<th>id_producto</th>
<th>imagen</th>
<th>código</th>
<th>producto</th>
<th>categoria</th>
<th>stock</th>
<th>precio_compra</th>
<th>precio_venta</th>
<th>fecha</th>
<th colspan="2" class="text-center">Acciones</th>
</tr>
</tfoot>
</table>
</div>
and this is the script to bring the info to me
<script>
import axios from "axios";
export default {
watch: {
$route: {
immediate: true,
handler(to, from) {
document.title = to.meta.title || 'Productos';
}
},
},
data() {
return{
productosdatos:{
id_producto: "",
codigo: "",
producto: "",
stock: "",
precio_compra: "",
precio_venta : "",
id_categoria: "",
},
id: 0,
modificar: true,
modal: 0,
tituloModal: '',
productos:[],
categorias:[],
}
},
methods: {
async listarcategorias(){
const res = await axios.get('http://localhost:8000/categoria/');
this.categorias = res.data;
console.log(this.categorias)
},
async listar(){
const res = await axios.get('http://localhost:8000/productos/');
this.productos = res.data;
console.log(this.productos)
},
cerrarModal(){
this.modal = 0;
}
},
created() {
this.listar();
}
}
</script>
as you can see I have a variable called products, where is the id_category corresponding to the product, and categories where I bring all the category info.
the table looks something like this:
how can I make it not show the id of the category but the name of the category in question ?
pd: the category data is received in json as follows:
{
"id_categoria": 8,
"categoria": "Electrodomesticos",
"fecha": "2021-10-24 13:55:00"
}
thank you if you can help me to show the name of the category thank you
You can implement a function like this:
const findCategory = (id)=>{this.categorias.find(category=>category.id_categoria===id)?.categoria}
And in the template:
<td>{{findCategory(pro.id_categoria)}}</td>
Only one small change. In your code you have to edit. pro.id_categoria to pro.categoria. see comment inline.
<tr v-for="pro in productos" :key="pro.id_productos">
<th>{{pro.id_productos}}</th>
<td>
<img :src="pro.imagen" class="img-circle elevation-2" alt="Product Image" width="60" />
</td>
<td>{{pro.codigo}}</td>
<td>{{pro.producto}}</td>
// this line
<td>{{pro.id_categoria}}</td>
// edit to
<td>{{ pro.categoria }} </td>

VueJS with Bootstrap icons for sort

I'm sorting column data by click on the table header.
I have added the Bootstrap icons based on the v-show condition(s).
Earlier, the sorting worked on the click of the table header but now it should work on the click of icon which are not visible now to the right of 'Present' column.
Need help on this. The origin of issue is on Line 13 in HTML:
<span class="glyphicon glyphicon-sort" v-show="toolAttribute != activeColumn"></span>
https://jsfiddle.net/L5p0ngdu/2/
new Vue({
el: '#app',
data: {
results: {
toolAttribute: [{attributeName: "Present", order: 1},{attributeName: "Present", order: 1},{attributeName: "Present", order: 1}],
device: [
{deviceName: "Device Name 1",
info: [{value: true}, {value: false}, {value: true}]},
{deviceName: "Device Name 2",
info: [{value: false}, {value: false}, {value: false}]},
{deviceName: "Device Name 3",
info: [{value: true}, {value: true}, {value: true}]},
{deviceName: "Device Name 4",
info: [{value: false}, {value: true}, {value: false}]},
{deviceName: "Device Name 5",
info: [{value: true}, {value: true}, {value: true}]}
]
},
activeColumn: {},
currentSort:['deviceName'],
currentSortDir:'asc'
},
computed:{
sortedResults:function() {
return this.results.device.sort(function(a,b){
let modifier = 1;
if(this.currentSortDir === 'desc') modifier = -1;
this.currentSort.forEach(x => {
a = a[x];
b = b[x];
})
if(a< b) return -1 * modifier;
if(a > b) return 1 * modifier;
return 0;
}.bind(this));
}
},
methods:{
flasecond(index){
let res = false
this.results.device[index].info.forEach(info=> {
if(!info.value) res = true
})
return res
},
sort:function(s) {
//if s == current sort, reverse
if(s.join('') === this.currentSort.join('')) {
this.currentSortDir = this.currentSortDir==='asc'?'desc':'asc';
}
this.currentSort = s;
},
}
})
.falseclass{
background:red;
color:white;
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<div id="app">
<table >
<tr>
<th rowspan="2" #click="sort(['deviceName'])">Device Info</th>
</tr>
<tr>
<th v-for="(toolAttribute, index) in results.toolAttribute" :key="index" #click="activeColumn = toolAttribute" :class="{active: toolAttribute == activeColumn}">{{toolAttribute.attributeName}}
<span #click="sort(['info', index, 'value']); toolAttribute.order = toolAttribute.order * (-1)" :class="toolAttribute.order > 0 ? 'glyphicon glyphicon-chevron-down' : 'glyphicon glyphicon-chevron-up'" v-show="toolAttribute == activeColumn"></span>
<span class="glyphicon glyphicon-sort" v-show="toolAttribute != activeColumn"></span></th>
</tr>
<tr v-for="(device, index) in sortedResults" >
<td :class="{'falseclass' : flasecond(index)}">{{ device.deviceName }}</td>
<td v-for="info in device.info" :class="{'falseclass' : !info.value}">{{info.value}}</td>
</tr>
</table>
</div>
In your jsfiddle I see that you are using Bootstrap 4. When Bootstrap migrated to v4 the Glyphicons icon font has been dropped. I'd suggest you to take a look at one of these free alternatives:
FontAwesome
Octicons
You could try this
i have made some changes to jsfiddle https://jsfiddle.net/thanseeh/tqy93meL/13/
<head>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<div id="app">
<table >
<tr>
<th rowspan="2" #click="sort(['deviceName'])">Device Info</th>
</tr>
<tr>
<th v-for="(toolAttribute, index) in results.toolAttribute" :key="index" #click="activeColumn = toolAttribute" :class="{active: toolAttribute == activeColumn}">{{toolAttribute.attributeName}}
<span #click="sort(['info', index, 'value']); toolAttribute.order = toolAttribute.order * (-1)" :class="toolAttribute.order > 0 ? 'glyphicon glyphicon-chevron-down' : 'glyphicon glyphicon-chevron-up'" v-show="toolAttribute == activeColumn"></span>
<span class="glyphicon glyphicon-sort" v-show="toolAttribute != activeColumn"></span></th>
</tr>
<tr v-for="(device, index) in sortedResults" >
<td :class="{'falseclass' : flasecond(index)}">{{ device.deviceName }}</td>
<td v-for="info in device.info" :class="{'falseclass' : !info.value}">{{info.value}}</td>
</tr>
</table>
</div>
</body>

How to pass a component to render in props in Vue Js?

I have a situation where i need to render data cell synamically
where tableProps contain all columns and dataProps.
tableProps: {
cols: [{
cellProps: {
class: "as"
},
cellRenderer: ((data) => {
return <a onlick = {
this.onDataClick
}
class = "btn btn-link" > {
data.name
} < /a>
}).bind(this),
dataKey: "name",
dataType: String,
label: "Name",
sortable: true
}
],
enableSelect: true,
onPageChange: this.onPageChange,
onSelect: (selectedRow) => console.log("selectedRow", selectedRow),
onSelectAll: (data) => console.log("slectAllClick", data),
page: 0,
rowProps: {
onClick: (event, rowData) => {
this.onClick(rowData);
}
},
rowsPerPage: 5,
title: "Nutrition"
}
There is a cell renderer where data can be passed to render custom data like buttons anchor etc..
the solution has been found, instead of sending a function, scoped slots can be used to render dynamic contents for each cell. Thank you for showing interest.
**Table.Vue(child, generic-table)**
<table class="table table-bordered">
<thead>
<tr>
<th v-for="col in options.cols" :key="col.id">
<template v-if="col.colRenderer">
{{col.colRenderer(col)}}
</template>
<template v-else>
{{col.label}}
</template>
</th>
</tr>
</thead>
<tbody>
<tr v-for="datum in data" :key="datum.id" #click="(e)=> options.rowProps.onClick ? options.rowProps.onClick(e, datum): ''">
<td v-for="col in options.cols" :key="col.id" #click="()=> col.onClick ? col.onClick(datum[col.dataKey]): ''">
<template v-if="col.cellSlot">
<slot :name="col.cellSlot.name" :data="datum[col.dataKey]"/>
</template>
<template v-else>
{{datum[col.dataKey]}}
</template>
</td>
</tr>
</tbody>
</table>
**Calling component(Parent, with Custom Data cells)**
<v-table
:name="carePlanName"
:options="tableProps"
:total-count="totalCount"
:data="users" >
<div
slot=""
slot-scope="slotProps">
<!-- Define a custom template for CellData Data -->
<!-- `slotProps` to customize each todo. -->
<span v-if="slotProps">✓
<button>{{ slotProps.name }}</button>
</span>
</div>
</v-table>

Reusing a form with Vue.js

I have a page with a list of things. I want the user to be able to click on an item in the list and open a modal dialog with a relatively complicated form with data for each item. I've implemented the form using Vue.js, and so far, it works, but I can't figure out how to get the Vue to switch from the data for one item to the data for another item, or do something equivalent to that.
The examples make it look like components are for small things and not entire forms. Here's the JavaScript from a JS fiddle with a hastily-coded example that's kind of similar to what I want to do.
HTML:
<table>
<thead>
<tr>
<th scope="col">Item</th>
<th scope="col">Data</th>
</tr>
</thead>
<tbody>
<tr data-identifier="1">
<td><a>Item 1</a></td>
<td class="json">{ "id": "1", "formData": { "whichOption": "1", "optionOneSetting": "ONE AYE", "optionTwoSetting": null, "optionThreeSetting": null } }</td>
</tr>
<tr data-identifier="2">
<td><a>Item 2</a></td>
<td class="json">{ "id": "2", "formData": { "whichOption": "2", "optionOneSetting": null, optionTwoSetting": "two-b", "optionThreeSetting": null } }</td>
</tr>
<tr data-identifier="3">
<td><a>Item 3</a></td>
<td class="json">{ "id": "3", "formData": { "whichOption": "2", "optionOneSetting": null, "optionTwoSetting": "two-c", "optionThreeSetting": null } }</td>
</tr>
<tr data-identifier="4">
<td><a>Item 4</a></td>
<td class="json">{ "id": "4", "formData": { "whichOption": "3", "optionOneSetting": null, "optionTwoSetting": null, "optionThreeSetting": "true" } }</td>
</tr>
<tr data-identifier="5">
<td><a>Item 5</a></td>
<td class="json">{ "id": "5", "formData": { "whichOption": "1", "optionOneSetting": "ONE AYE AGAIN", "optionThreeSetting": null } }</td>
</tr>
</tbody>
</table>
<div id="app" class="hidden">
<select v-model="formData.whichOption">
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</select>
<label v-if="formData.whichOption==1">1-A: <input type="text" v-model="formData.optionOneSetting" /></label>
<div v-if="formData.whichOption==2">
<label><input type="radio" name="optionTwoSetting" value="two-a" v-model="formData.optionTwoSetting" /> 2-A</label>
<label><input type="radio" name="optionTwoSetting" value="two-b" v-model="formData.optionTwoSetting" /> 2-B</label>
<label><input type="radio" name="optionTwoSetting" value="two-c" v-model="formData.optionTwoSetting" /> 2-C</label>
</div>
<div v-if="formData.whichOption==3">
<label><input type="checkbox" name="optionThreeSetting" value="three" v-model="formData.optionThreeSetting" /> 3-A</label>
</div>
<button v-on:click="doMagic">Do a Thing</button>
</div>
JavaScript:
$(document).ready(function() {
$("a").click(function() {
var data = $(this).closest("tr").find("td.json").text();
data = JSON.parse(data);
if (!document.vue) {
document.vue = new Vue({
el: "#app"
, data: data
, methods: {
doMagic: function(event) {
var $tr = $("tr[data-identifier=" + this.id + "]");
console.log($.extend({}, this));
$tr.find(".json").text(JSON.stringify({
"id": this.id
, "formData": this.formData
}));
}
}
})
$("#app").removeClass("hidden");
}
else {
// This isn't expected to work, but it's effectively what I'd like to happen.
document.vue.data = data;
}
})
})
https://jsfiddle.net/don01001100/eywraw8t/370396/

Vuejs2- Avoid repetition of select field options using vee-validate

Iam using vee-validate plugin for validation. In my form, there is a select field in the table. Rows will be added dynamically in the table. I don't want to select the same select(Description column) option again and again Image. Hence I want to throw an error like "Selected description already exists in a table" this using vee-validate. Kindly help me to solve this.
Here is my code:
<template>
<div>
<b-card>
<div class="panel-body" id="app">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 20px;">No.</th>
<th style="width: 330px;">Description</th>
<th style="width: 130px;" class="text-right">Charges</th>
<th style="width: 130px;">Total</th>
<th style="width: 130px;"></th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in rows" :key="row.qty">
<td>
{{ index +1 }}
</td>
<td>
<select class="form-control" v-model="row.billChgDesc" v-validate="'required|check'" :name="'billChgDesc' + index" data-vv-as="Description" #change="checkRepetation">
<option v-for="option in billChgDescOpt" v-bind:value="option.value"
:key="option.value"> {{ option.text }}
</option>
</select>
<span v-show=" errors.has('billChgDesc' + index)" class="is-danger">{{ errors.first('billChgDesc' + index) }}</span>
</td>
<td>
<input class="form-control text-right" type="text" v-model="row.charges" data-type="currency" v-validate="'required'" :name="'charges' + index" data-vv-as="Charges" >
<span v-show=" errors.has('charges' + index)" class="is-danger">{{ errors.first('charges' + index) }}</span>
<td>
<input class="form-control text-right" :value="row.qty * row.charges" number readonly />
<input type="hidden" :value="row.qty * row.charges * row.tax / 100" number/>
</td>
<td>
<button class="btn btn-primary btn-sm" #click="addRow(index)"><i class="fa fa-plus"></i></button>
<button class="btn btn-danger btn-sm" #click="removeRow(index)"><i class="fa fa-minus"></i></button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-right">TOTAL</td>
<td colspan="1" class="text-right"><input class="form-control text-right" v-model="delivery" number/></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</b-card>
</div>
</template>
<script>
import Vue from 'vue'
import accounting from 'accounting'
export default {
data: function () {
return {
billChgDescOpt: [
{ value: '', text: 'Select' },
{ value: 'M', text: 'Maintenance Fee'},
{ value: 'W', text: 'Water Charges'},
{ value: 'P', text: 'Penalty Fee'},
],
rows: [
{qty: 5, billChgDesc: '', charges: 55.20, tax: 10},
{qty: 19, billChgDesc: '', charges: 1255.20, tax: 20},
],
grandtotal: 0,
delivery: 40,
selectArr:[]
}
},
methods: {
addRow: function (index) {
try {
this.rows.splice(index + 1, 0, {});
} catch(e)
{
console.log(e);
}
},
removeRow: function (index) {
this.rows.splice(index, 1);
},
checkRepetation:function(){
this.$validator.extend('check', {
getMessage: field => '* Slected ' + field + ' already exists',
validate: function(value){
selectArr.push(value);
}
})
}
}
}
</script>
<style lang="scss" scoped>
.is-danger{
color: RED;
}
</style>
Thanks in advance.
You're on the right track, but a couple changes need to be made. When you call this.$validator.extend, that only needs to be done once - when your component is created. It attaches the check method to the validator, so then every time you have the attribute v-validate="'required|check'" in your HTML, it will run that check method.
In your check validator, you need to answer the question "is this value already selected". The answer is to go through the this.rows and see if any of them have the same billChgDesc property. Because this is in Vue, by the time the validator gets run, the row in question already does have that value, so you want to check if MORE than one row have that value. So, something like this:
mounted() {
var self = this;
this.$validator.extend('check', {
getMessage: field => '* Selected ' + field + ' already exists',
validate: function(value){
return (self.rows.filter(function(v){
return v.billChgDesc == value;
}).length <= 1);
}
});
}
This validator returns true if only one item has the given value. I'm using the built-in filter method of Array (see docs).
You can see an example of this all working here: https://jsfiddle.net/ryleyb/f9q50wx4/1/