Organizing Javascript

Managing Javascript and the code form this, partilcuarly with page-specific items, is quite a pain in Rails. To make this simpler, we have a standard methodology for managing our app's JS. This approach uses classes, and page-based calling of JS.

In the main assets/javascripts folder, we have appplication.js. This is the main places that aJS libraries are loaded.

  • If the asset is a third party library, put it in libs/.

  • If its our own code that is used across pages, put it in app/.

  • If its specific to a page, put it in controllers/:controllername/:action_name.

Then, the main item that is called is init.coffee, which currently looks like this:

window.App ||= {}

class PageInit
  constructor: ->
    page = "#{$('body').data('page')}"
    @execute_page_js(page)

  execute_page_js: (page) ->
    if "function" is typeof window[page]
      klass = window[page]
      @items = new klass()

# Initializer that runs on a page
# Access the page object with App.page in JS
# Best practice. Put an @items on the class and have it accessible
App.init = () ->
  @page = new PageInit()

App.hacks = () ->
  # Handles making sure the modal is above fixed positioned elements on page
  $('.modal').on 'show.bs.modal', (event)->
    $(this).appendTo("body")

$(document).on "turbolinks:load", ->
  App.init()
  App.hacks()

This code leverages some trickery to put in place the stuff we need. First, window.App creates a universal container for all our work. If a previous JS snippet has creates it, it leaves it alone. Or, it creates it.

The PageInit gets called by the App.init(). What this does, is grabs the name of the controller and action, and tries to instantiate the JS class with that name. If it exists, then it will return its contstructor onto its own @items attribute. This is then passed upstrream through the returns onto the @page for the App class.

class @KeywordsIndex
  constructor: ->
    @table = new AudientiTable(@selector, { columns: @columns, buttons: @buttons })
    return { table: @table }

  selector: '#keywords-table'

  buttons:
    [ {
        text: 'Add Tags'
        className: 'btn btn-secondary btn-table'
        href: '#modifyKeywordTags'
        data: {toggle: 'modal'}
        action: (e, dt, node, config) ->
          console.log('perfoming action to pop modal')
          selectedRows = dt.column(0).checkboxes.selected()
          selectedIds = selectedRows.toArray()
          $("modifyKeywordTags").modal('show')
      },
      {
          text: 'Second button'
          className: 'btn btn-secondary btn-table'
          action: (e, dt, node, config) ->
            selectedRows = dt.column(0).checkboxes.selected()
            selectedIds = selectedRows.toArray()
            alert "second button " + selectedIds
            return
      },
      {
          text: 'Clear Selection'
          className: 'btn btn-secondary btn-table'
          action: (e, dt, node, config) ->
            dt.column(0).checkboxes.deselectAll()
            return
      },
    ]

  columns:
    [
      { data: 'checkbox'  },
      { data: 'name'      },
      { data: 'created_at'}
    ]

For example, in the case our our Keywords Datatable, the effect of this is to put the datatable in a consistent place all the time.. at App.page.items.table. This makes writing additional JS much more straightforward. It also keeps code from running that is not necesary for page execution.

Note that there is also an App.hacks() called by the turbolinks::load. This is for running things that are necessary to hack something that is broken .For example, currently in Bootstrap4, the modals can get hidden. The way to solve that is to, when the become visible, reattaah them to the body emeent. This is what this hack does. When the problem is fixed, the hack can be removed. This keeps us from patching too much inside libs/ or app/ that live beyond their usefulness

Last updated