ActiveSupport::Concerns - App for Rails Code

ActiveSupport::Concerns - App for Rails Code

  • 2016-10-14
  • 342

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 :)

Suggest

Learn Rails: Build a Network Chatroom

Make Medium (medium.com) Clone in Ruby on Rails

Learn Rails: Quickly Code, Style and Launch 4 Web Apps

Rails 5: Learning Web Development the Ruby Way

Professional Rails Code Along

The Complete Ruby on Rails Developer Course

Learn Ruby on Rails from Scratch