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
>
Password validation
When creating user accounts, passwords tend to be entered twice and conform to a minimum length. Let's add another field and some more validation rules to enforce this:
import { required, email, minLength, sameAs } from 'vuelidate/lib/validators';
export default {
// Omitted
data() {
return {
email: '',
password: '',
repeatPassword: '',
firstName: '',
lastName: '',
};
},
validations: {
email: {
required,
email,
},
firstName: {
required,
},
lastName: {
required,
},
password: {
required,
minLength: minLength(6),
},
repeatPassword: {
required,
minLength: minLength(6),
sameAsPassword: sameAs('password'),
},
},
}
We've done the following:
- Added the repeatPassword field to our data object so that it can hold the repeated password
- Imported both the minLength and sameAs validators from Vuelidate
- Added the minLength of 6 characters to the password validator
- Added the sameAs validator to enforce the fact that repeatPassword should follow the same validation rules as password
As we now have appropriate password validation, we can add the new field and display any error messages:
<
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
>
<
div class="input"
>
<
label for="repeatPassword"
>
Repeat Password
<
/label
>
<
input
:class="{ error: $v.repeatPassword.$error }"
type="password"
id="repeatPassword"
v-model.trim="repeatPassword"
@input="$v.repeatPassword.$touch()"
>
<
div v-if="$v.repeatPassword.$dirty"
>
<
p class="error-message" v-if="!$v.repeatPassword.sameAsPassword"
>
Passwords must be identical.
<
/p
>
<
p class="error-message" v-if="!$v.repeatPassword.required"
>
Password must not be empty.
<
/p
>
<
/div
>
<
/div
>
Form submission
Next, we can disable ourSubmitbutton if the form is not valid:
<
button :disabled="$v.$invalid" type="submit"
>
Submit
<
/button
>
We can also get this value inside of our JavaScript withthis.$v.$invalid. Here's an example of how we can check to see whether the form is invalid and then create a user object based on our form elements:
methods: {
onSubmit() {
if(!this.$v.$invalid) {
const user = {
email: this.email,
firstName: this.firstName,
lastName: this.lastName,
password: this.password,
repeatPassword: this.repeatPassword
}
// Submit the object to an API of sorts
}
},
},
If you'd like to use your data in this fashion, you may prefer to set up your data object like so:
data() {
return {
user: {
email: '',
password: '',
repeatPassword: '',
firstName: '',
lastName: '',
}
};
},
We have now created a form with appropriate validation!
Render/functional components
We're going to take a detour and pivot away from validation and animations to consider the use of functional components and render functions to improve application performance. You may also hear these being referred to as "presentational components" as they're stateless and only receive data as an input prop.
So far, we've only declared the markup for our components with thetemplatetag, but it's also possible to use therenderfunction (as seen insrc/main.js):
import Vue from 'vue'
import App from './App.vue'
new Vue({
el: '#app',
render: h => h(App)
})
Thehcomes from hyperscript that allows us to create/describe DOM nodes with our JavaScript. In therenderfunction, we're simply rendering theAppcomponent and in the future, we'll be looking at this in more detail. Vue creates a Virtual DOM to make working with the actual DOM much simpler (as well as for improved performance when dealing with a vast amount of elements).
Rendering elements
We can replace ourApp.vuecomponent with the following object that takes arenderobject andhyperscriptinstead of usingtemplate:
<script>
export default {
render(h) {
return h('h1', 'Hello render!')
}
}
</script>
This then renders a newh1tag with the text node of'Hello render!'and this is then known as aVNode(Virtual Node) and the pluralVNodes(Virtual DOM Nodes), which describes the entire tree. Let's now look at how we can display a list of items inside of aul:
render(h){
h('ul', [
h('li', 'Evan You'),
h('li', 'Edd Yerburgh'),
h('li', 'Paul Halliday')
])
}
It's important to realize that we can only render one root node with hyperscript. This restriction is the same for our template, so it's expected that we wrap our items inside of adivlike so:
render(h) {
return h('div', [
h('ul', [
h('li', 'Evan You'),
h('li', 'Edd Yerburgh'),
h('li', 'Paul Halliday')
])
])
}
Attributes
We can also pass style elements and a variety of other attributes to our rendered items. Here's an example that uses thestyleobject to change the color of each itemred:
h('div', [
h('ul', { style: { color: 'red' } }, [
h('li', 'Evan You'),
h('li', 'Edd Yerburgh'),
h('li', 'Paul Halliday')
])
])
As you can imagine, we can add as manystyleattributes as we want, as well as extra options that we would expect, such asprops,directives,on(click handlers), and so on. Let's look at how we can map over elements to render a component withprops.
Components and props
Let's create ourselves aListItemcomponent undercomponents/ListItem.vuewith one prop,name. We'll render this component in place of ourliand map over an array that contains variousnames. Notice how we're also adding thefunctional: trueoption to our Vue instance; this tells Vue that this is purely a presentational component and it will not have any state of its own:
<
script
>
export default {
props: ['name'],
functional: true
}
<
/script
>
With ourrenderfunction,his often also referred to ascreateElement, and because we're in the JavaScript context, we're able to take advantage of array operators such asmap,filter,reduce, and so on. Let's replace the static names for dynamically generated components withmap:
import ListItem from './components/ListItem.vue';
export default {
data() {
return {
names: ['Evan You', 'Edd Yerburgh', 'Paul Halliday']
}
},
render(createElement) {
return createElement('div', [
createElement('ul',
this.names.map(name =>
createElement(ListItem,
{props: { name: name } })
))
])
}
}
The final thing we need to do is add arenderfunction to our component. As a second parameter, we're able to gain access to the context object, which allows us to accessoptionssuch as ourprops. For this example, we'll assume that thenameprop is always present and isn'tnullorundefined:
export default {
props: ['name'],
functional: true,
render(createElement, context) {
return createElement('li', context.props.name)
}
}
Once again, we now have a list of elements that includes items passed as aprop:
JSX
Although this is a great thought exercise, templates are superior in most cases. There may be times where you want to use the render function inside of your components and, in these circumstances, it may be simpler to use JSX.
Let's add the babel plugin for JSX into our project by running the following in our Terminal:
$ npm i -D babel-helper-vue-jsx-merge-props babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx
We can then update our.babelrcto use the new plugin:
{
"presets": [
["env", { "modules": false }],
"stage-3"
],
"plugins": ["transform-vue-jsx"]
}
This allows us to rewrite ourrenderfunction to take advantage of a simpler syntax:
render(h) {
return (
<div>
<ul>
{ this.names.map(name => <ListItem name={name} />) }
</ul>
</div>
)
}
This is much more declarative and is also easier to maintain. Under the hood, it's being transpiled down to the previoushyperscriptformat with Babel.
Summary
In this chapter, we learned how to take advantage of CSS animations and transitions within our Vue projects. This allows us to make the user experience more fluid and improve the look and feel of our applications.
We also learned about how we can construct our UI with therendermethod; this involved looking at creating VNodes with HyperScript and then using JSX for cleaner abstraction. While you may not want to use JSX in your project, you may find it more comfortable if you come from a React background.