Vue User Manager - Step 4

In the previous step, we started working on the UserForm component. In this step, we'll continue to work on it by adding input validation, and emitting an event to the parent (rootComponent) when a user has been updated.

Update the handleSubmit method in the UserForm component to look like this:

handleSubmit(){
    if (this.validate()) { 
        this.$emit("user-edited", this.user);
    }
}

Note that we have not yet defined the validate() method, we'll do that in just a minute. The validate() method will return true if all the properties of a user are entered correctly, and a 'user-edited' event will be emitted. Notice that the second parameter passed into the $emit() method (the event object), is the user data member. Remember that this is a copy of the user object that is passed into the UserForm's initialUser prop.

Before we add the validate method, we'll add a data member that will allow the component to display error messages if something is not entered correctly. Update the data method in the UserForm to look like this:

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

The errors data member will be an object that stores errors messages that will be displayed in the form.

In order to display the error messages, we'll bind them to SPAN tags in the template of the UserForm component. Go ahead and update the template to look like this (notice the SPAN tags that each display a specific property of the errors object):

<div class="user-form">
    <h2>User Details</h2>
    <form @submit.prevent="handleSubmit">
        <div>
            <label>First Name:</label>
            <input v-model="user.firstName" />
            <span class="validation" v-if="errors.firstName">{{errors.firstName}}</span>
        </div>
        <div>
            <label>Last Name:</label>
            <input v-model="user.lastName" />
            <span class="validation" v-if="errors.lastName">{{errors.lastName}}</span>
        </div>
        <div>
            <label>Email:</label>
            <input v-model="user.email" />
            <span class="validation" v-if="errors.email">{{errors.email}}</span>
        </div>
        <div>
            <input type="submit" id="btnSubmit" name="submit button">
        </div>
    </form>
</div>

Now go ahead and add the validate method to the UserForm:

validate() {

    // clear our any error messages from the previous submit
    this.errors = {};

    if (!this.user.firstName) {
        this.errors.firstName = "First name is required";
    }

    if (!this.user.lastName) {
        this.errors.lastName = "Last name is required";
    }
    
    if (!this.user.email) {
        this.errors.email = "Email is required";
    }else if(!this.validateEmailAddress(this.user.email)){
        this.errors.email = "The email address entered is not valid";
    }
    
    // if there are no keys in the errors object, then everything is valid
    return Object.keys(this.errors).length == 0;
},

And finally, we'll need to add the validateEmailAddress method to the UserForm, which is called from within the validate method:

validateEmailAddress(email){
    var regExp = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
    return regExp.test(email);
}

I suspect that there are many other (better) ways to validate input in Vue, but this is some code that I've used in the past, so it was convenient.

That should do it for the UserForm! When valid data is entered into the form, it will emit a 'user-edited' event and pass in the user object that has been edited.

The next step is to make some changes to the rootComponent so that it listens for 'user-edited' events. But we'll do one small thing first, so that we can use the app to add new users.

In the rootComponent, update the addUser method to look like this:

addUser(){
    this.selectedUser = {}; 
}

Remember (from step 1) that this method is triggered when the Add User button is pressed. And in step 3 we bound the key attribute of the UserForm to the id of the selectedUser. Setting the selectedUser to an empty object will force the UserForm to refresh and clear out all the data that is displayed in it's form.

The only thing left to do now is add code to the rootComponent that handles 'user-edited' events (which are emitted from the UserForm). Start out by updating the user-form element in the template of the rootComponent to look like this:

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

And finally, we need to define the handleUserEdited method:

handleUserEdited(user){
            
    let index = this.users.length;

    if (user.id > 0) {
        // An existing user just got edited
        // replace the element in this.users with the updated user
        // first get the index of the element to replace
        index = this.users.findIndex(u => u.id == user.id);
    } else {
        // A new user just got created (and they don't have an id yet, so we need to assign one)
        // To assign and id, first get the max id of all the users in the users data
        const highestId = this.users.reduce((maxId, u) => {
            return Math.max(maxId, u.id)
        }, 0); 

        // then add one to to the highestId and assign it to the new user
        user.id = highestId + 1;

    }

    // this will replace the element, but it won't force the user list to update
    this.users[index] = user;
    
    // this will clear out the form
    this.selectedUser = null;
}

When a 'user-edited' event is emitted, the rootComponent needs to determine whether a new user is being added, or if an existing user has been edited. To do this, we can look at the id of the user that was passed as the event object. If it has an id, then we know that a user is being updated. If it does not, then we know that a user is being added. When a user is added, we simply need to add the user object (the event object) to the users data array. When a user has been edited, we need to replace the existing user object in the users array with the one that was passed up in the event object. Hopefully the comments that I've included in the code sample help!

And there you have it! We've created a simple prototype that demonstrates some things to be aware of when creating Vue apps. Remember that you should never mutate a prop in a Vue component. Instead you can make a copy of the prop and edit that. If you do this, you may have to add a key attribute to the component so that you force it to refresh.

The final version of your main.js file should look like this:

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"
                        @user-edited="handleUserEdited" />
                </div>`,
    data(){
        return {
            users: userData,
            selectedUser: null
        }
    },
    methods:{
        addUser(){
            this.selectedUser = {}; 
        },
        handleUserSelected(user){
            this.selectedUser = user  
            console.log("TODO: Show details for " + this.selectedUser.firstName);
        },
        handleUserEdited(user){
            
            let index = this.users.length;
        
            if (user.id > 0) {
                // An existing user just got edited
                // replace the element in this.users with the updated user
                // first get the index of the element to replace
                index = this.users.findIndex(u => u.id == user.id);
            } else {
                // A new user just got created (and they don't have an id yet, so we need to assign one)
                // To assign and id, first get the max id of all the users in the users data
                const highestId = this.users.reduce((maxId, u) => {
                    return Math.max(maxId, u.id)
                }, 0); 
        
                // then add one to to the highestId and assign it to the new user
                user.id = highestId + 1;
        
            }
        
            // this will replace the element, but it won't force the user list to update
            this.users[index] = user;
            
            // this will clear out the form
            this.selectedUser = null;
        }
    }
};

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">
        <h2>User Details</h2>
        <form @submit.prevent="handleSubmit">
            <div>
                <label>First Name:</label>
                <input v-model="user.firstName" />
                <span class="validation" v-if="errors.firstName">{{errors.firstName}}</span>
            </div>
            <div>
                <label>Last Name:</label>
                <input v-model="user.lastName" />
                <span class="validation" v-if="errors.lastName">{{errors.lastName}}</span>
            </div>
            <div>
                <label>Email:</label>
                <input v-model="user.email" />
                <span class="validation" v-if="errors.email">{{errors.email}}</span>
            </div>
            <div>
                <input type="submit" id="btnSubmit" name="submit button">
            </div>
        </form>
    </div>`,
    props: {
        initialUser: {
            type: Object
        }
    },
    methods:{
        handleSubmit(){
            if (this.validate()) { 
                this.$emit("user-edited", this.user);
            }
        },
        validate() {

            // clear our any error messages from the previous submit
            this.errors = {};
        
            if (!this.user.firstName) {
                this.errors.firstName = "First name is required";
            }
        
            if (!this.user.lastName) {
                this.errors.lastName = "Last name is required";
            }
            
            if (!this.user.email) {
                this.errors.email = "Email is required";
            }else if(!this.validateEmailAddress(this.user.email)){
                this.errors.email = "The email address entered is not valid";
            }
            
            // if there are no keys in the errors object, then everything is valid
            return Object.keys(this.errors).length == 0;
        },
        validateEmailAddress(email){
            var regExp = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
            return regExp.test(email);
        }
    },
    data(){
        return {
            user: {...this.initialUser},
            errors: {}
        }
    }
});

app.mount('#app');