#74 Page Specific Javascript in Ruby on Rails
4-2-2017

Summary

Sometimes you may find yourself with an application that has javascript that needs to execute only on a specific page. This episode lays the foundation to easily manage page specific javascript.
9
rails javascript view 4:44

Summary

application.html.erb# use either the meta tag or body's data attributes.
<head>
  ...
  <%= tag :meta, name: :psj, action: action_name, controller: controller_name %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  ...
</head>

<%= content_tag :body, data: { action: action_name, controller: controller_name } do %>
  ...
<% end %> 
app/javascripts/init.coffeeclass Page
  controller: () =>
    $('meta[name=psj]').attr('controller')
    # $('body').data('controller')
  action: () =>
    $('meta[name=psj]').attr('action')
    # $('body').data('action')

@page = new Page

# Javascript equivalent

var Page, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
Page = (function() {
  function Page() {
    this.action = bind(this.action, this);
    this.controller = bind(this.controller, this);
  }
  Page.prototype.controller = function() {
    return $('meta[name=psj]').attr('controller');
  };
  Page.prototype.action = function() {
    return $('meta[name=psj]').attr('action');
  };
  return Page;
})();
this.page = new Page;
visitors.coffee$(document).on 'turbolinks:load', ->
  return unless page.controller() == 'visitors' && page.action() == 'index'
  # return unless $('meta[name=psj]').attr('controller') == 'visitors' # && $('meta[name=psj]').attr('action') == 'index'
  # return unless $('body').data('controller') == 'visitors' && $('body').data('action') == 'index'
  $('main').append '<li>Hello from visitors</li>'

# Javascript equivalent

$(document).on('turbolinks:load', function() {
  if (!(page.controller() === 'visitors' && page.action() === 'index')) { return; }
  return $('main').append('<li>Hello from visitors</li>');
});
app/javascripts/users/index.coffee$(document).on 'turbolinks:load', ->
  return unless page.controller() == 'users' && page.action() == 'index'
  $('main').append '<li>Hello from users index</li>'

# Javascript Equivalent

$(document).on('turbolinks:load', function() {
  if (!(page.controller() === 'users' && page.action() === 'index')) { return; }
  return $('main').append('<li>Hello from users index</li>');
});
app/javascripts/users/users.coffee$(document).on 'turbolinks:load', ->
  return unless page.controller() == 'users' 
  $('main').append '<li>Hello from users controller</li>'

# Javascript Equivalent

$(document).on('turbolinks:load', function() {
  if (page.controller() !== 'users') { return; }
  return $('main').append('<li>Hello from users controller</li>');
});
ninoM said over 1 year ago:

Thanks for providing the JS equivalent code!

selzlein said 9 months ago:

Great work, thank you!

I have implemented the controller action specific CoffeeScript approach. Therefore I have 1 .coffee file per controller action. However I stumbled upon an issue... let's say I have a controller with a 'new' action and a respective 'new.coffee' script. When I submit the 'new' page that triggers Rails validation errors, the controller action gets updated to 'create', since it was a Post. Therefore 'new.coffee' won't execute while my controller renders 'new.html.erb' directly, not through a redirect, as is standard Rails scaffold.

In this case what would you recommend? Should I simply use the 1 file per controller approach?

kobaltz PRO said 9 months ago:

You could mix and match based on the complexity of the JS needed. You could also look at something like client side validations to prevent the form from posting if the data isn’t valid (but not recommended for uniqueness validations because of potential security reasons).

selzlein said 9 months ago:

Got it! Thank you :D

owenstrevor PRO said 7 months ago:

Thanks for this, I have spent a lot of time trying different approaches of this for our app :).

IMO just adding the Javascript in the view is cleaner and easier to manage for us. Everything is in one place relating to the view.

Our app has 23 different controllers, each with several actions. Navigating all of those folders and different files, if we used this setup would be a pain.

kobaltz PRO said 7 months ago:

I do think that people and companies should do what they feel is best for them. Though, the same logic can apply to just controllers. Also, you wouldn’t have to create a file for every controller and/or action, just the ones you would want PSJ.

However, I wouldn’t put the JS directly in the views for a few reasons.

In the view, the JS will not be minified or compressed.

The CDN likely won’t cache the results of the page and cause extra bandwidth and load times (minimal but still). 

You lose access to the reuse of JS.

Kiran Patil said 7 months ago:

Hi,


Does page-specific-js-in-ruby  still make sense, since we have now Stimulus js ?


Thanks.

kobaltz PRO said 7 months ago:

I believe that it does. Not everything needs to be a Stimulus component. Sometimes it just makes sense to have some JS sprinkles where needed. I have personally never been a huge fan of page specific javascript as it seems like there is some disconnect in the architectural planning of the application. I typically tend to write JS functions so that they can be called on later in Server Rendered Javascript responses, when launching a bootstrap modal or wherever else appropriate.

smarquez1 said 6 months ago:

Thanks, this is great. I added a couple functions to the init.js file in my project to make the syntax shorter, hope that it helps someone:

#init.js
var Page, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
Page = (function() {
  function Page() {
    this.action = bind(this.action, this);
    this.controller = bind(this.controller, this);
  }

  Page.prototype.controller = function() {
    return $('meta[name=psj]').attr('controller');
  };

  Page.prototype.action = function() {
    return $('meta[name=psj]').attr('action');
  };

  Page.prototype.isController = function(controller) {
    return controller === this.controller()
  };

  Page.prototype.isAction = function(action) {
    return action === this.action()
  };

  Page.prototype.isControllerAndAction = function(controller, action) {
    return this.isController(controller) && this.isAction(action)
  };

  return Page;
})();
this.page = new Page;

So then you can do the check like this like this:

if(!page.isControllerAndAction('users', 'index')) return;

Login to Comment