Vue User Manager - Step 3

In the previous step, we created the UserList component. In this step, we'll add a component that allows you to view and edit the details of a user. We'll also see one of the pitfalls that many people run into when learning Vue, which is that you should never mutate a prop.

Add this component, just after the UserList component:

// UserForm component
app.component("user-form", {
    template: `
        <div class="user-form-container">
            <h2>User Details</h2>
            <form @submit.prevent="handleSubmit">
                <div>
                    <label>First Name:</label>
                    <input v-model="user.firstName" />
                </div>
                <div>
                    <label>Last Name:</label>
                    <input v-model="user.lastName" />
                </div>
                <div>
                    <label>Email:</label>
                    <input v-model="user.email" />
                </div>
                <div>
                    <input type="submit" id="btnSubmit" name="submit button">
                </div>
            </form>
        </div>`,
    props: {
        user: {
            type: Object
        }
    },
    methods:{
        handleSubmit(){
            console.log("TODO: handle the form submit");
        }
    }
});

Note that the component declares a user object as a prop, and uses the v-model directive to bind properties of user object to the form. This is one of the pitfalls that I mentioned earlier. We'll discuss it in a minute, after we plug this component into the app. Also note that when the form is submitted, the handleSubmit() method is triggered. We'll work more on that later.

Add the UserForm component to the template of the rootComponent like so (you can put it just under the user-list element):

<user-form 
    v-if="selectedUser" 
    :user="selectedUser" />

Note that a v-if directive is used to display the UserForm only if the selectedUser data member is not null (if there is no selected user, then there is nothing to display in the UserForm). Also note that the selectedUser is passed to the user prop of the UserForm.

Now run the app and click on a user from the UserList. This will set the selectedUser data member of the root component, which will cause the UserForm to appear. Remember that the selectedUser in the root component is bound to the user prop of the UserForm, which is why the selected user appears in the UserForm.

But notice what happens if you edit the firstName of a user - the UserList will automatically update, which is probablly not what you want. This is the result of allowing a prop to be mutated (edited). Since both the UserList and the UserForm are bound to the same user object, if a change occurs in one component, then Vue will automatically reflect the changes in the other component. The is the 'reactive' nature of Vue.

Once you start digging into Vue, you learn that you should never mutate a prop, because of the side effects it can have (any component that is bound to that prop will be affected). This caused me a fair amount of frustration earlier in my learning curve. It also led to confusion over the differences between data members and props (when should you declare data as a prop, and when should you declare it as a data member). Props are for passing data from a parent component to a child component. Data is supposed to be for private internal data used by a component. But in many cases you have something that meets both of those criteria.

One solution that seems to be broadly accepted is to use both a prop, and a data member. The trick is to make a copy of the prop for your data member. I actually wrote an article called Vue Props vs Data, or Both! on this, in case you are interested in digging a little deeper. So we'll go ahead and follow this approach in order to deal with the problem.

You are not allowed to have a prop and a data member that both have the same name in a Vue app, so we'll change name of the prop to 'initialUser' and add a data member called 'user'.

Update the name of the prop in the UserForm from 'user' to 'initialUser', like so:

props: {
    initialUser: {
        type: Object
    }
}

You'll also have to update it in the user-form element, which is in the template of the rootComponent:

<user-form 
    v-if="selectedUser" 
    :initialUser="selectedUser" />

Finally, add the following data method to the UserForm:

data(){
    return {
        user: {...this.initialUser}
    }
}

Notice how the user data member is initialized by creating a copy of the initialUser prop.

Now run the app, click on a user in the list, then change the first name in the form. You'll notice that the UserList does not reflect the changes made in the UserForm. This is because the UserForm is mutating a copy of the object that is displayed in the UserList. The User list is bound to the original, while the UserForm displays a copy.

If you play around with the app for a little bit, you'll notice that the UserForm only displays the first user that you select from the list. If you click on other users in the UserList, they do not appear in the UserForm. This is happening because the UserForm is binding it's template to a copy of the selected user. So when the selectedUser changes (by clicking on a different user in the UserList), the UserForm does nothing. We can force the UserForm to refresh and display the new selectedUser by adding a key attribute to the user-form element.

Update the user-form element (in the template of the rootComponent) to look like this:

<user-form 
    v-if="selectedUser" 
    :initialUser="selectedUser" 
    :key="selectedUser.id" />

In Vue, when a component's key changes, it will force the component to refresh both its props, and its UI. This is another helpful trick to know about when learning Vue. In the above code snippet, we have bound the UserForm's key attribute to the id of the selectedUser. When the selectedUser changes, the id of the selectedUser will also change, which will trigger the refresh.

In the next step, we'll figure out how to update the original user data when you edit a user in the UserForm component.

Your main.js file should look like this now:

var userData = [
    {id:1, firstName:"Jane", lastName:"Doe", email:"jdoe@acme.com"},
    {id:2, firstName:"Tony", lastName:"Thompsom", email:"tony@acme.com"},
    {id:3, firstName:"Jesse", lastName:"Jones", email:"jesse@acme.com"}
];

const rootComponent = {
    template:   `<div>
                    <h1>User Manager</h1>
                    <p>Number of users: {{users.length}}</p>
                    <br>
                    <button @click="addUser">Add User</button>
                    <!--We'll add a few Vue components here later-->
                    <user-list :users="users" @user-selected="handleUserSelected" />
                    <user-form 
                        v-if="selectedUser" 
                        :initialUser="selectedUser" 
                        :key="selectedUser.id" />
                </div>`,
    data(){
        return {
            users: userData,
            selectedUser: null
        }
    },
    methods:{
        addUser(){
            alert("TODO: Add User");
        },
        handleUserSelected(user){
            this.selectedUser = user  
            console.log("TODO: Show details for " + this.selectedUser.firstName);
        }
    }
};

const app = Vue.createApp(rootComponent);

// UserList component:
app.component("user-list", {
    props: {
        users: {
            type: Array,
            required: true
        }
    },
    template: `
        <div class="user-list">
            <h2>User List</h2>
            <ul>
                <li v-for="user in users" :key="user.id" @click="handleClick(user)">
                    {{ user.firstName + " " + user.lastName }}
                </li>
            </ul>
        </div>`,
    methods: {
        handleClick(user){
            this.$emit("user-selected", user);
        },
    }
});

// UserForm component
app.component("user-form", {
    template: `
        <div class="user-form-container">
            <h2>User Details</h2>
            <form @submit.prevent="handleSubmit">
                <div>
                    <label>First Name:</label>
                    <input v-model="user.firstName" />
                </div>
                <div>
                    <label>Last Name:</label>
                    <input v-model="user.lastName" />
                </div>
                <div>
                    <label>Email:</label>
                    <input v-model="user.email" />
                </div>
                <div>
                    <input type="submit" id="btnSubmit" name="submit button">
                </div>
            </form>
        </div>`,
    props: {
        initialUser: {
            type: Object
        }
    },
    methods:{
        handleSubmit(){
            console.log("TODO: handle the form submit");
        }
    },
    data(){
        return {
            user: {...this.initialUser}
        }
    }
});

app.mount('#app');
Next Step