Adding a value-dependent data attribute to a simple_form checkbox collection

yoz picture yoz · Feb 8, 2013 · Viewed 7.4k times · Source

I'm generating a list of checkboxes for a single collection like so:

= f.input :parts, as:check_boxes, collection: @parts_list

I want some checkboxes in the collection to disappear/reappear depending on the value of a select widget higher up in the form. (e.g. choosing "Tracker Robot" from the Robot select means that the "Legs" part checkbox disappears and the "Wheels" checkbox appears, etc.)

What I'd like to do is attach a computed data attribute to each individual Part checkbox, with the attribute value listing the Robots that can use that Part; then some JS will do the work of hiding/showing the checkboxes. However, I don't know how I can generate those data attributes using simple_form.

I would normally create a custom "parts" input, but there seems to be a problem with making custom collection inputs; it looks for a named method (collection_parts) inside form_builder.rb, which won't exist, and if I try and extend the FormBuilder it sends me down a major rabbit hole.

I could write some JS to load the data attrs into the generated HTML, but then I have to generate custom JS based on my Rails data, and that feels like the wrong way to do it.

Answer

Harish Shetty picture Harish Shetty · Feb 20, 2013

Let's assume that the form is for Order model and you are changing the parts collection based on the value of a field called region.

Update the form view. Specify the id for form, region field and parts field.

= simple_form_for(@order, :html => { :id => "order-form"}) do |f|
  = f.input :region, :wrapper_html => { :id => "order-form-region",                      |
      "data-parts-url" => parts_orders_path(:id => @order.id, :region => @order.region)} |

  = f.input :parts, as: check_boxes, collection: @parts_list,                            |
      :wrapper_html => { id' => 'parts-check-box-list'}                                  |

Add a new action called parts in the route.rb file.

resources :orders do
  collection do
    get :parts
  end
end

Add the new action to your controller

class OrdersController < ApplicationController

  # expects id and region as parameters
  def parts
    @order =  params[:id].present? ? Order.find(params[:id]) : Order.new
    @parts_list = Part.where(:region => params[:region])
  end
end

Add a helper

def parts_collection(order, parts_list)
  "".tap do |pc| 
    # to generate the markup for collection we need a dummy form
    simple_form_for(order) do |f| 
      pc << f.input(:parts, as: check_boxes, collection: parts_list, 
        :wrapper_html => {:id => 'parts-check-box-list'})
    end
  end
end

Add a js view for the action (orders/parts.js.erb)

$('#parts-check-box-list').replaceWith('<%= j(parts_collection(@order, @parts_list)) %>');

Register data change event handlers for region field in your application.js

$(document).ready(function() {
  $('#order-form').on("change", "#order-form-region", function () {
    // Access the data-parts-url set in the region field to submit JS request
    $.getScript($(this).attr('data-parts-url'));
  });
});