Turbo stream renders as plain text when user visits different page in the mean time - frames

Our app makes API calls that tend to take a few seconds. The results come back in various turbo frames that are updated using turbo streams from the controllers. When the user accesses a page that fires off an API call in the controller, and then navigates to a different page while the API call is ongoing, the resulting turbo stream renders as plain text
def data
response = read_lan_status_response('LanStatus', 2, lan_status_params)
stream = response.success? ? '' : turbo_flash_message(type: :danger, message: response.message)
stream = (stream || '') + turbo_stream.update(
'lan_status',
partial: 'ip/dashboard/lan_status',
locals: { data: response.result[:body] }
)
render turbo_stream: stream
The above controller action involves an API call that typically takes a few seconds, the result is displayed in a lan_status partial that is used to update a lan_status turbo frame on the page that fired triggered the controller action, in this case the dashboard page:
= turbo_frame_tag 'lan_status'
= render 'lan_status'
When the user navigates to a different page in the app while the controller action is ongoing, the request will still come back and render as a string that looks like a turbo stream:
<turbo-stream action="update" target="lan_status"><template> ... </template></turbo-stream>
I want it to simply ignore these turbo streams if the user has navigated to a different page. I have tried creating hidden turbo frames with the same target names on the other pages to "catch" the incoming streams but this didn't work (and in any case, the problem still seems to happen if the user navigates to a different page, and then back to the original page, so the issue is not the presence or absence of the turbo frame target, I think). Is there an obvious solution to this that I'm not seeing?

Related

Can you piggyback a data response to an Ajax partial rendering

I'm using ajax to update a partial, but i would like to get some data back from the server at the same time.
$('<%=escape_javascript #my_params.to_json%>');
$("#partial").html('<%= escape_javascript(render partial: "view/partial" )%>');
gives me the response with both data and partial rendering in it, but it breaks the Ajax render, so the partial is not rendered.
Using Rails 4, ROR. no errors in the browser or Rails
if i refresh the page i get the result i wanted.
I know this must be easier than i'm making it.
Assuming that you are using js.erb format for rendering ajax page.
Inside the view partial, write the code
run your js code here or assign to global js variable
I'll leave this here for edification, but my friends told me that they would disavow me if i abused a cookie this way. So i switched my implementation, can't afford to lose any friends. They told me to focus on making the turn around time for the ajax call fast. I still needed to know when the object had completed rendering so i simply added a hidden element in the partial and i set its size to zero and wait until the partial rendering changes the height from zero. It was pretty funny to see the interactions with two tabs open to the web page, even though i was planning to use the session ID avoid crosstalk.
Okay, I decided that for my purposes the best way was to put the data in a cookie and just detect when the cookie changed. This allowed me to change the cookie at the end of the send_data command and both get the data for the image position relative to the other image as well as detect when the send_data completed so i could slide the image into place (over the animated wait image).
it looks like this on the server
send_data(image.to_blob { self.format = 'png' },
type: 'image/png',
disposition: 'inline')
cookies[:duck_data] = { :value => $cookie_data.to_json}
and this on the client side
wait_for_paint=->
if $.cookie('duck_data') == 'keep waiting'
setTimeout (->wait_for_paint()), 250
else
my_data= $.parseJSON($.cookie('duck_data'))
do_ajax_response(my_data)

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.

How does render :js work?

How exactly does Rails render javascript code which is then executed by your browser? What is the data flow for this kind of thing.
Rails renders Javascript Code as a callback to your browser when executing a Controller View. Using something called AJAX.
This happens when I click on an element or make a call through the browser using AJAX. AJAX allows you to make REMOTE browser calls without refreshing the page. Since you don't refresh the page, the only way to update the current page that you are looking at, is to have the controller render javascript back at the browser. The browser takes this javascript and executes it like code. The javascript can be used to do anything, from updating a number that is being displayed on the page, to removing something from the page, to adding in whole DIVs of information.
SO for instance, an example of this would be that you click on a delete button on a webpage to delete your post. The delete button sends a REMOTE message to the server, usually calling a METHOD in a VIEW CONTROLLER in your RAILS project. In our case, it just told the server to delete the post from the database. It does this and then the METHOD sees that since you asked for something in the remote manner, it chooses to render a JS response.
def destroy
#note = Note.find(params[:id])
#note_id = #note.id
#note.destroy
respond_to do |format|
format.html do // This is Rendered if the Message to Delete was NOT sent remotely
flash[:success] = "Note Deleted Successfully"
redirect_to root_path
end
format.js // Since we did send it Remotely, it then renders our Javascript Response
end
end
The javascript response would be, in our case of deleting something, a message that tells the browser to remove the DIV or page element that holds the thing you are telling it to delete.
$('#note-<%= #note_id %>').hide('slow', function(){ $('#note-<%= #note_id %>').remove(); }); // Hides the Post and then Removes it in a Smooth slow animation
$('.alert').slideUp('slow', function(){ $('.alert').remove(); }); // Shows the Alert with the alert
If we didn't render the call back to the browser, since the page didn't refresh, it would not show that the post was deleted until you manually refresh the page. This allows for pages to be more dynamic and save bandwidth, since you don't have to refresh the page, you then no longer have to pull down everything again from the server, allowing a more seamless user experience. And, since you use animations with JQuery, it can look impressive as well.
Here is a mini tutorial that I found that goes over using Rails 3 and JQuery in a very similar manner:
http://blog.bernatfarrero.com/jquery-and-rails-3-mini-tutorial/

Render nothing !

I have a partial that show a form into a page.
As I submit the form, I just want that the controller do its operation and come back to the page without render anything. I just want to show the flash message of success or error and nothing more. But the controller by default try to render a new page. How can I do to avoid this behavior ?
To do what you want, only using asynchronous HTTP request with JavaScript (AJAX).
So you post the values of the form's fields and update the flash area with the result.
You can use jQuery to help in ajax requests:
<Edited>
If you do not have a flash area in your page you can create it putting a div element at the top or your page (or any other place you want).
Then in the button's onclick event you put this:
<input type=button onclick="$.post('/controller/action/update', $('#testform').serialize(), function success(data, textStatus, jqXHR){$.('#flash').html(data)})">
This function sends the form's data to the server and when return the callback function is called (success). Then you need only to update the flash content with the returned data.

rails ajax redirect

Here is my use case
I have a search model with two actions search_set and search_show.
1 - A user loads the home page which contains a search_form, rendered
via a partial (search_form).
2 - User does a search, and the request goes to search_set, the search
is saved and a redirect happens to search_show page which again
renders the search_form with the saved search preferences. This search
form is different than the one if step1, because it's a remote form
being submitted to the same action (search set)
3 - Now the user does another search, and the search form is submitted
via ajax to the search_set action. The search is saved and executed
and now I need to present the result via rjs templates (corresponding
to search_show). I am told that if the request is xhr then I can't
redirect to the search_show action? Is that right? If yes, how do I
handle this?
Here is my controller class
http://pastie.org/993460
Thanks
That's right. Either make the request non-XHR and redirect as normal, or you could try rendering the URL you want to redirect to as text or part of a JSON object which your AJAX request then uses to call document.location.href = [whatever] (but this seems hacky).
Right now what's happening is your XHR request is returning the result of the redirect, and not actually redirecting the page that made the XHR request.

Resources