19

My goal is to create an 'edit account' form such that a user can modify their account data. I want to present the account data in a form that is already filled with the users data i.e username, email, address ...

The user can then modify the data in the form and submit this form that will update their user information.

I am using v-model to bind the form input to an object called accountInfo in my data, that looks like this:

data() {
    return {
        accountInfo: {
                firstName: ''
        }
    }
}

And here is an example of a form input in my template:

<input v-model.trim="accountInfo.firstName" type="text" class="form-control" id="first-name" />

The values for the key's in the object are currently empty strings but I would like the values to come from an object called userProfile that is a state property in vuex.

In my 'edit account' component I am mapping the vuex state by importing:

import { mapState } from "vuex";

then using the following in a computed property

computed: {
    ...mapState(["userProfile"])
}

What I would like to do is instead of having empty strings as the values of accountInfo, assign them values from the userProfile computed property mapped from vuex, like so:

data() {
        return {
            accountInfo: {
                    firstName: this.userProfile.fristName,
            }
        }
    }

This will provide the desired initial data for my form but unfortunately this doesn't work, presumably because data is rendered earlier on in the life cycle than computed properties.

Full code:

EditAccount.vue

<template>
    <div class="container-fluid">
        <form id="sign_up_form" @submit.prevent>
            <div class="form-row">
                <div class="form-group col-md-6">
                    <input v-model.trim="signupForm.firstName" type="text" class="form-control" id="first_name" />
                </div>
            </div>
        </form>
    </div>
</template>

<script>
import { mapState } from "vuex";
import SideBar from "../common/SideBar.vue";

export default {
  name: "EditAccount",
  computed: {
    ...mapState(["userProfile"])
  },
  data() {
    return {
      accountInfo: {
        firstName: this.userProfile.firstName
      }
    };
  }
};
</script>

store.js:

export const store = new Vuex.Store({
    state: {
        userProfile: {firstName: "Oamar", lastName: "Kanji"}
    }
});
Oamar Kanji
  • 1,824
  • 6
  • 24
  • 39
  • Emile Bergeron that seems like the correct way to access the computed property (using this) but it still does not show in my form, presumably because the computed property is rendered after data in the vue life cycle..? – Oamar Kanji Jun 27 '18 at 13:13
  • Yes, it is populated upon log in, and my "edit account" component can only be accessed once a user is logged in so the userProfile state is guaranteed to be non empty when this component is mapping it – Oamar Kanji Jun 27 '18 at 13:22
  • Minimal and complete code included. I tired adding a test state test: "hi" that has a hard coded string as its value (it's not populated by any call), accessing this state similarly to how i access userProfile in my component does not yield a different result so I don't think it has anything to do with how the state is populated. – Oamar Kanji Jun 27 '18 at 13:41
  • 1
    Are you passing the store to the root Vue instance? See the [shopping cart example](https://github.com/vuejs/vuex/blob/caa663d69608fc36d1a88746df28961437d38786/examples/shopping-cart/app.js) – Emile Bergeron Jun 27 '18 at 13:44
  • 1
    Also explained in the [vuex state documentation](https://vuex.vuejs.org/guide/state.html#getting-vuex-state-into-vue-components) – Emile Bergeron Jun 27 '18 at 13:45
  • Yes am doing the above already. – Oamar Kanji Jun 27 '18 at 13:53
  • 1
    You're right, computed are evaluated after the initial `data` function is called. The parent component of your form component should use the store to pass down the initial userProfile data in a prop instead. – Emile Bergeron Jun 27 '18 at 14:08
  • 3
    `$store` should be ready before `data` function is called. Therefore, `firstName: this.$store.state.userProfile.firstName` should just work – Jacob Goh Jun 27 '18 at 14:14

2 Answers2

29

You were right, computeds are evaluated after the initial data function is called.

Quick fix

In the comments, @Jacob Goh mentioned the following:

$store should be ready before data function is called. Therefore, firstName: this.$store.state.userProfile.firstName should just work.

export default {
  name: 'EditAccount',
  data() {
    return {
      accountInfo: {
        firstName: this.$store.state.userProfile.firstName
      }
    }
  }
};

Really need computeds?

See @bottomsnap's answer, where setting the initial value can be done in the mounted lifecycle hook.

With your code, it would look like this:

import { mapState } from 'vuex';

export default {
  name: 'EditAccount',
  computed: {
    ...mapState(['userProfile'])
  },
  data() {
    return {
      accountInfo: {
        firstName: ''
      }
    }
  }
  mounted() {
    this.accountInfo.firstName = this.userProfile.firstName;
  }
};

Though it may render once without the value, and re-render after being mounted.

Container versus presentation

I explain Vue's communication channels in another answer, but here's a simple example of what you could do.

Treat the Form component as presentation logic, so it doesn't need to know about the store, instead receiving the profile data as a prop.

export default {
    props: {
        profile: {
            type: Object,
        },
    },
    data() {
        return {
            accountInfo: {
                firstName: this.profile.firstName
            }
        };
    }
}

Then, let the parent handle the business logic, so fetching the information from the store, triggering the actions, etc.

<template>
    <EditAccount :profile="userProfile" :submit="saveUserProfile"/>
</template>
<script>
import { mapState, mapActions } from "vuex";

export default {
    components: { EditAccount },
    computed: mapState(['userProfile']),
    methods: mapActions(['saveUserProfile'])
}
</script>

While Jacob is not wrong saying that the store is ready, and that this.$store.state.userProfile.firstName will work, I feel this is more a patch around a design problem that can easily be solved with the solution above.

Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
  • Any chance to access the computed method through `this` object to remain DRY? Like `data() { return {accountInfo: {firstName: this.options.computed.userProfile() }}}` or something? – Augustin Riedinger Jun 30 '22 at 08:52
  • @AugustinRiedinger, `computed` values are evaluated after the `data` function is called, so it's not really possible to do what you're suggesting. Though nothing stops you from extracting the logic into a stand-alone function and use it twice, once in the computed and once to initialize the state data. That said, it sounds like a bad design issue. – Emile Bergeron Jul 02 '22 at 21:00
11

Bind your input with v-model as you were:

<div id="app">
    <input type="text" v-model="firstName">
</div>

Use the mounted lifecycle hook to set the initial value:

import Vue from 'vue';
import { mapGetters } from 'vuex';

new Vue({
      el: "#app",
      data: {
        firstName: null
      },
      computed: {
        ...mapGetters(["getFirstName"])
      },
      mounted() {
        this.firstName = this.getFirstName
      }
    })
Lukasz Dynowski
  • 11,169
  • 9
  • 81
  • 124
bottomsnap
  • 221
  • 2
  • 5
  • I think it would be better to set the initial value in created hook. We would have the computed value but the component would not have rendered, which is a perfect place to update the data values – Keerthi Kumar P Aug 25 '23 at 17:18