4

I followed through this tutorial to try and learn Vue, I've finished and it works, but I'm trying to make a change that I'm struggling with.

https://savvyapps.com/blog/definitive-guide-building-web-app-vuejs-firebase

So there's a "settings" page which has the user profile (they can edit their name etc). When that "settings" / "profile" page loads I want the form to load their existing data so they can just modify it and press save.

It currently loads as a placeholder with :placeholder="userProfile.name" - I want it to just fill the form with the actual value instead of having it as a placeholder.

It feels like it should be ridiculously simple to do this but can't get it working elegantly.

Settings.vue

<template>
  <section id="settings">
    <div class="col1">
      <h3>Settings</h3>
      <p>Update your profile</p>

      <transition name="fade">
        <p v-if="showSuccess" class="success">profile updated</p>
      </transition>

      <form @submit.prevent>
        <label for="name">Name</label>
        <input v-model.trim="name" type="text" id="name" />

        <label for="title">Job Title</label>
        <input v-model.trim="title" type="text" id="title" />

        <button @click="updateProfile()" class="button">Update Profile</button>
      </form>
    </div>
  </section>
</template>

<script>
import { mapState } from "vuex";

export default {
  data() {
    return {
      name: "",
      title: "",
      showSuccess: false,
    };
  },
  computed: {
    ...mapState(["userProfile"]),
  },
  methods: {
    updateProfile() {
      this.$store.dispatch("updateProfile", {
        name: this.name !== "" ? this.name : this.userProfile.name,
        title: this.title !== "" ? this.title : this.userProfile.title,
      });

      this.name = "";
      this.title = "";

      this.showSuccess = true;

      setTimeout(() => {
        this.showSuccess = false;
      }, 2000);
    },
  },
};
</script>

<style lang="scss" scoped>
</style>

I tried changing the data section to this, which works when I leave the page and go back to it, but if I refresh the page (F5) the fields are blank until I leave the page and come back again.

data() {
    return {
      name: this.$store.state.userProfile.name,
      title: this.$store.state.userProfile.title,
      showSuccess: false,
    };
  },

And here's my store if you need to see that:

store/index.js

import Vue from "vue";
import Vuex from "vuex";
import * as fb from "../firebase";
import router from "../router/index";

Vue.use(Vuex);

// realtime firebase connection
fb.postsCollection.orderBy("createdOn", "desc").onSnapshot((snapshot) => {
  let postsArray = [];

  snapshot.forEach((doc) => {
    let post = doc.data();
    post.id = doc.id;

    postsArray.push(post);
  });

  store.commit("setPosts", postsArray);
});

const store = new Vuex.Store({
  state: {
    userProfile: {},
    posts: [],
  },
  mutations: {
    setUserProfile(state, val) {
      state.userProfile = val;
    },
    setPosts(state, val) {
      state.posts = val;
    },
  },
  actions: {
    async signup({ dispatch }, form) {
      // sign user up
      const { user } = await fb.auth.createUserWithEmailAndPassword(
        form.email,
        form.password
      );

      // create user profile object in userCollections
      await fb.usersCollection.doc(user.uid).set({
        name: form.name,
        title: form.title,
      });

      // fetch user profile and set in state
      dispatch("fetchUserProfile", user);
    },
    async login({ dispatch }, form) {
      // sign user in
      const { user } = await fb.auth.signInWithEmailAndPassword(
        form.email,
        form.password
      );

      // fetch user profile and set in state
      dispatch("fetchUserProfile", user);
    },
    async logout({ commit }) {
      await fb.auth.signOut();

      // clear userProfile and redirect to /login
      commit("setUserProfile", {});
      router.push("/login");
    },
    async fetchUserProfile({ commit }, user) {
      // fetch user profile
      const userProfile = await fb.usersCollection.doc(user.uid).get();

      // set user profile in state
      commit("setUserProfile", userProfile.data());

      // change route to dashboard
      if (router.currentRoute.path === "/login") {
        router.push("/");
      }
    },
    async createPost({ state }, post) {
      await fb.postsCollection.add({
        createdOn: new Date(),
        content: post.content,
        userId: fb.auth.currentUser.uid,
        userName: state.userProfile.name,
        comments: 0,
        likes: 0,
      });
    },
    async likePost(context, { id, likesCount }) {
      const userId = fb.auth.currentUser.uid;
      const docId = `${userId}_${id}`;

      // check if user has liked post
      const doc = await fb.likesCollection.doc(docId).get();
      if (doc.exists) {
        return;
      }

      // create post
      await fb.likesCollection.doc(docId).set({
        postId: id,
        userId: userId,
      });

      // update post likes count
      fb.postsCollection.doc(id).update({
        likes: likesCount + 1,
      });
    },
    async updateProfile({ dispatch }, user) {
      const userId = fb.auth.currentUser.uid;
      // update user object
      /*const userRef = */await fb.usersCollection.doc(userId).update({
        name: user.name,
        title: user.title,
      });

      dispatch("fetchUserProfile", { uid: userId });

      // update all posts by user
      const postDocs = await fb.postsCollection
        .where("userId", "==", userId)
        .get();
      postDocs.forEach((doc) => {
        fb.postsCollection.doc(doc.id).update({
          userName: user.name,
        });
      });

      // update all comments by user
      const commentDocs = await fb.commentsCollection
        .where("userId", "==", userId)
        .get();
      commentDocs.forEach((doc) => {
        fb.commentsCollection.doc(doc.id).update({
          userName: user.name,
        });
      });
    },
  },
  modules: {},
});

export default store;

EDIT

I should have mentioned that this data is being loaded into the state from a Firebase Firestore. It looks like it's just a timing thing, the data isn't quite loaded by the time it sets the data() on the component - I added some console logs.

Fetching user profile.. Settings.vue?e12e:29
Setting Data... index.js?4360:75
Performing setUserProfile commit.. index.js?4360:29
Setting user profile in state, last step..

Again just don't know enough about Vue yet to know how to best change that order..

BT643
  • 3,495
  • 5
  • 34
  • 55

3 Answers3

5

v-model gets and sets the value of whatever you pass to it. Since you want to edit a state property, as soon as you modify the <input>'s value it will try to change (a.k.a. mutate) the value of the state property. And that would break the immutability principle [1].
The solution here is to pass a computed property to v-model which uses a getter and a setter, where you tell the component where to get the value from and how to update it.

By default computed properties are a shorthand and only contain the getter. Basically,

computed: {
  name() {
    return this.$store.state.userProfile.name
  }
}

...can be written as:

computed: {
  name: {
    get() {
      return this.$store.state.userProfile.name
    }
  }
}

And what you need is to add a setter which commits the appropriate mutation so the state is updated:

computed: {
  ...mapState(["userProfile"]),
  name: {
    get() {
      return this.userProfile.name
    },
    set(val) {
      this.$store.commit('setUserProfile', {
        ...this.userProfile,
        name: val
      });
    }
  },
  title: {
    get() {
      return this.userProfile.title
    },
    set(val) {
      this.$store.commit('setUserProfile', {
        ...this.userProfile,
        title: val
      });
    }
  }
}

Computed setters are documented here.


[1] - the reason why you're using Vuex is because you don't want to allow any component to directly modify your data. Instead, you want them to commit mutations to the state so that every component using that data gets notified of the change. If you allowed v-model to change your data directly, you'd be breaking the immutability principle, so your state would cease to be the only source of truth.

tony19
  • 125,647
  • 18
  • 229
  • 307
tao
  • 82,996
  • 16
  • 114
  • 150
0

Two things to take on count, first when you want to get the value from variable in the state crate getters (as good vuex practices):

Vuex file:

const store = new Vuex.Store({
  state: {
    userProfile: {},
    posts: [],
  },

  getters:{
    getUserProfile: (state) => state.userProfile
  }

Settigs.vue So, to accomplish what you want you can load the variables in data() inside of the method mounted:

export default {
  data() {
    return {
      name: "",
      title: "",
      showSuccess: false,
    };
  },
  computed: {
    ...mapState(["getUserProfile"]),
  },

  mounted(){
   this.name = getUserProfile.name
  }

So, if you expect that the user will refresh the page without losing the data loaded, you can't use vuex alone, because when you refresh the page, vuex system restart too. If you want to maintain the data loaded after refresh the page use localstorage together with vuex or a similar solution.

Straight Coding
  • 376
  • 3
  • 7
  • Sorry I should also have mentioned that during the tutorial there are parts where it fetches the data (including user profile) from a Firebase DB. So if I refresh the page (form goes blank), navigate to the home screen, and back to the profile page, it fills in the form again without me having to type anything with the solution in my question. Just haven't quite got my head around it enough to figure out how to get it to do that "fetch" after a page refresh. – BT643 Sep 23 '20 at 09:44
0

Since it's just a timing thing:

I'd suggest you just bind your data value to a watcher on your state. Your component will simply listen everytime your state updates and will update your data accordingly.

export default {
  data() {
    return {
      name: "",
      title: "",
      showSuccess: false,
    };
  },
  computed: {
    ...mapState(["userProfile"]),
  },
  watch: {
    userProfile: {
      handler({ name, title }) {
        this.name = name;
        this.title = title;
      },
      deep: true, // deep is to listen to objects properly
      immediate: true // immediate so the watcher triggers right away when the component is mounted
    }
  },
  methods: {
    updateProfile() {
      this.$store.dispatch("updateProfile", {
        name: this.name !== "" ? this.name : this.userProfile.name,
        title: this.title !== "" ? this.title : this.userProfile.title,
      });
      
            /*  I just wouldn't reset the values here since they'll be updated in the watcher
      this.name = "";
      this.title = ""; */

      this.showSuccess = true;

      setTimeout(() => {
        this.showSuccess = false;
      }, 2000);
    },
  },
};
Pierre Burton
  • 1,954
  • 2
  • 13
  • 27