🤯 50% Off! 700+ courses, assessments, and books

DRY Off Your Rails Code with ActiveSupport::Concerns

Kingsley Silas
Share

hair dryer  icon

If you have been building Rails applications for a while, you have likely noticed a folder called concerns. This folder gets created inside the app/controllers and app/models directories whenever you generate a new Rails application. I thought it was useless until recently when we had to make use of it at work.

In this short tutorial, I want to show you how to harness the power of concerns.

We are going to build a mini tweeting application in Rails, I will call it Twik. In the application, we will have two TwitsController. One controller will be namespaced under admin (so it’s actions are accessible to just admins), and the other will be in the conventional Rails namespace. We will share functionality using ActiveSuppourt::Concern to make sure we abide by the DRY principle.

After seeing how it works for controllers, I’ll show you how to use ActiveSupport::Concern in models.

Let’s get started.

Application Setup

Generate your Rails application:

rails new twik -T

Add the following gems to your Gemfile:

...
gem 'devise'
gem 'bootstrap-sass'

Now bundle install your gems.

Run the command to install Devise:

rails g devise:install

Now let’s generate our Admin model:

rails g devise Admin

Rename app/assets/stylesheets/application.css to app/assets/stylesheets/application.scss and paste in the following:

#app/assets/stylesheets/application.scss

@import "bootstrap-sprockets";
@import "bootstrap";

Navigate to app/assets/javascripts/application.js and add the line above the last require:

...
//= require bootstrap-sprockets (ADD THIS)
//= require_tree .

Create the file app/views/layouts/_navigation.html.erb, and paste in the following:

#app/views/layouts/_navigation.html.erb

<nav class="navbar navbar-default">
  <div class="container-fluid">
    <!-- Brand and toggle get grouped for better mobile display -->
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse" aria-expanded="false">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <% if admin_signed_in? %>
        <%= link_to "Twik", admin_twits_path, class: "navbar-brand" %>
      <% else %>
        <%= link_to "Twik", root_path, class: "navbar-brand" %>
      <% end %>
    </div>
    <div class="collapse navbar-collapse" id="navbar-collapse">
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to 'Home', root_path %></li>
        <% if admin_signed_in? %>
          <li><%= link_to 'My Account', edit_admin_registration_path  %></li>
          <li><%= link_to 'Logout', destroy_admin_session_path, :method => :delete %></li>
        <% else %>
          <li><%= link_to 'Login', new_admin_session_path  %></li>
        <% end %>
      </ul>
    </div>
  </div>
</nav>

Now edit app/views/layouts/application.html.erb to look like this:

#app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>Twik</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= render "layouts/navigation" %>
    <div class="container-fluid">
      <%= yield %>
    </div>
  </body>
</html>

Twits Controllers and Concerns.

We want to write as little code as possible, ensuring that we do not repeat things. To achieve this we will use concerns to share actions in both controllers. Confused? Do the following:

  • Generate your Twit model: rails g model Twit tweet:text.
  • Generate your TwitsController: rails g controller TwitsController.
  • Navigate to app/controllers/concerns and create the file twitable.rb, pasting in the following:
  #app/controllers/concerns/twitable.rb

  module Twitable
    extend ActiveSupport::Concern

    included do
      before_action :set_twit, only: [:show, :edit, :destroy, :update]
    end

    def index
      @twits = Twit.all
    end

    def new
      @twit = Twit.new
    end

    def show
    end

    def create
      @twit = Twit.new(twit_params)
      if @twit.save
        flash[:notice] = "Successfully created twit."
        redirect_to @twit
      else
        flash[:alert] = "Error creating twit."
        render :new
      end
    end

    private

    def twit_params
      params.require(:twit).permit(:tweet)
    end

    def set_twit
      @twit = Twit.find(params[:id])
    end
  end

Now, let’s examine the code above.

Using extend ActiveSupport::Concern tells Rails that we are creating a concern. The code within the included block will be executed wherever the module is included. This is best for including third party functionality. In this case, we will get an error if the before_action is written outside of the included block. At this point, we are good to include our Twitable module to the controllers that need this behavior.

With that done, our TwitsController is very concise:

#app/controllers/twits_controllers.rb

class TwitsController < ApplicationController
  include Twitable
end

We need an admin directory to house the controller for admins:

mkdir app/controllers/admin
touch app/controllers/admin/twits_controllers.rb

Now paste this code into the file you just created:

#app/controllers/admin/twits_controllers.rb

class Admin::TwitsController < ApplicationController
  include Twitable

  def edit
  end

  def update
    if @twit.update_attributes(twit_params)
      flash[:notice] = "Successfully updated twit."
      redirect_to admin_twit_path
    else
      flash[:alert] = "Error creating twit."
      render :edit
    end
  end

  def destroy
    if @twit.destroy
      flash[:notice] = "Successfully deleted twit."
      redirect_to twits_path
    else
      flash[:alert] = "Error deleting twit."
    end
  end
end

Is that not cool? We do not have to repeat our code. Now, let’s create the views, so we can test if all of these really works:

mkdir -p app/views/admin/twits
touch app/views/admin/twits/index.html.erb
touch app/views/admin/twits/new.html.erb
touch app/views/admin/twits/show.html.erb
touch app/views/admin/twits/edit.html.erb
touch app/views/twits/new.html.erb
touch app/views/twits/show.html.erb
touch app/views/twits/index.html.erb

Paste the code below in the respective files.
Admin Twit Edit Page

#app/views/admin/twits/edit.html.erb

<div class="container-fluid">
  <div class="row">
    <div class="col-sm-offset-4 col-sm-4 col-xs-12">
      <%= form_for @twit, :url => {:controller => "twits", :action => "update" } do |f| %>
        <div class="form-group">
          <%= f.label :tweet %>
          <%= f.text_field :tweet, class: "form-control" %>
        </div>
        <div class="form-group">
          <%= f.submit "Update", class: "btn btn-primary" %>
          <%= link_to "Cancel", :back, class: "btn btn-default" %>
        </div>
      <% end %>
    </div>
  </div>
</div>

Admin Twit Index Page

#app/views/admin/twits/index.html.erb

<div class="container-fluid">
  <p id="notice"><%= notice %></p>

  <h1>Listing Twits</h1>

  <div class="row">
  <div class="col-sm-12 col-xs-12">
    <%= link_to "New Tweet", new_admin_twit_path, class: "btn btn-primary pull-right" %>
  </div>
  </div>
  <div class="row">
    <div class="col-sm-12 col-xs-12">
      <div class="table-responsive">
        <table class="table table-striped table-bordered table-hover">
          <tbody>
            <% @twits.each do |twit| %>
              <tr>
                <td class="col-sm-8 col-xs-8"><%= twit.tweet %></td>
                <td class="col-sm-4 col-xs-4"><%= link_to 'Show', admin_twit_path(twit), class: "btn btn-primary" %>
                    <%= link_to 'Edit', edit_admin_twit_path(twit), class: "btn btn-default" %>
                    <%= link_to "Delete", admin_twit_path(twit), class: "btn btn-danger", data: {:confirm => "Are you sure?"}, method: :delete %>
                </td>
              </tr>
            <% end %>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

Admin New Twit Page

#app/views/admin/twits/new.html.erb

<div class="container-fluid">
  <div class="row">
    <div class="col-sm-offset-4 col-sm-4 col-xs-12">
      <%= form_for @twit do |f| %>
        <div class="form-group">
          <%= f.label :tweet %>
          <%= f.text_field :tweet, class: "form-control" %>
        </div>
        <div class="form-group">
          <%= f.submit "Submit", class: "btn btn-primary" %>
          <%= link_to "Cancel", :back, class: "btn btn-default" %>
        </div>
      <% end %>
    </div>
  </div>
</div>

Admin Twit Show Page

#app/views/admin/twits/show.html.erb

<div>
  <h2><%= @twit.tweet %></h2>
</div>

Twit Index Page

#app/views/twits/index.html.erb

<div class="container-fluid">
  <p id="notice"><%= notice %></p>

  <h1>Listing Twits</h1>

  <div class="row">
  <div class="col-sm-12 col-xs-12">
    <%= link_to "New Tweet", new_twit_path, class: "btn btn-primary pull-right" %>
  </div>
  </div>
  <div class="row">
    <div class="col-sm-12 col-xs-12">
      <div class="table-responsive">
        <table class="table table-striped table-bordered table-hover">
          <tbody>
            <% @twits.each do |twit| %>
              <tr>
                <td><%= twit.tweet %></td>
                <td><%= link_to 'Show', twit, class: "btn btn-primary" %></td>
              </tr>
            <% end %>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

New Twit Page

#app/views/twits/new.html.erb

<div class="container-fluid">
  <div class="row">
    <div class="col-sm-offset-4 col-sm-4 col-xs-12">
      <%= form_for @twit do |f| %>
        <div class="form-group">
          <%= f.label :tweet %>
          <%= f.text_field :tweet, class: "form-control" %>
        </div>
        <div class="form-group">
          <%= f.submit "Submit", class: "btn btn-primary" %>
          <%= link_to "Cancel", :back, class: "btn btn-default" %>
        </div>
      <% end %>
    </div>
  </div>
</div>

Twit Show Page

#app/views/twits/show.html.erb

<div>
  <h2><%= @twit.tweet %></h2>
</div>

Now start up your Rails server by running rails server. Navigate through your website and you’ll see that everything works fine. Your codebase is neat and your controllers are thin, thanks to concern.

Concerns in Models

ActiveSupport::Concern works in Rails models, just like we saw in controllers. If you grasp what we did above you will be able to implement it in your models where necessary. Let’s say we have a reply feature in our application. With this feature, users can reply to twits (duh). Alongside the reply feature, we have a voting feature allowing users to vote on twits and replies. So, now we have three models: Twit, Reply, and Vote.

Twit and Reply have many votes, so their models look like this:

class Twit < ActiveRecord::Base
  has_many :votes, as: :votable
  has_many :replies

  def vote!
    votes.create
  end
end

class Reply < ActiveRecord::Base
  has_many :votes, as: :votable
  belongs_to :twits

  def vote!
    votes.create
  end
end

class Vote < ActiveRecord::Base
 belongs_to :votable, polymorphic: true
end

Using concerns, you can make things look pretty and neat. Here is how you might want to do it:

module Votable
  extend ActiveSupport::Concern

 included do
  has_many :votes, as: :votable
 end

 def vote!
  votes.create
 end
end

class Twit < ActiveRecord::Base
  include Votable
  has_many :replies
end

class Reply < ActiveRecord::Base
  include Votable
  belongs_to :twit
end

I am sure you will agree with me that this path is much better.

Conclusion

The goal of this tutorial is simple. I simply wanted to show you a way to abide by the DRY principle using ActiveSupport::Concern. I hope it was worth the time :)

CSS Master, 3rd Edition