2

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:

  1. 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?
  2. 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?
  3. 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?
Adam
  • 1,054
  • 1
  • 12
  • 26

1 Answers1

0

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 = {};
  }
}
chans
  • 5,104
  • 16
  • 43
  • Seems to work, thank you for the explanation! :) Just last thing, do you think it is a correct place to set the profile when changing routes in the 'mounted' func? – Adam Apr 27 '20 at 20:07
  • There are different approaches for update your data. It totally depends on your requirement – chans Apr 28 '20 at 05:10