How to Implement a Custom Action in Rails via a Button Click
This lesson provides a step by step guide for how to implement a custom method in Rails that can be activated via clicking a link. This implementation will cover each stage of the data flow process and show how to properly toggle the blog post's status via the view.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
Video locked
This video is viewable to users with a Bottega Bootcamp license

This lesson provides a step by step guide for how to implement a custom method in Rails that can be activated via clicking a link. This implementation will cover each stage of the data flow process and show how to properly toggle the blog post's status via the view.

We are getting into a fun portion of the application development process. We're going to build a button that toggles the status between the published and draft modes for our blogs view.

I'm purposely building this feature without any notes on the procedure because I want you to experience how I would approach the process of building this component completely from scratch, which is the way you would be doing it in a real-world scenario. This will also force me to analyze the data flow even more, which will be beneficial as that is the focus of this section.

Verify Available Data

The first thing I would do is open the rails console rails c, as I want to verify one thing. If I type Blog.last.draft?, (don’t forget the question mark) it should return a true or false value. And this returns => false. Next I want to run Blog.last.published? And this returns => true

I know that enums give a question mark value that will return true or false. So my test here is to verify the status on an item. Explicitly, you have to have an item selected, such as the results you would get from doing Blog.last . With that item you can run published or draft the same way you would without the question mark to verify if it is in a published or draft state. However when we use the question mark ?, with our enum method it will return a true or false answer.

This process tells me that when I build out the toggle method, I can ask these questions. So, in the controller I can now check the status of each blog, and whether it is in draft mode or published. Then I can render that value accordingly in the view. It can also help to decide if the value needs to be changed; whether it has to be published or changed to draft?

So, that's the first thing I wanted to check.

Render the Values in the View

Next, I'm going to render these values to the browser. So on our blogs page, I'll list out the status value on the right-hand side of each blog. Let switch over to sublime and open the app/views/blog/index.html.erb file.

In the <thead> tag section, there is a value of 3 for the colspan. What this means is there is no header now, but there is room for one. So, I need to change this value to 4.

- <th colspan=“3"></th>
+ <th colspan=“4"></th>

In the table body section <tbody> underneath blog.body , I'm going to add the status like this:
<td><%= blog.status %></td>. So your file should now look like this:

<p id="notice"><%= notice %></p>

<h1>Blogs</h1>

<table>
  <thead>
    <tr>
      <th>Blog Title</th>
      <th>Body</th>
      <th colspan="4"></th>
    </tr>
  </thead>
  <tbody>
    <% @blogs.each do |blog| %>
      <tr>
        <td><%= blog.title %></td>
        <td><%= blog.body %></td>
        <td><%= blog.status %></td>
        <td><%= link_to 'Show', blog %></td>
        <td><%= link_to 'Edit', edit_blog_path(blog) %></td>
        <td><%= link_to 'Delete Post', blog, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Blog', new_blog_path %>

If you save your file and refresh the browser, you can see the status rendered on the page. All blogs are in draft mode, except the last two because those are the blogs we changed. Eventually we can give the status a color or make it a label so it will look better. But, we will save that implementation for the design phase.

Making the Status a Link

The next step we want to take is to make the status a link.

I like to follow a process where I execute the most simple implementation possible to create the desired outcome. So I am going to use the link_to method and then designate the path.

- <td><%= blog.status %></td>
+ <td><%= link_to blog.status, root_path %></td>

If you save and refresh, you can see that all the status values are now links and they are pointed to the home page. While these are somewhat pointless links, I have verified that each blog has a link and the link is working.

The Benefit of Small Changes

The reason I like to follow this process, and you will see me implement minute changes this way frequently, is because I know if I make a really small change like this, and apply it, I can make small tests, exactly like I just did. If this works, I know that I can move on to the next stage.

Imagine, if I try to build out the entire functionality like the custom action and custom route and the custom link all at one time without testing any of it. Then, if something doesn't work, which happens quite a bit, I wouldn't know where to start to look to try and find the error. However, because I have verified that this link is working, I would know that this process was not causing the problem.

One thing I've noticed as a major difference between developers who are just starting out and senior developers, is that the most senior developers I know make incredibly small changes. With these small changes they are able to understand and build complex things because they can take things one step at a time (I will step off my soapbox and keep building now).

Creating a Custom Route

The next item we want to change is the route in our link. To do this we need to create our custom route. I think this is your best next step, so open your routes.rb file.

The best way to create a custom route is by adding a block to whichever resources you want. So we will be working with our blogs resources. We will use a do block, but it won’t need a variable. Inside of that we will indicate member do and inside of that we will specify post :toggle_status I don’t want to call it published or draft, nor do I want to call it status, because that could get confusing, so I believe toggle_status is a good choice. Our blog resources should now look like this:

  resources :blogs do
    member do
      post :toggle_status
    end
  end

This looks good, but we will want to test it out before we move on. Head to the terminal and use the command rake routes | grep blogs

large

Look at that! The first item in the table shows blogs#toggle_status. We have our new custom route, and the path is toggle_status_blog.

Start the rails server rails s, and go back to blogs/index.html.erb. Instead of root_path, we'll use toggle_status_blog_path.

- <td><%= link_to blog.status, root_path %></td>
+ <td><%= link_to blog.status, toggle_status_blog_path(blog) %></td>

Save the file. I already know what the next step will be, but let’s test this out in the browser by refreshing our localhost:3000/blogs page. You will see that this throws an error because No route matches. We have an action, but we don’t have a method inside of our controller that matches.

To fix this, open blogs_controller.rb so we can create the action. Below the destroy method add:

def toggle_status
end

Now that we've fixed that let's refresh the browser. We still have the same error! Let's test it out and see why this is occurring.

I am going to use the same process I would if I received this error in a ‘real-life’ scenario. I'm going to run the rake routes | grep blog command again. The reason I am doing this is that If you look closely at the error message, it seems to indicate it's expecting an id as it states missing required keys: [:id]

So here we can see this is the issue. Looking at the toggle action route you can see it states

/blogs/:id/toggle_status(.:format)

So the route is looking for an id. This is good because we want it to look for an ID, and because it needs to accept an id, we need to pass in an id. That's where our mistake is.

Go back to your blogs/index.html.erb file, and pass the variable blog, just like we did for edit_blog_path.

- <td><%= link_to blog.status, toggle_status_blog_path
+ <td><%= link_to blog.status, toggle_status_blog_path(blog)

Now, refresh the page to see if it works. Perfect! Our status now has a link of toggle status.

Introduction to Byebug

Now I'm going to show you a little trick. Though this concept is a little ahead of where we are at in the course, as I have a whole section dedicated to debugging, for now I want to show you one way of doing it.

Go to blogs_controller.rb and inside the toggle_status method, simply type byebug.

def toggle_status
  byebug
end

If you open up your Gemfile, you will see we have a gem called byebug. This is a debugging gem that allows you to stop your system and ask the system questions about itself while it is running. It's an amazing thing. If you have never implemented a true debugger before, it is important to understand that this is an invaluable tool. I use it every day. If you get into full Rails professional development, you will too.

Refresh your browser and then click on the draft link, you will get an error message that declares No route matches.

This is perfectly fine, as it's looking for a toggle_status route, and we haven’t told it to do anything differently.

Now, we have to change the way our toggle_status method functions, so that it will redirect to blogs_url.

def toggle_status
  byebug
  redirect_to blogs_url
end

If you refresh the page, and click on the status link it still throws an error. Look closely at the error message, and it says that there are No route matches for the verb GET. I know the problem.

We are going to have to stop the server and go to the routes.rb file. The request inside resources should be get and not post

- post :toggle_status
+ get :toggle_status

Make sure your file looks like this:

  resources :blogs do
    member do
      get :toggle_status
    end
  end

In our deep dive, we'll talk about the difference between get and post. For now, understand that it's the way the method is calling the server, specifically, in the way it calls the route, get works a little differently than post.

Next let restart the rails server rails sand refresh the browser for the blogs page. Now if you click on the "draft" link, you can see that nothing is happening. However, if you switch over to the terminal, you'll see that byebug has actually been called.

large

The system is right inside the toggle_status method and everything is working as it should. Also, as I mentioned before, you can ask questions of the system— that is what byebug allows us to do. So, if my question is What are the params?, I can simply type params hit enter, and the system will display what they are. Here we see that the parameters are the controller, the action and the id.

large

This is everything we need, and this is so amazing. We can ask question of the system and the answers can lead our development. This is why I'm really excited about byebug and how Rails works with it here.

From a data flow perspective, when you're in this toggle_status method , and you have clicked draft, what variable, what data is available to the method? The answer is going to tell you exactly what we can use.

We can also run queries. For example, I can runblog = Blog.friendly.find(params[:id])

large

This ran a database query and showed us everything that we need. I can even make changes to params. For example, this blog is in draft status, and I can change it to published with blog.published! (don’t forget the bang !) In the response you can see the system committed it and it all worked.

large

This is exactly how we can implement the method in our code. The same way I looked up the item when I ran the Blog.friendly.find(params[:id]) query, we can use that data and run the methods just like we did in our console, right in our code.

To get out of byebug, just use the command exit hit enter, then exit and enter again.

This will take us to where we were before and the error message will be displayed again.

Go ahead and exit the rails console as well (control d)

Now, go to your blogs_controller.rb and get remove byebug. You wouldn’t want that in your system permanently, as it would stop it from running.

Implementing the Toggle System

Now, let's implement our toggle system. If you remember what I did at the very beginning of the guide, I used the console to verify and checked the values for published and draft, and how I could change them. That is what we are going to build here in our controller.

First, I'm going to add toggle_status in our before_action list.

- before_action :set_blog, only: [:show, :edit, :update, :destroy]
+ before_action :set_blog, only: [:show, :edit, :update, :destroy, :toggle_status]

By doing this, our @blog instance variable will be available in our custom toggle_status method.

Next, inside the method I am going to add @blog.published! if @blog.draft? then on the next line, use the same code, but reverse it @blog.draft! if @blog.published? last, we will use the current redirection, but add a notice notice: 'Post status has been updated.’

def toggle_status
  @blog.published! if @blog.draft?
  @blog.draft! if @blog.published?
  redirect_to blogs_url, notice: 'Post status has been updated.’
end

First, let's test this out and determine if our method works, then I will explain it. If I explain it and it doesn't work then I will look pretty dumb, becausee I explained something that needed to be fixed!

Start the rails server rails s and go to "localhost:3000/blogs".

Each blog has a draft/published status. The published status you see about half way down the list is the one we changed in byebug . It’s very useful that we were able to alter it that way.

Click on one of the draft items to see if the status will change. Right at the top of the browser window, we have a notice that the "Post status has been updated", which is interesting because the status is not updated when we click on any link.

So, it looks like we have a little bug. Let’s try clicking on draft for My Blog Post 2. Once again, our method does not seem to be working.

In a case like this, the first thing I will do is go to the terminal and see what is happening on the process side so I can verify why this doesn’t seem to work.

large

Here, you can see that when I clicked on toggle_status, it passed the ID as the parameter. It found the record and tried to update the status from 1 to 0. But, it didn't actually do it. Oh! and you know what? I was trying to create some nice looking syntax and I made a simple mistake. The first line will change the status of draft to published, while the second one will change it back to draft again!

So, the code is working, but is just canceling each other out. A better way to implement it is through a regular conditional. What I want to declare is that if the blog is draft if @blog.draft?, then change it to published @blog.published! . Alternatively — elseif is is published elsif @blog.published?, change it to draft @blog.draft!.

def toggle_status
    if @blog.draft?
      @blog.published!
    elsif @blog.published?
      @blog.draft!
    end

    redirect_to blogs_url, notice: 'Post status has been updated.'
end

If you're wondering how this is different, in the first case when there were just two lines of code, they would be executed sequentially. In other words, it would run in two processes. I knew what the bug was because when I checked the terminal I could see that it ran the update and then it ran the update again, which essentially left us with no change in status.

Now with our new syntax, only one part of the conditional will be executed. For example, if the blog is in draft status, it'll change it to published and skip everything else. Or if it's published, then it will change it to draft status.

This should work exactly as we want now. Save the file and refresh the browser. To test, click on a draft link. The post should now have a status of published. Try another one listed as published and it should change to draft. The order of the posts changes so keep you will have to look for the particular one you updated, but the process is working just the way we wanted.

Though it took a little time to go through this guide, now you know how to update and have dynamic methods, and call them from the routes.

Hopefully you can see that we took the data flow from the routes, to the controller, communicated with the model, and implemented the full MVC data flow structure.

Resources