Call a controller method in a view using onclick option - ruby-on-rails

Hi I have the following problem. I would like to call a controller method which executes some sql statements by onklick option of a button or a link.
So I got a method "publish" in my controller. It looks sth like this:
def publish
do execute sql statements
flash[:notice] = 'sql statements executed.'
end
And in my view I would like to have a button or a link to call this method like this:
<input type="button" onclick="<% controller.publish %>">
But I get the error:
undefined local variable or method
I also tried to route it to use this method as :action. That did not work as well. I searched now for a while and found some solutions by making this method a helper_method, though I get the same error again.
I'm pretty new to Ruby and Rails so I hope you can help me.

If, in your view, you try something like <% controller.publish %>, that will call the publish action when the view is rendered, not when a user clicks on the button.
You can do something like this:
Create a route which will invoke publish when requested. For example, say the route is /publish.
In your view, put something like this:
<input type="button" onclick="window.location = '/publish'">
The value of the onclick attribute must be valid JavaScript, which will be executed when the button is clicked. In JS, setting window.location causes the browser to navigate to a different page. In this case, we are making the browser navigate to /publish, which will cause your publish action to be invoked. Whatever you render in publish will then appear in the browser.
In the code for publish which you show above, your Ruby syntax is wrong, but I am assuming that it is just a sample, not the actual code which you are using.

Your ruby on rails code is server side.
The code in your view e.g <input type="button" onclick="<% controller.publish %>">
is client side.
You can't call server side methods directly from client side code, since the client code is just running in the user's browser.
I've not used RoR so don't know the details but you will have to submit a message to the server from the client side code, at the server side you will then want to receive this and call the publish method from there.
Hope this helps.

First, if your SQL statements are going to change any data (which it looks like they are meant to), it's important that you use a POST request and not a GET request. To do that, you either need to use a basic form submission instead of clicking on a link, OR use javascript to submit the POST request when clicking on the link. Let's talk about the second way since that will stay consistent with how you want the app to look.
Assuming jQuery, you can use the jQuery.post method to post to your publish action when the button is clicked. In the success callback, you can display the message about the sql statements having been executed.

#alex-d's answer seems to be enough. Just use button_to and at the end of your publish action you can redirect to the same page (or the same action as before, if you're loading records to show in the view).
EDIT: From #alex-d's answer:
Create a route which will invoke publish when requested. For example, say the route is /publish.
GET '/publish' to: 'Controller#publish'
In your view, put something like this:
<input type="button" onclick="window.location = '/publish'">
Now on your controller method, redirect:
def publish
do execute sql statements
flash[:notice] = 'sql statements executed.'
redirect_to route_where_the_button_view_is_path #(or render if that controller method isnt necessary)
return #to avoid multiple render or redirect error (maybe not necessary)
end
PD: My code may not work at the first try, I'm working this out from memory.
GL & HF.

Related

In asp.net MVC5 how do we execute controller code after a View's submit button is clicked?

VS2013, MVC5, VB
I currently have a page that successfully sends a POST to a payment processor when the user clicks the payment button. I would like to do some additional actions in code after the request to process the payment is made in that View. I don't know how to do that. It may be that I'm thinking of this completely wrong, but that's where I am right now.
I'm assuming I would return to an ActionResult Function to do the final things in code and then go to another View to POST to the payment processor. I guess I'm asking how to force a View to POST immediately when a Return View() is executed so the user isn't required to click a Payment button again. Ergo my question about POSTing from a controller, because all I understand how to do is POST from a View.
Adding to the original post: Suppose I want to execute Sub ClearCart() (in reality it could be any number of things) after the Payment Submit button is clicked. So that means I don't want to POST away to the payment processor's website just yet, I want to first run one more piece of code on my end, then POST to the payment processor. But in order to do that, I only understand how to get back to a controller in order to run Sub ClearCart(). So now after I've run Sub ClearCart(), I have to setup another page with another Payment Submit button, and this time the POST to the payment processor can proceed because I've run the Sub ClearCart(). But that means a user is clicking Pay twice. Once to get me back where I can run some code, and then again when it's time to go to the payment processor for real.
I could very easily be lacking some fundamental concept and therefore my question doesn't make a lot of sense, but that's why I titled this thread the way I did. If I return to a Controller to run some code, I wondered how I can effectively force the POST from Controller code so it's transparent to the user. Am I explaining any better?
I thought in MVC all code is run in the Controllers, and the Views are simply to set up the page. I don't really run server side code from the page, right? And once I hit Submit on a POST to another website, I'm gone until the user and/or return payment information comes back. If I wanted to do anything after the user commits to the payment, I don't know or understand how to do that.
Additional add to the original post: There is a moment in time when the user clicks the submit button. Once that's done, action transfers to the destination in the 'action' parameter and I have no opportunity to do anything until the user or the destination site responds back. If I want to run some code after the Submit Button is clicked, how do I do that? When I started this thread I didn't really know what code to put in this thread to explain my question, but as a result of the comments, I now think this is what I need to show from the View that the user commits from:
<form id="simForm" method='post' action='https://test.authorize.net/gateway/transact.dll'>
...all the stuff presented or included in the view for the user to make his/her decision
<input type='submit' value="Submit Payment" class="btn btn-warning" />
</form>
I want to do things after the user commits, i.e. clicks the Submit Button, but it's too late once they click. I was thinking I could have a button that says Submit Payment, but instead of POSTing to my 'action' link above, POST back to a follow-on Controller where I can execute the code I want to run after the user makes their decision and before they leave my site. But I don't understand how to (1) initiate the real POST to the payment processor from within that follow-on Controller, or (2) force that follow-on Controller's View to execute a POST to the payment processor immediately without requiring another user action.
Now I've taken everyone on this horrible journey through my thinking, it just occurred to me what I'm wanting to do in a controller is redirect from the controller in a way that is a POST with all my page inputs to an external URL. Is there such a method? The only redirect I'm familiar with is for redirecting within the site.
Thanks.
Best Regards,
Alan
One way to do this is to use ajax to get back to a Controller Action, execute the code, and then let ajax finish with the Submit. The following code seems to work fine:
The Controller:
Public Class MiscController
Inherits Controller
Public Function ActionsBeforeSubmit() As ActionResult
ClearCart()
Return New EmptyResult()
End Function
End Class
The View:
<form id="simForm" method='post' target="_blank" action='https://test.authorize.net/gateway/transact.dll'>
...PAGE CONTENT HERE
<input type='button' onclick="UserCommit()" value="Submit Payment" class="btn btn-warning" />
</form>
<script >
function UserCommit() {
$.ajax({
url: '#Url.Action("ActionsBeforeSubmit", "Misc")'
async: false,
})
$("#simForm").submit()
}
</script>
When the button is clicked, the script UserCommit() is run. Within that script ajax runs the Controller Action ActionsBeforeSubmit. Updated this post: async: false required to force Submit to wait until ActionsBeforeSubmit is done. (although some texts suggest to not use async: false) After the Action finishes, the script performs the Submit.
Best Regards,
Alan

grails simple redirect not working

Right, I appear to be doing this right and its so simple there really shouldn't be anything missing but for the life of me I can't see why its not working, I have a remote link on my list view like thus: -
<g:remoteLink action="makeReady" id="${testExecQueueInstance.id}">X</g:remoteLink>
The makeReady is empty at this point apart from I want it to show the line selected, so the controller only has this in it: -
def makeReady(Long myid){
redirect(action:"show", id:myid)
}
And the show action is the bog standard show that grails creates with just a Lond being passed in...
Why isn't this working? What dumbass obviousness thing have I missed that I'll be kicking myself over for the next few days?
<g:remoteLink makes ajax request, and your page is already rendered, server can't make redirect from it, it's already displayed.
There are two ways to make it working:
use standard link
respond with a JSON/Javascript, and make redirection on client side.
For second option it's going to be something like:
<div id="success"></div>
<g:remoteLink action="makeReady" id="${testExecQueueInstance.id}" update="success">X</g:remoteLink>
and
def makeReady(Long myid){
String url = createLink(action:"show", id:myid)
render(text: "window.location.href='${url.encodeAsURL()}'", contentType: "application/javascript")
}

How can we circumvent these remote forms drawback?

In an effort to have everything translateable in our website ( including the error messages for the validations ), we switched almost all of our forms to remote forms. While this helps with the ability to translate error messages, we have encountered other problems, like:
if the user clicks on the submit button multiple times, the action gets called multiple times. If we have a remote form for creating a new record in the database, and assuming that the user's data is valid, each click will add a new object ( with the exact same contents ). Is there any way of making sure that such things cannot happen?
Is there somewhere I could read about remote forms best practices? How could I handle the multiple clicks problem? Is switching all the forms to remote forms a very big mistake?
There is a rails 3 option called :disable_with. Put this on input elements to disable and re-label them while a remote form is being submitted. It adds a data-disable-with tag to those inputs and rails.js can select and bind this functionality.
submit_tag "Complete sale", :disable_with => "Please wait..."
More info can be found here
Easy, and you can achieve that in many ways depending your preferences:
Post the form manually simply using an ajax request and while you wait for the response disable/hide (or whatever you need) the form to ensure the user can't keep doing posts as crazy. Once you get the response from the server, again you can allow the user to post again (cleaning the form first), or show something else or redirect it to another page or again whatever you need.
Use link_to :remote=>true to submit the form and add a callback function to handle the response and also to disable/hide (or whatever you need) the form when it's submitted
Add a js listener to the form to detect when it's submitted and then disable/hide/whatever the form
As you see, there are lots of different ways to achieve what you need.
EDIT: If you need info about binding or handling a form submit from js here you'll find very easy and interesting examples that may help you to do what I suggested you! jQuery Submit
I have remote forms extensively myself, and in most cases I would avoid them. But sometimes your layout or UX demands for on-the-fly drop-down forms, without reloading or refreshing the complete page.
So, let me tackle this in steps.
1. Preventing Normal form double-post
Even with a normal form, a user could double-click your button, or click multiple times, if the user does not get a clear indication that the click has been registered and the action has started.
There are a lot of ways (e.g. javascript) to make this visible, but the easiest in rails is this:
= f.button :submit, :disable_with => "Please wait..."
This will disable the button after the first click, clearly indicating the click has been registered and the action has started.
2. Handling the remote form
For a remote form it is not that much different, but the difference most likely is: what happens afterward ?
With a remote form you have a few options:
In case of error: you update the form with the errors.
you leave the form open, allowing users to keep on entering the data (I think this is your case?)
you redirect the users to some place.
Let me handle those cases. Please understand that those three cases are completely standard when doing a normal form. But not when doing a remote call.
2.1 In case of error
For a remote form to update correctly, you have to do a bit more magic. Not a lot, but a bit.
When using haml, you would have a view called edit.js.haml which would look something like
:plain
$('#your-form-id').replaceWith('#{j render(:partial => '_form') }');
What this does: replace the complete haml with only the form. You will have to structure your views accordingly, to make this work. That is not hard, but just necessary.
2.2 Clearing the form
You have two options:
* re-render the form completely, as with the errors. Only make sure you render the form from a new element, not the just posted one!!
* just send the following javascript instead:
$('#your-form-id').reset();
This will blank the form, and normally, that would effectively render any following clicking useless (some client validation could block posting until some fields are filled in).
2.3 Redirecting
Since you are using a remote form, you can't just redirect. This has to happen client-side, so that is a tad more complicated.
Using haml again this would be something like
:plain
document.location.href = '#{#redirect_uri}';
Conclusion
To prevent double (triple, quadruple, more) posts using remote forms you will have to
disable the button after first click (use :disable_with)
clear the form after succesful submission (reset the form or render with a new element)
Hope this helps.
The simplest solution would be to generate a token for each form. Then your create action could make sure it hasn't been used yet and determine whether the record should be created.
Here's how I would go about writing this feature. Note that I haven't actually tested this, but the concept should work.
1.
Inside the new action create a hash to identify the form request.
def new
#product = Product.new
#form_token = session["form_token"] = SecureRandom.hex(15)
end
2.
Add a hidden field to the form that stores the form token. This will be captured in the create action to make sure the form hasn't been submitted before.
<%= hidden_field_tag :form_token, #form_token %>
3.
In the create action you can make sure the form token matches between the session and params variables. This will give you a chance to see if this is the first or second submission.
def create
# delete the form token if it matches
if session[:form_token] == params[:form_token]
session[:form_token] = nil
else
# if it doesn't match then check if a record was created recently
product = Product.where('created_at > ?', 3.minutes.ago).where(title: params[:product][:title]).last
# if the product exists then show it
# or just return because it is a remote form
redirect_to product and return if product.present?
end
# normal create action here ...
end
Update: What I have described above has a name, it is called a Synchronizer (or Déjà vu) Token. As described in this article, is a proper method to prevent a double submit.
This strategy addresses the problem of duplicate form submissions. A synchronizer token is set in a user's session and included with each form returned to the client. When that form is submitted, the synchronizer token in the form is compared to the synchronizer token in the session. The tokens should match the first time the form is submitted. If the tokens do not match, then the form submission may be disallowed and an error returned to the user. Token mismatch may occur when the user submits a form, then clicks the Back button in the browser and attempts to resubmit the same form.
On the other hand, if the two token values match, then we are confident that the flow of control is exactly as expected. At this point, the token value in the session is modified to a new value and the form submission is accepted.
I hate to say it, but it sounds like you've come up with a cure that's worse than the disease.
Why not use i18n for translations? That certainly would be the 'Rails way'...
If you must continue down this route, you are going to have to start using Javascript. Remote forms are usually for small 'AJAXy things' like votes or comments. Creating whole objects without leaving the page is useful for when people might want to create lots of them in a row (the exact problem you're trying to solve).
As soon as you start using AJAX, you have to deal with the fact that you'll have to get into doing some JS. It's client-side stuff and therefore not Rail's speciality.
If you feel that you've gone so far down this road that you can't turn back, I would suggest that the AJAX response should at least reset the form. This would then stop people creating the same thing more than once by mistake.
From a UI/UX point of view, it should also bring up a flash message letting users know that they successfully created the object.
So in summary - if you can afford the time, git reset and start using i18n, if you can't, make the ajax callback reset the form and set a flash message.
Edit: it just occurred to me that you could even get the AJAX to redirect the page for you (but you'd have to handle the flash messages yourself). However, using a remote form that then redirects via javascript is FUGLY...
I've had similar issues with using a popup on mouseover, and not wanting to queue several requests. To get more control, you might find it easier to use javascript/coffeescript directly instead of UJS (as I did).
The way I resolved it was assigning the Ajax call to a variable and checking if the variable was assigned. In my situation, I'd abort the ajax call, but you would probably want to return from the function and set the variable to null once the ajax call is completed successfully.
This coffeescript example is from my popup which uses a "GET", but in theory it should be the same for a "POST" or "PUT".
e.g.
jQuery ->
ajaxCall = null
$("#popupContent").html " "
$("#popup").live "mouseover", ->
if ajaxCall
return
ajaxCall = $.ajax(
type: "GET"
url: "/whatever_url"
beforeSend: ->
$("#popupContent").prepend "<p class=\"loading-text\">Loading..please wait...</p>"
success: (data) ->
$("#popupContent").empty().append(data)
complete: ->
$"(.loading-text").remove()
ajaxCall = null
)
I've left out my mouseout, and timer handling for brevity.
You can try something like that for ajax requests.
Set block variable true for ajax requests
before_filter :xhr_blocker
def xhr_blocker
if request.xhr?
if session[:xhr_blocker]
respond_to do |format|
format.json, status: :unprocessable_entity
end
else
session[:xhr_blocker] = true
end
end
end
Clear xhr_blocker variable with an after filter method
after_filter :clear_xhr_blocker
def clear_xhr_blocker
session[:xhr_blocker] = nil
end
I would bind to ajax:complete, (or ajax:success and ajax:error) to redirect or update the DOM to remove/change the form as necessary when the request is complete.

Symfony/Routing: Using POST to Avoid Query Params in URL

I want to pass the id from one action to the next action, but I do not want it seen in the URL. Is there a way to hide it?
Using Symfony, I have created a sign-up done page whose URL should be /signup/done
When the form is processed, I want to redirect to the signupSuccess action and pass the recently created ID for future use. So I use...
$this->redirect('#signup_done?id=' . $sign_up->getId());
The routing is as follows:
signup_done:
url: /signup/done
param: { module: SignUp, action: signupDone }
I have avoided the :id at the end because I don't want it in the URL.
But the resulting URL is /signup/done?id=1
Just as an experiment, I tried putting this on a template.
<?php echo link_to('Sign-up again', '#signup_done?id=1', 'post=true') ?>
Even when using post, the query parameter appears in the URL.
The need is: I want to pass the id from one action to the next action, but I do not want it seen in the URL. Is there a way to hide it?
I set the id as a parameter in the request using $request->setParameter('id', $id) and it was available in the next action.
This kept the URL clean.
If you want to post, you need a form. Back in symfony 1.2 there were helpers that you could call and made you just that - but they were removed for the promotion of promoting good code.
Depending on how you want the 'Sign up again' text to look, you can either create a simple form and a submit button, or create a link, attach a click listener, and create a form there via JS, finally post it.
Any parameter that you pass to the route in url_for, link_to and such end up in the get parameters.

RJS method outputting raw javascript

I've got a page where I'm dynamically adding elements to a from a series of select boxes. I'm using RJS to do this and it's working great. Now, these elements in the div are a series of that are accompanied by Delete buttons, in case the user wants to remove a textarea. Here's where I'm hitting a wall.
Here's the code that runs the Delete button. This is working well to my knowledge:
<%= link_to image_tag("/images/button_delete.gif", :alt=>"Delete"), :controller=>"report", :action=>"remove", :id=>#sentence.id %>
In my report controller, I've got this very simple method being called by the above code:
def remove
#sentence_id = params[:id]
end
Again, I think that's working. Now, when I activate this action by hitting the button, off we go to the RJS file, remove.rjs:
page.remove 'sentence_'+#sentence_id
And here's what I get in my browser, instead of a happily removed element!
try
{
Element.remove("sentence_63");
}
catch (e)
{
alert('RJS error:\n\n' + e.toString());
alert('Element.remove(\"sentence_63\");');
throw e;
}
My understanding is that this happens when there's no page in the current context. Online documentation on this is kind of thin.
Any help appreciated!
Cheers,
Aaron.
Try link to remote. That will build the ajax call for you and should remove the element from the page.
Then link_to_remote syntax is slightly different than the link_to syntax, so don't let that trip you up either.
Since your remove function doesn't seem to actually delete a record, if you just want to remove an HTML element from a page you can use link_to_function with the Prototype remove() method for Elements. In addition, if you've got a recent version of Rails (for example, 2.3.2) you can take advantage of the dom_id helper to auto generate the sentance_id ID attribute
<%= link_to_function(image_tag("button_delete.gif", :alt=>"Delete"), "$('#{dom_id(#sentence}').remove();" %>
An approach like this could help keep the number of methods down in your controller (unless you intend on doing something else in the controller)
Your Delete link is setup as a normal link, i.e.
<a href="/report/remove" id="sentence_63">
<img src="/images/button_delete.gif" alt="Delete" />
</a>
which triggers a normal HTTP request. Since your intent is to trigger an AJAX request, try PJ Davis' recommendation and use link_to_remote

Resources