Detect browser close or page change VueJs - vue.js

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.

Related

How to create and destroy component data with VueJS?

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.

Vue.js this.$refs empty due to v-if

I have a simple Vue component that displays an address, but converts into a form to edit the address if the user clicks a button. The address field is an autocomplete using Google Maps API. Because the field is hidden (actually nonexistent) half the time, I have to re-instantiate the autocomplete each time the field is shown.
<template>
<div>
<div v-if="editing">
<div><input ref="autocomplete" v-model="address"></div>
<button #click="save">Save</button>
</div>
<div v-else>
<p>{{ address }}</p>
<button #click="edit">Edit</button>
</div>
</div>
</template>
<script>
export default {
data() {
editing: false,
address: ""
},
methods: {
edit() {
this.editing = true;
this.initAutocomplete();
},
save() {
this.editing = false;
}
initAutocomplete() {
this.autocomplete = new google.maps.places.Autocomplete(this.$refs.autocomplete, {});
}
},
mounted() {
this.initAutocomplete();
}
}
I was getting errors that the autocomplete reference was not a valid HTMLInputElement, and when I did console.log(this.$refs) it only produced {} even though the input field was clearly present on screen. I then realized it was trying to reference a nonexistent field, so I then tried to confine the autocomplete init to only when the input field should be visible via v-if. Even with this, initAutocomplete() is still giving errors trying to reference a nonexistent field.
How can I ensure that the reference exists first?
Maybe a solution would be to use $nextTick which will wait for your DOM to rerender.
So your code would look like :
edit() {
this.editing = true;
this.$nextTick(() => { this.initAutocomplete(); });
},
Moreover if you try to use your this.initAutocomplete(); during mounting it cannot work since the $refs.autocomplete is not existing yet but I'm not sure you need it since your v-model is already empty.
I think it's because your "refs" is plural
<input refs="autocomplete" v-model="address">
It should be:
<input ref="autocomplete" v-model="address">

Attach v-model to a dynamic element added with .appendChild in Vue.js

I'm working with a library that doesn't have a Vue.js wrapper.
The library appends elements in the DOM in a dynamic way.
I want to be able to bind the v-model attribute to those elements with Vue and once appended work with them in my model.
I've done this in the past with other reactive frameworks such as Knockout.js, but I can't find a way to do it with vue.js.
Any pay of this doing?
It should be something among these lines I assume:
var div = document.createElement('div');
div.setAttribute('v-model', '{{demo}}');
[VUE CALL] //tell vue.js I want to use this element in my model.
document.body.appendChild(div);
You could create a wrapper component for your library and then setup custom v-model on it to get a result on the lines of what you're looking for. Since your library is in charge of manipulating the DOM, you'd have to hook into the events provided by your library to ensure your model is kept up-to-date. You can have v-model support for your component by ensuring two things:
It accepts a value prop
It emits an input event
Here's an example of doing something similar: https://codesandbox.io/s/listjs-jquery-wrapper-sdli1 and a snipper of the wrapper component I implemented:
<template>
<div>
<div ref="listEl">
<ul ref="listUlEl" class="list"></ul>
</div>
<div class="form">
<div v-for="variable in variables" :key="variable">
{{ variable }}
<input v-model="form[variable]" placeholder="Enter a value">
</div>
<button #click="add()">Add</button>
</div>
</div>
</template>
<script>
export default {
props: ["value", "variables", "template"],
data() {
return {
form: {}
};
},
mounted() {
this.list = new List(
this.$refs.listEl,
{
valueNames: this.variables,
item: this.template
},
this.value
);
this.createFormModels();
},
methods: {
createFormModels() {
for (const variable of this.variables) {
this.$set(this.form, variable, "");
}
},
add() {
this.$emit("input", [
...this.value,
{
id: this.value.slice(-1)[0].id + 1,
...this.form
}
]);
}
},
watch: {
value: {
deep: true,
handler() {
this.$refs.listUlEl.innerHTML = "";
this.list = new List(
this.$refs.listEl,
{
valueNames: this.variables,
item: this.template
},
this.value
);
}
}
},
beforeDestroy() {
// Do cleanup, eg:
// this.list.destroy();
}
};
</script>
Key points:
Do your initialization of the custom library on mounted() in order to create the DOM. If it needs an element to work with, provide one via <template> and put a ref on it. This is also the place to setup event listeners on your library so that you can trigger model updates via $emit('value', newListOfStuff).
watch for changes to the value prop so that you can reinitialize the library or if it provides a way to update its collection, use that instead. Make sure to cleanup the previous instance if the library provides support for it as well as unbind event handlers.
Call any cleanup operations, event handler removals inside beforeDestroy.
For further reference:
https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components

Vue - trying to avoid changing props, with no success

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?

Good practice for re-usable form component in Vue

I'm trying to write a re-usable form component for a "user". I'd like to be able to use it in both an "edit" and a "create" flow. I'd pass it a user object, it would validate as a user enters/modifies the user data and it would update the user object. The parent component (e.g EditUser, CreateUser) will do the actual saving.
How should I do this without mutating props? I've avoided using events so far because I'd need to fire one on every user input, passing a copy of the user object back up to the parent (I think).
EDIT: adding some (non-working) code to demonstrate. I can't share my exact code.
Parent component
<template>
<div >
<h1>header</h1>
<MyFormComponent
v-model="user"
>
</MyFormComponent>
<button>Save</button>
</div>
</template>
<script>
export default {
data(){
return {
user: {
name: 'User 1'
}
}
}
}
</script>
Form component
<template>
<form>
<MyFormInputComponent
v-model="user.name"
></MyFormInputComponent>
</form>
</template>
<script>
export default {
props: ['user'],
model: {
prop: 'user'
}
}
</script>
Thanks!
I don't know exactly your context, but this is how I use to do:
First, you don't need both components Parent and Child. You can do all you want inside Form Component.
To deal with the differences between create and edit modes, an option is computed property based on current route (if they are different according to create/edit operations).
Using this property, you decide if data will be fetched from API, if delete button will has shown, the title of the page and so on.
Here is an example:
async created() {
if (this.isEditMode) {
// fetch form data from API according to User ID and copy to a local form
},
},
computed: {
formTitle() {
return (this.isEditMode ? 'Update' : 'Create') + ' User';
},
}