Tagging from Scratch in Rails

Share this article

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

Frequently Asked Questions (FAQs) about Tagging from Scratch in Rails

What is the purpose of tagging in Rails?

Tagging in Rails is a feature that allows you to categorize and organize data in your application. It is a way to add metadata to your models, which can be used for filtering, sorting, or searching data. For instance, if you have a blog application, you can use tags to categorize your blog posts into different topics. This makes it easier for users to find related content.

How do I implement tagging from scratch in Rails?

Implementing tagging from scratch in Rails involves creating a new model for tags and setting up a many-to-many relationship between the tag model and the model you want to tag. You will also need to create a join table to store the relationships between the two models. Once the models and relationships are set up, you can add methods to your models to add, remove, and query tags.

What are the benefits of using a gem like ‘acts-as-taggable-on’?

The ‘acts-as-taggable-on’ gem provides a robust and flexible tagging system for Rails applications. It supports multiple tag contexts, so you can have different sets of tags for different attributes of your models. It also provides methods for querying and manipulating tags. Using this gem can save you a lot of time and effort compared to implementing tagging from scratch.

How do I add tags to a model in Rails?

To add tags to a model in Rails, you first need to set up a relationship between the model and the tag model. Once the relationship is set up, you can add tags to a model instance by creating new tag instances and associating them with the model instance. You can also use the ‘acts-as-taggable-on’ gem, which provides a convenient method for adding tags.

How do I remove tags from a model in Rails?

To remove tags from a model in Rails, you need to find the tag instances associated with the model instance and delete them. If you are using the ‘acts-as-taggable-on’ gem, you can use the ‘remove_tag’ method provided by the gem.

How do I search for models by tags in Rails?

To search for models by tags in Rails, you can use the ‘where’ method provided by ActiveRecord. You can pass in a condition that matches the tags you are looking for. If you are using the ‘acts-as-taggable-on’ gem, you can use the ‘tagged_with’ method, which returns all models that are tagged with a specific tag.

Can I use multiple tags at once in Rails?

Yes, you can use multiple tags at once in Rails. You can add multiple tags to a model instance by creating multiple tag instances and associating them with the model instance. If you are using the ‘acts-as-taggable-on’ gem, you can pass an array of tags to the ‘tag_list’ method.

How do I display tags in a view in Rails?

To display tags in a view in Rails, you can iterate over the tags associated with a model instance and output each tag. You can use the ‘each’ method provided by Ruby to iterate over the tags.

Can I use tagging in Rails with other databases like MongoDB?

Yes, you can use tagging in Rails with other databases like MongoDB. However, the implementation might be different depending on the database. You will need to use a database-specific gem or library that provides support for tagging.

How do I handle duplicate tags in Rails?

To handle duplicate tags in Rails, you can add a uniqueness validation to the tag model. This will ensure that each tag is unique. If you are using the ‘acts-as-taggable-on’ gem, it automatically handles duplicate tags for you.

Nouran MahmoudNouran Mahmoud
View Author

Passionate web wizard focusing on web development using RoR and JS technologies, an all-round geek who loves reading and writing in technology, she is graduated from the faculty of Computers and Information Science, with Java and web development experience. She currently works as a Front-end Engineer at ta3rifah.com.

GlennG
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week