Rails / JavaScript exchange CSS-Classes before rendering the view - javascript

Currently, I'm working on a Rails project where I use a Bootstrap-Theme (CSS-Files, JavaScript-Files and so on) which I've included as a Gem in the rails project.
The Markup in the views is dependent on this Bootstrap-Theme.
We have other Rails-Engines included in this project which also depends on the markup of this Gem for the views.
What we want to do is to develop a "Templating"-System.
Lets say we have a defined box, which has a header and a body. Then if the Bootstrap-Theme is included, it attaches the appropriate Bootstrap Markup. If another Theme is used, the appropriate markup will be attached for this Theme.
Right now, if we want to exchange the Bootstrap-Template in the future with a e.g. Zurb-Foundation-Template. Then we're lost, because we have to refactor each and every view and replace the Bootstrap-Like Markup with the Zurb one.
The first idea was to develop this templating in JavaScript which will change the CSS-Classes on document ready, but this is not capable to handle large DOM trees.
Are there any Gems available for this purposes? How can such a system be implemented in Ruby / Rails?
Thanks in advance!

I doubt you'll be able to find a gem that will give you this kind of functionality out of the box. Basically, to achieve this, you'll need to write all your views using a DSL that can be translated into either Bootstrap, Zurb, or some other framework.
This DSL could be implemented as a series of helper methods in a Rails Engine.
For example, your view could look something like this: (Assuming you use ERB as the templating engine)
<%= box do %>
<%= header 'Title of the Box' %>
<%= body do %>
<p>Box content here</p>
<% end %>
<% end %>
The definitions of these methods could look something like this: (Pseudo-code)
def box(options = {})
case EngineName.config.framework.to_sym
when :bootstrap
klass = '..' # the class you want
when :zurb
klass = '...' # the class you want
end
content_tag(:section, class: klass) { yield }
end
In a config/initializers/framework.rb file, you could configure the engine like so:
EngineName.config.framework = :bootstrap # or :zurb
There are a few pros and cons with this approach.
Pros
You can theoretically switch template frameworks without actually having to change your views.
Cons
You have to maintain a separate templating DSL in order to abstract away the framework-specific class names and such. This could be time consuming, especially when the frameworks require very different markup to achieve the same result. If you didn't foresee the future markup, you'd end up needing to change your views to support it anyway.
Personally, I think you should consider just going ahead with one framework and not worry about this. It's unlikely that your template DSL would be future-proof, and so creating one could just be unnecessary effort.

Related

What's the best practices for frontend in Rails?

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.

How to use Rails helpers in coffeescript js

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.

How do you limit CoffeeScript (or JavaScript) execution to a particular controller and action in Rails 3.1?

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);

Where can I put this code, containing both Javascript and Ruby, so that it can be used by different views in my RoR app?

I have some javascript code that is used in a few different pages in my website.
The code changes depending on some parameters, which are set in the Ruby on Rails controller.
For example:
acontroller.rb
def aview
#notes = get_notes
end
aview.html.erb (the actual code is much longer)
<% #notes.each do |note| %>
var <%=note["something"]%> = new Array();
<% end %>
I want the changeable javascript code to be stored in a central place, able to be used by many different views. Initially I thought to put it in a helper method, using a heredoc, but then the code is just inserted into the view, and the Ruby part of the code is not actually run. I can't just put into a private method in the controller, because there's javascript in it. Is there an easy way to do this?
Could you put it in a partial? That would allow you to share the code wherever you'd like.

Ruby on Rails with Unobtrusive JavaScript - Managing URLs

I'm using a modular system of JavaScript files when working in Rails - basically each view will have its own module in a .js file. My issue comes when I need a dynamic, Rails generated string within my JavaScript, for example translation strings and URLs.
Translations are nicely solved using babilu but I'm still stuck on the generation of URLs. I could write something that looked at the routes in the application and generate JavaScript methods which I could pass stuff like IDs of objects.
An alternative would be to pass in the already-generated URL to any functions I was calling, which sounds messy but could be the most flexible alternative.
I don't know that there's any truly pleasing way to do this, but one possibility is to have your server-side code write a small <script> block into the page to declare some variables that your packaged Javascript can discover and use.
<script>
var pageGlobals = {
interestingURL: <% ... url %>,
...
};
</script>
I've done this to keep track of things like image subdirectories that are determined by customer "syndicate" affiliation. The Javascript code just knows that whenever it needs that for a URL, it can just go look in a global object and pick out a standardized variable.
In my experience there tend to be only a small number of such things to communicate to the canned Javascript, so the <script> block tends not to get out of hand. I've buried mine in a page template so I don't even have to think about it with new pages.
Oldish question, but here's another way. The HTML 5 spec allows for custom data- attributes.
In your view:
<button id="myButton" data-url="<%= my_resource_path %>">Click me</button>
Then in your packaged js:
var myurl = $("#myButton").data("url");
See here also.
I don't like this either. The ideal solution to me would be javascript templates. Imagine in the .js file you could do:
var users_path = '<%= users_path %>';
But that would mean the .js files would have to be regenerated in every request (well, one could use caching just like with rails html).
Anyway, what you can also do is put the dynamic stuff in data- attributes. So you can do for example
<%= select_tag :select_something, select_options, 'data-url' => users_url %>
And then read that attribute out in the javascript file. I prefer this over the solution suggested by Pointy.
Edit: Well actually someone implemented the dynamic .js file idea. Seems straight forward enough, just create a javascripts controller and link to its actions via javascript_include_tag: dynamic javascript files

Categories

Resources