Object-oriented way to separate page-specific JavaScript in Rails - javascript

Assume I have a basic Rails app with three resources: authors, blogs, and books. I created the resources with the scaffold command, so each resource has their own JavaScript file: authors.js, blogs.js, and books.js.
I saw a couple different implementations on how we can keep the global namespace clean for our JavaScript, as well as keep our page-specific JavaScript separate from each other so they don't clash.
Here is my devised implementation. It appears to do what I want, but I wanted to get feedback if this would be considered a bad implementation/bad practice for some reason. I would also welcome other implementations if people have a better way.
For all layouts within app/views/layouts specify this for the body tag:
<body class="<%= controller_name %>">
For the JavaScript code: As we know, all JavaScript in Rails is eventually pulled together into one big JavaScript file. For readability purposes here I pulled all the JavaScript from each of the separate JavaScript files and put them together (within the app I would keep the JavaScript code separate within each of their corresponding js files):
var authorsjs = {
init: function(){
alert("hello from authorjs");
// all author bindings specified here
$("#oneEl").on('click', function(){
alert("oneEl was clicked");
});
},
someAuthorFunc: function(){
alert("someAuthorFunc run");
}
};
$(document).on('page:change', function(){
if($('body').hasClass("authors") == true ){
authorsjs.init();
}
});
var booksjs = {
init: function(){
alert("init booksjs run");
// all books bindings specified here
$("#twoEl").on('click', function(){
alert("twoEl was clicked");
});
},
someBookFunc: function(){
alert("someBookFunc run");
}
};
$(document).on('page:change', function(){
if($('body').hasClass("authors") == true ){
authorsjs.init();
}
});
So as expected, these click events will not work for views associated to blogs, the event bound to #oneEl only works on views associated with authors, and the event bound to #twoEl only works on views associated with books.

Answer based on feedback
The following works. I am using the jquery-turbolinks gem. Each of the controller's js is encapsulated in an object. For each page request the body class is checked and the appropriate object's init method is run. I also went ahead and created a someGlobals object which provides common js code across all controllers.
This code works as intended but is quite messy. I wish there was some way to clean up front-end code. If you have any suggestions let me know:
$(document).ready(function(){
var authorsjs = {
init: function(){
alert("hello from authorjs");
// all author bindings specified here
$("#oneEl").on('click', function(){
alert("oneEl was clicked");
});
},
someAuthorFunc: function(){
alert("someAuthorFunc run");
}
};
if($('body').hasClass("authors") == true ){
authorsjs.init();
}
});
$(document).ready(function(){
var booksjs = {
init: function(){
alert("init booksjs run");
// all books bindings specified here
$("#twoEl").on('click', function(){
alert("twoEl was clicked");
booksjs.someBookFunc();
});
},
someBookFunc: function(){
alert("someBookFunc run");
}
};
if($('body').hasClass("booksjs") == true ){
booksjs.init();
}
});
$(document).ready(function(){
var blogsjs = {
init: function(){
blogsjs.formValidations();
},
formValidations: function(){
$('#blog_title').on("click", function(){
console.log($(this).val());
$('p').css("color", "green");
$(this).css("color", "blue");
alert("hello");
});
$('#blog_title').on("keypress", function(){
var value = $(this).val();
$("#preview").text(value);
});
}
};
if($('body').hasClass("blogs") == true){
blogsjs.init();
}
});
$(document).ready(function(){
var someGlobals = {
init: function(){
$('p').on('click', function(){
alert("Global: P was clicked somehwere!");
})
}
};
someGlobals.init();
});

Related

How to make function execute only once in JS / JQuery

i have a structure like this :
// App.js
var APP = {
viewIndex: function(){
EVT.doSomething();
},
// Another function below
}
// Event.js
var EVT = {
doSomething: function(){
deleteField();
function deleteField(){
$("body").on("click", "#btn", function(){
console.log("Clicked");
})
}
}
}
my project is SPA wannabe, so when i want to change the page, it must execute some function inside App.js, but my problem is, when i call APP.viewIndex() (when i go to Index, go back, and go to index again(without refreshing page) ), the function inside EVT -> doSomething() is execute twice, so i have no idea how to prevent it,
in my console i got this :
Clicked
Clicked
*sorry if my explanation is a bit complicated
hope you guys can help me out of this problem :D
thanks
Use a property to remember if you already called deleteField().
var EVT = {
didSomething: false,
doSomething: function(){
if (!this.didSomething) {
deleteField();
this.didSomething = true;
}
function deleteField(){
$("body").on("click", "#btn", function(){
console.log("Clicked");
})
}
}
}

Ember js - how to create a shared uitility

I am writing code to detect breakpoints via js using match media. In plain js, I know how to create a separate utility for this and consume it, but in Ember, how do I go about it, would I need to create a helper or something else.
Plain JS code:
define('viewportDimension', function() {
var viewportSize ={
mqMaxTablet : '959px',
isTablet: function(){
if(matchMedia){
var mq = window.matchMedia("(max-width:" + this.mqMaxTablet+ ")");
mq.addListener(this.viewportChanged);
this.viewportChanged(mq);
}
},
viewportChanged: function(mq){
if(mq.matches){
return true;
}
}
};
return viewportSize;
});
Ember Controller:
isTablet: function (){
viewportDimension.isTablet();
}.property('')
I understand the above code will not work. I dont know how to make it more Ember'ish type. The "isTablet" property should be set to true, as soon as the media query match is done.
Started converting my plain js to emberish (as below), but dont know how to proceed.
define('viewportDimension',function(){
var viewportSize = Ember.Object.extend({
isTablet: function(){
alert("1");
}.property('')
});
return viewportSize;
});
Ember.Application.initializer({
name: 'viewport-dimension',
initialize: function(container,app){
require('viewportDimension',function(object){
app.ViewportDimension = object
})
}
})
Please take a look at simple-breakpoint-detector

Emberjs common router events

I'm trying to join common events to a route, and then extend the route as needed
Like this:
App.ScrollTopRoute = Ember.Route.extend({
renderTemplate: function() {
console.log('Hi this works ?');
window.scrollTo(0, 0);
}
});
Then I extend the route:
App.TodoRoute = App.ScrollTopRoute.extend({
model: function(params) {
return App.Todo.find(params.todo_id);
}
});
The problem is the events inside ScrollTopRoute are not launch
So which is the best way to join common routines for routes?
You are doing DOM related operations which should go into the didInsertElement of a view.
So for your window.scrollTo(0, 0) to work it should be defined in such a hook, assuming you have a todos template than this should work.
App.TodosView = Ember.View.extend({
didInsertElement: function() {
window.scrollTo(0, 0);
}
});
In the case you need common code to be executed in different classes one possible way is to create a Mixin and mix it into the classes that need it.
For example:
App.CommonMixin = Ember.Mixin.create({
myCommonFunction: function() {
console.log('this works');
}
});
App.ScrollTopRoute = Ember.Route.extend(App.CommonMixin, {
...
// myCommonFunction is available here
});
App.AnotheScrollTopRoute = Ember.Route.extend(App.CommonMixin, {
...
// myCommonFunction is available here
});
As for the Mixin, see here for a simple demo.
Hope this answers your question, if not let me know so I can improve it further.

RequireJS define callback returning wrong module dependencies

I'm having a very strange and frustrating problem with RequireJS. When I call require for a module with a list of dependencies, all dependencies available in the callback reference a single module. This is probably easier to explained with code:
Including require.js script (with no data-main attribute)
<script type="text/javascript" src="/js/common/require.min.js" ></script>
Below that I include require my main.js (used in all pages of the site) which in the callback requires my page specific js.
<script type="text/javascript">
require(['/js/require/main.js'], function () {
require(['page/home_page']);
});
</script>
main.js
requirejs.config({
baseUrl: 'js/require'
});
requirejs(['base'],
function() {
var base = require('base');
base.init();
});
home_page.js
define(['home','searchbar'], function (home,searchbar){
console.log(home.init == searchbar.init); // This is always true !!!
home.init();
searchbar.init();
});
home.js
define(function(){
this.init = function(){
console.log('in home page');
}
return this;
});
searchbar.js
define(function(){
this.init = function(){
console.log('Now in the searchbar init')
}
return this;
});
The issue is in home_page.js both modules home and searchbar reference the same thing. What's strange is that now that I've simplified this example, it seems pretty random which one it chooses. Most times it's searchbar but every few refreshes it will be home.
Anyone have an ideas? Is it something terribly obvious?
EDIT: Simplified example and provided all module source.
You are assigning to this in both modules. This is not a good idea (excuse the pun). this will possibly be the window object in both cases. You could check by adding a
window.init === searchbar.init
test.
Rewrite the modules to return unique objects, like so:
define(function() {
return {
init: function() {
console.log('in home page');
}
};
});
and
define(function() {
return {
init: function() {
console.log('Now in the searchbar init');
}
};
});

Refactor $(document).ready(), currently have 2 instances

I've got following JavaScript functions but want to refactor the $(document).ready() as I've got 2 instance of it. How can I achieve this?
FlashMessenger = {
init: function() {
setTimeout(function() {
$(".flash").fadeOut("slow", function () {
$(".flash").remove();
});
}, 5000);
}
}
SelectLanguage = {
init: function() {
$('#selectLanguageId').change(function() {
$('#frmSelectLanguage').submit();
});
}
}
$(document).ready(FlashMessenger.init);
$(document).ready(SelectLanguage.init);
It’s perfectly acceptable to set multiple handlers for $(document).ready, although you may have a good reason to do otherwise that I’m not aware of. You might be interested in knowing that $(handler) can be used as shorthand for $(document).ready(handler):
$(FlashMessenger.init);
$(SelectLanguage.init);
If you really want them in one call though, try this:
$(function() {
FlashMessenger.init();
SelectLanguage.init();
});
First off, there's no reason you have to combine them.
But if you want to:
$(document).ready(function(jq){
FlashMessenger.init(jq);
SelectLanguage.init(jq);
});
Breaking it down:
Create a function to do all your init (it can be named or anonymous; the one above is anonymous).
Have it call the other init functions, passing in the jQuery instance that jQuery passes you just in case they use it.
You might choose to wrap each init call in a try/catch block as well, so that errors in one init don't prevent the next init from occuring, but that depends on your needs.
Just combine them into one call with an anonymous function:
$(document).ready(function()
{
FlashMessenger.init();
SelectLanguage.init();
});
$(document).ready(function() {
FlashMessenger.init();
SelectLanguage.init();
});
Option 1
FlashMessenger = {
init: function() {
setTimeout(function() {
$(".flash").fadeOut("slow", function () {
$(".flash").remove();
});
}, 5000);
}
}
SelectLanguage = {
init: function() {
$('#selectLanguageId').change(function() {
$('#frmSelectLanguage').submit();
});
}
}
$(function(){
FlashMessenger.init();
SelectLanguage.init();
});
Option 2
FlashMessenger = {
init: function() {
setTimeout(function() {
$(".flash").fadeOut("slow", function () {
$(".flash").remove();
});
}, 5000);
}
}
SelectLanguage = {
init: function() {
$('#selectLanguageId').change(function() {
$('#frmSelectLanguage').submit();
});
}
}
$(document).ready(function(){
FlashMessenger.init();
SelectLanguage.init();
});
Option 3
You actually don't need those 2 objects since the only hold the init methods, so here's the ultimate solution, in my opinion, unless you use those objects elsewhere.
$(function(){
$('#selectLanguageId').change(function() {
$('#frmSelectLanguage').submit();
});
setTimeout(function() {
$(".flash").fadeOut("slow", function () {
$(".flash").remove();
});
}, 5000);
})
I prefer 2 and 3 for this reason.
I think what the op is saying is, "If in the future I have a third function to be invoked at document.ready, then how do I do it without touching that piece of code?"
If you do not want multiple $(document).ready() calls, you could just create an array called startupHooks and add each method to it:
startupHooks[ startupHooks.length ] = myNewStartupHook;
and your startup script could look like
$(document).ready(function() {
for( var i=0; i<startupHooks.length; i++ ) {
startupHooks[i]();
}
}
I know that is not mighty useful, but if that appeals to you, you can do it this way.
Personally, I'd go with multiple $(document).ready() calls.
Personally I'd go for not using document.ready at all.
If you place the scripts at the end of your html-page(just before the tag) you can just write in any way you like.
Maybe this doesn't work for 0.01% of the scripts but it never failed to work for me.
Positive effect of this is that the initial HTML+CSS rendering goes faster.
You can also read about it on yahoo. http://developer.yahoo.com/performance/rules.html#js_bottom

Categories

Resources