« Bryan Larsen

Ajax filtering on a partially completed form

Posted by Bryan Larsen.

This is a neat trick I picked up reading some of Tom’s code.

This is useful when you’ve got a form that renders differently depending on the state of some of the values in the form. For example, when the user selects “United States” as the country, you may wish to add a “state” field, rename “Postal Code” to “Zip Code”, and modify the list of shipping options in a select box.

You could use lifecycles to make this a two stage form, but this is a neat trick you can use if you want to keep it all in a single form.

Write your view normally using one of the many different options Hobo gives you. In this example, I’m going to extend the form:

<extend tag="form" for="Order">
  <do part="order-part">
    <old-form>
      <field-list: fields="salutation, first_name, last_name, suffix, email_address, phone_number, country, shipping_method">
        <shipping-method-view:>
          <select-one include-none="&false" options="&(this_parent.country.nil? ? [] :  ShippingMethod.find_all_by_country_id(this_parent.country.id))"/>
        </shipping-method-view:>
      </field-list:>
    </old-form>
  </do>
</extend>

In this case, I’ve created an order that varies the options available in the shipping-method drop down depending on the country. You’ll also see that I’ve wrapped the whole form in a part.

In your controller (replace Order with your model):

def edit
  self.this = Order.new(params[:order]) if params[:order]
  hobo_show do
    hobo_ajax_response if request.xhr?
  end
end

def new
  hobo_create(Order.new(params[:order])) do
    hobo_ajax_response if request.xhr?
  end
end

In your application.js (replace order-part with your part and order_country with your select’s class):

Event.addBehavior({
    "select.order_country:change": function(ev) {
         Hobo.ajaxRequest(window.location.href, ['order-part'], {
             params: Form.serialize(this.up('form')),
             method: 'get',
             message: 'Please wait...'
         });
     }
});

There you go: fancy custom AJAX, without really writing any AJAX code.

How it works:

Normally, when you hit submit on a new item, it POSTs to a URL like /orders. We’ve added a lowpro javascript watcher that triggers on the CSS selector “select.order_country:change” that submits the form as a ajax request to the current location (ie, /orders/new).

In the edit controller action, instead of looking up the current value in the database, we create it from the parameters passed in. We don’t save it, though! Then we invoke the standard hobo ajax mechanism that renders our part. Most standard hobo actions will do this automatically for you, but the standard hobo_show and hobo_\new do not, so we add it in ourselves.

User contributed notes

  • On August 04, 2009 Bryan Larsen said:

    I got too excited. Due to the way refreshes of part context's work, only the "new" action above works. To get the edit action to work, you'd have to save the part, which kind of makes this whole recipe pointless.
  • On August 04, 2009 Bryan Larsen said:

    OK, here's a working controller:

    def edit
    self.this = User.new(params[:order]) if params[:order]
    hobo_show do
    @order = this
    hobo_ajax_response if request.xhr?
    end
    end

    And I had to modify the view:

    <form with="&@order" method="put">

    This mostly makes sense, except the method="put" is an ugly hack.
  • On August 18, 2009 Bryan Larsen said:

    Note that the reason why the above edit works is because the context does not have an id. If the context does have an ID, you trigger Hobo's magic "set up the context for a part" code.
  • On June 18, 2010 cbenedict said:

    After much digging through the Hobo framework, I created a patch that makes the edit view work with the original code in this recipe. This patch was coded against Hobo 1.0.1. Place "require 'template_environment_patch'" in your environment.rb file. Then add the following into a template_environment_patch.rb file located in lib:

    module Hobo::Dryml
    TemplateEnvironment.class_eval do
    def typed_id_use_this_for_edit_action(object=nil, attribute=nil)
    if object.nil?
    # nothing passed -- use context
    if this_parent && this_field && !this_parent.respond_to?(:member_class)
    object, attribute = this_parent, this_field
    else
    object = this
    end
    end

    id = if (typed_id = object.try.typed_id)
    typed_id
    elsif object == @this
    "this"
    end

    # For edit actions, use the current context
    id = "this" if self.params[:action] == "edit"

    attribute ? "#{id}:#{attribute}" : id
    end
    alias_method :typed_id, :typed_id_use_this_for_edit_action
    end
    end