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.
<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:
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;
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..
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.