From 18a44be8b9c13911da21fe2cc95a84e95dee7456 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sat, 5 Nov 2016 18:45:02 +0100 Subject: [PATCH 01/16] Add form_with to unify form_tag/form_for. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `form_tag` and `form_for` serve very similar use cases. This PR unifies that usage such that `form_with` can output just the opening form tag akin to `form_tag` and can just work with a url, for instance. `form_with` by default doesn't attach class or id to the form — removing them on fields is moved out to a default revisiting PR later. Ported over old tests where applicable to ensure maximum coverage, but left some commented out because they don't yet apply (e.g. `fields_for` later being replaced by `fields`). [ Kasper Timm Hansen & Marek Kirejczyk ] --- .../lib/action_view/helpers/form_helper.rb | 67 +- .../template/form_helper/form_with_test.rb | 2298 +++++++++++++++++ 2 files changed, 2364 insertions(+), 1 deletion(-) create mode 100644 actionview/test/template/form_helper/form_with_test.rb diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 9bffe860db246..f3edfbb1daf57 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -474,6 +474,71 @@ def apply_form_for_options!(record, object, options) #:nodoc: end private :apply_form_for_options! + # Passing model: @post will 1) set scope: :post, 2) set url: url_for(@post) + # + # form_with(model: @post) do |form| + # form.text_field :title # Will reference @post.title as normal + # form.text_area :description, "Overwrite @post.description if present, if not, it will still work" + # + # form.submit + # end + # + # form_with(scope: :post, url: posts_path) do |form| + # form.text_field :title # post[title] + # form.text_area :description, "Overwrite @post.description or ignore if it's not present" + # + # form.submit + # end + # # => + #
+ # + # + # + # + # + # + # + #
+ # + # form_with(url: different_path, class: 'something', id: 'specific') do |form| + # form.text_field :title, 'This is the value of the title' + # + # form.text_area :description, class: 'No value has been supplied here' + # + # form.fields(:permission) do |fields| + # # on/off instead of positional parameters for setting values + # fields.check_box :admin, on: 'yes', off: 'no' + # end + # + # form.select :category, Post::CATEGORIES, blank: 'None' + # form.select :author_id, Person.all.collect { |p| [ p.name, p.id ] }, blank: 'Pick someone' + # + # form.submit + # end + def form_with(model: nil, scope: nil, url: nil, format: nil, html: {}, **options) + 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 + + html_options = html.merge(options.except(:index, :include_id, :builder)) + html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted? + + if block_given? + builder = instantiate_builder(scope, model, options) + output = capture(builder, &Proc.new) + html_options[:multipart] ||= builder.multipart? + + html_options = html_options_for_form(url || {}, html_options) + form_tag_with_body(html_options, output) + else + html_options = html_options_for_form(url || {}, html_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. @@ -1183,7 +1248,7 @@ def instantiate_builder(record_name, record_object, options) 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 diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb new file mode 100644 index 0000000000000..6c4a6804fe918 --- /dev/null +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -0,0 +1,2298 @@ +require "abstract_unit" +require "controller/fake_models" + +class FormWithTest < ActionView::TestCase + include RenderERBUtils +end + +class FormWithActsLikeFormTagTest < FormWithTest + tests ActionView::Helpers::FormTagHelper + + setup do + @controller = BasicController.new + end + + def hidden_fields(options = {}) + method = options[:method] + enforce_utf8 = options.fetch(:enforce_utf8, true) + + "".tap do |txt| + if enforce_utf8 + txt << %{} + end + + if method && !%w(get post).include?(method.to_s) + txt << %{} + end + end + end + + def form_text(action = "http://www.example.com", options = {}) + remote, enctype, html_class, id, method = options.values_at(:remote, :enctype, :html_class, :id, :method) + + method = method.to_s == "get" ? "get" : "post" + + txt = %{
} + end + + def whole_form(action = "http://www.example.com", options = {}) + out = form_text(action, options) + hidden_fields(options) + + if block_given? + out << yield << "
" + end + + out + end + + def url_for(options) + if options.is_a?(Hash) + "http://www.example.com" + else + super + end + end + + def test_form_with_multipart + actual = form_with(multipart: true) + + expected = whole_form("http://www.example.com", enctype: true) + assert_dom_equal expected, actual + end + + def test_form_with_with_method_patch + actual = form_with(method: :patch) + + expected = whole_form("http://www.example.com", method: :patch) + assert_dom_equal expected, actual + end + + def test_form_with_with_method_put + actual = form_with(method: :put) + + expected = whole_form("http://www.example.com", method: :put) + assert_dom_equal expected, actual + end + + def test_form_with_with_method_delete + actual = form_with(method: :delete) + + expected = whole_form("http://www.example.com", method: :delete) + assert_dom_equal expected, actual + end + + def test_form_with_with_remote + actual = form_with(remote: true) + + expected = whole_form("http://www.example.com", remote: true) + assert_dom_equal expected, actual + end + + def test_form_with_with_remote_false + actual = form_with(remote: false) + + expected = whole_form + assert_dom_equal expected, actual + end + + def test_form_with_enforce_utf8_true + actual = form_with(enforce_utf8: true) + expected = whole_form("http://www.example.com", enforce_utf8: true) + assert_dom_equal expected, actual + assert actual.html_safe? + end + + def test_form_with_enforce_utf8_false + actual = form_with(enforce_utf8: false) + expected = whole_form("http://www.example.com", enforce_utf8: false) + assert_dom_equal expected, actual + assert actual.html_safe? + end + + def test_form_with_with_block_in_erb + output_buffer = render_erb("<%= form_with(url: 'http://www.example.com') do %>Hello world!<% end %>") + + expected = whole_form { "Hello world!" } + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_block_and_method_in_erb + output_buffer = render_erb("<%= form_with(url: 'http://www.example.com', method: :put) do %>Hello world!<% end %>") + + expected = whole_form("http://www.example.com", method: "put") do + "Hello world!" + end + + assert_dom_equal expected, output_buffer + end +end + +class FormWithActsLikeFormForTest < FormWithTest + def form_with(*) + @output_buffer = super + end + + teardown do + I18n.backend.reload! + end + + setup do + # Create "label" locale for testing I18n label helpers + I18n.backend.store_translations "label", + activemodel: { + attributes: { + post: { + cost: "Total cost" + }, + "post/language": { + spanish: "Espanol" + } + } + }, + helpers: { + label: { + post: { + body: "Write entire text here", + color: { + red: "Rojo" + }, + comments: { + body: "Write body here" + } + }, + tag: { + value: "Tag" + }, + post_delegate: { + title: "Delegate model_name title" + } + } + } + + # Create "submit" locale for testing I18n submit helpers + I18n.backend.store_translations "submit", + helpers: { + submit: { + create: "Create %{model}", + update: "Confirm %{model} changes", + submit: "Save changes", + another_post: { + update: "Update your %{model}" + } + } + } + + I18n.backend.store_translations "placeholder", + activemodel: { + attributes: { + post: { + cost: "Total cost" + }, + "post/cost": { + uk: "Pounds" + } + } + }, + helpers: { + placeholder: { + post: { + title: "What is this about?", + written_on: { + spanish: "Escrito en" + }, + comments: { + body: "Write body here" + } + }, + post_delegate: { + title: "Delegate model_name title" + }, + tag: { + value: "Tag" + } + } + } + + @post = Post.new + @comment = Comment.new + def @post.errors() + Class.new { + def [](field); field == "author_name" ? ["can't be empty"] : [] end + def empty?() false end + def count() 1 end + def full_messages() ["Author name can't be empty"] end + }.new + end + def @post.to_key; [123]; end + def @post.id; 0; end + def @post.id_before_type_cast; "omg"; end + def @post.id_came_from_user?; true; end + def @post.to_param; "123"; end + + @post.persisted = true + @post.title = "Hello World" + @post.author_name = "" + @post.body = "Back to the hill and over it again!" + @post.secret = 1 + @post.written_on = Date.new(2004, 6, 15) + + @post.comments = [] + @post.comments << @comment + + @post.tags = [] + @post.tags << Tag.new + + @post_delegator = PostDelegator.new + + @post_delegator.title = "Hello World" + + @car = Car.new("#000FFF") + end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + resources :posts do + resources :comments + end + + namespace :admin do + resources :posts do + resources :comments + end + end + + get "/foo", to: "controller#action" + root to: "main#index" + end + + def _routes + Routes + end + + include Routes.url_helpers + + def url_for(object) + @url_for_options = object + + if object.is_a?(Hash) && object[:use_route].blank? && object[:controller].blank? + object.merge!(controller: "main", action: "index") + end + + super + end + + # def test_form_with_requires_block + # error = assert_raises(ArgumentError) do + # form_for(@post, html: { id: "create-post" }) + # end + # assert_equal "Missing block", error.message + # end + + def test_form_with_requires_arguments + error = assert_raises(ArgumentError) do + form_for(nil, html: { id: "create-post" }) do + end + end + assert_equal "First argument in form cannot contain nil or be empty", error.message + + error = assert_raises(ArgumentError) do + form_for([nil, nil], html: { id: "create-post" }) do + end + end + assert_equal "First argument in form cannot contain nil or be empty", error.message + end + + def test_form_with + form_with(model: @post, id: "create-post") do |f| + concat f.label(:title) { "The Title" } + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + concat f.submit("Create post") + concat f.button("Create post") + concat f.button { + concat content_tag(:span, "Create post") + } + end + + expected = whole_form("/posts/123", "create-post", method: "patch") do + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + # def test_form_with_with_collection_radio_buttons + # post = Post.new + # def post.active; false; end + # form_for(post) do |f| + # concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) + # end + # + # expected = whole_form("/posts", "new_post", "new_post") do + # "" + + # "" + + # "" + + # "" + + # "" + # end + # + # assert_dom_equal expected, output_buffer + # end + + # def test_form_with_with_collection_radio_buttons_with_custom_builder_block + # post = Post.new + # def post.active; false; end + # + # form_for(post) do |f| + # rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b| + # b.label { b.radio_button + b.text } + # end + # concat rendered_radio_buttons + # end + # + # expected = whole_form("/posts", "new_post", "new_post") do + # "" + + # "" + + # "" + # end + # + # assert_dom_equal expected, output_buffer + # end + + # def test_form_with_with_collection_radio_buttons_with_custom_builder_block_does_not_leak_the_template + # post = Post.new + # def post.active; false; end + # def post.id; 1; end + # + # form_for(post) do |f| + # rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b| + # b.label { b.radio_button + b.text } + # end + # concat rendered_radio_buttons + # concat f.hidden_field :id + # end + # + # expected = whole_form("/posts", "new_post_1", "new_post") do + # "" + + # "" + + # "" + + # "" + # end + # + # assert_dom_equal expected, output_buffer + # end + + # def test_form_with_namespace_and_with_collection_radio_buttons + # post = Post.new + # def post.active; false; end + # + # form_for(post, namespace: "foo") do |f| + # concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) + # end + # + # expected = whole_form("/posts", "foo_new_post", "new_post") do + # "" + + # "" + + # "" + + # "" + + # "" + # end + # + # assert_dom_equal expected, output_buffer + # end + + # def test_form_with_index_and_with_collection_radio_buttons + # post = Post.new + # def post.active; false; end + # + # form_for(post, index: "1") do |f| + # concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) + # end + # + # expected = whole_form("/posts", "new_post", "new_post") do + # "" + + # "" + + # "" + + # "" + + # "" + # end + # + # assert_dom_equal expected, output_buffer + # end + + # def test_form_with_with_collection_check_boxes + # post = Post.new + # def post.tag_ids; [1, 3]; end + # collection = (1..3).map { |i| [i, "Tag #{i}"] } + # form_for(post) do |f| + # concat f.collection_check_boxes(:tag_ids, collection, :first, :last) + # end + # + # expected = whole_form("/posts", "new_post", "new_post") do + # "" + + # "" + + # "" + + # "" + + # "" + + # "" + + # "" + # end + # + # assert_dom_equal expected, output_buffer + # end + # + # def test_form_with_with_collection_check_boxes_with_custom_builder_block + # post = Post.new + # def post.tag_ids; [1, 3]; end + # collection = (1..3).map { |i| [i, "Tag #{i}"] } + # form_for(post) do |f| + # rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b| + # b.label { b.check_box + b.text } + # end + # concat rendered_check_boxes + # end + # + # expected = whole_form("/posts", "new_post", "new_post") do + # "" + + # "" + + # "" + + # "" + # end + # + # assert_dom_equal expected, output_buffer + # end + # + # def test_form_with_with_collection_check_boxes_with_custom_builder_block_does_not_leak_the_template + # post = Post.new + # def post.tag_ids; [1, 3]; end + # def post.id; 1; end + # collection = (1..3).map { |i| [i, "Tag #{i}"] } + # + # form_for(post) do |f| + # rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b| + # b.label { b.check_box + b.text } + # end + # concat rendered_check_boxes + # concat f.hidden_field :id + # end + # + # expected = whole_form("/posts", "new_post_1", "new_post") do + # "" + + # "" + + # "" + + # "" + + # "" + # end + # + # assert_dom_equal expected, output_buffer + # end + # + # def test_form_with_namespace_and_with_collection_check_boxes + # post = Post.new + # def post.tag_ids; [1]; end + # collection = [[1, "Tag 1"]] + # + # form_for(post, namespace: "foo") do |f| + # concat f.collection_check_boxes(:tag_ids, collection, :first, :last) + # end + # + # expected = whole_form("/posts", "foo_new_post", "new_post") do + # "" + + # "" + + # "" + # end + # + # assert_dom_equal expected, output_buffer + # end + # + # def test_form_with_index_and_with_collection_check_boxes + # post = Post.new + # def post.tag_ids; [1]; end + # collection = [[1, "Tag 1"]] + # + # form_for(post, index: "1") do |f| + # concat f.collection_check_boxes(:tag_ids, collection, :first, :last) + # end + # + # expected = whole_form("/posts", "new_post", "new_post") do + # "" + + # "" + + # "" + # end + # + # assert_dom_equal expected, output_buffer + # end + + def test_form_with_with_file_field_generate_multipart + Post.send :attr_accessor, :file + + form_with(model: @post, id: "create-post") do |f| + concat f.file_field(:file) + end + + expected = whole_form("/posts/123", "create-post", method: "patch", multipart: true) do + "" + end + + assert_dom_equal expected, output_buffer + end + + # def test_fields_for_with_file_field_generate_multipart + # Comment.send :attr_accessor, :file + # + # form_for(@post) do |f| + # concat f.fields_for(:comment, @post) { |c| + # concat c.file_field(:file) + # } + # end + # + # expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch", multipart: true) do + # "" + # end + # + # assert_dom_equal expected, output_buffer + # end + + def test_form_with_with_format + form_with(model: @post, format: :json, id: "edit_post_123", class: "edit_post") do |f| + concat f.label(:title) + end + + expected = whole_form("/posts/123.json", "edit_post_123", "edit_post", method: "patch") do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_model_using_relative_model_naming + blog_post = Blog::Post.new("And his name will be forty and four.", 44) + + form_with(model: blog_post) do |f| + concat f.text_field :title + concat f.submit("Edit post") + end + + expected = whole_form("/posts/44", method: "patch") do + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_symbol_scope + form_with(model: @post, scope: "other_name", id: "create-post") do |f| + concat f.label(:title, class: "post_title") + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + concat f.submit("Create post") + end + + expected = whole_form("/posts/123", "create-post", method: "patch") do + "" + + "" + + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_tags_do_not_call_private_properties_on_form_object + obj = Class.new do + private + + def private_property + raise "This method should not be called." + end + end.new + + form_with(model: obj, scope: "other_name", url: "/", id: "edit-other-name") do |f| + assert_raise(NoMethodError) { f.hidden_field(:private_property) } + end + end + + def test_form_with_with_method_as_part_of_html_options + form_with(model: @post, url: "/", id: "create-post", html: { method: :delete }) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/", "create-post", method: "delete") do + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_method + form_with(model: @post, url: "/", method: :delete, id: "create-post") do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/", "create-post", method: "delete") do + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_search_field + # Test case for bug which would emit an "object" attribute + # when used with form_for using a search_field form helper + form_with(model: Post.new, url: "/search", id: "search-post", method: :get) do |f| + concat f.search_field(:title) + end + + expected = whole_form("/search", "search-post", method: "get") do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_remote + form_with(model: @post, url: "/", remote: true, id: "create-post", method: :patch) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/", "create-post", method: "patch", remote: true) do + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_enforce_utf8_true + form_with(scope: :post, enforce_utf8: true) do |f| + concat f.text_field(:title) + end + + expected = whole_form("/", enforce_utf8: true) do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_enforce_utf8_false + form_with(scope: :post, enforce_utf8: false) do |f| + concat f.text_field(:title) + end + + expected = whole_form("/", enforce_utf8: false) do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_remote_in_html + form_with(model: @post, url: "/", html: { remote: true, id: "create-post", method: :patch }) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/", "create-post", method: "patch", remote: true) do + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_remote_without_html + @post.persisted = false + @post.stub(:to_key, nil) do + form_with(model: @post, remote: true) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/posts", remote: true) do + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + end + + def test_form_with_without_object + form_with(scope: :post, id: "create-post") do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/", "create-post") do + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_index + form_with(model: @post, scope: "post[]") do |f| + concat f.label(:title) + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/posts/123", method: "patch") do + "" + + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_nil_index_option_override + form_with(model: @post, scope: "post[]", index: nil) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/posts/123", method: "patch") do + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_label_error_wrapping + form_with(model: @post) do |f| + concat f.label(:author_name, class: "label") + concat f.text_field(:author_name) + concat f.submit("Create post") + end + + expected = whole_form("/posts/123", method: "patch") do + "
" + + "
" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_label_error_wrapping_without_conventional_instance_variable + post = remove_instance_variable :@post + + form_with(model: post) do |f| + concat f.label(:author_name, class: "label") + concat f.text_field(:author_name) + concat f.submit("Create post") + end + + expected = whole_form("/posts/123", method: "patch") do + "
" + + "
" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_label_error_wrapping_block_and_non_block_versions + form_with(model: @post) do |f| + concat f.label(:author_name, "Name", class: "label") + concat f.label(:author_name, class: "label") { "Name" } + end + + expected = whole_form("/posts/123", method: "patch") do + "
" + + "
" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_namespace + skip "Do namespaces still make sense?" + form_for(@post, namespace: "namespace") do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + # def test_form_with_with_namespace_with_date_select + # form_for(@post, namespace: "namespace") do |f| + # concat f.date_select(:written_on) + # end + # + # assert_select "select#namespace_post_written_on_1i" + # end + # + # def test_form_with_with_namespace_with_label + # form_for(@post, namespace: "namespace") do |f| + # concat f.label(:title) + # concat f.text_field(:title) + # end + # + # expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do + # "" + + # "" + # end + # + # assert_dom_equal expected, output_buffer + # end + # + # def test_form_with_with_namespace_and_as_option + # form_for(@post, namespace: "namespace", as: "custom_name") do |f| + # concat f.text_field(:title) + # end + # + # expected = whole_form("/posts/123", "namespace_edit_custom_name", "edit_custom_name", method: "patch") do + # "" + # end + # + # assert_dom_equal expected, output_buffer + # end + # + # def test_two_form_for_with_namespace + # form_for(@post, namespace: "namespace_1") do |f| + # concat f.label(:title) + # concat f.text_field(:title) + # end + # + # expected_1 = whole_form("/posts/123", "namespace_1_edit_post_123", "edit_post", method: "patch") do + # "" + + # "" + # end + # + # assert_dom_equal expected_1, output_buffer + # + # form_for(@post, namespace: "namespace_2") do |f| + # concat f.label(:title) + # concat f.text_field(:title) + # end + # + # expected_2 = whole_form("/posts/123", "namespace_2_edit_post_123", "edit_post", method: "patch") do + # "" + + # "" + # end + # + # assert_dom_equal expected_2, output_buffer + # end + + # def test_fields_for_with_namespace + # @comment.body = "Hello World" + # form_for(@post, namespace: "namespace") do |f| + # concat f.text_field(:title) + # concat f.text_area(:body) + # concat f.fields_for(@comment) { |c| + # concat c.text_field(:body) + # } + # end + # + # expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do + # "" + + # "" + + # "" + # end + # + # assert_dom_equal expected, output_buffer + # end + + def test_submit_with_object_as_new_record_and_locale_strings + with_locale :submit do + @post.persisted = false + @post.stub(:to_key, nil) do + form_with(model: @post) do |f| + concat f.submit + end + + expected = whole_form("/posts") do + "" + end + + assert_dom_equal expected, output_buffer + end + end + end + + def test_submit_with_object_as_existing_record_and_locale_strings + with_locale :submit do + form_with(model: @post) do |f| + concat f.submit + end + + expected = whole_form("/posts/123", method: "patch") do + "" + end + + assert_dom_equal expected, output_buffer + end + end + + def test_submit_without_object_and_locale_strings + with_locale :submit do + form_with(scope: :post) do |f| + concat f.submit class: "extra" + end + + expected = whole_form do + "" + end + + assert_dom_equal expected, output_buffer + end + end + + def test_submit_with_object_and_nested_lookup + with_locale :submit do + form_with(model: @post, scope: :another_post) do |f| + concat f.submit + end + + expected = whole_form("/posts/123", method: "patch") do + "" + end + + assert_dom_equal expected, output_buffer + end + end + + def test_nested_fields_for + @comment.body = "Hello World" + form_with(model: @post) do |f| + concat f.fields_for(@comment) { |c| + concat c.text_field(:body) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_deep_nested_fields_for + @comment.save + form_with(scope: :posts) do |f| + f.fields_for("post[]", @post) do |f2| + f2.text_field(:id) + @post.comments.each do |comment| + concat f2.fields_for("comment[]", comment) { |c| + concat c.text_field(:name) + } + end + end + end + + expected = whole_form do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_nested_collections + form_with(model: @post, scope: "post[]") do |f| + concat f.text_field(:title) + concat f.fields_for("comment[]", @comment) { |c| + concat c.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_index_and_parent_fields + form_with(model: @post, index: 1) do |c| + concat c.text_field(:title) + concat c.fields_for("comment", @comment, index: 1) { |r| + concat r.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_index_and_nested_fields_for + output_buffer = form_with(model: @post, index: 1) do |f| + concat f.fields_for(:comment, @post) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_index_on_both + form_with(model: @post, index: 1) do |f| + concat f.fields_for(:comment, @post, index: 5) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_auto_index + form_with(model: @post, scope: "post[]") do |f| + concat f.fields_for(:comment, @post) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_index_radio_button + form_with(model: @post) do |f| + concat f.fields_for(:comment, @post, index: 5) { |c| + concat c.radio_button(:title, "hello") + } + end + + expected = whole_form("/posts/123", method: "patch") do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_auto_index_on_both + form_with(model: @post, scope: "post[]") do |f| + concat f.fields_for("comment[]", @post) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_index_and_auto_index + output_buffer = form_with(model: @post, scope: "post[]") do |f| + concat f.fields_for(:comment, @post, index: 5) { |c| + concat c.text_field(:title) + } + end + + output_buffer << form_with(model: @post, index: 1) do |f| + concat f.fields_for("comment[]", @post) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "" + end + whole_form("/posts/123", method: "patch") do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_a_new_record_on_a_nested_attributes_one_to_one_association + @post.author = Author.new + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_explicitly_passed_object_on_a_nested_attributes_one_to_one_association + form_with(model: @post) do |f| + f.fields_for(:author, Author.new(123)) do |af| + assert_not_nil af.object + assert_equal 123, af.object.id + end + end + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association + @post.author = Author.new(321) + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_using_erb_and_inline_block + @post.author = Author.new(321) + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + af.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id + @post.author = Author.new(321) + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author, include_id: false) { |af| + af.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_inherited + @post.author = Author.new(321) + + form_with(model: @post, include_id: false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + af.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_override + @post.author = Author.new(321) + + form_with(model: @post, include_id: false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author, include_id: true) { |af| + af.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_one_to_one_association_with_explicit_hidden_field_placement + @post.author = Author.new(321) + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.hidden_field(:id) + concat af.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_with(model: @post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.author = Author.new(321) + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields_for(:comments, comment, include_id: false) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_inherited + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.author = Author.new(321) + + form_with(model: @post, include_id: false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_override + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.author = Author.new(321) + + form_with(model: @post, include_id: false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author, include_id: true) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_using_erb_and_inline_block + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_with(model: @post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + cf.text_field(:name) + } + end + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_explicit_hidden_field_placement + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_with(model: @post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.hidden_field(:id) + concat cf.text_field(:name) + } + end + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_new_records_on_a_nested_attributes_collection_association + @post.comments = [Comment.new, Comment.new] + + form_with(model: @post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_and_new_records_on_a_nested_attributes_collection_association + @post.comments = [Comment.new(321), Comment.new] + + form_with(model: @post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_empty_supplied_attributes_collection + form_with(model: @post) do |f| + concat f.text_field(:title) + f.fields_for(:comments, []) do |cf| + concat cf.text_field(:name) + end + end + + expected = whole_form("/posts/123", method: "patch") do + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes_collection + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments, @post.comments) { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_arel_like + @post.comments = ArelLike.new + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments, @post.comments) { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_label_translation_with_more_than_10_records + @post.comments = Array.new(11) { |id| Comment.new(id + 1) } + + params = 11.times.map { ["post.comments.body", default: [:"comment.body", ""], scope: "helpers.label"] } + assert_called_with(I18n, :t, params, returns: "Write body here") do + form_with(model: @post) do |f| + f.fields_for(:comments) do |cf| + concat cf.label(:body) + end + end + end + end + + def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes_collection_different_from_record_one + comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.comments = [] + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments, comments) { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_on_a_nested_attributes_collection_association_yields_only_builder + @post.comments = [Comment.new(321), Comment.new] + yielded_comments = [] + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments) { |cf| + concat cf.text_field(:name) + yielded_comments << cf.object + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + assert_equal yielded_comments, @post.comments + end + + def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association + @post.comments = [] + + form_with(model: @post) do |f| + concat f.fields_for(:comments, Comment.new(321), child_index: "abc") { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_child_index_as_lambda_option_override_on_a_nested_attributes_collection_association + @post.comments = [] + + form_with(model: @post) do |f| + concat f.fields_for(:comments, Comment.new(321), child_index: -> { "abc" }) { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + class FakeAssociationProxy + def to_ary + [1, 2, 3] + end + end + + def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association_with_proxy + @post.comments = FakeAssociationProxy.new + + form_with(model: @post) do |f| + concat f.fields_for(:comments, Comment.new(321), child_index: "abc") { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_index_method_with_existing_records_on_a_nested_attributes_collection_association + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_with(model: @post) do |f| + expected = 0 + @post.comments.each do |comment| + f.fields_for(:comments, comment) { |cf| + assert_equal cf.index, expected + expected += 1 + } + end + end + end + + def test_nested_fields_for_index_method_with_existing_and_new_records_on_a_nested_attributes_collection_association + @post.comments = [Comment.new(321), Comment.new] + + form_with(model: @post) do |f| + expected = 0 + @post.comments.each do |comment| + f.fields_for(:comments, comment) { |cf| + assert_equal cf.index, expected + expected += 1 + } + end + end + end + + def test_nested_fields_for_index_method_with_existing_records_on_a_supplied_nested_attributes_collection + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_with(model: @post) do |f| + expected = 0 + f.fields_for(:comments, @post.comments) { |cf| + assert_equal cf.index, expected + expected += 1 + } + end + end + + def test_nested_fields_for_index_method_with_child_index_option_override_on_a_nested_attributes_collection_association + @post.comments = [] + + form_with(model: @post) do |f| + f.fields_for(:comments, Comment.new(321), child_index: "abc") { |cf| + assert_equal cf.index, "abc" + } + end + end + + def test_nested_fields_uses_unique_indices_for_different_collection_associations + @post.comments = [Comment.new(321)] + @post.tags = [Tag.new(123), Tag.new(456)] + @post.comments[0].relevances = [] + @post.tags[0].relevances = [] + @post.tags[1].relevances = [] + + form_with(model: @post) do |f| + concat f.fields_for(:comments, @post.comments[0]) { |cf| + concat cf.text_field(:name) + concat cf.fields_for(:relevances, CommentRelevance.new(314)) { |crf| + concat crf.text_field(:value) + } + } + concat f.fields_for(:tags, @post.tags[0]) { |tf| + concat tf.text_field(:value) + concat tf.fields_for(:relevances, TagRelevance.new(3141)) { |trf| + concat trf.text_field(:value) + } + } + concat f.fields_for("tags", @post.tags[1]) { |tf| + concat tf.text_field(:value) + concat tf.fields_for(:relevances, TagRelevance.new(31415)) { |trf| + concat trf.text_field(:value) + } + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_hash_like_model + @author = HashBackedAuthor.new + + form_with(model: @post) do |f| + concat f.fields_for(:author, @author) { |af| + concat af.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_fields_for + output_buffer = fields_for(:post, @post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = + "" + + "" + + "" + + "" + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_with_index + output_buffer = fields_for("post[]", @post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = + "" + + "" + + "" + + "" + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_with_nil_index_option_override + output_buffer = fields_for("post[]", @post, index: nil) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = + "" + + "" + + "" + + "" + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_with_index_option_override + output_buffer = fields_for("post[]", @post, index: "abc") do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = + "" + + "" + + "" + + "" + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_without_object + output_buffer = fields_for(:post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = + "" + + "" + + "" + + "" + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_with_only_object + output_buffer = fields_for(@post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = + "" + + "" + + "" + + "" + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_object_with_bracketed_name + output_buffer = fields_for("author[post]", @post) do |f| + concat f.label(:title) + concat f.text_field(:title) + end + + assert_dom_equal "" + + "", + output_buffer + end + + def test_fields_for_object_with_bracketed_name_and_index + output_buffer = fields_for("author[post]", @post, index: 1) do |f| + concat f.label(:title) + concat f.text_field(:title) + end + + assert_dom_equal "" + + "", + output_buffer + end + + def test_form_builder_does_not_have_form_for_method + assert_not_includes ActionView::Helpers::FormBuilder.instance_methods, :form_for + end + + def test_form_with_and_fields_for + form_with(model: @post, scope: :post, id: "create-post") do |post_form| + concat post_form.text_field(:title) + concat post_form.text_area(:body) + + concat fields_for(:parent_post, @post) { |parent_fields| + concat parent_fields.check_box(:secret) + } + end + + expected = whole_form("/posts/123", "create-post", method: "patch") do + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_and_fields_for_with_object + form_with(model: @post, scope: :post, id: "create-post") do |post_form| + concat post_form.text_field(:title) + concat post_form.text_area(:body) + + concat post_form.fields_for(@comment) { |comment_fields| + concat comment_fields.text_field(:name) + } + end + + expected = whole_form("/posts/123", "create-post", method: "patch") do + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_and_fields_for_with_non_nested_association_and_without_object + form_with(model: @post) do |f| + concat f.fields_for(:category) { |c| + concat c.text_field(:name) + } + end + + expected = whole_form("/posts/123", method: "patch") do + "" + end + + assert_dom_equal expected, output_buffer + end + + class LabelledFormBuilder < ActionView::Helpers::FormBuilder + (field_helpers - %w(hidden_field)).each do |selector| + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def #{selector}(field, *args, &proc) + (" " + super + "
").html_safe + end + RUBY_EVAL + end + end + + def test_form_with_with_labelled_builder + form_with(model: @post, builder: LabelledFormBuilder) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/posts/123", method: "patch") do + "
" + + "
" + + "
" + end + + assert_dom_equal expected, output_buffer + end + + def test_default_form_builder + old_default_form_builder, ActionView::Base.default_form_builder = + ActionView::Base.default_form_builder, LabelledFormBuilder + + form_with(model: @post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/posts/123", method: "patch") do + "
" + + "
" + + "
" + end + + assert_dom_equal expected, output_buffer + ensure + ActionView::Base.default_form_builder = old_default_form_builder + end + + def test_lazy_loading_default_form_builder + old_default_form_builder, ActionView::Base.default_form_builder = + ActionView::Base.default_form_builder, "FormWithActsLikeFormForTest::LabelledFormBuilder" + + form_with(model: @post) do |f| + concat f.text_field(:title) + end + + expected = whole_form("/posts/123", method: "patch") do + "
" + end + + assert_dom_equal expected, output_buffer + ensure + ActionView::Base.default_form_builder = old_default_form_builder + end + + def test_form_builder_override + self.default_form_builder = LabelledFormBuilder + + output_buffer = fields_for(:post, @post) do |f| + concat f.text_field(:title) + end + + expected = "
" + + assert_dom_equal expected, output_buffer + end + + def test_lazy_loading_form_builder_override + self.default_form_builder = "FormWithActsLikeFormForTest::LabelledFormBuilder" + + output_buffer = fields_for(:post, @post) do |f| + concat f.text_field(:title) + end + + expected = "
" + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_with_labelled_builder + output_buffer = fields_for(:post, @post, builder: LabelledFormBuilder) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = + "
" + + "
" + + "
" + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_labelled_builder_with_nested_fields_for_without_options_hash + klass = nil + + form_with(model: @post, builder: LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new) do |nested_fields| + klass = nested_fields.class + "" + end + end + + assert_equal LabelledFormBuilder, klass + end + + def test_form_with_with_labelled_builder_with_nested_fields_for_with_options_hash + klass = nil + + form_with(model: @post, builder: LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new, index: "foo") do |nested_fields| + klass = nested_fields.class + "" + end + end + + assert_equal LabelledFormBuilder, klass + end + + def test_form_with_with_labelled_builder_path + path = nil + + form_with(model: @post, builder: LabelledFormBuilder) do |f| + path = f.to_partial_path + "" + end + + assert_equal "labelled_form", path + end + + class LabelledFormBuilderSubclass < LabelledFormBuilder; end + + def test_form_with_with_labelled_builder_with_nested_fields_for_with_custom_builder + klass = nil + + form_with(model: @post, builder: LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new, builder: LabelledFormBuilderSubclass) do |nested_fields| + klass = nested_fields.class + "" + end + end + + assert_equal LabelledFormBuilderSubclass, klass + end + + def test_form_with_with_html_options_adds_options_to_form_tag + form_with(model: @post, html: { id: "some_form", class: "some_class", multipart: true }) do |f| end + expected = whole_form("/posts/123", "some_form", "some_class", method: "patch", multipart: "multipart/form-data") + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_string_url_option + form_with(model: @post, url: "http://www.otherdomain.com") do |f| end + + assert_dom_equal whole_form("http://www.otherdomain.com", method: "patch"), output_buffer + end + + def test_form_with_with_hash_url_option + form_with(model: @post, url: { controller: "controller", action: "action" }) do |f| end + + assert_equal "controller", @url_for_options[:controller] + assert_equal "action", @url_for_options[:action] + end + + def test_form_with_with_record_url_option + form_with(model: @post, url: @post) do |f| end + + expected = whole_form("/posts/123", method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_existing_object + form_with(model: @post) do |f| end + + expected = whole_form("/posts/123", method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_new_object + post = Post.new + post.persisted = false + def post.to_key; nil; end + + form_with(model: post) {} + + expected = whole_form("/posts") + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_existing_object_in_list + @comment.save + form_with(model: [@post, @comment]) {} + + expected = whole_form(post_comment_path(@post, @comment), method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_new_object_in_list + form_with(model: [@post, @comment]) {} + + expected = whole_form(post_comments_path(@post)) + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_existing_object_and_namespace_in_list + @comment.save + form_with(model: [:admin, @post, @comment]) {} + + expected = whole_form(admin_post_comment_path(@post, @comment), method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_new_object_and_namespace_in_list + form_with(model: [:admin, @post, @comment]) {} + + expected = whole_form(admin_post_comments_path(@post)) + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_existing_object_and_custom_url + form_with(model: @post, url: "/super_posts") do |f| end + + expected = whole_form("/super_posts", method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_default_method_as_patch + form_with(model: @post) {} + expected = whole_form("/posts/123", method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_data_attributes + form_with(model: @post, data: { behavior: "stuff" }, remote: true) {} + assert_match %r|data-behavior="stuff"|, output_buffer + assert_match %r|data-remote="true"|, output_buffer + end + + def test_fields_for_returns_block_result + output = fields_for(Post.new) { |f| "fields" } + assert_equal "fields", output + end + + def test_form_with_only_instantiates_builder_once + initialization_count = 0 + builder_class = Class.new(ActionView::Helpers::FormBuilder) do + define_method :initialize do |*args| + super(*args) + initialization_count += 1 + end + end + + form_with(model: @post, builder: builder_class) {} + assert_equal 1, initialization_count, "form builder instantiated more than once" + end + + protected + def hidden_fields(options = {}) + method = options[:method] + + if options.fetch(:enforce_utf8, true) + txt = %{} + else + txt = "" + end + + if method && !%w(get post).include?(method.to_s) + txt << %{} + end + + txt + end + + def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil) + txt = %{
} + end + + def whole_form(action = "/", id = nil, html_class = nil, **options) + contents = block_given? ? yield : "" + + method, remote, multipart = options.values_at(:method, :remote, :multipart) + + form_text(action, id, html_class, remote, multipart, method) + hidden_fields(options.slice :method, :enforce_utf8) + contents + "
" + end + + def protect_against_forgery? + false + end + + def with_locale(testing_locale = :label) + old_locale, I18n.locale = I18n.locale, testing_locale + yield + ensure + I18n.locale = old_locale + end +end From c0df7c629014e007d3e39e6cb60aaa36e381ef04 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sat, 5 Nov 2016 23:22:49 +0100 Subject: [PATCH 02/16] Code climatize. --- actionview/test/template/form_helper/form_with_test.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index 6c4a6804fe918..bd9150442d29d 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -637,9 +637,7 @@ def test_form_with_with_symbol_scope def test_form_tags_do_not_call_private_properties_on_form_object obj = Class.new do - private - - def private_property + private def private_property raise "This method should not be called." end end.new From 1e7e5cb8f21b45ce3b031b6dc56663a0a81dfa4c Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 6 Nov 2016 11:40:17 +0100 Subject: [PATCH 03/16] Add fields DSL method. Strips `_for` and requires models passed as a keyword argument. --- .../lib/action_view/helpers/form_helper.rb | 18 +- .../template/form_helper/form_with_test.rb | 282 +++++++++--------- 2 files changed, 158 insertions(+), 142 deletions(-) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index f3edfbb1daf57..021bd14bbadf5 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -785,6 +785,17 @@ def fields_for(record_name, record_object = nil, options = {}, &block) capture(builder, &block) end + # TODO: Documentation + def fields(scope = nil, model: nil, **options, &block) + # TODO: Remove when ids and classes are no longer output by default. + 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..) or you specify it explicitly. @@ -1314,7 +1325,7 @@ class FormBuilder # 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, @@ -1651,6 +1662,11 @@ def fields_for(record_name, record_object = nil, fields_options = {}, &block) @template.fields_for(record_name, record_object, fields_options, &block) end + # TODO: Documentation + def fields(scope = nil, model: nil, **options, &block) + 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..) or you specify it explicitly. diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index bd9150442d29d..0930453699883 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -570,21 +570,21 @@ def test_form_with_with_file_field_generate_multipart assert_dom_equal expected, output_buffer end - # def test_fields_for_with_file_field_generate_multipart - # Comment.send :attr_accessor, :file - # - # form_for(@post) do |f| - # concat f.fields_for(:comment, @post) { |c| - # concat c.file_field(:file) - # } - # end - # - # expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch", multipart: true) do - # "" - # end - # - # assert_dom_equal expected, output_buffer - # end + def test_fields_with_file_field_generate_multipart + Comment.send :attr_accessor, :file + + form_with(model: @post) do |f| + concat f.fields(:comment, model: @post) { |c| + concat c.file_field(:file) + } + end + + expected = whole_form("/posts/123", method: "patch", multipart: true) do + "" + end + + assert_dom_equal expected, output_buffer + end def test_form_with_with_format form_with(model: @post, format: :json, id: "edit_post_123", class: "edit_post") do |f| @@ -1030,10 +1030,10 @@ def test_submit_with_object_and_nested_lookup end end - def test_nested_fields_for + def test_nested_fields @comment.body = "Hello World" form_with(model: @post) do |f| - concat f.fields_for(@comment) { |c| + concat f.fields(model: @comment) { |c| concat c.text_field(:body) } end @@ -1045,13 +1045,13 @@ def test_nested_fields_for assert_dom_equal expected, output_buffer end - def test_deep_nested_fields_for + def test_deep_nested_fields @comment.save form_with(scope: :posts) do |f| - f.fields_for("post[]", @post) do |f2| + f.fields("post[]", model: @post) do |f2| f2.text_field(:id) @post.comments.each do |comment| - concat f2.fields_for("comment[]", comment) { |c| + concat f2.fields("comment[]", model: comment) { |c| concat c.text_field(:name) } end @@ -1065,10 +1065,10 @@ def test_deep_nested_fields_for assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_nested_collections + def test_nested_fields_with_nested_collections form_with(model: @post, scope: "post[]") do |f| concat f.text_field(:title) - concat f.fields_for("comment[]", @comment) { |c| + concat f.fields("comment[]", model: @comment) { |c| concat c.text_field(:name) } end @@ -1081,10 +1081,10 @@ def test_nested_fields_for_with_nested_collections assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_index_and_parent_fields + def test_nested_fields_with_index_and_parent_fields form_with(model: @post, index: 1) do |c| concat c.text_field(:title) - concat c.fields_for("comment", @comment, index: 1) { |r| + concat c.fields("comment", model: @comment, index: 1) { |r| concat r.text_field(:name) } end @@ -1097,9 +1097,9 @@ def test_nested_fields_for_with_index_and_parent_fields assert_dom_equal expected, output_buffer end - def test_form_with_with_index_and_nested_fields_for + def test_form_with_with_index_and_nested_fields output_buffer = form_with(model: @post, index: 1) do |f| - concat f.fields_for(:comment, @post) { |c| + concat f.fields(:comment, model: @post) { |c| concat c.text_field(:title) } end @@ -1111,9 +1111,9 @@ def test_form_with_with_index_and_nested_fields_for assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_index_on_both + def test_nested_fields_with_index_on_both form_with(model: @post, index: 1) do |f| - concat f.fields_for(:comment, @post, index: 5) { |c| + concat f.fields(:comment, model: @post, index: 5) { |c| concat c.text_field(:title) } end @@ -1125,9 +1125,9 @@ def test_nested_fields_for_with_index_on_both assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_auto_index + def test_nested_fields_with_auto_index form_with(model: @post, scope: "post[]") do |f| - concat f.fields_for(:comment, @post) { |c| + concat f.fields(:comment, model: @post) { |c| concat c.text_field(:title) } end @@ -1139,9 +1139,9 @@ def test_nested_fields_for_with_auto_index assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_index_radio_button + def test_nested_fields_with_index_radio_button form_with(model: @post) do |f| - concat f.fields_for(:comment, @post, index: 5) { |c| + concat f.fields(:comment, model: @post, index: 5) { |c| concat c.radio_button(:title, "hello") } end @@ -1153,9 +1153,9 @@ def test_nested_fields_for_with_index_radio_button assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_auto_index_on_both + def test_nested_fields_with_auto_index_on_both form_with(model: @post, scope: "post[]") do |f| - concat f.fields_for("comment[]", @post) { |c| + concat f.fields("comment[]", model: @post) { |c| concat c.text_field(:title) } end @@ -1167,15 +1167,15 @@ def test_nested_fields_for_with_auto_index_on_both assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_index_and_auto_index + def test_nested_fields_with_index_and_auto_index output_buffer = form_with(model: @post, scope: "post[]") do |f| - concat f.fields_for(:comment, @post, index: 5) { |c| + concat f.fields(:comment, model: @post, index: 5) { |c| concat c.text_field(:title) } end output_buffer << form_with(model: @post, index: 1) do |f| - concat f.fields_for("comment[]", @post) { |c| + concat f.fields("comment[]", model: @post) { |c| concat c.text_field(:title) } end @@ -1189,12 +1189,12 @@ def test_nested_fields_for_with_index_and_auto_index assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_a_new_record_on_a_nested_attributes_one_to_one_association + def test_nested_fields_with_a_new_record_on_a_nested_attributes_one_to_one_association @post.author = Author.new form_with(model: @post) do |f| concat f.text_field(:title) - concat f.fields_for(:author) { |af| + concat f.fields(:author) { |af| concat af.text_field(:name) } end @@ -1207,21 +1207,21 @@ def test_nested_fields_for_with_a_new_record_on_a_nested_attributes_one_to_one_a assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_explicitly_passed_object_on_a_nested_attributes_one_to_one_association + def test_nested_fields_with_explicitly_passed_object_on_a_nested_attributes_one_to_one_association form_with(model: @post) do |f| - f.fields_for(:author, Author.new(123)) do |af| + f.fields(:author, model: Author.new(123)) do |af| assert_not_nil af.object assert_equal 123, af.object.id end end end - def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association + def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association @post.author = Author.new(321) form_with(model: @post) do |f| concat f.text_field(:title) - concat f.fields_for(:author) { |af| + concat f.fields(:author) { |af| concat af.text_field(:name) } end @@ -1235,12 +1235,12 @@ def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_using_erb_and_inline_block + def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association_using_erb_and_inline_block @post.author = Author.new(321) form_with(model: @post) do |f| concat f.text_field(:title) - concat f.fields_for(:author) { |af| + concat f.fields(:author) { |af| af.text_field(:name) } end @@ -1254,12 +1254,12 @@ def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id + def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id @post.author = Author.new(321) form_with(model: @post) do |f| concat f.text_field(:title) - concat f.fields_for(:author, include_id: false) { |af| + concat f.fields(:author, include_id: false) { |af| af.text_field(:name) } end @@ -1272,12 +1272,12 @@ def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_inherited + def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_inherited @post.author = Author.new(321) form_with(model: @post, include_id: false) do |f| concat f.text_field(:title) - concat f.fields_for(:author) { |af| + concat f.fields(:author) { |af| af.text_field(:name) } end @@ -1290,12 +1290,12 @@ def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_override + def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_override @post.author = Author.new(321) form_with(model: @post, include_id: false) do |f| concat f.text_field(:title) - concat f.fields_for(:author, include_id: true) { |af| + concat f.fields(:author, include_id: true) { |af| af.text_field(:name) } end @@ -1309,12 +1309,12 @@ def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_existing_records_on_a_nested_attributes_one_to_one_association_with_explicit_hidden_field_placement + def test_nested_fields_with_existing_records_on_a_nested_attributes_one_to_one_association_with_explicit_hidden_field_placement @post.author = Author.new(321) form_with(model: @post) do |f| concat f.text_field(:title) - concat f.fields_for(:author) { |af| + concat f.fields(:author) { |af| concat af.hidden_field(:id) concat af.text_field(:name) } @@ -1329,13 +1329,13 @@ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_one_to_o assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association + def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association @post.comments = Array.new(2) { |id| Comment.new(id + 1) } form_with(model: @post) do |f| concat f.text_field(:title) @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| + concat f.fields(:comments, model: comment) { |cf| concat cf.text_field(:name) } end @@ -1352,17 +1352,17 @@ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collecti assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id + def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id @post.comments = Array.new(2) { |id| Comment.new(id + 1) } @post.author = Author.new(321) form_with(model: @post) do |f| concat f.text_field(:title) - concat f.fields_for(:author) { |af| + concat f.fields(:author) { |af| concat af.text_field(:name) } @post.comments.each do |comment| - concat f.fields_for(:comments, comment, include_id: false) { |cf| + concat f.fields(:comments, model: comment, include_id: false) { |cf| concat cf.text_field(:name) } end @@ -1379,17 +1379,17 @@ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collecti assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_inherited + def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_inherited @post.comments = Array.new(2) { |id| Comment.new(id + 1) } @post.author = Author.new(321) form_with(model: @post, include_id: false) do |f| concat f.text_field(:title) - concat f.fields_for(:author) { |af| + concat f.fields(:author) { |af| concat af.text_field(:name) } @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| + concat f.fields(:comments, model: comment) { |cf| concat cf.text_field(:name) } end @@ -1405,17 +1405,17 @@ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collecti assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_override + def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_override @post.comments = Array.new(2) { |id| Comment.new(id + 1) } @post.author = Author.new(321) form_with(model: @post, include_id: false) do |f| concat f.text_field(:title) - concat f.fields_for(:author, include_id: true) { |af| + concat f.fields(:author, include_id: true) { |af| concat af.text_field(:name) } @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| + concat f.fields(:comments, model: comment) { |cf| concat cf.text_field(:name) } end @@ -1432,13 +1432,13 @@ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collecti assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_using_erb_and_inline_block + def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_using_erb_and_inline_block @post.comments = Array.new(2) { |id| Comment.new(id + 1) } form_with(model: @post) do |f| concat f.text_field(:title) @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| + concat f.fields(:comments, model: comment) { |cf| cf.text_field(:name) } end @@ -1455,13 +1455,13 @@ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collecti assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_explicit_hidden_field_placement + def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_with_explicit_hidden_field_placement @post.comments = Array.new(2) { |id| Comment.new(id + 1) } form_with(model: @post) do |f| concat f.text_field(:title) @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| + concat f.fields(:comments, model: comment) { |cf| concat cf.hidden_field(:id) concat cf.text_field(:name) } @@ -1479,13 +1479,13 @@ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collecti assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_new_records_on_a_nested_attributes_collection_association + def test_nested_fields_with_new_records_on_a_nested_attributes_collection_association @post.comments = [Comment.new, Comment.new] form_with(model: @post) do |f| concat f.text_field(:title) @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| + concat f.fields(:comments, model: comment) { |cf| concat cf.text_field(:name) } end @@ -1500,13 +1500,13 @@ def test_nested_fields_for_with_new_records_on_a_nested_attributes_collection_as assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_existing_and_new_records_on_a_nested_attributes_collection_association + def test_nested_fields_with_existing_and_new_records_on_a_nested_attributes_collection_association @post.comments = [Comment.new(321), Comment.new] form_with(model: @post) do |f| concat f.text_field(:title) @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| + concat f.fields(:comments, model: comment) { |cf| concat cf.text_field(:name) } end @@ -1522,10 +1522,10 @@ def test_nested_fields_for_with_existing_and_new_records_on_a_nested_attributes_ assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_an_empty_supplied_attributes_collection + def test_nested_fields_with_an_empty_supplied_attributes_collection form_with(model: @post) do |f| concat f.text_field(:title) - f.fields_for(:comments, []) do |cf| + f.fields(:comments, model: []) do |cf| concat cf.text_field(:name) end end @@ -1537,12 +1537,12 @@ def test_nested_fields_for_with_an_empty_supplied_attributes_collection assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes_collection + def test_nested_fields_with_existing_records_on_a_supplied_nested_attributes_collection @post.comments = Array.new(2) { |id| Comment.new(id + 1) } form_with(model: @post) do |f| concat f.text_field(:title) - concat f.fields_for(:comments, @post.comments) { |cf| + concat f.fields(:comments, model: @post.comments) { |cf| concat cf.text_field(:name) } end @@ -1558,12 +1558,12 @@ def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes assert_dom_equal expected, output_buffer end - def test_nested_fields_for_arel_like + def test_nested_fields_arel_like @post.comments = ArelLike.new form_with(model: @post) do |f| concat f.text_field(:title) - concat f.fields_for(:comments, @post.comments) { |cf| + concat f.fields(:comments, model: @post.comments) { |cf| concat cf.text_field(:name) } end @@ -1585,20 +1585,20 @@ def test_nested_fields_label_translation_with_more_than_10_records params = 11.times.map { ["post.comments.body", default: [:"comment.body", ""], scope: "helpers.label"] } assert_called_with(I18n, :t, params, returns: "Write body here") do form_with(model: @post) do |f| - f.fields_for(:comments) do |cf| + f.fields(:comments) do |cf| concat cf.label(:body) end end end end - def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes_collection_different_from_record_one + def test_nested_fields_with_existing_records_on_a_supplied_nested_attributes_collection_different_from_record_one comments = Array.new(2) { |id| Comment.new(id + 1) } @post.comments = [] form_with(model: @post) do |f| concat f.text_field(:title) - concat f.fields_for(:comments, comments) { |cf| + concat f.fields(:comments, model: comments) { |cf| concat cf.text_field(:name) } end @@ -1614,13 +1614,13 @@ def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes assert_dom_equal expected, output_buffer end - def test_nested_fields_for_on_a_nested_attributes_collection_association_yields_only_builder + def test_nested_fields_on_a_nested_attributes_collection_association_yields_only_builder @post.comments = [Comment.new(321), Comment.new] yielded_comments = [] form_with(model: @post) do |f| concat f.text_field(:title) - concat f.fields_for(:comments) { |cf| + concat f.fields(:comments) { |cf| concat cf.text_field(:name) yielded_comments << cf.object } @@ -1637,11 +1637,11 @@ def test_nested_fields_for_on_a_nested_attributes_collection_association_yields_ assert_equal yielded_comments, @post.comments end - def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association + def test_nested_fields_with_child_index_option_override_on_a_nested_attributes_collection_association @post.comments = [] form_with(model: @post) do |f| - concat f.fields_for(:comments, Comment.new(321), child_index: "abc") { |cf| + concat f.fields(:comments, model: Comment.new(321), child_index: "abc") { |cf| concat cf.text_field(:name) } end @@ -1654,11 +1654,11 @@ def test_nested_fields_for_with_child_index_option_override_on_a_nested_attribut assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_child_index_as_lambda_option_override_on_a_nested_attributes_collection_association + def test_nested_fields_with_child_index_as_lambda_option_override_on_a_nested_attributes_collection_association @post.comments = [] form_with(model: @post) do |f| - concat f.fields_for(:comments, Comment.new(321), child_index: -> { "abc" }) { |cf| + concat f.fields(:comments, model: Comment.new(321), child_index: -> { "abc" }) { |cf| concat cf.text_field(:name) } end @@ -1677,11 +1677,11 @@ def to_ary end end - def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association_with_proxy + def test_nested_fields_with_child_index_option_override_on_a_nested_attributes_collection_association_with_proxy @post.comments = FakeAssociationProxy.new form_with(model: @post) do |f| - concat f.fields_for(:comments, Comment.new(321), child_index: "abc") { |cf| + concat f.fields(:comments, model: Comment.new(321), child_index: "abc") { |cf| concat cf.text_field(:name) } end @@ -1694,13 +1694,13 @@ def test_nested_fields_for_with_child_index_option_override_on_a_nested_attribut assert_dom_equal expected, output_buffer end - def test_nested_fields_for_index_method_with_existing_records_on_a_nested_attributes_collection_association + def test_nested_fields_index_method_with_existing_records_on_a_nested_attributes_collection_association @post.comments = Array.new(2) { |id| Comment.new(id + 1) } form_with(model: @post) do |f| expected = 0 @post.comments.each do |comment| - f.fields_for(:comments, comment) { |cf| + f.fields(:comments, model: comment) { |cf| assert_equal cf.index, expected expected += 1 } @@ -1708,13 +1708,13 @@ def test_nested_fields_for_index_method_with_existing_records_on_a_nested_attrib end end - def test_nested_fields_for_index_method_with_existing_and_new_records_on_a_nested_attributes_collection_association + def test_nested_fields_index_method_with_existing_and_new_records_on_a_nested_attributes_collection_association @post.comments = [Comment.new(321), Comment.new] form_with(model: @post) do |f| expected = 0 @post.comments.each do |comment| - f.fields_for(:comments, comment) { |cf| + f.fields(:comments, model: comment) { |cf| assert_equal cf.index, expected expected += 1 } @@ -1722,23 +1722,23 @@ def test_nested_fields_for_index_method_with_existing_and_new_records_on_a_neste end end - def test_nested_fields_for_index_method_with_existing_records_on_a_supplied_nested_attributes_collection + def test_nested_fields_index_method_with_existing_records_on_a_supplied_nested_attributes_collection @post.comments = Array.new(2) { |id| Comment.new(id + 1) } form_with(model: @post) do |f| expected = 0 - f.fields_for(:comments, @post.comments) { |cf| + f.fields(:comments, model: @post.comments) { |cf| assert_equal cf.index, expected expected += 1 } end end - def test_nested_fields_for_index_method_with_child_index_option_override_on_a_nested_attributes_collection_association + def test_nested_fields_index_method_with_child_index_option_override_on_a_nested_attributes_collection_association @post.comments = [] form_with(model: @post) do |f| - f.fields_for(:comments, Comment.new(321), child_index: "abc") { |cf| + f.fields(:comments, model: Comment.new(321), child_index: "abc") { |cf| assert_equal cf.index, "abc" } end @@ -1752,21 +1752,21 @@ def test_nested_fields_uses_unique_indices_for_different_collection_associations @post.tags[1].relevances = [] form_with(model: @post) do |f| - concat f.fields_for(:comments, @post.comments[0]) { |cf| + concat f.fields(:comments, model: @post.comments[0]) { |cf| concat cf.text_field(:name) - concat cf.fields_for(:relevances, CommentRelevance.new(314)) { |crf| + concat cf.fields(:relevances, model: CommentRelevance.new(314)) { |crf| concat crf.text_field(:value) } } - concat f.fields_for(:tags, @post.tags[0]) { |tf| + concat f.fields(:tags, model: @post.tags[0]) { |tf| concat tf.text_field(:value) - concat tf.fields_for(:relevances, TagRelevance.new(3141)) { |trf| + concat tf.fields(:relevances, model: TagRelevance.new(3141)) { |trf| concat trf.text_field(:value) } } - concat f.fields_for("tags", @post.tags[1]) { |tf| + concat f.fields("tags", model: @post.tags[1]) { |tf| concat tf.text_field(:value) - concat tf.fields_for(:relevances, TagRelevance.new(31415)) { |trf| + concat tf.fields(:relevances, model: TagRelevance.new(31415)) { |trf| concat trf.text_field(:value) } } @@ -1790,11 +1790,11 @@ def test_nested_fields_uses_unique_indices_for_different_collection_associations assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_hash_like_model + def test_nested_fields_with_hash_like_model @author = HashBackedAuthor.new form_with(model: @post) do |f| - concat f.fields_for(:author, @author) { |af| + concat f.fields(:author, model: @author) { |af| concat af.text_field(:name) } end @@ -1806,8 +1806,8 @@ def test_nested_fields_for_with_hash_like_model assert_dom_equal expected, output_buffer end - def test_fields_for - output_buffer = fields_for(:post, @post) do |f| + def test_fields + output_buffer = fields(:post, model: @post) do |f| concat f.text_field(:title) concat f.text_area(:body) concat f.check_box(:secret) @@ -1822,8 +1822,8 @@ def test_fields_for assert_dom_equal expected, output_buffer end - def test_fields_for_with_index - output_buffer = fields_for("post[]", @post) do |f| + def test_fields_with_index + output_buffer = fields("post[]", model: @post) do |f| concat f.text_field(:title) concat f.text_area(:body) concat f.check_box(:secret) @@ -1838,8 +1838,8 @@ def test_fields_for_with_index assert_dom_equal expected, output_buffer end - def test_fields_for_with_nil_index_option_override - output_buffer = fields_for("post[]", @post, index: nil) do |f| + def test_fields_with_nil_index_option_override + output_buffer = fields("post[]", model: @post, index: nil) do |f| concat f.text_field(:title) concat f.text_area(:body) concat f.check_box(:secret) @@ -1854,8 +1854,8 @@ def test_fields_for_with_nil_index_option_override assert_dom_equal expected, output_buffer end - def test_fields_for_with_index_option_override - output_buffer = fields_for("post[]", @post, index: "abc") do |f| + def test_fields_with_index_option_override + output_buffer = fields("post[]", model: @post, index: "abc") do |f| concat f.text_field(:title) concat f.text_area(:body) concat f.check_box(:secret) @@ -1870,8 +1870,8 @@ def test_fields_for_with_index_option_override assert_dom_equal expected, output_buffer end - def test_fields_for_without_object - output_buffer = fields_for(:post) do |f| + def test_fields_without_object + output_buffer = fields(:post) do |f| concat f.text_field(:title) concat f.text_area(:body) concat f.check_box(:secret) @@ -1886,8 +1886,8 @@ def test_fields_for_without_object assert_dom_equal expected, output_buffer end - def test_fields_for_with_only_object - output_buffer = fields_for(@post) do |f| + def test_fields_with_only_object + output_buffer = fields(model: @post) do |f| concat f.text_field(:title) concat f.text_area(:body) concat f.check_box(:secret) @@ -1902,8 +1902,8 @@ def test_fields_for_with_only_object assert_dom_equal expected, output_buffer end - def test_fields_for_object_with_bracketed_name - output_buffer = fields_for("author[post]", @post) do |f| + def test_fields_object_with_bracketed_name + output_buffer = fields("author[post]", model: @post) do |f| concat f.label(:title) concat f.text_field(:title) end @@ -1913,8 +1913,8 @@ def test_fields_for_object_with_bracketed_name output_buffer end - def test_fields_for_object_with_bracketed_name_and_index - output_buffer = fields_for("author[post]", @post, index: 1) do |f| + def test_fields_object_with_bracketed_name_and_index + output_buffer = fields("author[post]", model: @post, index: 1) do |f| concat f.label(:title) concat f.text_field(:title) end @@ -1924,16 +1924,16 @@ def test_fields_for_object_with_bracketed_name_and_index output_buffer end - def test_form_builder_does_not_have_form_for_method - assert_not_includes ActionView::Helpers::FormBuilder.instance_methods, :form_for + def test_form_builder_does_not_have_form_with_method + assert_not_includes ActionView::Helpers::FormBuilder.instance_methods, :form_with end - def test_form_with_and_fields_for + def test_form_with_and_fields form_with(model: @post, scope: :post, id: "create-post") do |post_form| concat post_form.text_field(:title) concat post_form.text_area(:body) - concat fields_for(:parent_post, @post) { |parent_fields| + concat fields(:parent_post, model: @post) { |parent_fields| concat parent_fields.check_box(:secret) } end @@ -1948,12 +1948,12 @@ def test_form_with_and_fields_for assert_dom_equal expected, output_buffer end - def test_form_with_and_fields_for_with_object + def test_form_with_and_fields_with_object form_with(model: @post, scope: :post, id: "create-post") do |post_form| concat post_form.text_field(:title) concat post_form.text_area(:body) - concat post_form.fields_for(@comment) { |comment_fields| + concat post_form.fields(model: @comment) { |comment_fields| concat comment_fields.text_field(:name) } end @@ -1967,9 +1967,9 @@ def test_form_with_and_fields_for_with_object assert_dom_equal expected, output_buffer end - def test_form_with_and_fields_for_with_non_nested_association_and_without_object + def test_form_with_and_fields_with_non_nested_association_and_without_object form_with(model: @post) do |f| - concat f.fields_for(:category) { |c| + concat f.fields(:category) { |c| concat c.text_field(:name) } end @@ -2048,7 +2048,7 @@ def test_lazy_loading_default_form_builder def test_form_builder_override self.default_form_builder = LabelledFormBuilder - output_buffer = fields_for(:post, @post) do |f| + output_buffer = fields(:post, model: @post) do |f| concat f.text_field(:title) end @@ -2060,7 +2060,7 @@ def test_form_builder_override def test_lazy_loading_form_builder_override self.default_form_builder = "FormWithActsLikeFormForTest::LabelledFormBuilder" - output_buffer = fields_for(:post, @post) do |f| + output_buffer = fields(:post, model: @post) do |f| concat f.text_field(:title) end @@ -2069,8 +2069,8 @@ def test_lazy_loading_form_builder_override assert_dom_equal expected, output_buffer end - def test_fields_for_with_labelled_builder - output_buffer = fields_for(:post, @post, builder: LabelledFormBuilder) do |f| + def test_fields_with_labelled_builder + output_buffer = fields(:post, model: @post, builder: LabelledFormBuilder) do |f| concat f.text_field(:title) concat f.text_area(:body) concat f.check_box(:secret) @@ -2084,11 +2084,11 @@ def test_fields_for_with_labelled_builder assert_dom_equal expected, output_buffer end - def test_form_with_with_labelled_builder_with_nested_fields_for_without_options_hash + def test_form_with_with_labelled_builder_with_nested_fields_without_options_hash klass = nil form_with(model: @post, builder: LabelledFormBuilder) do |f| - f.fields_for(:comments, Comment.new) do |nested_fields| + f.fields(:comments, model: Comment.new) do |nested_fields| klass = nested_fields.class "" end @@ -2097,11 +2097,11 @@ def test_form_with_with_labelled_builder_with_nested_fields_for_without_options_ assert_equal LabelledFormBuilder, klass end - def test_form_with_with_labelled_builder_with_nested_fields_for_with_options_hash + def test_form_with_with_labelled_builder_with_nested_fields_with_options_hash klass = nil form_with(model: @post, builder: LabelledFormBuilder) do |f| - f.fields_for(:comments, Comment.new, index: "foo") do |nested_fields| + f.fields(:comments, model: Comment.new, index: "foo") do |nested_fields| klass = nested_fields.class "" end @@ -2123,11 +2123,11 @@ def test_form_with_with_labelled_builder_path class LabelledFormBuilderSubclass < LabelledFormBuilder; end - def test_form_with_with_labelled_builder_with_nested_fields_for_with_custom_builder + def test_form_with_with_labelled_builder_with_nested_fields_with_custom_builder klass = nil form_with(model: @post, builder: LabelledFormBuilder) do |f| - f.fields_for(:comments, Comment.new, builder: LabelledFormBuilderSubclass) do |nested_fields| + f.fields(:comments, model: Comment.new, builder: LabelledFormBuilderSubclass) do |nested_fields| klass = nested_fields.class "" end @@ -2230,8 +2230,8 @@ def test_form_with_with_data_attributes assert_match %r|data-remote="true"|, output_buffer end - def test_fields_for_returns_block_result - output = fields_for(Post.new) { |f| "fields" } + def test_fields_returns_block_result + output = fields(model: Post.new) { |f| "fields" } assert_equal "fields", output end From a4a5945a83e611a01e44a434405d73710361e10b Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 6 Nov 2016 20:41:31 +0100 Subject: [PATCH 04/16] Document form_with. Graft the `form_for` docs: rewrite, revise and expand where needed. Also test that a `format` isn't used when an explicit URL is passed. --- .../lib/action_view/helpers/form_helper.rb | 228 ++++++++++++++++-- .../template/form_helper/form_with_test.rb | 12 + 2 files changed, 217 insertions(+), 23 deletions(-) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 021bd14bbadf5..2c63095ebcf99 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -474,47 +474,229 @@ def apply_form_for_options!(record, object, options) #:nodoc: end private :apply_form_for_options! - # Passing model: @post will 1) set scope: :post, 2) set url: url_for(@post) + # Creates a form tag based on mixing URLs, scopes, or models. # - # form_with(model: @post) do |form| - # form.text_field :title # Will reference @post.title as normal - # form.text_area :description, "Overwrite @post.description if present, if not, it will still work" + # # Using just a URL: + # form_with url: posts_path do |form| + # form.text_field :title + # end + # # => + #
+ # + #
# - # form.submit + # # Adding a scope prefixes the input field names: + # form_with scope: :post, url: posts_path do |form| + # form.text_field :title # end + # # => + #
+ # + #
# - # form_with(scope: :post, url: posts_path) do |form| - # form.text_field :title # post[title] - # form.text_area :description, "Overwrite @post.description or ignore if it's not present" + # # Using a model infers both the URL and scope: + # form_with model: Post.new do |form| + # form.text_field :title + # end + # # => + #
+ # + #
# - # form.submit + # # An existing model makes an update form and fills out field values: + # form_with model: Post.first do |form| + # form.text_field :title # end # # => - #
- # - # + # + # + # + #
# - # - # + # The parameters in the forms are accessible in controlleres according to + # their name nesting. So inputs named +title+ and post[title] are + # accessible as params[:title] and params[:post][:title] + # respectively. + # + # 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 + # + # * :url - 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 :scope is passed without a :url the + # form just submits to the current URL. + # * :method - 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 _method is added to + # simulate the verb over post. + # * :format - The format of the route post submits to. + # Useful when submitting to another resource type, like :json. + # Skipped if a :url is passed. + # * :scope - The scope to prefix input field names with and + # thereby how the submitted parameters are grouped in controllers. + # * :model - A model object to infer the :url and + # :scope 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 :scope or :url to override the defaults. + # E.g. turn params[:post] into params[:article]. + # * :authenticity_token - Authenticity token to use in the form. + # Override with a custom authenticity token or pass false 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 + # config.action_view.embed_authenticity_token_in_remote_forms = false. + # This is helpful when fragment-caching the form. Remote forms + # get the authenticity token from the meta tag, so embedding is + # unnecessary unless you support browsers without JavaScript. + # * :remote - If set to true, will allow the Unobtrusive + # JavaScript drivers to control the submit behavior. By default this + # behavior is an XHR submit. + # * :enforce_utf8 - If set to false, a hidden input with name + # utf8 is not output. Default is true. + # * :builder - Override the object used to build the form. + # * :id - Optional HTML id attribute. + # * :class - Optional HTML class attribute. + # * :data - Optional HTML data attributes. + # * :html - 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 @document = Document.find(params[:id]). + # + # === 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. + # + # === Unobtrusive JavaScript # - # + # Specifying: + # + # remote: true + # + # allows the unobtrusive JavaScript drivers to modify its behavior. + # Which by default is backgrounded XMLHttpRequest submit, but ultimately + # the behavior is up to the JavaScript driver. + # + # Sets a data-remote="true" attribute on the form tag. + # + # === 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_with(url: different_path, class: 'something', id: 'specific') do |form| - # form.text_field :title, 'This is the value of the title' + # === 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. # - # form.text_area :description, class: 'No value has been supplied here' + # 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.fields(:permission) do |fields| - # # on/off instead of positional parameters for setting values - # fields.check_box :admin, on: 'yes', off: 'no' + # form_with(model: @post) do |form| + # form.fields(:comments, include_id: false) do |fields| + # ... # end + # end + # + # === Customized form builders # - # form.select :category, Post::CATEGORIES, blank: 'None' - # form.select :author_id, Person.all.collect { |p| [ p.name, p.id ] }, blank: 'Pick someone' + # 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 people/_labelling_form and the local + # variable referencing the form builder is called + # labelling_form. + # + # 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_for(**options, &block) + # form_with(**options.merge(builder: LabellingFormBuilder), &block) + # end def form_with(model: nil, scope: nil, url: nil, format: nil, html: {}, **options) if model url ||= polymorphic_path(model, format: format) diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index 0930453699883..4561fe16f1557 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -598,6 +598,18 @@ def test_form_with_with_format assert_dom_equal expected, output_buffer end + def test_form_with_with_format_and_url + form_with(model: @post, format: :json, url: "/") do |f| + concat f.label(:title) + end + + expected = whole_form("/", method: "patch") do + "" + end + + assert_dom_equal expected, output_buffer + end + def test_form_with_with_model_using_relative_model_naming blog_post = Blog::Post.new("And his name will be forty and four.", 44) From 31a9608395c8e1769c78b2edccdaf58b723a7490 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 6 Nov 2016 21:01:44 +0100 Subject: [PATCH 05/16] Enable remote by default. Brand new world! Forms submit via XHRs by default, woah. --- .../lib/action_view/helpers/form_helper.rb | 34 ++++++++----------- .../template/form_helper/form_with_test.rb | 31 +++++++---------- 2 files changed, 26 insertions(+), 39 deletions(-) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 2c63095ebcf99..ef114b3b212ed 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -481,7 +481,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # form.text_field :title # end # # => - #
+ # # #
# @@ -490,7 +490,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # form.text_field :title # end # # => - #
+ # # #
# @@ -499,7 +499,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # form.text_field :title # end # # => - #
+ # # #
# @@ -508,7 +508,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # form.text_field :title # end # # => - #
+ # # # #
@@ -518,6 +518,11 @@ def apply_form_for_options!(record, object, options) #:nodoc: # accessible as params[:title] and params[:post][:title] # respectively. # + # By default +form_with+ attaches the data-remote attribute + # submitting the form via an XMLHTTPRequest in the background if an + # an Unobtrusive JavaScript driver, like jquery-ujs, is used. See the + # :remote 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 @@ -556,9 +561,9 @@ def apply_form_for_options!(record, object, options) #:nodoc: # This is helpful when fragment-caching the form. Remote forms # get the authenticity token from the meta tag, so embedding is # unnecessary unless you support browsers without JavaScript. - # * :remote - If set to true, will allow the Unobtrusive - # JavaScript drivers to control the submit behavior. By default this - # behavior is an XHR submit. + # * :remote - Set to true to allow the Unobtrusive + # JavaScript drivers to control the submit behavior, defaulting to + # to an XHR submit. Disable with remote: false. # * :enforce_utf8 - If set to false, a hidden input with name # utf8 is not output. Default is true. # * :builder - Override the object used to build the form. @@ -621,18 +626,6 @@ def apply_form_for_options!(record, object, options) #:nodoc: # 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. # - # === Unobtrusive JavaScript - # - # Specifying: - # - # remote: true - # - # allows the unobtrusive JavaScript drivers to modify its behavior. - # Which by default is backgrounded XMLHttpRequest submit, but ultimately - # the behavior is up to the JavaScript driver. - # - # Sets a data-remote="true" attribute on the form tag. - # # === Setting HTML options # # You can set data attributes directly in a data hash, but HTML options @@ -697,7 +690,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # def labelled_form_for(**options, &block) # form_with(**options.merge(builder: LabellingFormBuilder), &block) # end - def form_with(model: nil, scope: nil, url: nil, format: nil, html: {}, **options) + def form_with(model: nil, scope: nil, url: nil, format: nil, html: {}, remote: true, **options) if model url ||= polymorphic_path(model, format: format) @@ -707,6 +700,7 @@ def form_with(model: nil, scope: nil, url: nil, format: nil, html: {}, **options html_options = html.merge(options.except(:index, :include_id, :builder)) html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted? + html_options[:remote] = remote unless html_options.key?(:remote) if block_given? builder = instantiate_builder(scope, model, options) diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index 4561fe16f1557..13474d0e8beca 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -27,8 +27,8 @@ def hidden_fields(options = {}) end end - def form_text(action = "http://www.example.com", options = {}) - remote, enctype, html_class, id, method = options.values_at(:remote, :enctype, :html_class, :id, :method) + def form_text(action = "http://www.example.com", remote: true, **options) + enctype, html_class, id, method = options.values_at(:enctype, :html_class, :id, :method) method = method.to_s == "get" ? "get" : "post" @@ -86,17 +86,10 @@ def test_form_with_with_method_delete assert_dom_equal expected, actual end - def test_form_with_with_remote - actual = form_with(remote: true) - - expected = whole_form("http://www.example.com", remote: true) - assert_dom_equal expected, actual - end - def test_form_with_with_remote_false actual = form_with(remote: false) - expected = whole_form + expected = whole_form("http://www.example.com", remote: false) assert_dom_equal expected, actual end @@ -707,14 +700,14 @@ def test_form_with_with_search_field assert_dom_equal expected, output_buffer end - def test_form_with_with_remote - form_with(model: @post, url: "/", remote: true, id: "create-post", method: :patch) do |f| + def test_form_with_enables_remote_by_default + form_with(model: @post, url: "/", id: "create-post", method: :patch) do |f| concat f.text_field(:title) concat f.text_area(:body) concat f.check_box(:secret) end - expected = whole_form("/", "create-post", method: "patch", remote: true) do + expected = whole_form("/", "create-post", method: "patch") do "" + "" + "" + @@ -748,14 +741,14 @@ def test_form_with_enforce_utf8_false assert_dom_equal expected, output_buffer end - def test_form_with_with_remote_in_html - form_with(model: @post, url: "/", html: { remote: true, id: "create-post", method: :patch }) do |f| + def test_form_with_disable_remote_in_html + form_with(model: @post, url: "/", html: { remote: false, id: "create-post", method: :patch }) do |f| concat f.text_field(:title) concat f.text_area(:body) concat f.check_box(:secret) end - expected = whole_form("/", "create-post", method: "patch", remote: true) do + expected = whole_form("/", "create-post", method: "patch", remote: false) do "" + "" + "" + @@ -2237,7 +2230,7 @@ def test_form_with_with_default_method_as_patch end def test_form_with_with_data_attributes - form_with(model: @post, data: { behavior: "stuff" }, remote: true) {} + form_with(model: @post, data: { behavior: "stuff" }) {} assert_match %r|data-behavior="stuff"|, output_buffer assert_match %r|data-remote="true"|, output_buffer end @@ -2287,10 +2280,10 @@ def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart txt << %{ method="#{method}">} end - def whole_form(action = "/", id = nil, html_class = nil, **options) + def whole_form(action = "/", id = nil, html_class = nil, remote: true, **options) contents = block_given? ? yield : "" - method, remote, multipart = options.values_at(:method, :remote, :multipart) + method, multipart = options.values_at(:method, :multipart) form_text(action, id, html_class, remote, multipart, method) + hidden_fields(options.slice :method, :enforce_utf8) + contents + "" end From 3a55155c2832dcd537a1fe9254cd4b2c3ad32aa4 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Tue, 8 Nov 2016 21:44:40 +0100 Subject: [PATCH 06/16] Readd commented out tests. Gives us something to revise when we're redoing the form options helpers. Also deletes the needless tests for the unsupported namespace option. --- .../template/form_helper/form_with_test.rb | 491 +++++++----------- 1 file changed, 184 insertions(+), 307 deletions(-) diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index 13474d0e8beca..94154d21e4dd9 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -279,13 +279,6 @@ def url_for(object) super end - # def test_form_with_requires_block - # error = assert_raises(ArgumentError) do - # form_for(@post, html: { id: "create-post" }) - # end - # assert_equal "Missing block", error.message - # end - def test_form_with_requires_arguments error = assert_raises(ArgumentError) do form_for(nil, html: { id: "create-post" }) do @@ -327,227 +320,190 @@ def test_form_with assert_dom_equal expected, output_buffer end - # def test_form_with_with_collection_radio_buttons - # post = Post.new - # def post.active; false; end - # form_for(post) do |f| - # concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) - # end - # - # expected = whole_form("/posts", "new_post", "new_post") do - # "" + - # "" + - # "" + - # "" + - # "" - # end - # - # assert_dom_equal expected, output_buffer - # end - - # def test_form_with_with_collection_radio_buttons_with_custom_builder_block - # post = Post.new - # def post.active; false; end - # - # form_for(post) do |f| - # rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b| - # b.label { b.radio_button + b.text } - # end - # concat rendered_radio_buttons - # end - # - # expected = whole_form("/posts", "new_post", "new_post") do - # "" + - # "" + - # "" - # end - # - # assert_dom_equal expected, output_buffer - # end - - # def test_form_with_with_collection_radio_buttons_with_custom_builder_block_does_not_leak_the_template - # post = Post.new - # def post.active; false; end - # def post.id; 1; end - # - # form_for(post) do |f| - # rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b| - # b.label { b.radio_button + b.text } - # end - # concat rendered_radio_buttons - # concat f.hidden_field :id - # end - # - # expected = whole_form("/posts", "new_post_1", "new_post") do - # "" + - # "" + - # "" + - # "" - # end - # - # assert_dom_equal expected, output_buffer - # end - - # def test_form_with_namespace_and_with_collection_radio_buttons - # post = Post.new - # def post.active; false; end - # - # form_for(post, namespace: "foo") do |f| - # concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) - # end - # - # expected = whole_form("/posts", "foo_new_post", "new_post") do - # "" + - # "" + - # "" + - # "" + - # "" - # end - # - # assert_dom_equal expected, output_buffer - # end - - # def test_form_with_index_and_with_collection_radio_buttons - # post = Post.new - # def post.active; false; end - # - # form_for(post, index: "1") do |f| - # concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) - # end - # - # expected = whole_form("/posts", "new_post", "new_post") do - # "" + - # "" + - # "" + - # "" + - # "" - # end - # - # assert_dom_equal expected, output_buffer - # end - - # def test_form_with_with_collection_check_boxes - # post = Post.new - # def post.tag_ids; [1, 3]; end - # collection = (1..3).map { |i| [i, "Tag #{i}"] } - # form_for(post) do |f| - # concat f.collection_check_boxes(:tag_ids, collection, :first, :last) - # end - # - # expected = whole_form("/posts", "new_post", "new_post") do - # "" + - # "" + - # "" + - # "" + - # "" + - # "" + - # "" - # end - # - # assert_dom_equal expected, output_buffer - # end - # - # def test_form_with_with_collection_check_boxes_with_custom_builder_block - # post = Post.new - # def post.tag_ids; [1, 3]; end - # collection = (1..3).map { |i| [i, "Tag #{i}"] } - # form_for(post) do |f| - # rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b| - # b.label { b.check_box + b.text } - # end - # concat rendered_check_boxes - # end - # - # expected = whole_form("/posts", "new_post", "new_post") do - # "" + - # "" + - # "" + - # "" - # end - # - # assert_dom_equal expected, output_buffer - # end - # - # def test_form_with_with_collection_check_boxes_with_custom_builder_block_does_not_leak_the_template - # post = Post.new - # def post.tag_ids; [1, 3]; end - # def post.id; 1; end - # collection = (1..3).map { |i| [i, "Tag #{i}"] } - # - # form_for(post) do |f| - # rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b| - # b.label { b.check_box + b.text } - # end - # concat rendered_check_boxes - # concat f.hidden_field :id - # end - # - # expected = whole_form("/posts", "new_post_1", "new_post") do - # "" + - # "" + - # "" + - # "" + - # "" - # end - # - # assert_dom_equal expected, output_buffer - # end - # - # def test_form_with_namespace_and_with_collection_check_boxes - # post = Post.new - # def post.tag_ids; [1]; end - # collection = [[1, "Tag 1"]] - # - # form_for(post, namespace: "foo") do |f| - # concat f.collection_check_boxes(:tag_ids, collection, :first, :last) - # end - # - # expected = whole_form("/posts", "foo_new_post", "new_post") do - # "" + - # "" + - # "" - # end - # - # assert_dom_equal expected, output_buffer - # end - # - # def test_form_with_index_and_with_collection_check_boxes - # post = Post.new - # def post.tag_ids; [1]; end - # collection = [[1, "Tag 1"]] - # - # form_for(post, index: "1") do |f| - # concat f.collection_check_boxes(:tag_ids, collection, :first, :last) - # end - # - # expected = whole_form("/posts", "new_post", "new_post") do - # "" + - # "" + - # "" - # end - # - # assert_dom_equal expected, output_buffer - # end + def test_form_with_with_collection_radio_buttons + post = Post.new + def post.active; false; end + form_with(model: post) do |f| + concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) + end + + expected = whole_form("/posts") do + "" + + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_collection_radio_buttons_with_custom_builder_block + post = Post.new + def post.active; false; end + + form_with(model: post) do |f| + rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b| + b.label { b.radio_button + b.text } + end + concat rendered_radio_buttons + end + + expected = whole_form("/posts") do + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_collection_radio_buttons_with_custom_builder_block_does_not_leak_the_template + post = Post.new + def post.active; false; end + def post.id; 1; end + + form_with(model: post) do |f| + rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b| + b.label { b.radio_button + b.text } + end + concat rendered_radio_buttons + concat f.hidden_field :id + end + + expected = whole_form("/posts") do + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_index_and_with_collection_radio_buttons + post = Post.new + def post.active; false; end + + form_with(model: post, index: "1") do |f| + concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) + end + + expected = whole_form("/posts") do + "" + + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_collection_check_boxes + post = Post.new + def post.tag_ids; [1, 3]; end + collection = (1..3).map { |i| [i, "Tag #{i}"] } + form_with(model: post) do |f| + concat f.collection_check_boxes(:tag_ids, collection, :first, :last) + end + + expected = whole_form("/posts") do + "" + + "" + + "" + + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_collection_check_boxes_with_custom_builder_block + post = Post.new + def post.tag_ids; [1, 3]; end + collection = (1..3).map { |i| [i, "Tag #{i}"] } + form_with(model: post) do |f| + rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b| + b.label { b.check_box + b.text } + end + concat rendered_check_boxes + end + + expected = whole_form("/posts") do + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_with_collection_check_boxes_with_custom_builder_block_does_not_leak_the_template + post = Post.new + def post.tag_ids; [1, 3]; end + def post.id; 1; end + collection = (1..3).map { |i| [i, "Tag #{i}"] } + + form_with(model: post) do |f| + rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b| + b.label { b.check_box + b.text } + end + concat rendered_check_boxes + concat f.hidden_field :id + end + + expected = whole_form("/posts") do + "" + + "" + + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_with_index_and_with_collection_check_boxes + post = Post.new + def post.tag_ids; [1]; end + collection = [[1, "Tag 1"]] + + form_with(model: post, index: "1") do |f| + concat f.collection_check_boxes(:tag_ids, collection, :first, :last) + end + + expected = whole_form("/posts") do + "" + + "" + + "" + end + + assert_dom_equal expected, output_buffer + end def test_form_with_with_file_field_generate_multipart Post.send :attr_accessor, :file @@ -897,85 +853,6 @@ def test_form_with_with_namespace assert_dom_equal expected, output_buffer end - # def test_form_with_with_namespace_with_date_select - # form_for(@post, namespace: "namespace") do |f| - # concat f.date_select(:written_on) - # end - # - # assert_select "select#namespace_post_written_on_1i" - # end - # - # def test_form_with_with_namespace_with_label - # form_for(@post, namespace: "namespace") do |f| - # concat f.label(:title) - # concat f.text_field(:title) - # end - # - # expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do - # "" + - # "" - # end - # - # assert_dom_equal expected, output_buffer - # end - # - # def test_form_with_with_namespace_and_as_option - # form_for(@post, namespace: "namespace", as: "custom_name") do |f| - # concat f.text_field(:title) - # end - # - # expected = whole_form("/posts/123", "namespace_edit_custom_name", "edit_custom_name", method: "patch") do - # "" - # end - # - # assert_dom_equal expected, output_buffer - # end - # - # def test_two_form_for_with_namespace - # form_for(@post, namespace: "namespace_1") do |f| - # concat f.label(:title) - # concat f.text_field(:title) - # end - # - # expected_1 = whole_form("/posts/123", "namespace_1_edit_post_123", "edit_post", method: "patch") do - # "" + - # "" - # end - # - # assert_dom_equal expected_1, output_buffer - # - # form_for(@post, namespace: "namespace_2") do |f| - # concat f.label(:title) - # concat f.text_field(:title) - # end - # - # expected_2 = whole_form("/posts/123", "namespace_2_edit_post_123", "edit_post", method: "patch") do - # "" + - # "" - # end - # - # assert_dom_equal expected_2, output_buffer - # end - - # def test_fields_for_with_namespace - # @comment.body = "Hello World" - # form_for(@post, namespace: "namespace") do |f| - # concat f.text_field(:title) - # concat f.text_area(:body) - # concat f.fields_for(@comment) { |c| - # concat c.text_field(:body) - # } - # end - # - # expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do - # "" + - # "" + - # "" - # end - # - # assert_dom_equal expected, output_buffer - # end - def test_submit_with_object_as_new_record_and_locale_strings with_locale :submit do @post.persisted = false From b714aa8728246cb5a44cf765c74ec610ec71cda2 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sat, 12 Nov 2016 22:53:46 +0100 Subject: [PATCH 07/16] [ci skip] Document the fields helpers. Treat both the FormBuilder and FormHelper. --- .../lib/action_view/helpers/form_helper.rb | 440 +++++++++++++++++- 1 file changed, 438 insertions(+), 2 deletions(-) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index ef114b3b212ed..c65aa7e41db54 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -961,7 +961,225 @@ def fields_for(record_name, record_object = nil, options = {}, &block) capture(builder, &block) end - # TODO: Documentation + # Scopes input fields with either an explicit scope or model. + # Like +form_with+ does with :scope or :model, + # 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 + # # => + # + # # Using a model infers the scope and assigns field values: + # fields model: Comment.new(body: "full bodied") do |fields| + # fields.text_field :body + # end + # # => + # + # + # # 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 :model. + # + # === 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. + # + # === Nested Attributes Examples + # + # When the current scope's object has a nested attribute writer — + # most likely through +accepts_nested_attributes_for+ — for an + # attribute, +fields+ yields a new scope for that attribute. Useful when + # setting attributes on a parent and its associations in one go. + # + # Writers are considered a nested attributes setter if they're of the + # *_attributes= form, e.g. address_attributes=. + # + # Depending on the association's reader method return value a different + # form builder is yielded. For single object returns a one-to-one builder, + # while a one-to-many builder for array returns. + # + # ==== One-to-one + # + # Consider a Person class which returns a _single_ Address from the + # address reader method and responds to the + # address_attributes= writer method: + # + # class Person + # def address + # @address + # end + # + # def address_attributes=(attributes) + # # Process the attributes hash + # end + # end + # + # This model can now be used with a nested +fields+, like so: + # + # form_with model: @person do |person_form| + # ... + # <%= person_form.fields_for :address do |address_fields| %> + # Street : <%= address_fields.text_field :street %> + # Zip code: <%= address_fields.text_field :zip_code %> + # <% end %> + # ... + # <% end %> + # + # When address is already an association on a Person you can use + # +accepts_nested_attributes_for+ to define the writer method for you: + # + # class Person < ActiveRecord::Base + # has_one :address + # accepts_nested_attributes_for :address + # end + # + # To destroy the associated model through the form, you have + # to enable it using :allow_destroy: + # + # class Person < ActiveRecord::Base + # has_one :address + # accepts_nested_attributes_for :address, allow_destroy: true + # end + # + # Now, when you use a form element with the _destroy parameter, + # with a value that evaluates to +true+, the associated model is destroyed + # (eg. 1, '1', true, or 'true'): + # + # <%= form_with model: @person do |person_form| %> + # ... + # <%= person_form.fields :address do |address_fields| %> + # ... + # Delete: <%= address_fields.check_box :_destroy %> + # <% end %> + # ... + # <% end %> + # + # ==== One-to-many + # + # Consider a Person class which returns an _array_ of Project instances + # from the projects reader method and responds to the + # projects_attributes= writer method: + # + # class Person + # def projects + # [@project1, @project2] + # end + # + # def projects_attributes=(attributes) + # # Process the attributes hash + # end + # end + # + # Note that the projects_attributes= writer method is in fact + # required for +fields+ to correctly identify :projects as a + # collection, and the correct indices to be set in the form markup. + # + # When projects is already an association on Person you can use + # +accepts_nested_attributes_for+ to define the writer method for you: + # + # class Person < ActiveRecord::Base + # has_many :projects + # accepts_nested_attributes_for :projects + # end + # + # This model can now be used with a nested +fields+. The block given to + # the nested +fields+ call will be repeated for each instance in the + # collection: + # + # <%= form_with model: @person do |person_form| %> + # ... + # <%= person_form.fields :projects do |project_fields| %> + # <% if project_fields.object.active? %> + # Name: <%= project_fields.text_field :name %> + # <% end %> + # <% end %> + # ... + # <% end %> + # + # It's also possible to specify the instance to be used: + # + # <%= form_with model: @person do |person_form| %> + # ... + # <% @person.projects.each do |project| %> + # <% if project.active? %> + # <%= person_form.fields :projects, model: project do |project_fields| %> + # Name: <%= project_fields.text_field :name %> + # <% end %> + # <% end %> + # <% end %> + # ... + # <% end %> + # + # Or a collection to be used: + # + # <%= form_with model: @person do |person_form| %> + # ... + # <%= person_form.fields :projects, model: @active_projects do |project_fields| %> + # Name: <%= project_fields.text_field :name %> + # <% end %> + # ... + # <% end %> + # + # To destroy any of the associated models through the + # form, you have to enable it using :allow_destroy: + # + # class Person < ActiveRecord::Base + # has_many :projects + # accepts_nested_attributes_for :projects, allow_destroy: true + # end + # + # This will allow you to specify which models to destroy in the + # attributes hash by adding a form element for the _destroy + # parameter with a value that evaluates to +true+ + # (eg. 1, '1', true, or 'true'): + # + # <%= form_with model: @person do |person_form| %> + # ... + # <%= person_form.fields :projects do |project_fields| %> + # Delete: <%= project_fields.check_box :_destroy %> + # <% end %> + # ... + # <% end %> + # + # When collections is used you might want to know the index of each + # object into the array. Use the FormBuilder's index method: + # + # <%= form_with model: @person do |person_form| %> + # ... + # <%= person_form.fields :projects do |project_fields| %> + # Project #<%= project_fields.index %> + # ... + # <% end %> + # ... + # <% end %> + # + # Note +fields+ automatically generates a hidden field to store the record + # ID. For circumstances where this is not needed pass + # include_id: false to skip it. def fields(scope = nil, model: nil, **options, &block) # TODO: Remove when ids and classes are no longer output by default. if model @@ -1838,7 +2056,225 @@ def fields_for(record_name, record_object = nil, fields_options = {}, &block) @template.fields_for(record_name, record_object, fields_options, &block) end - # TODO: Documentation + # Scopes input fields with either an explicit scope or model. + # Like +form_with+ does with :scope or :model, + # 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 + # # => + # + # # Using a model infers the scope and assigns field values: + # fields model: Comment.new(body: "full bodied") do |fields| + # fields.text_field :body + # end + # # => + # + # + # # 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 :model. + # + # === 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. + # + # === Nested Attributes Examples + # + # When the current scope's object has a nested attribute writer — + # most likely through +accepts_nested_attributes_for+ — for an + # attribute, +fields+ yields a new scope for that attribute. Useful when + # setting attributes on a parent and its associations in one go. + # + # Writers are considered a nested attributes setter if they're of the + # *_attributes= form, e.g. address_attributes=. + # + # Depending on the association's reader method return value a different + # form builder is yielded. For single object returns a one-to-one builder, + # while a one-to-many builder for array returns. + # + # ==== One-to-one + # + # Consider a Person class which returns a _single_ Address from the + # address reader method and responds to the + # address_attributes= writer method: + # + # class Person + # def address + # @address + # end + # + # def address_attributes=(attributes) + # # Process the attributes hash + # end + # end + # + # This model can now be used with a nested +fields+, like so: + # + # form_with model: @person do |person_form| + # ... + # <%= person_form.fields_for :address do |address_fields| %> + # Street : <%= address_fields.text_field :street %> + # Zip code: <%= address_fields.text_field :zip_code %> + # <% end %> + # ... + # <% end %> + # + # When address is already an association on a Person you can use + # +accepts_nested_attributes_for+ to define the writer method for you: + # + # class Person < ActiveRecord::Base + # has_one :address + # accepts_nested_attributes_for :address + # end + # + # To destroy the associated model through the form, you have + # to enable it using :allow_destroy: + # + # class Person < ActiveRecord::Base + # has_one :address + # accepts_nested_attributes_for :address, allow_destroy: true + # end + # + # Now, when you use a form element with the _destroy parameter, + # with a value that evaluates to +true+, the associated model is destroyed + # (eg. 1, '1', true, or 'true'): + # + # <%= form_with model: @person do |person_form| %> + # ... + # <%= person_form.fields :address do |address_fields| %> + # ... + # Delete: <%= address_fields.check_box :_destroy %> + # <% end %> + # ... + # <% end %> + # + # ==== One-to-many + # + # Consider a Person class which returns an _array_ of Project instances + # from the projects reader method and responds to the + # projects_attributes= writer method: + # + # class Person + # def projects + # [@project1, @project2] + # end + # + # def projects_attributes=(attributes) + # # Process the attributes hash + # end + # end + # + # Note that the projects_attributes= writer method is in fact + # required for +fields+ to correctly identify :projects as a + # collection, and the correct indices to be set in the form markup. + # + # When projects is already an association on Person you can use + # +accepts_nested_attributes_for+ to define the writer method for you: + # + # class Person < ActiveRecord::Base + # has_many :projects + # accepts_nested_attributes_for :projects + # end + # + # This model can now be used with a nested +fields+. The block given to + # the nested +fields+ call will be repeated for each instance in the + # collection: + # + # <%= form_with model: @person do |person_form| %> + # ... + # <%= person_form.fields :projects do |project_fields| %> + # <% if project_fields.object.active? %> + # Name: <%= project_fields.text_field :name %> + # <% end %> + # <% end %> + # ... + # <% end %> + # + # It's also possible to specify the instance to be used: + # + # <%= form_with model: @person do |person_form| %> + # ... + # <% @person.projects.each do |project| %> + # <% if project.active? %> + # <%= person_form.fields :projects, model: project do |project_fields| %> + # Name: <%= project_fields.text_field :name %> + # <% end %> + # <% end %> + # <% end %> + # ... + # <% end %> + # + # Or a collection to be used: + # + # <%= form_with model: @person do |person_form| %> + # ... + # <%= person_form.fields :projects, model: @active_projects do |project_fields| %> + # Name: <%= project_fields.text_field :name %> + # <% end %> + # ... + # <% end %> + # + # To destroy any of the associated models through the + # form, you have to enable it using :allow_destroy: + # + # class Person < ActiveRecord::Base + # has_many :projects + # accepts_nested_attributes_for :projects, allow_destroy: true + # end + # + # This will allow you to specify which models to destroy in the + # attributes hash by adding a form element for the _destroy + # parameter with a value that evaluates to +true+ + # (eg. 1, '1', true, or 'true'): + # + # <%= form_with model: @person do |person_form| %> + # ... + # <%= person_form.fields :projects do |project_fields| %> + # Delete: <%= project_fields.check_box :_destroy %> + # <% end %> + # ... + # <% end %> + # + # When collections is used you might want to know the index of each + # object into the array. Use the FormBuilder's index method: + # + # <%= form_with model: @person do |person_form| %> + # ... + # <%= person_form.fields :projects do |project_fields| %> + # Project #<%= project_fields.index %> + # ... + # <% end %> + # ... + # <% end %> + # + # Note +fields+ automatically generates a hidden field to store the record + # ID. For circumstances where this is not needed pass + # include_id: false to skip it. def fields(scope = nil, model: nil, **options, &block) fields_for(scope || model, model, **options, &block) end From e3eb691aff7bc05879d64c307e58c53d6f4de2fd Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Mon, 14 Nov 2016 20:51:39 +0100 Subject: [PATCH 08/16] Link to `fields` helper method docs. --- .../lib/action_view/helpers/form_helper.rb | 220 +----------------- 1 file changed, 1 insertion(+), 219 deletions(-) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index c65aa7e41db54..94251868a16d3 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -2056,225 +2056,7 @@ def fields_for(record_name, record_object = nil, fields_options = {}, &block) @template.fields_for(record_name, record_object, fields_options, &block) end - # Scopes input fields with either an explicit scope or model. - # Like +form_with+ does with :scope or :model, - # 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 - # # => - # - # # Using a model infers the scope and assigns field values: - # fields model: Comment.new(body: "full bodied") do |fields| - # fields.text_field :body - # end - # # => - # - # - # # 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 :model. - # - # === 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. - # - # === Nested Attributes Examples - # - # When the current scope's object has a nested attribute writer — - # most likely through +accepts_nested_attributes_for+ — for an - # attribute, +fields+ yields a new scope for that attribute. Useful when - # setting attributes on a parent and its associations in one go. - # - # Writers are considered a nested attributes setter if they're of the - # *_attributes= form, e.g. address_attributes=. - # - # Depending on the association's reader method return value a different - # form builder is yielded. For single object returns a one-to-one builder, - # while a one-to-many builder for array returns. - # - # ==== One-to-one - # - # Consider a Person class which returns a _single_ Address from the - # address reader method and responds to the - # address_attributes= writer method: - # - # class Person - # def address - # @address - # end - # - # def address_attributes=(attributes) - # # Process the attributes hash - # end - # end - # - # This model can now be used with a nested +fields+, like so: - # - # form_with model: @person do |person_form| - # ... - # <%= person_form.fields_for :address do |address_fields| %> - # Street : <%= address_fields.text_field :street %> - # Zip code: <%= address_fields.text_field :zip_code %> - # <% end %> - # ... - # <% end %> - # - # When address is already an association on a Person you can use - # +accepts_nested_attributes_for+ to define the writer method for you: - # - # class Person < ActiveRecord::Base - # has_one :address - # accepts_nested_attributes_for :address - # end - # - # To destroy the associated model through the form, you have - # to enable it using :allow_destroy: - # - # class Person < ActiveRecord::Base - # has_one :address - # accepts_nested_attributes_for :address, allow_destroy: true - # end - # - # Now, when you use a form element with the _destroy parameter, - # with a value that evaluates to +true+, the associated model is destroyed - # (eg. 1, '1', true, or 'true'): - # - # <%= form_with model: @person do |person_form| %> - # ... - # <%= person_form.fields :address do |address_fields| %> - # ... - # Delete: <%= address_fields.check_box :_destroy %> - # <% end %> - # ... - # <% end %> - # - # ==== One-to-many - # - # Consider a Person class which returns an _array_ of Project instances - # from the projects reader method and responds to the - # projects_attributes= writer method: - # - # class Person - # def projects - # [@project1, @project2] - # end - # - # def projects_attributes=(attributes) - # # Process the attributes hash - # end - # end - # - # Note that the projects_attributes= writer method is in fact - # required for +fields+ to correctly identify :projects as a - # collection, and the correct indices to be set in the form markup. - # - # When projects is already an association on Person you can use - # +accepts_nested_attributes_for+ to define the writer method for you: - # - # class Person < ActiveRecord::Base - # has_many :projects - # accepts_nested_attributes_for :projects - # end - # - # This model can now be used with a nested +fields+. The block given to - # the nested +fields+ call will be repeated for each instance in the - # collection: - # - # <%= form_with model: @person do |person_form| %> - # ... - # <%= person_form.fields :projects do |project_fields| %> - # <% if project_fields.object.active? %> - # Name: <%= project_fields.text_field :name %> - # <% end %> - # <% end %> - # ... - # <% end %> - # - # It's also possible to specify the instance to be used: - # - # <%= form_with model: @person do |person_form| %> - # ... - # <% @person.projects.each do |project| %> - # <% if project.active? %> - # <%= person_form.fields :projects, model: project do |project_fields| %> - # Name: <%= project_fields.text_field :name %> - # <% end %> - # <% end %> - # <% end %> - # ... - # <% end %> - # - # Or a collection to be used: - # - # <%= form_with model: @person do |person_form| %> - # ... - # <%= person_form.fields :projects, model: @active_projects do |project_fields| %> - # Name: <%= project_fields.text_field :name %> - # <% end %> - # ... - # <% end %> - # - # To destroy any of the associated models through the - # form, you have to enable it using :allow_destroy: - # - # class Person < ActiveRecord::Base - # has_many :projects - # accepts_nested_attributes_for :projects, allow_destroy: true - # end - # - # This will allow you to specify which models to destroy in the - # attributes hash by adding a form element for the _destroy - # parameter with a value that evaluates to +true+ - # (eg. 1, '1', true, or 'true'): - # - # <%= form_with model: @person do |person_form| %> - # ... - # <%= person_form.fields :projects do |project_fields| %> - # Delete: <%= project_fields.check_box :_destroy %> - # <% end %> - # ... - # <% end %> - # - # When collections is used you might want to know the index of each - # object into the array. Use the FormBuilder's index method: - # - # <%= form_with model: @person do |person_form| %> - # ... - # <%= person_form.fields :projects do |project_fields| %> - # Project #<%= project_fields.index %> - # ... - # <% end %> - # ... - # <% end %> - # - # Note +fields+ automatically generates a hidden field to store the record - # ID. For circumstances where this is not needed pass - # include_id: false to skip it. + # See the docs for the ActionView::FormHelper.fields helper method. def fields(scope = nil, model: nil, **options, &block) fields_for(scope || model, model, **options, &block) end From 5a4052ad613955d26fd1ac571d9368ceb4d46b28 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Mon, 14 Nov 2016 21:20:41 +0100 Subject: [PATCH 09/16] [ci skip] Documentation edits. --- actionview/lib/action_view/helpers/form_helper.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 94251868a16d3..519658328c679 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -513,7 +513,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # # # - # The parameters in the forms are accessible in controlleres according to + # The parameters in the forms are accessible in controllers according to # their name nesting. So inputs named +title+ and post[title] are # accessible as params[:title] and params[:post][:title] # respectively. @@ -525,7 +525,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # # 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 + # and adds an authenticity token needed for cross site request forgery # protection. # # ==== +form_with+ options @@ -538,13 +538,13 @@ def apply_form_for_options!(record, object, options) #:nodoc: # either "get" or "post". If "patch", "put", "delete", or another verb # is used, a hidden input named _method is added to # simulate the verb over post. - # * :format - The format of the route post submits to. + # * :format - The format of the route the form submits to. # Useful when submitting to another resource type, like :json. # Skipped if a :url is passed. # * :scope - The scope to prefix input field names with and # thereby how the submitted parameters are grouped in controllers. # * :model - A model object to infer the :url and - # :scope by plus fill out input field values. + # :scope 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 @@ -553,7 +553,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # E.g. turn params[:post] into params[:article]. # * :authenticity_token - Authenticity token to use in the form. # Override with a custom authenticity token or pass false to - # skip the authenticity_token field altogether. + # 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 @@ -637,7 +637,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # # generates # - #
+ # # # ... #
@@ -1019,7 +1019,7 @@ def fields_for(record_name, record_object = nil, options = {}, &block) # *_attributes= form, e.g. address_attributes=. # # Depending on the association's reader method return value a different - # form builder is yielded. For single object returns a one-to-one builder, + # form builder is yielded. For single object returns, a one-to-one builder, # while a one-to-many builder for array returns. # # ==== One-to-one From 7d6fc1f0e380b5c0514636df103f3a195da010d5 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Mon, 14 Nov 2016 21:24:50 +0100 Subject: [PATCH 10/16] Use ERB tags. --- .../lib/action_view/helpers/form_helper.rb | 116 +++++++++--------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 519658328c679..1ffd1d77c92dd 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -477,36 +477,36 @@ def apply_form_for_options!(record, object, options) #:nodoc: # 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_with url: posts_path do |form| %> + # <%= form.text_field :title %> + # <% end %> # # => #
# #
# # # Adding a scope prefixes the input field names: - # form_with scope: :post, url: posts_path do |form| - # form.text_field :title - # end + # <%= form_with scope: :post, url: posts_path do |form| %> + # <%= form.text_field :title %> + # <% end %> # # => #
# #
# # # Using a model infers both the URL and scope: - # form_with model: Post.new do |form| - # form.text_field :title - # end + # <%= form_with model: Post.new do |form| %> + # <%= form.text_field :title %> + # <% end %> # # => #
# #
# # # 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_with model: Post.first do |form| %> + # <%= form.text_field :title %> + # <% end %> # # => #
# @@ -576,23 +576,23 @@ def apply_form_for_options!(record, object, options) #:nodoc: # # 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. + # <%= 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| + # <%= form_with(model: [ :admin, @post ]) do |form| %> # ... - # end + # <% 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| + # <%= form_with(model: [ @document, Comment.new ]) do |form| %> # ... - # end + # <% end %> # # Where @document = Document.find(params[:id]). # @@ -602,15 +602,15 @@ def apply_form_for_options!(record, object, options) #:nodoc: # 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 + # <%= 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? + # <%= text_area :person, :biography %> + # <%= check_box_tag "person[admin]", "1", @person.company.admin? %> # - # form.submit - # end + # <%= form.submit %> + # <% end %> # # Same goes for the methods in FormOptionHelper and DateHelper designed # to work with an object as a base, like @@ -631,9 +631,9 @@ def apply_form_for_options!(record, object, options) #:nodoc: # 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| + # <%= form_with(model: @post, data: { behavior: "autosave" }, html: { name: "go" }) do |form| %> # ... - # end + # <% end %> # # generates # @@ -652,11 +652,11 @@ def apply_form_for_options!(record, object, options) #:nodoc: # 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, include_id: false) do |fields| + # <%= form_with(model: @post) do |form| %> + # <%= form.fields(:comments, include_id: false) do |fields| %> # ... - # end - # end + # <% end %> + # <% end %> # # === Customized form builders # @@ -665,13 +665,13 @@ def apply_form_for_options!(record, object, options) #:nodoc: # 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 + # <%= 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: # @@ -687,7 +687,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # In many cases you will want to wrap the above in another helper, so you # could do something like the following: # - # def labelled_form_for(**options, &block) + # 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, html: {}, remote: true, **options) @@ -966,26 +966,26 @@ def fields_for(record_name, record_object = nil, options = {}, &block) # 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 + # <%= fields :comment do |fields| %> + # <%= fields.text_field :body %> + # <% end %> # # => # # # Using a model infers the scope and assigns field values: - # fields model: Comment.new(body: "full bodied") do |fields| - # fields.text_field :body - # end + # <%= fields model: Comment.new(body: "full bodied") do |fields| %< + # <%= fields.text_field :body %> + # <% end %> # # => # # # # Using +fields+ with +form_with+: - # form_with model: @post do |form| - # form.text_field :title + # <%= form_with model: @post do |form| %> + # <%= form.text_field :title %> # - # form.fields :comment do |fields| - # fields.text_field :body - # end - # end + # <%= 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 @@ -997,12 +997,12 @@ def fields_for(record_name, record_object = nil, options = {}, &block) # match the stand-alone FormHelper methods and methods # from FormTagHelper: # - # fields model: @comment do |fields| - # fields.text_field :body + # <%= 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 + # <%= 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 From 694c2260b8186ed5fd542ac80a4e0d4fa645171a Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 20 Nov 2016 19:09:03 +0100 Subject: [PATCH 11/16] [ci skip] Remove nested attributes examples --- .../lib/action_view/helpers/form_helper.rb | 173 ------------------ 1 file changed, 173 deletions(-) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 1ffd1d77c92dd..ac5c07ea727ea 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1007,179 +1007,6 @@ def fields_for(record_name, record_object = nil, options = {}, &block) # 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. - # - # === Nested Attributes Examples - # - # When the current scope's object has a nested attribute writer — - # most likely through +accepts_nested_attributes_for+ — for an - # attribute, +fields+ yields a new scope for that attribute. Useful when - # setting attributes on a parent and its associations in one go. - # - # Writers are considered a nested attributes setter if they're of the - # *_attributes= form, e.g. address_attributes=. - # - # Depending on the association's reader method return value a different - # form builder is yielded. For single object returns, a one-to-one builder, - # while a one-to-many builder for array returns. - # - # ==== One-to-one - # - # Consider a Person class which returns a _single_ Address from the - # address reader method and responds to the - # address_attributes= writer method: - # - # class Person - # def address - # @address - # end - # - # def address_attributes=(attributes) - # # Process the attributes hash - # end - # end - # - # This model can now be used with a nested +fields+, like so: - # - # form_with model: @person do |person_form| - # ... - # <%= person_form.fields_for :address do |address_fields| %> - # Street : <%= address_fields.text_field :street %> - # Zip code: <%= address_fields.text_field :zip_code %> - # <% end %> - # ... - # <% end %> - # - # When address is already an association on a Person you can use - # +accepts_nested_attributes_for+ to define the writer method for you: - # - # class Person < ActiveRecord::Base - # has_one :address - # accepts_nested_attributes_for :address - # end - # - # To destroy the associated model through the form, you have - # to enable it using :allow_destroy: - # - # class Person < ActiveRecord::Base - # has_one :address - # accepts_nested_attributes_for :address, allow_destroy: true - # end - # - # Now, when you use a form element with the _destroy parameter, - # with a value that evaluates to +true+, the associated model is destroyed - # (eg. 1, '1', true, or 'true'): - # - # <%= form_with model: @person do |person_form| %> - # ... - # <%= person_form.fields :address do |address_fields| %> - # ... - # Delete: <%= address_fields.check_box :_destroy %> - # <% end %> - # ... - # <% end %> - # - # ==== One-to-many - # - # Consider a Person class which returns an _array_ of Project instances - # from the projects reader method and responds to the - # projects_attributes= writer method: - # - # class Person - # def projects - # [@project1, @project2] - # end - # - # def projects_attributes=(attributes) - # # Process the attributes hash - # end - # end - # - # Note that the projects_attributes= writer method is in fact - # required for +fields+ to correctly identify :projects as a - # collection, and the correct indices to be set in the form markup. - # - # When projects is already an association on Person you can use - # +accepts_nested_attributes_for+ to define the writer method for you: - # - # class Person < ActiveRecord::Base - # has_many :projects - # accepts_nested_attributes_for :projects - # end - # - # This model can now be used with a nested +fields+. The block given to - # the nested +fields+ call will be repeated for each instance in the - # collection: - # - # <%= form_with model: @person do |person_form| %> - # ... - # <%= person_form.fields :projects do |project_fields| %> - # <% if project_fields.object.active? %> - # Name: <%= project_fields.text_field :name %> - # <% end %> - # <% end %> - # ... - # <% end %> - # - # It's also possible to specify the instance to be used: - # - # <%= form_with model: @person do |person_form| %> - # ... - # <% @person.projects.each do |project| %> - # <% if project.active? %> - # <%= person_form.fields :projects, model: project do |project_fields| %> - # Name: <%= project_fields.text_field :name %> - # <% end %> - # <% end %> - # <% end %> - # ... - # <% end %> - # - # Or a collection to be used: - # - # <%= form_with model: @person do |person_form| %> - # ... - # <%= person_form.fields :projects, model: @active_projects do |project_fields| %> - # Name: <%= project_fields.text_field :name %> - # <% end %> - # ... - # <% end %> - # - # To destroy any of the associated models through the - # form, you have to enable it using :allow_destroy: - # - # class Person < ActiveRecord::Base - # has_many :projects - # accepts_nested_attributes_for :projects, allow_destroy: true - # end - # - # This will allow you to specify which models to destroy in the - # attributes hash by adding a form element for the _destroy - # parameter with a value that evaluates to +true+ - # (eg. 1, '1', true, or 'true'): - # - # <%= form_with model: @person do |person_form| %> - # ... - # <%= person_form.fields :projects do |project_fields| %> - # Delete: <%= project_fields.check_box :_destroy %> - # <% end %> - # ... - # <% end %> - # - # When collections is used you might want to know the index of each - # object into the array. Use the FormBuilder's index method: - # - # <%= form_with model: @person do |person_form| %> - # ... - # <%= person_form.fields :projects do |project_fields| %> - # Project #<%= project_fields.index %> - # ... - # <% end %> - # ... - # <% end %> - # - # Note +fields+ automatically generates a hidden field to store the record - # ID. For circumstances where this is not needed pass - # include_id: false to skip it. def fields(scope = nil, model: nil, **options, &block) # TODO: Remove when ids and classes are no longer output by default. if model From 407d9f28ed7732981162a94ba247d3276455b130 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 20 Nov 2016 19:26:47 +0100 Subject: [PATCH 12/16] Invert `include_id` to `skip_id`. `skip_id: true` reads better than `include_id: false` (since the `include_id` default is true). --- .../lib/action_view/helpers/form_helper.rb | 16 ++++++++++++++-- .../test/template/form_helper/form_with_test.rb | 16 ++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index ac5c07ea727ea..e9dbb97780d13 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -653,7 +653,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # thus there is no primary key for comments. # # <%= form_with(model: @post) do |form| %> - # <%= form.fields(:comments, include_id: false) do |fields| %> + # <%= form.fields(:comments, skip_id: true) do |fields| %> # ... # <% end %> # <% end %> @@ -698,7 +698,7 @@ def form_with(model: nil, scope: nil, url: nil, format: nil, html: {}, remote: t scope ||= model_name_from_record_or_class(model).param_key end - html_options = html.merge(options.except(:index, :include_id, :builder)) + html_options = html.merge(options.except(:index, :skip_id, :builder)) html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted? html_options[:remote] = remote unless html_options.key?(:remote) @@ -1583,6 +1583,9 @@ 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) : {} + + 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 @@ -1590,6 +1593,7 @@ def initialize(object_name, object, template, options) 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 @@ -1885,6 +1889,8 @@ def fields_for(record_name, record_object = nil, fields_options = {}, &block) # See the docs for the ActionView::FormHelper.fields helper method. def fields(scope = nil, model: nil, **options, &block) + convert_to_legacy_options(options) + fields_for(scope || model, model, **options, &block) end @@ -2236,6 +2242,12 @@ def nested_child_index(name) @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[:skip_id] + end + end end end diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index 94154d21e4dd9..5cbdf17494312 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -1141,7 +1141,7 @@ def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one form_with(model: @post) do |f| concat f.text_field(:title) - concat f.fields(:author, include_id: false) { |af| + concat f.fields(:author, skip_id: true) { |af| af.text_field(:name) } end @@ -1157,7 +1157,7 @@ def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_inherited @post.author = Author.new(321) - form_with(model: @post, include_id: false) do |f| + form_with(model: @post, skip_id: true) do |f| concat f.text_field(:title) concat f.fields(:author) { |af| af.text_field(:name) @@ -1175,9 +1175,9 @@ def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_override @post.author = Author.new(321) - form_with(model: @post, include_id: false) do |f| + form_with(model: @post, skip_id: true) do |f| concat f.text_field(:title) - concat f.fields(:author, include_id: true) { |af| + concat f.fields(:author, skip_id: false) { |af| af.text_field(:name) } end @@ -1244,7 +1244,7 @@ def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_a concat af.text_field(:name) } @post.comments.each do |comment| - concat f.fields(:comments, model: comment, include_id: false) { |cf| + concat f.fields(:comments, model: comment, skip_id: true) { |cf| concat cf.text_field(:name) } end @@ -1265,7 +1265,7 @@ def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_a @post.comments = Array.new(2) { |id| Comment.new(id + 1) } @post.author = Author.new(321) - form_with(model: @post, include_id: false) do |f| + form_with(model: @post, skip_id: true) do |f| concat f.text_field(:title) concat f.fields(:author) { |af| concat af.text_field(:name) @@ -1291,9 +1291,9 @@ def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_a @post.comments = Array.new(2) { |id| Comment.new(id + 1) } @post.author = Author.new(321) - form_with(model: @post, include_id: false) do |f| + form_with(model: @post, skip_id: true) do |f| concat f.text_field(:title) - concat f.fields(:author, include_id: true) { |af| + concat f.fields(:author, skip_id: false) { |af| concat af.text_field(:name) } @post.comments.each do |comment| From 42f4fb24c6dd23af3c2b8192dad1a51be26a1e65 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 20 Nov 2016 19:43:25 +0100 Subject: [PATCH 13/16] Invert `remote` to `local`. Since forms are remote by default, the option name makes more sense as `local: true`. --- .../lib/action_view/helpers/form_helper.rb | 11 +++--- .../template/form_helper/form_with_test.rb | 35 +++++-------------- 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index e9dbb97780d13..3e0430b658c82 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -561,9 +561,8 @@ def apply_form_for_options!(record, object, options) #:nodoc: # This is helpful when fragment-caching the form. Remote forms # get the authenticity token from the meta tag, so embedding is # unnecessary unless you support browsers without JavaScript. - # * :remote - Set to true to allow the Unobtrusive - # JavaScript drivers to control the submit behavior, defaulting to - # to an XHR submit. Disable with remote: false. + # * :local - By default form submits are remote and unobstrusive XHRs. + # Disable remote submits with local: true. # * :enforce_utf8 - If set to false, a hidden input with name # utf8 is not output. Default is true. # * :builder - Override the object used to build the form. @@ -690,7 +689,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # 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, html: {}, remote: true, **options) + def form_with(model: nil, scope: nil, url: nil, format: nil, html: {}, local: false, **options) if model url ||= polymorphic_path(model, format: format) @@ -700,7 +699,7 @@ def form_with(model: nil, scope: nil, url: nil, format: nil, html: {}, remote: t html_options = html.merge(options.except(:index, :skip_id, :builder)) html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted? - html_options[:remote] = remote unless html_options.key?(:remote) + html_options[:remote] = !local unless html_options.key?(:remote) if block_given? builder = instantiate_builder(scope, model, options) @@ -2247,6 +2246,8 @@ def convert_to_legacy_options(options) if options.key?(:skip_id) options[:include_id] = !options[:skip_id] end + + options[:remote] = !options[:local] end end end diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index 5cbdf17494312..0bf353a459e44 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -27,14 +27,14 @@ def hidden_fields(options = {}) end end - def form_text(action = "http://www.example.com", remote: true, **options) + def form_text(action = "http://www.example.com", local: false, **options) enctype, html_class, id, method = options.values_at(:enctype, :html_class, :id, :method) method = method.to_s == "get" ? "get" : "post" txt = %{} @@ -87,9 +87,9 @@ def test_form_with_with_method_delete end def test_form_with_with_remote_false - actual = form_with(remote: false) + actual = form_with(local: true) - expected = whole_form("http://www.example.com", remote: false) + expected = whole_form("http://www.example.com", local: true) assert_dom_equal expected, actual end @@ -697,23 +697,6 @@ def test_form_with_enforce_utf8_false assert_dom_equal expected, output_buffer end - def test_form_with_disable_remote_in_html - form_with(model: @post, url: "/", html: { remote: false, id: "create-post", method: :patch }) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end - - expected = whole_form("/", "create-post", method: "patch", remote: false) do - "" + - "" + - "" + - "" - end - - assert_dom_equal expected, output_buffer - end - def test_form_with_with_remote_without_html @post.persisted = false @post.stub(:to_key, nil) do @@ -723,7 +706,7 @@ def test_form_with_with_remote_without_html concat f.check_box(:secret) end - expected = whole_form("/posts", remote: true) do + expected = whole_form("/posts") do "" + "" + "" + @@ -2147,22 +2130,22 @@ def hidden_fields(options = {}) txt end - def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil) + def form_text(action = "/", id = nil, html_class = nil, local = nil, multipart = nil, method = nil) txt = %{} end - def whole_form(action = "/", id = nil, html_class = nil, remote: true, **options) + def whole_form(action = "/", id = nil, html_class = nil, local: false, **options) contents = block_given? ? yield : "" method, multipart = options.values_at(:method, :multipart) - form_text(action, id, html_class, remote, multipart, method) + hidden_fields(options.slice :method, :enforce_utf8) + contents + "" + form_text(action, id, html_class, local, multipart, method) + hidden_fields(options.slice :method, :enforce_utf8) + contents + "" end def protect_against_forgery? From 15a805c38059a951c5fba1f6bc9281d6707fed4e Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 20 Nov 2016 19:54:35 +0100 Subject: [PATCH 14/16] Invert `enforce_utf8` to `skip_enforcing_utf8`. --- .../lib/action_view/helpers/form_helper.rb | 7 ++-- .../template/form_helper/form_with_test.rb | 37 ++++++++----------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 3e0430b658c82..82b33502f94af 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -563,8 +563,8 @@ def apply_form_for_options!(record, object, options) #:nodoc: # unnecessary unless you support browsers without JavaScript. # * :local - By default form submits are remote and unobstrusive XHRs. # Disable remote submits with local: true. - # * :enforce_utf8 - If set to false, a hidden input with name - # utf8 is not output. Default is true. + # * :skip_enforcing_utf8 - By default a hidden field named +utf8+ + # is output to enforce UTF-8 submits. Set to true to skip the field. # * :builder - Override the object used to build the form. # * :id - Optional HTML id attribute. # * :class - Optional HTML class attribute. @@ -689,7 +689,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # 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, html: {}, local: false, **options) + def form_with(model: nil, scope: nil, url: nil, format: nil, html: {}, local: false, skip_enforcing_utf8: false, **options) if model url ||= polymorphic_path(model, format: format) @@ -700,6 +700,7 @@ def form_with(model: nil, scope: nil, url: nil, format: nil, html: {}, local: fa html_options = html.merge(options.except(:index, :skip_id, :builder)) html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted? html_options[:remote] = !local unless html_options.key?(:remote) + html_options[:enforce_utf8] = !skip_enforcing_utf8 if block_given? builder = instantiate_builder(scope, model, options) diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index 0bf353a459e44..73bdd7794cf24 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -14,10 +14,10 @@ class FormWithActsLikeFormTagTest < FormWithTest def hidden_fields(options = {}) method = options[:method] - enforce_utf8 = options.fetch(:enforce_utf8, true) + skip_enforcing_utf8 = options.fetch(:skip_enforcing_utf8, false) "".tap do |txt| - if enforce_utf8 + unless skip_enforcing_utf8 txt << %{} end @@ -93,16 +93,9 @@ def test_form_with_with_remote_false assert_dom_equal expected, actual end - def test_form_with_enforce_utf8_true - actual = form_with(enforce_utf8: true) - expected = whole_form("http://www.example.com", enforce_utf8: true) - assert_dom_equal expected, actual - assert actual.html_safe? - end - - def test_form_with_enforce_utf8_false - actual = form_with(enforce_utf8: false) - expected = whole_form("http://www.example.com", enforce_utf8: false) + def test_form_with_skip_enforcing_utf8_true + actual = form_with(skip_enforcing_utf8: true) + expected = whole_form("http://www.example.com", skip_enforcing_utf8: true) assert_dom_equal expected, actual assert actual.html_safe? end @@ -673,24 +666,24 @@ def test_form_with_enables_remote_by_default assert_dom_equal expected, output_buffer end - def test_form_with_enforce_utf8_true - form_with(scope: :post, enforce_utf8: true) do |f| + def test_form_with_skip_enforcing_utf8_true + form_with(scope: :post, skip_enforcing_utf8: true) do |f| concat f.text_field(:title) end - expected = whole_form("/", enforce_utf8: true) do + expected = whole_form("/", skip_enforcing_utf8: true) do "" end assert_dom_equal expected, output_buffer end - def test_form_with_enforce_utf8_false - form_with(scope: :post, enforce_utf8: false) do |f| + def test_form_with_skip_enforcing_utf8_false + form_with(scope: :post, skip_enforcing_utf8: false) do |f| concat f.text_field(:title) end - expected = whole_form("/", enforce_utf8: false) do + expected = whole_form("/", skip_enforcing_utf8: false) do "" end @@ -2117,10 +2110,10 @@ def test_form_with_only_instantiates_builder_once def hidden_fields(options = {}) method = options[:method] - if options.fetch(:enforce_utf8, true) - txt = %{} - else + if options.fetch(:skip_enforcing_utf8, false) txt = "" + else + txt = %{} end if method && !%w(get post).include?(method.to_s) @@ -2145,7 +2138,7 @@ def whole_form(action = "/", id = nil, html_class = nil, local: false, **options method, multipart = options.values_at(:method, :multipart) - form_text(action, id, html_class, local, multipart, method) + hidden_fields(options.slice :method, :enforce_utf8) + contents + "" + form_text(action, id, html_class, local, multipart, method) + hidden_fields(options.slice :method, :skip_enforcing_utf8) + contents + "" end def protect_against_forgery? From 17429bb950cc45decc7a21861577808ebafdb6a3 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 20 Nov 2016 19:57:23 +0100 Subject: [PATCH 15/16] Refer to the brand spanking new rails-ujs. Soon to be bundled in Rails proper, so jquery-ujs is out. --- actionview/lib/action_view/helpers/form_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 82b33502f94af..3dcf6c8d0417f 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -520,7 +520,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # # By default +form_with+ attaches the data-remote attribute # submitting the form via an XMLHTTPRequest in the background if an - # an Unobtrusive JavaScript driver, like jquery-ujs, is used. See the + # an Unobtrusive JavaScript driver, like rails-ujs, is used. See the # :remote option for more. # # For ease of comparison the examples above left out the submit button, From 30ebe5d8146d9bf53d43156ad26840d8a05415fc Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 20 Nov 2016 20:53:57 +0100 Subject: [PATCH 16/16] Make `form_with` a bit more composed. The flow is still not quite what it should be because the legacy methods and these new ones pull at opposite ends. Lots of options have been renamed, so now the new pieces don't fit in so well. I'll try to work on this in later commits after this PR (it's likely there's a much better way to structure this whole part of Action View). --- .../lib/action_view/helpers/form_helper.rb | 45 ++++++++++++++----- .../template/form_helper/form_with_test.rb | 22 +-------- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 3dcf6c8d0417f..1e1ddb4d713d5 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -689,7 +689,7 @@ def apply_form_for_options!(record, object, options) #:nodoc: # 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, html: {}, local: false, skip_enforcing_utf8: false, **options) + def form_with(model: nil, scope: nil, url: nil, format: nil, **options) if model url ||= polymorphic_path(model, format: format) @@ -697,20 +697,15 @@ def form_with(model: nil, scope: nil, url: nil, format: nil, html: {}, local: fa scope ||= model_name_from_record_or_class(model).param_key end - html_options = html.merge(options.except(:index, :skip_id, :builder)) - html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted? - html_options[:remote] = !local unless html_options.key?(:remote) - html_options[:enforce_utf8] = !skip_enforcing_utf8 - if block_given? builder = instantiate_builder(scope, model, options) output = capture(builder, &Proc.new) - html_options[:multipart] ||= builder.multipart? + options[:multipart] ||= builder.multipart? - html_options = html_options_for_form(url || {}, html_options) + html_options = html_options_for_form_with(url, model, options) form_tag_with_body(html_options, output) else - html_options = html_options_for_form(url || {}, html_options) + html_options = html_options_for_form_with(url, model, options) form_tag_html(html_options) end end @@ -1472,6 +1467,32 @@ def range_field(object_name, method, options = {}) 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.except(:index, :include_id, :builder).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 @@ -2245,10 +2266,12 @@ def nested_child_index(name) def convert_to_legacy_options(options) if options.key?(:skip_id) - options[:include_id] = !options[:skip_id] + options[:include_id] = !options.delete(:skip_id) end - options[:remote] = !options[:local] + if options.key?(:local) + options[:remote] = !options.delete(:local) + end end end end diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index 73bdd7794cf24..c80a2f61b9387 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -86,7 +86,7 @@ def test_form_with_with_method_delete assert_dom_equal expected, actual end - def test_form_with_with_remote_false + def test_form_with_with_local_true actual = form_with(local: true) expected = whole_form("http://www.example.com", local: true) @@ -690,26 +690,6 @@ def test_form_with_skip_enforcing_utf8_false assert_dom_equal expected, output_buffer end - def test_form_with_with_remote_without_html - @post.persisted = false - @post.stub(:to_key, nil) do - form_with(model: @post, remote: true) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end - - expected = whole_form("/posts") do - "" + - "" + - "" + - "" - end - - assert_dom_equal expected, output_buffer - end - end - def test_form_with_without_object form_with(scope: :post, id: "create-post") do |f| concat f.text_field(:title)