Is it possible to make a stacked b-table with multiple items with bootstrap-vue? - html-table

I would like to display multiple items in a bootstrap-vue b-table. Is it possible to do so with a stacked table? The documentation only shows examples of stacked tables with a single item.
How would I specify the fields or items? This does not work. This code creates a stacked table with two columns (column headers on the left and values on the right). Instead of displaying the two items side by side on the left half of the table, the column headers repeat and everything is organized in the same 2 vertical columns.
<template>
...
<b-table
small
stacked
:fields="fooParameters"
:items="foos"
:busy="true"
show-empty
empty-text="No duplicates"
>
</b-table>
...
</template>
<script>
...
duplicates: [{fooName: "Alice", fooAge: "25"}, {fooName: "Bob": fooAge: "24"}]
...
</script>

The stacked prop will generate a row for each item, but since that's not what you're looking for, you will have to create the functionality of having items be horizontal each having they own column.
I believe the below snippet is what you're looking for, and if not you can maybe use it as a starting reference.
new Vue({
el: "#app",
computed: {
fields() {
//'isRowHeader: true' to render it as a TH instead of TD
// Formatter to capitalize the first letter, can be removed
let fields = [{
key: "type",
stickyColumn: true,
isRowHeader: true,
formatter: this.ucFirstLetter
}];
Object.keys(this.results).forEach((key) => {
// Since the key is just an index, we hide it by setting label to an empty string.
fields.push({
key: key,
label: ""
});
});
return fields;
},
items() {
const items = [];
/* map our columuns */
const cols = Object.keys(this.results);
/*
map our types based on the first element
requires all the elements to contain the same properties,
or at least have the first one contain all of them
*/
const types = Object.keys(this.results[cols[0]]);
/* create an item for each type */
types.forEach((type) => {
const item = {
type: type
};
/* fill up item based on our columns */
cols.forEach((col) => {
item[col] = this.results[col][type];
});
items.push(item);
});
return items;
}
},
data() {
return {
results: [{
correct: 0.007,
errors: 0.081,
missing: 0.912
},
{
correct: 0.008,
errors: 0.098,
missing: 0.894
},
{
correct: 0.004,
errors: 0.089,
missing: 0.91
},
{
correct: 0.074,
errors: 0.07,
missing: 0.911
}
]
};
},
methods: {
ucFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
}
});
<link href="https://unpkg.com/bootstrap#4.5.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://unpkg.com/bootstrap-vue#2.16.0/dist/bootstrap-vue.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<script src="https://unpkg.com/bootstrap-vue#2.16.0/dist/bootstrap-vue.js"></script>
<div id="app">
<b-table :items="items" :fields="fields" small bordered striped sticky-header no-border-collapse responsive>
</b-table>
</div>

Related

Vue sorting a frozen list of non-frozen data

I have a frozen list of non-frozen data, the intent being that the container is not reactive but the elements are, so that an update to one of the N things does not trigger dependency checks against the N things.
I have a computed property that returns a sorted version of this list. But Vue sees the reactive objects contained within the frozen list, and any change to an element results in triggering the sorted computed prop. (The goal is to only trigger it when some data about the sort changes, like direction, or major index, etc.)
The general concept is:
{
template: someTemplate,
data() {
return {
list: Object.freeze([
Vue.observable(foo),
Vue.observable(bar),
Vue.observable(baz),
Vue.observable(qux)
])
}
},
computed: {
sorted() {
return [...this.list].sort(someOrdering);
}
}
}
Is there a Vue idiom for this, or something I'm missing?
...any change to an element results in triggering the sorted computed prop
I have to disagree with that general statement. Look at the example below. If the list is sorted by name, clicking "Change age" does not trigger recompute and vice versa. So recompute is triggered only if property used during previous sort is changed
const app = new Vue({
el: "#app",
data() {
return {
list: Object.freeze([
Vue.observable({ name: "Foo", age: 22}),
Vue.observable({ name: "Bar", age: 26}),
Vue.observable({ name: "Baz", age: 32}),
Vue.observable({ name: "Qux", age: 52})
]),
sortBy: 'name',
counter: 0
}
},
computed: {
sorted() {
console.log(`Sort executed ${this.counter++} times`)
return [...this.list].sort((a,b) => {
return a[this.sortBy] < b[this.sortBy] ? -1 : (a[this.sortBy] > b[this.sortBy] ? 1 : 0)
});
}
}
})
Vue.config.productionTip = false;
Vue.config.devtools = false;
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<div id="app">
<div>Sorted by: {{ sortBy }}</div>
<hr>
<button #click="list[0].name += 'o' ">Change name</button>
<button #click="list[0].age += 1 ">Change age</button>
<hr>
<button #click="sortBy = 'name'">Sort by name</button>
<button #click="sortBy = 'age'">Sort by age</button>
<hr>
<div v-for="item in sorted" :key="item.name">{{ item.name }} ({{item.age}})</div>
</div>

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>

setting up v-autocomplete with search function

Trying to setup v-autocomplete, without a watcher, the flow would be:
Type a string, value is accepted by function
Function searches api for string and returns a list
List is put into "entries"
Computed property "tagsFound" is reevaluated.
"tagsFound" are displayed (since they are :items)
The main difference between the docs here and my code is my attempt to do this without a watcher rather with a simple function.
Relevant code:
<v-autocomplete
v-model="newTag"
:items="tagsFound"
:loading="loading"
:search-input.sync="search"
color="white"
hide-no-data
hide-selected
:placeholder="$t('search_terms.new_tag')"
></v-autocomplete>
...
data() {
return {
newTag: '',
entries: [],
....
methods: {
...
async search(term){
this.query.term = term
this.entries = await this.searchTerms(this.query)
},
...
computed: {
tagsFound(){
return this.entries
}
}
Expected behavior is search for the term typed and display the results as a dropdown.
Actual behavior is that it does not search and therefore does not display anything.
The sync modifier effectively makes a prop behave like v-model, so just like with v-model there's a prop and an event. The value needs to be a property, not a method, so :search-input.sync="search" doesn't make sense if search is a method.
The tagsFound computed property in your example isn't really doing anything. If you're just going to return entries you might as well just use entries directly in your template.
Not sure why you would want to do this without a watch but it can be done, either by splitting search-input.sync into a prop/event pair or by using a computed property with a getter and setter. The example below uses the latter approach.
function fakeServer (search) {
return new Promise(resolve => {
setTimeout(() => {
resolve([
'Red', 'Yellow', 'Green', 'Brown', 'Blue', 'Pink', 'Black'
].filter(c => c.toLowerCase().includes(search.toLowerCase())))
}, 1000)
})
}
new Vue({
el: '#app',
data () {
return {
newTag: '',
entries: [],
queryTerm: ''
}
},
computed: {
search: {
get () {
return this.queryTerm
},
set (searchInput) {
if (this.queryTerm !== searchInput) {
this.queryTerm = searchInput
this.loadEntries()
}
}
}
},
created () {
this.loadEntries()
},
methods: {
async loadEntries () {
this.entries = await fakeServer(this.queryTerm || '')
}
}
})
<link href="https://unpkg.com/vuetify#1.5.16/dist/vuetify.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet">
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<script src="https://unpkg.com/vuetify#1.5.16/dist/vuetify.js"></script>
<div id="app">
<v-app>
<v-autocomplete
v-model="newTag"
:items="entries"
:search-input.sync="search"
></v-autocomplete>
</v-app>
</div>
In this part, you bind search-input to an async method, this is wrong. You need to bind search-input to a data field and create a watch over it.
<v-autocomplete
:search-input.sync="search"
></v-autocomplete>
Define your component like below:
data: function(){
return {
newTag: '',
entries: [],
searchInput: null
}
},
watch: {
searchInput(val){
this.entries = await this.searchTerms(val)
}
}
And v-autocomplete template:
<v-autocomplete
v-model="newTag"
:items="tagsFound"
:loading="loading"
:search-input.sync="searchInput"
color="white"
hide-no-data
hide-selected
:placeholder="$t('search_terms.new_tag')"
></v-autocomplete>
This is a working example I created on CodePen

Bootstrap-vue b-table with filter in header

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

Vuetify v-data-table - how to make it fill the available height?

I am using Vue.js and Vuetify.
I have a page with various sections that contain configurable items for the user.
The layout is a 2x2, so there are two items per row, and 2 rows.
One of the items should contain a table showing alarms.
The problem is the pagination. If I use the "default pagination" then the table starts at 5 rows and the user can select the number of rows in a drop-down.
But I want to use the external pagination of Vuetify. No problem here.
But ... how do I make the table fill up the available height, and compute the pagination based on that?
So that on a large screen ... say ... 12 items are displayed per page and on a smaller screen ... say ... 8 items are displayed?
If there are 24 data entries, then in the first case the external pagination would allow the user to select pages 1 or 2, and in the second case pages 1, 2 and 3.
The default (5 row, 10 rows, 15 rows, ...) ... that's nice but I wonder who would need it. The table should fill the height and calculate the pagination based on number of rows and available height itself ... and it probably/maybe does ... but I cannot figure out, how.
import Vue from 'vue';
import { Component } from 'vue-property-decorator';
import { DeviceAlarm } from '#/api/DashboardApi';
import { getDeviceAlarms } from '#/components/alarms/AlarmsStore';
#Component
export default class Alarms extends Vue {
deviceAlarms: DeviceAlarm[] = [];
alarmPagination: any = { sortBy: 'alarmTime', descending: true };
public mounted(): void {
this.deviceAlarms = getDeviceAlarms(this.$store);
};
get headers() {
return [
{ text: this.$t('biz.dashboard.alarms.devicename'), align: "left", sortable: true, value: "fridgeDisplayName" },
{ text: this.$t('biz.dashboard.alarms.eventdate'), align: "left", sortable: true, value: "alarmTime" },
{ text: this.$t('biz.dashboard.alarms.status'), align: "left", sortable: false, value: "state" },
{ text: this.$t('biz.dashboard.alarms.information'), align: "left", sortable: false, value: "task" }
]
};
get pagination() {
return this.alarmPagination;
};
set pagination(newPagination) {
this.alarmPagination = newPagination;
};
get alarms() {
return this.deviceAlarms;
};
get pages() {
if (this.alarmPagination.rowsPerPage == null || this.alarmPagination.totalItems == null) {
return 0
}
// Test stuff ... how to do that to fill the available space?
this.alarmPagination.totalItems = this.deviceAlarms.length;
this.alarmPagination.rowsPerPage = 9;
let page = Math.ceil(this.alarmPagination.totalItems / this.alarmPagination.rowsPerPage);
return page;
}
}
<template>
<div>
<v-card-title>{{ $t('biz.general.items.alarms') }}</v-card-title>
<div id="tableDiv">
<v-data-table :headers='headers' :items='alarms' :pagination.sync='pagination' hide-actions>
<template slot="items" slot-scope="props">
<td>{{ props.item.fridgeDisplayName }}</td>
<td>{{ props.item.alarmTime | formatDateTime }}</td>
<td>{{ props.item.state }}</td>
<td>{{ props.item.task }}</td>
</template>
</v-data-table>
<div class="text-xs-center pt-2">
<v-pagination circle v-model="pagination.page" :length="pages"></v-pagination>
</div>
</div>
</div>
</template>
<script lang = "ts" src="./Alarms.ts"></script>
Answer
Please see this other link. We made the table fill the height, but it also expanded the pre-set height of the v-card. That's discussed and worked-around in the other thread (where the focus was more on keeping the height and re-doing the pagination on the fly when the size changes).
For your height issue I suggest using a v-data-iterator since you can completely customize it for things other than raw data.
As for the table where you want the items per page to be responsive I was thinking you could use computed props in combitation with vuetify's display breakpoints:
<v-data-table
:items="items"
:items-per-page="rowsPerPage"
:page.sync="page"
:items-per-page="itemsPerPage"
hide-default-footer />
computed: {
itemsPerPage: function () {
if(this.$vuetify.breakpoint.smAndDown) {
return 2 // Say you want to show 2 items on small screens and smaller
}
if (this.$vuetify.breakpoint.mdAndDown) {
return 4
}
return 10
},
pageCount: function () {
return Math.ceil(this.items.length / this.itemsPerPage)
}
}
Codesandbox for example
If I did not understand you problem correctly, try making a Codesandbox containing the problem to make it easier for us to help you :)