Using Rails-UJS in JS modules (Rails 6 with webpacker)
Asked Answered
D

5

40

i just switched to Rails 6 (6.0.0.rc1) which uses the Webpacker gem by default for Javascript assets together with Rails-UJS. I want to use Rails UJS in some of my modules in order to submit forms from a function with:

const form = document.querySelector("form")
Rails.fire(form, "submit")

In former Rails versions with Webpacker installed, the Rails reference seemed to be "globally" available in my modules, but now i get this when calling Rails.fire

ReferenceError: Rails is not defined

How can i make Rails from @rails/ujs available to a specific or to all of my modules?

Below my setup…

app/javascript/controllers/form_controller.js

import { Controller } from "stimulus"

export default class extends Controller {
  // ...
  submit() {
    const form = this.element
    Rails.fire(form, "submit")
  }
  // ...
}

app/javascript/controllers.js

// Load all the controllers within this directory and all subdirectories. 
// Controller files must be named *_controller.js.

import { Application } from "stimulus"
import { definitionsFromContext } from "stimulus/webpack-helpers"

const application = Application.start()
const context = require.context("controllers", true, /_controller\.js$/)
application.load(definitionsFromContext(context))

app/javascript/packs/application.js

require("@rails/ujs").start()
import "controllers"

Thanks!

Dixon answered 14/5, 2019 at 10:17 Comment(0)
D
57

in my app/javascript/packs/application.js:

import Rails from '@rails/ujs';
Rails.start();

and then in whatever module, controller, component I'm writing:

import Rails from '@rails/ujs';
Doehne answered 18/8, 2019 at 11:0 Comment(5)
Thanks for the answer, it works simple and well for me. I am now importing the form_controller.js form a npm package in order to reuse it across several rails applications. Is there any downside in doing import Rails from "@rails/ujs" several times across different controllers? If not, i'd mark this one as accepted answer.Dixon
No downside - in fact, I'd say it's exactly how we're supposed to declare each file's dependencies in the modular ES6 world. Webpack will load @rails/ujs exactly once, no matter how many times it's imported. Each controller simply gets the same export of Rails, and this remains true even if you import it with a different name, or where there's a CommonJS require() rather than ES6 import. Take care to only call .start() once, probably in your application.js.Doehne
I would see @ThienSuBS's answer and read the docs here: webpack.js.org/plugins/provide-plugin . I had the same problem; I didn't want to import Rails in every file I used it, and the webpack plugin support is built exactly for that situation: "Automatically load modules instead of having to import or require them everywhere."Singlephase
@Dan L: depending on globals is one of the most notorious programming anti-patterns, so no, I don't endorse the ProvidePlugin, and instead specifically do endorse importing dependencies. Writing modules that depend on global runtime state is how people ended up in this schmozzle to begin with. All the more so given that the question is about writing modules. Modules that depend on global state aren't ... modular.Doehne
@inopinatus: I understand the concern with globals everywhere and generally agree. I'm ok with Rails specifically in the global space because it's a utility library that abstracts common features (like AJAX requests) that I want to use all the time. I view it as similar to jQuery; I don't want to import jQuery on every single JS file, because jQuery is a tool that I'm using to build my real application code. That said, there's a lot of wisdom in avoiding globals like you mentioned.Singlephase
C
19

First at all, using yarn add rails/ujs:

yarn add  @rails/ujs

And add to config/webpack/environment.js

const webpack = require('webpack')
environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    Popper: ['popper.js', 'default'],
    toastr: 'toastr/toastr',
    ApexCharts: ['apexcharts', 'default'],
    underscore: ['underscore', 'm'],
    Rails: ['@rails/ujs']
  })
)
module.exports = environment

Config and load Rails js.

# pack/application.js
require("@rails/ujs").start()
global.Rails = Rails;

And Then: This is result -> My result when i typed Rails in Firefox Console

Chil answered 30/9, 2019 at 4:21 Comment(0)
F
5

Just add it to your environment.js file, here is mine (with bootstrap and jquery):

const {environment} = require('@rails/webpacker')
const webpack = require('webpack')

module.exports = environment

environment.plugins.prepend(
    'Provide',
    new webpack.ProvidePlugin({
        $: 'jquery',
        jQuery: 'jquery',
        jquery: 'jquery',
        'window.jQuery': 'jquery',
        "window.$": "jquery",
        Popper: ['popper.js', 'default'],
        Rails: ['@rails/ujs']
    })
)
Ferren answered 10/6, 2019 at 22:9 Comment(6)
I've done exactly that. Webpack cries TS2304: Cannot find name 'Rails'.Jazzman
Did you also add it via package.json ? npm add @rails/ujsFerren
I apologize. Let me clarify. I can import the package, but using the global variable Rails gives TS2304: Cannot find name 'Rails'.Jazzman
However, it worked for JQuery, so thank you anyways. Just not Rails.Jazzman
Please see it to get Rails instance in javascript. https://mcmap.net/q/396115/-using-rails-ujs-in-js-modules-rails-6-with-webpackerChil
@Chil this is the one - I spent weeks looking for the answer. Big thanksLamere
C
4

I am currently messing around on 6.0.0.rc2 but I think I got an answer for you.

So if you separate out the:

app/javascript/packs/application.js

require("@rails/ujs").start()
import "controllers"

To instead:

export const rails_ujs = require("@rails/ujs")
console.log(rails_ujs)
rails_ujs.start()

You can obviously remove that console.log was just trying to figure things out. Then in your stimulus controller you can simply do:

// Visit The Stimulus Handbook for more details
// https://stimulusjs.org/handbook/introduction
//
// This example controller works with specially annotated HTML like:
//
// <div data-controller="hello">
//   <h1 data-target="hello.output"></h1>
// </div>

import { Controller } from "stimulus"
import { rails_ujs } from "packs/application.js"

export default class extends Controller {
  static targets = [ "output" ]

  connect() {
    // this.outputTarget.textContent = 'Hello, Stimulus!'
    console.log('hi')
    console.log(rails_ujs)
  }
}

Just using their little test controller here but I got it to console.log out and you can call rails_ujs.fire so that should be what you want :)

Let me know if this works for you!

Corrugate answered 12/8, 2019 at 18:54 Comment(0)
S
1

I think the best way is to use the expose-loader and configure it the same way webpacker would if you ran bundle exec rails webpacker:install:erb.


Install the expose-loader

$ yarn add expose-loader

Create a config file

  1. For loaders webpacker configures itself, it'll dump a config object in config/webpack/loaders. Create that folder if it doesn't exist.

  2. Create a file called config/webpack/loaders/expose.js

  3. Add this to that file:

    module.exports = {
      test: require.resolve('@rails/ujs'),
      use: [{
        loader: 'expose-loader',
        options: 'Rails'
       }]
    }
    
    // later versions of expose loader may allow the following API:
    module.exports = {
      test: require.resolve('@rails/ujs'),
      loader: 'expose-loader',
      options: {exposes: "Rails"}
    }
    

Add that loader to environment.js

Add these two lines to config/webpack/environment.js:

const expose = require('./loaders/expose')
environment.loaders.prepend('expose', expose)

The full file should look something like:

const { environment } = require('@rails/webpacker')
const expose = require('./loaders/expose')

environment.loaders.prepend('expose', expose)
module.exports = environment

That should give you access to the Rails object globally again.

Sorites answered 27/8, 2019 at 1:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.