Improve unobtrusive javascript (and possibly use CoffeeScript) in a Rails application - javascript

I have an application which uses some Javascript for basic Ajax requests such as autocompletion and live search. For example I implemented live search in the following way; I spotted some potential issue and would like to talk with you about it, so to have a better code.
app/controllers/company_controller.rb
def livesearch
#companies = Company.search(params[:query])
render :partial => "companies", :locals => {:companies => #companies}
end
app/views/companies/_companies.html.haml
- if companies.empty?
None
- else
%table#company_list
%tr
%th Name
%th Description
%th Products
%th
= render companies
app/views/companies/_livesearch_box.html.haml
= content_for :scripts, "jlivesearch companies"
= form_tag "#", :autocomplete => :off, :remote => true do
%span.light
Search:
= text_field_tag :search
:javascript
$('#search').livesearch({
searchCallback: update_listed_companies,
queryDelay: 200,
innerText: "Search companies"
});
public/javascripts/companies.js
function update_listed_companies(query) {
if(typeof query == "undefined")
query = "";
$("#company_list_container").showWith(
"/companies/livesearch?query=" + query,
false
);
}
public/javascripts/application.js
(function($) {
$.fn.showWith = function (what, popup) {
element = this;
$.get(what, function(data) {
element.html(data);
if(popup)
element.bPopup();
});
return element;
};
})(jQuery);
Here are the things that make me suspicious about the optimality of my code:
I have Javascript code in _livesearch_box.html.haml.
Even if I put it in a public/javascripts/companies_livesearch.js I would have to hardcode the #search part in it.
I have #company_list_container (which is the div in which _companies.html.haml is rendered) hardcoded in public/javascripts/companies.js.
I have the path /companies/liveseach?query= hardcoded in public/javascript/companies.js.
I'm not using CoffeeScript, mainly because it expects (at least if you use Barista) to find pure javascript code somewhere (e.g. in app/coffeescripts/) and compiles it in public/javascripts. But in my application I also have some .js.erb file in my app/views/companies; for example, I have a voting system that uses the following in
app/views/companies/_vote.js.erb:
$("#vote_link_<%= escape_javascript(#company.id.to_s) %>").html("<%= escape_javascript(vote_link_for(#company)) %>")
To replace the "Vote this company" link with the "Unvote this company" one (and vice-versa) with an Ajax request and is rendered by the vote and unvote actions in the controller.
I know that there is coffee-haml-filter that compiles CoffeeScript inside haml files but it's not what I exactly need and is usually deprecated and regarded as something dirty (?).
So the questions are at least:
How to have CoffeeScript in my app/views/*/*.js.*?
Should I have app/views/*/*.js.* files at all?
How to remove all those element ids and those paths hardcoded in javascripts in the most efficient and elegant way?
Sorry for the long question and thanks for getting to the end of it!

Routes
There are some solutions like js-routes (my fork) which will allow you to write Router.post_path(3) in your JS/CS. This way you can get around hardcoding urls.
Mixing JS and ERB
I would advise you to avoid mixing JS and Ruby. In most cases you can get around that by refactoring your JS code, the result will be easier to read and can simply be moved into a pure JS/CS-file.
# based on your vote-link example and assuming that your link
# looks like:
#
# %a(href="#"){:"data-company-id" => #company.id} Vote
# => Vote
makeAllCompaniesVotable: () ->
$('.company a.voteLink').click ->
companyId = $(this).data('company-id')
$.ajax
url: Router.vote_company_path(companyId)
# ...
Unless you do evil eval-magic, you won't even need escape_javascript. But you will have to remove the JavaScript from inside your partials. jquery.livequery made the transition easier.
$(`.company`).livequery ->
# do something with $(this)
will be called each time a .company is inserted into the document.
Hardcoding DOM-Paths
If you are writing code for a specific dom-tree (or a specifc view) I wouldn't consider it a bad practice. Writing unobtrusive JS is like writing CSS - and we hardcode #company_list_container in CSS too, don't we?
$("#vote_link_<%= escape_javascript(#company.id.to_s) %>") # this is ugly though
Calling the JS code from the frontend
To have an interface between the static CoffeeScript-files and the views I tend to write something like:
:javascript
$(function(){Companies.index()});
$(function(){Application.globalEnhancements()});
at the end of my views. This will then call a function I wrote with CoffeeScript, which will then enhance the site with all the needed scripts. There might be better approaches (like having a Rails-like Router for JavaScript - see Backbone.js) but it's simple and works for me.
Also if I need some data quite often (for example: the current_user):
:javascript
window.current_user = #{current_user.to_json};
However I don't think there is an efficient way to refactor. I had to do a lot of refactoring to get my ERB/JS mess removed. Still worth, though.

Related

<%= %> syntax in Typescript/Angular

I'm working on an Angular project that makes use of a variable called WEB_HOST. I can see in another file where the variable is defined (it's somehow defined in a class called ProjectConfig).
Definition (simplified):
export class ProjectConfig {
WEB_HOST = 'https://example.com'; // I didn't think properties could be assigned here?
constructor() { ... }
}
Now, from a completely separate *.ts file, this obviously doesn't work:
let x = WEB_HOST; // x is undefined
However, in the same file, some existing code manages to access the variable's value by using a bizarre string interpolation:
let x = `<%= WEB_HOST %>`; // x == 'https://example.com'
What is going on here? I've never seen this <%= syntax before. Is it part of Angular? Typescript? I haven't been able to find any documentation on what it does.
I also don't know how it manages to get a property out of ProjectConfig (and I'm not sure it's a class property either... the definition being outside of any class function confuses me too).
So, this is ASP Tags.
If you take a look at the following from w3schools. https://www.w3schools.com/asp/showasp.asp?filename=demo_syntax_tags
You will see that it is used in the HTML for templating. It can also be used I believe in other files as it is processed on the server before the client application runs on your browser.
HTH
Looks like ERB, a Ruby templating engine that Ruby on Rails uses.
https://codingbee.net/tutorials/ruby/ruby-the-erb-templating-system
Others have mentioned that various languages use <%= some_code_here %> for templating.
Regardless of what language is processing it, I imagine your .ts file runs through some server side framework, that will put some string value in place of WEB_HOST, and then the .ts file will be transpiled to JS.
You could try this way on HTML file
{{window.location.origin}}
if it doesn't recognize the window variable we might need this in ts file
declare const window;

Rails Javascript - Calling a javascript function from the view

I have a javascript file abc.js. I have kept it inside app/assets/javascripts/abc folder.
In my application.js file, I have given
//= require abc/abc
In my view, I need to display the object that is returned by a function inside the abc.js file.
I am calling the function by the following command
%h4.sub-header Test Project
%textarea.form-control{rows: 10}
:javascript
abc.diff(#a, #b)
I am getting the a & b values from the controller.
But the function is not being called. Instead the sentence "abc.diff(a,b)" is being displayed.
Could anyone tell me how to call the function inside my view?
Inside haml javascript blocks you can just do string interpolation. Also indentation is very important in haml (obviously). So your view should look like:
%h4.sub-header Test Project
%textarea.form-control{rows: 10}
:javascript
abc.diff(#{#a}, #{#b});
Without more info it is hard to say more, but in general this is not the preferred way: your code should be precompiled in javascript assets, and generally I define variable or add data attributes to specific items.
So either something like
:javascript
var a = #{#a};
var b = #{#b};
ad inside abs.js.coffee write
$ ->
abc.diff(a,b)
The cleanest solution imho is something like:
%body{'data-a': #a, 'data-b': #b}
and inside abs.js.coffee write
$ ->
a = $('body').data('a')
b = $('body'_.data('b')
abs.diff(a,b)
Of course, this depends whether it applies to your solution, my apologies for digressing :)

How to get rails controller variable in js file

I have a variable in rails controller like
def index
#approveflag = true
end
I need to access this variable in my javascript code, I used the code given below
in index.html.erb
<script>
alert("<%=#approveflag%>")
<script>
It works fine with result "true".But when I moved this code to its corresponding .js file it gives an alert with result string ""<%=#approveflag%>"".
What is the reason. How can I solve this?
Thanks in advance.
I personally don't like mixing js with erb so I would do something like this:
in index.html.erb
<%= content_tag :div, '', id: 'mycontainer', data: { source: #approveflag } %>
in js
$('#mycontainer').data('source')
alert($('#mycontainer').data('source'))
And you have the value from that instance variable, you can add a data attribute in a link_to if you want.
You cannot use the js files for this, they are part of the asset pipeline, they are compiled/compressed, and preferably only downloaded once by your client (browser). So they should not change. So at the time the js-files are precompiled, the instance variable is not set and would not make any sense anyway.
But there are a few options.
You can declare a javascript variable in your view, which your javascript can use (for global data)
Code (I use haml):
:javascript
var approveFlag = #{#approveflag} ;
You can declare data-tags on elements, if the data belongs to a specific element. But for instance, you could also a data-tag on body element
For instance
%body{:'data-approveflag' => #approveflag}
Or something like
= link_to 'Collapse', '#', 'data-collapse-div' => 'class-to-collapse'
Alternatively you can use ajax/json to download the data. This is clean, but adds an extra delay, so only do this is if the data is not required immediately.
I recommend that you to use the 'gon' gem
I'm usually more oriented to straight options that don't imply installing, but trust me using 'gon' is very direct and clean
In your Gemfile add this lines
# To use controller variables as javascript variables
gem 'gon'
Do a bundle install to get/install the gem
In your app/views/layouts/application.html.erb add the following line
<%= include_gon %>
Then in your controller pass your variable to gon as simple as this
gon.your_variable = #controller_variable
And in your javascripts retrieve your variable (in this case to show an alert)
alert (gon.your_variable) ;
And That's it !
For more examples and details see the Wiki page - https://github.com/gazay/gon/wiki
In my project I need sometimes user language related translations. So what I did is, I created a simple js file to store the localized text:
var TRANSLATION_ARRAY = []
function store_translation(key, value) {
TRANSLATION_ARRAY[key] = value
}
function get_translation(key) {
return TRANSLATION_ARRAY[key]
}
In den index.html.erb or other components I do this to store the localized text:
store_translation('text_id', "<%= translate_with_user_language('text_id') %>")
In my plain js file located in app/assets/javascripts i get the localized text by:
get_translation('text_id')
Works very well. You can use this approach also for other purposes not only translations.

Properly embed JavaScript variables in Ruby on Rails 4.x

I am using Ruby on Rails 4 and am trying to write a JavaScript that requires server-side data to populate. For instance, I might have a control called StudentsController and I want to return all particular students from a particular class. So in my controller I might do:
def index
#students = Student.all
end
I then have a script in /app/assets/javascripts/ called foo.js and in there I'm trying to split the students up by their class_id.
in foo.js
$(function() {
var class_a = <%= #students.select { |s| s.class_id = 'a' }.to_json %>
var class_b = <%= #students.select { |s| s.class_id = 'b' }.to_json %>
})
Problem is class_a is null when I'm checking on the browser side of things. I'm assuming foo.js isn't linked up to StudentsController and doesn't see #students. What's the right way to wire JavaScript this way so I can have access to javascript variables with my server-side objects?
One problem here is simply that your file should be called foo.js.erb to compile ruby.
Also, I think you want to do this:
var class_a = <%= #students.select { |s| s.class_id == 'a' }.to_json %>
(Notice the double equal sign)
You might even be better to find the specific student in your controller and pass each through individually - this would be a better separation of concerns and would keep this logic out of your views.

Accessing rails routes in javascript

Can anyone explain how i can access the rails routes/names routes in javascript ?
The following are some of the things i tried
http://github.com/jsierles/js_named_routes.
but no luck.
I was researching this question for a while and didn't found any implementation compatible with Rails 3.
So decided to write my own:
https://github.com/railsware/js-routes
kishore I think this is the simpliest way, just call:
Rails.application.routes.url_helpers.*_path
So, if you have in your routes.rb, let's say:
resources :users
then you want to call the index action from a javascript file, you do:
$.get('<%= Rails.application.routes.url_helpers.users_path %>', function(data){ ...
Take into account that the js file should have a .erb extension (or *.js.erb) so rails knows that must be preprocessed. Otherwise, the file will be served as is.
If I understand your requirement correctly you want Javascript code in your views to have access to the named routes. If so, then one simple way to do this is to render your Javascript code through a template (ERB, Haml, etc) and then to expand the routes something like the following:
ERB:
<script>
$.get('<%= some_named_route_path %>', function(data) {
});
</script>
Haml:
:javascript
$.get('#{ some_named_route_path }', function(data) {
});
UPDATE: Adding suggestions for public/javascripts/ case
Accessing named routes from the public folder is a bit trickier but there at least 2 ways that come to mind, neither of which is entirely satisfactory but, one or either of which might suit your application
Create javascript templates in (say) lib/javascripts using ERB named like '*.js.erb' and a rake task to expand those templates into public/javascripts before deploying (say as a step in your Capistrano recipe for example). Downside of that is that your changes are not made available live on the site until the templates are expanded and you might not get the right syntax highlighting of your javascript with an .erb extension file
Create per-view javascript helper functions in a content_for :head block which expand the named routes and make those available to code from your public javascript files. If you're careful with namespacing and conventions this might work out well. The downside is that the code calling these javascript helpers is decoupled from the code that defines them which could be confusing for maintainers or prone to abuse.
In some view:
<% content_for :head do %>
<script>
SomeNameSpace.helper_to_some_named_route = function() {
return '%<= some_named_route_path %>
};
</script>
<% end %>
Then in public/application.js (say)
$.get(SomeNameSpace.helper_to_some_named_route(), function(data) {
});
bjg really answered this, but I thought I'd extract the relevant part and amplify with an example.
You simply provide your view a string variable whose value is a named Rails path, and then use that value in the Javascript of your form. An example that illustrates how a controller method can specify the path for another method, to be opened by the script on the press of a button:
File config/routes.rb:
...
resource :foo, :only => [:show, :reset]
...
match 'foo_reset_path' => 'foo#reset'
Commanding rake routes will now produce, among other output, this:
foo GET /foo(.:format) foo#show
foo_reset_path /foo_reset_path(.:format) foo#reset
foo_reset_path is what we're going to use here, but you can of course use this method with any named Rails path.
File app/controllers/foo_controller.rb:
...
def show
#reset_path = "foo_reset_path" # simply the string you'd use in the
# Rails code for opening the path
...
end
...
def reset
... # reset some variables to their default values
redirect_to foo_path # causes the show method to be called, and the HTML
# page to be redisplayed
end
File app/views/foo/show.html.erb:
...
<input type="hidden" id="reset_path" name="reset_path" value="<%= #reset_path %>">
...
<script>
$(document).ready(function() {
...
/* Hang functionality on the "Reset form" button. */
$('#reset_button').click(function () {
var reset_path = $('#reset_path').val();
window.open(reset_path, "_self")
});
...
})
</script>
I'm using JQuery here, but the basic idea should be clear. The script adds a hook to the button element whose id is reset_button, so that clicking on the button causes the reset method of foo_controller to be called.
What I did based on Teemu's great answer:
In your controller:
def show
#section = Section.find(params[:id])
respond_to do |format|
format.html
format.json { render json: #section}
end
end
In your view:
<input type="hidden" id="section_path" data-path="<%= #section.id %>" name="section_path" value="foo">
In your js:
var id = $('#section_path').attr('data-path');
$.ajax({
url:'/sections/'+ id +'.json',
type:"GET",
success: function (data){
console.info(data);
},
error: function (xhr, status){
console.info(xhr.error);
}
});
If you are using a JS pipeline that supports import (ES6 + webpacker), you might want to check out js_from_routes, a code-generation library that works seamlessly with Rails reloader.
For each endpoint that you'd like to access from JS, it will generate a method that can help you build a URL or make a request.
resources :video_clips, only: [:update], export: true
By using it in combination with axios, these generated helpers can be convenient and easy to use:
import VideoClipsRequests from '#requests/VideoClipsRequests'
VideoClipsRequests.update({ data: video })
.then(onVideoUpdate)
Have in mind that you can adjust the generated code by providing a custom template, so it can adapt to any technology you are using, even plain jQuery.
Benefits:
No need to manually specify the URL, preventing mistakes and saving development time.
If an action is renamed or removed, the helper ceases to exist, which causes an error that is easier to detect than a 404.
See http://tore.darell.no/pages/javascript_routes:
JavascriptRoutes converts your Rails routes into JavaScript. You can then access (generate) them in the browser (or any other JS environment). It supports both normal and named routes, and creates helper functions for the latter.
Update: It looks as though someone has forked this if you prefer jQuery: https://github.com/ajnsit/javascript_routes
gem install the "angular_rails_templates"
Create a file called angular_rails_templates.rb in the config/initializers folder
copy following code in the file and restart server. (including "module CustomERBEngine" for it cannot be added to code block by 4 space)
module CustomERBEngine
class ERBTemplate < Tilt::ERBTemplate
def evaluate(scope, locals, &block)
scope.class_eval do
include Rails.application.routes.url_helpers
include Rails.application.routes.mounted_helpers
include ActionView::Helpers
end
super
end
end
end
Tilt.register CustomERBEngine::ERBTemplate, '.erb'

Categories

Resources