2. Adding Content through Form

Up until now we've added our ideas through the rails console, but that's a little bit inconvenient. Instead, let's create a form to add more ideas.

Setting Up Routes

The first thing we need to do is set up our routes. What we write in routes.rb defines how the web application is hooked up, or routed.

If we go to the terminal and type in rails routes, we can see all of our routes that are set up. Right now, all you should see is this:

Prefix Verb URI Pattern Controller#Action
  root GET  /           ideas#index

rails routes

rails routes is a command that you will be using a lot. The rails routes command displays how the application is mapped out. For example, let's take a look at the following:

Prefix Verb URI Pattern Controller#Action
  root GET  /           ideas#index

This is the routing for the home directory. As we talked about before, when a user accesses the home page, the index method of ideas_controller.rb is triggered.

We see four different terms above:

  • Prefix
  • Verb
  • URI Pattern
  • Controller#Action

Let's go through what each of these terms mean.

Prefix

Appending _path to the Prefix will generate the relative path - for example root_path will generate /.

On the other hand, you can also generate an absolute path by appending _url to the Prefix. For example, root_url will result in the full URL: https://techbrain-ideator.herokuapp.com/

URI Pattern

Notice how root_path resulted in creating a relative path of "/".

If we look back to the result of running rails routes, we can see that the URI Pattern is /. When a user accesses a URL that matches the URI Pattern, the Controller#Action is triggered.

Controller#Action

The Action (another word for method) of the Controller is triggered when a user makes a request to a URL that matches the URI Pattern.

For example, when a user goes to https://techbrain-ideator.herokuapp.com/, the URL matches the / URI Pattern. At this point, the index action of the Ideas Controller is triggered.

When a controller action is triggered, Rails will try to find an html.erb file with the name of the controller action inside the apps/views/controller_name directory.

For example, when the index action of the Ideas Controller is triggered, Rails will try to find a filed called index.html.erb inside the apps/views/ideas directory.

Verb

We won't go too deep into this topic right now, we will go over this topic in later lessons.

There are five different HTTP methods:

  • GET
  • POST
  • DESTROY
  • PUT
  • PATCH

Here's what each of the methods do in short (there is a whole chapter on this later on in the course):

  • GET requests are used to retrieve data. When you vist a web page, you are making a GET request.

    • When you type in https://www.facebook.com in your browser and press enter, you are making a GET request to Facebook's servers.
  • POST requests are used to create new objects.

    • When you create a Facebook post, you are making a POST request to Facebook's servers
  • DELETE requests are used to delete objects.

    • When you delete a Facebook post, you are making a DELETE request to Facebook's servers
  • PATCH requests are used to update objects.

    • When you update a Facebook post, you are making a PATCH request to Facebook's servers
  • PUT requests are not used anymore, use PATCH requests instead.

When a user types in a URL in the browser and presses enter, the user is making a GET request - the user is trying to retrieve data from the server.

For example, when a user accesses https://www.facebook.com, the user is making a GET request to the Facebook servers.

Let's go back to our problem. What we are trying to do right now is to create routes for the /ideas path as a POST request, so that we have a place to send the form data to.

Rails provides a convenient method called resources that allows us to generate routes that are most commonly used in web applications. Let's implement this and see how it works.

Go into routes.rb, and under root 'ideas#index', put resources :ideas. Your file should look like the following:

Rails.application.routes.draw do

  root 'ideas#index'

  resources :ideas

end

Save the file and run rails routes in the terminal. The output should now be this:

   Prefix Verb   URI Pattern               Controller#Action
     root GET    /                         ideas#index
    ideas GET    /ideas(.:format)          ideas#index
          POST   /ideas(.:format)          ideas#create
 new_idea GET    /ideas/new(.:format)      ideas#new
edit_idea GET    /ideas/:id/edit(.:format) ideas#edit
     idea GET    /ideas/:id(.:format)      ideas#show
          PATCH  /ideas/:id(.:format)      ideas#update
          PUT    /ideas/:id(.:format)      ideas#update
          DELETE /ideas/:id(.:format)      ideas#destroy

By putting in resources :ideas, rails generated all of these routes for us! Notice how now we have a /ideas path that handles POST requests.

resources

Web applications are made up of mainly the following components:

  • The Index Page - the page displays all of the records

    • When you go on to Facebook, it displays all of your friend's posts. This is the index page.
    • This is a GET request
  • The Show Page - the page that displays one individual record

    • In the index page, all of your friend's posts are displayed. When you click on a post, you can view the individual post alone. This is the show page.
    • This is a GET request
  • The New Page - the page where you typically fill out a form to create a new record

    • In Facebook, there is no New page since you can create a Facebook post from the front page. If there were a page only for creating a Facebook post, that would be the new page.
    • This is a GET request - we aren't actually posting to the server, this page typically just displays a form
  • The Create Action - the backend process that creates and saves a new record to the database

    • When you create a Facebook post, the create action is triggered. The create action takes care of creating the new post and saving it to the Facebook servers.
    • This is a POST request
  • The Edit Page - the page where there is typically a form where you can edit the record

    • In Facebook, there is no page for just editing a Facebook post. If there was, that would be the edit page.
    • This is a GET request
  • The Update Action - the backend process that updates a record into the database

    • When you edit a Facebook post and press submit, the update action is triggered.
    • This is a PATCH request
    • It is also mapped to PUT, but it is not used vey often
  • The Destroy Action - the backend process that deletes a record from the database

    • When you delete a naughty Facebook post, the destroy action is triggered and deletes the post from Facebook's server.
    • This is a DELETE request

Since these components are so common, Ruby on Rails made it super easy to create the routes for these actions.

When you add resources :controller_name to routes.rb, it automatically creates routes for the following actions that we talked about above:

  • index
  • show
  • new
  • create
  • edit
  • update
  • destroy

We can use these actions to create various features in our web application.

Now that we have our routing set up, we can send the form data to the /ideas path. If we look back at the output of rails routes, we see that a POST request to the /ideas path triggers the create action inside of ideas_controller.rb:

   Prefix Verb   URI Pattern               Controller#Action
    ideas POST   /ideas(.:format)          ideas#create

Right now, we don't have a create action inside of the ideas_controller.rb file.

Right now, when we trigger the form, the form data will be sent to the /ideas path and trigger the create action, but since we haven't created a create action, the form will not properly work.

image.png

Adding a Form

We're going to use a gem called simple_form that makes making forms super easy for us.

What are gems?

Gems are packages of code that often times solve common problems, for instance installing simple_form in this case.

Many common problems have been solved with gems, so it's usually a good idea to just implement the gems instead of re-implementing it yourself.

Let's go ahead and install our first gem.

In VScode, let's open up Gemfile. You'll see a list of gems already included in the application inside this file.

The documentation can be found here. Typically, the instructions on how to install and use these gems are written inside the documentation. Let's look at the documentation and follow the instructions.

Documentation

Many tools that are developed for software engineers come with documentation. Documentation provides a manual for how to use the tool and are useful in figuring out exactly how to use the tool.

As a web developer, you will spend a lot of time reading the documentation to try to figure out how to use various tools. That's why it's super important to get used to reading documentation.

The first thing it tells us to do is to add the gem to your Gemfile. So in your Gemfile, add this line:

gem 'simple_form', '~> 5.3'

Next, inside our terminal, let's run the following command:

bundle install

The bundle install command

Whenever you add a new gem to the Gemfile, you need to install it into the application. You can install the gem into your application by running the bundle install command. Running bundle install will install all of your new gems at once, and you will be using this command a lot as a Rails developer.

Every time we run bundle install, we should restart the server. Let's go back into the console where the server is running and restart it.

How to restart the server

To restart the server, first hit Control + C to stop the server. Then to start the server again, hit the up arrow and press enter, or type in bin/dev again.

Awesome. Now, if we look at the documentation, we see that there are several options on how to run the simple form generator. In our case, we are going to run the generator that integrates simple form. So let's run this command.

rails generate simple_form:install

Replace the simple_form.rb file inside config/initializers with the content from this link. This is to implement basic tailwind classes to simple form.

Awesome. Remember, every time we run bundle install, we should restart the server. Let's go ahead and do that.

Now that we've got simple form installed, let's go ahead and try to use it. At the top of index.html.erb, let's add this piece of code:

<%= simple_form_for Idea.new do |f| %>
  <%= f.input :description %>
  <%= f.input :author %>
  <%= f.submit 'Submit' %>
<% end %>

Simple form will detect what data type the form inputs should take in and generate HTML automatically.

The HTML that is generated will look something like this (some parts are abbreviated):

<form class="simple_form new_idea" id="new_idea" action="/ideas" accept-charset="UTF-8" method="post">
  <input type="hidden" name="authenticity_token" autocomplete="off">
  <div class="mb-4 text optional idea_description">
    <label class="block text optional text-sm font-medium text-gray-600" for="idea_description">Description</label>
    <textarea class="shadow appearance-none border border-gray-300 rounded w-full py-2 px-3 bg-white focus:outline-none focus:ring-0 focus:border-blue-500 text-gray-400 leading-6 transition-colors duration-200 ease-in-out text optional" name="idea[description]" id="idea_description"></textarea>
  </div>
  <div class="mb-4 string optional idea_author">
    <label class="block string optional text-sm font-medium text-gray-600" for="idea_author">Author</label>
    <input class="shadow appearance-none border border-gray-300 rounded w-full py-2 px-3 bg-white focus:outline-none focus:ring-0 focus:border-blue-500 text-gray-400 leading-6 transition-colors duration-200 ease-in-out string optional" type="text" name="idea[author]" id="idea_author">
  </div>
  <input type="submit" name="commit" value="Submit" data-disable-with="Submit">
</form>

Notice how textarea was chosen for description, since description is a text data type, and a string input field was chosen for author, since author is a string data type. Simple Form detects the datatypes for each form input and creates the appropriate input types.

Next, let's take a look at the first line of the form:

<form class="simple_form new_idea" id="new_idea" action="/ideas" accept-charset="UTF-8" method="post">

Notice how the action is automatically set to "/ideas" and the method is set to "post". Simple form also automatically detects where we want to send (action) the form and how (method).

The action is where the form data will be sent to when the user presses the submit button.

The method is the HTTP method that will be used when sending the data to the server. In this case, it is specified as "post". As mentioned in previous lessons, POST requests are used to create new instances.

So to recap, when a user presses submit on this form, the form will be sent to /ideas path as a POST request.

However, since we haven't created an /ideas path in our application, if we try to submit the form, it will just give us an error.

image.png

Setting up the Backend

In order to add ideas through a form, we need to tell rails what to do when the form is submitted and tries to create an idea. What we need is a create method which will be responsible for creating our ideas and saving them to our database.

In our ideas_controller, let's make a create method.

class IdeasController < ApplicationController
  def index
    @ideas = Idea.all
  end

  def create
  end
end

Inside this create action, we're going to call the create method on Idea.

When requests are sent in through forms, the data that is being sent looks like this:

Parameters: {"utf8"=>"✓", "authenticity_token"=>"Ew3W9K6QmcSG7HFTqhXbt0FUQOTYhG50Dm5ixVvxyeXIVm+dD1/g59ThIMI5IZ9OTh/rbaWO/7zt081/J/B1xA==", "idea"=>{"description"=>"Apples are better than pears.", "author"=>"Fruit Expert"}, "commit"=>"Submit"}

As we can see, the data being sent to the server is a hash. To review, hashes contain key value pairs. What we need to do is grab the values of the idea key and create an Idea object using those values.

image.png

We can access this data inside our controller with params. We can access the ideas with params[:ideas].

In other words, we are accessing the values for the idea key in the params hash:

params
=> {"utf8"=>"✓", "authenticity_token"=>"Ew3W9K6QmcSG7HFTqhXbt0FUQOTYhG50Dm5ixVvxyeXIVm+dD1/g59ThIMI5IZ9OTh/rbaWO/7zt081/J/B1xA==", "idea"=>{"description"=>"Apples are better than pears.", "author"=>"Fruit Expert"}, "commit"=>"Submit"}

params[:idea]
=> {"description"=>"Apples are better than pears.", "author"=>"Fruit Expert"}

As you can see, params[:idea] returns another hash.

To access the description and author, we can access the value for the description key, and access the value for the author key:

params[:idea][:description]
=> "Apples are better than pears."

params[:idea][:author]
=> "Fruit Expert"

Now that we know how to retrieve data that is sent into the server, we can use this data to create a new Idea and save it into the database.

Inside ideas_controller.rb, make the file look like the following:

class IdeasController < ApplicationController
  def index
    @ideas = Idea.all
  end

  def create
    @idea = Idea.create(description: params[:idea][:description], author: params[:idea][:author])
  end
end

If we submit the form, you will see that it works. However, we can refactor this code.

Let's take a look at the parameters we are passing into the create method:

description: params[:idea][:description], author: params[:idea][:author]

As we can see, we are passing in key value pairs.

When we submit a form with the description of "Apples are better than pears" and the author being "Fruit Expert", then the parameters will look like this:

description: "Apples are better than pears.", author: "Fruit Expert"

If we take a step back, this set of key-value pairs are the same as if we ran params[:idea].

In other words, this code:

Idea.create(params[:idea])

will run the same as this code:

Idea.create(description: params[:idea][:description], author: params[:idea][:author])

As we can see, writing Idea.create(params[:idea]) is shorter and more concise.

Security Issues

However, if we write Idea.create(params[:idea]), this exposes a set of security issues. That's why this code won't work - Rails detects that there's a security issue and prevents developers from doing this.

For example, let's say we had a boolean database column called admin for a users table.

A hacker could try and send data to the server like such:

{"user"=>{"name"=>"Hacker :D", "admin"=>"true"}}

Then, inside our users_controller.rb, let's say we had the following code:

class UsersController < ApplicationController
  def create
    User.create(params[:user])
  end
end

Since User.create(params[:user]) will create a User object based on the data passed into the server, the hacker can successfully create an admin account:

User.create(name: "Hacker :D", admin: true)

To prevent this, we need to implement strong parameters.

We can implement strong parameters by creating a method that whitelists only certain attributes.

We can do this by creating a private method called ideas_params:

private

def idea_params
  params.require(:idea).permit(:description, :author)
end

The code above whitelists description and author and will prevent users from sending in malicious data to the server.

The final step is to replace ideas[:params] with idea_params.

class IdeasController < ApplicationController
  def index
    @ideas = Idea.all
  end

  def create
    @idea = Idea.create(idea_params)
  end

  private

  def idea_params
    params.require(:idea).permit(:description, :author)
  end
end

When handling information in the server, you should always use strong parameters to whitelist what data can go into your application.

Awesome! Next, we need to tell rails what to do after we create the idea.

This is what we want to do:

We can redirect the user with the redirect_to method.

redirect_to

You can redirect users to a certain page in your application with redirect_to.

redirect_to root_path

The code above will redirect the user to root_path.

class IdeasController < ApplicationController
  def index
    @ideas = Idea.all
  end

  def create
    @idea = Idea.create(idea_params)
    redirect_to root_path
  end

  private

  def idea_params
    params.require(:idea).permit(:description, :author)
  end
end

Now let's test this out. Enter an idea and an author and press 'Submit'. Voila! You should see your idea on your page, just like that!

Deploying to Heroku (Optional)

After going through the standard git workflow, let's deploy all of the new changes that we have made to Heroku.

git push heroku master

Every time we push to Heroku, we should run migrations on the production server:

heroku run rails db:migrate

Then we should restart the server:

heroku restart
Lesson list