Can Vue.Draggable be used with Vuetify v-data-table and allow utilisation of table v-slot:item.<name>? - vue.js

Vuetify v-data-table supports several types of slots: v-slot:body, v-slot:item and v-slot:item.<name>.
We have been using v-slot:item.<name> extensively, as these provides a flexible way to style and process content in individual columns AND allow the table headers to be programmatically changed.
I'd like to add draggability to my v-data-table rows and have got this working using Vue.Draggable.
However the draggable component requires use of the v-data-table v-slot:body i.e. taking control of the full body of the table and thereby losing the flexibility of v-slot:item.<name>.
Is there a way these two components can be used together and provide v-slot:item.<name> support?

I have created a DataTableRowHandler component which allows v-slot:item.<name> support.
This is placed inside the draggable component, inserts the table <tr> element and feeds off the same "headers" array to insert <td> elements and v-slot:item.<name> entries. If no v-slot:item.<name> is defined then the cell value is output, in the same way that v-data-table works.
Here is the example component usage:
<v-data-table
ref="myTable"
v-model="selected"
:headers="headers"
:items="desserts"
item-key="name"
class="elevation-1"
>
<template v-slot:body="props">
<draggable
:list="props.items"
tag="tbody"
:disabled="!allowDrag"
:move="onMoveCallback"
:clone="onCloneCallback"
#end="onDropCallback"
>
<data-table-row-handler
v-for="(item, index) in props.items"
:key="index"
:item="item"
:headers="headers"
:item-class="getClass(item)"
>
<template v-slot:item.lock="{ item }">
<v-icon #click="item.locked = item.locked ? false : true">{{
item.locked ? "mdi-pin-outline" : "mdi-pin-off-outline"
}}</v-icon>
</template>
<template v-slot:item.carbs="{ item }">
{{ item.carbs }}
<v-icon>{{
item.carbs > 80
? "mdi-speedometer"
: item.carbs > 45
? "mdi-speedometer-medium"
: "mdi-speedometer-slow"
}}</v-icon>
</template>
</data-table-row-handler>
</draggable>
</template>
</v-data-table>
Here is the DataTableRowHandler component code
<template>
<tr :class="getClass">
<td v-for="(header, index) in headers" :key="index">
<slot :item="item" :name="columnName(header)">
<div :style="getAlignment(header)">
{{ getNonSlotValue(item, header) }}
</div>
</slot>
</td>
</tr>
</template>
<script>
export default {
name: "DataTableRowHandler",
components: {},
props: {
itemClass: {
type: String,
default: "",
},
item: {
type: Object,
default: () => {
return {};
},
},
headers: {
type: Array,
default: () => {
return [];
},
},
},
data() {
return {};
},
computed: {
getClass() {
return this.itemClass;
}
},
methods: {
columnName(header) {
return `item.${header.value}`;
},
getAlignment(header) {
const align = header.align ? header.align : "right";
return `text-align: ${align}`;
},
getNonSlotValue(item, header) {
const val = item[header.value];
if (val) {
return val;
}
return "";
},
},
};
</script>
An example of it's use is in this codesandbox link

I checked the source code of VDataTable and found that the content in tbody is generated by genItems().
The following example is implemented with functional components and is fully compatible with v-slot:item*:
<v-data-table ref="table" ...>
<template #body="props">
<draggable
v-if="$refs.table"
tag="tbody"
:list="props.items"
>
<v-nodes :vnodes="$refs.table.genItems(props.items, props)" />
</draggable>
</template>
<template #item.name="{ item }">
...
</template>
</v-data-table>
Here is the definition of the VNodes component:
components: {
VNodes: {
functional: true,
render: (h, ctx) => ctx.props.vnodes,
}
}

Related

How can I use <v-data-table> item slots? (inheritance scenario)

I created a BaseDataTable component:
<template>
<v-data-table
:class="{ clickable }"
:headers="reshapedHeaders"
:items="items"
:loading="loading"
:sort-by="sortBy"
sort-desc
:item-key="itemKey"
:expanded.sync="expanded"
:show-expand="showExpand"
:hide-default-footer="disablePagination"
:disable-pagination="disablePagination"
#click:row="handleClickRow"
#dblclick:row.stop="handleDblclickRow"
>
<!-- Translating headers
(translationPath is one of my header custom props) -->
<template
v-for="{ text, value, translationPath } in reshapedHeaders"
#[getHeaderSlotName(value)]
>
<!-- This component uses vue-i18n features under the cover -->
<ValueTranslator
:key="value"
:translation-path="translationPath
? translationPath
: commonTranslationPath"
:value="text"
/>
</template>
<!-- Overriding item slots -->
<template
v-for="{ value } in reshapedHeaders"
#[getItemSlotName(value)]="slotData"
>
<slot
:name="getItemSlotName(value)"
v-bind="slotData"
>
{{ slotData.value }}
</slot>
</template>
</v-data-table>
</template>
<script>
import ValueTranslator from '../ValueTranslator.vue
export default {
props: {
headers: Array,
items: Array,
loading: Boolean,
clickable: Boolean,
itemKey: {
type: String,
default: '_id'
}
showExpand: Boolean,
/* Defines pagination and footer visibility;
true = disable pagination and hide footer
false = paginate and show footer */
disablePagination: Boolean,
/* Value added before each translation */
commonTranslationPath: String,
/* Defines actions column visibility;
true = show actions column
false = hide actions column */
showActions: Boolean,
/* Defines table initial sorting;
true = sort (default sorting)
false = don't sort
string = sort by passed value */
sort: [Boolean, String]
},
components: {
ValueTranslator
},
data() {
return {
expanded: []
}
},
computed: {
reshapedHeaders() {
const reshapedHeaders = [...this.headers]
if (this.showActions) {
/* Pushing actions header */
this.reshapedHeaders.push({
text: 'actions',
value: 'actions',
translationPath: 'component.table'
sortable: false
})
}
return reshapedHeaders
},
sortBy() {
if (this.sort) {
return typeof this.sort === 'string'
? this.sort
: 'lastModifiedDate'
} else
return null
}
},
methods: {
handleClickRow(item, data) {
this.$emit('click:row', item, data)
},
handleDblclickRow(_, { item }) {
this.$emit('dblclick:row', item)
},
getHeaderSlotName(value) {
return 'header.' + value
},
getItemSlotName(value) {
return 'item.' + value
}
}
}
</script>
As you can see I did that because I needed to group together a bunch of features. For reasons I can't explain here, I created a DataTable component based on the previously created BaseDataTable:
<template>
<v-container fluid>
<BaseDataTable
class="elevation-1"
:headers="headers"
:items="items"
:loading="loading"
:clickable="clickable"
:item-key="itemKey"
:show-expand="showExpand"
:disable-pagination="disablePagination"
:common-translation-path="commonTranslationPath"
show-actions
:sort="sort"
#click:row="handleClickRow"
#dblclick:row="handleDblclickRow"
>
<!-- code snippet I'll show you later -->
</BaseDataTable>
</v-container>
</template>
<script>
import BaseDataTable from '../../base/BaseDataTable.vue
props: {
headers: Array,
items: Array,
loading: Boolean,
clickable: Boolean,
itemKey: String,
showExpand: Boolean,
disablePagination: Boolean,
commonTranslationPath: String,
sort: [Boolean, String]
},
components: {
BaseDataTable
},
methods: {
handleClickRow(item) {
this.$emit('click:row', item)
},
handleDblclickRow(item) {
this.$emit('dblclick:row', item)
}
}
</script>
If I stop here I won't be able to use any item slot, but if I proceed I'll end up repeating myself...
Code snippet mentioned above:
<!-- Overriding item slots -->
<template
v-for="{ value } in headers"
#[getItemSlotName(value)]="slotData"
>
<slot
:name="getItemSlotName(value)"
v-bind="slotData"
></slot>
</template>
Plus I'm not taking in consideration there is the actions column. Above v-for is cycling through headers, not reshapedHeaders (located inside BaseDataTable). Is there a clean way to implement what I'm trying to implement?
Adding a slot inside v-data-table might probably solve your problem.
<v-data-table
:class="{ clickable }"
:headers="reshapedHeaders"
:items="items"
:loading="loading"
:sort-by="sortBy"
sort-desc
:item-key="itemKey"
:expanded.sync="expanded"
:show-expand="showExpand"
:hide-default-footer="disablePagination"
:disable-pagination="disablePagination"
#click:row="handleClickRow"
#dblclick:row.stop="handleDblclickRow"
>
<!-- Pass on all named slots -->
<slot
v-for="slot in Object.keys($slots)"
:name="slot"
:slot="slot"
/>
<!-- Pass on all scoped slots -->
<template
v-for="slot in Object.keys($scopedSlots)"
:slot="slot"
slot-scope="scope"
>
<slot :name="slot" v-bind="scope" />
</template>
</v-data-table>

Pass component as prop in Vue JS

Intro: I am exploring Vue Js and got stuck while trying to make a dynamic data table component the problem I am facing is that I cannot pass a component via props and render it inside a table.
Problem: So basically what I am trying to do is to pass some custom component from headers prop in v-data-table such as:
headers = [
{ text: 'Name', value: 'name' },
{
text: 'Phone Number',
value: 'phone_number',
render: () => (
<div>
<p>Custom Render</p>
</div>
)
},
{ text: 'Actions', value: 'actions' }
]
So from the code above we can see that I want to render that paragraph from the render function inside Phone Number header, I did this thing in React Js before, but I cannot find a way to do it in Vue Js if someone can point me in the right direction would be fantastic. Thank you in advance.
You have 2 options - slots and dynamic components.
Let's first explore slots:
<template>
<v-data-table :items="dataItems" :headers="headerItems">
<template slot="item.phone_number" slot-scope="{item}">
<v-chip>{{ item.phone_number }}</v-chip>
</template>
<template slot="item.company_name" slot-scope="{item}">
<v-chip color="pink darken-4" text-color="white">{{ item.company_name }}</v-chip>
</template>
</v-data-table>
</template>
The data table provides you slots where you can customize the content. If you want to make your component more reusable and want to populate these slots from your parent component - then you need to re-expose these slots to the parent component:
<template>
<v-data-table :items="dataItems" :headers="headerItems">
<template slot="item.phone_number" slot-scope="props">
<slot name="phone" :props="props" />
</template>
<template slot="item.company_name" slot-scope="props">
<slot name="company" :props="props" />
</template>
</v-data-table>
</template>
If you don't know which slots will be customized - you can re-expose all of the data-table slots:
<template>
<v-data-table
:headers="headers"
:items="items"
:search="search"
hide-default-footer
:options.sync="pagination"
:expanded="expanded"
class="tbl_manage_students"
height="100%"
fixed-header
v-bind="$attrs"
#update:expanded="$emit('update:expanded', $event)"
>
<!-- https://devinduct.com/blogpost/59/vue-tricks-passing-slots-to-child-components -->
<template v-for="(index, name) in $slots" v-slot:[name]>
<slot :name="name" />
</template>
<template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
<slot :name="name" v-bind="data" />
</template>
<v-alert slot="no-results" color="error" icon="warning">
{{ $t("no_results", {term: search}) }}"
</v-alert>
<template #footer="data">
<!-- you can safely skip the "footer" slot override here - so it will be passed through to the parent component -->
<table-footer :info="data" #size="pagination.itemsPerPage = $event" #page="pagination.page = $event" />
</template>
</v-data-table>
</template>
<script>
import tableFooter from '#/components/ui/TableFooter'; // you can safely ignore this component in your own implementation
export default
{
name: 'TeacherTable',
components:
{
tableFooter,
},
props:
{
search:
{
type: String,
default: ''
},
items:
{
type: Array,
default: () => []
},
sort:
{
type: String,
default: ''
},
headers:
{
type: Array,
required: true
},
expanded:
{
type: Array,
default: () => []
}
},
data()
{
return {
pagination:
{
sortDesc: [false],
sortBy: [this.sort],
itemsPerPageOptions: [25, 50, 100],
itemsPerPage: 25,
page: 1,
},
};
},
watch:
{
items()
{
this.pagination.page = 1;
},
sort()
{
this.pagination.sortBy = [this.sort];
this.pagination.sortDesc = [false];
},
}
};
</script>
Dynamic components can be provided by props:
<template>
<v-data-table :items="dataItems" :headers="headerItems">
<template slot="item.phone_number" slot-scope="{item}">
<component :is="compPhone" :phone="item.phone_number" />
</template>
<template slot="item.company_name" slot-scope="{item}">
<component :is="compCompany" :company="item.company_name" />
</template>
</v-data-table>
</template>
<script>
export default
{
name: 'MyTable',
props:
{
compPhone:
{
type: [Object, String], // keep in mind that String type allows you to specify only the HTML tag - but not its contents
default: 'span'
},
compCompany:
{
type: [Object, String],
default: 'span'
},
}
}
</script>
Slots are more powerful than dynamic components as they (slots) use the Dependency Inversion principle. You can read more in the Markus Oberlehner's blog
Okay, I don't believe this is the best way possible but it works for me and maybe it will work for someone else.
What I did was I modified the headers array like this:
headers = [
{ text: 'Name', align: 'start', sortable: false, value: 'name' },
{
text: 'Phone Number',
key: 'phone_number',
value: 'custom_render',
render: Vue.component('phone_number', {
props: ['item'],
template: '<v-chip>{{item}}</v-chip>'
})
},
{ text: 'Bookings', value: 'bookings_count' },
{
text: 'Company',
key: 'company.name',
value: 'custom_render',
render: Vue.component('company_name', {
props: ['item'],
template:
'<v-chip color="pink darken-4" text-color="white">{{item}}</v-chip>'
})
},
{ text: 'Actions', value: 'actions', sortable: false }
]
And inside v-data-table I reference the slot of custom_render and render that component there like this:
<template v-slot:[`item.custom_render`]="{ item, header }">
<component
:is="header.render"
:item="getValue(item, header.key)"
></component>
</template>
To go inside the nested object like company.name I made a function which I called getValue that accepts 2 parametes, the object and the path to that value we need which is stored in headers array as key (ex. company.name) and used loadash to return the value.
getValue function:
getValue (item: any, path: string): any {
return loadash.get(item, path)
}
Note: This is just the initial idea, which worked for me. If someone has better ideas please engage with this post. Take a look at the props that I am passing to those dynamic components, note that you can pass more variables in that way.

Vuetify v-data table get Index

Hey there I am new to vue js and vuetify.In my editProductSave I want to pass another variable which is the index of the row in the table. This is my current code and how would i achieve that? The table is plotted using the vuetify v-data-table
Whole code
<template>
<v-card>
<v-data-table
:headers="tableFields"
:items="programs"
:items-per-page="5">
<template v-slot:[`item._id.$oid`]="{ item }">
{{item._id.$oid}}
</template>
<template v-slot:[`item.tags`]="props">
<v-edit-dialog
:return-value.sync="props.item.tags"
large
persistent
#save="editProductSave(props.item)">
<div>{{props.item.tags.length === 0 ? '' : props.item.tags}}</div>
<template v-slot:input>
<div class="mt-1 text-h2">
Update Tag
</div>
<v-text-field
v-model="props.item.tags"
label="Edit"
single-line
counter
autofocus
></v-text-field>
</template>
</v-edit-dialog>
</template>
<script>
import tdmApi from "../services/api/Database";
export default {
props: ["DatabaseList"],
computed: {
totalRows() {
return this.programs.length;
},
},
created () {
this.viewTdmDatabase();
},
data () {
return {
tableFields: [
{text:'ID',value:'_id.$oid'},
{text:'Tag',value:'tags'},
],
programs: [],
}
},
</script>
<template v-slot:item.tags="{item,index}">
{{index}} //Output index
</template>
The code above should work, make sure to cover it with object.
Try the below code:
<template v-slot:[`item.tags`]="{props, index}">
<v-edit-dialog
:return-value.sync="props.item.tags"
large persistent
#save="editProductSave(props.item, index)">
// ...
</v-edit-dialog>
</template>
And in script the method would be
methods: {
editProductSave(item, index) {
// ...
}
}
It seems that vuetify does not have in the v-data-table api the index field, so in order to get it you can change the structure of the v-data-table.
This is an example of how to get the index of each row.
https://www.codegrepper.com/code-examples/whatever/vuetify+v-data-table+get+row+index
You can simply add the index to the programs in a computed property and import it in the data table like so:
template
...
<v-data-table
:headers="tableFields"
:items="programsComputed"
...
script
export default {
...
computed: {
totalRows() {
return this.programs.length;
},
programsComputed () {
return this.programs.map((program, index) => {
program.index = index;
return program;
})
}
},
...
data () {
return {
tableFields: [
{text:'ID',value:'_id.$oid'},
{text:'Tag',value:'tags'},
],
programs: [],
}
},
In your editProductSave(item) you would just have to call item.index

Vue modify v-data-table column item templates in the parent component

I'm using Vuetify and made a component including a v-data-table. How could I change the table column templates in the parent component?
Child component example:
<template>
<v-card>
<v-data-table :items="items" :headers="headers">
<!-- ???????? -->
</v-data-table>
</v-card>
</template>
<script>
export default {
name: "ChildComponent",
props: {
items: Array,
headers: Array,
},
};
</script>
Parent component:
The item.id is just an example, I need a common solution for any kind of object field.
<template>
<ChildComponent :items="items" :headers="headers">
<template v-slot:item.id="{ item }">
<v-chip> {{ item.id }} </v-chip>
</template>
</ChildComponent>
</template>
<script>
import ChildComponent from "./ChildComponent";
export default {
components: {
ChildComponent,
},
data: () => ({
items: [/* ... */],
headers: [/* ... */],
}),
};
</script>
I guess I need dynamic slots but not really know how to use them in this case.
Need to modify the ChildComponent on the following way:
<template>
<v-card>
<v-data-table :items="items" :headers="headers">
<template
v-for="header in headers"
v-slot:[`item.${header.value}`]="{ item }"
>
<slot :name="[`item.${header.value}`]" :item="item">
{{ getVal(item, header.value) }}
</slot>
</template>
</v-data-table>
</v-card>
</template>
<script>
export default {
name: "childComponent",
props: {
items: Array,
headers: Array,
},
methods: {
/*
https://stackoverflow.com/a/40270942/6936938
*/
getVal(item, path) {
return path.split(".").reduce((res, prop) => res[prop], item);
},
},
};
</script>
The getVal method is for the nested item fields. Without it you can't use paths like item.author.name.
Try to create a scoped slot in the child component with item.id as name and pass item as scoped value :
<v-data-table :items="items" :headers="headers">
<template v-slot:item.id="{ item }">
<slot :name="item.id" :item="item"></slot>
</template>
</v-data-table>

Rendering a new Vue component after click

I am using Vue-CLI. I have a Vue component which is called viewGenres.vue. This component contains Vuetify table which presents all of the current genres from Vue store. I am trying to add a new option for each genre which is "Edit".
My objective is that for each line in the table there will be an edit button. Once the button is clicked, a new component called editGenre.vue should be rendered.
This component should contain a filled-out form with all the existing details of the specific genre.
I have several questions:
1) Once I click on the edit button, the following exception appears on browser:
ReferenceError: Vue is not defined at VueComponent.editGenre
2) In order for me to load the right properties from the DB, I need to define the "ID" prop of the editGenre component. Does anyone have any recommendation on the best method to do so?
This is viewGenres.vue: (the method editGenre is the one responsible for rendering the new component).
<template>
<div class="root" ref="container">
<h2>Genres Info</h2>
<br>
<v-data-table
:headers="headers"
:items="genres"
hide-actions
class="elevation-1">
<template slot="items" slot-scope="props">
<td class="text-xs-left">{{ props.item.id }}</td>
<td class="text-xs-left">{{ props.item.name }}</td>
<td class="text-xs-left">{{ props.item.desc }}</td>
<td class="text-xs-left">{{ props.item.artists }}</td>
<td class="text-xs-left"><v-btn flat #click="editGenre(props.item.id)">EDIT</v-btn></td>
<td class="text-xs-left"><v-btn flat #click="deleteGenre(props.item.id)">Delete</v-btn></td>
</template>
</v-data-table>
</div>
</template>
<script>
import editGenre from '#/components/administratorView/Genres/editGenre.vue'
const firebase = require('../../../firebaseConfig.js')
export default {
data: function(){
return{
headers: [
{ text: 'ID', value: 'id'},
{ text: 'Name', value: 'name'},
{ text: 'Description', value: 'desc'},
{ text: 'Artists', value: 'artists'},
{ text: 'Edit Genre'},
{ text: 'Delete From DB'}
]
}
},
computed: {
genres: function(){
return this.$store.state.genre.genres
}
},
components: {
editGenre
},
methods: {
editGenre: function(id){
var ComponentClass = Vue.extend(editGenre)
var instance = new ComponentClass()
instance.$mount()
this.$refs.container.appendChild(instance.$el)
},
deleteGenre: function(id){
console.log("Trying to delete " +id)
firebase.firestore.collection("genres").doc(id).delete().then(()=>{
this.$store.dispatch('genre/getGenresFromDB')
alert("Deleted Document Successfully")
}).catch(function(error){
alert(error)
})
}
},
mounted(){
this.$store.dispatch('genre/getGenresFromDB')
}
}
</script>
<style scoped>
</style>
This is editGenre.vue:
<template>
<v-dialog v-model="editGenre" persistent max-width="500px">
<v-card>
<v-card-title>
<h2>Edit Genre {{genre.name}}</h2>
</v-card-title>
<v-card-text>
<v-text-field
v-model="name"
label="Name"
:error-messages="nameErrors"
#touch="$v.name.$touch()"
#blur="$v.name.$touch()"
/>
<v-textarea
v-model="desc"
label="Description"
box
/>
<v-combobox
v-model="artists"
label="Artists"
:items="artistNames"
:error-messages="artistsErrors"
#touch="$v.artists.$touch()"
#blur="$v.artists.$touch()"
multiple>
</v-combobox>
<v-btn
color="primary"
#click="submit">
Submit
</v-btn>
<v-btn
color="primary"
#click="close">
Close
</v-btn>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import { required } from 'vuelidate/lib/validators'
const firebase = require('../../../firebaseConfig')
export default{
data: function(){
return{
name: '',
desc: '',
artists: []
}
},
props: {
id: String
},
mounted: function(){
let docRef = firebase.firestore.collection("genres").doc(this.id)
docRef.get().then(function(doc){
if(doc.exists){
this.name = doc.data().name
this.desc = doc.data().desc
this.artists = doc.data().artists
}
else{
console.error("Doc Doesn't Exist!")
}
}).catch(function(error){
console.error(error)
})
}
}
</script>
<style scoped>
</style>
Thank You!
Tom
You missed to import Vue in your viewGenres.vue component, so add it as follow :
....
<script>
import Vue from 'vue'
import editGenre from '#/components/administratorView/Genres/editGenre.vue'
const firebase = require('../../../firebaseConfig.js')
....
You could pass props by this way :
var ComponentClass = Vue.extend(
props:{
id:{type:String, default () { return id}}
},editGenre)
and remove this :
props: {
id: String
}
according to Evan You :
It's not recommended to use new to manually construct child components. It is imperative and hard to maintain. You probably want to make your child components data-driven, using and v-for to dynamically render child components instead of constructing them yourself.