I'm using Vue2.js and Element UI as a framework. I would like to be able to filter a table which is sliced. To do so, I use the table and filter components whose documentation could be found here.
Situation OK
The table is not sliced. When you picked a filter, a loop goes trough each row and check if the value of the column is equal to the filter.
Situation NOT OK
The table is sliced. When you picked a filter, a loop goes trough each row that results of the slice and check if the value of the column is equal to the filter. By doing that we don't filter the "hidden" values.
I've made a little https://jsfiddle.net/acm3q6q8/3/ so it is easier to understand.
All of this make sense since I'm not working on the whole data, but on a sliced version.
One solution could be to hide rows instead of excluding them by slicing the data, but I'm wondering if there is a better solution ?
What I want to achieve
In the jsfiddle, display only 2 items.
Filter the tag to display only rows whose tag is Office
Actual result
There is no row displayed since the row whose tag was office was not part of the sliced table.
Expected result
When filtering, I would like to take into account rows that are not necessarily displayed.
Important
This should work fine with a multiple filter (ie I select several tags)
EDIT
In the same extent if you want to sort the name by alphabetical order, Albert won't be displayed if you displayed only 2 items.
You can handle the filter-change event on the table component (documented here), and filter/slice yourself.
var Main = {
data() {
return {
numberItemToShow : 4,
tableData: [...],
filter: []
}
},
computed : {
filterData() {
if (!this.filter.length)
return this.tableData.slice(0, this.numberItemToShow)
else
return this.tableData
.filter(row => this.filter.includes(row.tag))
.slice(0, this.numberItemToShow);
}
},
methods: {
onFilterChange(filters){
if (filters.tag)
this.filter = filters.tag;
}
}
}
And the template
<template>
<input v-model="numberItemToShow" placeholder="edit me">
<p>Number of item to display: {{ numberItemToShow }}</p>
<el-table ref="tab" :data="filterData" border style="width: 100%" #filter-change="onFilterChange">
<el-table-column prop="name" label="Name" sortable>
</el-table-column>
<el-table-column prop="tag" label="Tag" column-key="tag" :filters="[{ text: 'Home', value: 'Home' }, { text: 'Office', value: 'Office' }]">
<template scope="scope">
<el-tag :type="scope.row.tag === 'Home' ? 'primary' : 'success'" close-transition>{{scope.row.tag}}</el-tag>
</template>
</el-table-column>
</el-table>
</template>
Example.
The problem is that the slicing is done before the filtering. The filter has to see the original data, and the row-counting must be part of the filtering.
Since the filter looks at one row at a time, keeping track of the matched rows is a little tricky. What I did here is keep a counter of matched rows that resets to zero when the row being looked at is the first row of data. This is hacky, but it works. There may be a better way; I am not familiar with the table widget.
var Main = {
data() {
return {
numberItemToShow : 4,
tableData: [{
name: 'One',
tag: 'Home'
}, {
name: 'Two',
tag: 'Home'
}, {
name: 'Three',
tag: 'Home'
}, {
name: 'Four',
tag: 'Office'
}],
scratchCounter: 0
}
},
methods: {
filterTag(value, row) {
const matched = row.tag === value;
if (row === this.tableData[0]) {
this.scratchCounter = 0;
}
if (matched) {
++this.scratchCounter;
}
return this.scratchCounter <= this.numberItemToShow && matched;
}
}
}
var Ctor = Vue.extend(Main)
new Ctor().$mount('#app')
#import url("//unpkg.com/element-ui/lib/theme-default/index.css");
<script src="//unpkg.com/vue/dist/vue.js"></script>
<script src="//unpkg.com/element-ui/lib/index.js"></script>
<div id="app">
<template>
<input v-model="numberItemToShow" placeholder="edit me">
<p>Number of item to display: {{ numberItemToShow }}</p>
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="name" label="Name">
</el-table-column>
<el-table-column prop="tag" label="Tag" :filters="[{ text: 'Home', value: 'Home' }, { text: 'Office', value: 'Office' }]" :filter-method="filterTag">
<template scope="scope">
<el-tag :type="scope.row.tag === 'Home' ? 'primary' : 'success'" close-transition>{{scope.row.tag}}</el-tag>
</template>
</el-table-column>
</el-table>
</template>
</div>
Related
So I am trying to filter my table (element plus table) based on the tags that user clicks. (I am working in a Vue project)
I have written a method that outputs all the rows that contains the tag clicked by the user.
<el-table-column prop="Keyword" label="Keyword" width="200" ">
<template #default="scope" >
<el-tag v-for="(k,index) in (scope.row.Keyword.split(','))" :key="index" #click="handlekey(k)">{{k}} </el-tag>
</template>
</el-table-column>
handlekey(val){
return this.data.filter((item)=>{return item.Keyword.split(',').includes(val)} )
}
When user clicks on a tag the console output is
Now I am trying to filter the table, so when a user clicks on a tag the table only shows the rows which contain that tag.
I have tried to write this function is computed but that gives an error sating that $options.handlekey is not a function
I have also tried to change the change the original data by doing
this.data = this.data.filter((item)=>{return item.Keyword.split(',').includes(val)} ) at the end of handlekey method but that doesn't work either.
I'll be grateful if anyone could give me suggestions on how I can achieve this.
You need a computed (derived state) which returns all rows containing one of the currently active keywords (if any), or all rows if there are no active keywords.
Instead of feeding the rows into the table, you feed this computed.
What's special about computed is they get recalculated every time the state involved in computing them changes.
Computed never mutate state. Picture a computed as a lens through which you see the state, but you can't actually touch it. If you mutate the source data inside a computed, you will trigger another computing of itself, ending up in an endless loop.
In the example below, the computed renderedRows will recalculate when:
rows change
the (selected) keywords change
Here's the example:
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
data: () => ({
rows: [
{
name: '1-5',
keywords: 'one,two,three,four,five'
},
{
name: '6-10',
keywords: 'six,seven,eight,nine,ten'
},
{
name: '2n + 1',
keywords: 'one,three,five,seven,nine'
},
{
name: '2n',
keywords: 'two,four,six,eight,ten'
},
{
name: '3n + 1',
keywords: 'one,four,seven,ten'
},
{
name: '5n + 2',
keywords: 'two,seven'
}
],
keywords: []
}),
computed: {
renderedRows() {
return this.keywords.length
? this.rows.filter((row) =>
this.keywords.some((kw) => row.keywords.split(',').includes(kw))
)
: this.rows
},
allKeywords() {
return [
...new Set(this.rows.map((row) => row.keywords.split(',')).flat())
]
}
},
methods: {
toggleKeyword(k) {
this.keywords = this.keywords.includes(k)
? this.keywords.filter((kw) => kw !== k)
: [...this.keywords, k]
}
}
})
td {
padding: 3px 7px;
}
button {
cursor: pointer;
margin: 0 2px;
}
button.active {
background-color: #666;
color: white;
border-radius: 3px;
}
button.active:hover {
background-color: #444;
}
<script src="https://unpkg.com/vue#2"></script>
<div id="app">
<h4>Keywords:</h4>
<div>
<button
v-for="key in allKeywords"
:key="key"
v-text="key"
:class="{ active: keywords.includes(key) }"
#click="toggleKeyword(key)"
/>
</div>
<h4>Rows:</h4>
<table>
<thead>
<th>Name</th>
<th>Keywords</th>
</thead>
<tbody>
<tr v-for="row in renderedRows" :key="row.name">
<td v-text="row.name" />
<td>
<button
v-for="key in row.keywords.split(',')"
:key="key"
v-text="key"
:class="{ active: keywords.includes(key) }"
#click="toggleKeyword(key)"
/>
</td>
</tr>
</tbody>
</table>
</div>
Note: provide what you have in a runnable minimal reproducible example on codesandbox.io or similar and I'll modify it to include the principle outlined above.
I know that "v-if" must avoid with "v-for" but not sure about "v-show" because it is just to toggle the display attribute.
This is the code in case anyone wants to know. Basically, I try to switch 3 different types of filter list. The code runs fine but I just wanna know if it should be avoid like "v-if".
<template>
<button
v-for="(filter, index) in filterList" :key="index"
#click="chosenFilter = filter.name"
>
{{ filter.name }}
</button>
<div
v-for="(filter, index) in filterList" :key="index"
v-show="chosenFilter === filter.name"
>
<div v-for="(listItem, index) in filter.list" :key="index">
{{ listItem }}
</div>
</div>
</template>
<script>
data () {
return {
filterList: [
{ name: 'Type 1', list: [] },
{ name: 'Type 2', list: [] },
{ name: 'Type 3', list: [] }
],
chosenFilter: 'Type 1'
}
}
</script>
From the official style guide: https://v2.vuejs.org/v2/style-guide/#Avoid-v-if-with-v-for-essential
There are 2 points in which it's not a good practice to use that, the 2nd one is interesting: To avoid rendering a list if it should be hidden. This one is basically fine since you're not doing heavy JS rendering, just basic CSS toggling.
So yeah, I'd say it's correct to have a v-show (and ESlint is not complaining btw).
But IMO, you can solve this kind of behavior in pretty much all cases with a computed: your filter button could be selected with an ID and your list rendering could be filtered with a filter here.
Replace #click="chosenFilter = filter.name" with #click="chooseFilter and get the ID (thanks to $event) of the item you've clicked on, then filter your list with the selected filter.
I am using the <b-table> component from BootstrapVue and trying to customize field output by using a formatter callback function. The table data displays fine , but for some reason the callback function method branchName() is not being called and the value is not being formatted to the branch name instead of the branch id.
I set up a codesandbox to demonstrate the problem: code demo
The goal of the method is to return the name of the branch item. However, only the branch_id is being returned. In other words, the record row under the Branch table column should say ACME10 and not 10.
My file named App.vue:
<template>
<div id="app">
<b-table striped hover :items="userProfiles"></b-table>
</div>
</template>
<script>
export default {
data() {
return {
userProfiles: [
{
uid: "3",
branch: 10
},
{
uid: "1",
branch: 20
},
{
uid: "2",
branch: 13
}
],
branches: [
{
branch_id: 13,
branch: "ACME13"
},
{
branch_id: 10,
branch: "ACME10"
},
{
branch_id: 20,
branch: "ACME20"
}
],
fields: [
{
key: "branch",
formatter: "branchName"
}
]
};
},
methods: {
branchName(value) {
const name = this.branches[0].find(branch => value === branch.branch_id);
return name;
}
}
};
</script>
Thanks for your help!
To answer the OPs original question (why the formatter function was not being called):
In your App.vue file you are missing binding your fields definition array to the fields prop of <b-table>. Try this:
<template>
<div id="app">
<b-table striped hover :items="userProfiles" :fields="fields"></b-table>
</div>
</template>
If <b-table> isn't passed a field definition array, it will peek at the first row's item data and grab the field names that it finds, and then generates its own internal fields definition (just the field keys and humanized labels)
You should use slots to implement custom behavior like. For more information https://bootstrap-vue.js.org/docs/components/table#scoped-field-slots
Here is how you could implement your code
<template>
<div id="app">
<b-table striped hover :items="userProfiles">
<template slot="[uid]" slot-scope="data">{{ data.item.uid }}</template>
<template slot="[branch]" slot-scope="data">{{ branchName(data.item.branch) }}</template>
</b-table>
</div>
</template>
Method:
branchName(value) {
const branch = this.branches.find(branch => value === branch.branch_id);
if (branch) {
return branch.branch
}
return ""
}
try this in your template
<template v-slot:cell(name)="data">
<router-link :to="/route/${data.index}">
{{ data.value }}
</router-link>
I have a Vue.js text-input component like the following:
<template>
<input
type="text"
:id="name"
:name="name"
v-model="inputValue"
>
</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: this.value
};
},
watch: {
inputValue: function () {
eventBus.$emit('inputChanged', {
type: 'text',
name: this.name,
value: this.inputValue
});
}
}
};
</script>
And I am using that text-input in another component as follows:
<ul>
<li v-for="row in rows" :key="row.id">
<text-input :name="row.name" :value="row.value">
</text-input>
</li>
</ul>
Then, within the JS of the component using text-input, I have code like the following for removing li rows:
this.rows = this.rows.filter((row, i) => i !== idx);
The filter method is properly removing the row that has an index of idx from the rows array, and in the parent component, I can confirm that the row is indeed gone, however, if I have, for example, two rows, the first with a value of 1 and the second with a value of 2, and then I delete the first row, even though the remaining row has a value of 2, I am still seeing 1 in the text input.
Why? I don't understand why Vue.js is not updating the value of the text input, even though the value of value is clearly changing from 1 to 2, and I can confirm that in the parent component.
Maybe I'm just not understanding how Vue.js and v-model work, but it seems like the value of the text input should update. Any advice/explanation would be greatly appreciated. Thank you.
You cannot mutate values between components like that.
Here is a sample snippet on how to properly pass values back and forth. You will need to use computed setter/getter. Added a button to change the value and reflect it back to the instance. It works for both directions.
<template>
<div>
<input type="text" :id="name" v-model="inputValue" />
<button #click="inputValue='value2'">click</button>
</div>
</template>
<script>
export default {
props: ['name', 'value'],
computed: {
inputValue: {
get() {
return this.value;
},
set(val) {
this.$emit('updated', val);
}
}
}
}
</script>
Notice that the "#updated" event updates back the local variable with the updated value:
<text-input :name="row.name" :value="row.value" #updated="item=>row.value=item"></text-input>
From your code you are trying to listen to changes.. in v-model data..
// Your Vue components
<template>
<input
type="text"
:id="name"
:name="name"
v-model="inputValue"
>
</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: ""
};
},
};
</script>
If You really want to listen for changes..
<ul>
<li v-for="row in rows" :key="row.id">
<text-input #keyup="_keyUp" :name="row.name" :value="row.value">
</text-input>
</li>
</ul>
in your component file
<template>...</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: ""
};
},
methods : {
_keyUp : () => {// handle events here}
};
</script>
check here for events on input here
To bind value from props..
get the props value, then assign it to 'inputValue' variable
it will reflect in tthe input element
I have this object structure:
lines: [{
order: '1',
text: ' blue'
},{
order: '2',
text: 'green'
},{
order: '3',
text: 'yellow'
}]
And this is rendered on the page like this:
Blue
Green
Yellow
I want reorder the elements (and the object) without drag-drop, but with button up and down. Like this:
Blue - [down]
Green [up, down]
Yellow [up]
Each bullet is a component. How can I achieve that?
Based on assumptions from the little information you provided, I gave it a go.
Read: Vuejs list caveats
From the documentation:
Due to limitations in JavaScript, Vue cannot detect the following changes to an array: When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
So when you modify an array in Vue you should use one of the following:
// Vue.set
Vue.set(example1.items, indexOfItem, newValue)
// Array.prototype.splice
example1.items.splice(indexOfItem, 1, newValue)
Your view can look something like:
<div id="app">
<div v-for="(line, index) in lines">
<p style="display: inline-block">{{line.text}}</p>
<button #click="up(index)" v-if="index !== 0">UP</button>
<button #click="down(index)" v-if="index !== lines.length-1">DOWN</button>
</div>
</div>