Creating Better UI
Transitions and animations are great ways of creating a better user experience within our applications. As there's so many different options and use cases, they can make or break the feel of an application if under or overused. We'll be looking at this concept further within this chapter.
We'll also be looking at form validation with a third-party library namedVuelidate. This will allow us to create forms that scale with the size of our application. We'll also gain the power to change the UI depending on form state, as well as display helpful validation messages to assist the user.
Finally, we'll look at how we can use therenderfunction and JSX to compose the user interface with Vue. While this is not perfect for every scenario, there are times where you'd want to take full advantage of JavaScript within your templates, as well as create smart/presentational components with the Functional Component model.
By the end of this chapter, you will have:
- Learned about CSS animations
- Created your own CSS animations
- Used Animate.css to create interactive UI with little work
- Investigated and created your own Vue transitions
- Taken advantage of Vuelidate to validate forms within Vue
- Used the render function as an alternative to template-driven UI
- Used JSX to compose UI similar to React
Let's start off by understanding why we should care about animation and transitions inside our project(s).
Animations
Animations can be used to draw focus to specific UI elements and to improve the overall experience for the user by bringing it to life. Animations should be used when there is no clear start state and end state. An animation can be set to play automatically or it can be triggered by user interaction.
CSS animations
CSS animations are not only a powerful tool, but they are also easy to maintain with little knowledge needed in order to use them within a project.
Adding them to an interface can be an intuitive method of capturing a user's attention and they can also be used in pointing a user to a specific element with ease. The animations can be tailored and customized, making them ideal for plenty of use cases within a variety of projects.
Before we dig deep into Vue transitions and other animated possibilities, we should have an understanding of how to do basic CSS3 animations. Let's create a simple project that looks at this in more detail:
# Create a new Vue project
$ vue init webpack-simple vue-css-animations
# Navigate to directory
$ cd vue-css-animations
# Install dependencies
$ npm install
# Run application
$ npm run dev
InsideApp.vuewe can first create the following styles:
<
style
>
button {
background-color: transparent;
padding: 5px;
border: 1px solid black;
}
h1 {
opacity: 0;
}
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
.animated {
animation: fade 1s;
opacity: 1;
}
<
/style
>
As you can see, nothing too out of the ordinary. We're declaring the CSS animation with@keyframesnamedfade, essentially giving CSS two states that we want our element to be in -opacity: 1andopacity: 0. It says nothing about how long or whether these keyframes are repeated; this is all done in theanimatedclass. We're applying thefadekeyframes for1s whenever we add the class to an element; at the same time, we're addingopacity: 1to ensure that it doesn't disappear after the animation has ended.
We can put this together by taking advantage ofv-bind:classto dynamically add/remove the class depending on the value oftoggle:
<
template
>
<
div id="app"
>
<
h1 v-bind:class="{ animated: toggle }"
>
I fade in!
<
/h1
>
<
button @click="toggle = !toggle"
>
Toggle Heading
<
/button
>
<
/div
>
<
/template
>
<
script
>
export default {
data () {
return {
toggle: false
}
}
}
<
/script
>
Cool. We now have the ability to fade in a heading based on aBooleanvalue. But what if we could do it better? In this particular circumstance, we could have used a transition to achieve similar results. Prior to looking at transitions in more detail, let's look at other ways we can use CSS animations inside our project.
Animate.css
Animate.cssis a great way of implementing different types of animation easily into your project. It's an open source CSS library created by Daniel Eden (https://daneden.me/) and it gives us access to "plug and play" CSS animations.
Prior to adding it to any project, head over tohttps://daneden.github.io/animate.css/and preview the different animation styles. There are a lot of different animations to choose from, with each offering a different default animation. These can be further customized and we’ll talk more about that later on in the section.
Go ahead and create a playground project by running the following in our Terminal:
Create a new Vue project
$ vue init webpack-simple vue-animate-css
# Navigate to directory
$ cd vue-animate-css
# Install dependencies
$ npm install
# Run application
$ npm run dev
Once the project is set up, go ahead and open it up in the editor of your choice and head to theindex.htmlfile. Inside the<head>tag, add the following stylesheet:
<
link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css"
>
This is the stylesheet reference needed forAnimate.cssto work on the project.
Using Animate.css
Now that we haveAnimate.cssinside the project, we can change ourApp.vueto have atemplatewith the following:
<
template
>
<
h1 class="animated fadeIn"
>
Hello Vue!
<
/h1
>
<
/template
>
Prior to adding any animations, we first need to add the animated class. Next, we can select any animation from theAnimate.csslibrary; we've chosenfadeInfor this example. This can then be switched out for other animations such asbounceInLeft,shake,rubberBand, and many more!
We could take our previous example, and turn this into a bound class value based on a Boolean - but transitions may be more exciting to look at.
Transitions
Transitionswork by starting off in one particular state and then transitioning into another state and interpolating the values in-between. A transition can't have multiple steps involved in an animation. Imagine a pair of curtains going from open to closed: the first state would be theopenposition, while the second state would be theclosedposition.
Vue has its own tags for dealing with transitions, known as<transition>and<transition-group>. These tags are customizable and can be easily used with JavaScript and CSS. There do not necessarily need to betransitiontags to make transitions work, as you simply bind the state variable to a visible property, but the tags typically offer more control and potentially better results.
Let's take thetoggleexample that we had before and create a version that usestransition:
<
template
>
<
div id="app"
>
<
transition name="fadeIn"
enter-active-class="animated fadeIn"
leave-active-class="animated fadeOut"
>
<
h1 v-if="toggle"
>
I fade in and out!
<
/h1
>
<
/transition
>
<
button @click="toggle = !toggle"
>
Toggle Heading
<
/button
>
<
/div
>
<
/template
>
<
script
>
export default {
data () {
return {
toggle: false
}
}
}
<
/script
>
Let's take a look at the moving parts in more detail.
We're surrounding the element inside a<transition>tag, which is applied toenter-active-classofanimated fadeInwhenever<h1>enters the DOM. This is triggered with thev-ifdirective as thetogglevariable is initially set tofalse. Clicking the button toggles our Boolean, triggering the transition and applying the appropriate CSS class.
Transition states
Every enter/leave transition applies up to six classes, which are made up of transitions upon entering the scene, during, and leaving the scene. Set one(v-enter-*)refers to Transitions initially entering and then moving out, while set two(v-leave-*)refers to ending transitions entering and then moving out:
Name | Description |
---|---|
v-enter | This is the very starting state for enter. It is removed one frame after the element is inserted. |
v-enter-active | enter-activeisenter's active state. It is active for the entirety of the active phase and is only removed once the transitions or animations have come to an end. This state also manages further instructions such as delays, duration, and so on. |
v-enter-to | This is the last state for enter, added one frame after the element is inserted, which is the same timev-enteris removed.Enter-tois then removed once the transition/animation ends. |
v-leave | This is the starting state for leave. Removed after one frame once a leave transition is triggered to take place. |
v-leave-active | leave-activeisleave's active state. It is active for the entirety of the leaving phase and is only removed once the transition or animation have come to an end. |
v-leave-to | The last state for leave, added one frame after a leave is triggered, which is the same timev-leaveis removed.Leave-tois then removed when the transition/animation ends. |
Eachenterandleavetransition features a prefix, which in the table is shown as the default value ofvbecause the transition itself has no name. When adding the enter or leave transitions into a project, ideally proper naming conventions should apply to act as unique identifiers. This can help if you plan on using multiple transitions within a project and can be done through a simple assignment operation:
<
transition name="my-transition"
>
Form validation
Throughout the book, we've looked at various different ways that we can capture user input with the likes ofv-model. We'll be using a third-party library namedVuelidateto perform model validation depending on a particular ruleset. Let's create a playground project by running the following in your Terminal:
# Create a new Vue project
$ vue init webpack-simple vue-validation
# Navigate to directory
$ cd vue-validation
# Install dependencies
$ npm install
# Install Vuelidate
$ npm install vuelidate
# Run application
$ npm run dev
What is Vuelidate?
Vuelidateis an open source, lightweight library that helps us perform model validation with a variety of validation contexts. Validation can be functionally composed and it also works well with other libraries such asMoment,Vuex, and more. As we've installed it in our project withnpm install vuelidate, we now need to register it as a plugin withinmain.js:
import Vue from 'vue';
import Vuelidate from 'vuelidate';
import App from './App.vue';
Vue.use(Vuelidate);
new Vue({
el: '#app',
validations: {},
render: h =
>
h(App),
});
Adding the empty validations object to our main Vue instance bootstraps Vuelidate's$vthroughout the project. This then allows us to use the$vobject to gain information about the current state of our form within our Vue instance across all components.
Using Vuelidate
Let's create a basic form that allows us to input afirstName,lastName,email, andpassword. This will allow us to add validation rules withVuelidateand visualize them on screen:
<
template
>
<
div
>
<
form class="form" @submit.prevent="onSubmit"
>
<
div class="input"
>
<
label for="email"
>
Email
<
/label
>
<
input
type="email"
id="email"
v-model.trim="email"
>
<
/div
>
<
div class="input"
>
<
label for="firstName"
>
First Name
<
/label
>
<
input
type="text"
id="firstName"
v-model.trim="firstName"
>
<
/div
>
<
div class="input"
>
<
label for="lastName"
>
Last Name
<
/label
>
<
input
type="text"
id="lastName"
v-model.trim="lastName"
>
<
/div
>
<
div class="input"
>
<
label for="password"
>
Password
<
/label
>
<
input
type="password"
id="password"
v-model.trim="password"
>
<
/div
>
<
button type="submit"
>
Submit
<
/button
>
<
/form
>
<
/div
>
<
/template
>
<
script
>
export default {
data() {
return {
email: '',
password: '',
firstName: '',
lastName: '',
};
},
methods: {
onSubmit(){
}
},
}
<
/script
>
There's a lot going on here, so let's break it down step by step:
- We're creating a new form with the @submit.prevent directive so that the page doesn't reload when the form is submitted, which is the same as calling the submit on this form and having preventDefault on the event
Next, we're adding
v-model.trim
to each form input element so that we trim any white space and capture the input as a variableWe're defining these variables inside of our data function so that they're reactive
The submit button is defined with the type="submit" so that when it's clicked the form's submit function is ran
- We're stubbing out a blank onSubmit function, which we'll be creating soon
Now we need to add the@inputevent and call thetouchevent on each one of ourinputelements, binding to the data propertyv-model, and providing validation to the field like so:
<
div class="input"
>
<
label for="email"
>
Email
<
/label
>
<
input
type="email"
id="email"
@input="$v.email.$touch()"
v-model.trim="email"
>
<
/div
>
<
div class="input"
>
<
label for="firstName"
>
First Name
<
/label
>
<
input
type="text"
id="firstName"
v-model.trim="firstName"
@input="$v.firstName.$touch()"
>
<
/div
>
<
div class="input"
>
<
label for="lastName"
>
Last Name
<
/label
>
<
input
type="text"
id="lastName"
v-model.trim="lastName"
@input="$v.lastName.$touch()"
>
<
/div
>
<
div class="input"
>
<
label for="password"
>
Password
<
/label
>
<
input
type="password"
id="password"
v-model.trim="password"
@input="$v.password.$touch()"
>
<
/div
>
We can then add the validations to our Vue instance by importing them fromVuelidateand adding avalidationsobject that corresponds to the form elements.
Vuelidatewill bind the same name set here with ourdatavariable like so:
import { required, email } from 'vuelidate/lib/validators';
export default {
// Omitted
validations: {
email: {
required,
email,
},
firstName: {
required,
},
lastName: {
required,
},
password: {
required,
}
},
}
We're simply importing the required email validators and applying them to each model item. This essentially makes sure that all of our items are required and that the email input matches an email regular expression. We can then visualize the current state of the form and each field by adding the following:
<
div class="validators"
>
<
pre
>
{{$v}}
<
/pre
>
<
/div
>
We can then add some styling to show the validation on the right and the form on the left:
<
style
>
.form {
display: inline-block;
text-align: center;
width: 49%;
}
.validators {
display: inline-block;
width: 49%;
text-align: center;
vertical-align: top;
}
.input {
padding: 5px;
}
<
/style
>
If everything has gone as planned, we should get the following result:
Displaying form errors
We can use the$invalidBoolean inside of the$v.model_nameobject (wheremodel_nameis equal toemail,firstName,lastName, orpassword) to display messages or change the look and feel of our form field(s). Let's start off by adding a new class namederrorthat adds aredborderaround the input field:
<
style
>
input:focus {
outline: none;
}
.error {
border: 1px solid red;
}
<
/style
>
We can then conditionally apply this class whenever the field is invalid and touched usingv-bind:class:
<
div class="input"
>
<
label for="email"
>
Email
<
/label
>
<
input
:class="{ error: $v.email.$error }"
type="email"
id="email"
@input="$v.email.$touch()"
v-model.trim="email"
>
<
/div
>
<
div class="input"
>
<
label for="firstName"
>
First Name
<
/label
>
<
input
:class="{ error: $v.firstName.$error }"
type="text"
id="firstName"
v-model.trim="firstName"
@input="$v.firstName.$touch()"
>
<
/div
>
<
div class="input"
>
<
label for="lastName"
>
Last Name
<
/label
>
<
input
:class="{ error: $v.lastName.$error}"
type="text"
id="lastName"
v-model.trim="lastName"
@input="$v.lastName.$touch()"
>
<
/div
>
<
div class="input"
>
<
label for="password"
>
Password
<
/label
>
<
input
:class="{ error: $v.password.$error }"
type="password"
id="password"
v-model.trim="password"
@input="$v.password.$touch()"
>
<
/div
>
This then gives us the following results whenever the field is invalid or valid:
Subsequently, we can then display an error message if this is the case. This can be done in numerous ways depending on the type of message you want to show. Let's use theemailinput as an example, and show an error message when theemailfield has an invalid email address:
<
div class="input"
>
<
label for="email"
>
Email
<
/label
>
<
input
:class="{ error: $v.email.$error }"
type="email"
id="email"
@input="$v.email.$touch()"
v-model.trim="email"
>
<
p class="error-message" v-if="!$v.email.email"
>
Please enter a valid email address
<
/p
>
<
/div
>
// Omitted
<
style
>
.error-message {
color: red;
}
<
/style
>
As we can see from the representation of our$vobject, the email Boolean is true when the field has a valid email address, and if not, is false. While this checks to see if email is correct, it doesn't check to see whether the field is empty. Let's add another error message that checks this based on therequiredvalidator:
<
p class="error-message" v-if="!$v.email.email"
>
Please enter a valid email address.
<
/p
>
<
p class="error-message" v-if="!$v.email.required"
>
Email must not be empty.
<
/p
>
If we wanted to, we could even take this a step further and create our own wrapper component that would render the various error messages of each field. Let's fill in the rest of our error messages along with a check to see whether the form element has been touched (is$dirty):
<
div class="input"
>
<
label for="email"
>
Email
<
/label
>
<
input
:class="{ error: $v.email.$error }"
type="email"
id="email"
@input="$v.email.$touch()"
v-model.trim="email"
>
<
div v-if="$v.email.$dirty"
>
<
p class="error-message" v-if="!$v.email.email"
>
Please enter a
valid email address.
<
/p
>
<
p class="error-message" v-if="!$v.email.required"
>
Email must not
be empty.
<
/p
>
<
/div
>
<
/div
>
<
div class="input"
>
<
label for="firstName"
>
First Name
<
/label
>
<
input
:class="{ error: $v.firstName.$error }"
type="text"
id="firstName"
v-model.trim="firstName"
@input="$v.firstName.$touch()"
>
<
div v-if="$v.firstName.$dirty"
>
<
p class="error-message" v-if="!$v.firstName.required"
>
First Name
must not be empty.
<
/p
>
<
/div
>
<
/div
>
<
div class="input"
>
<
label for="lastName"
>
Last Name
<
/label
>
<
input
:class="{ error: $v.lastName.$error}"
type="text"
id="lastName"
v-model.trim="lastName"
@input="$v.lastName.$touch()"
>
<
div v-if="$v.lastName.$dirty"
>
<
p class="error-message" v-if="!$v.lastName.required"
>
Last Name
must not be empty.
<
/p
>
<
/div
>
<
/div
>
<
div class="input"
>
<
label for="password"
>
Password
<
/label
>
<
input
:class="{ error: $v.password.$error }"
type="password"
id="password"
v-model.trim="password"
@input="$v.password.$touch()"
>
<
div v-if="$v.password.$dirty"
>
<
p class="error-message" v-if="!$v.password.required"
>
Password must
not be empty.
<
/p
>
<
/div
>
<
/div
>