Rails form_with defaulting to incorrect method - ruby-on-rails

I'm using Rails 6. I have a route that defines a get request:
namespace :admin do
get '/machines/search', to: 'machines#search', as: 'search_machines'
end
Then, I have a form_with that sets the url to the route. When the form is loaded, the HTML that's generated for the form contains a method="post" instead of method="get" which is what I would've expected since the route is a GET request and not a POST. I can add the method: "get" parameter to the form_with and this fixes the issue but I don't understand why Rails didn't pick up the correct method initially.
<%= form_with url: admin_search_machines_path() do |i| %>
<%= i.text_field :q, placeholder: "Search", autocomplete: "off", class: "debounce-form-submit form-control" %>
<% end %>

According to the documentation form_with does default to POST if method is not specified. Which is not 100% correct, as it will switch to PATCH (via hidden field trick) if the model is persisted (source).
Up to today, the code does not look at the route itself to specify the method.
I think the (backwards incompatible) automagic you suggest is not totally off, though. You might ask about that in the rubyonrails forums and get feedback whether this would be a welcomed change. Personally, I never expected it to inspect the route. Also, you might have the same route with post and get methods, which would make lookup more involved than what meets the eye on first sight.

Related

Rails No route matches[POST] on second update

I have a form in Rails like so:
= form_for cart, as: :cart, url: backoffice_cart_path(cart) , method: :put, html: { autocomplete: "off", class: 'cart-quoter' } do |f|
When its rendered it shows the following header
<form action="/backoffice/carts/4" ...>
When I update some stuff in the view I am returned to the same place, however, if I try to do the same stuff again, it throws me this error
No route matches [POST] "/backoffice/carts/3"
Which is exactly the same previous address that was being used.
Any ideas on what it could be?
Note:
The cart object does exist from the beginning, so I can effectively update the object the first time, but sending it again the second time the router does not find the specified route.
At the beginning the form_for code was like this:
= form_for ['backoffice', cart], html: { autocomplete: "off", class: 'cart-quoter' } do |f|
I just added more explicit code trying to force the form submission.
The route exists but just to be sure
backoffice_cart GET /backoffice/carts/:id(.:format) backoffice/carts#show
PATCH /backoffice/carts/:id(.:format) backoffice/carts#update
PUT /backoffice/carts/:id(.:format) backoffice/carts#update
DELETE /backoffice/carts/:id(.:format) backoffice/carts#destroy
The controller has an update method, no magic in there.
Replace this:
= form_for cart, as: :cart, url: backoffice_cart_path(cart) , method: :put, html: { autocomplete: "off", class: 'cart-quoter' } do |f|
by
= form_for cart, as: :cart, url: backoffice_cart_path(cart) , html: { autocomplete: "off", class: 'cart-quoter' } do |f|
The form object / record will have information on its own, on whether it is a new record or a existing record. You can check it by calling:
cart.new_record?
If it returns true, then it means this form is going to perform a POST /create according to the REST resource rules. If it returns false, then it will perform a PATCH to /update (standard is now PATCH as opposed to PUT)
Ideally, you should not have url or method parameters in the form tag for a REST resource / record. You don't even need as here, because if cart is an instance of a class named Cart, Rails will make sure everything is treated as cart, parameters wise.
In the end, your tag would look like:
= form_for cart do |f|
... form stuff ...
I understand you're using a namespace here, backoffice, so you could achieve it by something like:
= form_for [:backoffice, cart] do |f|
.... form stuff ....
Hopefully this should fix your problem. Make sure cart is coming from a source where it will always be a ActiveRecord model. And of course, your controller needs both create and update methods.
The answer to the problem is as weird as it can be.
The form has some manual input inside, like this one
input[type='text' value=item.net_unit_price name="cart[cart_items][][id]"]
One of them was written this way
input[type='text' value=item.net_unit_price name="cart[cart_items][price]"]
With some missing brackets. I fixed that difference and the controller is working correctly.
I don't fully understand why this is the solution for my problem, but it is something you should consider when encountering this kind of problem. Looks like a form that is filled with some errors brings trouble to the router, and later the controllers in rails.

Is a link_to a custom route or a form the best way to provide state change?

What is the 'Rails way' to provide access to methods such as the following from a view
def approve!
self.update_attribute status, 'approved'
end
Is it best to create a link to a custom route
<%= link_to 'Approve', approve_object_path(#object) %>
#objects_controller.rb
def approve
#object.approve!
end
Or to create an update form
<%= simple_form_for #object do |f| %>
<%= f.input :status, input_html { value: 'approved' }, as: :hidden %>
<%= f.submit %>
<% end %>
On the one hand, using a form and not using the approve! method at all seems to align better with restful routes.
On the other hand, a link to a custom route seems to provide less opportunity for submitted values to be manipulated by the user, and also requires less code to implement.
Which is the preferred way to do this?
I don't know if there's a preferred best practice, per se...
Just my opinion, but I normally do the link_to approach, and for an "state machine" like your example. The need for an entire form for a simple action like this is a lot of extra code that isn't necessary when an action can be called to change the state.
The counter argument to this is that it breaks CRUD, and requires a non-CRUD route. Convention over configuration champions would probably prefer an entire new controller to change the state of the object.
TL;DR - I do the link_to approach, and I use :remote => true to make it asynchronous so the page doesn't even reload (unless you need the page to redirect elsewhere).
You can change state remotely with both the scenarios.
But I think if only a state has to be changed then use link_to. As we don't need to have form features with listed attributes in params here.

Rails 3: How do I mix form params and URL params?

I am working on a legacy app and am trying to upgrade it to Rails 3. Currently when I do a search I get the URL: http://localhost:3000/search/external_search/?keyword=argentina. However, when I try to filter out the results, I get http://localhost:3000/search/external_search/?order=Name:ASC, without the keyword parameter in the URL. Since there wasn't a keyword params, the filter wouldn't work. This was messing up my filter, so I decided to add a hidden field tag
<%= hidden_field_tag 'keyword', #search.text %>
which makes it work, but the URL didn't change. I want it to look like:
http://localhost:3000/search/external_search/?keyword=argentina&order=Name:ASC
Where exactly would I accomplish this? In the controller or the form?
If you want to see the parameters of the submited form in the URL, you must use GET method instead of POST.
Add the option :method => :get to the form_for or form_tag enclosing the <%= hidden_field_tag 'keyword', #search.text %>

routing scope problem with form_for (partial)

Trying to route:
scope :shortcut do
resources :text_elems
end
Using basic scaffold with form partial
*_form.html.erb*
<%= form_for(#text_elem, :shortcut => #shortcut) do |f| %>
...
Problem is: When I call the edit action, the form html shows as:
<form ... action="/25/text_elems/25">
Note: The new action renders the form action correctly:
<form ... action="/home/text_elems">
So it appears that my :shortcut param is getting trumped by the :id param when form_for processes it's block. Now I am able to get the action to correctly route with the :shortcut param if I manually make the :url => {...} in the form_for block, but I would prefer to keep the code dry, plus I want to report this problem to rails if it is indeed a bug.
Can anyone else confirm this as a bug?
Actually, you can pass the values as a full hash, rather than trying to rely on the default to_param (which is what gets called if all you do is pass the #text_elem)
<%= form_for({:id => #text_elem.to_param, :shortcut => #shortcut}) do |f| %>
however, if this is actually a nested-resource, you could also do:
<%= form_for([#shortcut, #text_elem]) do |f| %>
I was having the same issues and none of the above answers helped.
The last answer on this page worked for me though...
https://rails.lighthouseapp.com/projects/8994/tickets/6736-problem-with-scoped-routes-and-form_for-helper

How can you pass an object from the form_for helper to a method?

So let's say I have a form which is being sent somewhere strange (and by strange we mean, NOT the default route:
<% form_for #form_object, :url => {:controller => 'application',
:action => 'form_action_thing'} do |f| %>
<%= f.text_field :email %>
<%= submit_tag 'Login' %>
<% end %>
Now let's say that we have the method that accepts it.
def form_action_thing
User.find(????? :email ?????)
end
My questions are thus:
How can I make the object #form_object available to the receiving method (in this case, form_action_tag)?
I've tried params[:form_object], and I've scoured this site and the API, which I have to post below because SO doesn't believe I'm not a spammer (I'm a new member), as well as Googled as many permutations of this idea as I could think of. Nothing. Sorry if I missed something, i'm really trying.
How do I address the object, once I've made it accessible to the method? Not params[:form_object], I'm guessing.
EDIT
Thanks so much for the responses, guys! I really appreciate it. I learned my lesson, which is that you shouldn't deep-copy an object from a form, and that the parameters of a form are actually included when you submit it.
I will admit it's sort of disheartening to not know stuff that seems so obvious though...
you need to pass the "id" of your "#form_object" in the url and then lookup that object (assuming you have a model and using ActiveRecord)
It depends on how do you set up your routes. If you're using the default /:controller/:action/:id route, you can pass it as a parameter in the URL. Note that not the whole #form_object can/should be passed, but it's id or some other attribute to identify it instead. In this case, you should make your URL:
<% form_for #form_object, :url => {:controller => 'application',
:action => 'form_action_thing', :email => some_email} do |f| %>
<%= f.text_field :email %>
<%= submit_tag 'Login' %>
<% end %>
And in your controller
def form_action_thing
#user = User.find_by_email(params[:email])
end
You can pass parameters through the url, but when submitting a form the only thing that should (probably) be passed through the url is the record id for a RESTful record.
And it appears you didn't find out yet where your form data can be found in the params.
So
All the data from your form should end up in params[:form_object]. The actual value for :form_object is selected by Rails, it's probably coming from the object's class (too lazy to look that up right now)
In any case, you can easily find out where your form values are submitted by looking at your console/log output. All the params for each requests are dumped there.
The form fields will be inside the params like params[:form_object][:email] - each field that is submitted has an entry corresponding to the field name.
The params hash not contain all the original values from your #form_object. There will be only those values that you included in the form.
If you need to pass non-editable values to the controller with your form, use hidden_field(s) These will be submitted with the form, but are not visible to the user.

Resources