Skip to content
This repository
file 201 lines (182 sloc) 9.009 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
module Formtastic
  module Inputs

    # A CheckBoxes input is used to render a series of checkboxes. This is an alternative input choice
    # for `has_many` or `has_and_belongs_to_many` associations like a `Post` belonging to many
    # `categories` (by default, a {SelectInput `:select`} input is used, allowing multiple selections).
    #
    # Within the standard `<li>` wrapper, the output is a `<fieldset>` with a `<legend>` to
    # represent the "label" for the input, and an `<ol>` containing `<li>`s for each choice in
    # the association. Each `<li>` choice contains a hidden `<input>` tag for the "unchecked"
    # value (like Rails), and a `<label>` containing the checkbox `<input>` and the label text
    # for each choice.
    #
    # @example Basic example with full form context
    #
    # <%= semantic_form_for @post do |f| %>
    # <%= f.inputs do %>
    # <%= f.input :categories, :as => :check_boxes %>
    # <% end %>
    # <% end %>
    #
    # <li class='check_boxes'>
    # <fieldset>
    # <legend class="label"><label>Categories</label></legend>
    # <ol>
    # <li>
    # <input type="hidden" name="post[category_ids][1]" value="">
    # <label for="post_category_ids_1"><input id="post_category_ids_1" name="post[category_ids][1]" type="checkbox" value="1" /> Ruby</label>
    # </li>
    # <li>
    # <input type="hidden" name="post[category_ids][2]" value="">
    # <label for="post_category_ids_2"><input id="post_category_ids_2" name="post[category_ids][2]" type="checkbox" value="2" /> Rails</label>
    # </li>
    # </ol>
    # </fieldset>
    # </li>
    #
    # @example `:collection` can be used to customize the choices
    # <%= f.input :categories, :as => :check_boxes, :collection => @categories %>
    # <%= f.input :categories, :as => :check_boxes, :collection => Category.all %>
    # <%= f.input :categories, :as => :check_boxes, :collection => Category.some_named_scope %>
    # <%= f.input :categories, :as => :check_boxes, :collection => [Category.find_by_name("Ruby"), Category.find_by_name("Rails")] %>
    # <%= f.input :categories, :as => :check_boxes, :collection => ["Ruby", "Rails"] %>
    # <%= f.input :categories, :as => :check_boxes, :collection => [["Ruby", "ruby"], ["Rails", "rails"]] %>
    # <%= f.input :categories, :as => :check_boxes, :collection => [["Ruby", "1"], ["Rails", "2"]] %>
    # <%= f.input :categories, :as => :check_boxes, :collection => [["Ruby", 1], ["Rails", 2]] %>
    # <%= f.input :categories, :as => :check_boxes, :collection => [["Ruby", 1, {'data-attr' => 'attr-value'}]] %>
    # <%= f.input :categories, :as => :check_boxes, :collection => 1..5 %>
    # <%= f.input :categories, :as => :check_boxes, :collection => [:ruby, :rails] %>
    # <%= f.input :categories, :as => :check_boxes, :collection => [["Ruby", :ruby], ["Rails", :rails]] %>
    # <%= f.input :categories, :as => :check_boxes, :collection => Set.new([:ruby, :rails]) %>
    #
    # @example `:hidden_fields` can be used to skip Rails' rendering of a hidden field before every checkbox
    # <%= f.input :categories, :as => :check_boxes, :hidden_fields => false %>
    #
    # @example `:disabled` can be used to disable any checkboxes with a value found in the given Array
    # <%= f.input :categories, :as => :check_boxes, :collection => ["a", "b"], :disabled => ["a"] %>
    #
    # @example `:member_label` can be used to call a different method (or a Proc) on each object in the collection for rendering the label text (it'll try the methods like `to_s` in `collection_label_methods` config by default)
    # <%= f.input :categories, :as => :check_boxes, :member_label => :name %>
    # <%= f.input :categories, :as => :check_boxes, :member_label => :name_with_post_count
    # <%= f.input :categories, :as => :check_boxes, :member_label => { |c| "#{c.name} (#{pluralize("post", c.posts.count)})" }
    #
    # @example `:member_label` can be used with a helper method (both examples have the same result)
    # <%= f.input :categories, :as => :check_boxes, :member_label => method(:fancy_label)
    # <%= f.input :categories, :as => :check_boxes, :member_label => Proc.new { |category| fancy_label(category) }
    #
    # @example `:member_value` can be used to call a different method (or a Proc) on each object in the collection for rendering the value for each checkbox (it'll try the methods like `id` in `collection_value_methods` config by default)
    # <%= f.input :categories, :as => :check_boxes, :member_value => :code %>
    # <%= f.input :categories, :as => :check_boxes, :member_value => :isbn
    # <%= f.input :categories, :as => :check_boxes, :member_value => Proc.new { |c| c.name.downcase.underscore }
    #
    # @example `:member_value` can be used with a helper method (both examples have the same result)
    # <%= f.input :categories, :as => :check_boxes, :member_value => method(:some_helper)
    # <%= f.input :categories, :as => :check_boxes, :member_value => Proc.new { |category| some_helper(category) }
    #
    # @example `:value_as_class` can be used to add a class to the `<li>` wrapped around each choice using the checkbox value for custom styling of each choice
    # <%= f.input :categories, :as => :check_boxes, :value_as_class => true %>
    #
    # @see Formtastic::Helpers::InputsHelper#input InputsHelper#input for full documentation of all possible options.
    # @see Formtastic::Inputs::BooleanInput BooleanInput for a single checkbox for boolean (checked = true) inputs
    #
    # @todo Do/can we support the per-item HTML options like RadioInput?
    class CheckBoxesInput
      include Base
      include Base::Collections
      include Base::Choices

      def to_html
        input_wrapping do
          choices_wrapping do
            legend_html <<
            hidden_field_for_all <<
            choices_group_wrapping do
              collection.map { |choice|
                choice_wrapping(choice_wrapping_html_options(choice)) do
                  choice_html(choice)
                end
              }.join("\n").html_safe
            end
          end
        end
      end

      def choice_html(choice)
        template.content_tag(:label,
          hidden_fields? ?
            check_box_with_hidden_input(choice) :
            check_box_without_hidden_input(choice) <<
          choice_label(choice),
          label_html_options.merge(:for => choice_input_dom_id(choice), :class => nil)
        )
      end

      def hidden_field_for_all
        if hidden_fields?
          ""
        else
          options = {}
          options[:class] = [method.to_s.singularize, 'default'].join('_') if value_as_class?
          options[:id] = [object_name, method, 'none'].join('_')
          template.hidden_field_tag(input_name, '', options)
        end
      end

      def hidden_fields?
        options[:hidden_fields]
      end

      def check_box_with_hidden_input(choice)
        value = choice_value(choice)
        builder.check_box(
          association_primary_key || method,
          extra_html_options(choice).merge(:id => choice_input_dom_id(choice), :name => input_name, :disabled => disabled?(value), :required => false),
          value,
          unchecked_value
        )
      end

      def check_box_without_hidden_input(choice)
        value = choice_value(choice)
        template.check_box_tag(
          input_name,
          value,
          checked?(value),
          extra_html_options(choice).merge(:id => choice_input_dom_id(choice), :disabled => disabled?(value), :required => false)
        )
      end

      def extra_html_options(choice)
        input_html_options.merge(custom_choice_html_options(choice))
      end

      def checked?(value)
        selected_values.include?(value)
      end

      def disabled?(value)
        disabled_values.include?(value)
      end

      def selected_values
        @selected_values ||= make_selected_values
      end

      def disabled_values
        vals = options[:disabled] || []
        vals = [vals] unless vals.is_a?(Array)
        vals
      end

      def unchecked_value
        options[:unchecked_value] || ''
      end

      def input_name
        if builder.options.key?(:index)
          "#{object_name}[#{builder.options[:index]}][#{association_primary_key || method}][]"
        else
          "#{object_name}[#{association_primary_key || method}][]"
        end
      end

      protected

      def make_selected_values
        if object.respond_to?(method)
          selected_items = object.send(method)

          # Construct an array from the return value, regardless of the return type
          selected_items = [*selected_items].compact.flatten

          [*selected_items.map { |o| send_or_call_or_object(value_method, o) }].compact
        else
          []
        end
      end
    end
  end
end
Something went wrong with that request. Please try again.