What is is causing the script block in the html head of a gsp to be overwritten - grails

I have 2 grails views that both refer to the same layout:
index.gsp & custom.gsp
Both pages have a <script> block in the <head>. I would like to be able to go to the custom.gsp directly OR work with the content in a tab in index.gsp.
I'm including the custom.gsp content using:
<g:include controller='controllername' action='custom'/>
It appears as if the custom.gsp's <script> block is overwritting the index.gsp's <script> block. If I remove the include everything else on the index page operates normally. If I add the include the code in the <script> block of index.gsp is not executed and upon inspection does not appear to be there.
What is really happening here and why? How can I structure things differently so this doesn't happeN?
UPDATE
I've tried moving the code in custom.gsp out of a <script> block and into a separate javascript file. However; when the include happens now it still pulls the code in as a <script> block and replaces the old one.
I've tried moving the block of each one into the body instead and I've tried adding id attributes to each thinking maybe sitemesh might see it as a unique block then. Still no luck.
UPDATE #2
I tried adding this to the body of index.gsp:
<div>this is a script
<script type="application/javascript">
var xyz='xyz';
</script>
</div>
And the text this is a script is visible but the script is gone!
UPDATE #3
update #2 is wrong, the text is visible but the script is actually there too. I thought it was gone because I was observing the same behavior as when it was gone. However; the real issue is that the entire head of index.gsp was replaced with the included page's head so index.gsp lost the libraries it needed to execute the script code.

As commented by #joshua the view returned by this call:
<g:include controller='controllername' action='custom'/>
must be a fragment instead of a whole gsp view with html, head .. tags.
Think of this when including views using g:include, if you have HomeController and NewsController (check figures below), and you want to include NewsController's view with HomeController's view,
You have to define an action on your NewsController that will only return the content fragment you see on the figure. To do this you can keep the news.gsp view but refactor it in such a way you put the content section in a new template(content fragment). I hope you get what I mean, but leave a comment if something is not clear on my example.

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..

Can I make a partial template that have readonly fields when used by show and not when used by edit and create?

I've partial templates that are used by the show, edit and create form.
In the show-form I don't want them editable, it can be confusing for the user.
Is there a simple solution for this otherwise I need a different template for the show-form or... why use a template then.
I've tied this and created 2 scripts, one that disables and one that enables.
Script 1.
$(document).ready(function(){
$('.elements').attr('readonly',true);
$('.elements').prop('disabled',true);
});
Script 2.
$(document).ready(function(){
$('.elements').attr('readonly',false);
$('.elements').prop('disabled',false);
});
Then I stored those scripts in assets\javascript.
It worked good in show but both edit and create went read-only too.
It seems like everything that is put in this directory is automatically used in each form, because even though I removed the call from the forms, it was working.
Here I show where I originally added the script-call:
<asset:javascript src="myScript_1.js"/>
</body>
</html>
I was going to add it as a comment but then it started to get long and complicated to follow.
why store it in assets ?
simply add a function block to the templates that need it
_template1.gsp
<script>
$(document).ready(function(){
setReadOnly('${someDefinition}')
function setReadOnly(value) {
if (value==='READONLY') {
$('.elements').attr('readonly',true).prop('disabled',true);
}
}
})
</script>
If this function needs to be shared by a bunch of pages, you could add it to an assets / javascript file but rather than declare it in application.js call the js file <asset:javascript tags specificially on each page
and maybe a variable that you pass to template to say when it should be called
or put that above template1 as a master template and call in each of the other templates that needs the js file (So many options)
Now on the main controller doing action
def MyController {
def view() {
String mode='READONLY'
render view: 'index', model:[instance:params,mode:mode]
}
}
the in index.gsp
if js is there then it will pick up mode and set readonly for that controller action or pass that from one template to another, the controller does not have to define actual mode, the main master view page or index page could define mode too
Maybe you need to play/understand then implement you can't rush these things otherwise you will end up wasting time and rewriting
Just to ensure we are on the same page.
By default the application.js file in the javascript folder has a tree line enabled - this by default then reads in all js files. If you wish to manually call js in different places you will need to remove this line and declare each and every js file that you use like the other lines provided in that file.
So that is the price to pay (no more auto loading js files) until declared in application.js
But then most importantly as you have noticed these are global js functions and really nothing new should be going in there that hasn't got a function something() { } a function call.
They will then react when the actual function is called rather than how you had it which was open for call from any old page since it happened as documents opened regardless

How to include one js script in all views except the landing page

I am converting css template into MVC application. Needless to say, I am just a beginner into webdesign. Template contains reference to one js script, which needs to be included on all pages except the landing page.
<script type="text/javascript" src="js/slider_func_innerpage.js"></script>
How to achieve it in MVC without creation of another master page? I tried to put this line in all views except of default view, but nothing happened. Ideally, I would like to handle this in my _Layout.cshtml file. Is it possible to set there some "if" condition, which would, if true, call partial view containing that single line I posted above? Or is there a more elegant way to load that js file into all views with the exception of the default one?
If you do not prefer to create another layout for landing page (Why not ? This seems to be the right way of doing it) , you might consider conditionally loading this js file in your layout.
So in the view for landing page, set a flag to the ViewBag dictionary
#{
ViewBag.IsLandingPage = true;
}
Now in the layout, check this flag and if it is not null and true, Exclude the inclusion of your js file
#{
if (ViewBag.IsLandingPage == null)
{
<script type="text/javascript" src="js/slider_func_innerpage.js"></script>
}
}

Render scripts for use in Partial View?

I've seen numerous sources stating that it's incorrect / bad practice to put scripts in Partial Views, but this raises a huge question...
How are you supposed to run scripts that interact with Partial Views? I've tried using #section scripts { } or something like that, but it doesn't work. At all. The section contains the scripts just fine, but they don't get rendered when the Partial is loaded into the full View.
In addition, I can't render the scripts my Partial View needs on my full View because the scripts pull values from the Model, which is only rendered on the Partial View, since it's the piece of the puzzle that actually works with data.
I hope that doesn't sound too complicated... How can I effectively, efficiently, and correctly render scripts for use with my Partial View's elements and Model?
#keyCrumbs I will not give you a direct answer, but something for you to analyze.
One of the most biggest problems in you call scripts for you partial view is replication of code.
Think you'll use ajax to get the partial view and you will continue doing this for a while. In every call you'll download the script code, and you put it in html. Every time you reset the script block in html, the functions are reset, the variables are reset. It's can be a big problem depending on your js code.
Other point is the size of the response, you can say, but js has a small size, and I'll say multiply this size for every call of a user and next multiply for ever user connected.
So finally, a solution for you, in this case is: create function in the page and call the function in partial view like this:
Your page: Page.cshtml
<script type="text/javascript">
function myPartialView_Load() {
$("birth").datepicker();
$("phone").mask("000-0000");
}
</script>
<!-- Some other code here -->
<div>
<!-- Or you can call from a ajax or other way... -->
#Html.Action("MyActionPartialView")
</div>
Your partial view: MyPartialView.cshtml
<script type="text/javascript">
$(function () { myPartialView_Load(); });
</script>
<form>
<input type="text" name="birth" id="birth" />
<input type="text" name="phone" id="phone" />
</form>
So as you see the problem lies not in putting js in partial view, but the way as you do it. All rules of good practice has a "why" behind, if you understand the "why" you can determine how far the rule is a help and not a burden. And decide break it or not.
Like a said, I'm not given you a ultimate answer but something you thing about. You can use other ways to contour the problem.

How to separate javascript libraries and calls in the Rails 3.1 asset pipeline

I'm trying to get the hang of the whole asset pipeline thing, and read the guide and several tutorials about them. But one thing that doesn't become quite clear is wether I should view my javascript asset files as a library or a place to put code that is actually run i.e. $(document).ready. Because by default all the javascript files are included, and it would be weird to have several $(document).ready's in there, not to mention that you don't want the $(document).ready function for every page to be run in the first place. What would be the way to go at this? Use my asset files as a library and put actual calls in my views (ugly)? Or is there a better way to do this?
I too ran into this issue. In a large project you can have somebody put code into document ready to, for example, add a click function to each li within a div with class container.
Now we could all argue that the above code would be too generic and of course may affect li tags in other parts of the application, but the bigger the project, the more likely it is that you will run into a conflict like this leading to unexpected behaviour.
I for one am uncomfortable with a whole bunch of document ready functions running for each and every page loaded. My solution is not necessarily the perfect one, but it's one that I have taken up and will share with you.
In the body tag of each page I add data elements signifying the controller and the action. I then have one document ready script that looks for a class named after the controller with the name Ready appended e.g. HomeReady. It will then call a method on this class (presuming it exists) named after the action. So in your asset coffee file you could write:
class #HomeReady
#index: ->
alert("Hello")
#show: ->
alert("Goodbye")
This allows control right down to the action level. When I came across your question I decided to package this solution into a gem as I have already used it in several projects. You can find it at: https://github.com/intrica/rails_document_ready
If you absolutely don't want a certain piece of initialization code to be run unless the current page is a specific controller/action, then you can try adding an empty element on the page with an id built from that info like "posts_index" using these two helpers:
"#{controller_name}_#{action_name}"
Then in your javascript you can wrap the code inside an if statement that checks for the existence of an element with the appropriate id.
edit: Here's an example of the js partial that I mentioned in the comments.
show.html.haml
= render 'map'
map.html.erb (I normally use haml but it's easier to write js in erb)
<script src='http://www.google.com/jsapi' type='text/javascript'></script>
<script type='text/javascript'>
...
</script>
It's probably not as clean as it could be and it doesn't get the benefits of being part of the asset pipeline but I don't mind because it's only something that gets included on a specific page.

Resources