I have a Modal component in Vue.
This Modal component has a few form fields in it:
<template>
<div :class="visible">
<input type="text" v-model="form.name">
<input type="text" v-model="form.email">
<input type="text" v-model="form.pass">
<button #click="sendForm">Submit</button>
<button #click="closeModal">Close</button>
</div>
</template>
<script>
export default {
data() {
return {
visible: false,
form: {
name: '',
email: '',
pass: ''
}
}
},
methods: {
sendForm() {
// Send form
},
showModal() {
this.visible = true
},
closeModal() {
this.visible = false
}
},
}
</script>
When the modal becomes visible, I am showing the form fields to the user. Let's say the user fills out the form fields but does not click on Submit. Instead, he closes the modal.
In the case, I want to reset the modal completely to its original form. Basically destroy the component. Then re-initialize it when the user clicks on the button to make it visible.
When the modal becomes visible again, the component should be a new instance and all the component data should be clear.
What I tried: I tried to clear the form values in the component on close. However, with more complicated forms/components it could become hard to reset each and every data property to their original state.
How can I destroy the component and re-create it? Are there any best practices for this situation?
You can create method to reset component data:
reset() {
Object.assign(this.$data, this.$options.data())
}
And call it, when you close modal.
Be carefull! It's works only on data properties.
Related
Say you have 3 components:
<Modal>
<Navbar>
<Hero>
Your Modal component has data saying whether it's open or not, along with the appropriate methods:
data() {
return {
active: false,
}
},
methods: {
open() {this.active = true},
close() {this.active = false},
switch() {this.active ? this.close(): this.open()}
}
and you want a link in your Navbar component to be able to open it:
template:
/*html*/
`<nav class="navbar">
<router-link :to="etc.">Home</router-link>
<router-link :to="etc.">About</router-link>
<a #click="openSiblingModalSomehow">Contact</a> <!-- This one -->
</nav>`
As well as the Call to Action button on your Hero component:
template:
/*html*/
`<div class="hero">
<h1>Hello, World</h1>
<button #click="openSiblingModelSomehow">Contact Me</button>
</div>`
Assuming you DON'T want a global property to access this... For example, what if you want more than one type of modal?:
<ContactModal>
<SignUpModal>
<OtherModal>
<Navbar>
<Hero>
and knowing that the Modal also needs to be able to close itself,
How would you trigger a specific sibling element / component to open the Modal (in this case, let's say ContactModal) using Vue 3?
I thought about using a variable on the App itself, but it seems a bit hectic to change a globalProperty only for a specific component with it's own data.
I had a similar challenge at my project. My approach was to not use a Boolean property.
Step by step, first declare a empty string at the parent, that provides it for your modal boxes:
data() {
return {
active: ""
}
}
Declare a method, that handles that string:
methods: {
switchActive(string) {
if (string) {
this.active = string;
}
else {
this.active = ""
}
}
}
This would be one of your modal components:
<template>
<Dialog header="Header" footer="Footer" :visible="checkActive">
I am the modal dialog.
<button #click="this.$emit('close')">Close Me</button>
</Dialog>
</template>
<script>
export default {
name: "modal-123",
props: {
active: String
},
computed: {
checkActive() {
return this.active === this.$options.name;
}
}
}
</script>
And call this component:
<modal :active="active" #close="switchActive('')"></modal>
If you want to open one of your modal boxes, you call switchActive with the name property of your modal box.
Scenario / context
I have an overview component which contains a table and an add button. The add button opens a modal component. When i fill in some text fields in the modal and click the save button, a callback (given as prop) is called so the parent component (the overview) is updated. The save button also triggers the model toggle function so the model closes.
So far works everything like expected but when i want to add a second entry, the modal is "pre-filled" with the data of the recently added item.
Its clear to me that this happens because the model component keeps mounted in the background (so its just hidden). I could solve this by "reset" the modals data when the toggle function is triggered but i think there should be a better way.
I have a similar issue when i want to fetch data in a modal. Currently i call the fetch function in the mounted hook of the modal. So in this case the fetch happens when the parent component mounts the modal. This does not make sense as it should only (and each time) fetch when the modal is opened.
I think the nicest way to solve this is to mount the modal component dynamically when i click the "add" (open modal) button but i can't find how i can achieve this. This also avoids that a lot of components are mounted in the background which are possibly not used.
Screenshot
Example code
Overview:
<template>
<div>
// mount of my modal component
<example-modal
:toggleConstant = modalToggleUuid
:submitHandler = submitHandler />
// The overview component HTML is here
</div>
</template>
<script>
export default {
data() {
return {
modalToggleUuid: someUuid,
someList: [],
}
},
mounted() {
},
methods: {
showModal: function() {
EventBus.$emit(this.modalToggleUuid);
},
submitHandler: function(item) {
this.someList.push(item);
}
}
}
</script>
Modal:
<template>
<div>
<input v-model="item.type">
<input v-model="item.name">
<input v-model="item.location">
</div>
</template>
<script>
export default {
data() {
return {
modalToggleUuid: someUuid,
item: {},
}
},
mounted() {
// in some cases i fetch something here. The data should be fetched each time the modal is opened
},
methods: {
showModal: function() {
EventBus.$emit(this.modalToggleUuid);
},
submitHandler: function(item) {
this.someList.push(item);
}
}
}
</script>
Question
What is the best practive to deal with the above described scenario?
Should i mount the modal component dynamically?
Do i mount the component correctly and should i reset the content all the time?
You are on the right way and in order to achieve what you want, you can approach this issue with v-if solution like this - then mounted() hook will run every time when you toggle modal and it also will not be present in DOM when you are not using it.
<template>
<div>
// mount of my modal component
<example-modal
v-if="isShowModal"
:toggleConstant="modalToggleUuid"
:submitHandler="submitHandler"
/>
// The overview component HTML is here
</div>
</template>
<script>
export default {
data() {
return {
isShowModal: false,
modalToggleUuid: someUuid,
someList: []
};
},
mounted() {},
methods: {
showModal: function() {
this.isShowModal = true;
},
submitHandler: function(item) {
this.someList.push(item);
this.isShowModal = false;
}
}
};
</script>
I try to detect when user change/insert into an input and he try to change/close page to give him a warning. I do some research but till now I didn't find anything.
<b-form-group label="Name" label-for="name-input">
<b-form-input
id="name-input"
v-model="name"
></b-form-input>
</b-form-group>
created() {
document.addEventListener('beforeunload', this.handlerClose)
},
handlerClose: function handler(event) {
console.log('CHANGE!!!!');
},
Detect navigating to a different page or close the page
You can try using the same eventhandler beforeunload on the window object, not the document object, as stated in the MDN Web Docs for example ( https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event ). The event should handle both cases, switching page and closing page.
<script>
export default {
created() {
window.addEventListener('beforeunload', (event) => {
// Cancel the event as stated by the standard.
event.preventDefault();
// Chrome requires returnValue to be set.
event.returnValue = '';
});
}
}
</script>
This event enables a web page to trigger a confirmation dialog asking the user if they really want to leave the page. If the user confirms, the browser navigates to the new page, otherwise it cancels the navigation.
About your second question to detect whetever changes has been made : This eventhandler does not detect changes.
In order to mantain a state whetever the user made a change, e.g. to a form, I would outsource this state with a data prop isChanged and initialize it with false. Then use Vue directives v-on:change or v-on:input to change the prop from false to true.
<template>
<div>
<input type="text" #change="userMadeChange" v-model="inputText" />
</div>
</template>
<script>
export default {
data() {
return {
inputText : "",
isChanged : false
}
},
methods : {
userMadeChange() {
this.isChanged = true;
}
}
}
</script>
The easier way is to simply compare the stringified JSON of your selected data. If they are equivalent, then we know that the data has not been changed/updated/mutated by the user.
Here's a simple setup:
Create a method that generates the JSON for the user data that you want to observe for changes.
When the compoonent/app is created, you cache the data that it is created with and store/cache it
Create a computed property that simply returns the current state of the user data and cached user data
In the beforeunload handler, you can then check the returned value of this computed property to determine of the user has mutated data or not.
See proof-of-concept below:
new Vue({
el: '#app',
// COMPONENT DATA
data: {
// Dummy data
firstName: 'John',
lastName: 'Doe',
// Cache form data
cachedFormData: null,
},
// COMPONENT LIFECYCLE HOOK
created: function() {
// Create a cache when component/app is created
this.cachedFormData = this.formDataForComparison();
document.addEventListener('beforeunload', this.handlerClose);
},
// COMPUTED PROPERTIES
computed: {
// Compares cached user data to live data
hasChanged() {
return this.cachedFormData !== this.formDataForComparison();
}
},
// COMPONENT METHODS
methods: {
// Callback handler
handlerClose: function() {
if (this.hasChanged) {
// Logic when change is detected
// e.g. you can show a confirm() dialog to ask if user wants to proceed
} else {
// Logic when no change is detected
}
},
// Helper method that generates JSON for string comparison
formDataForComparison: function() {
return JSON.stringify({
firstName: this.firstName,
lastName: this.lastName
});
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
<br />
<br />
<span>Has user changed data? <strong>{{ hasChanged }}</strong></span>
</div>
An alternative method would be simply storing a flag that has a ground state of false, as proposed by the other answer. The flag state is switched to true wheneveran input/change event is detected on the element. However, there are several disadvantages associated with this method:
Even when the user undo his/her changes, it will still marked as changed. This constitutes a false positive.
You will need to either bind watchers to all the v-model members, or bind input/change event listeners to all input elements on the page. If your form is huge, there is a chance that you will forget to do this to an input element.
parent component: Groups
child component: Modal
in Groups, I have Modal as a component.
<modal
v-if="$store.getters.getClusters"
id="editIndFeedbackModal"
title="Edit group feedback"
:atok="atok"
:feedback="$store.getters.getCurrentClusterFeedback"
:hint="$store.getters.getCurrentClusterHint"
:grade="$store.getters.getCurrentClusterGrade"
:nextAss="$store.getters.getCurrentClusterNextAss"
:recRead="$store.getters.getCurrentClusterRecRead"
:link="$store.getters.getCurrentClusterLink"
:clusterName="$store.getters.getCurrentClusterName"
:clusterId="$store.getters.getCurrentClusterId"
aria-labelledby="textEditClusterName"
/>
In Modal, I have a bootstrap modal.
props: [
'id',
'ariaLabelledby',
'atok',
'file_id_arr',
'feedback',
'hint',
'grade',
'nextAss',
'recRead',
'link',
'name',
'clusterId',
'clusterName',
],
and used in Modal template as follows:
<div class="form-group">
<textarea v-model="feedback" name="text_feedback" id="text_feedback"
class="form-control" style="resize: vertical;" rows="3" placeholder="">
</textarea>
<label for="text_feedback">Feedback</label>
</div>
Since Modal saves this input form, I am using v-model to change the prop and thus getting the mutating props warning.
What I've tried:
I tried assigning the props to local data on Modal comp.:
data: function () {
return {
currentFeedback: {
atok: this.$props.atok,
feedback: this.$props.feedback,
},
}
},
atok comes back great since it exists throughout the lifecycle, yet feedback is empty since feedback comes from an async operation, where this whole issue is stemming from.
the async operation is dispatched to vuex via Groups mounted():
getAssFiles: function () {
if (this.atok) {
this.$store.dispatch('GET_ASSIGNMENT_FILES', {
assign_token: this.atok
});
}
Thank you kindly.
p.s.
this firm use allot of abbreviations, so ass = assignment..
EDIT
I've done this:
data: function () {
return {
currentFeedback: {
feedback: this.migrateFeedback,
},
}
},
computed:{
migrateFeedback(){
this.currentFeedback.feedback = this.$props.feedback
}
same result, yet when using the vue console, when I click to open the data tab, it suddenly updates view and now it's there, any idea how to solve this erroneous issue?
On a page I have a Vuetify v-form/v-text-field. Users can type a query in to this form and press Enter to update the page.
Sometimes the query is erroneous and a v-dialog modal pops up to explain the error. It's a complex error and can't just be packed in the rules attribute of the v-text-field.
The problem: when the modal is dismissed, the v-text-field is no longer focused and requires a click before it can be used again. This interrupts the user and requires a shift from keyboard to mouse and back.
What is Vue best practice for re-focusing on the input that triggered the modal?
I can think of one idea it doesn't feel Vue-ish: put a ref on the v-text-field and watch the dialog data property. If dialog becomes false, call this.$refs.input.focus(). However, this seems like the old-fashioned, imperative (not reactive) way to do it. For example:
<template>
<v-form v-model='valid' #submit.prevent='submit'>
<v-text-field
placeholder="Search..."
v-model="query"
:rules="[foo]"
ref='input'
autofocus
></v-text-field>
<v-dialog
v-model="dialog"
#keydown.esc='dialog = false'
>
{{ someErrorMessage }}
</v-dialog>
</v-form>
</template>
// Vue instance
export default {
data: function() {
return {
dialog: false,
query: "",
valid: false
}
},
...
watch: {
// Focus on query after dismissing an error
dialog(newState) {
if (!newState) {
this.$refs.input.focus();
}
}
},
methods: {
foo(value) {
// ... validate value
},
submit(e) {
if (!this.valid) {
this.dialog = true;
return;
}
}
}
}
In the dialog element, I'd probably call a method instead of directly changing dialog to false. Then in that method, I'd add focus to the textarea with some plain old javascript.