Adapt this principle to any form where you want custom error messages.
Here I will show how I did client and server side form validations for one of my recent projects. I will split this blog post into 2 parts:
- React (client side) form validations
- Rails (server side) validations.
We will make a user sign up form that looks like this:
I chose a sign up form as an example because this is where you want to be very careful when creating new entries to your database.
So let’s get into it.
CLIENT SIDE (React):
Make a component called SignUpForm.js
and in the state we will have:
state = {
username: '',
email: '',
password: '',
errors: {},
}
Errors is where we will push our custom error messages, such as:
We render
the form with div
s for potential error messages:
render() {const { username, email, password } = this.statereturn (
<div id='signUp'>
<h3>Sign up</h3>
<form onSubmit={this.handleSubmit} >
<label>Username:
<input
className="input"
type='text'
name='username'
value={username}
onChange={this.handleChange}
/>
</label>
<div className='errorMsg'>{this.state.errors.username}</div> <label>Email:
<input
className="input"
type='email'
name='email'
value={email}
onChange={this.handleChange}
/>
</label>
<div className='errorMsg'>{this.state.errors.email}</div> <label>Password:
<input
className="input"
type='password'
name='password'
value={password}
onChange={this.handleChange}
/>
</label>
<div className='errorMsg'>{this.state.errors.password}</div>
<input type='submit' className='button' value='Sign Up'/>
</form>
</div>
)
}
Now let’s build out the validations we want for each input field and add custom error messages we want to throw:
validateForm = () => {
let errors = {}
let formIsValid = trueif (!this.state.username) {
formIsValid = false
errors['username'] = '*Please enter your username'
}if (this.state.username) {
if (!this.state.username.match(/^\w+$/)) {
formIsValid = false
errors['username'] = '*Please use alphanumeric characters only'
}
}if (!this.state.email) {
formIsValid = false
errors['email'] = '*Please enter your email'
}if (this.state.email) {
//regular expression for email validation
let pattern = new RegExp(/^(('[\w-\s]+')|([\w-]+(?:\.[\w-]+)*)|('[\w-\s]+')([\w-]+(?:\.[\w-]+)*))(@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(@\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,2}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\]?$)/i);
if (!pattern.test(this.state.email)) {
formIsValid = false
errors['email'] = '*Please enter valid email'
}
}if (!this.state.password) {
formIsValid = false
errors['password'] = '*Please enter your password'
}if (this.state.password) {
if (!this.state.password.match(/^.*(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,}).*$/)) {
formIsValid = false
errors['password'] = '*Please enter secure and strong password'
}
}this.setState({ errors })return formIsValid}
Feel free to use different regex
and make it as relaxed or as strict as you wish, this is just an example of what it could be.
So we don’t just accept fields that are not empty, we also check if the format is what we allow, if not, we throw another round of errors:
Then we have our handleChange
method:
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value
})
}
… and handleSubmit
, where we will use our validateForm()
method:
handleSubmit = (e) => {
e.preventDefault()
if (this.validateForm()) {
createUser(this.state)
}
}
createUser
is a method we imported:
import { createUser } from '../adapter/api'
Which will send our user object to the backend:
export const createUser = (user) => {
return fetch(`http://localhost:3000/users`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify(user)
})
.then(res => res.json())
}
Where HEADERS
are:
const HEADERS = {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
See CSS I used here.
This is it for the front end validations. We prevent the form being submitted with inputs we don’t allow.
Take a closer look here: SignUpForm.js GitHub repo
Now part 2. Here we want to check if username or email already exists and throw appropriate errors.
SERVER SIDE (Rails):
We will create users
table in our database with this migration:
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :username
t.string :email
t.string :password_digest
t.timestamps
end
end
end
In user.rb
model I will require all values to be present (we do that with our client side validations already) and I also require username
and email
to be unique =>
no two users with the same username or email:
class User < ApplicationRecord
has_secure_password validates :username, presence: true, uniqueness: true
validates :email, presence: true, uniqueness: true
validates :password, presence: true
end
You will need jwt
(issue web token) and bcrypt
(hash password) gems for this.
So from the front end we sent a POST
request to http://localhost:3000/users
, which takes us to the users_controller.rb
create method.
user.valid?
will run our presence and uniqueness validations we have in user.rb
model. If all good, we will issue a web token (user is in).
ELSE there are two things that can go wrong:
- username is not unique
- email is not unique
And that’s where we can throw custom errors, such as:
def create
user = User.create(username: params[:username], email: params[:email], password: params[:password])
if user.valid?
render json: { token: issue_token({ id: user.id }) }
else
user = User.find_by(username: params[:username])
if user
render json: {error: "*Username already exists"}
else
render json: {error: "*Email already exists"}
end
end
end
To issue a token, we will need to add a few lines in our application_controller.rb
class ApplicationController < ActionController::API def issue_token(payload)
JWT.encode(payload, secret)
end def secret
'Polar Bears' //be creative here and keep it secret
endend
And now let’s go back to our front end handleSubmit
method and allow these errors to see the daylight:
handleSubmit = (e) => {
e.preventDefault()
if (this.validateForm()) {
createUser(this.state)
.then(res => {
if(res.error) {
let errors = {}
if (res.error === "*Username already exists"){
errors['username'] = res.error
}else {
errors['email'] = res.error
}
this.setState({ errors })
}else {
console.log("USER SUCCESSFULLY CREATED")
this.props.handleLogin(res) // res is a token
}
})
}
}
We get back a response, if response.error,
remember we can only have two different errors, and we can show them where appropriate — either under username or email field.
If there are no errors, we get a token
that we then can use and set in localStorage
and play the login/logout
game which is a great topic for another blog post.
Thank you for reading! Next steps:
Was this useful? How do you go about form validations?
Let me know on Twitter =>
@AllegraCodes