I have two components one is a Parent is some page random, and children is a component will use and more components for grids.
Parent
<template>
...
<DataTable
:items="items"
:fields="fields"
:currentPage="currentPage"
:perPage="perPage"
:filter="filter"
:sortBy="sortBy"
:sortDesc="sortDesc"
:sortDirection="sortDirection">
</DataTable>
...
</template>
<script>
import DataTable from "./DataTable.vue";
components: {
DataTable,
},
data: function(){
return {
fields: [],
items: [],
currentPage: 1,
perPage: 2,
filter: null,
sortBy: null,
sortDesc: false,
sortDirection: 'asc',
}
}
</script>
Child
<template>
...
<b-table
show-empty
stacked="md"
:items="items"
:fields="fields"
:current-page="currentPage"
:per-page="perPage"
:filter="filter"
:sort-by="sortBy"
:sort-desc="sortDesc"
:sort-direction="sortDirection">
</b-table>
<b-row>
<b-col md="6" class="my-1">
<b-pagination
:total-rows="items.length"
:per-page="perPage"
v-model="currentPageComputed"
class="my-0" />
</b-col>
</b-row>
...
</template>
<script>
props: {
items: Array,
fields: Array,
currentPage: Number,
perPage: Number,
filter: String,
sortBy: String,
sortDesc: Boolean,
sortDirection: String,
},
computed: {
currentPageComputed: {
get () {
return this.$props.currentPage
},
set (i) {
this.$props.currentPage = i;
}
},
},
</script>
Finally looks similar to:
when use the pagination for change of page; works But, send me this error:
I read this problem is referent to "props in Vue is an anti-pattern".
So, how a fix?
The simplest solution is to to use the #input event of b-pagination to emit the changed value from child to parent:
<b-pagination
:total-rows="items.length"
:per-page="perPage"
:value="currentPage"
#input='emitPageValue'
class="my-0" />
</b-col>
And in methods:
methods: {
emitPageValue(value){
this.$emit('update:current-page', value)
}
}
Then, in parent you have to accept the changed value by applying the modifier .sync to the prop, so it will also handle the #update event:
<template>
...
<DataTable
:items="items"
:fields="fields"
:current-page.sync="currentPage"
:per-page="perPage"
:filter="filter"
:sort-by="sortBy"
:sort-desc="sortDesc"
:sort-direction="sortDirection">
</DataTable>
...
</template>
NB: Also pay attention to naming convention for props in template. It is recommended to use kebab-case for props in template and access the same property in camelCase in Javascript.
Related
In my Table component, I'm using Vue Bootstrap's b-table component to create a table, which retrieves its' data from an external JSON file through Vuex. Now I also have another component, Actions, which is rendered on each row of the table. This component contains an edit button which is supposed to open a modal when clicked.
The problem is that whenever I click the edit button, 4 modals come up one on top of another. The issue seems to lie in the number of rows rendered, because in the JSON file, there are 4 objects, each of which contains the student's name, date of birth and so on. When I get rid of three of these objects, the modal only renders once. My conclusion is that the modal is rendering 4 times, for each row, but I have no idea how to fix this.
Here's the Table and Actions component:
<script>
import Actions from "./Actions.vue"
export default {
data() {
return {
fields: [
'index',
'full_name',
{ key: "date_of_birth", label: 'Date of Birth' },
'municipality',
{ key: "action", label: 'Action' }
],
// tableItems: this.$store.state.registeredStudents.registeredStudents
}
},
components: {
Actions
},
methods: {
generateIndex() {
return Math.floor(1000000 * Math.random()).toString().slice(0, 6);
}
},
computed: {
rows() {
return this.tableItems.length
},
tableItems() {
const registeredStudents = this.$store.state.registeredStudents.registeredStudents
return registeredStudents.map(student => ({
index: this.generateIndex(), ...student
}))
}
},
}
</script>
<template>
<b-table :fields="fields" :items="tableItems" :per-page="perPage" :current-page="currentPage" responsive="sm" primary-key="index"
striped hover>
<template #cell(action)="data">
<Actions/>
</template>
</b-table>
</template>
<script>
import { BIconPencilFill, BIconTrashFill } from 'bootstrap-vue';
export default {
}
</script>
<template>
<div>
<b-button variant="primary" class="mx-1 p-1" v-b-modal.edit-student>
<b-icon-pencil-fill></b-icon-pencil-fill>
</b-button>
<b-modal id="edit-student" title="Edit student info">
<p class="my-4">Hello from modal!</p>
</b-modal>
<b-button variant="danger" class="mx-1 p-1">
<b-icon-trash-fill></b-icon-trash-fill>
</b-button>
</div>
</template>
The problem here is most likely because your id's are not unique. For each row of your table you are generating the following;
<b-modal id="edit-student" title="Edit student info">
<p class="my-4">Hello from modal!</p>
</b-modal>
with the same ID.
I think the best approach here would be to include the Actions in the Table component. You don't need the extra component since it has no internal logic or props.
Table.vue
data() {
return {
fields: [
'index',
'full_name',
{ key: "date_of_birth", label: 'Date of Birth' },
'municipality',
{ key: "action", label: 'Action' }
],
lastClickedRowIndex: -1
}
},
...
<template>
<b-table :fields="fields" :items="tableItems" :per-page="perPage" :current-page="currentPage" responsive="sm" primary-key="index"
striped hover>
<template #cell(action)="data">
<b-button variant="primary" class="mx-1 p-1" #click="() => (lastClickedRowIndex = data.index)" v-b-modal.edit-student>
<b-icon-pencil-fill></b-icon-pencil-fill>
</b-button>
<b-button variant="danger" class="mx-1 p-1">
<b-icon-trash-fill></b-icon-trash-fill>
</b-button>
</template>
</b-table>
<b-modal id="edit-student" title="Edit student info">
<p class="my-4">Hello from modal!</p>
<!-- Dynamically change contents here based on lastClickedRowIndex -->
</b-modal>
</template>
I apologise if any syntax is incorrect I've only used Vue 3.
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.
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'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>
How do I access the data entered in my input elements, which are passed through via a slot, to my child component that opens up a modal with the form elements inside of it?
I've been reading the vue docs about scoped slots but honestly, I just can't figure it out how to make it work in my example. None of the examples make use of an input element with a v-model that is being passed to the child component.
I have created a component "BaseFormModal" which contains the following code:
Note that the validation (vee-validate) occurs inside here, so this child component emits a "submit" event when the data is considered valid, which I then pick up in my parent component.
<template v-slot:default="slotProps">
<b-modal ref="base-form-modal" :title="title" :no-close-on-backdrop="true" #ok.prevent="onSubmit">
<validation-observer ref="observer" v-slot="{handleSubmit}">
<b-form ref="form" #submit.stop.prevent="handleSubmit(onSubmit)">
<slot />
</b-form>
</validation-observer>
</b-modal>
</template>
<script>
import { ValidationObserver } from 'vee-validate'
export default {
name: 'BaseFormModal',
components: {
ValidationObserver,
},
props: {
title: {
type: String,
required: true,
},
},
data () {
return {
formData: {},
}
},
methods: {
async onSubmit () {
let valid = await this.$refs.observer.validate()
if (!valid) {
return
}
this.$emit('submit', this.formData)
this.$nextTick(() => {
this.$refs['base-form-modal'].hide()
})
this.formData = {}
},
showModal () {
this.$refs['base-form-modal'].show()
},
},
}
</script>
<style lang="scss" scoped>
</style>
In my page, I have a button which opens up the modal, like so:
<b-button variant="primary" #click="$refs['addOrgUserModal'].showModal()">
<i class="far fa-plus" aria-hidden="true" /> {{ $t('organisation_settings_manage_users_add_user') }}
</b-button>
Then I have defined the base form modal component in my page as this:
<base-form-modal
ref="addOrgUserModal"
:title="$tU('organisation_settings_manage_users_add_user_modal_title')"
#submit="addOrgUser"
>
<b-row>
<b-col md="6">
<form-control-wrapper :rules="{required: true}" :label="$tU('first_name_label')">
<b-form-input
v-model="user.firstName"
type="text"
lazy-formatter
:formatter="trimSpaces"
:placeholder="$t('first_name_field_placeholder')"
/>
</form-control-wrapper>
</b-col>
<b-col md="6">
<form-control-wrapper :rules="{required: true}" :label="$tU('family_name_label')">
<b-form-input
v-model="user.familyName"
type="text"
lazy-formatter
:formatter="trimSpaces"
:placeholder="$t('family_name_field_placeholder')"
/>
</form-control-wrapper>
</b-col>
</b-row>
</base-form-modal>