i'm a front-end dev currently working in a team on a big Ruby on Rails project. It is not SPA. All the page rendering is done on server side.
Project had dozens of views with logic like
if true render this partial else render another partial
I try to follow DRY principles, i learned a lot from arkency blog, and tried to implement as much reusable components as possible.
But I feel the lack the of things like componentDidMount from React.
So my question is: is it ok to write inline javascript (i.e. add logic) in rails partials?
And what's the best way to write Javascript for maintainable Rails apps?
is it ok to write inline javascript (i.e. add logic) in rails partials?
My answer is no. It's not maintainable and it's ugly.
And what's the best way to write Javascript for maintainable Rails apps?
See below
You are going to get a lot of opinion based answers. There really is not true answer to this problem. My answer is 1) opinion-based and 2) no where near perfect.
That being said, I'd like to offer my own opinion. I have seen and built large and small sized Rails applications over the past few years (apps of 10 js files and apps of 100+), and I have been very dissatisfied with the organization of JavaScript in those apps and the apps I've seen across GitHub. I've seen countless JavaScript files full of unorganized and disassociated code. That doesn't seem very Rails-like to me. Over the past few months, I've been trying to find the solution to this, and there is one solution I found that gets the closest to a well organized JavaScript codebase on a Rails app. And I think it stays true to some Rails ideals. One downside to this method is it litters the global scope...I'd love to here from a JS developer on how to fix this.
Here is the Medium post:
https://medium.com/#cblavier/rails-with-no-js-framework-26d2d1646cd#.36zis335e
I have made a few tweaks to this, because sometimes you need to share code, for example, code that powers a shared form for your users. However, I'd like to give all credit to #cblavier. So please please take the time to read his post, because it has a ton of great information, and I won't go into complete detail below.
Requirements: Coffeescript, Turbolinks, and jQuery
# app/helpers/application_helper.rb
def js_class_name
action = case action_name
when 'create' then 'New'
when 'update' then 'Edit'
else action_name
end.camelize
"Views.#{controller_name.camelize}.#{action}View"
end
For the above helper, you will need to account for namespaced controllers if you app has namespaced controllers. That should be fairly easy though. I think the below would do the trick.
"Views.#{controller_path.camelize.gsub('::', '.')}.#{action}View"
Alright, now you want to add that to the <body> tag in your layout.
<body data-class-name="<%= js_class_name %>">
Time for the javascript!
# initializer.coffee
pageLoad = ->
className = $('body').attr('data-class-name')
initializePage(className)
initializePageBase(className)
initializePage = (className) ->
window.applicationView = try
eval("new #{className}()")
catch error
new Views.ApplicationView()
window.applicationView.render()
initializePageBase = (className) ->
modules = className.split('.')
modules.splice(modules.length - 1, 1)
window.baseView = try
eval("new #{modules.join('.')}.BaseView")
window.baseView.render() unless window.baseView is undefined
$(document).on 'turbolinks:load', pageLoad # turbolinks:load is master branch of turbolinks, if you are using older version, it's page:load
$(document).on 'page:before-change', ->
window.applicationView.cleanup()
true
$(document).on 'page:restore', ->
window.applicationView.cleanup()
pageLoad()
true
# app/assets/javascripts/views/application_view.coffee
window.Views ||= {}
class Views.ApplicationView
render: ->
# pretty much global JS code can be initialized here. It's nice
# to keep the render() method clean though. Like this:
#setupElements()
setupElements: ->
$('[data-toggle=tooltip]').tooltip() # just an example
cleanup: ->
Now that you have those setup, it's time to start adding your page JavaScript. Here is just a example of one for the page. users_controller#show
Views.Users ||= {}
class Views.Users.ShowView extends Views.ApplicationView
constructor: ->
# find and cache DOM objects, etc
# ex:
#someButton = $('[data-behavior=expand-user-info]')
render: ->
super() # this is important. It calls render() on ApplicationView
# example stuff
#bindEventListeners()
bindEventListeners: ->
t = this
#someButton.on 'click', ->
t.expandUserInfo()
expandUserInfo: ->
alert('woohoo!')
cleanup: ->
super()
If you noticed earlier, in the initializer.coffee method, we called a method, initializePageBase(). When I was using the structure from that Medium post, I ran into an issue where I needed the same javascript on both the edit and new views. That initializePageBase() is the first step to solving it. It will look for a BaseView class. Here is an example:
# app/assets/javascripts/views/users/base_view.coffee
Views.Users ||= {}
class Views.Users.BaseView # you don't need to extend ApplicationView here, because we are already initializing it.
render: ->
# code
One way to solve your problem is to use the react.rb gem (and the reactive_rails_generator to do the installation)
This will allow you to write your views in react style you are used to.
Even though its not a single page app react.rb can simply as your templating langugage. If you have some interactive on page features you are good to go.
There is also a reactive-record gem which might be worth looking into.
Related
I'm giving a try to RoR 6 (I'm coming from MEAN and I've not touched RoR from version 3) and I'm finding some troubles to find the best way to manage JavaScript code. Maybe because of my background.
I've read a lot about the topic (including official guides https://guides.rubyonrails.org/working_with_javascript_in_rails.html) but It seems official documentation is out of date.
According to documentation, when you generate a controller from cli it should create a .js file for that controller but it doesn't occur. Besides, right now Webpack has been added to RoR 6 and JavaScript is no longer managed by Asset Pipeline (Am I right?) but there's not any reference to this matter.
I want to find a way to write code JS for every view and keep that code isolated from the rest.
Where should I put all the JS code?
How can I get isolation for the JS code of every view?
I've added jQuery to the project due to Bootstrap (by using Yarn) and to Webpack this way but $ or jQuery is undefined:
const { environment } = require('#rails/webpacker')
const webpack = require("webpack")
environment.plugins.append("Provide", new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
Popper: ['popper.js', 'default']
}))
module.exports = environment
I'd appreciate some help.
Thanks!
SOLUTION:
I found what I was looking for --> https://stimulusjs.org
Stimulusjs, created by Basecamp, adds a JS layer to every HTML view and let us to keep order and clarity when writing JS code. It connects the JS file with the DOM and nothing more. Enough to add some JS to improve functionality.
It pairs perfectly with Turbolinks and is ready to be used with Webpack. Besides, it can be learned in 10 minutes (no more). Installation is also absurdly easy.
Anyway, if you need to get some knowledge about RoR and Webpack/Webpacker, you can visit these links:
https://webpack.js.org/guides/getting-started/
https://github.com/rails/webpacker
https://medium.com/statuscode/introducing-webpacker-7136d66cddfb
And finally, if you don't wanna use a JS framework like Stimulus for managing JS code under RoR, you can always try these gems for specific page JS:
Paloma gem: https://github.com/gnclmorais/paloma (not checked)
Pagescript gem: https://github.com/maxcal/pagescript (not checked)
The change to Webpack is very new and the documentation has not quite caught up.
Generating asset files when running the generator was only done with the old assets pipeline and even then was a not really good idea. It relied on Sprockets special require_tree directive that would slurp up all the files in the directory and add them to the manifest. In alphabetical order, so you had no control over the order of execution.
It also fooled beginners into thinking that the js they put into users.js was only executed in their users controller when in fact it was all just slurped up into a single manifest.
With Webpack you explicitly import assets.
Where should I put all the JS code?
You're encouraged to place your actual application logic in a relevant structure within app/javascript.
How can I get isolation for the JS code of every view?
While you can use javascript_pack_tag in the view itself to require specific files this is not really a good idiom as it creates unnecessary HTTP requests and logic that is hard to follow.
If you want to ensure that code is executed when a particular view loads you can add data attributes to the body tag and create special events:
# app/layouts/application.html.erb
<body data-action="<%= action_name >" data-controller="<%= controller_name %>">
// fired when turbolinks changes pages.
$(document).on('turbolinks:load', ()=>{
let data = $(body).data();
// replace myns with whatever you want
$(this).trigger(`myns:${data.controller}`, data)
.trigger(`myns:${data.controller}#${data.action}`, data)
.trigger(`myns:#${data.action}`, data)
});
Then you can wrap functionality that should happen when a special page loads by listening for your custom events.
$(document).on('myns:users#show', ()=>{
console.log("We are on users#show");
});
I don't quite understand how helpers work in the view / controllers. I have never used them.
My specific question is: almost all of my views implement AJAX. In most of my controllers, the update.js.coffee and create.js.coffee have some form of the following code:
jQuery ->
<% if #product.errors.any? %>
error_info = '<%= j(render :partial => "shared/errors", :locals => { :record => #product }) %>'
popup error_info
<% else %>
.
.
.
where popup is a javascript function to display some element.
Is there a way to abstract this into a helper? What is the best way to do this? this code is almost exactly the same in every case, except the 2 uses of #product would of course be different depending on the model in question.
If this isn't what helpers are used for, then 1) what are they used for? and 2) what should I be using instead?
Edit: BONUS QUESTION: actually, many of my new, create, edit, and update functions are similar across models. How do you DRY this up? or do you just not worry about it?
The first step is to change the extension of your file to .js.coffee.erb. This let's the asset pipeline know that you want the file to be interpreted with ERB.
The second, optional step is to add custom helpers to Sprokets so that you can call your own methods from your Coffee script files*. Simply create a new helper module and then register it in an initializer:
Sprockets::Context.send :include, CoffeeHelper
*: The assets will not be able to access all the helpers you are used to using because the ERB is run when the assets are compiled rather than as part of an HTTP request. The normal controller/helper setup is not present.
If you want to refactor this code into a rails helper it would be done just like any other helper method, you have to have all the javascript code as a string and your rails helpers will need to return a string.
Rails helpers are there to help refactor logic out of your views so that you can keep your code logic-less as possibly and it is there to allow repeated code to be more dry.
If you find that some code is being repeated across your models, you may also look into refactoring that code into a ruby module in the lib directory and include the module into your models.
I've recently read that embedding ruby inside JavaScript is not a good idea.
However, in books such as David Heinemeier Hansson's Agile Web Development with Rails, that's exactly what it does.
If embedding ruby with JS is NOT a good a idea, then what's the best practice for such case?
Given something as simple as this: (jQuery + ruby)
posts_controller
def create
#post = Post.new(params[:post])
respond_to do |format|
if #post.save
format.html { redirect_to(#post, :notice => 'Post was successfully created.') }
format.js #will use this response to process ajax
else
format.html { render :action => "new" }
end
end
end
create.js.erb
$tr = $('<tr>');
$td1 = $('<td>').text('<%= #post.title %>');
$td2 = $('<td>').text('<%= #post.content %>');
$tr.append($td1, $td2);
$('table tbody').append($tr);
How should it be refactored to follow the "best practice" of not embedding ruby with JS?(if that's the case)
I really need to be enlightened on this, and maybe get the concepts right because I have read that rails 3.1 will separate JS from Ruby completely through assets ?(Is this correct?)
Thanks !
RJS templates were essentially the way things were done for a long time, which is why you still see such a prevalence of the technique in tutorials. That being said, as you've noticed, they're on the way out, and with good reason.
I'm sure there are many reasons why RJS templates are a very bad thing, but a big one is how tightly they couple your view JS to your view HTML. Many problems arise from this, a few being:
Lack of flexibility.
Using your bit of code as an example. What if you wanted to be able to create posts from a different view? And have a different effect? Maybe there's no <table> on the page at all and you just want to pop up a "success" message? Etc.
What if you just wanted a data representation of your posts, but your javascript templates were written to manipulate HTML?
How do you handle all this in one create.js.erb, which is tightly coupled to, most likely, the posts new.html.erb template?
Complexity.
RJS templates operate in somewhat of a vacuum. Generated on server side, their execution is not bound to DOM events. This makes it tricky to do things like, say, update a <form> on a page after an object is being created, as you have no frame of reference to select the appropriate <form> in the JS template (e.g. <form id="new_post_123">). There are workarounds for this, but they're more convoluted than they need to be.
By binding to a form client-side and dealing with the result, this problem is eliminated. You don't need to find the proper form to update after a response.
With RJS you have no such binding, and you're forced to use the known structure of the HTML to retrieve and manipulate the appropraite elements. One example of unnecessary complexity.
Regarding your question:
I really need to be enlightened on this, and maybe get the concepts right because I have read that rails 3.1 will separate JS from Ruby completely through assets ?(Is this correct?)
This is essentially true. The asset pipeline, while cool, doesn't offer anything brand new. Asset frameworks like Sprockets and Sass, and the workflows they enable, have been around for a while. The Rails 3.1 asset pipeline just brings them into standard practice. It's more a matter of perspective, as DHH talked about in his recent RailsConf keynote. By bringing greater organization to these assets, js files in particular, it makes them feel more like first-class citizens, so to speak, deserving of as much attention as your back-end code. As opposed to the "junk drawer" feel of public/javascripts.
As for the implementation:
You might do it something like this (although untested and a bit simplified, e.g. in Rails 3 you might use responders for this rather than a respond_to block:
# as you have, but returning the object as data which can be handled client-side,
# rather than RJS generated by the server
def create
#post = Post.new(params[:post])
respond_to do |format|
if #post.save
format.js { render :json => #post }
else
# ...
end
end
end
// Then in your client side JS, a handler for your remote form.
$(function(){
$('#your-create-form').bind('ajax:success', function(data, status, xhr) {
// simply repeating what you had in the template, replacing the erb with
// attributes from the returned json
$tr = $('<tr>');
$td1 = $('<td>').text(data.title);
$td2 = $('<td>').text(data.content);
$tr.append($td1, $td2);
$('table tbody').append($tr);
});
}
If you embed dynamic data in JavaScript files, then you often have to give it caching rules suitable for content that can change frequently.
If you need to process dynamic content with JS then it is usually better to have a static, cacheable, reusable script that gets data from elsewhere.
In this specific example, although I'm not sure since there isn't a great deal of context, it looks like you would be better off generating HTML directly instead of generating JS that generates HTML.
I'm sure you can find this question asked by beginning PHP devs, Perl, ASP and whatever other server-side languages there are out there. There don't seem to be too many cases where you'd want to have your javascript code inline.. the only one might be where you're customizing it to a particular situation, but off the top of my head I can't think of any situations where you'd need to customize it like that.
Peformance-wise, if you have a lot of javascript and you include it all in your output, you bloat your page. A 1k page will turn into a >100k page if you're adding a big application in. And, along with that, it will likely be more difficult to cache that information since you're probably customizing the output just a little bit if the user is logged in, or if there's a special ad out that day, etc. You're much better off splitting it off into its own file so that you can "minimize" it (http://en.wikipedia.org/wiki/Minification_%28programming%29) as well.
From a developer to a developer, I can give you horror stories of having javascript (and html) embedded in both PHP and Smarty templates. Be nice to whoever you're working with and split all the languages out into their own files. Let's say you're working with Bob and he's a great javascript guy.. but doesn't know a thing about the application and how it spits out its code. Having javascript smattered in the erb's is going to be frustrating due to the lack of a single point of truth for the application - if he wants to tweak something, he's going to be looking through all your erb's to figure out where exactly he has to put it or where was it originally. And do you really want someone who knows little about your backend's structure to be poking around through those files?
There's a couple of cases though where you'd want to put bootstrap data in.. if you've got a lot of data to inject to the page on startup, having that assigned to a few variables when the page starts is more performant than going out and making an ajax request on every page load.
Please be nice to us front-end guys and unless you've got a real good reason.. keep the JS in separate files :)
Edit: Here's an example of doing what you want (I think), without really using templated javascript:
app.js:
postUpdater = function (data) {
$tr = $('<tr>');
$td1 = $('<td>').text(data.title);
$td2 = $('<td>').text(data.content);
$tr.append($td1, $td2);
$('table tbody').append($tr);
};
$("#save_button").click(function () {
$.post('/save/ur/', {the:data}, postUpdater);
});
In ruby, you'll want to just render the #post to json.. I think it's literally #post.to_json, but you may have to require 'json' in order to get it to work. This might get you there faster though: output formated json with rails 3 (I'm not really a ruby guy.. we just use it at the company I work for so of course I've been picking it up)
Now, as for a theoretical on why not to just output a templated javascript file. Let's say you've got a whole bunch of stuff that needs to happen when you save that javascript file. That means you've got to output a whole lot of stuff on that ajax request and execute it. You'd be pushing a larger file over the internet (a tiny bit slower) and possibly having to 'eval' a response (eval is evil). So, that's one bad thing. Another possibly bad thing is that you lose access to where it was you called the save from. So, if I've got this view..
new PostViewer({
savePost: function () {
var self = this;
$.post('/save/url/', {the:data}, function (response) {
self.trigger('saveSuccessful', response);
});
}
});
When the savePost gets called it will be more difficult (without possibly introducing numerous hacks) to simply tell the view "Hey, the save was successful!".
Hopefully that's a bit more on target for what you were looking for.
yes, you can write but that file extension should be
".js.erb"
I have an app where I keep track of different "Games". In the "show"-view, I would like to show the game information plus a select, which I can use to quick-jump to a game.
The quick-jump feature would obviously be some javascript but here's my problem: After some reading, I still don't get how I can have a separate javascript file for each action of a controller. I could add the code to the application.js but I think that would blow-up that file unnecessarily. Any link to a good basic guide for the interaction between Rails 3 and Javascript would also help very much. Thanks!
The answer to the following question spells this out a bit more specifically and with a helper method to make it a bit more elegant. Same idea though:
Rails 3.1 asset pipeline: how to load controller-specific scripts?
Just introduce separate javascript files per controller+action basis (e.g. CONTROLLERNAME_ACTIONNAME.js, replacing CONTROLLERNAME and ACTIONNAME with your actual names ;) ) and include them in your views with javascript_include_tag(CONTROLLERNAME_ACTIONNAME.js).
The new Rails 3.1 asset pipeline is really nice, but since all CoffeeScript (or JavaScript) files get melded down into a single file that is included in every page, it raises this question:
How do I limit the execution of my script to a particular controller or action? Is there a way within my CoffeeScript to know which controller and action was used during the request so that I can put conditional statements in my script?
Or am I approaching this the wrong way altogether?
Trevor Burnham answers this question nicely here: How do I associate a CoffeeScript file with a view?
He says:
There are two common approaches:
Make behavior conditional on the presence of a particular element. For
instance, code to run a signup sheet
should be prefaced with something like
if $('#signup').length > 0
Make behavior conditional on a class on the body element. You can
set the body class using ERB. This is
often desirable for stylesheets as
well. The code would be something like
if $('body').hasClass 'user'
And if you're interested in CoffeeScript, Trevor is working on a book that looks to be very good: http://pragprog.com/titles/tbcoffee/coffeescript
One way to restrict coffeescript to a particular view is to make a custom sprockets file for the javascript in question, similar in format to application.js. Say you call it extras.js.
//= require my_code.js.coffee
Then use javascript_include_tag "extras" to include that code in the views you want, either by making a custom layout for those views, or by using content_for()
BTW, your question stated that the rails pipeline forces you to put all your js assets in one file. That's not true. That's efficient often to avoid multiple round trips, but you can have multiple sprocket files.
Why not to put the javascript for the particular controller as a view on this controller (as they correspond there if are so specific)?
If they are general enaugh you can mark your view with classes, ids or data (html5) and make your javascript look for that (so you can reuse your code).
what i normally do is to have a yield :js under the javascripts in my layout and when I need a specific script it, I load it directly from my view with:
content_for :js do
javascript_include_tag "myscript"
end
If you are using the gon gem for your vars in coffee script you can use this pattern:
Put a flag for every action in the controller:
def index
#gps_coords = GpsCoord.all
# Flag for the CoffeeScript to decide which part to run
gon.index = true;
end
def show
#gps_coord = GpsCoord.find(params[:id])
gon.lat = #gps_coord.latitude
gon.lon = #gps_coord.longitude
gon.show = true;
end
In the correlating coffee script use those flags to distiguish between the both actions:
# index action?
if gon.index?
first_coord = gon.gps_coords[0]
map.setView([first_coord.latitude, first_coord.longitude], 15);
# show action?
if gon.show?
map.setView([gon.lat, gon.lon], 15);