21
votes

My question here is the same as Using Rails 3.1, where do you put your "page specific" JavaScript code?, just for Rails 6 instead of Rails 3.1.

Suppose I have some JavaScript that I want to use for my posts index page. Where do I put that file, and how do I include it?

In the previous question the answers usually utilize the Rails Asset Pipeline. However, with Rails 6, my understanding is that it uses webpacker instead of the Asset Pipeline for JavaScript files.

Note: I don't want the file to always be included. Eg. if I am on the authors index page, I don't want the JavaScript file for the posts index page to be included. Why? Imagine I have $('li').on('click', changeColor); in the JS file for the posts index page. I probably don't want that code to run on the authors index page, but it will if the file is included. You could get around this problem by namespacing, but I think it would be cleaner (and more performant) to just not include unnecessary files.

5
Did you manage to solve? I'm having the same problems, there's some code I only want to run at a specific pagetruongnm

5 Answers

18
votes

I'll describe a few options in order of increasing level of difficulty with regards to experience with Webpack and Webpacker.

  1. Forget page-specific JavaScript and put everything in the application.js bundle. This will most definitely be the easiest approach to understand. For a small application, the tradeoff may well be worth it as having an application that's easier to maintain may outweigh added cost of learning how to best to split up your JS code with Webpack with little performance gain.

  2. Use dynamic imports to lazy load your page-specific code. In this scenario, you would still place all your JavaScript within the application.js dependency graph, but not all of this code would be loaded up-front. Webpack recognizes the dynamic import() function when it compiles your JS and will split the imported chunk out into a separate file that can be loaded on-demand in the browser using a JS framework router or a simple trigger.

    For example, you could have code that looks like this:

    if (document.querySelectorAll(".post-index").length) {
      import("./post/index") // webpack will load this JS async
    }
    
  3. Use page-specific "packs" combined with the splitChunks configuration API. In this scenario, instead of using an application.js pack, you would construct one for each page you want to treat differently, e.g, posts.js, admin.js etc. Using the splitChunks plugin means that your bundles can properly share code. I highly recommend treading carefully with this approach until you understand how Webpack works OR be willing to go through the process of learning Webpack in choosing this path. Webpack typically works best on the assumption you use only one entry point per page, otherwise, you may end up duplicate code across bundles unless you know what you're doing.
7
votes

Let's go through the contents of this directory in a empty Rails 6 application.

▶ tree app/javascript
app/javascript
├── channels
│   ├── consumer.js
│   └── index.js
└── packs
    └── application.js

2 directories, 3 files

the packs directory is significant for us so let's see what it contains.

// app/javascript/application.js
require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

webpack has a concept of entry points which are the files that it looks for first when it starts compiling your JavaScript code.

Webpacker gem creates the application pack in the form of this application.js file under app/javascript/packs. If you remember the assets pipeline, this file is equivalent to the app/assets/javascripts/application.js file

Simple example

How to Add Javascript to Ruby on Rails (Adding Javascript as a Module):

  1. create folder in Javascript path app/javascript ex: 'post'

  2. add your javascript files in folder like index.js

  3. add postmodule in app/javascript/application.js -> require('post')

    require("@rails/ujs").start() require("turbolinks").start() require("@rails/activestorage").start() require("channels") require("post")

Let's go through the contents of this directory in a Rails 6 application after add post module

▶ tree app/javascript
app/javascript
├── channels
│   ├── consumer.js
│   └── index.js
└── packs
|    └── application.js
|ـــ post
     |__ index.js

This simple way to use webpack same old rails style.

0
votes

This how I am serving page specific code with Turbolinks:

# javascript/packs/application.js
import {posts_show              } from '../per_page/posts_show';
import {users_new               } from '../per_page/users_new';
const pages = {posts_show, users_new};
document.addEventListener("turbolinks:load", () => {
    # I am using gon to save the page name, but you can add page name to document body and then const page = document.body.className
    const page = gon.controller_full
    # check if method exist if true execute
    if ('function' === typeof pages[page]){
      new pages[page]
    }
});

As you noticed, I'm compiling all the js inside one file, and execute them as they needed.

Here is an example of post_show:

# javascript/per_page/posts_show.js
import {default as share_menu_init} from './methods/share_menu_init.js'
export class posts_show {
    constructor() {
        share_menu_init()
        ... # some other code here
    }
}

You could see that I'm importing the share_menu_init module. If I need to share some methods between the pages, I store them inside modules and Webpack is smart enough to not create duplicates (if I import it twice, in posts_show and other pages) it will only organize files so that my posts_show has access to this method.

const share_menu_init = () => {
    ... # some code here
}
export default share_menu_init

Not sure if its the best way to serve page-specific code, if you have one better, I would like to hear.

0
votes

The way you should do it is to create a folder structure to match your views:

app/javascript
├── channels
│   ├── consumer.js
│   └── index.js
└── packs
     └── application.js
     └── post
         └── index.js

Then inside of your views index.js you want to include :

<%= javascript_pack_tag('post/index') %>

After you add that, in your application.rb add:

config.assets.precompile += ['listings/index.js']

This way you only load it in the specific view you need it, you can continue to append more assets as you make view specific JS.

Notes:

  • A common argument is that you may accidentally push production code missing an asset in the precompile list so you should just load all your JS but this is just lazy, you can't even load the page if its missing an asset, if you push code missing an asset you didn't test anything.

References:

  1. https://guides.rubyonrails.org/asset_pipeline.html#how-to-use-the-asset-pipeline
-1
votes

You can use $.getScript

https://coderwall.com/p/xubjcq/handling-large-javascript-files-with-turbolinks

(function(){
  var requiredScripts = [];

  window.requireOnce = function (path, cb) {
    if ($.inArray(path, requiredScripts) == -1) {
      requiredScripts.push(path)
      $.getScript(path, cb)
    } else {
      cb()
    }
  }
})();

Use it like this:

requireOnce('wysiwyg.js', function(){
  $('textarea').wysiwyg()
});