Rails with no JS framework

Because all apps don’t need Ember or Angular


I’m writing code for over a decade now, consider myself as a geek, and as a geek I’m always very curious and excited about any new fancy technology released every now and then.

These days client-side MVC frameworks are very trendy, and to be honest what I read and learned regarding Ember or Angular (among many others) looked really shiny and well-thought. These are typically frameworks that I would love to love.

But right now, I don’t need them. Do you?


The playground

In addition to my freelancing activity, I’m running Folyo (which by the way, can help you to find great designer talents). It’s as Ruby on Rails website with a UI that could be defined as slick, reactive and responsive (especially thanks to Sacha, my co-founder), but by no means complex.

As a job board, Folyo has quite a common page driven architecture where any action performed on a page will often lead you to another page or refresh current page content. Folyo’s UI does not involve rich interactions like live data charts or dynamic data binding. No, just web pages and it’s perfectly fine!

So what about client-side frameworks?

Why not, since it seems like the way to go these days?

Because such frameworks are complex tools designed to solve complex interaction issues. You must be aware that they also have serious drawbacks regarding productivity, testing, SEO, etc. (read this) and also have a steep learning curve.

Building a website with client-side MVC frameworks will require you to build your Rails server-side application as an API application only, which means depriving you of some of Rails beauty and increasing your development effort (API + UI) significantly.

Still, using a framework also brings some virtues. One of the first being to help you with organizing your code in a more manageable way. While Rails is really directive regarding the way you need to organize server-side code, it dictates only one rule for Javascript:

Put whatever you want in application.js.

Eh. Really?

Time to clean it up

I will now explain the way we decided to organize Javascript code on Folyo, in a much more manageable way than everything in application.js. I believe it’s quite pragmatic and I’m sure it will fit with a lot of other applications.

Required libraries:

  • jQuery, obviously.
  • Coffeescript, not mandatory but its syntax for class definition is very convenient.
  • Turbolinks. It will really improve user experience by making navigation from page to page AJAX only.
  • HeadJS, used through headjs-rails gem, will speed up your app by loading your JS asynchronously.

Code hierarchy

To organize the code, I will simply obey to following rules:

Folyo JS code hierarchy
  • Any page requiring JS code will have it’s own class (a JS view).
  • If you need to share piece of code between views, put it in a widget class.
  • application.js is just doing the glue between your classes, jQuery, HeadJS and Turbolinks.
  • That’s all!

Views

ApplicationView is the default view, inherited by any other view, and instantiated by default when no specific view is provided.

window.Views ||= {}
class Views.ApplicationView
 render: ->
Widgets.FancyBox.enable()
Widgets.MarkdownEditor.enable()
 cleanup: ->
Widgets.FancyBox.cleanup()
Widgets.MarkdownEditor.cleanup()

Since we want FancyBox and our Markdown editor to work on a lot of pages, we put it in the ApplicationView as a default behavior.

Then a typical view would look like that:

window.Views.Newsletters ||= {}
class Views.Newsletters.EditView extends Views.ApplicationView
 render: ->
super()
$('a.preview').click (e) ->
e.preventDefault()
url = $(e.target).attr('href')
window.open(url, '_blank', 'width=800,height=800')
 cleanup: ->
super()

Simple as that! But why the cleanup stuff?

Because with Turbolinks, the Javascript environment is not reset between each page. For instance, if you define a timer on a page, it will keep ticking on next pages. So, just remember to stop your timer in the cleanup method (or remove any document-wide event listener)

Widgets

Sorry, no rocket science here.

window.Widgets ||= {}
class Widgets.FancyBox
  @enable:  -> $(".fancybox").fancybox()
@cleanup: -> $(".fancybox").off()

The glue

application.js is now only the entry point which will listen to Turbolinks events to render proper views and clean them up.

#= require everything you need
pageLoad = ->
className = $('body').attr('data-class-name')
window.applicationView = try
eval("new #{className}()")
catch error
new Views.ApplicationView()
window.applicationView.render()
head ->
$ ->
pageLoad()
$(document).on 'page:load', pageLoad
    $(document).on 'page:before-change', ->
window.applicationView.cleanup()
true
    $(document).on 'page:restore', ->
window.applicationView.cleanup()
pageLoad()
true

You also need to define a specific data attribute on each page, to indicate which JS view needs to be rendered.

In your application_controller.rb, define js_class_name method:

 def js_class_name
action = case action_name
when ‘create’ then ‘New’
when ‘update’ then ‘Edit’
else action_name
end.camelize
"Views.#{self.class.name.gsub('::', '.').gsub(/Controller$/, '')}.#{action}View"
end

And then use it in your layout (as well as HeadJS initialization)

%html
%head
= javascript_include_tag 'head.min'
= headjs_include_tag 'vendor', 'application'
%body{'data-class-name' => js_class_name}

Final words

I’m seing most and most Rails projects bootstrapped using either Angular, Ember or Backbone as their de facto choice.

Whilst these frameworks are very powerful, I hope that after reading this post you will consider that not using a JS framework is also a valid choice.