Ruby - - By Nouran Mahmoud

Tagging from Scratch in Rails

Price tag icon

Tags are sort of like categories, describing a piece of information (content) and allowing to the user to search for it again. In this tutorial, you will see how to build a simple tagging system, from the ground up, in Rails.

This tutorial assumes a basic knowledge with Ruby on Rails. I’ll try to keep it simple.

A common use of tags, which we all already know, is by Twitter to collect tweets around a certain topic related to the (hash)tag.

Tools

  • Rails 4.1.4
  • Ruby 2.0.1
  • Foundation 5

Getting Started

The enitre source code of this application can be found in this repository

First, create your Rails project:

rails new TaggingTut

Then, add the foundation-rails gem to the Gemfile, are remove the turbolinks gem. Run bundle install.

We will use sqlite, the default database used by Rails

Remove this line from app/assets/javascript/application.js., since we removed the turbolinks gem.

//= require turbolinks

Creating Tags

Generate the model for tags with a single attribute, name:

rails g model tag name:string

We will index the name attribute (index: true) to speed up our search with these tags. I recommend this tutorial for using indexes in Rails associations.

Tags have many Posts, and Posts can have more than one tag. As such, the relationship will be many-to-many. We can represent this association in Rails in two ways:

  • First: Use a has_and_belongs_to_many association. This will generate the join table in the database, but there wont be a generated model for the join. So, you won’t be able to add validations or any other attributes to the join.

  • Second: Use has_many, through which requires a model to be created for the join table. This way is preferred for most cases, so we will use it.

Create models “Post” and “Tagging”, as well

rails g model post author:string content:text
rails g model tagging post:belongs_to tag:belongs_to

After creating these models, run rake db:migrate.

Now, we will create associations between posts and tags through ActiveRecord as follows:

app/models/post.rb

has_many :taggings
has_many :tags, through: :taggings

app/models/tag.rb

has_many :taggings
has_many :posts, through: :taggings

app/models/tagging.rb will be generated like so:

belongs_to :post
belongs_to :tag

We now have a join between posts and tags through the taggings join table.

Now, we will need to handle the creation of tags as part of the post create action. So, we will define a method to take all the entered tags, strip them, and then write each tag to the database.

The Post model had two attributes, author and content, which are also defined in the current form. An attribute for all_tags will be added to the form data, as well. In Rails 4, add the desired (virtual, in this case) attribute using strong parameters. Virtual attributes are very simple, in this case defined as a getter and setter methods. The strip function is for removing whitespace

app/models/post.rb

def all_tags=(names)
  self.tags = names.split(",").map do |name|
      Tag.where(name: name.strip).first_or_create!
  end
end

def all_tags
  self.tags.map(&:name).join(", ")
end

The all_tags function will be customized to render all the tags separated by commas.

Before creating the controller and views, install Zurb Foundation:

rails g foundation:install

Now, customize the controller and views for rendering the posts including all the tags. Create app/controllers/posts_controller.rb by typing:

rails g controller posts index create

Specify the strong parameters as follows, including our virtual attribute to hold all the tags entered through the view:

private
def post_params
  params.require(:post).permit(:author, :content, :all_tags)
end

The permit method creates a whitelist of parameters to be allowed to pass. Read more about about strong parameters here

Let’s create the form with a text field for tags. We will create the post using AJAX. It’s pretty simple:

app/views/posts/_new.html.erb

<div class="row text-center">
  <%= form_for(Post.new, remote: true) do |f| %>
    <div class="large-10 large-centered columns">
      <%= f.text_field :author, placeholder: "Author name" %>
    </div>
    <div class="large-10 large-centered columns">
      <%= f.text_area :content, placeholder: "Your post", rows: 5 %>
    </div>
    <div class="large-10 large-centered columns">
      <%= f.text_field :all_tags, placeholder: "Tags separated with comma" %>
    </div>
    <div class="large-10 large-centered columns">
      <%= f.submit "Post", class: "button"%>
    </div>
  <% end %>
</div>

__remote: true__ is the attribute that tells the form to be submitted via AJAX rather than by the browser’s normal submit mechanism.

After creating our post, create will redirect to the index action and view the existing posts.

app/controllers/posts_controller.rb

def index
  @posts = Post.all
end

app/views/posts/index.html.erb

<div class="row">
  <div class="large-8 columns">
    <%= render partial: "posts/new" %>
  </div>
</div>

Don’t forget to handle the routes.
config/routes.rb

root 'posts#index'
resources :posts, only: [:create]

Add some very simple styling to the view as follows:
app/assets/stylesheets/posts.css.scss

.tags-cloud {
  margin-top: 16px;
  padding: 14px;
}

.top-pad {
  padding: 25px;
}
.glassy-bg{
  box-shadow: 0px 3px 8px -4px rgba(0,0,0,0.15);
  background: white;
  border-radius: 4px;
  padding-bottom: 12px;
}

.mt{
  margin-top: 10px;
}
.mb{
  margin-bottom: 10px;
}

.pt{
  padding-top: 10px;
}
.pb{
  padding-bottom: 10px;
}

Run rails s and let’s see what we have.
Form for post

Oops, there are no posts!. We never wrote the create action.

app/controllers/posts_controller.rb

def create
  @post = Post.new(post_params)
  respond_to do |format|
    if @post.save
      format.js # Will search for create.js.erb
    else
      format.html { render root_path }
    end
  end
end

This snippet creates a new Post with the parameters specified by the user, checking whether it’s valid and returning the result. Since the form is submitted with AJAX, the respond format is js.

Now, we need to create the create.js.erb file to hold the javascript that will run after creating the post:
app/views/posts/create.js.erb

var new_post = $("<%= escape_javascript(render(partial: @post))%>").hide();
$('#posts').prepend(new_post);
$('#post_<%= @post.id %>').fadeIn('slow');
$('#new_post')[0].reset();

This code renders a partial view of the newly created post, the prepend function allows it to be rendered on top of the old posts with a fadeIn effect.

Create a partial that will render each post:

app/views/posts/_post.html.erb

<%= div_for post do %>
  <div class="large-12 columns border border-box glassy-bg mt pt">
    <strong><%= h(post.author) %></strong><br />
    <sup class="text-muted">From <%= time_ago_in_words(post.created_at)%></sup><br />
    <div class="mb pb">
      <%= h(post.content) %>
    </div>
    <div class="tags">
      <%=raw post.all_tags %>
    </div>
  </div>
<% end %>

Before we check the output, modify the index view to hold the partial for posts:

app/views/index.html.erb

<div class="row mt pt">
  <div class="large-5 columns">
    <div class="top-pad glassy-bg">
      <%= render partial: "posts/new" %>
    </div>
  </div>
  <div class= "large-7 columns" id="posts">
    <%= render partial: @posts.reverse %>
  </div>
</div>

Posts will be in reverse order from top to bottom, meaning, the most recenlty entered post will be first.

At this stage, we have posts with tags stored in the database using the two tables, tags and taggings . The taggings table saves the association between posts and tags. Here’s what our posts look like:

posts

Tag-based Search

In this section, we will create scope-based searches on tag name.

Create a class method called tagged_with(name) which will take the name of the specified tag and search for posts associated with it.

app/model/post.rb

def self.tagged_with(name)
  Tag.find_by_name!(name).posts
end

Create an instance variable holding the results on the controller.

app/controllers/posts_controller.rb

def index
  if params[:tag]
    @posts = Post.tagged_with(params[:tag])
  else
    @posts = Post.all
  end
end

Add a get route to hold the tag name and point to the posts_controller#index method:

config/routes.rb

get 'tags/:tag', to: 'posts#index', as: "tag"

After that, change the tags of each post to be links to the ‘index’ method, as follows:

app/views/_post.html.erb

<%=raw tag_links(post.all_tags)%>

tag_links(tags) is a helper method which will hold the logic of converting the tags to links.

app/helpers/posts_helper.rb

def tag_links(tags)
  tags.split(",").map{|tag| link_to tag.strip, tag_path(tag.strip) }.join(", ") 
end

Yay! Now, we have tag-based search for our posts!

tag-based search

Tag Cloud

Let’s generate one of those cool tag clouds based on counting the number of occurrences for each tag across all posts.

First, create a method to count all tags associated with posts:

app/models/tag.rb

def self.counts
  self.select("name, count(taggings.tag_id) as count").joins(:taggings).group("taggings.tag_id")
end

This query groups the matched tag_ids from the taggings join table and counts them.

We will style them according to their counts by creating a helper method called tag_cloud which take the result of calling the counts function and CSS classes.

app/helpers/posts_helper.rb

def tag_cloud(tags, classes)
  max = tags.sort_by(&:count).last
  tags.each do |tag|
    index = tag.count.to_f / max.count * (classes.size-1)
    yield(tag, classes[index.round])
  end
end

This helper method will get the tag with the max count. Then, it loops on each tag to calculate the index which will choose the CSS class based on rounded value. Then, the passed block will be executed.

We need to add styles for different sizes as follows:

app/assets/tags.css.scss

.css1 { font-size: 1.0em;}
.css2 { font-size: 1.2em;}
.css3 { font-size: 1.4em;}
.css4 { font-size: 1.6em;}

Don’t forget to add *= require tags to application.css.

Finally, add the code to display the tags in the view and apply the CSS classes to them.

app/views/posts/index.html.erb

<div class="tags-cloud glassy-bg">
  <h4>Tags Cloud</h4>
  <% tag_cloud Tag.counts, %w{css1 css2 css3 css4} do |tag, css_class| %>
    <%= link_to tag.name, tag_path(tag.name), class: css_class %>
  <% end %>
</div>

Check it out, our tags are in a cloud!
tag cloud

actastaggable_on

After this article, you should be able to handle the act_as_taggable_on gem without issue. You can read more about it on its github repo.

Conclusion

I hope this tutorial helps you understand what goes into creating a basic tagging system. Tag, you’re it! :)