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.
Related
In a Sails.js application, how can I include javascript assets selectively?
For instance, if I have an admin page and admin.js lives inside the assets/js directory. How do I keep the admin.js from loading on the public index page?
I'm aware that I could move the js out to the public directory, and include the script in my admin view's template. But I'm still unable to include it after the assets.js() call inserts it's javascript. I need it to be inserted after the sails.io.js script is loaded.
Is there any way to selectively load scripts and still have access to the sails.io.js which is automatically included with the assets.js() function call? Is there a better paradigm for this kind of situation?
EDIT:
Since the release of SailsJS 0.9 and the restructuring of the asset management system, this question doesn't really apply anymore.
Sailsjs uses asset-rack to serve /assets. With the default layout page, sailsjs serves pages that look like (dummy2.js is included with an explicit < script >):
<html>
<head>
...
<script type="text/javascript" src="/assets/mixins/sails.io-d338eee765373b5d77fdd78f29d47900.js"></script>
<script type="text/javascript" src="/assets/js/dummy0-1cdb8d87a92a2d5a02b910c6227b3ca4.js"></script>
<script type="text/javascript" src="/assets/js/dummy1-8c1b254452f6908d682b9b149eb55d7e.js"></script>
</head>
<body>
...
<script src="/public/dummy2.js"></script>
...
</body>
</html>
So sailsjs does not concatenate files (at least not in development mode). sails.io (socket-io) is always included before /assets/js in layout, and before < script > on the page.
It looks like your admin.js is expecting a condition which sails.io has not yet set, perhaps its negotiating a transport with the server? Try waiting for the condition to be set.
In a Sails.js application, how can I include javascript assets selectively?
I selectively load js assets using a wrapper around assets.js(). This snippet uses Jade's "-" and "!{...}". EJS would instead use "<%...%>" and "<%=...%>"
<!-- JavaScript and stylesheets from your assets folder are included here -->
!{assets.css()}
-function selectAssets (assetsjs, files, ifInclude) {
- return assetsjs.split('\n').reduce(function (prev, curr, i) {
- var frag = curr.match(/src="\/assets\/js\/\w{1,}\-/);
- if(frag) {
- var file = frag[0].substring(16, frag[0].length - 1);
- if ((files.indexOf(file) === -1) == ifInclude) { return prev; }
- }
- return prev + curr + '\n';
- }, '');
-}
//!{assets.js()} this line is replaced by:
!{selectAssets(assets.js(), ['dummy1', 'dummy5', 'dummy6'], false)}
The above would not include /assets/js/dummy1.js, dummy5, dummy6 with the layout. If you wanted to include dummy1 and dummy5 on a particular page, you would place
!{selectAssets(assets.js(), ['dummy1', 'dummy5'], true)}
on that page.
Note: The code assumes file name don't contain "-". Its straighforward to generalize for css and templates. sails-io would be a special case for mixins.
I know this is a old question but since people are still looking for this.
Sails has a folder called tasks in the root after you create a new project.
The file you are looking for is
pipeline.js
That file holds a variable called jsFilesToInject
// Client-side javascript files to inject in order
// (uses Grunt-style wildcard/glob/splat expressions)
var jsFilesToInject = [
// Load sails.io before everything else
// 'js/dependencies/sails.io.js',
'js/sails.io.js'
// Dependencies like jQuery, or Angular are brought in here
'js/dependencies/**/*.js',
// All of the rest of your client-side js files
// will be injected here in no particular order.
'js/**/*.js'
];
Just put your script that you want loaded before sails.io.js.
This is relevant for sails 0.11.x
another way that is valid is to create a views/partials folder
and create a new file like mapScripts.ejs
drop the script tags there and in your view use
<% include ../partials/mapScripts %>
Yes, you put a condition in template. :)
or add another "block" for your js.
extends ../layout
block body
<h1>hello</h1>
block jsscripts
<scripts> ..... </script>
To answer part of your question about where included files are located.
In 0.10 version order of the files is set in file tasks/values/injectedFiles.js as I recall in previous versions it was determined in Gruntfile.js itself.
You can add reference of your custom file in tasks/pipeline.js
var jsFilesToInject = [
'js/dependencies/sails.io.js',
'js/dependencies/**/*.js',
'js/**/*.js',
'js/yourcustomFile.js'
];
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();
I'm building a Rails app that uses Pusher to use web sockets to push updates to directly to the client. In javascript:
channel.bind('tweet-create', function(tweet){ //when a tweet is created, execute the following code:
$('#timeline').append("<div class='tweet'><div class='tweeter'>"+tweet.username+"</div>"+tweet.status+"</div>");
});
This is nasty mixing of code and presentation. So the natural solution would be to use a javascript template. Perhaps eco or mustache:
//store this somewhere convenient, perhaps in the view folder:
tweet_view = "<div class='tweet'><div class='tweeter'>{{tweet.username}}</div>{{tweet.status}}</div>"
channel.bind('tweet-create', function(tweet){ //when a tweet is created, execute the following code:
$('#timeline').append(Mustache.to_html(tweet_view, tweet)); //much cleaner
});
This is good and all, except, I'm repeating myself. The mustache template is 99% identical to the ERB templates I already have written to render HTML from the server. The intended output/purpose of the mustache and ERB templates are 100% the same: to turn a tweet object into tweet html.
What is the best way to eliminate this repetition?
UPDATE: Even though I answered my own question, I really want to see other ideas/solutions from other people--hence the bounty!
imo the easiest way to do this would involve using AJAX to update the page when a new tweet is created. This would require creating two files, the first being a standard html.erb file and the second being a js.erb file. The html.erb will be the standard form which can iterate through and display all the tweets after they are pulled from the database. The js.erb file will be your simple javascript to append a new tweet upon creation, i.e.:
$('#timeline').append("<div class='tweet'><div class='tweeter'><%= tweet.username %></div><%= tweet.status %></div>")
In your form for the new tweet you would need to add:
:remote => true
which will enable AJAX. Then in the create action you need to add code like this:
def create
...Processing logic...
respond_to do |format|
format.html { redirect_to tweets_path }
format.js
end
end
In this instance, if you post a tweet with an AJAX enabled form, it would respond to the call by running whatever code is in create.js.erb (which would be the $('#timeline').append code from above). Otherwise it will redirect to wherever you want to send it (in this case 'Index' for tweets). This is imo the DRYest and clearest way to accomplish what you are trying to do.
Thus far, the best solution I found was Isotope.
It lets you write templates using Javascript which can be rendered by both the client and server.
I would render all tweets with Javascript. Instead of rendering the HTML on the server, set the initial data up as JS in the head of your page. When the page loads, render the Tweets with JS.
In your head:
%head
:javascript
window.existingTweets = [{'status' : 'my tweet', 'username' : 'glasner'}];
In a JS file:
$.fn.timeline = function() {
this.extend({
template: "<div class='tweet'><div class='tweeter'>{{tweet.username}}</div>{{tweet.status}}</div>",
push: function(hash){
// have to refer to timeline with global variable
var tweet = Mustache.to_html(timeline.template, hash)
timeline.append(tweet);
}
});
window.timeline = this;
channel.bind('tweet-create', this.push);
// I use Underscore, but you can loop through however you want
_.each(existingTweets,function(hash) {
timeline.push(hash);
});
return this
};
$(document).ready(function() {
$('#timeline').timeline();
});
I haven't tried this, but this just occurred to me as a possible solution:
In your view create a hidden div which contains an example template (I'm using HAML here for brevity):
#tweet-prototype{:style => "display:none"}
= render :partial => Tweet.prototype
Your tweet partial can render a tweet as you do now.
.tweet
.tweeter
= tweet.username
.status
= tweet.status
When creating a tweet prototype you set the fields you want to the js-template replacement syntax, you could definitely dry this up, but I'm including it here in full for example purposes.
# tweet.rb
def self.prototype
Tweet.new{:username => "${tweet.username}", :status => "${tweet.status}"}
end
On the client you'd do something like:
var template = new Template($('#tweet-prototype').html());
template.evaluate(.. your tweet json..);
The last part will be dependent on how you're doing your templating, but it'd be something like that.
As previously stated, I haven't tried this technique, and it's not going to let you do stuff like loops or conditional formatting directly in the template, but you can get around that with some creativity I'm sure.
This isn't that far off what you're looking to do using Isotope, and in a lot of ways is inferior, but it's definitely a simpler solution. Personally I like haml, and try to write as much of my mark up in that as possible, so this would be a better solution for me personally.
I hope this helps!
To be able to share the template between the javascript and rails with a mustache template there is smt_rails: https://github.com/railsware/smt_rails ("Shared mustache templates for rails 3") and also Poirot: https://github.com/olivernn/poirot.
I will explain my idea behind this:
I use python for google app engine + js + css
the main project will be stored under the src folder like this:
\src
\app <--- here goes all the python app for gae
\javascript <--- my non-packed javascript files
\static_files <--- static files for gae
now the javascript dir looks like this
\javascript
\frameworks <--- maybe jQuery && jQueryUI
\models <--- js files
\controllers <--- js files
\views <--- HTML files!
app.js <--- the main app for js
compile.py <--- this is the file I will talk more
About compile.py:
This file will have 2 methods one for the min and other for the development javascript file;
When is run will do:
Join all the files with "js" extension;
The app.js contains a variable named "views" and is an object, like a hash; Then the compiler copy the contents of each file with "html" extension located in the "/javascript/views/" dir using this rule;
example: if we have a view like this "/views/login.html" then the "views" js var will have a property named "login"; views['login'] = '...content...';
example2: "/views/admin/sexyBitcy.html" then view['admin.sexyBitcy'] = '...content...' or whatever exists in that html file..;
Then this big file will be saved into the "/src/static_files/core.js"; if is minified will be saved as "/src/static_files/core.min.js";
The javascript will use dependenccy injection, or sort of it. (:
I will explain how it will work then:
the index.html that is loaded when you come into the site loads the core.js and the jquery.js;
the core.js will create the layout of the page, as SEO is not important for the most of the pages;
the core.js uses the controllers-models-views to create the layout of course; the html for the layout is inside the var "views"; will be a heavy variable of course!
Some code:
mvcInjector = new MVCInjector;
mvcInjector.mapView(views['login'], 'login', LoginController);
parent = $('#jscontent');
jquery
view = mvcInjector.instanceView('login', parent); // <--- this will create the contents of the views['login'] in the parent node "parent = $('#jscontent');" then will instance the LoginController that will map the "SkinParts" (like in FLEX if you know); what does it mean map the "SkinParts"? - when the user will click on a button an handler for that action is defined in the controller; ex.:
// LoginController
this.init = function(){
// map skin parts
this.mapSkinPart('email', 'input[name]="email"');
this.mapSkinPart('submit', 'input[name]="submit"');
// link skin parts to handlers
this.getSkinPart('submit').click = this.login;
}
// handlers
this.login = function(event){
// connect to the db
// some problems here the get the value as the "this" keyword references to the this of the controller class, I will work it around soon
alert('open window button1' + this.getSkinPart('email').value());
}
If something is not clear just say something, I will be happy to explain;
So the question remains: is this scalable, manageable and fast enough for a big RIA application build with javascript+jquery and maybe with jqueryUI?
Thanks ;)
I like your idea quit a bit.
I would think about loading html pages by ajax, if they are big and there are many of them...
Have a look on angular project, I hope, it could help you a lot. It's a kind of JS framework, designed to work together with jQuery. Well suitable for test driven development.
It uses html as templates, you can simply create your own controllers, use dependency injector, etc... Feel free to ask any question on mailing list.
Then, I must recommend JsTestDriver - really cool test runner for JS (so you can easily run unit tests in many browsers, during development - let's say after save...)
When I play with alfresco share, I found it is difficult to track the UI and javascript. you can only see some class name in the HTML tags, But you are difficult to know how are they constructed, And When, where and how can these scattered HTML code can render such a fancy page.
Can someone help me ? Please offer several example and explain how they work!
Thanks in advance!
Here is some example that will hopefully help you (it's also available on Wiki). Most of the magic happens in JavaScript (although the layout is set in html partly too).
Let's say you want to build a dashlet. You have several files in the layout like this:
Server side components here:
$TOMCAT_HOME/share/WEB-INF/classes/alfresco/site-webscripts/org/alfresco/components/dashlets/...
and client-side scripts are in
$TOMCAT_HOME/share/components/dashlets...
So - in the server side, there is a dashlet.get.desc.xml - file that defines the URL and describes the webscript/dashlet.
There is also a dashlet.get.head.ftl file - this is where you can put a <script src="..."> tags and these will be included in the <head> component of the complete page.
And finally there is a dashlet.get.html.ftl file that has the <script type="text/javascript"> tag which usually initializes your JS, usually like new Alfresco.MyDashlet().setOptions({...});
Now, there's the client side. You have, like I said, a client-side script in /share/components/dashlets/my-dashlet.js (or my-dashlet-min.js). That script usually contains a self-executing anonymous function that defines your Alfresco.MyDashlet object, something like this:
(function()
{
Alfresco.MyDashlet = function(htmlid) {
// usually extending Alfresco.component.Base or something.
// here, you also often declare array of YUI components you'll need,
// like button, datatable etc
Alfresco.MyDashlet.superclass.constructor.call(...);
// and some extra init code, like binding a custom event from another component
YAHOO.Bubbling.on('someEvent', this.someMethod, this);
}
// then in the end, there is the extending of Alfresco.component.Base
// which has basic Alfresco methods, like setOptions(), msg() etc
// and adding new params and scripts to it.
YAHOO.extend(Alfresco.MyDashlet, Alfresco.component.Base,
// extending object holding variables and methods of the new class,
// setting defaults etc
{
options: {
siteId: null,
someotherParam: false
},
// you can override onComponentsLoaded method here, which fires when YUI components you requested are loaded
// you get the htmlid as parameter. this is usefull, because you
// can also use ${args.htmlid} in the *html.ftl file to name the
// html elements, like <input id="${args.htmlid}-my-input"> and
// bind buttons to it,
// like this.myButton =
// so finally your method:
onComponentsLoaded: function MyDaslet_onComponentsLoaded(id) {
// you can, for example, render a YUI button here.
this.myButton = Alfresco.util.createYUIButton(this, "my-input", this.onButtonClick, extraParamsObj, "extra-string");
// find more about button by opening /share/js/alfresco.js and look for createYUIButton()
},
// finally, there is a "onReady" method that is called when your dashlet is fully loaded, here you can bind additional stuff.
onReady: function MyDashlet_onReady(id) {
// do stuff here, like load some Ajax resource:
Alfresco.util.Ajax.request({
url: 'url-to-call',
method: 'get', // can be post, put, delete
successCallback: { // success handler
fn: this.successHandler, // some method that will be called on success
scope: this,
obj: { myCustomParam: true}
},
successMessage: "Success message",
failureCallback: {
fn: this.failureHandler // like retrying
}
});
}
// after this there are your custom methods and stuff
// like the success and failure handlers and methods
// you bound events to with Bubbling library
myMethod: function (params) {
// code here
},
successHandler: function MyDAshlet_successHandler(response) {
// here is the object you got back from the ajax call you called
Alfresco.logger.debug(response);
}
}); // end of YAHOO.extend
}
So now you have it. If you go through the alfresco.js file, you'll find out about stuff you can use, like Alfresco.util.Ajax, createYUIButton, createYUIPanel, createYUIeverythingElse etc. You can also learn a lot by trying to play with, say, my-sites or my-tasks dashlets, they're not that complicated.
And Alfresco will put your html.ftl part in the page body, your .head.ftl part in the page head and the end user loads a page which:
loads the html part
loads the javascript and executes it
javascript then takes over, loading other components and doing stuff
Try to get that, and you'll be able to get the other more complicated stuff. (maybe :))
You should try firebug for stepping through your client side code.
Alfresco includes a bunch of files that are all pulled together on the server side to serve each "page".
I highly recommend Alfresco Developer Guide by Jeff Potts (you can buy it and view it online instantly).
James Raddock
DOOR3 Inc.