Rails 4 - Select tag from collection using Acts as Taggable gem with select_tag
In my rails app I have two models: Team and Post, each Post can have multiple tags, and each Team can have many Posts. In my team's show action I want to present a select_tag with which the user can select one tag of all the created tags for it's posts. When the user selects a tag using the select_tag I only want to show the posts that also have that tag. I use the Acts as Taggable On gem.
What I currently have is this (I'm using HAML):
= select_tag "tags", options_from_collection_for_select(@posts, "tags", :tag_list)
This give me a select that looks something like this:
[row 1] Tag 1 [row 2] Tag 1, Tag 2 [...]
What I want is a select list that looks like this:
[row 1] Tag 1 [row 2] Tag 2 [row 3] Tag 3 [...]
I haven't used select_tag much. Would it be possible to do what I want somehow? And how would that look?
Answers
There's two aspects to this: showing the correct tag list, and filtering the posts by the selected tag.
Showing the correct tag list
You want to create a <select> element that only contains tags for that team's posts. options_from_collection_for_select takes 3 parameters: a collection, a method to get the value for each option, and a method to get the name of each option. So you should be passing a set of tags to options_from_collection_for_select, not a set of posts. And that set of tags shouldn't be all tags - just the ones for your team's posts. Fortunately, acts_as_taggable_on provides a method to do that so you can build tag clouds.
So you're going to update your controller to look something like this:
class TeamController < ApplicationController def show @team = Team.find params[:id] @posts = team.posts @team_tags = team.posts.tag_counts_on(:tags) end end
And update your view to use @team_tags instead:
= select_tag "tag", options_from_collection_for_select(@team_tags, 'id', 'name')
This should give you a select box populated with the tags specific to that team.
Filtering posts by tag
Now we've got the correct tag list, we want to be able to filter posts by tag. That <select> tag should be within a form; you can either create a new controller action that handles that form, or adapt your show method accordingly. The latter's a bit trickier, so I've taken that approach here. Let's update your view to make the assumptions clearer:
= form_tag team_path(@team), method: 'get', class: 'tag_form' do = select_tag "tag", options_from_collection_for_select(@team_tags, 'id', 'name', @tag) = submit_tag "Filter posts"
And update the controller action to filter the posts, if we've got a 'tags' parameter:
class TeamController < ApplicationController def show @team = Team.find params[:id] @team_tags = team.posts.tag_counts_on(:tags) if params[:tag] @tag = Tag.find params[:tag] @posts = team.posts.tagged_with @tag.name else @tag = nil @posts = team.posts end end end
Notice that we're passing in the selected tag as the instance variable @tag, and using that in our options_from_collection_for_select to select the right tag if we chose a filter.
The icing on the cake
You can use a little jQuery and CoffeeScript to hide your submit button and make the filtering happen automatically when the user chooses a new tag, which will improve your user experience a bit. Add this to app/assets/javascripts/teams.js.coffee:
$ -> $forms = $('.tag_form') $forms.each (i, el) -> $('input[type=submit]', el).hide() $('select', el).on 'change', (ev) -> $(el).submit()
This works by finding all elements with a class of tag_form, removes all <input type="submit"> elements within each one, and then listens for change events on the select elements within those forms.