Rails: Models, Forms & Validations

Views & Forms

Creating a View

You follow these same four steps every time you want to create a view. You don't have to repeat a step if you've already done it, though!

  1. Create the controller class

    # app/controllers/places_controller.rb
    class PlacesController < ApplicationController
    end
    
  2. Create the action method

    # app/controllers/places_controller.rb
    class PlacesController < ApplicationController
      def index
      end
    end
    
  3. Create the routes for the controller & action method

    # config/routes.rb
    resources :places
    
  4. Create the view template file.

    <!-- app/views/places/index.html.erb -->
    <h1>Index!</h1>
    

CRUD views

We've talked a lot about index and show, but what about the rest of the methods?

HTTP Verb Path Controller#Action Used for
GET /articles articles#index display a list of all articles
GET /articles/new articles#new return an HTML form for creating a new article
POST /articles articles#create create a new article
GET /articles/:id articles#show display a specific article
GET /articles/:id/edit articles#edit return an HTML form for editing a article
PATCH/PUT /articles/:id articles#update update a specific article
DELETE /articles/:id articles#destroy delete a specific article

new/create

The new and create methods are paired together. The new method will display an empty form to the user. The create method is the submission action of the form. When a user clicks submit, the form will POST to the create method.

new

  1. Instantiate (not create) a new model
  2. Pass that model into a form_for

create

  1. Instantiate (not create) a new model
  2. Use strong parameters to parse the params hash for form data
  3. Apply the parsed parameters to the new model
  4. Save the model
  5. If it succeeds, redirect to the show page
  6. If it fails, render the new page.

edit/update

Just like new/create, the edit and update methods are paired together in the same way. The edit method displays a form for the user, and the update method.

edit

  1. Find the model in the database that was passed in by params[:id]
  2. Create a form using form_for with that model

update

  1. Find the model in the database that was passed in by params[:id]
  2. Use strong parameters to parse the params hash for form data
  3. Apply the parameters to the model
  4. If it succeds, redirect to the show page
  5. If it fails, render the edit page.

destroy

The destroy method simply deletes a model.

  1. Find the model in the database that was passed in by params[:id]
  2. Delete it
  3. Redirect to the index page.

Creating a form

Rails uses the form_for helper to quickly and easily generate a form. Rails understands the state of a model, and will correctly choose what the destination of the form is, and the form method.

# app/controllers/places_controller.rb
class PlacesController < ApplicationController
  def new
    @place = Place.new
  end
end
<!-- app/views/places/new.html.erb -->
<%= form_for @place do |f| %>
  <p>
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </p>

  <p>
    <%= f.label :description %><br>
    <%= f.text_area :description %>
  </p>

  <p>
    <%= f.submit %>
  </p>
<% end %>

Strong Parameters

When creating forms, make sure that you're using strong parameters in your controller! Rails will give you an error if you don't, so it's hard to forget.

However, it is easy to forget to update your strong parameters when you add columns to your models.

# app/controllers/places_controller.rb
  # ...

  private
  def place_params
    params.require(:place).permit(:name, :description)
  end
end

If you add another field to the form, make sure you update your strong parameters so you don't filter out data you want to save!

Partials

What if you want to share code between different views? The new.html.erb and edit.html.erb forms are usually identical. Remember, don't repeat yourself? That's what view partials are for.

Partials are just a regular view template, except their names always start with an underscore, like _form.html.erb.

Here's how we use that with the new and edit actions.

<!-- app/views/places/new.html.erb -->
<%= render 'form' %>
<!-- app/views/places/edit.html.erb -->
<%= render 'form' %>
<!-- app/views/places/_form.html.erb -->
<%= form_for @place do |f| %>
  <p>
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </p>

  <p>
    <%= f.label :description %><br>
    <%= f.text_area :description %>
  </p>

  <p>
    <%= f.submit %>
  </p>
<% end %>

As you can see, a view partial allows you to share view code between multiple views by using the render method.

HTML is a repetitive language though, because it defines the structure of a document. Don't overuse partials. You should think of a partial when you have a kind of component of a page, like a sidebar, a form, a comments box, etc.

Layouts

The HTML on each view is very basic. So where does all of that extra HTML, like the head and body tag, come from in our rendered web pages? Layouts!

Layouts are simply a "top level" view that Rails will automatically render and put your code inside of.

The default layout is app/views/layouts/application.html.erb. Any application-specific CSS, JavaScript, Google Fonts, etc. should go in this file, along with any site-wide nav bars, login buttons, etc.

You usually don't need more than one layout, and by the time you do, you'll know a lot more about Rails.

Advanced Models

Validations

From the Rails Guides on Active Record Validations:

Validations are used to ensure that only valid data is saved into your database. For example, it may be important to your application to ensure that every user provides a valid email address and mailing address. Model-level validations are the best way to ensure that only valid data is saved into your database.

Validations also us to display error messages on our forms if the users enter invalid data.

We'll outline a few examples, but you'll have to dig into the Active Record Validations for all of the options; there are a lot!

presence

If we want to require the name column for our Place model, we would do this:

class Place < ActiveRecord::Base
  validates :name, presence: true
end

length

Sometimes we want to set a maximum or minimum value for a text field. We might want our Place model to require the name to be at least 4 characters, but no more than 50.

class Place < ActiveRecord::Base
  validates :name, length: { minimum: 4, maximum: 50 }
end

uniqueness

It's very common for us to require that data in a column be unique. The most common example is a username or an email address that a user uses to log in. If two users had the same username, we'd have no way of knowing who to log in!

In our Lekker Plekke app, we don't want users to enter the same location twice, so we'll require that the name be unique.

class Place < ActiveRecord::Base
  validates :name, uniqueness: true
end

numericality

We use numbers in our databases a lot. For Lekker Plekke, we're storing the number of likes that a post has. What does 4.5 likes mean? What about -5 likes? That is meaningless data, so we'll create validation that requries our data to be a specific type of number.

class Place < ActiveRecord::Base
  validates :likes, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
end

This ensures that our likes are always a whole number, and that they're always 0 or greater.

Checking errors

If your model tries to save and fails, Rails will update the .errors object with information about the errors that happened during save.

>> place = Place.new
>> place.save
=> false
>> place.errors
=> #<ActiveModel::Errors:0x007fa35bfe8908
 @base=
  #<Place:0x007fa35f959b88
   id: nil,
   name: nil,
   description: nil,
   created_at: nil,
   updated_at: nil,
   address: nil>,
 @messages={:name=>["can't be blank"]}>

# Access the errors by using column names as a hash key:
>> place.errors[:name]
=> ["can't be blank"]

# Get the full error messages for display with `.full_messages`
>> place.errors.full_messages
=> ["Name can't be blank"]

Displaying error messages on forms

You should always display errors in the user input in the form, so the users know that something was wrong. There are many ways of making this prettier, but here's a basic "good enough" way of doing it.

<%= form_for @place do |f| %>
  <% if @place.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@place.errors.count, "error") %> prohibited this place from being saved:</h2>

      <ul>
      <% @place.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <!-- your form stuff here -->
<% end %>

Associations

How do we tell Rails that two models are related to each other? We use model associations.

This is a common use case:

When to use Associations

These are ordered by most common to least common.

association description
has_many When one object has many of another, like a user's photos
belongs_to Always the other side of a has_many or has_one association
has_many :through When one object has many of another, and there's a model connecting them
has_and_belongs_to_many When both objects have_many of each other. Movies have_many genres, and Genres have_many movies.
has_one An association where the model only has one. Users have_one UserProfile
has_one :through An association where the model only has one, and there's another model connecting them.

Foreign Key

A foreign key is the column that contains a model's id that references another object. The foreign key is always named othermodel_id. The foreign key is always on the table that belongs to another model.

This is easier to explain with an example. If our Movies have many Reviews, this is what it would look like.

# app/models/movie.rb
class Movie
  has_many :reviews
end
# app/models/review.rb
class Review
  belongs_to :movie
end

Because our Review model has the belongs_to, it is the model with the foreign key. This is always how it works. Reviews must know which movie they belong to. Let's peek at the schema.rb file to see how it looks in the database.

# db/schema.rb
create_table "movies", force: :cascade do |t|
  t.string   "title"
  t.text     "description"
  t.datetime "created_at",  null: false
  t.datetime "updated_at",  null: false
end

create_table "reviews", force: :cascade do |t|
  t.integer  "movie_id"
  t.text     "body"
end