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.
Related
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>
How can I show a custom component- BaseButton.vue in the rows of the table, if table component and button are common components?
I made the component BaseTable.vue. I use this component for all tables.
<template>
<b-table :fields="fields" :items="items"></b-table>
</template>
<script>
export default {
props: {
fields:{
type: Array,
require: true
},
items:{
type: Array,
require: true
},
}
fields - object in a parent component.
items - object from API.
Parent component (base button doesn't appear in table on page):
<template>
<base-table :fields="fields" :items="items">
<template #cell(actions)>
<base-button></base-button>
</template>
<base-table>
</template>
<script>
import BaseButton from '...';
export default {
data() {
return {
fields: [
{ key: 'Caption', label: 'Name' },
{ key: 'PreviousMonthCounter', label: 'Prev. month' },
{ key: 'CurrentMonthCounter', label: 'Curr. month' },
{ key: 'actions', label: 'Action' }
],
items: []
}
},
components: {
BaseButton
}
}
And now I need to render BaseButton.vue in the last column of the table and I can't to make it - table has only text fields from items.
Need to create a child component (BaseTable):
<template>
<b-table :items="items" :fields="fields">
<slot v-for="slot in Object.keys($slots)" :name="slot" :slot="slot" />
<template v-for="slot in Object.keys($scopedSlots)" :slot="slot" slot-scope="scope">
<slot :name="slot" v-bind="scope"></slot>
</template>
</b-table>
</template>
And use it in the parent component like , but must be replaced by your component name.
For example:
<base-table :fields="fields" :items="items">
<template #cell(nameOfTheField)="{item}">
{{ item.key }}
</template>
<template #cell(nameOfTheField)>
<base-button></base-button>
</template>
I have a hierarchical list component where child items have checkboxes. Checkbox actions(check/uncheck) must keep the parent component in sync with the checkbox's changed state. I cannot figure out how to achieve this using v-bind.sync recursively. My code is as below:
Menu.vue
This component holds the hierarchical list. (Only relevant code included)
HierarchicalCheckboxList is the component that displays the hierarchical list
Property 'value' holds the check/uncheck value (true/false)
Property 'children' contains the child list items
How do I define the .sync attribute on HierarchicalCheckboxList and with what parameter?
<template>
<div>
<HierarchicalCheckboxList
v-for="link in links"
#checked="primaryCheckChanged"
:key="link.id"
v-bind="link">
</HierarchicalCheckboxList>
</div>
</template>
<script>
import HierarchicalCheckboxList from 'components/HierarchicalCheckboxList'
data () {
return {
links: [{
id: 1,
title: 'Home',
caption: 'Feeds, Dashboard & more',
icon: 'account_box',
level: 0,
children: [{
id: 2,
title: 'Feeds',
icon: 'feeds',value: true,
level: 1,
children: [{
id: '3',
title: 'Dashboard',
icon: 'settings',
value: true,
level: 1
}]
}]
}]
}
},
methods: {
primaryCheckChanged (d) {
// A child's checked state is propogated till here
console.log(d)
}
}
</script>
HierarchicalCheckboxList.vue
This component calls itself recursively:
<template>
<div>
<div v-if="children != undefined && children.length == 0">
<!--/admin/user/user-->
<q-item clickable v-ripple :inset-level="level" :to="goto">
<q-item-section>
{{title}}
</q-item-section>
</q-item>
</div>
<div v-else>
<div v-if="children != undefined && children.length > 0">
<!-- {{children}} -->
<q-expansion-item
expand-separator
:icon="icon"
:label="title"
:caption="caption"
:header-inset-level="level"
default-closed>
<template v-slot:header>
<q-item-section>
{{ title }}
</q-item-section>
<q-item-section side>
<div class="row items-center">
<q-btn icon="add" dense flat color="secondary"></q-btn>
</div>
</q-item-section>
</template>
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
#checked="primaryCheckChanged"
v-bind="child">
</HierarchicalCheckboxList>
</q-expansion-item>
</div>
<!-- to="/admin/user/user" -->
<div v-else>
<q-item clickable v-ripple :inset-level="level">
<q-item-section>
<q-checkbox :label="title" v-model="selection" />
</q-item-section>
</q-item>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'HierarchicalCheckboxList',
props: {
id: { type: String, required: true },
title: { type: String, required: false },
caption: { type: String, default: '' },
icon: { type: String, default: '' },
value: { type: Boolean, default: false },
level: { type: Number, default: 0 },
children: { type: Array }
},
data () {
return {
localValue: this.$props.value
}
},
computed: {
selection: {
get: function () {
return this.localValue
},
set: function (newvalue) {
this.localValue = newvalue
this.$emit('checked', this.localValue)
// or this.$emit('checked', {id: this.$props.id, value: this.localValue })
}
}
},
methods: {
primaryCheckChanged (d) {
this.$emit('checked', d)
}
}
}
</script>
What works so far
As a work-around I am able to get the checkbox state emitted with $emit('checked'), which I use to send it to the next process. But the parent's state is not updated until I refresh it back from the database.
How do I update the parent component's state using v-bind.sync recursively?
Appreciate any help!!
UI
Figured out how to do it after I broke the code down from the whole 2000 line code to a separate 'trial-n-error' code of 20 lines and then things became simple and clear.
Menu.vue
A few changes in the parent component in the HierarchicalCheckboxList declaration:
Note the sync property
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="link.value"
v-bind="child">
</HierarchicalCheckboxList>
HierarchicalCheckboxList.vue
Change the same line of code in the child component (as its recursive)
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="child.value"
v-bind="child">
</HierarchicalCheckboxList>
And in the computed set property, emit as below:
this.$emit('update:u', this.localValue)
That's it - parent n children components now stay in snyc.
What I want to achieve is something like:
<li v-for="(item, index) in items" :key="index>
<div v-if="item.Component">
<item.Component :value="item.value" />
</div>
<div v-else>{{ item.value }}</div>
</li>
But anyway I don't like at all this solution. The idea of defining Component key for an item in items list is hard to maintain since at least it is hard to write it in template-style way (usually we are talking about too long HTML inside). Also I don't like to wrap item.Component inside div.
data() {
return {
list: [{
value: 'abc',
Component: {
props: ['value'],
template: `123 {{ value }} 312`
}
}]
};
}
Does anyone know the best-practice solution for this and where Vue describes such case in their docs?
You can use Vue's <component/> tag to dynamically set your component in your list.
<li v-for="(item, index) in items" :key="index>
<component v-if="item.Component" :is="item.Component" :value="item.value"></component>
<div v-else>{{ item.value }}</div>
</li>
<script>
...,
data: () => ({
list: [{
value: 'abc',
Component: {
props: ['value'],
template: `<div>123 {{ value }} 312</div>` // must be enclosed in a element.
}
}]
})
</script>
You can also import a component too so you can create a new file and put your templates and scripts there.
Parent.vue
<script>
import SomeComponent from "#/components/SomeComponent.vue"; //import your component here.
export default {
data() {
return {
list: [
{
value: "abc",
Component: SomeComponent // define your imported component here.
},
]
};
}
};
</script>
SomeComponent.vue
<template>
<div>123 {{ value }} 312</div>
</template>
<script>
export default {
name: "SomeComponent",
props: ["value"]
};
</script>
Here's a demo.
I'm a very new at programming. I'm trying to figure it out how to bind the data to get the link :href work using store, vuex and bootstrap-vue table. I have spent 4 days for this, and now I'm dying. Please help.
books.js(store, vuex)
books: [
{
id: 1,
name: "name 1",
bookTitle: "book1",
thumbnail: '../../assets/img/book01.jpeg',
url: "https://www.google.com",
regDate: '2019-10'
},
{
id: 2,
name: "name2",
bookTitle: "book2",
thumbnail: "book2",
url: "http://www.yahoo.com",
regDate: '2019-10'
},
BookList.vue
<script>
export default {
name: "BookList",
components: {
},
computed: {
fields() {
return this.$store.state.fields
},
books() {
return this.$store.state.books
},
bookUrl() {
return this.$store.state.books.url
}
},
data() {
return {
itemFields: this.$store.state.fields,
items: this.$store.state.books,
//url: this.$store.state.books.url
}
}
};
</script>
<template>
<b-container>
<b-table striped hover :items="items" :fields="itemFields" >
<template v-slot:cell(thumbnail)="items">
<img src="" alt="image">
</template>
<template v-slot:cell(url)="items">
<b-link :href="bookUrl" >link</b-link>
</template>
</b-table>
</b-container>
</template>
The cell slot contains two properties you're generally interested in:
item (the current row, or, to be exact, the current item in items)
value (the cell - or, to be exact, the value of the current column within the item).
Therefore, considering your data, in the case of v-slot:cell(url)="{ value, item }", value is equivalent to item.url
Any of these would work:
<template v-slot:cell(url)="{ value }">
<b-link :href="value">link</b-link>
</template>
<template v-slot:cell(url)="slot">
<b-link :href="slot.value">{{ slot.item.bookTitle }}</b-link>
</template>
<template v-slot:cell(url)="{ item }">
<b-link :href="item.url">{{ item.bookTitle }}</b-link>
</template>
Working example here.
Note your question contains a few minor issues which might prevent your code from working (itemFields is referenced but not defined, not using proper getters, etc...). For details have a look at the working example.
And read the docs!