How do I open and close v-dialog from a component under its parent? Use Vuex? - vue.js

I need to open a CRUD dialog from a data table component. Both the dialog and data table share the same parent. The data table is reusable but the CRUD dialog is not.
The use case seems very common. An admin page contains a table of data, each row containing an edit button that opens edit dialog.
I've attempted using Vuex below - however this error occurs:
[Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'showUserModal' of undefined"
found in
---> <VBtn>
<VSimpleTable>
<VData>
<VDataTable>
<DataTable> at src/components/DataTable.vue
<Settings> at src/views/Settings.vue
<VContent>
<VApp>
<App> at src/App.vue
<Root>
Why is the imported mutator not available and is this a good approach to achieving the common functionality?
I arrived at my current solution using these 2 approaches
https://markus.oberlehner.net/blog/building-a-modal-dialog-with-vue-and-vuex/
https://forum.vuejs.org/t/how-to-trigger-a-modal-component-from-vuex-store/27243/9
UserAdmin.vue
<template>
<v-container fluid >
<DataTable v-bind:rows="allUsers" v-bind:headers="headers" />
<EditUser />
</v-container>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import DataTable from '../components/DataTable';
import EditUser from '../modals/EditUser';
export default {
name: 'UserAdmin',
methods: {
...mapActions(["getUsers"])
},
computed: mapGetters(["allUsers"]),
components: {
DataTable, EditUser
},
data(){
return {
headers: [
{ text: 'Name', value: 'name' },
{ text: 'Username', value: 'email' },
{ text: 'Administrator', value: 'admin' },
{ text: "", value: "controls", sortable: false}
]
}
},
created(){
this.getUsers();
}
}
</script>
DataTable.vue
<template>
<v-data-table
:headers="headers"
:items="rows"
:items-per-page="5"
class="elevation-1"
>
<!-- https://stackoverflow.com/questions/59081299/vuetify-insert-action-button-in-data-table-and-get-row-data -->
<template v-slot:item.controls="props">
<v-btn class="my-2" fab dark x-small color="blue" #click="onButtonClick(props.item.email)">
<v-icon dark>mdi-pencil</v-icon>
</v-btn>
</template>
</v-data-table>
</template>
<script>
import { mapMutations } from "vuex";
export default {
name: "DataTable",
props:["headers", "rows"],
methods: {
...mapMutations(["toggleUserModal"]),
onButtonClick: function(email) {
console.log("clicked: " + email)
this.toggleUserModal();
}
}
}
</script>
EditUser.vue
<template>
<v-row justify="center">
<v-dialog v-model="dialog" persistent max-width="600px" v-show='showUserModal'>
<v-card>
<v-card-title>
<span class="headline">User Profile</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field label="Legal first name*" required></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field label="Legal middle name" hint="example of helper text only on focus"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="Legal last name*"
hint="example of persistent helper text"
persistent-hint
required
></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field label="Email*" required></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field label="Password*" type="password" required></v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-select
:items="['0-17', '18-29', '30-54', '54+']"
label="Age*"
required
></v-select>
</v-col>
<v-col cols="12" sm="6">
<v-autocomplete
:items="['Skiing', 'Ice hockey', 'Soccer', 'Basketball', 'Hockey', 'Reading', 'Writing', 'Coding', 'Basejump']"
label="Interests"
multiple
></v-autocomplete>
</v-col>
</v-row>
</v-container>
<small>*indicates required field</small>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text #click="dialog = false">Close</v-btn>
<v-btn color="blue darken-1" text #click="dialog = false">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-row>
</template>
<script>
export default {
data: () => ({
dialog: false,
}),
computed: {
showUserModal(){
return this.$store.state.showUserModal
}
}
}
</script>
modals.js
const state = {
showUserModal: false
}
const mutations = {
toggleUserModal: () => (this.showUserModal = !this.showUserModal)
}
const getters = {
showUserModal: state => {
return state.showUserModal
}
}
export default {
state,
getters,
mutations
}
New code based on #Anatoly suggestions - everything works except the events emitted from the dialog, ex: onEditUserConfirmed are not picked up in the parent component.
ModalComponent
<template>
<v-row justify="center">
<v-dialog v-model="visible" persistent max-width="600px">
<v-card v-if="user">
<v-card-title>
<span class="headline">User Profile</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="user.name" label="Legal first name*" required></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field label="Legal middle name" hint="example of helper text only on focus"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="Legal last name*"
hint="example of persistent helper text"
persistent-hint
required
></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field label="Email*" required></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field label="Password*" type="password" required></v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-select :items="['0-17', '18-29', '30-54', '54+']" label="Age*" required></v-select>
</v-col>
<v-col cols="12" sm="6">
<v-autocomplete
:items="['Skiing', 'Ice hockey', 'Soccer', 'Basketball', 'Hockey', 'Reading', 'Writing', 'Coding', 'Basejump']"
label="Interests"
multiple
></v-autocomplete>
</v-col>
</v-row>
</v-container>
<small>*indicates required field</small>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text #click="onCancel">Close</v-btn>
<v-btn color="blue darken-1" text #click="onSave">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-row>
</template>
<script>
export default {
name: "EditUser",
props: {
user: Object,
visible: {
type: Boolean,
default: false
}
},
methods: {
onSave() {
console.log('save button gets here...')
this.$emit("onEditUserConfirmed", this.user);
},
onCancel() {
console.log('cancel button gets here...')
this.$emit("onEditUserCancelled");
}
}
};
</script>
Parent Component
<template>
<v-container fluid>
<v-data-table :headers="headers" :items="allUsers" :items-per-page="5" class="elevation-1">
<!-- https://stackoverflow.com/questions/59081299/vuetify-insert-action-button-in-data-table-and-get-row-data -->
<template v-slot:item.controls="props">
<v-btn class="my-2" fab dark x-small color="blue" #click="onEditClick(props.item)">
<v-icon dark>mdi-pencil</v-icon>
</v-btn>
</template>
</v-data-table>
<EditUser
:user="user"
:visible="isDialogVisible"
#confirmed="onEditUserConfirmed"
#cancelled="onEditUserCancelled"
/>
</v-container>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import EditUser from "../modals/EditUser";
export default {
name: "Settings",
data() {
return {
user: null,
isDialogVisible: false,
headers: [
{ text: "Name", value: "name" },
{ text: "Username", value: "email" },
{ text: "Administrator", value: "admin" },
{ text: "", value: "controls", sortable: false }
]
};
},
methods: {
...mapActions(["getUsers"]),
onEditClick: function(user) {
console.log('Editing user: ' + user.email)
this.user = user;
this.isDialogVisible = true;
},
onEditUserConfirmed(user) {
console.log('Saving user: ' + user.email)
this.isDialogVisible = false;
},
onEditUserCancelled () {
this.isDialogVisible = false;
}
},
computed: mapGetters(["allUsers"]),
components: {
EditUser
},
created() {
this.getUsers();
}
};
</script>

I would not recommend using the state for this task. since it not a very complex scenarios. you should use props and events for handling this kind of scenario
just modify the code a bit.
DataTable.vue
<script>
methods: {
onButtonClick: function(email) {
console.log("clicked: " + email)
this.$emit('openDialog') // or use any name here
}
}
</script>
UserAdmin.vue
<template>
<v-container fluid >
<!-- Listen to the event that you are emitting from DataTable.vue -->
<DataTable :rows="allUsers" :headers="headers" #showDialog="editUser = true" />
<!-- Pass that variable as a Prop -->
<EditUser :showDialog="editUser" />
</v-container>
</template>
<script>
....
data: () => ({
headers: [
{ text: 'Name', value: 'name' },
{ text: 'Username', value: 'email' },
{ text: 'Administrator', value: 'admin' },
{ text: "", value: "controls", sortable: false}
],
editUser: false, // a flag to keep the status of modal.
})
....
</script>
EditUser.vue
<script>
export default {
props: {
showDialog: {
type: Boolean,
default: false
}
},
data: () => ({
dialog: false,
}),
mounted() {
this.dialog = this.showDialog
},
watch: {
showDialog() {
if (this.showDialog)
this.dialog = true
}
}
}
</script>
I hope it should work, it worked for me in my scenario. I won't recommend using the Vuex store in this simple single level structure. Vuex should be used in case of some complex data structures where there are deep layers of components.
The code might have some syntax mistakes (do let me know). but i hope i just conveyed the concept

Use an event in a table component to inform a parent component you wish to edit a user (send a selected user in this event).
Catch the event in a parent component, write a user from the event to a prop in data section and pass this prop to a dialog component.
Use a prop to show/hide dialog from a parent component
Use an event to receive edited user after dialog confirmation.
Something like this:
Parent component
<DataTable v-bind:rows="allUsers" v-bind:headers="headers" #onEdit="onEditUser"/>
<EditUser :user="user" :visible="isDialogVisible" #confirmed="onEditUserConfirmed" #cancelled="onEditUserCancelled"/>
...
data: {
return {
// other data
user: null,
isDialogVisible : false
}
},
methods: {
onEditUser (user) {
this.user = user
this.isDialogVisible = true
},
onEditUserConfirmed (user) {
// hide a dialog
this.isDialogVisible = false
// save a user and refresh a table
},
onEditUserCancelled () {
// hide a dialog
this.isDialogVisible = false
}
}
Table component:
// better send a whole user object insteaf of just e-mail prop? It's up to you
#click="onButtonClick(props.item)"
...
methods: {
onButtonClick: function(user) {
this.$emit('onEdit', user)
}
}
Dialog component:
<v-dialog v-model="visible" ...
// render card only if user is passed
<v-card v-if="user">
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="user.firstName" label="Legal first name*" required></v-text-field>
</v-col>
...
<v-btn color="blue darken-1" text #click="onCancel">Close</v-btn>
<v-btn color="blue darken-1" text #click="onSave">Save</v-btn>
...
export default {
props: {
user: {
type: Object
},
visible: {
type: Boolean,
default: false
}
},
...
methods: {
onSave() {
this.$emit('confirmed', this.user)
},
onCancel () {
this.$emit('cancelled')
}
}
}

Related

How do I capture the value of the prop in the text field?

I have a prop and currently am able to get the data of the prop, Am trying a way to capture the item of the prop when saving the form.
Is there a way where i can take the value and pass if in a hidden text-area and bind the data to the vmodel?
Any help I appreciate.
<v-dialog v-model="dialog" persistent max-width="800">
<template v-slot:activator="{ on }">
<v-btn dark v-on="on" color="primary" round> Make payment </v-btn>
</template>
<v-card>
<v-card-title class="headline primary">
<span class="white--text">Add a new Doctor Payment Record {{ queueId }}</span>
<v-btn icon dark #click.native="dialog = false" absolute right>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<users-search
:leave-selected="true"
idOnly
label="Select Doctor"
#results="setDoctor"
>
</users-search>
<div class="row px-3">
<v-autocomplete
class="px-3 col-sm-8"
v-model="expense.bank"
v-if="banks.data"
:items="banks.data"
outline
chips
label="Select bank"
item-text="name"
item-value="id"
>
</v-autocomplete>
<v-text-field
class="px-3 col-sm-8"
outline
flat
v-model="expense.amount"
type="number"
#input="expense.percentage()"
required
label="Amount *"
persistent-hint
/>
</div>
<v-text-field
class="px-3"
outline
flat
v-model="expense.total_paid"
required
label="amount paid"
persistent-hint
/>
<v-text-field
class="px-3"
outline
flat
:value="setQueue"
v-model="expense.queueId"
required
:label=queueId
persistent-hint
/>
<v-alert :value="true" type="error" v-if="errors.any()">
<div v-html="errors.display()"></div>
</v-alert>
<v-layout row wrap>
<v-flex xs12>
<v-btn
color="success"
:loading="saveLoader"
#click="recordExpense()"
>save</v-btn
>
</v-flex>
</v-layout>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import NewUser from "#finance/libs/users/NewUser";
import {mapActions, mapGetters} from "vuex";
export default {
props: [
'queueId'
],
data: () => ({
dialog: false,
expense: new NewUser(),
saveLoader: false,
}),
computed: {
...mapGetters({
banks: "getBanks",
}),
balance: function () {
return parseFloat(10);
},
submitted() {
return this.expense.form.submitted;
},
contaminated() {
return this.expense.form.errorDetected;
},
errors() {
return this.expense.form.errors;
},
},
watch: {
submitted(v) {
if (v) {
this.saveLoader = false;
}
},
contaminated() {
this.saveLoader = false;
},
},
methods: {
...mapActions({
fetchBanks: "setBanks",
}),
setDoctor(user) {
this.expense.doctor_id = user.id;
},
setQueue(){
console.log(this.queueId);
this.expense.queueId = this.queueId;
},
async recordExpense() {
this.saveLoader = true;
let response = await this.expense.saveExpense();
this.saveLoader = false;
if (response) {
this.dialog = false;
this.$emit("expenseCreated");
}
},
},
mounted() {
this.fetchBanks();
}
};
</script>
The prop queueId i also want to store it along with the user information from the form.
Try this one, it should work:
<template>
<textarea v-model="hiddenValue" :style="{ display: 'none' }"></textarea>
</template>
<script>
export default {
props: [ 'queueId' ],
data() {
return {
hiddenValue: this.queueId
}
}
}
</script>
In case you will no need the prop to be modified, please bind the texarea value to the prop directly:
<textarea hidden v-model="queueId" :style="{ display: 'none' }></textarea>

Vuetify datatable crud on click "new"

I using example from documentation datatable with crud options.
How to change "New" on-click function?
What is v-slot:activator="{ on, attrs }"?
I need to call api with vuex dispatch.
I want that before edit dialog is opened row will immediately created with ID parameter inside from api.
I added #click, to make call. Is it correct?
<template>
<v-data-table
:headers="headers"
:items="PIATConsignments"
:footer-props="footerProps"
class="elevation-1"
>
<template v-slot:top>
<v-toolbar
flat
>
<v-toolbar-title>Items</v-toolbar-title>
<v-divider
class="mx-4"
inset
vertical
></v-divider>
<v-spacer></v-spacer>
<v-dialog
v-model="dialogEdit"
scrollable
max-width="800px"
>
<template v-slot:activator="{ on, attrs }">
<v-btn
color="primary"
dark
class="mb-2"
v-bind="attrs"
v-on="on"
#click="newItem"
>
New
</v-btn>
</template>
<v-card>
<v-card-title>
<span class="text-h5">{{ formTitle }}</span>
</v-card-title>
<v-card-text style="height: 600px;">
<v-container>
<v-row>
<v-col
cols="12"
sm="12"
md="6"
>
<v-autocomplete
v-model="editedItem.DepartureCountryDetails"
:items="countries"
item-text="ShortCountryName"
label="Departure"
class="input-group--focused"
v-bind:readonly="isReadonly"
return-object
></v-autocomplete>
</v-col>
<v-col
cols="12"
sm="12"
md="6"
>
<v-autocomplete
v-model="editedItem.DestinationCountryDetails"
:items="countries"
item-text="ShortCountryName"
label="Destination"
class="input-group--focused"
v-bind:readonly="isReadonly"
return-object
></v-autocomplete>
</v-col>
</v-row>
<v-row>
<v-col
cols="12"
sm="12"
md="6"
>
<v-text-field
v-model="editedItem.CAInvoiceValueAmount"
label="Amount"
></v-text-field>
</v-col>
<v-col
cols="12"
sm="12"
md="6"
>
<v-text-field
v-model="editedItem.UnifiedGrossMassMeasure"
label="Weight"
></v-text-field>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue darken-1"
text
#click="close"
>
Close
</v-btn>
<v-btn
color="blue darken-1"
text
#click="save"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="dialogDelete" max-width="500px">
<v-card>
<v-card-title class="text-h5">Do you want to delete item?</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text #click="closeDelete">Нет</v-btn>
<v-btn color="blue darken-1" text #click="deleteItemConfirm">Да</v-btn>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-dialog>
</v-toolbar>
</template>
<template v-slot:item.actions="{ item }">
<v-icon
small
class="mr-2"
#click="editItem(item)"
>
mdi-pencil
</v-icon>
<v-icon
small
#click="deleteItem(item)"
>
mdi-delete
</v-icon>
</template>
</v-data-table>
</template>
<script>
import modelPIATConsignment from '#/models/form/consignment/piat/PIATConsignment'
import DepartureCountryDetails from './DepartureCountryDetails.vue'
export default {
name: 'PIATConsignmentDetailsTable',
components: {
DepartureCountryDetails,
},
props: {
consignmentForm: { Object },
},
data: function() {
return {
dialogEdit: false,
dialogDelete: false,
headers: [
{ text: 'Departure', value: 'DepartureCountryDetails.ShortCountryName' },
{ text: 'Destination', value: 'DestinationCountryDetails.ShortCountryName' },
{ text: 'Amount', value: 'CAInvoiceValueAmount' },
{ text: 'Weight', value: 'UnifiedGrossMassMeasure' },
{ text: 'Actions', value: 'actions' },
],
footerProps: {
'disable-items-per-page': true,
'items-per-page-options': [],
'items-per-page-all-text': '',
},
PIATConsignments: this.consignmentForm.PIATConsignmentDetails,
editedIndex: -1,
editedItem: modelPIATConsignment,
defaultItem: modelPIATConsignment,
}
},
computed: {
formTitle () { return this.editedIndex === -1 ? 'New' : 'Edit' },
countries() { return this.$store.getters['dict/countries'] },
},
watch: {
dialogEdit (val) { val || this.close() },
dialogDelete (val) { val || this.closeDelete() },
},
methods: {
editItem (item) {
this.editedIndex = this.PIATConsignments.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialogEdit = true
},
deleteItem (item) {
this.editedIndex = this.PIATConsignments.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialogDelete = true
},
deleteItemConfirm () {
this.PIATConsignments.splice(this.editedIndex, 1)
this.closeDelete()
},
close () {
this.dialogEdit = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
closeDelete () {
this.dialogDelete = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
save () {
if (this.editedIndex > -1) {
Object.assign(this.PIATConsignments[this.editedIndex], this.editedItem)
}
else {
this.PIATConsignments.push(this.editedItem)
}
this.close()
},
}
}
</script>
After hour trying.
Yes. Adding #click event on the button is right way.
After this just need manipulate with "current" row data.
methods: {
...
newItem() {
this.$store.dispatch('our-dispatch-method')
.then(res => {
res.id // For example we created element in our api and response is new ID.
// Now push to the table item list editedItem and set the index
this.PIATConsignments.push(this.editedItem)
this.editedIndex = this.PIATConsignments.indexOf(this.editedItem)
// Set your ID from api inside editedItem
this.editedItem.id = res.id;
})
}
...
}

Grabbing data from multiple child components Vue js

I'm breaking my head for a few days now, trying to figure out how to grab the data from child components.
Situation is like this.
I have one parent component called Post where user can select date, title, description and which can contain multiple instances of Event compontents.
Event component contains fields like title, description, attendees.
User should be able to add multiple Eventcomponents which means I have multiple components Event within the Post component.
So, I can't figure out how can I compose my components to have an array of Event objects inside my Post component which I can later on send to my API.
the structure of the post object I need is:
// Post.vue
{
"date": '',
"name": '',
"description": '',
"events": {
{
"title": '',
"description": '',
"attendees": ''
},
{
"title": '',
"description": '',
"attendees": ''
}
}
}
So, I don't know should and how I would use vuex for it. I've tried using $emit to pass the data but I couldn't find it fit to get the data into Post model.
Can someone point me where should I look for it?
EDIT #1: Added sample code
The code for the components:
<template>
<v-form>
<v-container>
<v-row>
<v-col
cols="12"
md="4"
>
<v-date-picker v-model="post.date" scrollable>
<v-spacer />
<v-btn text color="primary" #click="modal = false">
Cancel
</v-btn>
<v-btn text color="primary" #click="$refs.dialog.save(date)">
OK
</v-btn>
</v-date-picker>
</v-col>
<v-col
cols="12"
md="4"
>
<v-text-field
v-model="post.name"
label="name"
required
/>
</v-col>
<v-col
cols="12"
md="4"
>
<v-textarea
v-model="post.description"
name="description"
label="Description"
dense
value
rows="4"
hint
/>
</v-col>
</v-row>
<v-row>
<v-btn primary rounded #click="addLine">
Add Event
</v-btn>
<v-expansion-panels accordion>
<UserEvent
v-for="(line, index) in lines"
:key="index"
#addLine="addLine"
#removeLine="removeLine(index)"
/>
</v-expansion-panels>
</v-row>
</v-container>
</v-form>
</template>
<script>
import UserEvent from './partials/event'
export default {
name: 'Post',
components: { UserEvent },
data () {
return {
post: [],
lines: [],
blockRemoval: true
}
},
watch: {
lines () {
this.blockRemoval = this.lines.length <= 1
}
},
mounted () {
},
methods: {
addLine () {
const checkEmptyLines = this.lines.filter(line => line.number === null)
if (checkEmptyLines.length >= 1 && this.lines.length > 0) { return }
this.lines.push({
title: null,
description: null,
attendees: null
})
},
removeLine (lineId) {
if (!this.blockRemoval) { this.lines.splice(lineId, 1) }
}
}
}
</script>
And the child component UserEvent
// UserEvent.vue
<template>
<v-expansion-panel>
<v-expansion-panel-header>Event details</v-expansion-panel-header>
<v-expansion-panel-content>
<v-row>
<v-col cols="12" md="6">
<v-text-field
v-model="event.title"
label="Title"
required
/>
</v-col>
<v-col
cols="12"
md="6"
>
<v-text-field
v-model="event.atttendees"
label="Atendees"
required
/>
</v-col>
<v-col
cols="12"
md="12"
>
<v-textarea
v-model="event.description"
name="description"
label="Description"
dense
value
rows="4"
hint
/>
</v-col>
<v-col
cols="12"
md="3"
>
<div class="block float-right">
<v-btn #click="removeLine(index)" />
<v-btn v-if="index + 1 === lines.length" #click="addLine" />
</div>
</v-col>
</v-row>
</v-expansion-panel-content>
</v-expansion-panel>
</template>
<script>
export default {
name: 'UserEvent',
props: ['line', 'index'],
data () {
return {
event: []
}
},
methods: {
addLine () {
this.$emit('addLine')
},
removeLine (index) {
this.$emit('removeLine', index)
}
}
}
</script>
Here's an example with a similar structure what was posed in the question:
{
name: String,
events: [
title: String,
description: String,
],
}
This example allows the user to open a form to add a new event. When that form is submitted, the event data is added to the parent component's state.
Parent
<template>
<div>
<input v-model="name" />
<ul v-if="events.length">
<li v-for="(event, index) in events" :key="index">
<span>{{ event.title }}</span>
<span>{{ event.description }}</span>
</li>
</ul>
<Event v-if="isNewEventFormVisible" #submit="addEvent" />
<button v-else #click="showNewEventForm">add event</button>
</div>
</template>
import Event from '~/components/Event';
export default {
components: { Event },
data() {
return {
name: 'Example Post',
events: [],
isNewEventFormVisible: false,
};
},
methods: {
addEvent({ title, description }) {
this.isNewEventFormVisible = false;
this.events.push({ title, description });
// TODO: call you API here to update
},
showNewEventForm() {
this.isNewEventFormVisible = true;
},
},
};
Event
<template>
<form #submit.prevent="onSubmit">
<input v-model.trim="title" type="text" />
<br />
<textarea v-model.trim="description" />
<button type="submit">submit</button>
</form>
</template>
export default {
data() {
return {
title: '',
description: '',
};
},
methods: {
onSubmit() {
this.$emit('submit', {
title: this.title,
description: this.description,
});
},
},
};
You could imagine a more sophisticated version of this where events are editable. In that case, each Event could take props and bind them as values to its input instead of using v-models.

v-btn of v-dialog disappears when adding v-bind to attribute

With Vuetify, I created a dialog in which I want to bind placeholders of my different v-text-field.
<template>
<v-row justify="center">
<v-dialog v-model="dialog" persistent max-width="600px">
<template v-slot:activator="{ on }">
<v-btn text icon dark v-on="on" color="black">
<v-icon>mdi-pencil-outline</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title>
<span class="headline">Edit profile</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="f_name" :label="getName()" required></v-text-field>
</v-col>
</v-row>
</v-container>
</v-card-text>
</v-card>
</v-dialog>
</v-row>
</template>
<script>
export default {
name: "edit_form_personal",
data() {
return {
dialog: false,
f_name: '',
l_name: '',
email: '',
phone: ''
}
},
methods: {
getName() {
return 'joe'
}
},
}
</script>
I am using getName() to bind 'joe' inside my form.
The issue is that when I change
<v-text-field v-model="f_name" label="First Name" required></v-text-field>
to :
<v-text-field v-model="f_name" :label="getName()" required></v-text-field>
the <v-btn> disappears as if the v-dialog was broken.
Move the getName function from methods to computed property
see the working codepen here: https://codepen.io/chansv/pen/xxxOJGq?editors=1010
<script>
export default {
name: "edit_form_personal",
data() {
return {
dialog: false,
f_name: '',
l_name: '',
email: '',
phone: ''
}
},
computed: {
getName() {
return 'joe'
}
},
}
</script>
also use getName instead of getName() in text field and :label instead of :v-label
<v-text-field v-model="f_name" :label="getName" required></v-text-field>

Vuetify Data Table does not reload on state update

I would like to kindly ask you about Vuetify and its Data Table. I am using Vuex for state managing. When I open the page Vuex loads all the data from API. It works perfectly. Then I have the Data Table which also works (update, add, delete), but only one column does not... Data Table is filled with GET_CENTERS and the "one column" is an object (center.customer). When I change center.customer.name state is updated, database is updated, but the value in Data Table is not. (I have tried to just create a list of customers on same page and this list changes if :key is set).
CentersTable
<template>
<div>
<v-data-table
:headers="headers"
:items="GET_CENTERS"
sort-by="name"
class="elevation-1"
>
<template v-slot:top>
<v-toolbar flat color="white">
<v-toolbar-title>My CRUD</v-toolbar-title>
<v-divider
class="mx-4"
inset
vertical
></v-divider>
<div class="flex-grow-1"></div>
<v-dialog v-model="dialog" max-width="500px">
<template v-slot:activator="{ on }">
<v-btn color="primary" dark class="mb-2" v-on="on">Nový Záznam</v-btn>
</template>
<v-card>
<v-card-title>
<span class="headline">{{ formTitle }}</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12" sm="6" md="6">
<v-text-field v-model="editedItem.name" label="Název"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field v-model="editedItem.address" label="Adresa"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field v-model="editedItem.city" label="Město"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field v-model="editedItem.zip" label="PSČ"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field v-model="editedItem.email" label="Email"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field v-model="editedItem.phone" label="Telefon"></v-text-field>
</v-col>
<v-col cols="12" sm="12" md="12">
<v-combobox
v-model="editedItem.customer"
:items="GET_CUSTOMERS"
:search-input.sync="search"
item-text="name"
item-value="_id"
:hide-selected="hideSelected"
label="Zákazník"
persistent-hint
:clearable="clearable"
return-object
>
<template v-if="noData" v-slot:no-data>
<v-list-item>
<v-list-item-content>
<v-list-item-title>
No results matching "<strong>{{ search }}</strong>". Press <kbd>enter</kbd> to create a new one
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
</v-combobox>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<div class="flex-grow-1"></div>
<v-btn color="blue darken-1" text #click="close">Cancel</v-btn>
<v-btn color="blue darken-1" text #click="save">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-toolbar>
</template>
<template v-slot:item.action="{ item }">
<v-icon
small
class="mr-2"
#click="editItem(item)"
>
edit
</v-icon>
<v-icon
small
#click="deleteItem(item)"
>
delete
</v-icon>
</template>
</v-data-table>
<template>
<v-list-item v-for="item in GET_CUSTOMERS" :key="item._id">
<v-list-item-content>{{item}}</v-list-item-content>
</v-list-item>
</template>
</div>
</template>
<script>
import {mapActions, mapGetters} from 'vuex'
export default {
name: "CentersTable",
data: () => ({
dialog: false,
editedItem: {},
headers: [
{
text: 'Název',
align: 'left',
sortable: true,
value: 'name',
},
{
text: 'Adresa',
sortable: true,
value: 'address',
},
{
text: 'Město',
sortable: true,
value: 'city',
},
{
text: 'PSČ',
sortable: true,
value: 'zip',
},
{
text: 'Email',
sortable: true,
value: 'email',
},
{
text: 'Telefon',
sortable: true,
value: 'phone',
},
{
text: 'Zákazník',
sortable: true,
value: 'customer.name',
},
{ text: 'Akce', value: 'action', sortable: false, align: 'right' },
],
search: null,
chips: true,
multiple: true,
hideSelected: true,
noData: true,
clearable: false,
}),
watch: {
dialog (val) {
val || this.close()
},
},
computed: {
formTitle() {
return this.editedItem._id === undefined ? 'Nový Záznam' : 'Upravit Záznam'
},
...mapGetters([
'GET_CUSTOMERS',
'GET_CENTERS'
]),
},
methods: {
...mapActions([
'createCenter',
'updateCenter',
'deleteCenter',
]),
editItem (item) {
this.editedItem = Object.assign({}, item)
this.dialog = true
},
deleteItem (item) {
confirm('Opravdu chcete smazat tento záznam?') && this.deleteCenter(item).then(() => {
this.$noty.success('Záznam byl smazán')
}).catch(() => {
this.$noty.alert('Při mazání záznamu došlo k chybě')
})
},
close () {
this.dialog = false
this.editedItem = {}
setTimeout(() => {
}, 300)
},
save () {
if (this.editedItem._id === undefined) {
this.createCenter(this.editedItem).then(() => {
this.$noty.success('Nový zákazník byl vytvořen')
}).catch(() => {
this.$noty.alert('Při vytváření zákazníka došlo k chybě')
})
} else {
this.updateCenter(this.editedItem).then(() => {
this.$noty.success('Záznam byl upraven')
}).catch(() => {
this.$noty.alert('Při ukládání záznamu došlo k chybě')
})
}
this.close()
},
}
}
</script>
<style scoped>
</style>
And Vuex store
import axios from "axios";
import Vue from "vue";
const state = {
customers: []
};
const mutations = {
SET_CUSTOMERS(state, payload) {
state.customers = payload;
},
UPDATE_CUSTOMER(state, payload) {
const customer = state.customers.findIndex(x => x._id === payload._id);
Vue.set(state.customers, customer, payload);
},
ADD_CUSTOMER(state, payload) {
state.customers.push(payload);
},
REMOVE_CUSTOMER(state, payload) {
const customer = state.customers
.map(customer => customer._id)
.indexOf(payload);
state.customers.splice(customer, 1);
}
};
const actions = {
loadCustomers({ commit }) {
axios.get("/api/customers").then(data => {
commit("SET_CUSTOMERS", data.data);
});
},
updateCustomer({ commit }, payload) {
axios.put(`/api/customers/${payload._id}`, payload).then(response => {
commit("UPDATE_CUSTOMER", response.data);
});
},
createCustomer({ commit }, payload) {
axios.post("/api/customers", payload).then(data => {
commit("ADD_CUSTOMER", data.data);
});
},
deleteCustomer({ commit }, payload) {
axios.delete(`/api/customers/${payload._id}`).then(() => {
commit("REMOVE_CUSTOMER", payload);
});
}
};
const getters = {
GET_CUSTOMERS: state => state.customers
};
export default {
state,
mutations,
actions,
getters
};