V-data-table controlling expanded items via v-slot:body - vuetify.js

The documentation on vuetify 2.0 v-data-tables doesn't specify how to control the expanded items via the v-slot:body. I have a table i need to specify with the body v-slot and would like to use the expand feature.
Expected behavior is by clicking the button in one column in the table, the row expands below.
I'm using the v-slot:body as i will need to fully customize the column content. I'm migrating the code from vuetify 1.5 where props.expanded enabled this functionality.
Codepen: https://codepen.io/thokkane/pen/PooemJP
<template>
<v-data-table
:headers="headers"
:items="deserts"
:expanded.sync="expanded"
:single-expand="singleExpand"
item-key="name"
hide-default-footer
>
<template v-slot:body="{ items }">
<tbody>
<tr v-for="item in items" :key="item.name">
<td>
<v-btn #click="expanded.includes(item.name) ? expanded = [] : expanded.push(item.name)">Expand</v-btn>
</td>
</tr>
</tbody>
</template>
<template v-slot:expanded-item="{ headers, item }">
<span>item.name</span>
</template>
</v-data-table>
</template>
<script>
export default {
data () {
return {
expanded: [],
singleExpand: false,
headers: [
{ text: 'Dessert (100g serving)', align: 'left', sortable: false, value: 'name' },
{ text: 'Calories', value: 'calories' },
{ text: 'Fat (g)', value: 'fat' },
{ text: 'Carbs (g)', value: 'carbs' },
{ text: 'Protein (g)', value: 'protein' },
{ text: 'Iron (%)', value: 'iron' },
{ text: '', value: 'data-table-expand' },
],
desserts: [
{
name: 'Frozen Yogurt',
calories: 159,
fat: 6.0,
carbs: 24,
protein: 4.0,
iron: '1%',
},
{
name: 'Ice cream sandwich',
calories: 237,
fat: 9.0,
carbs: 37,
protein: 4.3,
iron: '1%',
},
{
name: 'Eclair',
calories: 262,
fat: 16.0,
carbs: 23,
protein: 6.0,
iron: '7%',
},
],
}
},
}
</script>

When you use v-slot:body together with v-slot:expanded-item, you are overriding your changes inside v-slot:expanded-item. This is because expanded-item slot is inside the body slot. If you are going to use body for customization, you unfortunately have to customize everything inside.
Imagine a structure like this:
<div slot="body">
<div slot="item">...</div>
<div slot="expanded-item">...</div>
etc...
<div>
So in this case <template v-slot:body> will replace <div slot="body"> and anything inside. Therefore usage of <template v-slot:expanded-item> will not work with <template v-slot:body>. Besides v-slot:body props does not include item-specific properties and events like select(), isSelected, expand(), isExpanded etc.
I would recommend you to use v-slot:item instead. You can accomplish same thing without needing to customize everything with less code.
Something like this should work:
<template v-slot:item="{ item, expand, isExpanded }">
<tr>
<td>
<v-btn #click="expand(!isExpanded)">Expand</v-btn>
</td>
<td class="d-block d-sm-table-cell" v-for="field in Object.keys(item)">
{{item[field]}}
</td>
</tr>
</template>
<template v-slot:expanded-item="{ headers, item }">
<td :colspan="headers.length">Expanded Content</td>
</template>
If you want to have access to expanded items in JavaScript, don't forget to add :expanded.sync="expanded" to <v-data-table> . To open unique item on click you need to set item-key="" property on <v-data-table>.

With newer versions you can achieve this using body slot as well, here's my code
https://codepen.io/saurabhtalreja/pen/JjyWxEr
new Vue({
el: '#app',
vuetify: new Vuetify(),
data: () => ({
headers: [{
name: 'id',
text: 'id',
value: 'id'
},
{
name: 'text',
text: 'text',
value: 'text'
}
],
items: [{
id: 1,
text: "Item 1"
},
{
id: 2,
text: "Item 2"
},
{
id: 3,
text: "Item 3"
},
{
id: 4,
text: "Item 4"
},
{
id: 5,
text: "Item 5"
}
]
})
})
<link href="https://cdn.jsdelivr.net/npm/#mdi/font#3.x/css/materialdesignicons.min.css" rel="stylesheet"/>
<link href="https://cdn.jsdelivr.net/npm/vuetify#2.x/dist/vuetify.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/vue#2.x/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify#2.x/dist/vuetify.js"></script>
<div id="app">
<v-app>
<v-content>
<v-container grid-list-xl>
<v-data-table class="elevation-1" :headers="headers" :items="items" show-expand hide-default-footer>
<template v-slot:body="{ items, headers, expand, isExpanded }">
<tbody>
<tr v-for="item in items" :key="item.name">
<td>
<v-btn #click="expand(item,!isExpanded(item))">Expand</v-btn>
<div v-if="isExpanded(item)" :style="{width: '250px'}">
Expanded content for {{ item.text }}
</div>
</td>
<td>{{item.id}}</td>
<td>{{item.text}}</td>
</tr>
</tbody>
</template>
</v-data-table>
</v-container>
</v-content>
</v-app>
</div>
<div id="app">
<v-app>
<v-content>
<v-container grid-list-xl>
<v-data-table class="elevation-1" :headers="headers" :items="items" show-expand hide-default-footer>
<template v-slot:expanded-item="{item, headers}">
<td :colspan="headers.length">
expanded content for {{ item.text }}
</td>
</template>
</v-data-table>
</v-container>
</v-content>
</v-app>
</div>

Related

Vuetify v-data-table, How to render line break (enter key) from v-textarea input

I have input data from v-textarea with enter key and the result like
Hello\nWorld
Rendering this in v-data-table with default or rawHtml not working
<v-data-table
:headers="headers"
:items="dataTable"
hide-default-footer>
<template v-slot:item="props">
<tr>
<td><span v-html="props.item.s"></span></td> <!-- rawHtml -->
<td>{{ props.item.s }}</td> <!-- default-->
</tr>
</template>
</v-data-table>
Vuetify uses browser conventions to render tables,
To render a line break inside a <td>, the suggestion is to either use a pre tag or the CSS style td { white-space:pre }
See answers to this post.
I included the CSS inline using a template within the v-data-table
new Vue({
el: '#app',
vuetify: new Vuetify(),
data () {
return {
headers: [
{
text: 'ID',
align: 'start',
sortable: false,
value: 'name'
},
{
text: 'Description',
align: 'start',
sortable: false,
value: 'description'
}
],
items: [
{
name: "001",
description: `First line of content.
Second line of content.
Third line of content (with left tab).`
},
{
name: "002",
description: `First line of content.
Second line of content.
Third line of content (with left tab).`
},
]
}
},
})
<!-- begin snippet: js hide: false console: true babel: false -->
<link href="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.js"></script>
<div id="app">
<v-app id="other">
<v-data-table :headers="headers" :items="items" item-key="name" class="elevation-1">
<template v-slot:item.description="{ item }">
<div style="white-space: pre-wrap;">{{ item.description }}</div>
</template>
</v-data-table>
</v-app>
</div>

Problem with expandable child component in parent v-data-table using Vuetify 2

I upgraded to Vuetify 2 from 1.5. Everything went pretty smoothly except for one thing. I have a parent component with a v-data-table and I want to pass data and expand each row with a child component.
ScanGrid(parent component):
<template>
<v-container>
<v-card>
<v-card-text>
<v-layout row align-center>
<v-data-table
:headers="headers"
:items="items"
:hide-default-footer="true"
item-key="id"
>
<template slot="items" slot-scope="props">
<tr #click="props.expanded = !props.expanded">
<td>{{ props.item.name }}</td>
<td class="text-xs-left large-column">
{{ props.item.scanned }}
</td>
<td class="text-xs-left large-column">
{{ props.item.incoming }}
</td>
<td class="text-xs-left large-column">
{{ props.item.outgoing }}
</td>
<td class="text-xs-left large-column">
{{ props.item.unknown }}
</td>
</tr>
</template>
<template slot="expand" slot-scope="props">
<ScanGridChild :value="props.item"></ScanGridChild>
</template>
</v-data-table>
</v-layout>
</v-card-text>
</v-card>
</v-container>
</template>
ScanGridChild(child component):
<template>
<v-card>
<v-card-text>{{ value }}</v-card-text>
</v-card>
</template>
<script>
export default {
name: "ScanGridChildComponent",
props: {
value: {
Type: Object,
Required: true
}
},
computed: {},
watch: {
props: function(newVal, oldVal) {
console.log("Prop changed: ", newVal, " | was: ", oldVal);
this.render();
}
}
};
</script>
<style></style>
It worked fine in Vuetify 1.5.19. I'm on Vuetify 2.1.6 and using single file components. Thanks.
Vuetify 2.x has major changes to many components, slot-scopes are replaced with v-slot, and many new properties and slots added to vuetify data table
Here is the working codepen reproduced the same feature with above code
https://codepen.io/chansv/pen/BaaWbKR?editors=1010
You need to make sure that you have vue js 2.x and vuetify 2.x
Parent component code:
<template>
<v-container>
<v-card>
<v-card-text>
<v-layout row align-center>
<v-data-table
:headers="headers"
:items="items"
item-key="name"
single-expand
:expanded="expanded"
hide-default-footer
#click:row="clickedRow"
>
<template v-slot:expanded-item="{ item }">
<td :colspan="headers.length">
<ScanGridChild :value="item"></ScanGridChild>
</td>
</template>
</v-data-table>
</v-layout>
</v-card-text>
</v-card>
</v-container>
</template>
<script>
export default {
...
data () {
return {
expanded: [],
headers: [
{
text: "Localisation",
sortable: true,
value: "name"
},
{
text: "Paquets scannés",
sortable: true,
value: "scanned"
},
{
text: "Paquets entrants",
sortable: true,
value: "incoming"
},
{
text: "Paquets sortants",
sortable: true,
value: "outgoing"
},
{
text: "Paquets inconnus",
sortable: true,
value: "unknown"
}
],
items: [
{
id: 1,
name: "Location 1",
scanned: 159,
incoming: 6,
outgoing: 24,
unknown: 4,
test: "Test 1"
},
{
id: 2,
name: "Location 2",
scanned: 45,
incoming: 6,
outgoing: 24,
unknown: 4,
test: "Test 2"
}
],
}
},
methods: {
clickedRow(value) {
if (this.expanded.length && this.expanded[0].id == value.id) {
this.expanded = [];
} else {
this.expanded = [];
this.expanded.push(value);
}
}
}
...
}
</script>
In child component
Replace
props: {
value: {
Type: Object,
Required: true
}
},
with( Type and Required change to lower case type and required)
props: {
value: {
type: Object,
required: true
}
},

Vuetify 2.x: v-data-table expand row on click

I am using Vuetify.js and I am creating a a v-data-table
I cannot figure out how to expand a row by clicking anywhere on it, it seems that the only possibility is by using the show-expand property and clicking the icon
Final solution
As suggested here, the v-data-table can link an array to its expanded property, so if you push any item from the table, to this array, it will expand the corresponding row. Intuitive and smart
<template>
<v-data-table
:headers="headers"
:items="items"
:expanded="expanded"
item-key="id"
#click:row="expandRow"
/>
</template>
<script>
data () {
return {
expanded: [],
}
},
expandRow (item) {
// would be
// this.expanded = [item]
// but if you click an expanded row, you want it to collapse
this.expanded = item === this.expanded[0] ? [] : [item]
},
</script>
It worked for me:
<v-data-table
...
#click:row="(item, slot) => slot.expand(!slot.isExpanded)"
/>
Api docs here
There is a click event which gives you the current row on the v-data-table. This one you can use. In the event you update the expanded value
Like this:
HTML:
<div id="app">
<v-app id="inspire">
<v-data-table
:headers="headers"
:items="desserts"
:expanded="expanded"
item-key="name"
show-expand
class="elevation-1"
#click:row="clicked">
<template v-slot:top>
<v-toolbar flat color="white">
<v-toolbar-title>Expandable Table</v-toolbar-title>
<div class="flex-grow-1"></div>
<v-switch v-model="singleExpand" label="Single expand" class="mt-2"></v-switch>
</v-toolbar>
</template>
<template v-slot:expanded-item="{ headers }">
<td :colspan="headers.length">Peek-a-boo!</td>
</template>
</v-data-table>
</v-app>
</div>
vue js:
new Vue({
el: '#app',
vuetify: new Vuetify(),
methods: {
clicked(value) {
this.expanded.push(value)
}
},
data () {
return {
expanded: ['Donut'],
singleExpand: false,
headers: [
{
text: 'Dessert (100g serving)',
align: 'left',
sortable: false,
value: 'name',
},
{ text: 'Calories', value: 'calories' },
{ text: 'Fat (g)', value: 'fat' },
{ text: 'Carbs (g)', value: 'carbs' },
{ text: 'Protein (g)', value: 'protein' },
{ text: 'Iron (%)', value: 'iron' },
],
desserts: [
{
name: 'Frozen Yogurt',
calories: 159,
fat: 6.0,
carbs: 24,
protein: 4.0,
iron: '1%',
},
{
name: 'Ice cream sandwich',
calories: 237,
fat: 9.0,
carbs: 37,
protein: 4.3,
iron: '1%',
},
{
name: 'Eclair',
calories: 262,
fat: 16.0,
carbs: 23,
protein: 6.0,
iron: '7%',
}
],
}
},
})
i am not sure if you still need this :P but i wanted to improve dreijntjens reply for people who see this post
so add a listener attr
<v-data-table
:headers="headers"
:items="desserts"
:expanded="expanded"
item-key="name"
show-expand
class="elevation-1"
#click:row="clicked">
and add this method
clicked (value) {
const index = this.expanded.indexOf(value)
if (index === -1) {
this.expanded.push(value)
} else {
this.expanded.splice(index, 1)
}
},
my method will actually both expand and close on click
If you are using v-slot:item, or you set the prop :hide-default-header="true", then you can implement it this way:
<template v-slot:item="{item, index}">
<tr #click="expandedIndex = expandedIndex==-1?index:-1">
<span> {{item}} </span>
</tr>
<tr :colspan="headers.length" v-if="expandedIndex==index">Expanded content</tr>
</template>
If you want to be able to expand multiple rows at the same time, you can do this:
<v-data-table :headers="headers" :items="items">
<template v-slot:item="{item, index}">
<tr #click="expandRow(index)">
<span> {{item}} </span>
</tr>
<tr v-if="expandedRows.includes(index)">
<td :colspan="headers.length">
Expanded content
</td>
</tr>
</template>
</v-data-table>
<script>
...
...
expandedRows=[];
expandRow(index){
const indexOfRow = this.expandedRows.indexOf(index)
if(indexOfRow>-1){
this.expandedRows.splice(indexOfRow,1);
return;
}
this.expandedRows.push(index);
}
</script>

Vuetify - bottom align text field

I'm trying to align the bottom of my search text field with the bottom of my floating action button. I've created a code pen which you can see here:
I've tried everything I know including: setting a negative margin, and vertical-align to both baseline and bottom to no effect. You can see my code in the codepen, but I've attached it below as well:
<template id="app">
<v-container>
<v-layout row wrap align-center class="text-xs-center">
<v-flex xs12 align-center>
<v-flex>
<h1 class="display-1 sans_pro_medium fix-title-height pb-3">Failed Order Report</h1>
</v-flex>
<v-layout row>
<v-flex xs12>
<side_drawer v-show="side_drawer_show"></side_drawer>
</v-flex>
</v-layout>
<!--<v-flex xs12>-->
<!--<v-spacer></v-spacer>-->
<v-layout row>
<v-flex xs1>
<v-btn fab large color="purple darken-4" align-left>
<v-icon x-large color="white">refresh</v-icon>
</v-btn>
</v-flex>
<v-flex xs4 offset-xs7>
<v-text-field
align-right
v-model="search"
append-icon="search"
label="Search"
single-line
hide-details
>
</v-text-field>
</v-flex>
</v-layout>
<!--</v-flex>-->
<v-data-table
:headers="headers"
:items="desserts"
:search="search"
class="elevation-11"
>
<template v-slot:no-data>
<v-alert :value="true" type="success">
Your orders are looking great! No orders have failed.
</v-alert>
</template>
<template slot="items" slot-scope="props">
<td>{{ props.item.name }}</td>
<td class="text-xs-center">{{ props.item.calories }}</td>
<td class="text-xs-center">{{ props.item.fat }}</td>
<!--<td class="text-xs-right">{{ props.item.carbs }}</td>-->
<!--<td class="text-xs-right">{{ props.item.protein }}</td>-->
<!--<td class="text-xs-right">{{ props.item.iron }}</td>-->
</template>
</v-data-table>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
/*global localStorage*/
// import side_drawer from '../components/side_drawer.vue';
// import {dataShare} from '../packs/application.js';
import axios from 'axios';
export default {
components: {
side_drawer
},
data () {
return {
search: '',
side_drawer_show: true,
headers: [
{
text: 'Shopify Store URL',
align: 'center',
sortable: true,
value: 'url'
},
{ text: 'Shopify Order Number', value: 'shopify_order_number', align: 'center', sortable: true},
{ text: 'Amazon Order Id', value: 'amazon_order_id', align: 'center', sortable: true },
{ text: 'Shopify Order Status', value: 'shopify_order_status', align: 'center', sortable: true },
{ text: 'Amazon Order Status', value: 'amazon_order_status', align: 'center', sortable: true },
{ text: 'Action Needed', value: 'action' , sortable: true}
],
desserts: [
{
name: 'Frozen Yogurt',
calories: 159,
fat: 6.0,
carbs: 24,
protein: 4.0,
iron: '1%'
},
{
name: 'Ice cream sandwich',
calories: 237,
fat: 9.0,
carbs: 37,
protein: 4.3,
iron: '1%'
},
{
name: 'Eclair',
calories: 262,
fat: 16.0,
carbs: 23,
protein: 6.0,
iron: '7%'
},
{
name: 'Cupcake',
calories: 305,
fat: 3.7,
carbs: 67,
protein: 4.3,
iron: '8%'
}
]
}
}
}
</script>
<style>
.nudge_up {
padding-bottom: 10px !important;
}
</style>
You just need to add align-center to your v-layout So:
<v-layout row align-center>
<v-flex xs1>
<v-btn fab large color="purple darken-4" align-left>
<v-icon x-large color="white">refresh</v-icon>
</v-btn>
</v-flex>
<v-flex xs4 offset-xs7>
<v-text-field
align-right
v-model="search"
append-icon="search"
label="Search"
single-line
hide-details
>
</v-text-field>
</v-flex>
</v-layout>
You can read more about it here

How to wire up external data in Vuetify datatables

I have just started with Vue and found Vuetify ( and very impressed ) . I'm a bit of a newbie with node.js as well but some experience.
I am trying to find some examples on loading data from external API's into the vuetify datagrid - CRUD type stuff, reasonably large amounts of data paginated. The documentation in Vuetify is a little lacking in this regard. Should I be using Vuex?
If you want to call external API using REST, you'll need to use axios, which is a NPM package allowing you to make GET, POST and all that kind.
Let's use this online working API for our example. First, you need to get your data by calling this API. A good tutorial on Internet will show you more details, but let's use this code.
this.todos = axios.get('https://jsonplaceholder.typicode.com/todos/')
.then(response => { this.todos = response.data })
.catch(error => { console.log(error)});
Then you just have to use the datatable like in the documentation. Here is a CodePen to help you see, briefly, how I made the API call and then displayed it. It all comes from the official documentation, just modified to call a REST API. I'll put the code also here, in order to save it for future readers too.
<div id="app">
<v-app id="inspire">
<div>
<v-toolbar flat color="white">
<v-toolbar-title>Todos CRUD</v-toolbar-title>
<v-divider
class="mx-2"
inset
vertical
></v-divider>
<v-spacer></v-spacer>
<v-dialog v-model="dialog" max-width="500px">
<v-btn slot="activator" color="primary" dark class="mb-2">New Item</v-btn>
<v-card>
<v-card-title>
<span class="headline">{{ formTitle }}</span>
</v-card-title>
<v-card-text>
<v-container grid-list-md>
<v-layout wrap>
<v-flex xs12 sm6 md4>
<v-text-field v-model="editedItem.userId" label="User ID"></v-text-field>
</v-flex>
<v-flex xs12 sm6 md4>
<v-text-field v-model="editedItem.id" label="ID"></v-text-field>
</v-flex>
<v-flex xs12 sm6 md4>
<v-text-field v-model="editedItem.title" label="Title"></v-text-field>
</v-flex>
<v-flex xs12 sm6 md4>
<v-checkbox v-model="editedItem.completed" label="Completed?"></v-checkbox>
</v-flex>
</v-layout>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" flat #click="close">Cancel</v-btn>
<v-btn color="blue darken-1" flat #click="save">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-toolbar>
<v-data-table
:headers="headers"
:items="todos"
class="elevation-1"
>
<template slot="items" slot-scope="props">
<td class="text-xs-right">{{ props.item.userId }}</td>
<td class="text-xs-right">{{ props.item.id }}</td>
<td class="text-xs-right">{{ props.item.title }}</td>
<td class="text-xs-right">{{ props.item.completed }}</td>
<td class="justify-center layout px-0">
<v-icon
small
class="mr-2"
#click="editItem(props.item)"
>
edit
</v-icon>
<v-icon
small
#click="deleteItem(props.item)"
>
delete
</v-icon>
</td>
</template>
<template slot="no-data">
<v-btn color="primary" #click="initialize">Reset</v-btn>
</template>
</v-data-table>
</div>
</v-app>
</div>
And then the associated JS.
new Vue({
el: '#app',
data: () => ({
dialog: false,
headers: [
{
text: 'User ID',
align: 'left',
sortable: false,
value: 'userId',
width: '10'
},
{ text: 'ID', value: 'id', width: '10' },
{ text: 'Title', value: 'title' },
{ text: 'Completed', value: 'completed' }
],
todos: [],
editedIndex: -1,
editedItem: {
userId: 0,
id: 0,
title: '',
completed: false
},
defaultItem: {
userId: 0,
id: 0,
title: '',
completed: false
}
}),
computed: {
formTitle () {
return this.editedIndex === -1 ? 'New Item' : 'Edit Item'
}
},
watch: {
dialog (val) {
val || this.close()
}
},
created () {
this.initialize()
},
methods: {
initialize () {
this.todos = axios.get('https://jsonplaceholder.typicode.com/todos/')
.then(response => { this.todos = response.data })
.catch(error => { console.log(error)});
},
editItem (item) {
this.editedIndex = this.todos.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialog = true
},
deleteItem (item) {
const index = this.todos.indexOf(item)
confirm('Are you sure you want to delete this item?') && this.todos.splice(index, 1)
},
close () {
this.dialog = false
setTimeout(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
}, 300)
},
save () {
if (this.editedIndex > -1) {
Object.assign(this.todos[this.editedIndex], this.editedItem)
} else {
this.todos.push(this.editedItem)
}
this.close()
}
}
})

Resources