现在的位置: 首页 > 综合 > 正文

Ajax in Rails 3.1 – A Roadmap by Andrea Singh | December 05, 2011

2018年04月19日 ⁄ 综合 ⁄ 共 30243字 ⁄ 字号 评论关闭

Ajax in Rails 3.1 - A Roadmap
by Andrea Singh | December 05, 2011

By adopting jQuery and including the UJS adapter, Rails 3.1 has made initiating Ajax requests as easy as adding a data-remote attribute to a DOM element.

However, in order to make optimum use of this new infrastructure, it helps to know a bit about what's going on under the hood. In this article, we'll take a look at which parts of the Ajax request the UJS adapter handles, which parts it doesn't and how you
can customize its functioning in those instances when you need more fine-grained control. After discussing the technical details, we will provide some practical examples to illustrate how the Rails UJS adapter can be harnessed to facilitate various types of
Ajax calls.
jQuery and the $.ajax() Function

Before we dive into the UJS Ajax helper functions, here is a quick refresher on how jQuery itself handles asynchronous calls to the server. If you're familiar with doing Ajax with jQuery, you might want to skip this section, but note that we'll be referring
to a lot of the terms introduced here.

The special sauce that makes the asynchronous communication between the browser and the server possible is the XMLHttpRequest (XHR) object. For each Ajax request, an instance of this object needs to be created in order to encapsulate the different details of
the request and handle the response.

In practice, instantiating the XHR object is rather involved because of browser differences, but luckily jQuery abstracts away the process of setting up the Ajax request and deals with the server response. It does this by means of the general utility function
$.ajax() which lies at the heart of every jQuery Ajax call. This function exposes the configurable properties of the XHR object and also allows you to hook into different stages of the Ajax lifecycle via callbacks.

The $.ajax() function takes a single parameter, an options object. This object is comprised of a set of key-value pairs used for configuration. You can find a full list of possible settings here. The more commonly used ones are:
url: where to send the request to (default is the current page)
type: usually GET or POST (default is GET)
dataType: the format of the data we expect back from the server. Sets a property called "Accept" in the HTTP request header.
Callback functions to hook into various stages of the request, such beforeSend, complete, error and success

Several convenience methods are also provided. These include $.get(), $.getScript(), $.getJSON(), $.post() and .load(), all of which pre-configure some of the above mentioned properties. Eventually, though, they all call the $.ajax() function in the background.
Rails 3.1 and the jQuery UJS Adapter

As convenient as jQuery makes Ajax calls, the Rails UJS adapter goes one step further in facilitating asynchronous requests.
Note that the jQuery UJS adapter is part of the jquery-rails gem that is included in every new Rails 3.1 project. The file we're referring to here - jquery_ujs.js - is located inside that gem. It is available in the search path for Sprockets and not actually
part of the application tree. To check it out you can either open the jquery-rails gem and navigate to vendor/assets/javascripts/jquery_ujs.js, or else take a look at the file on Github.

Before the advent of Rails 3.1, a typical Ajax request with jQuery would look something like the code below. In this example, we're binding the click event of a link and initiating an Ajax request to load a comment form into the DOM:
// adds a comment form upon click
$('a#add_comment').live('click', function(event) {
$.ajax({
url: $(this).href,
dataType: 'script'
});
return false;
});

Things are much simpler with Rails 3.1. The UJS adapter allows us to make exactly the same Ajax call with just one line of code:
<%= link_to "Add a Comment", new_comment_path, :remote => true %>

This will generate the following HTML:
<a href="/comments/new" data-remote="true">Add a Comment</a>

The UJS adapter finds all DOM elements with the HTML5 attribute data-remote set to true and binds common events associated with those element, such as the click event for links or the submit event for forms. When the event fires, an $.ajax() call is executed
in the background.
In HTML5 data-* attributes allow us to embed custom data on all HTML elements. These custom attributes need to be prefixed by data- followed by the name of the attribute. The value can be anything. An example is data-remote="true". Data attributes are most
often interacted with via JavaScript. In fact, jQuery has a convenient $.data( key ) method that returns the value for the data-[key] attribute on the element in question. For more information, see the jQuery API reference for the .data() method.

Let's now take a look at the relevant parts of the UJS adapter to see how this works.
Setting Up the Ajax Request: Configuration Defaults and How to Override Them

To get the most out of the UJS helpers, we first need to familiarize ourselves with the types of event listeners available, what kind of elements they're bound to and what Ajax configuration options are automatically set and finally how they can be customized.

At first glance the UJS file might appear a bit daunting, but rest assured that it's actually quite readable and well documented.

The UJS adapter is basically a jQuery plugin named $.rails. Its main purpose is to make Ajax requests and other common JavaScript tasks, such as confirm dialogs, unobtrusive. In our discussion, we will be focusing on the Ajax-related parts of the UJS adapter.

Near the beginning of the file, you'll find a bunch of different jQuery selectors grouped together and assigned to properties of the $.rails object:
// Link elements bound by jquery-ujs
linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]',

// Select elements bound by jquery-ujs
inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]',

// Form elements bound by jquery-ujs
formSubmitSelector: 'form',

// Form input elements bound by jquery-ujs
formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not(button[type])'

For example the linkClickSelector property called has as its value a list of a (link) tags with various data-* attributes. a[data-remote] is one of them.

This initial setup suggests that the UJS adapter is aware of:
links with data-* attributes
input, select and textarea elements, that have the data-remote attribute
forms and the different elements by which forms can be submitted

There is a reason those selectors have been assembled in this way. As we will see, they all will get specific event listeners attached to them.
In jQuery, as in other JavaScript frameworks, there are many ways to bind event listeners to DOM elements. As of jQuery 1.7, the preferred way is via the .on() method. Other methods to attach event handlers are .bind(), .live() and .delegate(). To find out
more about how they differ, see this blog post on The Difference Between jQuery’s .bind(), .live(), and .delegate()

For example, any link with a data-remote attribute set to true, and therefore part of the linkClickSelector group, gets a click event listener attached to it. (Actually, it's the click.rails event, which is an example of a namespaced event).

In the code below you can see how this is being done. After checking for a few things like whether the link is supposed to generate a confirm popup, has been disabled or was right clicked, the callback function invokes the rails.handleRemote() method sending
on the clicked DOM link as an argument.
// https://github.com/rails/jquery-rails/blob/master/vendor/assets/javascripts/jquery_ujs.js

$(document).delegate(rails.linkClickSelector, 'click.rails', function(e) {
var link = $(this),
method = link.data('method'),
data = link.data('params');

// This has to do with the :confirm attribute that you can set
if (!rails.allowAction(link)) return rails.stopEverything(e);

// check whether link is disabled
if (link.is(rails.linkDisableSelector)) rails.disableElement(link);

if (link.data('remote') !== undefined) {
// Ajax call won't happen on right click
if ( (e.metaKey || e.ctrlKey) && (!method || method === 'GET') && !data ) { return true; }

if (rails.handleRemote(link) === false) { rails.enableElement(link); }
return false;

} else if (link.data('method')) {
rails.handleMethod(link);
return false;
}
});

Similar things take place for "ajaxified" input, select and textarea elements, which listen for the change event, as well as forms that will fire an Ajax call on submit.

The callback functions associated with these event listeners ultimately end up at the rails.handleRemote() function which is responsible for setting up and initializing the Ajax request. There is quite a bit going on in that function, so I won't paste it in
its entirety, but you should check it out for yourself.

You will notice that the actual Ajax call only takes place at the very end of the function, while the main body is concerned with building up the Ajax options object that is destined for jQuery's $.ajax() method.

In following three tables, I've assembled some of the default configuration values for this options depending on what type of DOM element has initiated the Ajax request. Also included are some notes on how these properties can be customized. $.ajax() properties
for links (on event type: "click")
url Read from the href attribute of the link.
method GET by default but can be overridden by setting the data-method attribute

dataType The default is script, i.e. the server is expected to return valid JavaScript. Can be overridden by setting the data-type attribute.

data Used for sending additional query parameters not included in the url string. You define extra parameters with the data-params attribute, e.g. data-params="data1=value1&data2=value2"

$.ajax() properties for input, select, textarea (on event type: "change")
url By default the current page, but can be overridden with the data-url attribute.

method GET by default but can be overridden by setting the data-method attribute

dataType The default dataType is script, i.e. the server is expected to return valid JavaScript. Can be overridden by setting the data-type attribute.

data Parameters are sent by serializing the element via element.serialize();

$.ajax() properties for forms (on event type: "submit")
url Picked up from the action attribute on forms.
method Rails usually sets the correct method attribute on forms to POST or PUT if you use the form_for view helper. You can also set the method attribute yourself.

dataType The default dataType is script, i.e. the server is expected to return valid JavaScript. Can be overridden by setting the data-type attribute.

data Form data is sent to the server by serializing the form via element.serializeArray();

Receiving the Ajax Response: UJS Custom Events

Note that the handleRemote() described above only takes care of initiating the Ajax request. You are still responsible for making sure that the server responds appropriately and for processing the data returned. In this context, you will sometimes need to make
use of callback functions.

The $.ajax() method allows for user-defined callbacks to hook into the different stages of the Ajax request. As we have seen, the UJS adapter has wrapped the $.ajax() call, in which we would normally define the callbacks. However, it does provide alternate
ways for us to declare callbacks as needed.

This is accomplished by firing custom events that tie into the regular jQuery callbacks in the lifecycle of the Ajax request. The names of those custom events mirror the ones they correspond to, but with an ajax: prefix, e.g. ajax:beforeSend.

You can view a full list of those events and when they are fired here.

To better understand that those custom events are basically a proxy for the actual $.ajax() callbacks, let's take a quick look at the part of the handleRemote() function that is responsible for setting them up.
// The options object will be eventually passed to the ajax() function
options = {
// stopping the "ajax:beforeSend" event will cancel the ajax request
beforeSend: function(xhr, settings) {
if (settings.dataType === undefined) {
xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
}
return rails.fire(element, 'ajax:beforeSend', [xhr, settings]);
},
success: function(data, status, xhr) {
element.trigger('ajax:success', [data, status, xhr]);
},
complete: function(xhr, status) {
element.trigger('ajax:complete', [xhr, status]);
},
error: function(xhr, status, error) {
element.trigger('ajax:error', [xhr, status, error]);
}
};

For example, you can see that the ajax:success event gets triggered in the callback function for success. So by subscribing your DOM element to this event, you get the chance to define a callback function like so:
$(".delete_comment").live('ajax:success', function(evt, data, status, xhr){
$(this).closest('div').fadeOut();
});
The Format of the Server Response

As we have seen, setting the dataType property tells the server what type of response data the client is requesting.

Deciding what this format should be is probably one of the most debated parts of Ajax in Rails and perhaps Ajax in general. In Rails you can choose convention over configuration and let the dataType default to script. That way you can handle everything to do
with the response in a .js.erb or .js.coffee file and thereby avoid the need for callbacks, etc.

While I was researching this blog post, I cam across an article by Chad Fowler that grew out of a survey he had posted on Twitter: How Rails Developers do Ajax (with jQuery) in 2011.

From the survey results and the comments that followed, it would appear that there is no consensus amongst Rails developers as to whether the server should respond with JavaScript and/or HTML or whether it should be sending raw data in form of JSON and delegate
the responsibility to the browser to deal with the data.

One can argue for both approaches. I agree with Chad when he says that while sending JSON might be better from a purist point of view (after all the server is not supposed to deal with the nitty gritty of DOM structure), sending JavaScript or HTML is often
the more pragmatic approach.

And in fact, the UJS adapter in Rails sets up the stage in a way that actually favors this latter approach and is the least hassle.

Notwithstanding philosophical biases, there might be cases when you might want to return something other than JavaScript. So it's good to know your options when it comes to the dataType property.
Setting the dataType property

The dataTypes that jQuery recognizes are these:
text: simple strings
html: blocks of html
script: a snippet of JavaScript, executed on the page
json: JSON-formatted data
jsonp: "padded" JSON for cross-domain requests
xml: XML-formatted data

As we will see a little later on, these names are short forms that jQuery understands and can translate into the correct HTTP Accept header values.

Looking at how the handleRemote() method tries to set the dataType property, we can see that we have three opportunities to customize the setting. Here is the code from the UJS adapter:
// Submits "remote" forms and links with ajax
handleRemote: function(element) {
// ....
dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType);

options = {
beforeSend: function(xhr, settings) {
if (settings.dataType === undefined) {
xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
}
return rails.fire(element, 'ajax:beforeSend', [xhr, settings]);
}
};

rails.ajax(options);
}

The first way is by adding a data-type attribute to the DOM element in question. Doing this will take precedence over everything else. For example, here is how you'd set the dataType to accept json:
<%= link_to "Add a new task", new_project_task_path(@project), "data-type" => "json", :id => "add_task_btn" %>

You can also define a default dataType via jQuery's global $.ajaxSetup() function. This will change the default dataType for all Ajax requests:
$.ajaxSetup({
dataType: 'json'
});

Finally, in the beforeSend callback, you have one last chance to override the dataType property. Note that in this case you cannot use the short form, but have to use the correct string that will be sent as the HTTP Accept header. The ability to reset the dataType
in this fashion comes in handy for example when for some reason you don't have direct access to the DOM element itself.
$("#add_task_button").live("ajax:beforeSend", function(e, xhr, settings){
new_data_type = "application/json, text/javascript, */*; q=0.01";
xhr.setRequestHeader('accept', new_data_type);
})
The dataType property and HTTP Headers

The dataType setting ends up as the "Accept" Header, or more precisely, the value of the Accept key in the Request Header. The value is a comma-separated string of formats that will be accepted by the client. For instance:
*/*;q=0.5, text/javascript

The individual values represent acceptable MIME types and are sometimes accompanied by a value for the relative degree of preference q. The q is a number between 0 and 1 and, unless specified, is implicitly 1.

*/* represents a generic request that tells the server to return whatever it has. In practice, Rails will return the first format defined in the controller action.

So this setting - */*;q=0.5, text/javascript -, which happens to be the default in the UJS adapter, tells the server that javascript is preferred (implicit q=1), but failing that any other format will do, though relatively less preferred (q=0.5).

There are historic reasons for this default setting. The original default was simply text/javascript, but that meant that unless the javascript MIME type was handled by the controller, i.e. the js format was defined, Rails would throw a MissingTemplate error.
By adding the generic request as the second preference, Rails will return any other defined response. Source
Sending the right data format back from the server

It is your task to ensure that the Controller returns the expected data type, or else something that is compatible.

In practice, by leaving the default dataType as script you can handle the Ajax request with a js.erb or a js.coffee file in which you can intermingle Ruby and JavaScript to determine the response. The server will send this back as pure JavaScript, which will
be automatically evaluated by the browser.

Generally speaking, several things have to play nicely together for there to be a smooth transaction between browser and server:

First, Ajax sends the request with the Accept header set to the desired MIME type.

Next, the Rails Controller checks the Accept header to determine what it should (ideally) return. To do that it has to first translate the content of the Accept header into known MIME types.

Lastly, the Controller will determine whether the action in question handles the particular MIME type. Before Rails 3 this was typically done in a respond_to block in the Controller action. With Rails 3 you can take advantage of the respond_with method to save
you some typing.

For the rest of the article, we'll look at a variety of ways in which Ajax can be implemented in Rails 3.1 apps.
Example 1: Click Event: Inserting DOM Elements

Let's say we have a link that prompts users to leave a comment. When it is clicked, we want to insert a comment form via Ajax.

Here we can easily use the convenience provided by the UJS adapter to get the job done. Let's first look at the actual link that triggers the Ajax request when clicked:
<%= link_to "Add a Comment", new_comment_path, :remote => true %>

Recall that by adding the :remote => true attribute the UJS adapter will attach an event listener for the click event that will initiate the Ajax request with the following properties:
url: '/comments/new' (read from the href attribute)
method: 'GET' (this is the default for links)
dataType: 'script' (also the default)

Very little code is needed in the Controller:
class CommentsController < ApplicationController
respond_to :html, :js

def new
@comment = Comment.new
respond_with(@comment)
end
end

Finally, the app/views/comments/new.js.erb template:
var comment_form = $('<%= j(render(:partial => "form"))%>');

$('#new_comment').html(comment_form);

And that's it!
Example 2: Submitting a Form via Ajax

Submitting a form via Ajax is not much more involved. For this example we'll submit a comment form.

Again, we just need to add the :remote => true attribute to the form:
<div id="new_comment">
<%= form_for @comment, :remote => true do |f| %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :body, "Comment" %><br />
<%= f.text_area :body %>
</div>
<div class="actions">
<%= submit_tag "Submit" %>
</div>
<% end %>
</div>
<div id="comments-container">
<%= render :partial => @comments %>
</div>

Again, the Controller side of things is fairly routine. Notice that we are using the :location option of the respond_with method. While this does not affect the Ajax request, for an HTML request it would tell Rails to redirect the user to the comments/index
page (instead of the default comments/show/:id) after saving the comment successfully.
class CommentsController < ApplicationController
respond_to :html, :js

def create
@comment = Comment.new(params[:comment])
flash[:notice] = 'Comment was successfully created.' if @comment.save
respond_with(@comment, :location => comments_path)
end
end

Let's assume that we'd like to avoid blank comment submissions and that the model has some validations in place to ensure that. So how would we handle the case of a blank form submission? It turns out that error handling in .js.erb templates is a cinch:
var form = $('#new_comment');

<% if @comment.errors.any? %>

// Create a list of errors
var errors = $('<ul />');

<% @comment.errors.full_messages.each do |error| %>
errors.append('<li><%= escape_javascript(error) %></li>');
<% end %>

// Display errors before the form
$('<div />').addClass('.errors').html(errors).prependTo("#new_comment");

<% else %>

// Add the new comment to the beginning of the #comment-container division
$("#comments-container").prepend('<%= j(render(:partial => "comment", :object => @comment))%>');

// empty the form for new submission
form.find('input:text,textarea').val('');

<% end %>
Example 3: Deleting via Ajax

So far we've been using the default Ajax setup taking advantage of the UJS adapter and the respond_with feature in Rails. Now we're going to be looking at a scenario where you might want to diverge from the default, even though you don't strictly need to.

We want to add the possibility to delete a comment without refreshing the page. The comment should simply disappear when we click the corresponding "Delete" link. Here is what the view code looks like:
<div id="comment-<%= comment.id %>">
<p><%= comment.name %></p>
<div><%= comment.body %></div>
<%= link_to "Delete", comment_path(comment), :remote => true, :method => :delete, :confirm => 'Are you sure?', :class => "delete_comment" %>
<hr />
</div>

We could again just simply handle the server response with a js.erb template. However, in this case, generating a new template file seems like overkill.

If you think about it, we don't need the server to return anything. The resource, in a REST-ful sense, has been deleted. It is unlikely that the deletion process will be unsuccessful, since the delete links all have the correct url with the right comment id.
So unless the user tampers with the url everything should go as planned. Ideally, we just need the server to let us know that it has finished its business. We'll do that by overriding the default template rendering behavior of the controller by telling it
to render "nothing":
class CommentsController < ApplicationController
respond_to :html, :js

def destroy
@comment = Comment.find(params[:id])
@comment.destroy
respond_with(@comment) do |format|
format.js { render :nothing => true }
end
end
end

Once we get the green light from the server, we'll remove every trace of that comment from the page. Since we don't render a template we need to define what to do if the request is successful in a callback function. As we discussed, with the UJS adapter this
means subscribing to custom events:
$(document).ready(function() {
$(".delete_comment").live('ajax:success', function(evt, data, status, xhr){
$(this).closest('div').fadeOut();
});
});

In the code above we simply fade out the division directly surrounding the comment that has just been deleted.
Example 4: Client-side Validation with Ajax (Using data-remote on an input field)

Links and forms are not the only elements that when given a data-remote attribute will trigger an Ajax call. As we have seen in the UJS adapter file, certain form elements such as inputs, selects and text areas can just as easily be "ajaxified". The event that
was attached to them is change, which means that the Ajax call gets triggered when the user changes the value of the input field and leaves it.

As an example use case of this feature, consider that you want to check whether a username has already been taken before a sign up form is being submitted. To accomplish this client-side validation task, you need to perform a server roundtrip via Ajax.

To provide feedback we want to display a message next to the input field informing the user whether the username is taken or still available.

Again, the question arises as to what type of data the server should return. We could theoretically leave it to the server to display the right message in the DOM by reverting to the use of js.erb templates. However, again, this seems like overkill. One alternative
would be to let the server search for a user by the username and return its search results as a JSON object.

But first things first. Here is a user signup form with an input field wired up to respond to the change event:
<%= form_for @user do |f| %>
<div class="field">
<%= f.label :username %><br />
<%= f.text_field :username, :remote => true, "data-url" => "/users/check_username", "data-type" => :json %>
<span id="username_check">&nbsp;</span>
</div>
<div class="field">
<%= f.label :password %><br />
<%= f.password_field :password %>
</div>
<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation %>
</div>
<div class="actions">
<%= f.submit "Sign Up" %>
</div>
<% end %>

Notice that in addition to the data-remote attribute, the input field also sports two more custom data- attributes, namely data-type and data-url.

The data-url attribute will override the UJS default setting of sending the request to the current page. In addition, we also need to create a separate route to handle the username check with a url of /users/check_username:
// config/routes.rb

resources :users do
collection do
get 'check_username'
end
end

Using the convenience of respond_with, this is all we need to do in the Controller to ensure that the @user resource is returned as a JSON object:
class UsersController < ApplicationController
respond_to :html, :json

def check_username
@user = User.find_by_username(params[:user][:username])
respond_with(@user)
end

end

We can handle the response from the server in a success callback. The data returned from the server will be the @user object in JSON format. If the username is still available, @user will be nil. nil.to_json returns null. We therefore check for null in our
success callback.

On the other hand, if the @user does exist, we can parse the JSON response and extract the username:
$(document).ready(function() {

$("#user_username").bind('ajax:success', function(evt, data, status, xhr){
if (data !== null) {
$('#username_check').html(data.username + ' is already taken');
} else {
$('#username_check').html('Username is available!');
}
});

// cancel Ajax call if input field empty
$("#user_username").live('ajax:before', function(){
if ($(this).val() == '') {
return false;
}
});

$("#user_username").focus(function(){
$('#username_check').empty();
});

});

The rest of the code handles the case when the change event is triggered by a blank input field. To deal with this scenario, we bind the ajax:before event and return false, which serves to cancel the Ajax call. Finally, we also need to make sure that the message
about username availability disappears when the input field gets refocused.
Example 5: Adding Dynamic Selects (Using data-remote on a select field)

Like the input field from Example 4, select fields with a data-remote attribute also trigger Ajax calls on change.

To demonstrate how to make use of this setup, we will create a dynamic, dependent select in a user signup form. When a user selects her country, a second select will become active making the states in that country selectable. Since in many countries the state
is not a required part of the address, the state select will remain deactivated when one of those countries is chosen.

For the actual geographical data needed to populate the various dropdowns, we'll make use of a gem named carmen. It touts itself as a "simple collection of geographic names and abbreviations for Ruby". Among other things, carmen replaces the official Rails
country_select view helper and supports states for a number of countries like Australia, Brazil, Canada, China, Cuba, Denmark, Germany, India, Italy, Mexico, the Netherlands, New Zealand, Norway, Spain, Ukraine, and United States.

To set this up, you simply need to add gem "carmen" to your Gemfile. You can then use the following code to the user signup form:
<%= form_for @user do |f| %>
<!-- other form fields here -->
<div class="field">
<%= f.label :country %><br />
<%= f.country_select :country, nil, {:include_blank => true}, "data-remote" => true, "data-url" => "/users/get_states", "data-type" => :json %>
</div>
<div class="field">
<%= f.label :state %><br />
<%= f.select :state, [], { :include_blank => true }, :disabled => true %>
</div>
<!-- other form fields here -->
<div class="actions">
<%= f.submit "Sign Up" %>
</div>
<% end %>

Notice that the state select is disabled by default and contains no options. The country_select has three data-* attributes defined: data-url to tell UJS where to send the request, data-type => :json to override the default script data format and as always
data-remote to trigger the Ajax call when a country is selected.

Here we choose to once again to return the states as JSON and, in a success callback, add them as options to the state select. Since this is a bit of an oddball request, we'll add a new, non-RESTful route called get_states to the User resource. Here is the
Controller code:
class UsersController < ApplicationController
respond_to :html, :json

rescue_from Carmen::StatesNotSupported, Carmen::NonexistentCountry, :with => :state_not_supported

def get_states
@states = Carmen.states(params[:user][:country])
respond_with(@states)
end

protected

def state_not_supported
@states = nil
respond_with(@states)
end

end

One side effect of using the Carmen gem was that I had to rescue a special type of error raised if states are requested for a country that doesn't have any. (I guess the gem was not designed for dependent selects.) Also, I had to handle the case that the country
code is a blank string, which would trigger a Carmen::NonexistentCountry error. This can happen for example when the user selects a country and afterwards resets the select to the blank option.

To get the states for a particular country, we use the class method Carmen.states which takes a country code and returns an array structure:
Carmen.states('US') => [['Alabama', 'AL'], ['Arkansas', 'AR'], ... ]

The JSON representation of this array looks like this:
{0: ["Alabama","AL"], 1: ["Alaska","AK"], 2: ["Arizona","AZ"], ...}

In the success callback we parse the JSON structure and construct select options for inserting into the state select, which then gets re-enabled:
$(document).ready(function() {

$("#user_country").bind('ajax:success', function(evt, data, status, xhr){
var select = $('#user_state');

if (data !== null) {
select.removeAttr('disabled');
$.each(data, function(key, value) {
$("<option/>").val(value[1]).text(value[0]).appendTo(select);
});
} else {
select.empty();
select.attr('disabled', 'disabled');
}
});

});

Voilå, a functioning dynamic country and state drop down pair!
Example 6: Ajax Pagination - Returning HTML directly

There are many ways to do pagination via Ajax. For demo purposes, though, I'll use it as an example of where we could return an HTML partial rather than JavaScript or JSON.

Pagination in Rails is usually done with the help of a gem. I considered using the will_paginate gem for this demo, but unfortunately adding extra attributes, such as data-remote, to the underlying links generated by the will_paginate view helper method turned
out to be harder than expected. If you're curious, it involves creating a custom link renderer that inherits from WillPaginate::LinkRenderer.

The alternative pagination gem that has gained some traction lately is Kaminari, which from a setup and interaction perspective is much like will_paginate and happens to support Ajax links for its paginate helper method.

In this example, we'll paginate a list of users, each displayed with their name, avatar and location info. When we click on a pagination link, we'd like to render an HTML partial containing the next page of users along with the updated pagination links.

To begin with, you need to add gem "kaminari" to your Gemfile. Next, set up the view page in such a way that the varying content is contained in a partial that is surrounded by an identifiable division. Something like this would work:
<div id="container">
<%= render :partial => "users" %>
</div>

And this is the content of the _users.html.erb partial including the kaminari paginate helper:
<%= paginate @users, :remote => true %>

<%= @users.each do |user| %>
<dl>
<dd class='img'>
<%= gravatar_image_tag(user.email) %>
</dd>
<dt>
<%= link_to user.username, user_path(user) %>
</dt>
</dt>
<dd>
<%= [user.country, user.state].compact.join(", ") %>
</dd>
</dl>
<% end %>

It was at this point that I realized that while the Kaminari pagination helper supports the data-remote attribute, it doesn't recognize other custom data-* attributes. For this to work as an HTML data type example, we would need to set the data-type attribute.
I got around this limitation by setting that attribute in a ajax:beforeSend callback:
$(document).ready(function() {
$(".pagination span a").live('ajax:beforeSend', function(event, xhr, settings){
xhr.setRequestHeader('accept', 'text/html');
});
});

In the Controller we have a new sort of situation: Ajax has requested HTML as the response MIME type. However, we only want to render the partial if the request came via Ajax. Otherwise, that is in the case of a non-Ajax request we want to render the regular
HTML template. Luckily, there is a simple way to distinguish between the two types of requests:
class UsersController < ApplicationController
respond_to :html, :js, :json

def index
@users = @users = User.order(:username).page(params[:page]).per(10)
respond_with(@users) do |format|
format.html {
if request.xhr?
render :partial => "users"
end
}
end
end

The last piece of the puzzle is to actually insert the contents of the partial in the right place. Here is the code for the success callback that accomplishes this:
$(document).ready(function() {
$(".pagination span a").live('ajax:success', function(evt, data, status, xhr){
$("#container").html(data);
});
});

And there you have it: Ajax pagination with Kaminari.
There was a problem loading Disqus. For more information, please visit status.disqus.com.

抱歉!评论已关闭.