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 :)
Frequently Asked Questions (FAQs) about ActiveSupport::Concerns in Rails
What is the main purpose of ActiveSupport::Concerns in Rails?
ActiveSupport::Concerns in Rails is a module that provides a simple and clean way to encapsulate a shared piece of code across multiple classes. It is designed to make it easier to declare class-level methods, instance methods, and even hooks in a module, which can then be included in any class. This helps in keeping your code DRY (Don’t Repeat Yourself), organized, and easy to maintain.
How does ActiveSupport::Concerns differ from traditional Ruby modules?
While traditional Ruby modules allow you to define methods that can be mixed into classes, ActiveSupport::Concerns takes it a step further by providing a structure for defining both instance and class methods, as well as other module-level aspects, such as dependencies and hooks. This makes it easier to share complex code structures between classes.
How do I define a class method within a concern?
In ActiveSupport::Concerns, class methods are defined within a block called class_methods
. Here’s an example:module Reusable
extend ActiveSupport::Concern
class_methods do
def some_class_method
# method body
end
end
end
Can I use hooks within a concern?
Yes, ActiveSupport::Concerns allows you to use hooks within a concern. These hooks will be executed in the context of the class that includes the concern. For example:module Reusable
extend ActiveSupport::Concern
included do
before_save :do_something
end
def do_something
# method body
end
end
How do I include a concern in a class?
To include a concern in a class, you simply use the include
keyword followed by the name of the concern. For example:class MyClass < ApplicationRecord
include Reusable
end
Can I include multiple concerns in a single class?
Yes, you can include multiple concerns in a single class. Each concern should be included with a separate include
statement.
Can concerns depend on each other?
Yes, concerns can depend on each other. If a concern depends on methods defined in another concern, you can use the requires
keyword to specify this dependency.
Can I use concerns in controllers?
Yes, concerns can be used in both models and controllers. This allows you to share code between different parts of your application.
Can concerns be tested?
Yes, concerns can and should be tested. You can write unit tests for your concerns just like you would for any other piece of code.
Are there any drawbacks to using concerns?
While concerns can help keep your code organized and DRY, they can also lead to issues if not used carefully. For example, they can make it harder to understand the code if they are overused or if the relationship between the concern and the class is not clear. It’s important to use concerns judiciously and to keep them focused and small.
Kingsley Silas is a web developer from Nigeria. He has a hunger for acquiring new knowledge in every aspect he finds interesting.