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.
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,
};
},
puted: {
...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 e 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);
});
storemit("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({ mit }) {
await fb.auth.signOut();
// clear userProfile and redirect to /login
mit("setUserProfile", {});
router.push("/login");
},
async fetchUserProfile({ mit }, user) {
// fetch user profile
const userProfile = await fb.usersCollection.doc(user.uid).get();
// set user profile in state
mit("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,
ments: 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 ments by user
const mentDocs = await fbmentsCollection
.where("userId", "==", userId)
.get();
mentDocs.forEach((doc) => {
fbmentsCollection.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 ponent - I added some console logs.
Fetching user profile.. Settings.vue?e12e:29
Setting Data... index.js?4360:75
Performing setUserProfile mit.. 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..
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./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,
};
},
puted: {
...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 e 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.mit("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({ mit }) {
await fb.auth.signOut();
// clear userProfile and redirect to /login
mit("setUserProfile", {});
router.push("/login");
},
async fetchUserProfile({ mit }, user) {
// fetch user profile
const userProfile = await fb.usersCollection.doc(user.uid).get();
// set user profile in state
mit("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,
ments: 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 ments by user
const mentDocs = await fb.mentsCollection
.where("userId", "==", userId)
.get();
mentDocs.forEach((doc) => {
fb.mentsCollection.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 ponent - I added some console logs.
Fetching user profile.. Settings.vue?e12e:29
Setting Data... index.js?4360:75
Performing setUserProfile mit.. 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..
Share Improve this question edited Sep 24, 2020 at 0:25 BT643 asked Sep 22, 2020 at 21:41 BT643BT643 3,8555 gold badges38 silver badges56 bronze badges3 Answers
Reset to default 5 +150v-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 puted property to v-model
which uses a getter and a setter, where you tell the ponent where to get the value from and how to update it.
By default puted
properties are a shorthand and only contain the getter. Basically,
puted: {
name() {
return this.$store.state.userProfile.name
}
}
...can be written as:
puted: {
name: {
get() {
return this.$store.state.userProfile.name
}
}
}
And what you need is to add a setter which mits the appropriate mutation so the state is updated:
puted: {
...mapState(["userProfile"]),
name: {
get() {
return this.userProfile.name
},
set(val) {
this.$store.mit('setUserProfile', {
...this.userProfile,
name: val
});
}
},
title: {
get() {
return this.userProfile.title
},
set(val) {
this.$store.mit('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 ponent to directly modify your data. Instead, you want them to mit mutations to the state so that every ponent 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.
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 acplish what you want you can load the variables in data() inside of the method mounted:
export default {
data() {
return {
name: "",
title: "",
showSuccess: false,
};
},
puted: {
...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.
Since it's just a timing thing:
I'd suggest you just bind your data value to a watcher on your state. Your ponent will simply listen everytime your state updates and will update your data accordingly.
export default {
data() {
return {
name: "",
title: "",
showSuccess: false,
};
},
puted: {
...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 ponent 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);
},
},
};