Rails Call Custom Controller Action from Custom Form - ruby-on-rails

In a Rails project, I have the following controller action for the controller exchanges.rb:
def update_ordid
# Get the active exchange
#exchange = Exchange.find(params[:id])
# Decide which order ID field to update
active_order_field = params[:ordfld]
# Save the order ID
order_id = params[:ordid]
if active_order_field == 1 then
#exchange.order_id_1 = order_id
else
#exchange.order_id_2 = order_id
end
#active_exchange.save
respond_with(#exchange)
end
I've set up a route to this controller action:
resources :exchanges do
collection do
get 'update_ordid'
end
end
I want to call this action that accepts an order ID from a form on an exchanges show.html.erb page. I need to pass three values:
The ID of the current exchange, such as the integer in this example URL localhost:3000/exchanges/2 (This is the page the form is on)
The order ID as input from a text-field
Which of the two possible exchange fields the action should update
Next I need to create a custom form which will pass these values as parameters to the action. I haven't been able to find a good tutorial on how to do this yet, but my first thought was to set up the following:
<%= form_for(#exchange) do |f| %>
<div class="field">
<%= f.label :ordid, "Order ID" %><br>
<%= f.text_field :ordid, class: "form-control" %>
</div>
<% if #isrequestor == true %>
<%f.hidden_field :ordfld, :value => "1" %>
<% else %>
<%f.hidden_field :ordfld, :value => "2" %>
<% end %>
<div class="actions">
<%= f.submit "Submit", class: "btn btn-primary" %>
</div>
<% end %>
This gives me a NoMethodError stating the method 'ordid' is undefined. I'm guessing I need to modify the first line of code to associate the form with the custom action I've set up, but have no idea how to do so properly.

Yah, I got your point. So you wanted the following thing:
You wrote an custom action
You wanted to submit a form that action
You have registered your action in the router.
So let me answer the following solutions and find some mistakes you made in your code.
# in route.rb
resources :exchanges do
patch :update_ordid, on: :member # this is the best practice I would say,
#when you are trying to modify an existing record. So this action will only
#be reached with patch http methods
# on :member action an parameter id is required.
end
now if you generate your routes by running:
bundle exec rake routes
you will see a path like:
update_ordid_exchange /exchange/:id/update_ordid # :id parameter for exchange record
in your form set the url:
<%= form_for(#exchange, url: update_ordid_exchange_path) do |f| %>
or
<%= form_for(#exchange, url: url_for(controller: :exchange, action: update_ordid)) do |f| %>
Now then you will this form can submit this values within the parameter in your desire field.
So let me summarize things up here:
1. Setup your route properly
2. Check the url based on your route by generating rake routes command as shown above
3. Set the proper url and check if http method is correctly define in your form helper. For member actions, form helper by default use patch as http method. you just have to set the url.
Hope you understand my flow.

Related

Rails 6 create order confirmation before submit

I'm trying to create an order confirmation page for my Rails 6 app. The idea is that user will see a preview of the item they are creating before submitting and the object being saved in the database. Below desired flow:
User visits /cash_transactions/withdrawals/new
User enters data and clicks submit
User is redirected to /cash_transactions/withdrawals/confirm which
displays the entry
User clicks confirm to save object to db or cancel
Object is saved
I followed two main threads that describe this type of action, but they are quite old - 11 and 12 years old. Nevertheless based on that I've created below code:
# controllers/cash_transactions/withdrawals_controller.tb
module CashTransactions
class WithdrawalsController < CashTransactions::BaseController
(...)
def confirm
#cash_transaction = CashTransaction.new(cash_transaction_params)
render 'cash_transactions/_confirm'
end
end
end
# routes.rb
namespace :cash_transactions do
resources :withdrawals, only: %i[new create] do
collection do
post :confirm
end
end
end
With corresponding views:
# app/views/cash_transactions/new.html.erb
<%= render 'cash_transactions/form', cash_transaction: #cash_transaction %>
# views/cash_transactions/_form
# the form is rendered for cash_transaction create action
<%= simple_form_for cash_transaction, url: { action: :confirm } do |f| %>
<%= f.input :amount %>
<%= f.button :submit, 'Submit' %>
<% end %>
# confirmation page under views/cash_transactions/_confirm.html.erb
<div>
Total of withdrawal: <%= #cash_transaction.amount.to_i %>
</div>
<%= link_to 'Confim', cash_transactions_withdrawals_path(#cash_transaction), method: :post %>
<%= link_to 'Cancel', cash_transactions_path %>
And everything works until the user clicks confirm button in views/cash_transactions/_confirm.html.erb - instead of creating a record an error appears:
param is missing or the value is empty: cash_transaction
Did you mean?
authenticity_token
action
controller
_method
where did I go wrong? or there is a completely different way to do so?
tl/dr: You need to add parameters to your create request.
Why this is happening
The /confirm view is being rendered with an (unsaved) #cash_transaction object, however that object is not being used and so the information is being lost when the page is rendered.
The line:
<%= link_to 'Confim', cash_transactions_withdrawals_path(#cash_transaction), method: :post %>
Will submit a POST request with no parameters to the /cash_transactions/withdrawals#create (because you've given it no parameters to post). It doesn't know to include the params from the previous request.
There are a few options to fix this... you can add params as URL parameters in link_to like this, however I wouldn't recommend posting with params in the URL.
You can use button_to instead, and pass in the cash_transaction arguments from the previous request in the params: option (or pull them out of the unsaved #cash_transaction object).
Approach #1 - reuse create params
# Get them from the params sent in the previous request. In the controller...
def create
#cash_transaction = CashTransaction.create!(cash_transaction_params)
# etc...
end
#...
protected
def cash_transaction_params
params[:cash_transaction].permit(:amount, :whatever)
end
helper_method :cash_transaction_params
# In the view
<%= button_to 'Confirm', {action: 'create', params: cash_transaction_params}
Approach #2 - Access attributes from the model you built
<%= button_to 'Confirm', {action: 'create', params: #cash_transaction.attributes.slice('amount', 'other_attribute') }
Or you could do something like render the form again but hidden and have the "confirm" button submit the hidden form (with { action: :create } instead of { action: :confirm}). This last solution is probably the easiest to understand.

Publish method doesn't work properly

I have an Entry model with a boolean column published, which is set to false by default. I wrote the following method in the model:
def self.publish
self.update(published: true)
end
and in my controller I have
def publish
#entry = Entry.find(params[:id]
#entry.publish
redirect_to entries_path
end
(I thought to make it similar to the calling of destroy method in the model). Finally, in my view I have this:
<%= link_to "Publish", entries_path, method: :publish %>
But when I click the link, the request is processed by create method and returns me the following error:
ActionController::ParameterMissing in Multiflora::EntriesController#create
param is missing or the value is empty: entry
The method is wrong in link_to as per the API so you have to mention one of valid Http methods (patch preferred in your case) , and then edit your route.rb file to transfer this patch request to your specified function like this:
patch'/entries/publish', to: 'entries#publish'
then change the "entries_path" to "entry_path"
so link code should look like this:
<%= link_to "Publish", entry_path, method: :patch%>
First thing, there is no HTTP method called :publish it should be :put or :patch
Second you need to pass id as parameter
<%= link_to "Publish", publish_entry_path(#entry) %>
Also you will need to add route for publish action
resources :events do
member do
put :publish
end
end
publish method should be instance method
def publish
self.update(published: true)
end
Thanks for all the answers, I've figured out what was my mistake, but I took a little think and decided to make it much more simple: I just added a checkbox to edit form, that sets :published attribute of entry true. Here it is:
<%=form_for(#entry, as: :entry, url: content_entry_path(#entry)) do |f| %>
# ...
<p>
<%= f.label "Publish" %> <br />
<%= f.hidden_field :published, value: '' %>
<%= f.check_box :published, checked: true %>
</p>
<% end %>
Anyways a lot of thanks for your answers! That was my lack of knowledge and I'll remember what have I done wrong

Why doesn't submitting this form raise an error?

Hi I am wondering why I am not getting an error when I submit a form with an action set to a method that isn't set up yet. After submission it just keeps me on the same page with different url parameters.
Here is the form:
<form>
<%= form_tag( drop_piece_path, :method => "post" ) do %>
<% 7.times do |col| %>
<%= label_tag col %>
<%= radio_button_tag(:column, col) %>
<% end %>
<%= submit_tag("Enter move")%>
<% end %>
</form>
Here is my route:
post 'drop_piece' => 'connect_four#drop_piece', as: :drop_piece
And here is my drop_piece method in my controller:
def drop_piece
redirect_to fake_path #should raise an error because fake_path is not a real path
end
I am confused because since my form is set to submit to the drop_piece_path shouldn't that trigger the drop_piece method in my controller? Which should then raise an error?
Again, right now after form submission the application just stays on the same page with params corresponding to whichever radio button I selected.
Why doesn't this trigger my drop_piece method in my connect_four controller? Thanks for the help.
This happens because you have nested form tags. One is <form> tag and another one is <%= form_tag... %>. HTML spec doesn't allow nesting forms, so the outer one is submitted. Since it doesn't have action attribute, it is submitted to the current URL.

How do I create multiple submit buttons for the same form in Rails?

I need to have multiple submit buttons.
I have a form which creates an instance of Contact_Call.
One button creates it as normal.
The other button creates it but needs to have a different :attribute value from the default, and it also needs to set the attribute on a different, but related model used in the controller.
How do I do that? I can't change the route, so is there a way to send a different variable that gets picked up by [:params]?
And if I do then, what do I do in the controller, set up a case statement?
You can create multiple submit buttons and provide a different value to each:
<% form_for(something) do |f| %>
..
<%= f.submit 'A' %>
<%= f.submit 'B' %>
..
<% end %>
This will output:
<input type="submit" value="A" id=".." name="commit" />
<input type="submit" value="B" id=".." name="commit" />
Inside your controller, the submitted button's value will be identified by the parameter commit. Check the value to do the required processing:
def <controller action>
if params[:commit] == 'A'
# A was pressed
elsif params[:commit] == 'B'
# B was pressed
end
end
However, remember that this tightly couples your view to the controller which may not be very desirable.
There is also another approach, using the formaction attribute on the submit button:
<% form_for(something) do |f| %>
...
<%= f.submit "Create" %>
<%= f.submit "Special Action", formaction: special_action_path %>
<% end %>
The code stays clean, as the standard create button doesn't need any change, you only insert a routing path for the special button:
formaction:
The URI of a program that processes the information submitted by the input element, if it is a submit button or image. If specified, it overrides the action attribute of the element's form owner.
Source: MDN
You can alternatively recognized which button was pressed changing its attribute name.
<% form_for(something) do |f| %>
..
<%= f.submit 'A', name: 'a_button' %>
<%= f.submit 'B', name: 'b_button' %>
..
<% end %>
It's a little bit uncomfortable because you have to check for params keys presence instead of simply check params[:commit] value: you will receive params[:a_button] or params[:b_button] depending on which one was pressed.
Similar solution to one suggested by #vss123 without using any gems:
resources :plan do
post :save, constraints: lambda {|req| req.params.key?(:propose)}, action: :propose
post :save, constraints: lambda {|req| req.params.key?(:finalize)}, action: :finalize
end
Notice that I avoid using value and use input name instead since submit button value is often internationalized / translated. Also, I'd avoid using this too much since it will quickly clutter your routes file.
We solved using advanced constraints in rails.
The idea is to have the same path (and hence the same named route & action) but with constraints routing to different actions.
resources :plan do
post :save, constraints: CommitParamRouting.new("Propose"), action: :propose
post :save, constraints: CommitParamRouting.new("Finalize"), action: :finalize
end
CommitParamRouting is a simple class that has a method matches? which returns true if the commit param matches the given instance attr. value.
This available as a gem commit_param_matching.
An old question, but since I've been dealing with the same situation, I thought I'd post my solution. I'm using controller constants to avoid introducing a discrepancy between the controller logic and the view button.
class SearchController < ApplicationController
SEARCH_TYPES = {
:searchABC => "Search ABCs",
:search123 => "Search 123s"
}
def search
[...]
if params[:commit] == SEARCH_TYPES[:searchABC]
[...]
elsif params[:commit] == SEARCH_TYPES[:search123]
[...]
else
flash[:error] = "Search type not found!"]
[...]
end
end
[...]
end
And then in the view:
<% form_for(something) do |f| %>
[...]
<%= f.submit SearchController::SEARCH_TYPES[:searchABC] %>
<%= f.submit SearchController::SEARCH_TYPES[:search123] %>
[...]
<% end %>
This way the text only lives in one place - as a constant in the controller. I haven't tried to figure out how to i18n this yet, however.
I have a variable number of submit buttons on my form thanks to nested_form_fields, so just using the name wasn't enough for me. I ended up including a hidden input field in the form and using Javascript to populate it when one of the form submit buttons was pressed.

form_for using :symbol not working, error [Only get, put, and delete requests are allowed]

When submitting a form to create a new object i get an error message when submitting the form. When i use the same form but then with an instance variable everything seems to go fine, any clue why the submit with the :symbol fails?
The error message says: Only get, put, and delete requests are allowed.
The code for the new form with :symbol is:
<% form_for :ecard do |f| %>
<%= label(:ecard, :title) %><br/>
<%= f.text_field :title, :tabindex => "1" %>
<%= f.submit "Create Ecard" %>
<% end %>
The form goes ok when i use
<% form_for #ecard do |f| %>
<%= label(:ecard, :title) %><br/>
<%= f.text_field :title, :tabindex => "1" %>
<%= f.submit "Create Ecard" %>
<% end %>
Some code out of my controller:
# GET new_ecard_url
# return an HTML form for describing the new ecard
def new
#ecard = Ecard.new
end
# POST ecard_url
def create
# create an ecard
#ecard = Ecard.new(params[:ecard])
if #ecard.save
flash[:notice] = "Succesfully created a new Ecard"
redirect_to :action => 'index'
else
flash[:warning] = "Error when saving Ecard"
render :action => 'new'
end
end
If the first argument is a symbol, it describes the object the form is about and name the instance variable, and then you need to provide the URL.
The actual object can be used and then it will use your routes to try and determine the URL. This means that it must know of a new_ecard_path and edit_ecard_path.
It will look at the object and see if it is a new record to determine which to use.
If you are using resource routes with the default restful routes, then you can probably just use the instance object. If you need to specify the URL that the form goes to, then use the symbol and specify the URL.
There are a few examples at http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html
A good way to see what's going on is to use Firebug and inspect the generated HTML. If you don't have Firebug, get it now, it's really invaluable.
If you create your form using the instance variable, you'll get something along the lines of:
<form id="new_ecard" class="new_ecard" method="post" action="/ecards">
So this will create a POST request to the /ecards action, which is the create method (by the way, your comment above the create method should be POST ecards_url, not ecard_url, unless you've defined it otherwise).
However if you only use the :ecard symbol instead of the instance variable, you'll get:
<form method="post" action="/ecards/new">
Since you haven't specified a URL, it uses the current one. This means your form will call itself in this case, and nothing will happen.
All this is due to all the so called magic Rails does - convention over configuration. But as danivo said, you can specify the URL manually and explicitly state each parameter for the form if you do not want to have this magic happen for you.

Resources