how to implement a reusable vuetify dialog - vue.js

I want to create a reusable v-dialog with vuetify,
I created the BaseModal:
<v-dialog
v-model="inputVal"
:hide-overlay="overlay"
:max-width="width"
>
<v-card>
<v-toolbar flat>
<v-btn color="black" dark icon right #click="closeModal">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-spacer></v-spacer>
<v-toolbar-title id="toolbar-title">{{ title }}</v-toolbar-title>
</v-toolbar>
<v-card-title class="headline">
<!--Card Title-->
</v-card-title>
<v-card-text>
<slot name="content"></slot>
</v-card-text>
</v-card>
</v-dialog>
InputVal as computed and dataBindingName as props :
computed: {
inputVal: {
get() {
return this.dataBindingName;
},
set(val) {
this.$emit("input", val);
}
}
},
props: {
dataBindingName: {
Boolean,
required: false
},}
I'm calling this base component in another component like this:
<modal-simple
:dataBindingName="dataBindingName"
title="Nouveau"
width="418"
>
<template v-slot:content>
<add-time-sheet-form />
</template>
</modal-simple>
My problem now is how to implement correctly the closeModal and how to implement a button inside that close the modal and open a new modal

Part 1. Modal component.
Take care that you should not use v-model="dialog". As already described in this answer, you need to replace it with :value and #input.
Moreover, you should not mutate dialog prop directly, that's why you need to emit close-dialog event when you need to close your modal.
<template>
<v-dialog
:value="dialog"
#input="$emit('input', $event)"
max-width="500px"
persistent
>
<v-card>
<v-card-title>
... dialog header ...
</v-card-title>
<v-card-text>
... dialog content ...
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn #click="close">
Close
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
props: {
editedId: Number,
dialog: Boolean,
},
methods: {
close() {
this.$emit("close-dialog");
},
},
};
</script>
Part 2. Parent component.
Imagine you want to use your component when adding or editing entities.
You need to implement two methods: addItem and editItem to open your dialog. You also need to pass extra entity props (editedId in my example), dialog prop with sync modifier and close-dialog event handler that was emitted from modal component.
<template>
<div>
<v-toolbar flat color="white">
<v-spacer />
<edit-modal
:edited-id="editedId"
:dialog.sync="dialog"
#close-dialog="
editedId = null;
dialog = false;
"
/>
<v-btn color="primary" dark class="mb-2" #click="addItem">
Add entity
</v-btn>
</v-toolbar>
<v-data-table :items="items" :headers="headers">
<template v-slot:item="props">
<tr>
<td>{{ props.item.name }}</td>
<td class="justify-center text-center">
<v-icon small class="mr-2" #click="editItem(props.item)">
edit
</v-icon>
</td>
</tr>
</template>
</v-data-table>
</div>
</template>
<script>
import Dialog from "./ReusableDialog";
export default {
components: {
"edit-modal": Dialog,
},
data() {
return {
items: [
... some items ...
],
headers: [
... some headers ...
],
editedId: null,
dialog: false,
};
},
methods: {
addItem() {
this.dialog = true;
},
editItem(item) {
this.editedId = item.id;
this.dialog = true;
},
},
};
</script>
Part 3. Modifying modal to reopen automatically.
In this example, modal component will reopen automatically until editedId will not be set.
In modal component:
...
close() {
this.$emit("close-dialog");
if (this.editedId === null) {
this.$emit("open-dialog");
}
},
...
In parent component:
...
<edit-modal
... some props ...
#open-dialog="
editedId = 999;
dialog = true;
"
/>
...
There is a codesandbox with working example.

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 reset form after submitting

I am using a form inside dialog using vuetify.
Imported the component in page like this -
<template>
<div>
<topicForm :dataRow="dataRow" v-model="dialog" />
</div>
</template>
methods: {
openDialog(item = {}) {
this.dataRow = item;
this.dialog = true;
},
}
Dialog form code -->
<template>
<div>
<v-dialog v-model="value" max-width="500px" #click:outside="close">
<v-card outlined class="pt-5">
<v-form ref="form" class="px-3">
<v-card-text class="pt-5">
<v-row no-gutters>
<v-text-field
required
outlined
label=" Name"
v-model="data.name"
:rules="[rules.required]"
></v-text-field>
</v-row>
<v-row no-gutters>
<v-textarea
required
outlined
label=" Description"
v-model="data.description"
></v-textarea>
</v-row>
</v-card-text>
</v-form>
<v-divider> </v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
large
dark
outlined
color="success"
#click="save"
class="ma-3"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
props: [
"dataRow",
"value",
// ----------------------
],
methods: {
save() {
if (this.$refs.form.validate()) {
this.$root
.$confirm("Are you sure you want to save?")
.then((confirm) => {
if (confirm) {
this.ADD_TOPIC_DATA(this.data)
.then((data) => {
this.FETCH_TOPIC_DATA();
this.$refs.form.reset();
this.$refs.form.resetValidation();
this.close();
})
.catch((err) => {
console.log(err)
});
}
});
}
},
close() {
this.$emit("input", false);
},
}
watch: {
dataRow(val) {
this.data = { ...val };
},
},
Problem I am having is after adding a data, then if I try to add again by opening the dialog, the required field shows validation error, which is name here!
Image of that -->
Searched in stackoverflow. Found that should use this.$refs.form.reset(). Used that in save method without success. Also used this.$refs.form.resetValidation(), but don't work.
Any suggestion?
Thanks in advance.
The problem here is you're assigning new value to dataRow when opening the dialog which triggers validation inside the dialog. You could also use lazy-validation prop which allows you to only manually trigger the validation.

How to get the right content to be displayed with buttons? (Vue/vuetify)

I'm trying to use v-if and v-show to display the contents of two different pages. If you click on button A, the table for page A is displayed, and if you click on button B, the table for page B is displayed. But with the code I have now, both pages are displayed when clicking on button B. How can I get the right page depending on the button that is clicked? I'm thinking of using v-if/else but I'm not sure how.
My code:
<template>
<v-main>
<v-row align="center" justify="space-around">
<v-col class="text-center" cols="10" sm="7">
<v-btn v-on:click="first= !first">View first table</v-btn>
<p v-show="first"><FirstTable/></p>
<v-btn v-on:click="second= !second">View second table</v-btn>
<p v-show="second"><SecondTable/></p>
</v-col>
</v-row>
</v-main>
</template>
<script>
import FirstTable from '#/pages/main-page/FirstTable'
import SecondTable from '#/pages/main-page/SecondTable'
export default {
name: 'MainPage',
components: {
FirstTable,
SecondTable
},
data() {
return {
first: false,
second: false
}
},
}
This is because you are not setting first to false again. anyway heres a way of doing it.
<template>
<v-main>
<v-row align="center" justify="space-around">
<v-col class="text-center" cols="10" sm="7">
<v-btn #click="toggleTable('first')">View first table</v-btn>
<FirstTable v-if="activeTable === 'first'"/>
<v-btn #click="toggleTable('second')">View second table</v-btn>
<SecondTable v-if="activeTable === 'second'"/>
</v-col>
</v-row>
</v-main>
</template>
<script>
import FirstTable from '#/pages/main-page/FirstTable'
import SecondTable from '#/pages/main-page/SecondTable'
export default {
name: 'MainPage',
components: {
FirstTable,
SecondTable
},
data() {
return {
activeTable: null,
}
},
methods: {
toggleTable(val) {
this.activeTable = val;
}
},
};

"Maximum call stack size exceeded" error using v-dialog component twice on the same view with Vue Observable

When I open and close the v-dialog I get:
VDialog.ts:238 Uncaught RangeError: Maximum call stack size exceeded.
at VueComponent.onFocusin (VDialog.ts:238)
at VueComponent.onFocusin (VDialog.ts:238)
...
My view includes the same component twice as you can see below. Each uses a different slot. If I include the component once I do not get this error.
How to replicate the error:
1. Open the dialog
2. click close on on the dialog card
3. re-open the dialog
4. Click close again
5. The error appears
Main.vue
<CreateProjectDialog>
<template v-slot:default="slotProps">
<v-btn v-on="slotProps.dialog.on" color="primary" class="white--text">
Project <v-icon>mdi-plus</v-icon>
</v-btn>
</template>
</CreateProjectDialog>
<CreateProjectDialog>
<template v-slot:default="slotProps">
<v-btn
v-on="slotProps.dialog.on"
class="ma-5 mr-md-12"
fixed
x-large
fab
bottom
right
color="primary"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</template>
</CreateProjectDialog>
CreateProjectDialog component
<template>
<div>
<v-dialog v-model="dialog" max-width="600px">
<template v-slot:activator="{ on }">
<slot v-bind:dialog="{ on }"></slot>
</template>
<v-form ref="form" v-model="valid">
<v-card>
<v-card-title class="justify-center">
</v-card-title>
<v-card-text>
</v-card-text>
<v-card-actions>
<v-btn color="blue darken-1" text #click="dialog = false">Close</v-btn>
<v-btn color="blue darken-1 white--text" #click="submitProject" :disabled="!valid">Create</v-btn>
</v-card-actions>
</v-card>
</v-form>
</v-dialog>
</div>
</template>
I use this computed property to calculate the dialog value through an observer in store.js
createProjectDialog component
computed: {
dialog: {
get () {
return getters.projectDialog();
},
set (val) {
mutations.toggleProjectDialog(val);
}
}
},
store.js
import Vue from 'vue';
const state = Vue.observable({
projectDialog: false
});
export const getters = {
projectDialog () {
return state.projectDialog;
}
};
export const mutations = {
toggleProjectDialog (val) {
state.projectDialog = val;
}
};
If I remove the computed property dialog entirely, and instead return a normal data property it works.
export default {
data () {
return {
dialog: false
}
}
}
I want to understand why this happens exactly and how I can make it work with the observer.
Is there a better way of handling this instead of using slots?

Vue components data and methods disappear on one item when rendered with v-for as Vuetify's cards

I have Vue component that renders a list of Vuetify cards:
<restaurant-item
v-for="card in userRestaurantCards"
:key="card['.key']"
:card="card"
>
</restaurant-item>
The card displays info obtained from props, Vuex, as well as info defined in the restaurant-item card itself:
<v-card>
<v-img
class="white--text"
height="200px"
:src="photo"
>
<v-container fill-height fluid class="card-edit">
<v-layout fill-height>
<v-flex xs12 align-end flexbox>
<v-menu bottom right>
<v-btn slot="activator" dark icon>
<v-icon>more_vert</v-icon>
</v-btn>
<v-list>
<edit-restaurant-dialog :card="card" :previousComment="comment"></edit-restaurant-dialog>
<v-list-tile >
<v-list-tile-title>Delete</v-list-tile-title>
</v-list-tile>
</v-list>
</v-menu>
</v-flex>
</v-layout>
</v-container>
</v-img>
<v-card-title>
<div>
<span class="grey--text">Friends rating: {{ card.rating }}</span><br>
<h3>{{ card.name }}</h3><br>
<span>{{ card.location }}</span>
</div>
</v-card-title>
<v-card-actions>
<v-btn flat color="purple">Comments</v-btn>
<v-spacer></v-spacer>
<v-btn icon #click="show = !show">
<v-icon>{{ show ? 'keyboard_arrow_down' : 'keyboard_arrow_up' }}</v-icon>
</v-btn>
</v-card-actions>
<v-slide-y-transition>
<v-card-text v-show="show">
<div> {{ comment.content }} </div>
</v-card-text>
</v-slide-y-transition>
</v-card>
The script is:
import { find, isEmpty } from 'lodash-es'
import { mapGetters } from 'vuex'
import EditRestaurantDialog from '#/components/dashboard/EditRestaurantDialog'
export default {
name: 'restaurant-item',
components: {
EditRestaurantDialog
},
props: {
card: Object
},
data() {
return {
show: false,
name: this.card.name,
location: this.card.location,
rating: this.card.rating,
link: this.card.link,
photo: this.getPhotoUrl()
}
},
computed: {
comment() {
// Grab the content of the comment that the current user wrote for the current restaurant
if (isEmpty(this.card.comments)) {
return { content: 'You have no opinions of this place so far' }
} else {
const userComment = find(this.card.comments, o => o.uid === this.currentUser)
return userComment
}
},
...mapGetters(['currentUser'])
},
methods: {
getPhotoUrl() {
const cardsDefault = find(this.card.photos, o => o.default).url
if (isEmpty(cardsDefault)) {
return 'https://via.placeholder.com/500x200.png?text=No+pics+here+...yet!'
} else {
return cardsDefault
}
}
}
}
Here is the kicker: when I have 2 objects in the data, the first card component renders correctly... while the second doesn't have any of the methods or data defined right there in the script.
Here's a link to a screenshot of the Vue Devtools inspecting the first card:
https://drive.google.com/file/d/1LL4GQEj0S_CJv55KRgJPHsCmvh8X3UWP/view?usp=sharing
Here's a link of the second card:
https://drive.google.com/open?id=13MdfmUIMHCB_xy3syeKz6-Bt9R2Yy4Xe
Notice how the second one has no Data except for the route?
Also, note that both components loaded props, vuex bindings and computed properties just as expected. Only the Data is empty on the second one...
I've been scratching my head for a while over this. Any ideas would be more than welcome.
I got it to work after I moved the method getPhotoUrl method to a computed property:
computed: {
comment() {
// Grab the content of the comment that the current user wrote for the current restaurant
if (isEmpty(this.card.comments)) {
return { content: 'You have no opinions of this place so far' }
} else {
const userComment = find(this.card.comments, o => o.uid === this.currentUser)
return userComment
}
},
photoUrl() {
const cardsDefault = find(this.card.photos, o => o.default)
if (isEmpty(cardsDefault)) {
return 'https://via.placeholder.com/500x200.png?text=No+pics+here+...yet!'
} else {
return cardsDefault.url
}
},
...mapGetters(['currentUser'])
}