#258 Token Fields (revised)
- Download:
- source codeProject Files in Zip (173 KB)
- mp4Full Size H.264 Video (26.8 MB)
- m4vSmaller H.264 Video (12.3 MB)
- webmFull Size VP8 Video (15.4 MB)
- ogvFull Size Theora Video (28.1 MB)
Below is bookstore application that currently has no books.
When we create a book we can only set its name but we also want to be able to assign multiple authors to it. We already have a many-to-many association set up between the Book
and Author
models through an Authorship
model.
class Book < ActiveRecord::Base attr_accessible :name has_many :authorships has_many :authors, through: :authorships end
The question is how do we set up our form so that we can assign authors to a book? One option is to use checkboxes like we shown in episode 17 but the problem with this approach here is that as we have a lot of authors managing them through checkboxes isn’t practical. A solution to this problem is to use Chosen which allows us to turn a long select menu into one that the user can search through and filter the results. We can also use it on a many-to-many association where it will be turned into a list from which many items can be selected.
We’ll use Chosen to assign authors to a book. The first thing we need to do is add multiple select menu for the authors. We’ll add this below the name field.
<div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :author_ids, "Authors" %><br/> <%= f.collection_select :author_ids, Author.order(:name), :id, :name, {}, {multiple:true} %> </div> <div class="actions"> <%= f.submit %> </div>
We use collection_select
here on the author_ids
attribute which is set up by the has_many
association. Note that in the HTML options hash we set to multiple
to true
so that the list is rendered as a multiple select. In the Book
model we need to update the call to attr_accessible
so that we can update the author_ids
through the form.
attr_accessible :name, :author_ids
Reloading the page now shows the list of authors and we can now assign multiple authors to any books we create.
This now works and when we edit an existing book the correct authors are selected. All we need to do now is use Chosen to turn this in to better way to select authors. There’s a chosen-rails
gem available which makes it easier to integrate Chosen into the asset pipeline and we’ll add this to the assets
group in our gemfile and then run bundle
to install it.
# Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.2.3' gem 'coffee-rails', '~> 3.2.1' gem 'uglifier', '>= 1.0.3' gem 'chosen-rails' end
In the application.js
file we need to add a line to include chosen-jquery
.
//= require jquery //= require jquery_ujs //= require chosen-jquery //= require_tree .
We need to do something similar in the application.css
file.
*= require_self *= require chosen *= require_tree .
We still need to add Chosen to the authors field and we’ll do this inside the books CoffeeScript file.
jQuery -> $('#book_author_ids').chosen()When we reload the page now we’ll see that Chosen is being used for the authors.
We can now type in the authors box to filter the list of authors then select an author to add or easily remove an existing author by clicking the cross next to it. We can make the box look a little prettier by adding some styling.
#book_author_ids { width: 450px; }
jQuery Tokeninput
Chosen is a great way to make a tokenized input field but it doesn’t work well in every situation. If we have hundreds of thousands of records to select from we don’t want to load them all into the client at once and in these cases it would be better to use AJAX to fetch only the records we want. There are some extensions to Chosen to enable this behaviour but we’ll use a different project that designed for these scenarios: jQuery Tokeninput. If we have a lot of records to choose from this is a perfect solution.
To demonstrate this we’ll start our application again but this time we’ll use jQuery Token input to do the same thing. First we’ll download the latest version of the project from Github as this includes some fixes that aren’t in the current release. We’ll need to copy the JavaScript file in the download’s src
directory to our application’s /vendor/assets/javascripts
directory and the stylesheet files from styles
to /vendor/assets/stylesheets
. We’ll need to include the JavaScript file in the application.js
file like this:
//= require jquery //= require jquery_ujs //= require jquery.tokeninput //= require_tree .
In application.css
we’ll include token-input-facebook
as we want to use the Facebook theme.
*= require_self *= require token-input-facebook *= require_tree .
In our form template we need to add a new text field for adding the author tokens.
<div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :author_tokens, "Authors" %><br/> <%= f.text_field :author_tokens %> </div> <div class="actions"> <%= f.submit %> </div>
We don’t have an author_tokens
attribute in our Book
model so we’ll add getter and setter methods for it now.
class Book < ActiveRecord::Base attr_accessible :name, :author_tokens has_many :authorships has_many :authors, through: :authorships attr_reader :author_tokens def author_tokens=(ids) self.author_ids = ids.split(",") end end
The getter method is simple; the string passed to the setter will be a list of ids separated by commas so we’ll split this string and set the author_ids
from the resulting array. We need author_tokens
to be settable through the form through mass assignment so we’ve added to attr_accessible
. What we have so far is an authors text field in which we can enter a comma-separated list of ids. Next we’ll turn it into a token input field.
We can do this in the books CoffeeScript file by getting the book_author_tokens
field and calling tokenInput
on it, passing in the path to load the JSON data when a search is made. There are a variety of other options we can pass in here and these are described in the documentation.
jQuery -> $('#book_author_tokens').tokenInput '/authors.json' theme: 'facebook'
To get this working we need to make /authors.json
respond with some JSON data containing the matching authors. We already have an AuthorsController
with an index action. We’ll add a respond_to
block to it to enable it to respond to JSON requests.
def index @authors = Author.order(:name) respond_to do |format| format.html format.json { render json: @authors } end end
Now when we reload the page we’ll have a token field. When we enter text in to this field we’ll get a list of authors that we can select from.
There’s a problem, however: the authors aren’t filtered by the text we enter. We can fix this by adding a where clause when we fetch the authors.
def index @authors = Author.order(:name) respond_to do |format| format.html format.json { render json: @authors.where("name like ?", "%#{params[:q]}%") } end end
When we start typing into the authors field now the results are filtered.
Updating Existing Records
When we select some authors then save the new book it works but when we go back to edit that book the authors field is blank. We’re going to have to preload the data and we’ll use a data-
attribute on the input field to do this.
<div class="field"> <%= f.label :author_tokens, "Authors" %><br/> <%= f.text_field :author_tokens, data: {load: @book.authors} %> </div>
This will encode the book’s authors in JSON format for us in a data-load
attribute. In our books CoffeeScript file we can use tokenInput
’s prePopulate
option to populate the text field based on this data.
jQuery -> $('#book_author_tokens').tokenInput '/authors.json' theme: 'facebook' prePopulate: $('#book_author_tokens').data('load')
When we reload the page now the book’s authors are shown.
Adding New Authors
The token input field is now pretty much complete and will fetch each book’s authors from the database. It would be nice though if a new author was created when we type a name that isn’t in the current list. To do this we need to have the JSON response from the index
action return something for creating a new author if no match is found. There’s quite a bit of logic involved in doing this so we’ll create a new class method in the Author
model called tokens that takes the search term as an argument.
def index @authors = Author.order(:name) respond_to do |format| format.html format.json { render json: @authors.tokens(params[:q]) } end end
This new method looks like this:
def self.tokens(query) authors = where("name like ?", "%#{query}%") if authors.empty? [{id: "<<<#{query}>>>"}, name: "New: \"#{query}\""}] end end
This performs the same query we had before but now if no matching authors are found it will return a special response for creating a new author with the name that was entered and an id
in a special format that we’ll need to parse on the receiving end. This parsing happens in the Book model where we receive the author tokens. We’ll replace the code that parses these tokens with another new method in Author
.
def author_tokens=(tokens) self.author_ids = Author.ids_from_tokens(tokens) end
Here’s the code for the new method.
def self.tokens(query) authors = where("name like ?", "%#{query}%") if authors.empty? [{id: "<<<#{query}>>>", name: "New: \"#{query}\""}] else authors end end
This code performs a find and replace for the special string. If it finds it it will create a new author with the name that was passed in. Now if we enter an author who doesn’t exist we’ll be given the option to create them and when we submit the form that new author will be added to the list.
We now have a way to manage a many-to-many relationship by using jQuery Tokeninput. One thing to point out is that as have a name
attribute on our Author model returning the JSON for the authors worked. If our model didn’t have a name but instead had a different attribute, say title
, that we want to use for the token input we need to set the propertyToSearch
option to the appropriate attribute.
One final note. There’s a project called Select2 which is based on Chosen but which adds many more features including AJAX loading. This project is still fairly early in development but it’s worth keeping an eye on as it progresses.