How to do per-page javascript with the Rails asset pipeline - javascript

I understand that for performance reasons it is better to let the asset pipeline concatenate and minify all my javascript and send the whole lot with every page request. That's fair enough
However, a bunch of my javascript is things like binding specific behaviours to specific page elements - stuff like
$('button').click(function(e) { $('input.sel').val(this.name); }
and I would feel more comfortable if I knew that this code was being executed only on that page - not on evey other page which might coincidentally have elements with the same IDs or which matched the same selectors How do people deal with this?
I would rather not put all this stuff inline in elements, just because when it gets to be more than about two lines long, keeping javascript correctly indented inside an .html.erb file is more work than it needs to be

Here is what I do (based on some stackoverflow answers):
application_helper.rb
def body_page_name
[controller_name.classify.pluralize, action_name.classify].join
end
application.html.haml
%body{data: {page: body_page_name}}
application.js
$(function() {
var page = $("body").data("page");
if("object" === typeof window[page])
window[page].init();
});
And in appropriate js file there's an object called ControllerAction:
tickets.js
var TicketsShow = new function() {
var self = this;
self.init = function() {
// code which may call other functions in self
};
};
There's probably better way to do it, but this works for me

I'll describe what I currently do, just in case it gives anyone a better idea
1) I changed the 'body' tag in my application.html.erb to add the current controller and action as data- attributes
<body data-controller="<%= controller.controller_name %>"
data-action="<%= controller.action_name %>" >
2) I test this at the top of the relevant javascript
$(document).ready(function() {
if($('body').data('controller')=='stories') {
$('.story').click(function(e) {
var u=$(this).data('url');
u && (document.location=u);
});
}
});
I can't decide if I think this is a good idea or not

For page specific JavaScript, I typically do something like this:
Application Helper
In the application helper I create a class attribute (though you could just as well use a data attribute instead).
module ApplicationHelper
def body_attributes
controller = params[:controller].gsub('/', ' ')
action = params[:action]
version = #version ? "version_#{#version}" : nil
{
class: ([controller, action, version] - [nil]).join(' ')
}
end
end
Note I'm also adding a version string. This helps with Google content experiments, and makes A/B testing a breeze.
Application.html.haml
In my global layout file, I do something like this to insert the attributes on the body tag:
!!! 5
%html
%head
...
%body{body_attributes}
script.js
Now in my page specific script, I just check for the class attributes, like this:
$(function () {
if ($('body.pledge.new, body.pledge.create').length > 0) {
// do work here...
}
});
The advantage of this method is that getting the body by class is very quick. The script inside the conditional will not be executed at all on any page apart than the ones I choose, so minimal overhead, and I don't need to change my selectors throughout the code.
EDIT
Note that this answer is now 3 years old. You should be using client-side routing with a framework like React instead.

I'd add a class to the BODY tag, allowing you to identify each page, and therefore each control per page.
<body class='page1'>
JS:
$('.page1 button').click(function(e) { $('input.sel').val(this.name); }

I've done it and seen it done in several different ways:
Rigging up the mvc to be able to load a particular js file per page, named along the same lines as a controller file. Like: <controller-name>.js
Making a url parser in JS and then setting a global variable to the current page: UrlParams.currentView = 'dashboard'; and then saying if(UrlParams.currentView == 'dashboard') { //do specific js here }
Setting a unique identifier as the page class or ID and then targeting that with your JS selectors. $('#dashboard').xyz();

Related

How to write JavaScript for Ruby On Rails?

Let's say I have a website which has the following pages: Home About Friends.
What I want to do is create a simple JavaScript file (called test.js), so when a user has pressed a button I can do something simple such as :
function myFunction(name) {
console.log("Name = " + name);
}
Now here comes the part where I have my questions. I noticed that ruby uses project_name/app/views/layout/application.html.erb and provides the same <head> for all three pages (Home, About and Friends).
However, that's not what I want to do here! I only want the test.js to work for Friends page!
Do I reference that in my project_name/app/views/home/friends.html.erb as shown below:
<!-- At the bottom of friends.html.erb -->
<script type="text/javascript" src="directory-to-javascript-file"></script>
Also I have noticed that there is a JavaScript folder under this directory project_name/app/javascript which contains two folders; channels and packs.
Is this where I want to add my test.js, and if so in which folder?
So to summaries:
In which folder do I have to save my test.js file?
Where to reference test.js so that it is only visible by Friends Page?
You have 2 choices for to put the js, the assets pipeline or webpack.
The assets pipeline is maybe a bit simpler to understand,
in app/assets/javascripts/application.js you can simply put your test function after all the require statements (or extract it to a standalone file and require it). Make sure your application.html.erb has a <%= javascript_include_tag 'application'%>
You do not need to put a script tag in friends.html.erb, remove it and let the layout and asset pipeline / webpacker take care of it for you
Second option is to use webpacker. There are quite a few resources written about how to use it with Rails, it's probably better to get familiar with it eventually, I just thought for beginners maybe the old assets pipeline is a bitter easier; the webpack process is very similar though so once you understand one you will easily be able to transfer to the other.
Where to reference test.js so that it is only visible by Friends Page?
You can create a new layout for the friends controller.
class FriendsController < ApplicationController
layout "friends"
end
So in your layouts dir create a friends.html.erb. In friends.html.erb use<%= javascript_include_tag 'friends' %>. Next create a manifest (or pack) file for friends:
app/assets/javascripts/friends.js and require whatever you need there. This will mean friends controller will get its own layout with its own separate javascript.
I think this is what you are looking for.
you mentioned that you have directory pack in app/javascript, seems you are using web packer for js and css.
so here is the solution for you.
create a test.js inside the pack directory.
And write following in to the project_name/app/views/home/friends.html.erb that you mentioned in your question.
<%= javascript_pack_tag 'test' %>
and write all your js code in test.js file. and that will be loaded into only mentioned page.
If you want to same thing with other areas like home and about. create js files for that and include the <%= javascript_pack_tag 'js file name' %> into the appropriate html.erb file.
This will help you have same layout for view of every module, but still have it's own uniq js that required only for specific view.
You might need to restart the rails server.
Hope my answer helps.
In Rails you really want to forget the concept of "I just want to load this JS file on this page" - I would consider it an anti-pattern that will hold you back.
Rails uses both an assets pipeline / webpacker to streamline your JS delivery, turbolinks to "ajaxify" regular page loads in older versions and Rails UJS does a lot of ajax trickery. Which means that you very likely will be dealing with ajax and persistent browser sessions which make the idea into a real problem as your JS leaks into contexts you didn't intially imagine or just plain won't work unless you're reloading the whole page.
Instead think in terms of event based programming and behaviors that you can augment UI elements with. Think "I want this kind of button to do X" instead of "I want this specific thing on this page to do X". Use event handlers and css-classes to reach that goal:
document.addEventListener("click", (event) => {
if (!event.target.matches('.magic-button')) return;
console.log('You clicked a magical button!');
});
This makes your JS decoupled from the page itself and reusable and actually makes your intial problem moot. If you don't want to behavior on other pages then don't add the magic-button class.
If you REALLY want to do "per page" javascript one approach that I have used it to attach the controller_name and action_name to the body element:
<body data-controller="<%= controller_name" %>" data-action="<%= action_name %>">
That lets you know where you are in your application simply by reading the data attributes:
// given controller = foos and action = bar the events are:
// * MyApp:loaded
// * MyApp:foos#bar
// * MyApp:foos
// * MyApp:#bar
const firePageEvents = (event) => {
// read the data attributes off the body element
const body = document.querySelector('body');
const details = {
action: body.dataset.action,
controller: body.dataset.controller
};
const eventNames = [
"MyApp:loaded",
`MyApp:${controller}#${action}`,
`MyApp:${controller}`,
`MyApp:#${action}`
];
// fire the events
eventNames.forEach((name) => {
let event = new CustomEvent(name, { detail: details });
document.dispatch(event);
});
}
// fires the events either when turbolinks replaces the page
// or when the DOM is ready
if (window.Turbolinks && Turbolinks.supported) {
document.addEventListener("turbolinks:load", (event) => {
firePageEvents(event);
});
} else {
document.addEventListener("DOMContentLoaded", (event) => {
firePageEvents(event);
});
}
// this will be trigged when any page is loaded
document.addEventListener("MyApp:loaded", (event) => {
switch(event.details.action) {
case 'about':
console.log('about page loaded');
break;
case 'friends':
console.log('friends page loaded');
break;
}
});
// this will be trigged when the an action named "about" is rendered:
document.addEventListener("MyApp:#about", (event) => {
console.log('About page loaded', event.details);
});
// this will be triggered on the users load page
document.addEventListener("MyApp:users#index", (event) => {
console.log('User#index page loaded', event.details);
});
We then fire custom events that you can attach event listeners to make JS fire for specific controllers / actions.
This code belongs in your assets pipeline / packs where its effectivly concatenated and delivered.

Put javascript in external file with MVC Razor (dynamic)

An example is the easiest way to explain this one. I am working with code that has lots of javascript inside <script> tags on the actual page. I normally like to put javascript in external files (plus then it's better for refactoring when you find things in common across many pages). The difficulty is that the code if full of dynamic function names, class names and ids... like this:
function handlePosData#(mySuffix)(data) {
$('#myDiv#(mySuffix)').css('cursor', 'auto');
if (data && data.length > 0) {
$('#lstPos#(mySuffix)').data('kendoGrid').dataSource.data(data);
}
}
mySuffix is a GUID generated at the top of the the Razor code
(I am brand-new to this codebase, so don't ask me WHY it is like this. The web app can have many different popups open at once, and I am assuming this is a solution to the need to ensure unique names ... )
Any ideas how to enable keeping the same scheme, but with external javascript files?
I take it these functions are called from your razor page (rather than an external js file). If so you can do the following:
Change the function so it is standard and instead of using the razor directly in the function, use a js variable:
// this can be moved to external js
function handlePosData(data, mySuffix) { // pass in mySuffix to function so it is a js var
$('#myDiv' + mySuffix).css('cursor', 'auto');
if (data && data.length > 0) {
$('#lstPos' + mySuffix).data('kendoGrid').dataSource.data(data);
}
}
Then in your razor code, you just call the function like this:
handlePosData(data, '#(mySuffix)'); // not sure what your data is so just left that as a var that you pass in

Insert JavaScript data chunk (JSON) into an HTML page

I need to include a JavaScript object (JSON) in my HTML page.
JSON is rendered at the same time page HTML is rendered on server. Data is not retrieved using AJAX call.
I can think of two ways of doing this, and looking for feedback and recommendations.
What are good practices for passing JavaScript (JSON) blob with a page?
Option 1
HTML:
<script type='text/javascript'>
var model = { <JSON> };
</script>
.js:
function doSomething() { <use this.model here> }
Option 2
HTML:
<script type='text/javascript'>
loadModel({<JSON>});
</script>
.js (included at the top of the html file):
var model = null;
function loadModel(model) { this.model = model; }
function doSomething() { <use this.model here> }
Variation
Instead of including JSON in HTML, JSON can be stored in a separate .js file. Any comments on doing so?
Option 1 lets you include .js file anywhere, and including it at the bottom of the page makes it render faster (good thing), but since JavaScript renders the model on the page, this makes it a moot point. Still not depending on the location of the .js inclusion makes it less error prone.
Also R# complains (reasonably) about model being uninitialized.
Option 2 feels better (it encapsulate details better, for one), but .js must be included before call to loadModel.
I have seen and done both ways, but didn't notice any significant advantages of one way over the other.
Server platform should be irrelevant, but it is IIS 7.5/ASP.NET MVC 3/Razor
Forget your two suggestions - both are extremely vulnerable to XSS. NEVER PUT UNTRUSTED TEXT IN A SCRIPT TAG.
Instead, use the owasp recommendation.
Stick your (HTML encoded) JSON in the DOM like so:
<div id="init_data" hidden>
<%= html_escape(data.to_json) %>
</div>
Then read it in JavaScript like so:
// external js file
var dataElement = document.getElementById('init_data');
// decode and parse the content of the div
var initData = JSON.parse(dataElement.textContent);
There would be ever so slightly more overhead with option two. As you have the overhead of a function call, and an extra variable (your parameter), which will be allocated and deallocated.
As you said, there is little advantage/disadvantage to either way.
Can you use jQuery? Then you can use the DOM ready event instead of including javascript in your HTML.
EDIT:
Hmm, in that case you could include the JSON inside a hidden element when the page is generated. Then inside the DOM ready event you could read and parse it from the page using jQuery.
Another alternative might be to use HTML 5 data attributes and including the data in one of those.
If it were me I'd probably just use an ajax call since it is easier and seems a little cleaner.

AJAX & ASP.net, Referencing server controls in external file

I've got some JavaScript in an ASP.NET page that looks like this:
var list = $get('<%=Topics.ClientID %>');
I have many functions written now where this syntax is used, and I would like to centralize my JavaScript and move this into an external JavaScript file. This breaks however, since 'Topics' cannot be found.
What is the best strategy for getting this to work? I assume I should pass the control/control information as a parameter to the function, but I can't seem to get the syntax to work. Any suggestions?
It's a common problem for ASP.NET JS development. As for me, I'm using same approach each time and it looks fine.
I'm used to OOP in Javascript, so most my JS external files look like:
function CouponManager()
{
}
And in .aspx code i do:
<script language="javascript">
var couponManager = new CouponManager();
</script>
If I need to pass some parameters I change the declaration of class to:
function CouponManager(params)
{
// .. stuff here
this.initialize = function(initParams)
{
// .. accessing initParams variable for server control IDs
};
this.initialize(params);
}
And from .aspx code I do the following:
<script language="javascript">
var couponManager = new CouponManager
({
txtCouponNameId = '<%= txtCouponName.ClientID %>',
txtCouponDescriptionId = '<%= txtCouponDescription.ClientID %>'
});
</script>
This approach allows me to separate JS from .aspx page and have all server control dependencies in a single tag.
You should create a javascript method from inside the usercontol which returns the client side element. Then in your other page/control, just access that method
In User Control
<script language="javascript">
function GetTopics() {
return = $get('<%=Topics.ClientID %>');
}
</script>
In other page/control
<script language="javascript">
var list = GetTopics();
</script>
Edit - The problem you are facing is you need Topics.ClientID where it doesn't exist. So the only real way to bridge that gap is to put it in a common place. If you really don't want to do that, you can try and select your element by some other criteria. If you are using jQuery, you could mark an element with a class of Topics then find it with $(".Topics").
if you know that you only have one server control called "Topics" per page, and you use naming conventions you can inherit from whatever the control Topics is (maybe it's a HiddenField? you don't specify) and override its ClientId getter to return its server id like this:
http://andreascode.wordpress.com/2009/04/27/tiny-drips-of-aspnet-juice/
then you can know in your javascript files that there will be a hidden field in the page with the id set to "Topics" and use that directly.
depending on your domain/situation this could either save you a lot of time or screw you over big time.

Javascript functions

We are attempting to only make available certain functions to be run based on what request address is.
I was wondering how we could do this:
if(condition1)
{
$(document).ready(function() {
...
...
// condition1's function
});
}
else if(condition2)
{
$(document).ready(function() {
...
...
// condition2's function
});
else if...
I was wondering what a good pattern would work for this? since we have all of our functions in one file.
It depends on what your conditions are like...
If they're all of a similar format you could do something like
array = [
["page1", page1func],
["page2", page2func],
...
]
for(i=0; i<array.length; ++i)
{
item = array[i];
if(pageName == item[0]) $(document).ready(item[1]);
}
I like Nick's answer the best, but I might take a hash table approach, assuming the 'request address' is a known fixed value:
var request_addresses = {
'request_address_1': requestAddress1Func,
'request_address_2': requestAddress2Func
};
$(document).ready(request_addresses[the_request_address]);
Of course, request_addresses could look like this as well:
var request_addresses = {
'request_address_1': function () {
/* $(document).ready() tasks for request_address_1 */
},
'request_address_2': function () {
/* $(document).ready() tasks for request_address_2 */
}
};
I don't see any problem with that. But this might be better:
$(document).ready(function() {
if (condition1)
// condition1's function
else if (condition2)
// condition2's function
...
});
It would probably be cleaner to do the site URL checking on the server (if you can?) and include different .js files depending on the condition, e.g.
** Using ASP.NET MVC
<html>
<head>
<%
if(Request.Url.Host == "domain.com")
{ %><script type="text/javascript" src="/somejsfile1.js"></script><% }
else
{ %><script type="text/javascript" src="/somejsfile2.js"></script><% }
%>
</head>
</html>
This way, each js file would be stand-alone, and also your HTML wouldn't include lines of JS it doesn't need (i.e. code meant for "other" sites)
Maybe you could give more detail as to what exactly you are doing, but from what I can tell why wouldn't you just make a different JS file containing the necessary functions for each page instead of trying to dump all of them into one file.
I would just leave all of the functions in one file if that's the way they already are. That will save you time in rework, and save the user time with reduced latency costs and browser caching. Just don't let that file get too large. Debugging and modifying will become horrendous.
If you keep them all in one file, Add a script onn each page that calls the one(s) you want.
function funcForPage1() {...}
function funcForPage2() {...}
Then, on page1
$(funcForPage1);
etc.
Instead of doing what you're planning, consider grouping the functions in some logical manner and namespace the groups.
You'd have an object that holds objects that holds functions and call like this:
serial = myApp.common.getSerialNumber(year,month);
model = myApp.common.getModelNumber(year);
or
myApp.effects.blinkText(textId);
If you wanted to hide a function or functions per page, I suppose you could null them out by function or group after the load. But hopefully having things organized would satisfy your desire to clean up the global namespace.
I can't think of a particularly elegant way to achieve this using only JavaScript. If that's all that's available to you, then I'd at least recommend you use a switch statement or (preferably) a hash table implementation to reference your functions.
If I had to do something like this, given my development environment is fully under my control, I'd break up the JavaScript into individual files and then, having determined the request, I would use server side code to build a custom bundled JavaScript file and serve that. You can create cache copies of these files on the server and send client side caching headers too.
This article, which covers this technique as part of a series may be of interest to you.

Categories