require 'set' require 'active_support/json' require 'active_support/core_ext/object/returning' module ActionView module Helpers # Prototype[http://www.prototypejs.org/] is a JavaScript library that provides # DOM[http://en.wikipedia.org/wiki/Document_Object_Model] manipulation, # Ajax[http://www.adaptivepath.com/publications/essays/archives/000385.php] # functionality, and more traditional object-oriented facilities for JavaScript. # This module provides a set of helpers to make it more convenient to call # functions from Prototype using Rails, including functionality to call remote # Rails methods (that is, making a background request to a Rails action) using Ajax. # This means that you can call actions in your controllers without # reloading the page, but still update certain parts of it using # injections into the DOM. A common use case is having a form that adds # a new element to a list without reloading the page or updating a shopping # cart total when a new item is added. # # == Usage # To be able to use these helpers, you must first include the Prototype # JavaScript framework in your pages. # # javascript_include_tag 'prototype' # # (See the documentation for # ActionView::Helpers::JavaScriptHelper for more information on including # this and other JavaScript files in your Rails templates.) # # Now you're ready to call a remote action either through a link... # # link_to_remote "Add to cart", # :url => { :action => "add", :id => product.id }, # :update => { :success => "cart", :failure => "error" } # # ...through a form... # # <% form_remote_tag :url => '/shipping' do -%> #
<%= submit_tag 'Recalculate Shipping' %>
# <% end -%> # # As you can see, there are numerous ways to use Prototype's Ajax functions (and actually more than # are listed here); check out the documentation for each method to find out more about its usage and options. # # === Common Options # See link_to_remote for documentation of options common to all Ajax # helpers; any of the options specified by link_to_remote can be used # by the other helpers. # # == Designing your Rails actions for Ajax # When building your action handlers (that is, the Rails actions that receive your background requests), it's # important to remember a few things. First, whatever your action would normally return to the browser, it will # return to the Ajax call. As such, you typically don't want to render with a layout. This call will cause # the layout to be transmitted back to your page, and, if you have a full HTML/CSS, will likely mess a lot of things up. # You can turn the layout off on particular actions by doing the following: # # class SiteController < ActionController::Base # layout "standard", :except => [:ajax_method, :more_ajax, :another_ajax] # end # # Optionally, you could do this in the method you wish to lack a layout: # # render :layout => false # # You can tell the type of request from within your action using the request.xhr? (XmlHttpRequest, the # method that Ajax uses to make background requests) method. # def name # # Is this an XmlHttpRequest request? # if (request.xhr?) # render :text => @name.to_s # else # # No? Then render an action. # render :action => 'view_attribute', :attr => @name # end # end # # The else clause can be left off and the current action will render with full layout and template. An extension # to this solution was posted to Ryan Heneise's blog at ArtOfMission["http://www.artofmission.com/"]. # # layout proc{ |c| c.request.xhr? ? false : "application" } # # Dropping this in your ApplicationController turns the layout off for every request that is an "xhr" request. # # If you are just returning a little data or don't want to build a template for your output, you may opt to simply # render text output, like this: # # render :text => 'Return this from my method!' # # Since whatever the method returns is injected into the DOM, this will simply inject some text (or HTML, if you # tell it to). This is usually how small updates, such updating a cart total or a file count, are handled. # # == Updating multiple elements # See JavaScriptGenerator for information on updating multiple elements # on the page in an Ajax response. module PrototypeHelper unless const_defined? :CALLBACKS CALLBACKS = Set.new([ :create, :uninitialized, :loading, :loaded, :interactive, :complete, :failure, :success ] + (100..599).to_a) AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url, :asynchronous, :method, :insertion, :position, :form, :with, :update, :script, :type ]).merge(CALLBACKS) end # Creates a button with an onclick event which calls a remote action # via XMLHttpRequest # The options for specifying the target with :url # and defining callbacks is the same as link_to_remote. def button_to_remote(name, options = {}, html_options = {}) button_to_function(name, remote_function(options), html_options) end # Returns a form tag that will submit using XMLHttpRequest in the # background instead of the regular reloading POST arrangement. Even # though it's using JavaScript to serialize the form elements, the form # submission will work just like a regular submission as viewed by the # receiving side (all elements available in params). The options for # specifying the target with :url and defining callbacks is the same as # +link_to_remote+. # # A "fall-through" target for browsers that doesn't do JavaScript can be # specified with the :action/:method options on :html. # # Example: # # Generates: # #
# form_remote_tag :html => { :action => # url_for(:controller => "some", :action => "place") } # # The Hash passed to the :html key is equivalent to the options (2nd) # argument in the FormTagHelper.form_tag method. # # By default the fall-through action is the same as the one specified in # the :url (and the default method is :post). # # form_remote_tag also takes a block, like form_tag: # # Generates: # #
# #
# <% form_remote_tag :url => '/posts' do -%> #
<%= submit_tag 'Save' %>
# <% end -%> def form_remote_tag(options = {}, &block) options[:form] = true options[:html] ||= {} options[:html][:onsubmit] = (options[:html][:onsubmit] ? options[:html][:onsubmit] + "; " : "") + "#{remote_function(options)}; return false;" form_tag(options[:html].delete(:action) || url_for(options[:url]), options[:html], &block) end # Creates a form that will submit using XMLHttpRequest in the background # instead of the regular reloading POST arrangement and a scope around a # specific resource that is used as a base for questioning about # values for the fields. # # === Resource # # Example: # <% remote_form_for(@post) do |f| %> # ... # <% end %> # # This will expand to be the same as: # # <% remote_form_for :post, @post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %> # ... # <% end %> # # === Nested Resource # # Example: # <% remote_form_for([@post, @comment]) do |f| %> # ... # <% end %> # # This will expand to be the same as: # # <% remote_form_for :comment, @comment, :url => post_comment_path(@post, @comment), :html => { :method => :put, :class => "edit_comment", :id => "edit_comment_45" } do |f| %> # ... # <% end %> # # If you don't need to attach a form to a resource, then check out form_remote_tag. # # See FormHelper#form_for for additional semantics. def remote_form_for(record_or_name_or_array, *args, &proc) options = args.extract_options! case record_or_name_or_array when String, Symbol object_name = record_or_name_or_array when Array object = record_or_name_or_array.last object_name = ActionController::RecordIdentifier.singular_class_name(object) apply_form_for_options!(record_or_name_or_array, options) args.unshift object else object = record_or_name_or_array object_name = ActionController::RecordIdentifier.singular_class_name(record_or_name_or_array) apply_form_for_options!(object, options) args.unshift object end concat(form_remote_tag(options)) fields_for(object_name, *(args << options), &proc) concat(''.html_safe!) end alias_method :form_remote_for, :remote_form_for # Returns a button input tag with the element name of +name+ and a value (i.e., display text) of +value+ # that will submit form using XMLHttpRequest in the background instead of a regular POST request that # reloads the page. # # # Create a button that submits to the create action # # # # Generates: # <%= submit_to_remote 'create_btn', 'Create', :url => { :action => 'create' } %> # # # Submit to the remote action update and update the DIV succeed or fail based # # on the success or failure of the request # # # # Generates: # <%= submit_to_remote 'update_btn', 'Update', :url => { :action => 'update' }, # :update => { :success => "succeed", :failure => "fail" } # # options argument is the same as in form_remote_tag. def submit_to_remote(name, value, options = {}) options[:with] ||= 'Form.serialize(this.form)' html_options = options.delete(:html) || {} html_options[:name] = name button_to_remote(value, options, html_options) end # Returns 'eval(request.responseText)' which is the JavaScript function # that +form_remote_tag+ can call in :complete to evaluate a multiple # update return document using +update_element_function+ calls. def evaluate_remote_response "eval(request.responseText)" end # Returns the JavaScript needed for a remote function. # Takes the same arguments as link_to_remote. # # Example: # # Generates: { :action => :update_options }) %>"> # # # def remote_function(options) javascript_options = options_for_ajax(options) update = '' if options[:update] && options[:update].is_a?(Hash) update = [] update << "success:'#{options[:update][:success]}'" if options[:update][:success] update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure] update = '{' + update.join(',') + '}' elsif options[:update] update << "'#{options[:update]}'" end function = update.empty? ? "new Ajax.Request(" : "new Ajax.Updater(#{update}, " url_options = options[:url] url_options = url_options.merge(:escape => false) if url_options.is_a?(Hash) function << "'#{escape_javascript(url_for(url_options))}'" function << ", #{javascript_options})" function = "#{options[:before]}; #{function}" if options[:before] function = "#{function}; #{options[:after]}" if options[:after] function = "if (#{options[:condition]}) { #{function}; }" if options[:condition] function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm] return function end # All the methods were moved to GeneratorMethods so that # #include_helpers_from_context has nothing to overwrite. class JavaScriptGenerator #:nodoc: def initialize(context, &block) #:nodoc: context._evaluate_assigns_and_ivars @context, @lines = context, [] include_helpers_from_context @context.with_output_buffer(@lines) do @context.instance_exec(self, &block) end end private def include_helpers_from_context extend @context.helpers if @context.respond_to?(:helpers) extend GeneratorMethods end # JavaScriptGenerator generates blocks of JavaScript code that allow you # to change the content and presentation of multiple DOM elements. Use # this in your Ajax response bodies, either in a