In this iteration we’ll add the ability to tag articles for organization and navigation.
First we need to think about what a tag is and how it’ll relate to the Article model. If you’re not familiar with tags, they’re commonly used in blogs to assign the article to one or more categories.
For instance, if I write an article about a feature in Ruby on Rails, I might want it tagged with all of these categories: “ruby”, “rails” and “programming”. That way if one of my readers is looking for more articles about one of those topics they can click on the tag and see a list of my articles with that tag.
What is a tag? We need to figure that out before we can create the model. First, a tag must have a relationship to an article so they can be connected. A single tag, like “ruby” for instance, should be able to relate to many articles. On the other side of the relationship, the article might have multiple tags (like “ruby”, “rails”, and “programming” as above) - so it’s also a many relationship. Articles and tags have a many-to-many relationship.
Many-to-many relationships are tricky because we’re using an SQL database. If an Article “has many” tags, then we would put the foreign key
article_id inside the
tags table - so then a Tag would “belong to” an Article. But a tag can connect to many articles, not just one. We can’t model this relationship with just the
When we start thinking about the database modeling, there are a few ways to achieve this setup. One way is to create a “join table” that just tracks which tags are connected to which articles. Traditionally this table would be named
articles_tags and Rails would express the relationships by saying that the Article model
has_and_belongs_to_many Tags, while the Tag model
Most of the time this isn’t the best way to really model the relationship. The connection between the two models usually has value of its own, so we should promote it to a real model. For our purposes, we’ll introduce a model named “Tagging” which is the connection between Articles and Tags. The relationships will setup like this:
- An Article
- A Tag
- A Tagging
belongs_toan Article and
With those relationships in mind, let’s design the new models:
name: A string
tag_id: Integer holding the foreign key of the referenced Tag
article_id: Integer holding the foreign key of the referenced Article
Note that there are no changes necessary to Article because the foreign key is stored in the Tagging model. So now lets generate these models in your terminal:
Now that our model files are generated we need to tell Rails about the relationships between them. For each of the files below, add these lines:
After Rails had been around for awhile, developers were finding this kind of relationship very common. In practical usage, if I had an object named
article and I wanted to find its Tags, I’d have to run code like this:
That’s a pain for something that we need commonly.
An article has a list of tags through the relationship of taggings. In Rails we can express this “has many” relationship through an existing “has many” relationship. We will update our article model and tag model to express that relationship.
Now if we have an object like
article we can just ask for
article.tags or, conversely, if we have an object named
tag we can ask for
To see this in action, start the
rails console and try the following:
The first interface we’re interested in is within the article itself. When I write an article, I want to have a text box where I can enter a list of zero or more tags separated by commas. When I save the article, my app should associate my article with the tags with those names, creating them if necessary.
Add the following to our existing form in
app/views/articles/_form.html.erb (below the input title):
With that added, try to create a new article in your browser and you should see this error:
An Article doesn’t have an attribute or method named
tag_list. We made it up in order for the form to display related tags, but we need to add a method to the
app/models/article.rb file like this:
Back in your console, find that article again, and take a look at the results of
That is not quite right. What happened?
Our array of tags is an array of Tag instances. When we joined the array Ruby called the default
#to_s method on every one of these Tag instances. The default
#to_s method for an object produces some really ugly output.
We could fix the
tag_list method by:
- Converting all our tag objects to an array of tag names
- Joining the array of tag names together
Another alternative is to define a new
Tag#to_s method which overrides the default:
Now, when we try to join our
tags, it’ll delegate properly to our name
attribute. This is because
#to_s on every element of the
Your form should now show up and there’s a text box at the bottom named “Tag list”.
Enter content for another sample article and in the tag list enter ‘ruby, technology’.
Click save. It…. worked? Oh, Yeah!
But it didn’t. Click ‘edit’ again, and you’ll see that we’re back to the
#<Tag... business, like before. What gives?
Check the output on the console:
Unpermitted parameters? Oh yeah! Strong Parameters has done its job, saving us from parameters we don’t want. But in this case, we do want that parameter. Open up your
app/helpers/articles_helper.rb and fix the
If you go back and put “ruby, technology” as tags, and click save, you’ll get this new error:
What is this all about? Let’s start by looking at the form data that was posted when we clicked SAVE. This data is in the terminal where you are running the rails server. Look for the line that starts “Processing ArticlesController#create”, here’s what mine looks like:
The field that’s interesting there is the
"tag_list"=>"technology, ruby". Those are the tags as I typed them into the form. The error came up in the
create method, so let’s peek at
app/controllers/articles_controller.rb in the
create method. See the first line that calls
Article.new(article_params)? This is the line that’s causing the error as you could see in the middle of the stack trace.
create method passes all the parameters from the form into the
Article.new method, the tags are sent in as the string
"technology, ruby". The
new method will try to set the new Article’s
tag_list equal to
"technology, ruby" but that method doesn’t exist because there is no attribute named
There are several ways to solve this problem, but the simplest is to pretend like we have an attribute named
We can define the
tag_list= method inside
article.rb like this: (do not delete your original tag_list method)
Just leave it blank for now and try to resubmit your sample article with tags. It goes through!
Did it really work? It’s hard to tell. Let’s jump into the console and have a look.
I bet the console reported that
 tags – an empty list. (It also probably said something about an
ActiveRecord::Associations::CollectionProxy 😉 ) So we didn’t generate an error, but we didn’t create any tags either.
We need to return to the
Article#tag_list= method in
article.rb and do some more work.
Article#tag_list= method accepts a parameter, a string like “tag1, tag2, tag3” and we need to associate the article with tags that have those names. The pseudo-code would look like this:
- Split the tags_string into an array of strings with leading and trailing whitespace removed (so
"tag1, tag2, tag3"would become
- For each of those strings…
- Ensure each one of these strings are unique
- Look for a Tag object with that name. If there isn’t one, create it.
- Add the tag object to a list of tags for the article
- Set the article’s tags to the list of tags that we have found and/or created.
The first step is something that Ruby does very easily using the
String#split method. Go into your console and try “tag1, tag2, tag3”.split. By default it split on the space character, but that’s not what we want. You can force split to work on any character by passing it in as a parameter, like this:
"tag1, tag2, tag3".split(",").
Look closely at the output and you’ll see that the second element is
" tag2" instead of
"tag2" – it has a leading space. We don’t want our tag system to end up with different tags because of some extra (non-meaningful) spaces, so we need to get rid of that. The
String#strip method removes leading or trailing whitespace – try it with
" my sample ".strip. You’ll see that the space in the center is preserved.
So first we split the string, and then trim each and every element and collect those updated items:
String#split(",") will create the array with elements that have the extra spaces as before, then the
Array#collect will take each element of that array and send it into the following block where the string is named
s and the
String#downcase methods are called on it. The
downcase method is to make sure that “ruby” and “Ruby” don’t end up as different tags. This line should give you back
["programming", "ruby", "rails"].
Lastly, we want to make sure that each and every tag in the list is unique.
Array#uniq allows us to remove duplicate items from an array.
Now, back inside our
tag_list= method, let’s add this line:
So looking at our pseudo-code, the next step is to go through each of those
tag_names and find or create a tag with that name. Rails has a built in method to do just that, like this:
And finally we need to collect up these new or found new tags and then assign them to our article.
Go back to your console and try these commands:
You should get back a list of the two tags. If you’d like to check the other side of the Article-Tagging-Tag relationship, try this:
And you’ll see that this Tag is associated with just one Article.
According to our work in the console, articles can now have tags, but we haven’t done anything to display them in the article pages.
Let’s start with
app/views/articles/show.html.erb. Right below the line that displays the
<h1><%= @article.title %></h1>, add these lines:
Refresh your view and…BOOM:
link_to helper is trying to use
tag_path from the router, but the router doesn’t know anything about our Tag object. We created a model, but we never created a controller or route. There’s nothing to link to – so let’s generate that controller from your terminal:
Then we need to add tags as a resource to our
config/routes.rb, it should look like this:
Refresh your article page and you should see tags, with links, associated with this article.
The links for our tags are showing up, but if you click on them you’ll see our old friend “The action ‘show’ could not be found…” error.
app/controllers/tags_controller.rb and define a show action:
Then create the show template
Refresh your view and you should see a list of articles with that tag. Keep in mind that there might be some abnormalities from articles we tagged before doing our fixes to the
tag_list= method. For any article with issues, try going to its
edit screen, saving it, and things should be fixed up. If you wanted to clear out all taggings you could do
Tagging.destroy_all from your console.
We’ve built the
show action, but the reader should also be able to browse the tags available at http://localhost:3000/tags. I think you can do this on your own. Create an
index action in your
tags_controller.rb and an
index.html.erb in the corresponding views folder. Look at your
articles_controller.rb and Article
index.html.erb if you need some clues.
Now that we can see all of our tags, we also want the capability to delete them. I think you can do this one on your own too. Create a
destroy action in your
tags_controller.rb and edit the
index.html.erb file you just created. Look at your
articles_controller.rb and Article
show.html.erb if you need some clues.
With that, a long “Tagging” tutorial is complete!
Woah! The tagging feature is now complete. Good on you. You’re going to want to push this to the repo.