diff options
Diffstat (limited to 'actionview/lib/action_view/helpers/form_helper.rb')
-rw-r--r-- | actionview/lib/action_view/helpers/form_helper.rb | 582 |
1 files changed, 463 insertions, 119 deletions
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 7ced37572e..6185aa133f 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1,18 +1,20 @@ -require 'cgi' -require 'action_view/helpers/date_helper' -require 'action_view/helpers/tag_helper' -require 'action_view/helpers/form_tag_helper' -require 'action_view/helpers/active_model_helper' -require 'action_view/model_naming' -require 'action_view/record_identifier' -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/string/output_safety' -require 'active_support/core_ext/string/inflections' +# frozen_string_literal: true + +require "cgi" +require "action_view/helpers/date_helper" +require "action_view/helpers/tag_helper" +require "action_view/helpers/form_tag_helper" +require "action_view/helpers/active_model_helper" +require "action_view/model_naming" +require "action_view/record_identifier" +require "active_support/core_ext/module/attribute_accessors" +require "active_support/core_ext/hash/slice" +require "active_support/core_ext/string/output_safety" +require "active_support/core_ext/string/inflections" module ActionView # = Action View Form Helpers - module Helpers + module Helpers #:nodoc: # Form helpers are designed to make working with resources much easier # compared to using vanilla HTML. # @@ -201,9 +203,9 @@ module ActionView # <%= f.submit %> # <% end %> # - # This also works for the methods in FormOptionHelper and DateHelper that + # This also works for the methods in FormOptionsHelper and DateHelper that # are designed to work with an object as base, like - # FormOptionHelper#collection_select and DateHelper#datetime_select. + # FormOptionsHelper#collection_select and DateHelper#datetime_select. # # === #form_for with a model object # @@ -416,13 +418,13 @@ module ActionView # # To set an authenticity token you need to pass an <tt>:authenticity_token</tt> parameter # - # <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| + # <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %> # ... # <% end %> # # If you don't want to an authenticity token field be rendered at all just pass <tt>false</tt>: # - # <%= form_for @invoice, url: external_url, authenticity_token: false do |f| + # <%= form_for @invoice, url: external_url, authenticity_token: false do |f| %> # ... # <% end %> def form_for(record, options = {}, &block) @@ -467,13 +469,297 @@ module ActionView ) options[:url] ||= if options.key?(:format) - polymorphic_path(record, format: options.delete(:format)) - else - polymorphic_path(record, {}) - end + polymorphic_path(record, format: options.delete(:format)) + else + polymorphic_path(record, {}) + end end private :apply_form_for_options! + mattr_accessor :form_with_generates_remote_forms, default: true + + mattr_accessor :form_with_generates_ids, default: false + + # Creates a form tag based on mixing URLs, scopes, or models. + # + # # Using just a URL: + # <%= form_with url: posts_path do |form| %> + # <%= form.text_field :title %> + # <% end %> + # # => + # <form action="/posts" method="post" data-remote="true"> + # <input type="text" name="title"> + # </form> + # + # # Adding a scope prefixes the input field names: + # <%= form_with scope: :post, url: posts_path do |form| %> + # <%= form.text_field :title %> + # <% end %> + # # => + # <form action="/posts" method="post" data-remote="true"> + # <input type="text" name="post[title]"> + # </form> + # + # # Using a model infers both the URL and scope: + # <%= form_with model: Post.new do |form| %> + # <%= form.text_field :title %> + # <% end %> + # # => + # <form action="/posts" method="post" data-remote="true"> + # <input type="text" name="post[title]"> + # </form> + # + # # An existing model makes an update form and fills out field values: + # <%= form_with model: Post.first do |form| %> + # <%= form.text_field :title %> + # <% end %> + # # => + # <form action="/posts/1" method="post" data-remote="true"> + # <input type="hidden" name="_method" value="patch"> + # <input type="text" name="post[title]" value="<the title of the post>"> + # </form> + # + # # Though the fields don't have to correspond to model attributes: + # <%= form_with model: Cat.new do |form| %> + # <%= form.text_field :cats_dont_have_gills %> + # <%= form.text_field :but_in_forms_they_can %> + # <% end %> + # # => + # <form action="/cats" method="post" data-remote="true"> + # <input type="text" name="cat[cats_dont_have_gills]"> + # <input type="text" name="cat[but_in_forms_they_can]"> + # </form> + # + # The parameters in the forms are accessible in controllers according to + # their name nesting. So inputs named +title+ and <tt>post[title]</tt> are + # accessible as <tt>params[:title]</tt> and <tt>params[:post][:title]</tt> + # respectively. + # + # By default +form_with+ attaches the <tt>data-remote</tt> attribute + # submitting the form via an XMLHTTPRequest in the background if an + # Unobtrusive JavaScript driver, like rails-ujs, is used. See the + # <tt>:local</tt> option for more. + # + # For ease of comparison the examples above left out the submit button, + # as well as the auto generated hidden fields that enable UTF-8 support + # and adds an authenticity token needed for cross site request forgery + # protection. + # + # === Resource-oriented style + # + # In many of the examples just shown, the +:model+ passed to +form_with+ + # is a _resource_. It corresponds to a set of RESTful routes, most likely + # defined via +resources+ in <tt>config/routes.rb</tt>. + # + # So when passing such a model record, Rails infers the URL and method. + # + # <%= form_with model: @post do |form| %> + # ... + # <% end %> + # + # is then equivalent to something like: + # + # <%= form_with scope: :post, url: post_path(@post), method: :patch do |form| %> + # ... + # <% end %> + # + # And for a new record + # + # <%= form_with model: Post.new do |form| %> + # ... + # <% end %> + # + # is equivalent to something like: + # + # <%= form_with scope: :post, url: posts_path do |form| %> + # ... + # <% end %> + # + # ==== +form_with+ options + # + # * <tt>:url</tt> - The URL the form submits to. Akin to values passed to + # +url_for+ or +link_to+. For example, you may use a named route + # directly. When a <tt>:scope</tt> is passed without a <tt>:url</tt> the + # form just submits to the current URL. + # * <tt>:method</tt> - The method to use when submitting the form, usually + # either "get" or "post". If "patch", "put", "delete", or another verb + # is used, a hidden input named <tt>_method</tt> is added to + # simulate the verb over post. + # * <tt>:format</tt> - The format of the route the form submits to. + # Useful when submitting to another resource type, like <tt>:json</tt>. + # Skipped if a <tt>:url</tt> is passed. + # * <tt>:scope</tt> - The scope to prefix input field names with and + # thereby how the submitted parameters are grouped in controllers. + # * <tt>:model</tt> - A model object to infer the <tt>:url</tt> and + # <tt>:scope</tt> by, plus fill out input field values. + # So if a +title+ attribute is set to "Ahoy!" then a +title+ input + # field's value would be "Ahoy!". + # If the model is a new record a create form is generated, if an + # existing record, however, an update form is generated. + # Pass <tt>:scope</tt> or <tt>:url</tt> to override the defaults. + # E.g. turn <tt>params[:post]</tt> into <tt>params[:article]</tt>. + # * <tt>:authenticity_token</tt> - Authenticity token to use in the form. + # Override with a custom authenticity token or pass <tt>false</tt> to + # skip the authenticity token field altogether. + # Useful when submitting to an external resource like a payment gateway + # that might limit the valid fields. + # Remote forms may omit the embedded authenticity token by setting + # <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>. + # This is helpful when fragment-caching the form. Remote forms + # get the authenticity token from the <tt>meta</tt> tag, so embedding is + # unnecessary unless you support browsers without JavaScript. + # * <tt>:local</tt> - By default form submits are remote and unobstrusive XHRs. + # Disable remote submits with <tt>local: true</tt>. + # * <tt>:skip_enforcing_utf8</tt> - By default a hidden field named +utf8+ + # is output to enforce UTF-8 submits. Set to true to skip the field. + # * <tt>:builder</tt> - Override the object used to build the form. + # * <tt>:id</tt> - Optional HTML id attribute. + # * <tt>:class</tt> - Optional HTML class attribute. + # * <tt>:data</tt> - Optional HTML data attributes. + # * <tt>:html</tt> - Other optional HTML attributes for the form tag. + # + # === Examples + # + # When not passing a block, +form_with+ just generates an opening form tag. + # + # <%= form_with(model: @post, url: super_posts_path) %> + # <%= form_with(model: @post, scope: :article) %> + # <%= form_with(model: @post, format: :json) %> + # <%= form_with(model: @post, authenticity_token: false) %> # Disables the token. + # + # For namespaced routes, like +admin_post_url+: + # + # <%= form_with(model: [ :admin, @post ]) do |form| %> + # ... + # <% end %> + # + # If your resource has associations defined, for example, you want to add comments + # to the document given that the routes are set correctly: + # + # <%= form_with(model: [ @document, Comment.new ]) do |form| %> + # ... + # <% end %> + # + # Where <tt>@document = Document.find(params[:id])</tt>. + # + # === Mixing with other form helpers + # + # While +form_with+ uses a FormBuilder object it's possible to mix and + # match the stand-alone FormHelper methods and methods + # from FormTagHelper: + # + # <%= form_with scope: :person do |form| %> + # <%= form.text_field :first_name %> + # <%= form.text_field :last_name %> + # + # <%= text_area :person, :biography %> + # <%= check_box_tag "person[admin]", "1", @person.company.admin? %> + # + # <%= form.submit %> + # <% end %> + # + # Same goes for the methods in FormOptionsHelper and DateHelper designed + # to work with an object as a base, like + # FormOptionsHelper#collection_select and DateHelper#datetime_select. + # + # === Setting the method + # + # You can force the form to use the full array of HTTP verbs by setting + # + # method: (:get|:post|:patch|:put|:delete) + # + # in the options hash. If the verb is not GET or POST, which are natively + # supported by HTML forms, the form will be set to POST and a hidden input + # called _method will carry the intended verb for the server to interpret. + # + # === Setting HTML options + # + # You can set data attributes directly in a data hash, but HTML options + # besides id and class must be wrapped in an HTML key: + # + # <%= form_with(model: @post, data: { behavior: "autosave" }, html: { name: "go" }) do |form| %> + # ... + # <% end %> + # + # generates + # + # <form action="/posts/123" method="post" data-behavior="autosave" name="go"> + # <input name="_method" type="hidden" value="patch" /> + # ... + # </form> + # + # === Removing hidden model id's + # + # The +form_with+ method automatically includes the model id as a hidden field in the form. + # This is used to maintain the correlation between the form data and its associated model. + # Some ORM systems do not use IDs on nested models so in this case you want to be able + # to disable the hidden id. + # + # In the following example the Post model has many Comments stored within it in a NoSQL database, + # thus there is no primary key for comments. + # + # <%= form_with(model: @post) do |form| %> + # <%= form.fields(:comments, skip_id: true) do |fields| %> + # ... + # <% end %> + # <% end %> + # + # === Customized form builders + # + # You can also build forms using a customized FormBuilder class. Subclass + # FormBuilder and override or define some more helpers, then use your + # custom builder. For example, let's say you made a helper to + # automatically add labels to form inputs. + # + # <%= form_with model: @person, url: { action: "create" }, builder: LabellingFormBuilder do |form| %> + # <%= form.text_field :first_name %> + # <%= form.text_field :last_name %> + # <%= form.text_area :biography %> + # <%= form.check_box :admin %> + # <%= form.submit %> + # <% end %> + # + # In this case, if you use: + # + # <%= render form %> + # + # The rendered template is <tt>people/_labelling_form</tt> and the local + # variable referencing the form builder is called + # <tt>labelling_form</tt>. + # + # The custom FormBuilder class is automatically merged with the options + # of a nested +fields+ call, unless it's explicitly set. + # + # In many cases you will want to wrap the above in another helper, so you + # could do something like the following: + # + # def labelled_form_with(**options, &block) + # form_with(**options.merge(builder: LabellingFormBuilder), &block) + # end + def form_with(model: nil, scope: nil, url: nil, format: nil, **options) + options[:allow_method_names_outside_object] = true + options[:skip_default_ids] = !form_with_generates_ids + + if model + url ||= polymorphic_path(model, format: format) + + model = model.last if model.is_a?(Array) + scope ||= model_name_from_record_or_class(model).param_key + end + + if block_given? + builder = instantiate_builder(scope, model, options) + output = capture(builder, &Proc.new) + options[:multipart] ||= builder.multipart? + + html_options = html_options_for_form_with(url, model, options) + form_tag_with_body(html_options, output) + else + html_options = html_options_for_form_with(url, model, options) + form_tag_html(html_options) + end + end + # Creates a scope around a specific model object like form_for, but # doesn't create the form tags themselves. This makes fields_for suitable # for specifying additional model objects in the same form. @@ -531,9 +817,9 @@ module ActionView # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is # of class +Permission+, the field will still be named <tt>permission[admin]</tt>. # - # Note: This also works for the methods in FormOptionHelper and + # Note: This also works for the methods in FormOptionsHelper and # DateHelper that are designed to work with an object as base, like - # FormOptionHelper#collection_select and DateHelper#datetime_select. + # FormOptionsHelper#collection_select and DateHelper#datetime_select. # # === Nested Attributes Examples # @@ -720,6 +1006,64 @@ module ActionView capture(builder, &block) end + # Scopes input fields with either an explicit scope or model. + # Like +form_with+ does with <tt>:scope</tt> or <tt>:model</tt>, + # except it doesn't output the form tags. + # + # # Using a scope prefixes the input field names: + # <%= fields :comment do |fields| %> + # <%= fields.text_field :body %> + # <% end %> + # # => <input type="text" name="comment[body]> + # + # # Using a model infers the scope and assigns field values: + # <%= fields model: Comment.new(body: "full bodied") do |fields| %< + # <%= fields.text_field :body %> + # <% end %> + # # => + # <input type="text" name="comment[body] value="full bodied"> + # + # # Using +fields+ with +form_with+: + # <%= form_with model: @post do |form| %> + # <%= form.text_field :title %> + # + # <%= form.fields :comment do |fields| %> + # <%= fields.text_field :body %> + # <% end %> + # <% end %> + # + # Much like +form_with+ a FormBuilder instance associated with the scope + # or model is yielded, so any generated field names are prefixed with + # either the passed scope or the scope inferred from the <tt>:model</tt>. + # + # === Mixing with other form helpers + # + # While +form_with+ uses a FormBuilder object it's possible to mix and + # match the stand-alone FormHelper methods and methods + # from FormTagHelper: + # + # <%= fields model: @comment do |fields| %> + # <%= fields.text_field :body %> + # + # <%= text_area :commenter, :biography %> + # <%= check_box_tag "comment[all_caps]", "1", @comment.commenter.hulk_mode? %> + # <% end %> + # + # Same goes for the methods in FormOptionsHelper and DateHelper designed + # to work with an object as a base, like + # FormOptionsHelper#collection_select and DateHelper#datetime_select. + def fields(scope = nil, model: nil, **options, &block) + options[:allow_method_names_outside_object] = true + options[:skip_default_ids] = !form_with_generates_ids + + if model + scope ||= model_name_from_record_or_class(model).param_key + end + + builder = instantiate_builder(scope, model, options) + capture(builder, &block) + end + # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly. @@ -860,26 +1204,8 @@ module ActionView # # file_field(:attachment, :file, class: 'file_input') # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" /> - # - # ==== Gotcha - # - # The HTML specification says that when a file field is empty, web browsers - # do not send any value to the server. Unfortunately this introduces a - # gotcha: if a +User+ model has an +avatar+ field, and no file is selected, - # then the +avatar+ parameter is empty. Thus, any mass-assignment idiom like - # - # @user.update(params[:user]) - # - # wouldn't update the +avatar+ field. - # - # To prevent this, the helper generates an auxiliary hidden field before - # every file field. The hidden field has the same name as the file one and - # a blank value. - # - # In case you don't want the helper to generate this hidden field you can - # specify the <tt>include_hidden: false</tt> option. def file_field(object_name, method, options = {}) - Tags::FileField.new(object_name, method, self, options).render + Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(options.dup)).render end # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+) @@ -983,6 +1309,7 @@ module ActionView # # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" /> # # <input type="radio" id="post_category_java" name="post[category]" value="java" /> # + # # Let's say that @user.receive_newsletter returns "no": # radio_button("user", "receive_newsletter", "yes") # radio_button("user", "receive_newsletter", "no") # # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" /> @@ -1066,7 +1393,7 @@ module ActionView # Returns a text_field of type "time". # # The default value is generated by trying to call +strftime+ with "%T.%L" - # on the objects's value. It is still possible to override that + # on the object's value. It is still possible to override that # by passing the "value" option. # # === Options @@ -1092,42 +1419,9 @@ module ActionView Tags::TimeField.new(object_name, method, self, options).render end - # Returns a text_field of type "datetime". - # - # datetime_field("user", "born_on") - # # => <input id="user_born_on" name="user[born_on]" type="datetime" /> - # - # The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T.%L%z" - # on the object's value, which makes it behave as expected for instances - # of DateTime and ActiveSupport::TimeWithZone. - # - # @user.born_on = Date.new(1984, 1, 12) - # datetime_field("user", "born_on") - # # => <input id="user_born_on" name="user[born_on]" type="datetime" value="1984-01-12T00:00:00.000+0000" /> - # - # You can create values for the "min" and "max" attributes by passing - # instances of Date or Time to the options hash. - # - # datetime_field("user", "born_on", min: Date.today) - # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" /> - # - # Alternatively, you can pass a String formatted as an ISO8601 datetime - # with UTC offset as the values for "min" and "max." - # - # datetime_field("user", "born_on", min: "2014-05-20T00:00:00+0000") - # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" /> - # - def datetime_field(object_name, method, options = {}) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - datetime_field is deprecated and will be removed in Rails 5.1. - Use datetime_local_field instead. - MESSAGE - Tags::DatetimeField.new(object_name, method, self, options).render - end - # Returns a text_field of type "datetime-local". # - # datetime_local_field("user", "born_on") + # datetime_field("user", "born_on") # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" /> # # The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T" @@ -1135,25 +1429,27 @@ module ActionView # of DateTime and ActiveSupport::TimeWithZone. # # @user.born_on = Date.new(1984, 1, 12) - # datetime_local_field("user", "born_on") + # datetime_field("user", "born_on") # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" value="1984-01-12T00:00:00" /> # # You can create values for the "min" and "max" attributes by passing # instances of Date or Time to the options hash. # - # datetime_local_field("user", "born_on", min: Date.today) + # datetime_field("user", "born_on", min: Date.today) # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" /> # # Alternatively, you can pass a String formatted as an ISO8601 datetime as # the values for "min" and "max." # - # datetime_local_field("user", "born_on", min: "2014-05-20T00:00:00") + # datetime_field("user", "born_on", min: "2014-05-20T00:00:00") # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" /> # - def datetime_local_field(object_name, method, options = {}) + def datetime_field(object_name, method, options = {}) Tags::DatetimeLocalField.new(object_name, method, self, options).render end + alias datetime_local_field datetime_field + # Returns a text_field of type "month". # # month_field("user", "born_on") @@ -1223,6 +1519,34 @@ module ActionView end private + def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms, + skip_enforcing_utf8: false, **options) + html_options = options.slice(:id, :class, :multipart, :method, :data).merge(html) + html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted? + html_options[:enforce_utf8] = !skip_enforcing_utf8 + + html_options[:enctype] = "multipart/form-data" if html_options.delete(:multipart) + + # The following URL is unescaped, this is just a hash of options, and it is the + # responsibility of the caller to escape all the values. + html_options[:action] = url_for(url_for_options || {}) + html_options[:"accept-charset"] = "UTF-8" + html_options[:"data-remote"] = true unless local + + html_options[:authenticity_token] = options.delete(:authenticity_token) + + if !local && html_options[:authenticity_token].blank? + html_options[:authenticity_token] = embed_authenticity_token_in_remote_forms + end + + if html_options[:authenticity_token] == true + # Include the default authenticity_token, which is only generated when it's set to nil, + # but we needed the true value to override the default of no authenticity_token on data-remote. + html_options[:authenticity_token] = nil + end + + html_options.stringify_keys! + end def instantiate_builder(record_name, record_object, options) case record_name @@ -1231,7 +1555,7 @@ module ActionView object_name = record_name else object = record_name - object_name = model_name_from_record_or_class(object).param_key + object_name = model_name_from_record_or_class(object).param_key if object end builder = options[:builder] || default_form_builder_class @@ -1257,7 +1581,7 @@ module ActionView # In the above block, a +FormBuilder+ object is yielded as the # +person_form+ variable. This allows you to generate the +text_field+ # and +check_box+ fields by specifying their eponymous methods, which - # modify the underlying template and associates the +@person+ model object + # modify the underlying template and associates the <tt>@person</tt> model object # with the form. # # The +FormBuilder+ object can be thought of as serving as a proxy for the @@ -1296,14 +1620,15 @@ module ActionView include ModelNaming # The methods which wrap a form helper call. - class_attribute :field_helpers - self.field_helpers = [:fields_for, :label, :text_field, :password_field, - :hidden_field, :file_field, :text_area, :check_box, - :radio_button, :color_field, :search_field, - :telephone_field, :phone_field, :date_field, - :time_field, :datetime_field, :datetime_local_field, - :month_field, :week_field, :url_field, :email_field, - :number_field, :range_field] + class_attribute :field_helpers, default: [ + :fields_for, :fields, :label, :text_field, :password_field, + :hidden_field, :file_field, :text_area, :check_box, + :radio_button, :color_field, :search_field, + :telephone_field, :phone_field, :date_field, + :time_field, :datetime_field, :datetime_local_field, + :month_field, :week_field, :url_field, :email_field, + :number_field, :range_field + ] attr_accessor :object_name, :object, :options @@ -1319,7 +1644,7 @@ module ActionView end def self._to_partial_path - @_to_partial_path ||= name.demodulize.underscore.sub!(/_builder$/, '') + @_to_partial_path ||= name.demodulize.underscore.sub!(/_builder$/, "") end def to_partial_path @@ -1333,19 +1658,23 @@ module ActionView def initialize(object_name, object, template, options) @nested_child_index = {} @object_name, @object, @template, @options = object_name, object, template, options - @default_options = @options ? @options.slice(:index, :namespace) : {} + @default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids, :allow_method_names_outside_object) : {} + + convert_to_legacy_options(@options) + if @object_name.to_s.match(/\[\]$/) - if object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param) + if (object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param) @auto_index = object.to_param else raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" end end + @multipart = nil @index = options[:index] || options[:child_index] end - (field_helpers - [:label, :check_box, :radio_button, :fields_for, :hidden_field, :file_field]).each do |selector| + (field_helpers - [:label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field]).each do |selector| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{selector}(method, options = {}) # def text_field(method, options = {}) @template.send( # @template.send( @@ -1414,9 +1743,9 @@ module ActionView # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is # of class +Permission+, the field will still be named <tt>permission[admin]</tt>. # - # Note: This also works for the methods in FormOptionHelper and + # Note: This also works for the methods in FormOptionsHelper and # DateHelper that are designed to work with an object as base, like - # FormOptionHelper#collection_select and DateHelper#datetime_select. + # FormOptionsHelper#collection_select and DateHelper#datetime_select. # # === Nested Attributes Examples # @@ -1614,26 +1943,37 @@ module ActionView record_name = model_name_from_record_or_class(record_object).param_key end + object_name = @object_name index = if options.has_key?(:index) options[:index] elsif defined?(@auto_index) - self.object_name = @object_name.to_s.sub(/\[\]$/,"") + object_name = object_name.to_s.sub(/\[\]$/, "") @auto_index end record_name = if index - "#{object_name}[#{index}][#{record_name}]" - elsif record_name.to_s.end_with?('[]') - record_name = record_name.to_s.sub(/(.*)\[\]$/, "[\\1][#{record_object.id}]") - "#{object_name}#{record_name}" - else - "#{object_name}[#{record_name}]" - end + "#{object_name}[#{index}][#{record_name}]" + elsif record_name.to_s.end_with?("[]") + record_name = record_name.to_s.sub(/(.*)\[\]$/, "[\\1][#{record_object.id}]") + "#{object_name}#{record_name}" + else + "#{object_name}[#{record_name}]" + end fields_options[:child_index] = index @template.fields_for(record_name, record_object, fields_options, &block) end + # See the docs for the <tt>ActionView::FormHelper.fields</tt> helper method. + def fields(scope = nil, model: nil, **options, &block) + options[:allow_method_names_outside_object] = true + options[:skip_default_ids] = !FormHelper.form_with_generates_ids + + convert_to_legacy_options(options) + + fields_for(scope || model, model, **options, &block) + end + # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly. @@ -1760,7 +2100,7 @@ module ActionView # # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" /> # # <input type="radio" id="post_category_java" name="post[category]" value="java" /> # - # # Let's say that @user.category returns "no": + # # Let's say that @user.receive_newsletter returns "no": # radio_button("receive_newsletter", "yes") # radio_button("receive_newsletter", "no") # # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" /> @@ -1837,11 +2177,11 @@ module ActionView # <%= f.submit %> # <% end %> # - # In the example above, if @post is a new record, it will use "Create Post" as - # submit button label, otherwise, it uses "Update Post". + # In the example above, if <tt>@post</tt> is a new record, it will use "Create Post" as + # submit button label; otherwise, it uses "Update Post". # - # Those labels can be customized using I18n, under the helpers.submit key and accept - # the %{model} as translation interpolation: + # Those labels can be customized using I18n under the +helpers.submit+ key and using + # <tt>%{model}</tt> for translation interpolation: # # en: # helpers: @@ -1849,7 +2189,7 @@ module ActionView # create: "Create a %{model}" # update: "Confirm changes to %{model}" # - # It also searches for a key specific for the given object: + # It also searches for a key specific to the given object: # # en: # helpers: @@ -1857,7 +2197,7 @@ module ActionView # post: # create: "Add %{model}" # - def submit(value=nil, options={}) + def submit(value = nil, options = {}) value, options = nil, value if value.is_a?(Hash) value ||= submit_default_value @template.submit_tag(value, options) @@ -1870,11 +2210,11 @@ module ActionView # <%= f.button %> # <% end %> # - # In the example above, if @post is a new record, it will use "Create Post" as - # button label, otherwise, it uses "Update Post". + # In the example above, if <tt>@post</tt> is a new record, it will use "Create Post" as + # button label; otherwise, it uses "Update Post". # - # Those labels can be customized using I18n, under the helpers.submit key - # (the same as submit helper) and accept the %{model} as translation interpolation: + # Those labels can be customized using I18n under the +helpers.submit+ key + # (the same as submit helper) and using <tt>%{model}</tt> for translation interpolation: # # en: # helpers: @@ -1882,7 +2222,7 @@ module ActionView # create: "Create a %{model}" # update: "Confirm changes to %{model}" # - # It also searches for a key specific for the given object: + # It also searches for a key specific to the given object: # # en: # helpers: @@ -1982,12 +2322,16 @@ module ActionView @nested_child_index[name] ||= -1 @nested_child_index[name] += 1 end + + def convert_to_legacy_options(options) + if options.key?(:skip_id) + options[:include_id] = !options.delete(:skip_id) + end + end end end ActiveSupport.on_load(:action_view) do - cattr_accessor(:default_form_builder, instance_writer: false, instance_reader: false) do - ::ActionView::Helpers::FormBuilder - end + cattr_accessor :default_form_builder, instance_writer: false, instance_reader: false, default: ::ActionView::Helpers::FormBuilder end end |