diff options
Diffstat (limited to 'actionview/lib/action_view/helpers')
20 files changed, 541 insertions, 97 deletions
diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index b1563ac490..72a094c629 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -1,6 +1,5 @@ require "active_support/core_ext/array/extract_options" require "active_support/core_ext/hash/keys" -require "active_support/core_ext/regexp" require "action_view/helpers/asset_url_helper" require "action_view/helpers/tag_helper" @@ -36,18 +35,37 @@ module ActionView # When the Asset Pipeline is enabled, you can pass the name of your manifest as # source, and include other JavaScript or CoffeeScript files inside the manifest. # + # ==== Options + # + # When the last parameter is a hash you can add HTML attributes using that + # parameter. The following options are supported: + # + # * <tt>:extname</tt> - Append a extention to the generated url unless the extension + # already exists. This only applies for relative urls. + # * <tt>:protocol</tt> - Sets the protocol of the generated url, this option only + # applies when a relative url and +host+ options are provided. + # * <tt>:host</tt> - When a relative url is provided the host is added to the + # that path. + # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline + # when it is set to true. + # + # ==== Examples + # # javascript_include_tag "xmlhr" - # # => <script src="/assets/xmlhr.js?1284139606"></script> + # # => <script src="/assets/xmlhr.debug-1284139606.js"></script> + # + # javascript_include_tag "xmlhr", host: "localhost", protocol: "https" + # # => <script src="https://localhost/assets/xmlhr.debug-1284139606.js"></script> # # javascript_include_tag "template.jst", extname: false - # # => <script src="/assets/template.jst?1284139606"></script> + # # => <script src="/assets/template.debug-1284139606.jst"></script> # # javascript_include_tag "xmlhr.js" - # # => <script src="/assets/xmlhr.js?1284139606"></script> + # # => <script src="/assets/xmlhr.debug-1284139606.js"></script> # # javascript_include_tag "common.javascript", "/elsewhere/cools" - # # => <script src="/assets/common.javascript?1284139606"></script> - # # <script src="/elsewhere/cools.js?1423139606"></script> + # # => <script src="/assets/common.javascript.debug-1284139606.js"></script> + # # <script src="/elsewhere/cools.debug-1284139606.js"></script> # # javascript_include_tag "http://www.example.com/xmlhr" # # => <script src="http://www.example.com/xmlhr"></script> @@ -169,7 +187,7 @@ module ActionView # # favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png' # # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" /> - def favicon_link_tag(source="favicon.ico", options={}) + def favicon_link_tag(source = "favicon.ico", options = {}) tag("link", { rel: "shortcut icon", type: "image/x-icon", @@ -207,7 +225,7 @@ module ActionView # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" /> # image_tag("/icons/icon.gif", data: { title: 'Rails Application' }) # # => <img data-title="Rails Application" src="/icons/icon.gif" /> - def image_tag(source, options={}) + def image_tag(source, options = {}) options = options.symbolize_keys check_for_image_tag_errors(options) diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index 0967245855..c6a5e04aba 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -1,5 +1,4 @@ require "zlib" -require "active_support/core_ext/regexp" module ActionView # = Action View Asset URL Helpers @@ -37,7 +36,7 @@ module ActionView # some asset downloads to wait for previous assets to finish before they can # begin. You can use the <tt>%d</tt> wildcard in the +asset_host+ to # distribute the requests over four hosts. For example, - # <tt>assets%d.example.com<tt> will spread the asset requests over + # <tt>assets%d.example.com</tt> will spread the asset requests over # "assets0.example.com", ..., "assets3.example.com". # # image_tag("rails.png") @@ -97,8 +96,8 @@ module ActionView # have SSL certificates for each of the asset hosts this technique allows you # to avoid warnings in the client about mixed media. # Note that the request parameter might not be supplied, e.g. when the assets - # are precompiled via a Rake task. Make sure to use a Proc instead of a lambda, - # since a Proc allows missing parameters and sets them to nil. + # are precompiled via a Rake task. Make sure to use a +Proc+ instead of a lambda, + # since a +Proc+ allows missing parameters and sets them to +nil+. # # config.action_controller.asset_host = Proc.new { |source, request| # if request && request.ssl? @@ -233,7 +232,7 @@ module ActionView stylesheet: ".css" } - # Compute extname to append to asset path. Returns nil if + # Compute extname to append to asset path. Returns +nil+ if # nothing should be added. def compute_asset_extname(source, options = {}) return if options[:extname] == false diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb index 09d243c46d..3538515aee 100644 --- a/actionview/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb @@ -103,7 +103,7 @@ module ActionView xml = options.delete(:xml) || eval("xml", block.binding) xml.instruct! if options[:instruct] - options[:instruct].each do |target,attrs| + options[:instruct].each do |target, attrs| if attrs.respond_to?(:keys) xml.instruct!(target, attrs) elsif attrs.respond_to?(:each) @@ -113,7 +113,7 @@ module ActionView end feed_opts = { "xml:lang" => options[:language] || "en-US", "xmlns" => "http://www.w3.org/2005/Atom" } - feed_opts.merge!(options).reject! { |k,v| !k.to_s.match(/^xml/) } + feed_opts.merge!(options).reject! { |k, v| !k.to_s.match(/^xml/) } xml.feed(feed_opts) do xml.id(options[:id] || "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}") @@ -163,7 +163,7 @@ module ActionView @xml, @view, @feed_options = xml, view, feed_options end - # Accepts a Date or Time object and inserts it in the proper format. If nil is passed, current time in UTC is used. + # Accepts a Date or Time object and inserts it in the proper format. If +nil+ is passed, current time in UTC is used. def updated(date_or_time = nil) @xml.updated((date_or_time || Time.now.utc).xmlschema) end @@ -174,7 +174,7 @@ module ActionView # # * <tt>:published</tt>: Time first published. Defaults to the created_at attribute on the record if one such exists. # * <tt>:updated</tt>: Time of update. Defaults to the updated_at attribute on the record if one such exists. - # * <tt>:url</tt>: The URL for this entry or false or nil for not having a link tag. Defaults to the polymorphic_url for the record. + # * <tt>:url</tt>: The URL for this entry or +false+ or +nil+ for not having a link tag. Defaults to the +polymorphic_url+ for the record. # * <tt>:id</tt>: The ID for this entry. Defaults to "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}" # * <tt>:type</tt>: The TYPE for this entry. Defaults to "text/html". def entry(record, options = {}) diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 5258a01144..7fdf0fd0e1 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -88,7 +88,7 @@ module ActionView # # === Explicit dependencies # - # Some times you'll have template dependencies that can't be derived at all. This is typically + # Sometimes you'll have template dependencies that can't be derived at all. This is typically # the case when you have template rendering that happens in helpers. Here's an example: # # <%= render_sortable_todolists @project.todolists %> @@ -215,10 +215,10 @@ module ActionView private - def fragment_name_with_digest(name, virtual_path) #:nodoc: + def fragment_name_with_digest(name, virtual_path) virtual_path ||= @virtual_path if virtual_path - name = controller.url_for(name).split("://").last if name.is_a?(Hash) + name = controller.url_for(name).split("://").last if name.is_a?(Hash) digest = Digestor.digest name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies [ name, digest ] else @@ -226,7 +226,7 @@ module ActionView end end - def fragment_for(name = {}, options = nil, &block) #:nodoc: + def fragment_for(name = {}, options = nil, &block) if content = read_fragment_for(name, options) @cache_hit = true content @@ -236,11 +236,11 @@ module ActionView end end - def read_fragment_for(name, options) #:nodoc: + def read_fragment_for(name, options) controller.read_fragment(name, options) end - def write_fragment_for(name, options) #:nodoc: + def write_fragment_for(name, options) # VIEW TODO: Make #capture usable outside of ERB # This dance is needed because Builder can't use capture pos = output_buffer.length diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 61ce1d36e0..09dc6ef6bd 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -98,7 +98,7 @@ module ActionView from_time = from_time.to_time if from_time.respond_to?(:to_time) to_time = to_time.to_time if to_time.respond_to?(:to_time) from_time, to_time = to_time, from_time if from_time > to_time - distance_in_minutes = ((to_time - from_time)/60.0).round + distance_in_minutes = ((to_time - from_time) / 60.0).round distance_in_seconds = (to_time - from_time).round I18n.with_options locale: options[:locale], scope: options[:scope] do |locale| @@ -220,7 +220,7 @@ module ActionView # the respective locale (e.g. [:year, :month, :day] in the en locale that ships with Rails). # * <tt>:include_blank</tt> - Include a blank option in every select field so it's possible to set empty # dates. - # * <tt>:default</tt> - Set a default date if the affected date isn't set or is nil. + # * <tt>:default</tt> - Set a default date if the affected date isn't set or is +nil+. # * <tt>:selected</tt> - Set a date that overrides the actual value. # * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled. # * <tt>:prompt</tt> - Set to true (for a generic prompt), a prompt string or a hash of prompt strings @@ -267,7 +267,7 @@ module ActionView # date_select("article", "written_on", default: 3.days.from_now) # # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute - # # which is set in the form with todays date, regardless of the value in the Active Record object. + # # which is set in the form with today's date, regardless of the value in the Active Record object. # date_select("article", "written_on", selected: Date.today) # # # Generates a date select that when POSTed is stored in the credit_card variable, in the bill_due attribute @@ -303,7 +303,7 @@ module ActionView # # the sunrise attribute. # time_select("article", "start_time", include_seconds: true) # - # # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30 and 45. + # # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30, and 45. # time_select 'game', 'game_time', {minute_step: 15} # # # Creates a time select tag with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. @@ -866,7 +866,7 @@ module ActionView end # Returns translated month names, but also ensures that a custom month - # name array has a leading nil element. + # name array has a leading +nil+ element. def month_names @month_names ||= begin month_names = @options[:use_month_names] || translated_month_names diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 124a14f1d9..26a625e4fe 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -474,6 +474,266 @@ module ActionView end private :apply_form_for_options! + # 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. + # + # ==== +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>. + # + # When using labels +form_with+ requires setting the id on the field being + # labelled: + # + # <%= form_with(model: @post) do |form| %> + # <%= form.label :title %> + # <%= form.text_field :title, id: :post_title %> + # <% end %> + # + # See +label+ for more on how the +for+ attribute is derived. + # + # === 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 FormOptionHelper and DateHelper designed + # to work with an object as a base, like + # FormOptionHelper#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] = true + + 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. @@ -720,6 +980,74 @@ 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>. + # + # When using labels +fields+ requires setting the id on the field being + # labelled: + # + # <%= fields :comment do |fields| %> + # <%= fields.label :body %> + # <%= fields.text_field :body, id: :comment_body %> + # <% end %> + # + # See +label+ for more on how the +for+ attribute is derived. + # + # === 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 FormOptionHelper and DateHelper designed + # to work with an object as a base, like + # FormOptionHelper#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] = true + + 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. @@ -1175,6 +1503,32 @@ module ActionView end private + def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: false, + 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 + + if !local && !embed_authenticity_token_in_remote_forms && + html_options[:authenticity_token].blank? + # The authenticity token is taken from the meta tag in this case + html_options[:authenticity_token] = false + elsif html_options[:authenticity_token] == true + # Include the default authenticity_token, which is only generated when its 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 @@ -1183,7 +1537,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 @@ -1249,7 +1603,7 @@ module ActionView # The methods which wrap a form helper call. class_attribute :field_helpers - self.field_helpers = [:fields_for, :label, :text_field, :password_field, + self.field_helpers = [: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, @@ -1285,7 +1639,10 @@ 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}")) && object.respond_to?(:to_param) @auto_index = object.to_param @@ -1293,11 +1650,12 @@ module ActionView 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( @@ -1566,10 +1924,11 @@ 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 @@ -1586,6 +1945,16 @@ module ActionView @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] = true + + 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. @@ -1809,7 +2178,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) @@ -1934,6 +2303,12 @@ 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 diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 7bd473507b..ffc64e7118 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -442,26 +442,14 @@ module ActionView # # => <input name='commit' type='submit' value='Save' data-disable-with="Save" data-confirm="Are you sure?" /> # def submit_tag(value = "Save changes", options = {}) - options = options.stringify_keys + options = options.deep_stringify_keys tag_options = { "type" => "submit", "name" => "commit", "value" => value }.update(options) - - if ActionView::Base.automatically_disable_submit_tag - unless tag_options["data-disable-with"] == false || (tag_options["data"] && tag_options["data"][:disable_with] == false) - disable_with_text = tag_options["data-disable-with"] - disable_with_text ||= tag_options["data"][:disable_with] if tag_options["data"] - disable_with_text ||= value.to_s.clone - tag_options.deep_merge!("data" => { "disable_with" => disable_with_text }) - else - tag_options["data"].delete(:disable_with) if tag_options["data"] - end - tag_options.delete("data-disable-with") - end - + set_default_disable_with value, tag_options tag :input, tag_options end # Creates a button element that defines a <tt>submit</tt> button, - # <tt>reset</tt>button or a generic button which can be used in + # <tt>reset</tt> button or a generic button which can be used in # JavaScript, for example. You can use the button tag as a regular # submit tag but it isn't supported in legacy browsers. However, # the button tag does allow for richer labels such as images and emphasis, @@ -898,6 +886,22 @@ module ActionView def sanitize_to_id(name) name.to_s.delete("]").tr("^-a-zA-Z0-9:.", "_") end + + def set_default_disable_with(value, tag_options) + return unless ActionView::Base.automatically_disable_submit_tag + data = tag_options["data"] + + unless tag_options["data-disable-with"] == false || (data && data["disable_with"] == false) + disable_with_text = tag_options["data-disable-with"] + disable_with_text ||= data["disable_with"] if data + disable_with_text ||= value.to_s.clone + tag_options.deep_merge!("data" => { "disable_with" => disable_with_text }) + else + data.delete("disable_with") if data + end + + tag_options.delete("data-disable-with") + end end end end diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index 75b898c3e9..9e80f0b2ee 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -171,6 +171,9 @@ module ActionView # to ","). # * <tt>:separator</tt> - Sets the separator between the # fractional and integer digits (defaults to "."). + # * <tt>:delimiter_pattern</tt> - Sets a custom regular expression used for + # deriving the placement of delimiter. Helpful when using currency formats + # like INR. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when # the argument is invalid. # @@ -187,6 +190,9 @@ module ActionView # number_with_delimiter(98765432.98, delimiter: " ", separator: ",") # # => 98 765 432,98 # + # number_with_delimiter("123456.78", + # delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/) # => "1,23,456.78" + # # number_with_delimiter("112a", raise: true) # => raise InvalidNumberError def number_with_delimiter(number, options = {}) delegate_number_helper_method(:number_to_delimited, number, options) diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb index 8e63e59fac..9f1a890f6a 100644 --- a/actionview/lib/action_view/helpers/output_safety_helper.rb +++ b/actionview/lib/action_view/helpers/output_safety_helper.rb @@ -28,7 +28,7 @@ module ActionView #:nodoc: # safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />") # # => "<p>foo</p><br /><p>bar</p>" # - def safe_join(array, sep=$,) + def safe_join(array, sep = $,) sep = ERB::Util.unwrapped_html_escape(sep) array.flatten.map! { |i| ERB::Util.unwrapped_html_escape(i) }.join(sep).html_safe diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index 24c6d03cd1..306b71c85e 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -138,7 +138,7 @@ module ActionView # # ==== Options # - # Any passed options become attributes on the generated tag. + # Use symbol keyed options to add attributes to the generated tag. # # tag.section class: %w( kitties puppies ) # # => <section class="kitties puppies"></section> diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index e3e3c8b109..0895533a60 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -11,10 +11,19 @@ module ActionView @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup @template_object = template_object - @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]") + @object_name.sub!(/\[\]$/, "") || @object_name.sub!(/\[\]\]$/, "]") @object = retrieve_object(options.delete(:object)) + @skip_default_ids = options.delete(:skip_default_ids) + @allow_method_names_outside_object = options.delete(:allow_method_names_outside_object) @options = options - @auto_index = Regexp.last_match ? retrieve_autoindex(Regexp.last_match.pre_match) : nil + + if Regexp.last_match + @generate_indexed_names = true + @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) + else + @generate_indexed_names = false + @auto_index = nil + end end # This is what child classes implement. @@ -25,7 +34,11 @@ module ActionView private def value(object) - object.public_send @method_name if object + if @allow_method_names_outside_object + object.public_send @method_name if object && object.respond_to?(@method_name) + else + object.public_send @method_name if object + end end def value_before_type_cast(object) @@ -81,15 +94,21 @@ module ActionView def add_default_name_and_id(options) index = name_and_id_index(options) options["name"] = options.fetch("name") { tag_name(options["multiple"], index) } - options["id"] = options.fetch("id") { tag_id(index) } - if namespace = options.delete("namespace") - options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace + + unless skip_default_ids? + options["id"] = options.fetch("id") { tag_id(index) } + if namespace = options.delete("namespace") + options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace + end end end def tag_name(multiple = false, index = nil) # a little duplication to construct less strings - if index + case + when @object_name.empty? + "#{sanitized_method_name}#{"[]" if multiple}" + when index "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}" else "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}" @@ -98,7 +117,10 @@ module ActionView def tag_id(index = nil) # a little duplication to construct less strings - if index + case + when @object_name.empty? + sanitized_method_name.dup + when index "#{sanitized_object_name}_#{index}_#{sanitized_method_name}" else "#{sanitized_object_name}_#{sanitized_method_name}" @@ -110,7 +132,7 @@ module ActionView end def sanitized_method_name - @sanitized_method_name ||= @method_name.sub(/\?$/,"") + @sanitized_method_name ||= @method_name.sub(/\?$/, "") end def sanitized_value(value) @@ -152,7 +174,15 @@ module ActionView end def name_and_id_index(options) - options.key?("index") ? options.delete("index") || "" : @auto_index + if options.key?("index") + options.delete("index") || "" + elsif @generate_indexed_names + @auto_index || "" + end + end + + def skip_default_ids? + @skip_default_ids end end end diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb index 2a6bf49567..7252d4f2d9 100644 --- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -7,7 +7,7 @@ module ActionView include CollectionHelpers class CheckBoxBuilder < Builder # :nodoc: - def check_box(extra_html_options={}) + def check_box(extra_html_options = {}) html_options = extra_html_options.merge(@input_html_options) html_options[:multiple] = true @template_object.check_box(@object_name, @method_name, html_options, @value, nil) @@ -24,7 +24,7 @@ module ActionView builder.check_box + builder.label end - def hidden_field_name #:nodoc: + def hidden_field_name "#{super}[]" end end diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb index 70d7c484eb..75d237eb35 100644 --- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb +++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb @@ -17,7 +17,7 @@ module ActionView @input_html_options = input_html_options end - def label(label_html_options={}, &block) + def label(label_html_options = {}, &block) html_options = @input_html_options.slice(:index, :namespace).merge(label_html_options) html_options[:for] ||= @input_html_options[:id] if @input_html_options[:id] @@ -41,9 +41,9 @@ module ActionView sanitize_attribute_name(value), text, value, html_options) end - # Generate default options for collection helpers, such as :checked and - # :disabled. - def default_html_options_for_collection(item, value) #:nodoc: + # Generate default options for collection helpers, such as :checked and + # :disabled. + def default_html_options_for_collection(item, value) html_options = @html_options.dup [:checked, :selected, :disabled, :readonly].each do |option| @@ -67,11 +67,11 @@ module ActionView html_options end - def sanitize_attribute_name(value) #:nodoc: + def sanitize_attribute_name(value) "#{sanitized_method_name}_#{sanitized_value(value)}" end - def render_collection #:nodoc: + def render_collection @collection.map do |item| value = value_for_collection(item, @value_method) text = value_for_collection(item, @text_method) @@ -82,7 +82,7 @@ module ActionView end.join.html_safe end - def render_collection_for(builder_class, &block) #:nodoc: + def render_collection_for(builder_class, &block) options = @options.stringify_keys rendered_collection = render_collection do |item, value, text, default_html_options| builder = instantiate_builder(builder_class, item, value, text, default_html_options) @@ -103,12 +103,12 @@ module ActionView end end - def hidden_field #:nodoc: + def hidden_field hidden_name = @html_options[:name] || hidden_field_name @template_object.hidden_field_tag(hidden_name, "", id: nil) end - def hidden_field_name #:nodoc: + def hidden_field_name "#{tag_name(false, @options[:index])}" end end diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb index eed7941cd6..a5f72af9ff 100644 --- a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb +++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb @@ -7,7 +7,7 @@ module ActionView include CollectionHelpers class RadioButtonBuilder < Builder # :nodoc: - def radio_button(extra_html_options={}) + def radio_button(extra_html_options = {}) html_options = extra_html_options.merge(@input_html_options) @template_object.radio_button(@object_name, @method_name, @value, html_options) end diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb index b31d5fda66..cab15ae201 100644 --- a/actionview/lib/action_view/helpers/tags/label.rb +++ b/actionview/lib/action_view/helpers/tags/label.rb @@ -73,6 +73,10 @@ module ActionView def render_component(builder) builder.translation end + + def skip_default_ids? + false # The id is used as the `for` attribute. + end end end end diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb index 8cc34e3180..667c7e945a 100644 --- a/actionview/lib/action_view/helpers/tags/select.rb +++ b/actionview/lib/action_view/helpers/tags/select.rb @@ -28,10 +28,10 @@ module ActionView private - # Grouped choices look like this: - # - # [nil, []] - # { nil => [] } + # Grouped choices look like this: + # + # [nil, []] + # { nil => [] } def grouped_choices? !@choices.empty? && @choices.first.respond_to?(:last) && Array === @choices.first.last end diff --git a/actionview/lib/action_view/helpers/tags/translator.rb b/actionview/lib/action_view/helpers/tags/translator.rb index 62b1df81c6..ced835eaa8 100644 --- a/actionview/lib/action_view/helpers/tags/translator.rb +++ b/actionview/lib/action_view/helpers/tags/translator.rb @@ -14,6 +14,8 @@ module ActionView translated_attribute || human_attribute_name end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :object_name, :method_and_value, :scope, :model diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 0fea4df09c..07dccf5b41 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -151,7 +151,7 @@ module ActionView # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+, # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. Use the # <tt>:separator</tt> option to choose the delimitation. The resulting string will be stripped in any case. If the +phrase+ - # isn't found, nil is returned. + # isn't found, +nil+ is returned. # # excerpt('This is an example', 'an', radius: 5) # # => ...s is an exam... @@ -225,14 +225,7 @@ module ActionView # # pluralize(2, 'Person', locale: :de) # # => 2 Personen - def pluralize(count, singular, deprecated_plural = nil, plural: nil, locale: I18n.locale) - if deprecated_plural - ActiveSupport::Deprecation.warn("Passing plural as a positional argument " \ - "is deprecated and will be removed in Rails 5.1. Use e.g. " \ - "pluralize(1, 'person', plural: 'people') instead.") - plural ||= deprecated_plural - end - + def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale) word = if (count == 1 || count =~ /^1(\.0+)?$/) singular else diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index cbabaf5757..47ed41a129 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -1,6 +1,5 @@ require "action_view/helpers/tag_helper" require "active_support/core_ext/string/access" -require "active_support/core_ext/regexp" require "i18n/exceptions" module ActionView diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index dad0e9dac3..1f753bccd6 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -2,7 +2,6 @@ require "action_view/helpers/javascript_helper" require "active_support/core_ext/array/access" require "active_support/core_ext/hash/keys" require "active_support/core_ext/string/output_safety" -require "active_support/core_ext/regexp" module ActionView # = Action View URL Helpers @@ -106,10 +105,9 @@ module ActionView # driver to prompt with the question specified (in this case, the # resulting text would be <tt>question?</tt>. If the user accepts, the # link is processed normally, otherwise no action is taken. - # * <tt>:disable_with</tt> - Value of this parameter will be - # used as the value for a disabled version of the submit - # button when the form is submitted. This feature is provided - # by the unobtrusive JavaScript driver. + # * <tt>:disable_with</tt> - Value of this parameter will be used as the + # name for a disabled version of the link. This feature is provided by + # the unobtrusive JavaScript driver. # # ==== Examples # Because it relies on +url_for+, +link_to+ supports both older-style controller/action/id arguments @@ -519,6 +517,9 @@ module ActionView # current_page?('http://www.example.com/shop/checkout') # # => true # + # current_page?('http://www.example.com/shop/checkout', check_parameters: true) + # # => false + # # current_page?('/shop/checkout') # # => true # @@ -532,7 +533,7 @@ module ActionView # # We can also pass in the symbol arguments instead of strings. # - def current_page?(options) + def current_page?(options, check_parameters: false) unless request raise "You cannot use helpers that need to determine the current " \ "page unless your view context provides a Request object " \ @@ -541,12 +542,14 @@ module ActionView return false unless request.get? || request.head? + check_parameters ||= !options.is_a?(String) && options.try(:delete, :check_parameters) url_string = URI.parser.unescape(url_for(options)).force_encoding(Encoding::BINARY) # We ignore any extra parameters in the request_uri if the # submitted url doesn't have any either. This lets the function # work with things like ?order=asc - request_uri = url_string.index("?") ? request.fullpath : request.path + # the behaviour can be disabled with check_parameters: true + request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path request_uri = URI.parser.unescape(request_uri).force_encoding(Encoding::BINARY) url_string.chomp!("/") if url_string.start_with?("/") && url_string != "/" @@ -564,7 +567,7 @@ module ActionView html_options = html_options.stringify_keys html_options["data-remote"] = "true".freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options) - method = html_options.delete("method".freeze) + method = html_options.delete("method".freeze) add_method_to_attributes!(html_options, method) if method @@ -587,7 +590,7 @@ module ActionView html_options["data-method".freeze] = method end - def token_tag(token=nil, form_options: {}) + def token_tag(token = nil, form_options: {}) if token != false && protect_against_forgery? token ||= form_authenticity_token(form_options: form_options) tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token) @@ -616,7 +619,18 @@ module ActionView # # to_form_params({ name: 'Denmark' }, 'country') # # => [{name: 'country[name]', value: 'Denmark'}] - def to_form_params(attribute, namespace = nil) # :nodoc: + def to_form_params(attribute, namespace = nil) + attribute = if attribute.respond_to?(:permitted?) + unless attribute.permitted? + raise ArgumentError, "Attempting to generate a buttom from non-sanitized request parameters!" \ + " Whitelist and sanitize passed parameters to be secure." + end + + attribute.to_h + else + attribute + end + params = [] case attribute when Hash |