Exploring how a new model and UI is created in Rails using `rails generate scaffold`

16 May 2020
·
rails

As a newbie to Rails and Ruby, I found the code generated by the rails generate scaffold command initially very hard to understand. A lot of Googling later, I think I have a better understanding of what things are doing, and so I decided to write a post about it!

Running the generate scaffold command

If we were to create a to-do list app, we would need a way of storing our tasks in a database. With Rails, we can do this using something called a model - which acts like a wrapper aorund a database table.

In our case, we are going to need a table that stores tasks, and so we need to create a Task model that stores the task's name, due date, and whether it has been completed or not.

We can call rails generate scaffold with the name of our model, and any number of arguments in the shape fieldName:dataType:

rails generate scaffold Task name:string done:boolean due:datetime

Note that you don't need to specify an ID - Rails will handle this one for you.

If you're not sure what data types to use for your fields, I found this StackOverflow answer on data types really useful.

One of the new files created by this command will be a new file in db/migrate that will look something like this:

class CreateTasks < ActiveRecord::Migration[6.0]
  def change
    create_table :tasks do |t|
      t.string :name
      t.boolean :done
      t.datetime :due

      t.timestamps
    end
  end
end

To create a table of tasks in our database, we will need to run that file. We can do that with the following:

rails db:migrate

Now that your database can handle storing tasks you can start up your app:

rails s --binding=127.0.0.1

You will see that at http://localhost:3000/tasks, Rails has generated a basic UI for you that lets you create, delete and edit tasks - all for free! (I was actually pretty amazed by this). I recommend having a little play around with it and creating a few tasks.

In the next section, we'll be exploring how all this UI has been generated for us.

How tasks routing works

If we take a look at config/routes.rb we will see a new line has been added:

resources :tasks

This is a shorthand for 7 separate routes that allow you to view, modify and create tasks.

e.g. two of them would be:

get '/tasks' 'tasks#index'
post '/tasks' 'tasks#create'

Each line maps a URL to a controller and action, so if we make a GET call to /tasks, we will execute the code inside of the index action in the tasks controller.

What's happening in the tasks controller?

The tasks controller is another file that has been generated for us at app/controllers/tasks_controller.rb. I'm going to break down some of what is happening.

before_action

Right up the top of the file, you'll see this:

before_action :set_task, only: [:show, :edit, :update, :destroy]

Here we're saying that the set_task method (which is defined near the bottom of the file) should be called before the given list of actions (show, edit, update and destroy).

The colon (:) before set_task indicates that it is a Symbol, which is like a reference to the method. If we didn't use the colon, before_action would use the results of calling the set_task method.

set_task

That set_task method does the following:

def set_task
    @task = Task.find(params[:id])
end
  • @task - In Ruby, the @ defines a variable as an instance variable, which means it can be accessed outside of where it has been defined (even in the view file!)
  • Task - Refers to the Task class that lives in models/task.rb.
  • Task.find() - we can call find(id) on the model, which will find the task that matches the ID we passed in
  • params - this object contains any parameters defined in the routes file. In our case one of our routes is get '/tasks/:id' so if a user lands on localhost:3000/tasks/2, then in our params object there will be an :id with a value of 2.

In short, before we do certain things like viewing a specific task or deleting one, we will store in @task the relevant task that we are looking for. This means each action that access it immediately without having to add in that extra line of code themselves.

Getting all tasks using the index action

Now the first action is our index action. This maps to the following route:

get '/tasks' 'tasks#index'

The action itself is very short:

def index
  @tasks = Task.all
end

What we're doing here is storing a list of all the tasks in @tasks.

And then with Rails magic, this action will map to the view file defined at tasks/index.html.erb. This view file will have access to all the tasks, and can loop through them and render them all on the page:

<% @tasks.each do |task| %>
    <tr>
        <td><%= task.name %></td>
    </tr>
<% end %>

Updating a task using the update action

The update action maps to the following route:

put '/tasks/:id' 'tasks#update'
def update
    respond_to do |format|
      if @task.update(task_params)
        format.html { redirect_to @task, notice: 'Task was successfully updated.' }
        format.json { render :show, status: :ok, location: @task }
      else
        format.html { render :edit }
        format.json { render json: @task.errors, status: :unprocessable_entity }
      end
    end
end

Let's break this down!

  • respond_to - a method available to us in controller classes, which will give access to a format method
  • @task - we got this from the set_task method (described above)
  • @task.update() - we can call update() on the specific task, and pass in new values
  • task_params - gives us the values in the body of the call that was made (described in more detail below)
  • format.html and format.json - we're defining what is returned, depending on if the request was an HTML or JSON request.
  • if and else - if @task.update fails, we will enter the else block.

task_params

The other method defined at the bottom of the file is task_params:

def task_params
    params.require(:task).permit(:name, :done, :due)
end

When this method is called, we will require that a :task symbol exists inside of the params object, and filter out any values from task that aren't the values for name, done and due.

What this means in practice is that if you called the endpoint with the data to create a new task, this would be accessible inside of params[:task]. And if you passed in an extra field like "description", this would get filtered out by this method.


And that's it for this post! The other actions are fairly similar to the code I've gone through above, so hopefully shouldn't be too hard to understand.

Thanks for reading!

Comments