how to detect change of actual value not just OnChange nuxt vuetify - vue.js

As a result of
export default {
name: "Details",
async asyncData({ redirect, params, store }) {
if (
!store
I am returning a few values in which one of them is
return {
camera: c,
thumbnail_url: thumbnail_url,
camera, and then in my form fields where I am populating a Vuetify dialog, Text Field inputs
such as
<v-dialog v-model="dialog" max-width="600px">
<v-card>
<v-card-text>
<v-layout class="model-container">
<v-row>
<v-col cols="12" lg="7" md="7" sm="12" xs="12">
<v-text-field
v-model="camera.name"
class="caption bottom-padding"
required
>
<template v-slot:label>
<div class="caption">
Name
</div>
</template>
</v-text-field>
my issue is, I have a button as
<v-btn color="primary" text #click="updateCamera">
Save
</v-btn>
which I only want to make disable false, only if there is an actual change occurs to, this.camera, in updateCamera method, I can use the updated values as
async updateCamera() {
let payload = {
name: this.camera.name,
but I want to enable or disable the button on when change occurs,
I had tried #input, I have also tried to watch camera object
<v-text-field
v-model="camera.name"
class="caption bottom-padding"
required
#input="up($event, camera)"
>
This way I tried to get some info about event, such as which text field it is, so I can compare, but in up method it only passes input value.
in watch
camera: function() {
this.$nextTick(() => {
console.log(this.camera)
})
}
camera: {
handler: function(val) {
this.$nextTick(() => {
console.log(val)
})
/* ... */
},
immediate: true
}
I have tried this but nothing worked.
Of course, we can enable or disable a button on change but not just if the user places an A and then deletes it, not such change.
Any help would be wonderful
Update:
Even after using this
camera: {
handler: function(newValue) {
if (newValue === this.dumpyCamera) {
console.log(this.dumpyCamera)
console.log(newValue)
console.log("here")
this.updateButton = true
} else {
this.updateButton = false
}
},
deep: true
}
both new and old values are the same.
I have tried to add new variable dumyCamera and on mount I have assigned this.camera value to this.dumyCamera but when something changes in camera, it changes this.dumyCamera as well? why is this the case?

You should be able to recognize any changes made to this.camera by using a watcher
watch: {
camera: {
handler (newValue, oldValue) {
// do something here because your this.camera changed
},
deep: true
}
}

Related

beforeRouteLeave doesn't work imediately when using with modal and emit function

I have a Vue application with many child components. In my case, I have some parent-child components like this. The problem is that in some child components, I have a section to edit information. In case the user has entered some information and router to another page but has not saved it, a modal will be displayed to warn the user. I followed the instructions on beforeRouteLeave and it work well but I got a problem. When I click the Yes button from the modal, I'll emit a function #yes='confirm' to the parent component. In the confirm function, I'll set this.isConfirm = true. Then I check this variable inside beforeRouteLeave to confirm navigate. But in fact, when I press the Yes button in modal, the screen doesn't redirect immediately. I have to click one more time to redirect. Help me with this case
You can create a base component like the following one - and then inherit (extend) from it all your page/route-level components where you want to implement the functionality (warning about unsaved data):
<template>
<div />
</template>
<script>
import events, { PAGE_LEAVE } from '#/events';
export default
{
name: 'BasePageLeave',
beforeRouteLeave(to, from, next)
{
events.$emit(PAGE_LEAVE, to, from, next);
}
};
</script>
events.js is simply a global event bus.
Then, in your page-level component you will do something like this:
<template>
<div>
.... your template ....
<!-- Unsaved changes -->
<ConfirmPageLeave :value="modified" />
</div>
</template>
<script>
import BasePage from 'src/components/BasePageLeave';
import ConfirmPageLeave from 'src/components/dialogs/ConfirmPageLeave';
export default
{
name: 'MyRouteName',
components:
{
ConfirmPageLeave,
},
extends: BasePage,
data()
{
return {
modified: false,
myData:
{
... the data that you want to track and show a warning
}
};
}.
watch:
{
myData:
{
deep: true,
handler()
{
this.modified = true;
}
}
},
The ConfirmPageLeave component is the modal dialog which will be shown when the data is modified AND the user tries to navigate away:
<template>
<v-dialog v-model="showUnsavedWarning" persistent>
<v-card flat>
<v-card-title class="pa-2">
<v-spacer />
<v-btn icon #click="showUnsavedWarning = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="pt-2 pb-3 text-h6">
<div class="text-h4 pb-4">{{ $t('confirm_page_leave') }}</div>
<div>{{ $t('unsaved_changes') }}</div>
</v-card-text>
<v-card-actions class="justify-center px-3 pb-3">
<v-btn class="mr-4 px-4" outlined large tile #click="showUnsavedWarning = false">{{ $t('go_back') }}</v-btn>
<v-btn class="ml-4 px-4" large tile depressed color="error" #click="ignoreUnsaved">{{ $t('ignore_changes') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import events, { PAGE_LEAVE } from '#/events';
export default
{
name: 'ConfirmPageLeave',
props:
{
value:
{
// whether the monitored data has been modified
type: Boolean,
default: false
}
},
data()
{
return {
showUnsavedWarning: false,
nextRoute: null,
};
},
watch:
{
showUnsavedWarning(newVal)
{
if (!newVal)
{
this.nextRoute = null;
}
},
},
created()
{
events.$on(PAGE_LEAVE, this.discard);
window.addEventListener('beforeunload', this.pageLeave);
},
beforeDestroy()
{
events.$off(PAGE_LEAVE, this.discard);
window.removeEventListener('beforeunload', this.pageLeave);
},
methods:
{
discard(to, from, next)
{
if (this.value)
{
this.nextRoute = next;
this.showUnsavedWarning = true;
}
else next();
},
pageLeave(e)
{
if (this.value)
{
const confirmationMsg = this.$t('leave_page');
(e || window.event).returnValue = confirmationMsg;
return confirmationMsg;
}
},
ignoreUnsaved()
{
this.showUnsavedWarning = false;
if (this.nextRoute) this.nextRoute();
},
}
};
</script>
<i18n>
{
"en": {
"confirm_page_leave": "Unsaved changes",
"unsaved_changes": "If you leave this page, any unsaved changes will be lost.",
"ignore_changes": "Leave page",
"go_back": "Cancel",
"leave_page": "You're leaving the page but there are unsaved changes.\nPress OK to ignore changes and leave the page or CANCEL to stay on the page."
}
}
</i18n>

Vuetify: v-dialog won't close although v-model variable is set to false

I have a couple of dialogs which are created with v-for (exchangeTypeAbbreviation and exchangeType come from there). When I click on the activator button, the dialog opens and the value in the object I use for storing the dialogs' state is updated to "true".
But when I click the cancel or save button, the dialog won't close, although the object's value is updated to "false".
<v-list-item>
<v-dialog
max-width="400"
v-model="dialogs[exchangeTypeAbbreviation]"
>
<template v-slot:activator="{ on }">
<v-list-item v-on="on">
<v-icon class="pr-4">
mdi-plus
</v-icon>
Add Product Flow
</v-list-item>
</template>
<v-card>
<v-card-title>Add Product Flow</v-card-title>
<v-card-subtitle
v-text="exchangeType"
></v-card-subtitle>
<v-card-actions>
<v-btn
#click="
dialogs[exchangeTypeAbbreviation] = false;
createUnitProcessExchange(
exchangeTypeAbbreviation
);
"
>Save</v-btn
>
<v-btn
#click="dialogs[exchangeTypeAbbreviation] = false"
>Cancel</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
</v-list-item>
<script>
export default {
name: 'Activities',
data: () => ({
dialogs: {},
exchangeTypes: {},
unitProcessExchangesOptions: null,
}
}),
mounted() {
Promise.all([
this.loadUnitProcessExchangeOptions()
])
},
methods: {
async loadUnitProcessExchangeOptions() {
return this.$api
.options('/unitprocessexchanges/', {
headers: {
Authorization: 'Token ' + localStorage.getItem('token')
}
})
.then(response => {
this.unitProcessExchangesOptions = response.data.actions.POST
for (const exchangeType of this.unitProcessExchangesOptions
.exchange_type.choices) {
this.exchangeTypes[exchangeType.value] = exchangeType.display_name
this.dialogs[exchangeType.value] = false
}
})
},
async createUnitProcessExchange(exchangeTypeAbbreviation) {
this.newUnitProcessExchange.activity = this.activities[
this.selectedActivity
].url
this.newUnitProcessExchange.exchange_type = exchangeTypeAbbreviation
this.dialogs[exchangeTypeAbbreviation] = false
// eslint-disable-next-line no-debugger
debugger
}
}
}
</script>
I was able to figure out why it doesn't work. Due to limitations in JavaScript, Vue.js has some difficulties to observe changes in Objects and Arrays. This is documented here.
In my case, I added nested variables inside my "dialogs" variable by assigning them directly, e.g. like this
this.dialogs[index] = false
However, this creates a sub-element which can't be tracked by Vue.js. To make sure that changes on this element can be tracked, it either has to be pre-defined from the beginning or needs to be set by using the Vue.$set command. Always using the following command, solved the issue for me:
this.$set(dialogs, index, false)
I think the first problem is you are trying to change the object with an array notation i.e array[0] but it should be a dot notation with object property, in your case it would be dialogs.exchangeTypeAbbreviation = false.
With that one more problem would be that property doesn't exist so in
data: () => ({
dialogs: {exchangeTypeAbbreviation:Boolean},
exchangeTypes: {},
unitProcessExchangesOptions: null,
}
}),
with this now you can set the value of exchangeTypeAbbreviation.

Update data from local copy of Vuex store

I am implementing a user profile edit page that initially consists of the data loaded from the vuex store. Then the user can freely edit his data and finally store them in the store.
Since the user can also click the cancel button to revert back to his original state, I decided to create a 'local' view copy of the user data fetched from the store. This data will be held in the view and once the user presses save, they will be saved in the store.
The view looks as following:
<template class="user-profile">
<v-form>
<template v-if="profile.avatar">
<div class="text-center">
<v-avatar width="120" height="120">
<img
:src="profile.avatar"
:alt="profile.firstname"
>
</v-avatar>
</div>
</template>
<div class="text-center mt-4">
<v-btn
color="primary"
dark
#click.stop="showImageDialog=true"
>
Change Image
</v-btn>
</div>
<v-row>
<v-col>
<v-text-field
label="First name"
single-line
disabled
v-model="profile.firstname"
></v-text-field>
</v-col>
<v-col>
<v-text-field
label="Last name"
single-line
disabled
v-model="profile.lastname"
></v-text-field>
</v-col>
</v-row>
<v-text-field
label="Email"
single-line
v-model="profile.email"
></v-text-field>
<v-text-field
id="title"
label="Title"
single-line
v-model="profile.title"
></v-text-field>
<v-textarea
no-resize
clearable
label="Biography"
v-model="profile.bio"
></v-textarea>
<v-dialog
max-width="500"
v-model="showImageDialog"
>
<v-card>
<v-card-title>
Update your profile picture
</v-card-title>
<v-card-text>
<v-file-input #change="setImage" accept="image/*"></v-file-input>
<template v-if="userAvatarExists">
<vue-cropper
ref="cropper"
:aspect-ratio="16 / 9"
:src="profile.avatar"
/>
</template>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="green darken-1"
text
#click="showImageDialog=false"
>
Cancel
</v-btn>
<v-btn
color="green darken-1"
text
#click="uploadImage"
>
Upload
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<div class="mt-8">
<v-btn #click="onUpdateUser">Update</v-btn>
</div>
</v-form>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import VueCropper from 'vue-cropperjs';
import 'cropperjs/dist/cropper.css';
export default {
components: { VueCropper},
mounted() {
this.profile = this.getUserProfile ? this.getUserProfile : {}
},
data() {
return {
profile: {},
avatar: null,
userAvatarExists: false,
showImageDialog: false,
}
},
watch: {
getUserProfile(newData){
this.profile = newData;
},
deep: true
},
computed: {
...mapGetters({
getUserProfile: 'user/me',
})
},
methods: {
...mapActions({
storeAvatar: 'user/storeAvatar',
updateUser: 'user/update'
}),
onUpdateUser() {
const data = {
id: this.profile.id,
email: this.profile.email,
title: this.profile.title,
bio: this.profile.bio,
avatar: this.profile.avatar,
}
this.updateUser(data)
},
uploadImage() {
this.$refs.cropper.getCroppedCanvas().toBlob((blob => {
this.storeAvatar(blob).then((filename => {
this.profile.avatar = filename.data
this.$refs.cropper.reset()
}));
this.showImageDialog = false
}));
},
setImage(file) {
this.userAvatarExists = true;
if (file.type.indexOf('image/') === -1) {
alert('Please select an image file');
return;
}
if (typeof FileReader === 'function') {
const reader = new FileReader();
reader.onload = (event) => {
this.$refs.cropper.replace(event.target.result);
};
reader.readAsDataURL(file);
} else {
alert('Sorry, FileReader API not supported');
}
}
}
}
</script>
Issues/Questions:
As you can see from the code, after the user changes his profile
picture, the image should be rendered based on the
v-if="profile.avatar". The issue is that after the
profile.avatar is set in the uploadImage function, the
template does not see this change and no image is rendered.
However if I change the code so that the profile.avatar becomes
just avatar (it is no longer within the profile object), the
template starts to see the changes and renders the image
correctly. Why so? Does it have something to do with making a
copy from the store in the watch function?
Is it in general a good approach to keep the profile just as a local
view state or should it rather be stored in the vuex store even if
it is just a temporary data?
As you can see in the mounted function, I am setting the profile
value based on the getUserProfile getter. This is because the
watch function does not seem to be called again when switching
routes. Is there any other way how to do this?
The issue is due to the reactivity of data properties
You have used profile as an object, default it doesn't have any properties like avatar or firstname, its just empty
In vue js, If you are declaring an object, whatever the key mention in the declaration is only the part of reactivity. Once the keys inside profile changes, it rerenders the template
But still you can add new properties to a data property object by using $set
lets say in data you have declared
profile: {}
if you want to set avatar as new reactive property in runtime use
this.$set(this.profile, key, value)
which is
this.$set(this.profile, avatar, imageData)
In your above code, the setIuploadImage function
uploadImage() {
var self = this;
self.$refs.cropper.getCroppedCanvas().toBlob((blob => {
self.storeAvatar(blob).then((filename => {
self.$set(self.profile, "avatar", filename.data)
self.$refs.cropper.reset()
}));
self.showImageDialog = false
}));
},
this won't work inside arrow function in vuejs, so just preserved the this inside another variable "self" and used inside arrow function
Also in mounted function, if this.getUserProfile returns empty object, then as per javascript empty object is always truthy and directly assigning object to profile doesn't make the object reactive
mounted() {
this.profile = this.getUserProfile ? this.getUserProfile : {}
},
above code can be written as
mounted() {
if (this.getUserProfile && Object.keys(this.getUserProfile).length) {
var self = this;
Object.keys(this.getUserProfile).map(key => {
self.$set(self.profile, key, self.getUserProfile[key])
});
} else {
this.profile = {};
}
}

How can I call ajax before display datepicker in vuetify?

Look at this : https://codepen.io/positivethinking639/pen/mddejJN
My script of vue like this :
data: () => ({
modalTest: false,
dateTest: null,
time: null,
allowedTimes: ['8:30 am','9:00am','10:30am','1:30pm','3:30 pm']
}),
methods: {
saveData() {
this.$refs.dialogTest.save(this.dateTest)
},
allowedDates: val => parseInt(val.split('-')[2], 10) % 2 === 0,
setTime(time) {
this.time = time
}
I want before call datepicker, I call ajax first
How can I do it?
#Max proposal is not fully answers the question.
Let's add new data property which will trigger the show of calendar component:
isAjaxCompl: false,
Move the button out of template to directly change dialog v-model:
<v-btn color="success" #click="openDialog()">call date</v-btn>
Make the function which will be fired on dialog open:
openDialog() {
this.modalTest = true;
axios.get('https://reqres.in/api/users?delay=1').then((response) => {
this.isAjaxCompl = true;
})
},
Finally, add v-if which will show calendar component only when axios get the response:
<v-date-picker v-if="isAjaxCompl" v-model="dateTest" scrollable :allowed-dates="allowedDates">
Link to the corresponding CodePen:
https://codepen.io/RobbyFront/pen/RwwWewM
You need to move the dialog button outside and add a #click method for showing the dialog
Your Code
<template v-slot:activator="{ on }">
<v-btn color="success" dark v-on="on">call date</v-btn>
</template>
New Code
Html
<v-btn color="success" dark #click="showDate">call date</v-btn>
Code
showDate(){
console.log("Ajax calling");
this.modalTest = true;
}
Here the pen

Vue emmitting component property appears to skip parent

I've been stuck on this for a long time now and I can't find anything remotely similar to this problem online.
Context: Simple restaurant review website.
My application needs to be able to take webcam photos using WebRTC or upload photos from storage. I built a component to do this called AddImageDialog that lets the user select either webcam or upload a stored image by storing the image URL in prop on this component and then emitting it. The parent then handles where to place the image.
One part of my program that requires this is the NewReview component, that allows 5 photos to be uploaded, all using the same AddImageComponent.
There is a page for users to upload a new restaurant They may also leave a review on this page, so the Review component with the AddImageDialog is imported onto this page. This page must also allow the user to upload an official photo for the venue, so I have reused my AddImagedDialog on this page.
The problem... When uploading review photos (on the new restaurant page) whenever uploading via webcam, everything works fine but when uploading a stored image, the image seems to be emmitted but skips the NewReview parent component and jumps right back to the grandparent - NewRestaurant.
AddImageDialog.vue
<template>
<div>
<v-layout row justify-center>
<v-dialog v-model="thisShowDialog" max-width="550px">
<v-card height="350px">
<v-card-title>
Take a new photo or choose an existing one...
</v-card-title>
<v-card-text>
<div>
<!-- Add a new photo option -->
<div class="clickBox left" #click="showCamera = true">
<v-container fill-height>
<v-layout align-center>
<v-flex>
<v-icon size="40pt" class="enlarge">add_a_photo</v-icon>
</v-flex>
</v-layout>
</v-container>
</div>
<!-- Add photo from library -->
<div class="clickBox right" #click="uploadImage">
<v-container fill-height>
<v-flex>
<input type="file" class="form-control" id="file-input" v-on:change="onFileChange">
<v-icon size="40pt" class="enlarge">photo_library</v-icon>
</v-flex>
</v-container>
</div>
</div>
</v-card-text>
</v-card>
</v-dialog>
</v-layout>
<Camera
:showCamera.sync="showCamera"
:photoData.sync="thisPhotoData">
</Camera>
</div>
</template>
<script>
import Camera from '#/components/Camera'
export default {
props: ['showDialog', 'photoData'],
data () {
return {
thisShowDialog: false,
showCamera: false,
thisPhotoData: null
}
},
methods: {
uploadImage () {
var fileUpload = document.getElementById('file-input') // createElement('input', {type: 'file'})
fileUpload.click()
},
onFileChange (event) {
let files = event.target.files || event.dataTransfer.files
if (!files.length) {
return
}
this.createImage(files[0])
this.thisShowDialog = false
this.thisShowDialog = false
},
createImage (file) {
let reader = new FileReader()
reader.onload = e => {
alert('reader loaded')
this.thisPhotoData = e.target.result
}
reader.readAsDataURL(file)
}
},
watch: {
showDialog () {
this.thisShowDialog = this.showDialog
},
thisShowDialog () {
this.$emit('update:showDialog', this.thisShowDialog)
},
showCamera () {
// when hiding the camera, leave this component too.
if (!this.showCamera) {
this.thisShowDialog = false
}
},
thisPhotoData () {
alert('emmiting from add image comp')
this.$emit('update:photoData', this.thisPhotoData)
}
},
components: {
Camera
}
}
</script>
Sorry to post so much code, but I literally have no idea what's causing this problem.
You will notice I have tried to get both webcam and image uploads to work in the same way by assigning to a `photoData' variable and emitting it whenever it's been changed. I'm confused as to why they behave differently.
NewReview.vue
(this is the component being skipped by stored imaged uploads)
<template>
<!-- LEAVE A REVIEW -->
<v-expansion-panel class="my-4" popout>
<v-expansion-panel-content expand-icon="mode_edit">
<div slot="header">Been here before? Leave a review...</div>
<v-card class="px-3">
<v-alert :value="isLoggedIn" type="warning">
You are not logged in. Log in.
</v-alert>
<v-form v-model="validReview">
<!-- Title -->
<v-text-field
name="title"
label="Review Title"
v-model="thisReview.title"
:rules="reviewTitleRules">
</v-text-field>
<!-- Body -->
<v-text-field
name="body"
label="Review Body"
multi-line
v-model="thisReview.reviewBody"
:rules="reviewBodyRules">
</v-text-field>
<v-card-actions>
<!-- Submit -->
<v-btn v-if="canSubmit" :disabled="!validReview" class="primary" #click="submitReview">Submit</v-btn>
<!-- Select Rating -->
<star-rating-input
class="ratingSelectStyle"
:star-size="25"
active-color="#E53935"
v-model="thisReview.reviewRating"
:show-rating="false">
</star-rating-input>
<v-spacer />
<!-- Add and View 5 review images -->
<div v-for="i in 5" :key="i" #click="addImage(i-1)">
<review-image
:photoData="reviewPhotos[i-1]"
:id="i"
></review-image>
</div>
<!-- Add Images Component -->
<AddImageDialog
:showDialog.sync="showCamera"
:photoData.sync="photoData"
></AddImageDialog>
</v-card-actions>
</v-form>
</v-card>
</v-expansion-panel-content>
</v-expansion-panel>
</template>
<script>
import RestaurantService from '../services/RestaurantService'
import ReviewImage from '#/components/ReviewImage'
import StarRatingInput from 'vue-star-rating'
import {mapGetters} from 'vuex'
import AddImageDialog from '#/components/AddImageDialog'
export default {
props: ['review', 'restaurantID'],
data () {
return {
showCamera: false,
photoData: '',
reviewPhotos: ['', '', '', '', ''],
currentReviewPhoto: 0,
thisReview: this.review || {
title: '',
reviewBody: '',
reviewRating: 5,
user: 'Anon'
},
validReview: true,
reviewTitleRules: [
s => !!s || 'Title is required',
s => s.length >= 5 || 'Title must be at least 5 characters'
],
reviewBodyRules: [
s => !!s || 'Body is required',
s => s.length >= 10 || 'Body must be at least 10 characters'
]
}
},
methods: {
addImage (i) {
this.currentReviewPhoto = i
this.showCamera = true
// the rest of this is handled by watching for photodata to be changed by the camera component.
},
setName () {
this.thisReview.user = this.$store.state.isLoggedIn ? this.$store.state.user.name : 'Anon'
},
submitReview: async function () {
try {
await RestaurantService.submitReview(this.restaurantID, this.thisReview).then(res => {
if (res.status === 200) {
this.$store.dispatch('setNoticeText', 'Review Submitted')
}
})
} catch (err) {
if (err.response.status === 404) {
// console.log('404 - Failed to submit')
this.$store.dispatch('setNoticeText', '404 - Failed to submit review')
} else {
this.$store.dispatch('setNoticeText', 'An unexpected problem has occured. [' + err.response.status + ']')
}
}
}
},
computed: {
...mapGetters({isLoaded: 'isLoaded'}),
canSubmit () {
if (this.restaurantID) {
return true
} else {
return false
}
},
isLoggedIn () {
if (this.isLoaded && !this.$store.state.isLoggedIn) {
return true
} else {
return false
}
}
},
watch: {
thisReview: {
handler () {
this.$emit('update:review', this.thisReview)
},
deep: true
},
isLoaded () {
if (this.isLoaded) {
this.setName()
}
},
photoData () {
if (this.photoData !== '') {
this.reviewPhotos[this.currentReviewPhoto] = this.photoData
this.photoData = ''
}
}
},
components: {
StarRatingInput,
ReviewImage,
AddImageDialog
},
mounted () {
this.setName()
}
}
</script>
This works by defining 5 images for a review and an array of 5 strings, then depending on what image was selected, assigning the emmitted image from AddImageDialog to the appropriate array by watching for photo data. As mention, this watcher function will work with the webcam, but is skipped when uploading images.
I'm not even sure if whatever is causing this problem is in this code, but any suggestions would be greatly appreciated :)