Bootstrap-vue b-table with filter in header - vue.js

I have a table generated with bootstrap-vue that shows the results of a system search.
The Results Table shows the records to the user, and the user can sort them and filter them.
How can I add the search field underneath the table header <th> generated with the bootstrap-vue <b-table> element?
Screenshot of the current table:
Mockup of the wanted table:

You can use the top-row slot to customise your own first-row. See below for a bare-bones example.
new Vue({
el: '#app',
data: {
filters: {
id: '',
issuedBy: '',
issuedTo: ''
},
items: [{id:1234,issuedBy:'Operator',issuedTo:'abcd-efgh'},{id:5678,issuedBy:'User',issuedTo:'ijkl-mnop'}]
},
computed: {
filtered () {
const filtered = this.items.filter(item => {
return Object.keys(this.filters).every(key =>
String(item[key]).includes(this.filters[key]))
})
return filtered.length > 0 ? filtered : [{
id: '',
issuedBy: '',
issuedTo: ''
}]
}
}
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css"/><link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.css"/><script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.min.js"></script><script src="//unpkg.com/babel-polyfill#latest/dist/polyfill.min.js"></script><script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.js"></script>
<div id="app">
<b-table striped show-empty :items="filtered">
<template slot="top-row" slot-scope="{ fields }">
<td v-for="field in fields" :key="field.key">
<input v-model="filters[field.key]" :placeholder="field.label">
</td>
</template>
</b-table>
</div>
Note: I've used a computed property to filter the items instead of the :filter prop in b-table because it doesn't render rows if all the items are filtered out, including your custom first-row. This way I can provide a dummy data row if the result is empty.

Have upvoted phil's answer, just making it more generic
filtered() {
const filtered = this.items.filter(item => {
return Object.keys(this.filters).every(key =>
String(item[key]).includes(this.filters[key])
);
});
return filtered.length > 0
? filtered
: [
Object.keys(this.items[0]).reduce(function(obj, value) {
obj[value] = '';
return obj;
}, {})
];
}

Thanks to you for these useful answers. It saved some of my time today.
However, in case items are given asynchronously i had to add a test on items size like this
filtered() {
if (this.items.length > 0) {
const filtered = this.items.filter(item => {
return Object.keys(this.filters).every(key => String(item[key]).includes(this.filters[key])
);
});
return filtered.length > 0
? filtered
: [
Object.keys(this.items[0]).reduce(function (obj, value) {
obj[value] = '';
return obj;
}, {})
];
}
},
On another hand if needed to have column with no filter, i added this test below
In the template
<td v-for="field in fields" :key="field.key">
<input v-if="fieldIsFiltered(field)" v-model="filters[field.key]" :placeholder="field.label">
</td>
and within component methods
fieldIsFiltered(field) {
return Object.keys(this.filters).includes(field.key)
}

mistake
const filtered = this.items.filter(item => {
return Object.keys(this.filters).every(key =>
// String(item[key]).includes(this.filters[key]))
return String(item[key]).includes(this.filters[key]))
})

Related

How to solve v-switch vuetify only binds one way?

My v-switch from vuetify is only binding one way.
If i load in my data it switches on or off. so its working if i load data in the v-model of the v-switch.
But if i switch the v-switch, it switches off, but does not change anything.
here is the code:
<v-data-table :headers="datatable.headers" :items="datatable.items" class="elevation-1">
<template v-slot:body="{ items }">
<tr v-for="(item, index) in items" :key="index">
<td>{{item.name}}</td>
<td #click="() => { $router.push(`/settings/${item.name.toLowerCase()}`) }"><v-icon small>edit</v-icon></td>
<td><v-switch v-model="inMenu[item.name.toLowerCase()]" :label="`Switch 1: ${inMenu[item.name.toLowerCase()]}`"></v-switch></td>
</tr>
</template>
</v-data-table>
<script>
export default {
data() {
return {
tabs: [
'Content types'
],
tab: null,
datatable: {
items: [],
headers: [{
text: 'Content types', value: "name"
}]
},
settings: null,
inMenu: {},
}
},
mounted() {
this.$axios.get('/settings.json').then(({data}) => {
this.settings = data
});
this.$axios.get('/tables.json').then(({data}) => {
// set all content_types
data.map(item => {
this.datatable.items.push({
name: item
})
})
// check foreach table if it's in the menu
this.datatable.items.forEach(item => {
this.inMenu[item.name.toLowerCase()] = JSON.parse(this.settings.menu.filter(menuitem => menuitem.key == item.name.toLowerCase())[0].value)
})
})
},
updated() {
console.log(this.inMenu)
}
}
</script>
so i clicked on the first switch and it does not change the state
i tried to have a normal prop in the data function.
i made a switch: null prop and it will react fine to that, but not to my code.
Any idea?
My guess is that your data is not reactive when you write:
// check foreach table if it's in the menu
this.datatable.items.forEach(item => {
this.inMenu[item.name.toLowerCase()] = JSON.parse(this.settings.menu.filter(menuitem => menuitem.key == item.name.toLowerCase())[0].value)
})
You should use the $set method instead and write:
// check foreach table if it's in the menu
this.datatable.items.forEach(item => {
this.$set(this.inMenu, item.name.toLowerCase(), JSON.parse(this.settings.menu.filter(menuitem => menuitem.key == item.name.toLowerCase())[0].value)
}))
See https://v2.vuejs.org/v2/guide/reactivity.html for more information on reactivity
Does this solve your problem?

VueJS dynamic data + v-model

In my data object, I need to push objects into an array called editions.
data() {
return {
editions: []
}
}
To do this, I am dynamically creating a form based on some predetermined field names. Here's where the problem comes in. I can't get v-model to cooperate. I was expecting to do something like this:
<div v-for="n in parseInt(total_number_of_editions)">
<div v-for="field in edition_fields">
<input :type="field.type" v-model="editions[n][field.name]" />
</div>
</div>
But that isn't working. I get a TypeError: _vm.editions[n] is undefined. The strange thing is that if I try this: v-model="editions[n]"... it works, but I don't have the property name. So I don't understand how editions[n] could be undefined. This is what I'm trying to end up with in the data object:
editions: [
{
name: "sample name",
status: "good"
},
...
]
Can anyone advise on how to achieve this?
But that isn't working. I get a TypeError: _vm.editions[n] is undefined.
editions is initially an empty array, so editions[n] is undefined for all n. Vue is essentially doing this:
const editions = []
const n = 1
console.log(editions[n]) // => undefined
The strange thing is that if I try this: v-model="editions[n]"... it works
When you use editions[n] in v-model, you're essentially creating the array item at index n with a new value. Vue is doing something similar to this:
const editions = []
const n = 2
editions[n] = 'foo'
console.log(editions) // => [ undefined, undefined, "foo" ]
To fix the root problem, initialize editions with an object array, whose length is equal to total_number_of_editions:
const newObjArray = n => Array(n) // create empty array of `n` items
.fill({}) // fill the empty holes
.map(x => ({...x})) // map the holes into new objects
this.editions = newObjArray(this.total_number_of_editions)
If total_number_of_editions could change dynamically, use a watcher on the variable, and update editions according to the new count.
const newObjArray = n => Array(n).fill({}).map(x => ({...x}))
new Vue({
el: '#app',
data() {
const edition_fields = [
{ type: 'number', name: 'status' },
{ type: 'text', name: 'name' },
];
return {
total_number_of_editions: 5,
editions: [],
edition_fields
}
},
watch: {
total_number_of_editions: {
handler(total_number_of_editions) {
const count = parseInt(total_number_of_editions)
if (count === this.editions.length) {
// ignore
} else if (count < this.editions.length) {
this.editions.splice(count)
} else {
const newCount = count - this.editions.length
this.editions.push(...newObjArray(newCount))
}
},
immediate: true,
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.min.js"></script>
<div id="app">
<label>Number of editions
<input type="number" min=0 v-model="total_number_of_editions">
</label>
<div><pre>total_number_of_editions={{total_number_of_editions}}
editions={{editions}}</pre></div>
<fieldset v-for="n in parseInt(total_number_of_editions)" :key="n">
<div v-for="field in edition_fields" :key="field.name+n">
<label>{{field.name}}{{n-1}}
<input :type="field.type" v-if="editions[n-1]" v-model="editions[n-1][field.name]" />
</label>
</div>
</fieldset>
</div>

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);
});
}

Filtering a list of objects in Vue without altering the original data

I am diving into Vue for the first time and trying to make a simple filter component that takes a data object from an API and filters it.
The code below works but i cant find a way to "reset" the filter without doing another API call, making me think im approaching this wrong.
Is a Show/hide in the DOM better than altering the data object?
HTML
<button v-on:click="filterCats('Print')">Print</button>
<div class="list-item" v-for="asset in filteredData">
<a>{{ asset.title.rendered }}</a>
</div>
Javascript
export default {
data() {
return {
assets: {}
}
},
methods: {
filterCats: function (cat) {
var items = this.assets
var result = {}
Object.keys(items).forEach(key => {
const item = items[key]
if (item.cat_names.some(cat_names => cat_names === cat)) {
result[key] = item
}
})
this.assets = result
}
},
computed: {
filteredData: function () {
return this.assets
}
},
}
Is a Show/hide in the DOM better than altering the data object?
Not at all. Altering the data is the "Vue way".
You don't need to modify assets to filter it.
The recommended way of doing that is using a computed property: you would create a filteredData computed property that depends on the cat data property. Whenever you change the value of cat, the filteredData will be recalculated automatically (filtering this.assets using the current content of cat).
Something like below:
new Vue({
el: '#app',
data() {
return {
cat: null,
assets: {
one: {cat_names: ['Print'], title: {rendered: 'one'}},
two: {cat_names: ['Two'], title: {rendered: 'two'}},
three: {cat_names: ['Three'], title: {rendered: 'three'}}
}
}
},
computed: {
filteredData: function () {
if (this.cat == null) { return this.assets; } // no filtering
var items = this.assets;
var result = {}
Object.keys(items).forEach(key => {
const item = items[key]
if (item.cat_names.some(cat_names => cat_names === this.cat)) {
result[key] = item
}
})
return result;
}
},
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<button v-on:click="cat = 'Print'">Print</button>
<div class="list-item" v-for="asset in filteredData">
<a>{{ asset.title.rendered }}</a>
</div>
</div>

how to do filters in vue.js

i am new to vue.js.so i want to apply a filter thing in my project.i tried to do this from past 2 to 3 days..but i couldn't..can any one help me how to do this..
I have 3 input boxes one is experience,expected_ctc,and profile_role depending on these 3 i want to display the results..
Here is my js page:
const app = new Vue({
el: '#app',
data() {
return {
page: 0,
itemsPerPage: 3,
}
},
components: { Candidate },
methods: {
//custom method bound to page buttons
setPage(page) {
this.page = page-1;
this.paginedCandidates = this.paginate()
},
paginate() {
return this.filteredCandidates.slice(this.page * this.itemsPerPage, this.page * this.itemsPerPage + this.itemsPerPage)
},
},
computed: {
//compute number of pages, we always round up (ceil)
numPages() {
console.log(this.filteredCandidates)
return Math.ceil(this.filteredCandidates.length/this.itemsPerPage);
},
filteredCandidates() {
//filter out all candidates that have experience less than 10
const filtered = window.data.candidates.filter((candidate) => {
if(candidate.experience === 5) {
return false;
}
return true;
})
console.log(filtered);
return filtered;
},
paginedCandidates() {
return this.paginate()
}
}
});
here is my buttons from view page:
<div class="container">
<b>Experience:</b><input type="text" v-model="experience" placeholder="enter experience">
<b>Expected CTC:</b><input type="text" v-model="expected_ctc" placeholder="enter expected ctc">
<b>Profile Role:</b><input type="text" v-model="profile_role_id" placeholder="enter role">
<input type="button" name="search" value="search" >
</div>
Can anyone help me..
Thanks in advance..
Ok let's start with a smaller example. Lets say you have "Candidates" one one candidate might look like
{
name: 'John Doe',
expected_ctc: 'A',
experience: 'B',
profile_role_id: 1
}
From your current state I'd say you have all candidates in an array returned by laravel. Let's say we're in your current template where you have your vue app
<div id="app">
<!-- lets start with one filter (to keept it clean) -->
<input type="text" v-model="experienceSearch"/>
<ul class="result-list">
<li v-for="result in candidatelist">{{ result.name }}</li>
</ul>
</div>
And now to the script
// always init your v-model bound props in data
// usually you wouldn't take the candidates from the window prop. However, to keep it easy for you here I will stick to this
data: function() {
return {
experienceSearch: '',
candidates: window.data.candidates
}
},
// the list that is displayed can be defined as computed property. It will re-compute everytime your input changes
computed: {
candidatelist: function() {
// now define how your list is filtered
return this.candidates.filter(function(oCandidate) {
var matches = true;
if (this.experienceSearch) {
if (oCandidate.experience != this.experienceSearch) {
matches = false;
}
}
// here you can define conditions for your other matchers
return matches;
}.bind(this));
}
}
General steps:
all candidates are in data -> candidates
relevant (filtered) candidates are represented by the computed prop candidatelist
inputs are bound with v-model to EXISTING data prop definitions
Fiddle https://jsfiddle.net/sLLk4u2a/
(Search is only exact and Case Sensitive)