Basic list app with form validation and transition animations with Vue.js

Doing a app with form that ads stuff to list is a good practice project every time you try new language, library or framework. This time we look at Vue.js. We will also include form validation using VeeValidate and some transition animations using animate.css library.

First let’s install Vue CLI globally to our machine and then go to our folder and create a new vue project. Let’s also install our dependencies. In our case this means VeeValidate validation library.

npm install -g @vue/cli

vue create my-project
npm install vee-validate --save

Then we can import and init VeeValidate in main.js. This is how our main.js should look like.

import Vue from 'vue'
import App from './App.vue'
import VeeValidate from 'vee-validate'

Vue.use(VeeValidate)
Vue.config.productionTip = false

new Vue({
  render: h => h(App)
}).$mount('#app')

From App.vue let us remove all the extra stuff there is by default and just render our component. I will call our component memories. Vue components allways have three parts: Template which includes your HTML, Script which includes all your actual JavaScript code and Style, which includes your styles (you can also link extarnal stylesheets inside this). App.vue should look like this.

<template>
  <div id="app">
    <Memories msg="Memory Log" />
  </div>
</template>

<script>
import Memories from './components/Memories.vue'

export default {
  name: 'app',
  components: {
    Memories
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Ok. Let’s get coding. First we make a basic HTML structure with list and form.

<template>
  <div class="container">
    <h1>Our memory list</h1>
    <form>
      <input type="text" name="memory" placeholder="Enter new memory">
      <p class="error"></p>
      <select>
        <option value="Choose" disabled selected>Choose type</option>
        <option value="skills">Skills</option>
        <option value="milestones">Milestones</option>
      </select>
      <p class="error"></p>
      <input type="submit" value="Save"/>
    </form>
    <div class="grid">
      <ul>
        <li class="ghost"> <span class="type">Type: </span></li>
          <li>         
            <span class="type"></span>
            <i class="fa fa-close delete"></i>
          </li>
      </ul>
    </div>
  </div>
</template>

Then we add some styles. If you are wondering animate.css import, we will use that later. We will scope our styles to effect only this component by adding scoped attribute inside our style tag.

<style scoped>
@import 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.css';
@import 'https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css';

h3 {
  margin: 40px 0 0;
}
.container {
  width: 60%;
  max-width: 700px;
  margin: 0 auto;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: block;
  margin: 5px 0;
  padding: 25px;
  text-align: left;
  background: #666;
  color: white;
}
.ghost {
  background: whitesmoke;
  color: #666;
}
.type {
  font-weight: 700;
  margin-left: 45px;
}
a {
  color: #42b983;
}
input[type="text"] {
  height: 40px;
  text-align: center;
  background: whitesmoke;
  color: #666;
  font-size: 18px;
  width: 100%;
  border: 1px solid #666;
}
input[type="submit"] {
  height: 40px;
  text-align: center;
  background: #666;
  color: white;
  font-size: 18px;
  width: 100%;
}
select {
  height: 40px;
  text-align: center;
  font-size: 18px;
  width: 100%;
  border-radius: 0 !important;
}
.error {
  color: red;
}

.delete {
  font-size: 25px;
  float: right;
  cursor: pointer;
}

</style>

Then let’s add our data and logic. First part just exports our component so it can be rendered in App.vue. Then in data we will create our state. We will have array of objects named memories and two string elements that mirror those inside an object. memory and type outside our objects are for preview purposes. We will also bind our form to these and then push them to memories array on form submit.

We have two methods. One for adding user input to our list and one for removing list items. We will also add VeeValidate check for addMemory method to prevent adding item if it’s not valid.

<script>
export default {
  name: "Memories",
  data() {
    return {
      memory: "",
      type: "Choose",
      memories: [
        {
          memory: "This is an example memory",
          type: "milestone"
        }
      ]
    };
  },
  methods: {
    addMemory() {
      this.$validator.validateAll().then(result => {
        if (result) {
          this.memories.push({ memory: this.memory, type: this.type });
          this.type = "Choose";
          this.memory = "";
          this.errors.clear();

        } else {
          console.log("Not valid to submit");
        }
      });
    },
    deleteMemory(id) {
      this.memories.splice(id,1)
    }
  }
};
</script>

Then we combine our logic and template to our template.

First for form we add our addMemory method to be called on submit and prevent page from reloading.

Second we bind our text input to our memory item on our data. We will use v-model to do this. Let’s also add validation rules of minimum 3 letters to this field. After this we will display possible errors. For contional we will use v-if.  Then we will do same operations for type select field.

Third step: we have our ghost element. We only want to display this if user is typing something so let’s add conditional that it will only display if memory state is not empty. We can display data from our state simply by wrapping name on data item to double curly braces {{ memory }}.

Fourth step: Let’s loop our list using Vue’s practical vue-for.

<template>
  <div class="container">
    <h1>Memory list</h1>
    <form @submit.prevent="addMemory">
      <input type="text" name="memory" placeholder="Enter new memory" v-model="memory" v-validate="'min:3'">
      <p class="error" v-if="errors.has('memory')">{{ errors.first('memory')}}</p>
      <select v-model="type" v-validate="'required|not_in:Choose'" name="type">
        <option value="Choose" disabled selected>Choose type</option>
        <option value="skills">Skills</option>
        <option value="milestones">Milestones</option>
      </select>
      <p class="error" v-if="errors.has('type')&&memory!==''">{{ errors.first('type')}}</p>
      <input type="submit" value="Save"/>
    </form>
    <div class="grid">
      
      <ul>
        <li class="ghost" v-if='memory!==""'>{{ memory }} <span class="type">Type: {{ type }}</span></li>
          <li v-for="(data, index) in memories" :key='index'>
            {{ data.memory }} 
            <span class="type">Type: {{ data.type }}</span>
            <i class="fa fa-close delete" v-on:click="deleteMemory(index)"></i>
          </li>   
      </ul>
    </div>
  </div>
</template>

Then only thing to do is to add our animations. This will be done by wrapping our loop inside transition-group element and using enter-active-class lifecycle method to call animate.css library. Our final code will look like this.

<template>
  <div class="container">
    <h1>{{msg}}</h1>
    <form @submit.prevent="addMemory">
      <input type="text" name="memory" placeholder="Enter new memory" v-model="memory" v-validate="'min:3'">
      <p class="error" v-if="errors.has('memory')">{{ errors.first('memory')}}</p>
      <select v-model="type" v-validate="'required|not_in:Choose'" name="type">
        <option value="Choose" disabled selected>Choose type</option>
        <option value="skills">Skills</option>
        <option value="milestones">Milestones</option>
      </select>
      <p class="error" v-if="errors.has('type')&&memory!==''">{{ errors.first('type')}}</p>
      <input type="submit" value="Save"/>
    </form>
    <div class="grid">
      
      <ul>
        <li class="ghost" v-if='memory!==""'>{{ memory }} <span class="type">Type: {{ type }}</span></li>
        <transition-group name="item-in" enter-active-class="animated bounceInUp" leave-active-class="animated bounceOutDown">
          <li v-for="(data, index) in memories" :key='index'>
            {{ data.memory }} 
            <span class="type">Type: {{ data.type }}</span>
            <i class="fa fa-close delete" v-on:click="deleteMemory(index)"></i>
          </li>
        </transition-group>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  name: "Memories",
  props: {
    msg: String
  },
  data() {
    return {
      memory: "",
      type: "Choose",
      memories: [
        {
          memory: "This is a memory",
          type: "words"
        }
      ]
    };
  },
  methods: {
    addMemory() {
      this.$validator.validateAll().then(result => {
        if (result) {
          this.memories.push({ memory: this.memory, type: this.type });
          this.type = "Choose";
          this.memory = "";
          this.errors.clear();

        } else {
          console.log("Not valid to submit");
        }
      });
    },
    deleteMemory(id) {
      this.memories.splice(id,1)
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
@import 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.css';
@import 'https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css';

h3 {
  margin: 40px 0 0;
}
.container {
  width: 60%;
  max-width: 700px;
  margin: 0 auto;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: block;
  margin: 5px 0;
  padding: 25px;
  text-align: left;
  background: #666;
  color: white;
}
.ghost {
  background: whitesmoke;
  color: #666;
}
.type {
  font-weight: 700;
  margin-left: 45px;
}
a {
  color: #42b983;
}
input[type="text"] {
  height: 40px;
  text-align: center;
  background: whitesmoke;
  color: #666;
  font-size: 18px;
  width: 100%;
  border: 1px solid #666;
}
input[type="submit"] {
  height: 40px;
  text-align: center;
  background: #666;
  color: white;
  font-size: 18px;
  width: 100%;
}
select {
  height: 40px;
  text-align: center;
  font-size: 18px;
  width: 100%;
  border-radius: 0 !important;
}
.error {
  color: red;
}

.delete {
  font-size: 25px;
  float: right;
  cursor: pointer;
}

</style>