At the moment, we simply save records into the database without checking if the data is valid or not. However, our application is built with the assumption that a post should always have a title, a body and an author. If we allow posts without a title, body or an author to be saved into the database, repercussions will surface in many places and cause bugs in our code. This would occur because of the mismatch of the integrity of data and the assumption of the application code.
As a general guideline, whenever an application allows user's input, we want to validate the data as early as possible. Let's look at how we can do that with our Post
model before the records are saved into the database.
### app/models/post.rb ###
class Post
# ...
def valid?
title.present? && body.present? && author.present?
end
# ...
end
We are adopting the validation rule that a post is only valid when title
, body
and author
fields are all present.
And notice when we check for the presence of each of the attributes, we're using the present?
method, which is a method that Rails provides:
> ''.present?
=> false
> nil.present?
=> false
> 'something'.present?
=> true
As you can see above, this present?
method returns false
if it's called on nil
or an empty string. Just what we're looking for to test if a title, body, or author is present.
And now that we have a way to check if a post is valid or not, let's make sure we do that before saving a post, and react appropriately:
### app/models/post.rb ###
class Post
# ...
def save
return false unless valid?
if new_record?
insert
else
update
end
true
end
# ...
end
Here we use what's called a guard clause:
return false unless valid?
We use this guard clause here to "guard" against execution further into Post#save
unless the post is valid. This prevents invalid posts from being saved to the database.
So now when we call Post#save
, we'll return true
if validation passes and the post is saved to the DB. Otherwise, if the validation fails and the post is not saved, we'll return false
.
We'll see how to integrate this model validation with the form submission workflow next.
Let's now look at how we can integrate the validation logic to the form submission process.
From a user experience point of view, when the user provides invalid inputs, we want to:
Let's see how to do this with the "create a post" workflow:
### app/controllers/application_controller.rb ###
class ApplicationController < ActionController::Base
# ...
def create_post
post = Post.new('title' => params['title'], 'body' => params['body'],
'author' => params['author'])
if post.save
redirect_to '/list_posts'
else
render 'application/new_post', locals: { post: post }
end
end
# ...
end
From this code you can see if save
returns false
, in the case of validation failure, we will render the new_post
template again with the post object that contains all the user's inputs.
We render
the new_post
view here because we need to provide the user with a form to keep editing the post.
We also have to change the new_post
view so that it would render with a post
object passed into it.
app/views/application/new_post.html.erb
<html>
<body>
<form method="post" action="/create_post">
<label for="title"> Title</label>
<input id="title" name="title" type="text" value="<%= post.title %>"/>
<br /> <br />
<label for="body"> Body</label>
<textarea id="body" name="body"><%= post.body %></textarea>
<br /> <br />
<label for="author"> Author</label>
<input id="author" name="author" type="text" value="<%= post.author %>"/>
<br /> <br />
<input type="submit" value="Create Post" />
</form>
<br />
<a href="/list_posts">Back to Posts</a>
</body>
</html>
With our view ready, we'll just need to adjust the new
controller action to pass in an instantiated "empty" post object.
### app/controllers/application_controller.rb ###
class ApplicationController < ActionController::Base
# ...
def new_post
post = Post.new
render 'application/new_post', locals: { post: post }
end
# ...
end
While we're at it, let's also alter our code for updating a post, as we'll want to include validations for that workflow as well.
class ApplicationController < ActionController::Base
# ...
def update_post
post = Post.find(params['id'])
post.set_attributes('title' => params['title'], 'body' => params['body'],
'author' => params['author'])
if post.save
redirect_to '/list_posts'
else
render 'application/edit_post', locals: { post: post }
end
end
# ...
end
<!-- app/views/application/edit_post.html.erb -->
<html>
<body>
<form method="post" action="/update_post/<%= post.id %>">
<label for="title"> Title</label>
<input id="title" name="title" type="text" value="<%= post.title %>"/>
<br /> <br />
<label for="body"> Body</label>
<textarea id="body" name="body"><%= post.body %></textarea>
<br /> <br />
<label for="author"> Author</label>
<input id="author" name="author" type="text" value="<%= post.author %>"/>
<br /> <br />
<input type="submit" value="Update Post" />
</form>
<br />
<a href="/list_posts">Back to Posts</a>
</body>
</html>
Now we have implemented 1) and 2) from the three requirements we listed before. We are going to tackle the displaying errors requirement in the next lesson.
NOTE: When you reload a page with a browser, it asks the browser to replay the last HTTP request. In the case of a form submission with errors, the last request will be a POST
request because the behavior on the server side is to render the form and not redirect to another path.
The last piece we need to do to ensure a good user experience, is to show the validation errors so the user knows what went wrong with their inputs.
Those messages should be created while we validate the user inputs, which currently happens in the Post
model. We could just store an array of error messages in the model. Since the model will be passed into the view template when it's rendered, the view template can then pull the error messages out of the model to display on the screen.
Here's what we'll do:
### app/models/post.rb ###
class Post
attr_reader :id, :title, :body, :author, :created_at, :errors
# ...
def initialize(attributes={})
# ...
@errors = {}
end
def valid?
@errors['title'] = "can't be blank" if title.blank?
@errors['body'] = "can't be blank" if body.blank?
@errors['author'] = "can't be blank" if author.blank?
@errors.empty?
end
# ...
end
The blank?
method is defined by Rails to work on nil
and strings with the following behavior:
> ''.blank?
=> true
> nil.blank?
=> true
> 'something'.blank?
=> false
As you probably have guessed, blank?
is the opposite of present?
Now that we know an invalid post created from user input would always have errors set in the object, we can adjust our view to show the error messages:
app/views/application/new_post.html.erb
<html>
<body>
<div class="errors">
<% post.errors.each do |attribute, error| %>
<p class="error" style="color: orange">
<%= attribute %>: <%= error %>
</p>
<% end%>
</div>
<form method="post" action="/create_post">
<label for="title"> Title</label>
<input id="title" name="title" type="text" value="<%= post.title %>"/>
<br /> <br />
<label for="body"> Body</label>
<textarea id="body" name="body"><%= post.body %></textarea>
<br /> <br />
<label for="author"> Author</label>
<input id="author" name="author" type="text" value="<%= post.author %>"/>
<br /> <br />
<input type="submit" value="Create Post" />
</form>
<br />
<a href="/list_posts">Back to Posts</a>
</body>
</html>
Make sure to include the changes we've made above in your edit_post.html.erb
file as well.
Now is a good time to run through our app and test the code that we have just added in. Try making a post with invalid inputs; the new post view should render and the application should display a list of errors.