Our task today is to add tags to the Article models of a blog application written in Ruby on Rails. The user should be able to enter a list of words separated by comma; the app should then create tags from the words entered if they don't exist, and link them to the article.
The Models
Let's start by creating firs the Tag model:
bundle exec rails g model tag name:string
The Tag model has only one attribute, name, which will be extracted from the words entered by the user.
Since the relationship between articles and tags is many to many, we also need an intermediary model, which we'll call Tagging:
bundle exec rails g model tagging article:belongs_to tag:belongs_to
The belongs_to
directive will make Rails to add correct indexes, foreign keys, and macros to the Tagging model.
We then run the migration to update the database:
bundle exec rake db:migrate
The Concern
To link the Article with the Tag we'll make use of ActiveSupport::Concern
, a module we can include inside the Article model.
# app/models/concerns/taggable.rb
module Taggable
extend ActiveSupport::Concern
included do
has_many :taggings
has_many :tags, :through => :taggings
after_save :save_tags
end
end
When using concerns, we need to add the ActiveRecord macros inside the included
block. The first two has_many
create the many to many relationship between articles and tags.
The after_save
callback saves the tags when saving the article. Let's add that method now:
# app/models/concerns/taggable.rb
private
def save_tags
tag_names.split(',').each do |name|
tag = Tag.find_or_create_by(name: name.strip)
tags << tag unless tags.exists?(tag.id)
end
clean_tags
end
def clean_tags
names = tag_names.split(',').map { |n| n.strip }
tags.each do |tag|
tags.destroy(tag) unless names.include?(tag.name)
end
end
Here we split the tag names and save them into our database if they don't exist. We also remove any tags that are no longer in the list provided, useful when we edit the article's tags.
The tag_names
attribute doesn't exist yet, so let's create that now:
# app/models/concerns/taggable.rb
def tag_names
@tag_names.blank? ? tags.map(&:name).join(', ') : @tag_names
end
def tag_names=(names)
@tag_names = names
end
The tag_names
is a reader/writer attribute, which will not exist as such in the database (as we want different Tag models). They will be useful in our article view form.
At the end, we include the Taggable module to our Article model:
class Article < ActiveRecord::Base
include Taggable
end
The View
Assuming we have a normal Rails style form for the Article, we add the following to it:
<div class="field">
<%= f.label :tag_names %><br>
<%= f.text_field :tag_names %>
</div>
When we run the app, we should now be able to add tags separated by comma (e.g. Ruby on Rails, JavaScript, CSS), and they will create tags attached to the article.
Happy coding!