In a Rails app, how can I make a link load in a div as opposed to refreshing the whole page? - ruby-on-rails

I'm still a beginner at web development. It's not my profession. So go easy.
I started building a rails app today, and realized it would make my application so much better if I could get certain links to display in a separate div instead of a new page, or refreshing the entire page. I'm not quite sure how to search for this, and I keep chasing red herrings with google.
Basically, I have a list in a div on the left side of the page, and when one item from that list is clicked, it should appear in the right div. (Nothing else on the page need be changed)
That's really as simple as it is. Do I need to use Javascript for this? Can I get away with the rails js defaults, or should I be using JQuery?
Is there a way to do this without javascript? I really just need a push in the right direction here, I'm tired of not even knowing how to search for this, or what documentation I should be reading.
Like I said, go easy, and you should just go ahead and err to the side of caution, and assume I know nothing. Seriously. :)
Thanks in advance,
-Kevin
(By the way, I'm developing with Rails 3)

Create your views (along with controllers) to be shown inside the div for each item on the left menu. Lets say we have the following structure now:
Item1 (Clicking on it will fetch:
http://myapp.com/item1)
Item2 (Clicking on it will fetch:
http://myapp.com/item2)
and so on...
make sure you only render the html to be put inside your content div. Should not include <head> <body> etc. tags
In your main page you may have your markup like this >
<div id="leftMenu">
Item 1
Item 2
</div>
<div id="content">
Please click on an item on the left menu to load content here
</div>
Finally, add the following Javascript (you'll need jQuery; trust me it's a good decision).
$("#leftMenu a").click(function () {
$("#content").load($(this).attr("href")); //load html from the url and put it in the #content element
return false; //prevent the default href action
});

You will need JavaScript if you want to avoid reloading the page. You can use link_to for links in your lists, and you'll need to use :remote => true to make it send AJAX requests to the server. The server will need to respond appropriately and supply HTML for your div.
link_to documentation is here: http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to (and admittedly it isn't very useful for AJAX functionality).
The last post in this thread shows one possible solution you could use.

Related

React Component not rendered properly with Turbolinks in Rails 5.1

I have a very simple Rails app with a react component that just displays "Hello" in an existing div element in a particular page (let's say the show page).
When I load the related page using its URL, it works. I see Hello on the page.
However, when I'm previously on another page (let's say the index page and then I go to the show page using Turbolinks, well, the component is not rendered, unless I go back and forth again. (going back to the index Page and coming back to the show page)
From here every time I go back and forth, I can say that the view is rendered twice more time.Not only twice but twice more time! (i.e. 2 times then 4, then 6 etc..)
I know that since in the same time I set the content of the div I output a message to the console.
In fact I guess that going back to the index page should still run the component code without the display since the div element is not on the index page. But why in a cumulative manner?
The problems I want to solve are:
To get the code run on the first request of the show page
To block the code from running in other pages (including the index page)
To get the code run once on subsequent requests of the show page
Here the exact steps and code I used (I'll try to be as concise as possible.)
I have a Rails 5.1 app with react installed with:
rails new myapp --webpack=react
I then create a simple Item scaffold to get some pages to play with:
rails generate scaffold Item name
I just add the following div element in the Show page (app/views/items/show.html.erb):
<div id=hello></div>
Webpacker already generated a Hello component (hello_react.jsx) that I modified as following in ordered to use the above div element. I changed the original 'DOMContentLoaded' event:
document.addEventListener('turbolinks:load', () => {
console.log("DOM loaded..");
var element = document.getElementById("hello");
if(element) {
ReactDOM.render(<Hello name="React" />, element)
}
})
I then added the following webpack script tag at the bottom of the previous view (app/views/items/show.html.erb):
<%= javascript_pack_tag("hello_react") %>
I then run the rails server and the webpack-dev-server using foreman start (installed by adding gem 'foreman' in the Gemfile) . Here is the content of the Procfile I used:
web: bin/rails server -b 0.0.0.0 -p 3000
webpack: bin/webpack-dev-server --port 8080 --hot
And here are the steps to follow to reproduce the described behavior:
Load the index page using the URL http://localhost:3000/items
Click New Item to add a new item. Rails redirects to the item's show page at the URL localhost:3000/items/1. Here we can see the Hello React! message. It works well!
Reload the index page using the URL http://localhost:3000/items. The item is displayed as expected.
Reload the show page using the URL http://localhost:3000/items/1. The Hello message is displayed as expected with one console message.
Reload the index page using the URL http://localhost:3000/items
Click to the Show link (should be performed via turbolink). The message is not shown neither the console message.
Click the Back link (should be performed via turbolink) to go to the index page.
Click again to the Show link (should be performed via turbolink). This time the message is well displayed. The console message for its part is shown twice.
From there each time I go back to the index and come back again to the show page displays two more messages at the console each time.
Note: Instead of using (and replacing) a particular div element, if I let the original hello_react file that append a div element, this behavior is even more noticeable.
Edit: Also, if I change the link_to links by including data: {turbolinks: false}. It works well. Just as we loaded the pages using the URLs in the browser address bar.
I don't know what I'm doing wrong..
Any ideas?
Edit: I put the code in the following repo if interested to try this:
https://github.com/sanjibukai/react-turbolinks-test
This is quite a complex issue, and I am afraid I don't think it has a straightforward answer. I will explain as best I can!
To get the code run on the first request of the show page
Your turbolinks:load event handler is not running because your code is run after the turbolinks:load event is triggered. Here is the flow:
User navigates to show page
turbolinks:load triggered
Script in body evaluated
So the turbolinks:load event handler won't be called (and therefore your React component won't be rendered) until the next page load.
To (partly) solve this you could remove the turbolinks:load event listener, and call render directly:
ReactDOM.render(
<Hello name="React" />,
document.body.appendChild(document.createElement('div'))
)
Alternatively you could use <%= content_for … %>/<%= yield %> to insert the script tag in the head. e.g. in your application.html.erb layout
…
<head>
…
<%= yield :javascript_pack %>
…
</head>
…
then in your show.html.erb:
<%= content_for :javascript_pack, javascript_pack_tag('hello_react') %>
In both cases, it is worth nothing that for any HTML you add to the page with JavaScript in a turbolinks:load block, you should remove it on turbolinks:before-cache to prevent duplication issues when revisiting pages. In your case, you might do something like:
var div = document.createElement('div')
ReactDOM.render(
<Hello name="React" />,
document.body.appendChild(div)
)
document.addEventListener('turbolinks:before-cache', function () {
ReactDOM.unmountComponentAtNode(div)
})
Even with all this, you may still encounter duplication issues when revisiting pages. I believe this is to do with the way in which previews are rendered, but I have not been able to fix it without disabling previews.
To get the code run once on subsequent requests of the show page
To block the code from running in other pages (including the index page)
As I have mentioned above, including page-specific scripts dynamically can create difficulties when using Turbolinks. Event listeners in a Turbolinks app behave very differently to that without Turbolinks, where each page gets a new document and therefore the event listeners are removed automatically. Unless you manually remove the event listener (e.g. on turbolinks:before-cache), every visit to that page will add yet another listener. What's more, if Turbolinks has cached that page, a turbolinks:load event will fire twice: once for the cached version, and another for the fresh copy. This is probably why you were seeing it rendered 2, 4, 6 times.
With this in mind, my best advice is to avoid adding page-specific scripts to run page-specific code. Instead, include all your scripts in your application.js manifest file, and use the elements on your page to determine whether a component gets mounted. Your example does something like this in the comments:
document.addEventListener('turbolinks:load', () => {
var element = document.getElementById("hello");
if(element) {
ReactDOM.render(<Hello name="React" />, element)
}
})
If this is included in your application.js, then any page with a #hello element will get the component.
Hope that helps!
I was struggling with similar problem (link_to helper method was changing URL but react content was not loaded; had to refresh page manually to load it properly). After some googling I've found simple workaround on this page.
<%= link_to "Foo", new_rabbit_path(#rabbit), data: { turbolinks: false } %>
Since this causes a full page refresh when the link is clicked, now my react pages are loaded properly. Maybe you will find it useful in your project as well :)
Upon what you said I tested some code.
First, I simply pull out the ReactDOM.render method from the listener as you suggested in your first snippet.
This provide a big step forward since the message is no longer displayed elsewhere (like in the index page) but only in the show page as wanted.
But something interesting happen in the show page. There is no more accumulation of the message as appended div element, which is good. In fact it's even displayed once as wanted. But.. The console message is displayed twice!?
I guess that something related to the caching mechanism is going on here, but since the message is supposed to be appended why it isn't displayed twice as the console message?
Putting aside this issue, this seems to work and I wonder why it's necessary in the first place to put the React rendering after the page is loaded (without Turbolinks there was the DOMContentLoaded event listener)?
I guess that this has do with unexpected rendering by javascript code executed when some DOM elements are yet to be loaded.
Then, I tried your alternative way using <%= content_for … %>/<%= yield %>.
And as you expected this give mitigate results ans some weird behavior.
When I load via the URL the index page and then go to the show page using the Turbolink, it works!
The div message as well as the console message are shown once.
Then if I go back (using Turbolink), the div message is gone and I got the ".. unmounted.." console message as wanted.
But from then on, whenever I go back to the show page, the div and the console message are both never displayed at all.
The only message that's displayed is the ".. unmounted.." console message whenever I go back to the index page.
Worse, if I load the show page using the URL, the div message is not displayed anymore!? The console message is displayed but I got an error regarding the div element (Cannot read property 'appenChild' of null).
I will not deny that I completely ignore what's happening here..
Lastly, I tried your last best advice and simply put the last code snippet in the HTML head.
Since this is jsx code, I don't know how to handle it within the Rails asset pipeline / file structure, so I put my javascript_pack_tag in the html head.
And indeed, this works well.
This time the code is executed everywhere so it makes sense to use page-specific element (as previously intended in the commented code).
The downside, is that this time the code could be messy unless I put all page-specific code inside if statements that test for the presence of the page-specific element.
However since Rails/Webpack has a good code structure, it should be easily manageable to put page-specific code into page-specific jsxfiles.
Nevertheless the benefit is that this time all the page-specific parts are rendered at the same time as the whole page, thus avoiding a display glitch that occurs otherwise.
I didn't address this issue at the first place, but indeed, I would like to know how to get page specific contents rendered at the same time as the whole page.
I don't know if this is possible when combining Turbolink with React (or any other framework).
But in conclusion I leave this question for later on.
Thank you for your contribution Dom..

Display selection of index in a partial

I've been trying to solve the following challenge all day without any luck.
When going through forum posts I came across jQuery and AJAX which are both new concepts to me and which I'd rather skip for now, if possible.
I've got a partial, "navbar-left", which shows a list of all bank accounts in my model Account.
When the user clicks on one of the items in the list, all transactions of that account should be shown in the same page at the right. The partial below links to a new page which is not how I'd like it.
The navbar-partial:
<ul class="nav nav-pills nav-stacked">
<% #accounts.each do |account| %>
<li role="presentation"><%= link_to account.account_holder, account_mutations_path(account.id) %></li>
<% end %>
</ul>
Any tips on how to get this fixed is much appreciated!
The page with the navbar at the left
The mutations in a separate page instead of a partial
Either you're sending viewers to a new page, or dynamically loading content within their current page.
If the latter, then the only solution is AJAX.
Luckily, Ruby on Rails makes transitioning from one to the other very easy.
Here is a gist of how it works:
<%= link_to account.account_holder, account_mutations_path(account.id), remote: true %>
This was pointing back to some page previously (e.g. action.html.erb).
Because of remote: true, it's going to be sending JS directly to the browser instead of a new HTML page (e.g. action.js.erb in the same view folder and same action name).
Here we can control the behavior we want by rendering a partial using ERB and using JS to change the HTML content of some part of the page:
// action.js.erb
$('#some_element').html('<%= j render "partial" %>')
Which will insert the HTML of the partial directly into the JQuery that changes the content dynamically.
Where j is a shorthand for escape_javascript.
Without escaping, the Ruby output is interpreted as file output and newlines would break your JS.
Example JS output without escaping:
// Bad
$('#some_element').html('<span>Content</span>
<span>More Content</span>')
Example with escaping:
// Good
$('#some_element').html('<span>Content</span>\n<span>More Content</span>')
http://guides.rubyonrails.org/working_with_javascript_in_rails.html
https://launchschool.com/blog/the-detailed-guide-on-how-ajax-works-with-ruby-on-rails
There are more great examples online and even Railscasts.
Really AJAX is the best way to do this, and it's not as complicated as you might think. But if you really want to skip AJAX then your best approach is probably to load ALL transactions for all accounts, in different div's and then show or hide them based on which is clicked.
For a rudimentary introduction to this look at javascript tabs... you click on a tab, the appropriate information is shown.
http://www.w3schools.com/howto/howto_js_tabs.asp
You can do this very simply without ajax. The big difference would be - it's not the same page. One page would be the account#index (as you have now), the other page is the account#show page.
For the show page, use a very similar view as the index page, the left side would include the partial with one of the account li class="active" to highlight the account you are currently on. For the right side of the page, render the account mutations list items.

How to accomplish this MVC layout

Being relatively new to MVC I have been struggling for the past several weeks getting my layout to work.
I have managed to get myself really twisted into knots. So instead of trying to explain and unravel my mess perhaps instead someone could explain how I would accomplish the following at a high level.
_Layout this would have all the css js etc. It would also have basic structure.
Of course HTML tags not allowed in code block....each render is in a div.
#RenderPartial(Header)</div>
#RenderBody()</div>
#RenderPartial(Footer)</div>
RenderBody is Index.cshtml and it would be broken into three pieces
#
#Html.Partial(NavMenu, model)</div>
#Html.Partial(SubNavMenu, model)</div>
#Html.Partial(MainContent, model)</div>
I have this basic layout and it looks fine until you click one of the menu items.
The menu items render as:
<a class="k-link" href="/stuffroute">Stuff</a>
That route goes to a controller that returns a view and that navigates away from the above arrangement in Index.cshtml. So I end up with the header, footer, and subdash nav....
So the question is...
How do I route / orchestrate my layout to not lose the differing pieces?
Partials don't do anything for you here. You're essentially asking about how to create SPA (single page application), although in this case your application will have other pages, it's just that the index view will act like a SPA.
That requires JavaScript, specifically AJAX, to make requests to endpoints that will return HTML fragments you can use to replace portions of the DOM with. For example, clicking "Stuff 1" causes an AJAX request to be made to the URL that routes to FooController.GetSubNav([stuff identifier]). That action then would use what was passed to it to retrieve the correct sub-nav and return a partial view that renders that sub-nav. Your AJAX callback will then take this response, select a portion of the DOM (specifically the parent of the sub-nav) and insert the new HTML as its innerHTML.
If you're going to be doing a lot of this, you'll want to make use of some client-side MVC-style JavaScript library, like Angular for example. These make it trivial to wire everything up.

How to hide a content placeholder if it has no children without client code (MVC)

I have a ContentPlaceholder inside of a MasterPageView. All of my other pages come from the same master and I have one page that needs about 70% of the behavior in this master. There is a navigation panel in the master that is spitting out un-necessary html even if left blank by the page. Looks like this:
<div class="span3">
<div class="side_navigation">
<ul>
<asp:ContentPlaceHolder ID="SideNavigation" runat="server" />
</ul>
</div>
</div><%-- /master sub-navigation --%>
I simply want to hide ALL of this markup whenever my placeholder (SideNavigation) has 0 children. I don't want to use javascript. I'd rather do this work on the server and deliver it to the client with less responsibility and markup. I've already tried doing "this.SideNavigation.Controls.Count" but it always ends up being 0. If there was a way I could tie into a loaded event and then test this logic that would be great. I am ok with making a code-behind file for my master, but it would be nice to be able to accomplish my goal in the .master file only.
Let me know what you think.
I would probably recommend using a different master page for the page without the navigation. You can have nested master pages so you don't necessarily need to duplicate code to do this.
However if you do wish to keep it like this, I would personally use a bit of javascript (with jquery) as follows
$(function(){
if($('.span3 .side_navigation ul li').length() == 0){
$('.span3').hide();
}
});
obviously i'd give span3 an ID to make it not hide every span3 but you hopefully get the idea.

MVC - How to structure views for a search form with results on same page

I have MVC 1.0 app on VS2008.
I have webpage that has a search form fields and then when search button clicked uses
Ajax.BeginForm to load results in a div. Works great!
But the results have a pager that just uses an anchor with href passing in page index to controller action.
Of course the same action that is used when the search button is clicked.
So what happens is the results are displayed in new page by themselves. Because the links
are not called using Ajax.
So how can I structure my views and actions so that when a link is clicked in the pager
that the form is submitted to the action as well as the page index for the results??
Do you understand me??
Malcolm
I think I understand what you are saying.
Currently, you're using Ajax to dynamically update your results to a div. Kewl.
The trick here is to make sure each 'page' in the pager has a similar javascript function defined on the onclick event. This way, the pager doesn't do a 'postback' to the server, but the javascript method is ran ... which calls some ajax.
here's some sample html...
<a href="#" onclick="DoPagedSearch(1)>1</a> |
<a href="#" onclick="DoPagedSearch(2)>2</a> .. etc
does this make sence? make sure the pager is NOT inside a form AND notice the '#' characters? that makes sure that when u click on the text, it doesn't try and goto another HTML page, elsewhere.
Do you know how to wire up any javascript to an html element? How do u create the html code for the pager?
try that and keep us posted.
Use jquery to have the page anchors make an ajax call to the controller. Return the results as JSON or xhtml or whatever format makes you feel happy and use that to replace the content of the div, or build up and replace the contents if JSON.
If you haven't dug into jquery, I highly recommend it. The documentation is rather excellent. Let me provide you a few useful links for this:
JSON.net serializer
jQuery Documentation
fair example of using jquery for paging
The example uses an rss feed (xml) as the source, but It should get you going.

Resources