I'm upgrading an older application to Rails 6, which uses webpacker
for all the JS asset management.
I'm using the pikaday
calendar library, and have added it via yarn add pikaday
, verified it shows up in packages.json
and then added it to my app/javascript/packs/application.js
via require("pikaday")
.
I have a JS class called Datepicker
that I'm using to abstract the actual pikaday
calendar. I'm doing this because I may someday change the datepicker implementation, and this way I'll only have to change a single class instead of update all my pikaday
calls.
However, it doesn't seem to matter if I require("pikaday")
in the application.js
pack file or not; as long as I import Pikaday from "pikaday"
in the class I'm referencing it in, it makes no difference.
Questions
I'm trying to gain understanding of what's going on.
Do I need to add require("pikaday")
or import Pikaday from "pikaday"
in the app/javascript/pack/application.js
file? Why or why not?
I'm familiar with the principle that globals are bad and should be avoided, but is there a way to avoid having to import CLASS from "class_file"
on every JS file that references it? In my example I want to use the Datepicker
class in multiple places. The reason I ask is because I have 10+ classes like this, and it's a little annoying to have 10+ import
statements at the top of every JS file that I want to use these in. There are certain classes that I always want access to. I've played with the webpacker ProvidePlugin functionality, but it complains that Datepicker
is not a constructor, so I'm probably missing something but I don't have enough knowledge to know what.
import Pikaday from "pikaday"
export default class Datepicker {
constructor(element, options) {
if (options == null) { options = {} }
// Store DOM element for reference
this.element = element
// Do not re-run on elements that already have datepickers
if (this.element.datepicker === undefined) {
options = Object.assign({},
this.defaultOptions(),
options
)
const picker = new Pikaday(options)
// Store picker on element for reference
this.element.datepicker = picker
return picker
} else {
console.log("Datepicker already attached")
return
}
}
// Overridden by `options` in constructor
defaultOptions() {
return {
field: this.element,
format: "M/D/YYYY",
bound: true,
keyboardInput: false,
showDaysInNextAndPreviousMonths: true
}
}
}
require("@rails/ujs").start()
require("turbolinks").start()
require("moment")
// Note: if using `@rails/ujs`, you do not need to use `jquery-ujs`.
import "jquery"
// Does not matter if I require this or not, as long as it is imported in the
// class file, I can remove this require statement and everything still works.
require("pikaday")
// StimulusJS
// Webpack's `require` looks for `controllers/index.js` by default
require("controllers")
require("custom/datepicker")
You asked a couple questions:
You only need import Pikaday from 'pikaday'
in the file where you reference the imported variable, Pikaday
. In this case, you only need this import in your custom datepicker module. You can remove require("pikaday")
from your application.js pack file.
The reason for this is Webpack will take your application.js pack as an entry point to a dependency graph; starting there, it will recursively traverse each required/imported module, find the dependencies of those modules, and so on, until all declared modules are included in the bundle. Since you have declared import 'custom/datepicker'
in the application.js pack, and the custom datepicker imports pikaday
, it will get included in the bundle as dependency.
Your custom Datepicker
gets compiled as an ES module (rather, Webpack's implementation of ES module), since you are using the ES module syntax export default ...
. This is important to the way ProvidePlugin works. From the Webpack 4 documentation of ProvidePlugin
:
For importing the default export of an ES2015 module, you have to specify the default property of module.
This means your Webpack configuration for the plugin entry for Datepicker
would look something like this (using the Rails Webpacker environment api):
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
const {resolve} = require('path');
environment.plugins.append('Provide', new webpack.ProvidePlugin({
Datepicker: [resolve('app/javascript/custom/datepicker'), 'default']
}))
module.exports = environment
Opinion: That said, I encourage you to make your imports explicit, e.g. import Datepicker from 'custom/datepicker'
in each module where Datepicker
is referenced. Even if repetitive, it will become much easier to integrate with tools like ESlint, which, with certain code editors, can provide inline feedback about errors from compilation—much easier to setup with explicit dependencies declared in each module.
I put together a working demo of Pikaday using your custom Datepicker with the ProvidePlugin here: https://github.com/rossta/rails6-webpacker-demo/commit/be3d20107c2b19baa8b9560bce05e0559f90086d