3

I tried both form_tag and form_with - the result is the same, controller's action never gets triggered.

# routes.rb
resources :products do
  member do
    patch :add_comment
  end
end

# products_controller.rb
def add_comment
  # !!! damn form_with never gets here!!!
  product.add_comment!(params[:comment_id])
  redirect_back(fallback_location: products_path)
end

# view
<%= form_with(url: add_comment_product_path, local: true) do |form| %>
  <%= form.text_field :comment_id %>
  <%= form.submit 'Add comment' %>
<% end %>

Actual logs:

Started PATCH "/products/1"
Processing by ProductsController#update as HTML
Parameters: {
  "utf8"=>"✓",
  "authenticity_token"=>"token",
  "products"=>{a_lot: :of_stuff},
  "comment_id"=>"2",
  "commit"=>"Add comment",
  "id"=>"1"
}

Expected logs:

Started PATCH "/products/1/add_comment?comment_id=2"
Processing by ProductsController#add_comment as HTML
Parameters: {
  "utf8"=>"✓",
  "authenticity_token"=>"token",
  "comment_id"=>"2",
  "id"=>"1"
}

Edit:

I think it has something to do with the fact that this form_with is nested into bigger form and it looks when I hit Add comment it triggers the outer submit

Andrey Deineko
  • 51,333
  • 10
  • 112
  • 145
  • The original comment asked for the routes and I just saw it then. Is comment a separate resource or does the product have a comment_id in its record? – BenKoshy Oct 05 '18 at 15:50
  • @BKSpurgeon it shouldn't matter I think, the point here is that I can't hit the controller's action using `form_tag` or `form_with`. "`Comment`" is a separate model but the question contains very simplified form of what I am working on (I hope it's obvious). Main goal is to make `form_with` call the right controller's action – Andrey Deineko Oct 05 '18 at 15:53
  • Where is the `form` defined? You do not seem to pass it to the block (i.e. `form_with ... do |form|`) – AbM Oct 05 '18 at 15:57
  • @AbM good catch, it's a typo. Edited – Andrey Deineko Oct 05 '18 at 15:58
  • What is the output of `rake routes | grep add_comment_product`? Also, you should be passing the id of the product to the url, i.e. `add_comment_product_path(some_id)`? This would generate `/products/1/add_comment` and not `/products/1/add_comment?comment_id=2` since you aren't passing a `comment_id` to the url. – AbM Oct 05 '18 at 16:01
  • You should note that the HTML standards (both HTML5 and older (x)HTML standards) do not allow nested
    elements and the behaviour can be very unpredictable as its not specified if the browser should use the action attribute of the nested form or bubble the event to the parent form element which is most likely happening in your case. https://www.w3.org/TR/html5/forms.html
    – max Oct 05 '18 at 16:37
  • @AndreyDeineko i ask because otherwise it's a nested resource as max outlined (see max's answer below) – BenKoshy Oct 06 '18 at 08:04

4 Answers4

2

The Rails way to handle this would be as a seperate but nested resource - as you´re really creating a new resource (a comment) and not modifying the product itself.

This also keeps your code in line with the Single Responsibility Principle (SRP) as each controller only handles CRUD'ing a single type of resource.

You can nest resources by nesting the calls to resources:

resources :products do
  resources :comments, shallow: true
end

Then setup a CommentsController to handle CRUD'ing comments:

class CommentsController < ApplicationController
  before_action :set_comment, only: [:index, :new, :create]

  # GET /products/:product_id/comments
  def index
    @comments = @product.comments
  end

  # GET /products/:product_id/comments/new
  def new
    @comment = @product.comments.new
  end

  # POST /products/:product_id/comments
  def create
    @comment = @product.comments.new(comment_params)
    if @comment.save
      redirect_to @product, success: 'Comment created'
    else
      render :new
    end
  end

  # ...

  private
  def set_product
    @product = Product.find(params[:product_id])
  end

  def comment_params
    params.require(:comment)
          .permit(:foo, :bar)
  end
end

To set the form action attribute to point to a nested route you simply use an array or the named product_comments(product_id: @product.to_param) route helper.

<%= form_with(model: @comment, url: [@comment.product, @comment], local: true) do |form| %>
  <%= form.submit 'Add comment' %>
<% end %>

As the product id is passed through the URI there is no need to pass it via a hidden input.

I think it has something to do with the fact that this form_with is nested into bigger form and it looks when I hit Add comment it triggers the outer submit

You should note that the HTML standards (both HTML5 and older (x)HTML standards) do not allow nested form elements and the behaviour can be very unpredictable as its not specified if the browser should use the action attribute of the nested form or bubble the event to the parent form element which is most likely happening in your case. see: http://w3.org/TR/html5/forms.html

max
  • 96,212
  • 14
  • 104
  • 165
  • You can omit the `index` and `new` action if the form and display of comments is on `/products/:id`. – max Oct 05 '18 at 16:27
  • I realized that after the edition I made to question. I'll test nested resource soon but I'm sure it's the way to go. Maan I haven't worked on views for a few years now, just heavy backend stuff, totally forgot how simple stuff it's done. Thanks for the answer, I'll let you know how it went once tried – Andrey Deineko Oct 05 '18 at 16:36
  • I hope you saw my comment about nested `
    ` elements not being allowed in the HTML standards - that would explain why the form is not posting to the expected route.
    – max Oct 05 '18 at 17:52
0
add_comment_product PATCH  /products/:id/add_comment(.:format)    products#add_comment

You have declared it as a member route, but I don't see you are passing any value for the :id the path helper.

Try changing it to

<%= form_with(url: add_comment_product_path(product), local: true) do |form| %>

where product is the Product instance.

Pavan
  • 33,316
  • 7
  • 50
  • 76
  • makes no difference unfortunately – Andrey Deineko Oct 05 '18 at 16:13
  • @AndreyDeineko Does this work `<%= form_with(model: @your_product_instance, url: add_comment_product_path, local: true) do |form| %>` ? – Pavan Oct 05 '18 at 16:34
  • `add_comment_product_path(product)` will actually create the correct route. I believe the issue is that the `<%= form_with %>` call is nested inside another form which creates invalid HTML. – max Oct 05 '18 at 17:55
0

I simply had to move the nested form out of the bigger form to make it work.

Andrey Deineko
  • 51,333
  • 10
  • 112
  • 145
-1

Yon try this -

# products_controller.rb
def add_comment
  # You need add permitted for get parameters
  params.permit(:comment_id)
  product.add_comment!(params[:comment_id])
  redirect_back(fallback_location: products_path)
end

# You can place this form anywhere in your application, but you need to specify product object and comment_id
<%- @product = Product.find(1) %>
<%= form_with(url: add_comment_product_path(@product, comment_id: 2), local: true, method: :patch) do |form| %>
  <%= form.text_field :comment_id %>
  <%= form.submit 'Add comment' %>
<% end %>

Started PATCH "/products/1/add_comment?comment_id=2" for 127.0.0.1 at 2018-10-05 22:01:37 +0600
Processing by ProductsController#add_comment as HTML

Parameters: {"utf8"=>"✓", "authenticity_token"=>"token", "comment_id"=>"2", "commit"=>"Add comment", "id"=>"1"}