Webpack Setup
The @ruby2js/webpack-loader
lets you compile .rb.js
files to JavaScript via Webpack.
Fun fact: this loader itself is written in Ruby and compiles via Ruby2JS + Babel. 😁
Installation
Add the following to your Gemfile:
gem "ruby2js", ">= 3.5"
and run bundle install
.
Then run yarn add @ruby2js/webpack-loader
to pull in this Webpack loader plugin.
You will need to add a config file for Ruby2JS in order to perform the file conversions. In your root folder (alongside Gemfile
, package.json
, etc.), create rb2js.config.rb
:
require "ruby2js/filter/functions"
require "ruby2js/filter/camelCase"
require "ruby2js/filter/return"
require "ruby2js/filter/esm"
require "ruby2js/filter/tagged_templates"
require "json"
module Ruby2JS
class Loader
def self.options
# Change the options for your configuration here:
{
eslevel: 2021,
include: :class,
underscored_private: true
}
end
def self.process(source)
Ruby2JS.convert(source, self.options).to_s
end
def self.process_with_source_map(source)
conv = Ruby2JS.convert(source, self.options)
{
code: conv.to_s,
sourceMap: conv.sourcemap
}.to_json
end
end
end
That’s just one possible configuration—you can edit this file as needed to modify or add additional Ruby2JS filters, pass options to the converter, and so forth.
Webpack Configuration
(For Rails-specific configuration, see below.)
You’ll need to edit your Webpack config so it can use the @ruby2js/webpack-loader
plugin. In your webpack.config.js
file, add the following in the modules.rules
section:
{
test: /\.js\.rb$/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
plugins: [
[
"@babel/plugin-transform-runtime",
{
helpers: false,
},
],
],
}
},
"@ruby2js/webpack-loader"
]
}
(If you currently use other Babel plugins elsewhere in your Webpack config, feel free to copy them here, or define them once in a variable up top and simply add them to both parts of the config tree.)
Now wherever you save your .js
files, you can write .js.rb
files which will be converted to Javascript and processed through Babel. You’ll probably have a main index.js
file already, so you can simply import the Ruby files from there and elsewhere. You’ll have to include the full extension in the import statement, i.e. import MyClass from "./lib/my_class.js.rb"
.
See the next example in the Rails section for how to write a web component based on open standards using LitElement.
Source Maps
By default, @ruby2js/webpack-loader
will provide source maps if requested by Webpack. If you’re having trouble debugging code and want to inspect the JS output from Ruby2JS, you can keep source maps on at the Webpack level but turn them off at the loader level in your config:
{
use: [
// …
{
loader: "@ruby2js/webpack-loader",
options: { provideSourceMaps: false }
}
]
}
Of course to get back to the default behavior, simply change provideSourceMaps
to true
or remove it from the options.
Usage with Rails and ViewComponent
In your existing Rails + ViewComponent setup, add the following configuration initializer:
app/config/initializers/rb2js.rb
:
Rails.autoloaders.each do |autoloader|
autoloader.ignore("app/components/**/*.js.rb")
end
This ensures any .js.rb
files in your components folder won’t get picked up by the Zeitwerk autoloader. Only Webpack + Ruby2JS should look at those files.
You’ll also need to tell Webpacker how to use the @ruby2js/webpack-loader
plugin. In your config/webpack/environment.js
file, add the following above the ending module.exports = environment
line:
const babelOptions = environment.loaders.get('babel').use[0].options
// Insert rb2js loader at the end of list
environment.loaders.append('rb2js', {
test: /\.js\.rb$/,
use: [
{
loader: "babel-loader",
options: {...babelOptions}
},
"@ruby2js/webpack-loader"
]
})
Now, by way of example, let’s create a wrapper component around the Duet Date Picker component we can use to customize the picker component and handle change events.
First, run yarn add lit @duetds/date-picker
to add the Javascript dependencies.
Next, in app/javascript/packs/application.js
, add:
import "../components"
Then create app/javascript/components.js
:
function importAll(r) {
r.keys().forEach(r)
}
importAll(require.context("../components", true, /_component\.js$/))
importAll(require.context("../components", true, /_elements?\.js\.rb$/))
Now we’ll write the ViewComponent in app/components/date_picker_component.rb
:
class DatePickerComponent < ApplicationComponent
def initialize(identifier:, date:)
@identifier = identifier
@date = date
end
end
And the Rails view template: app/components/date_picker_component.html.erb
:
<app-date-picker identifier="<%= @identifier %>" value="<%= @date.strftime("%Y-%m-%d") %>"></app-date-picker>
Hey, what’s all this custom element stuff? Well that’s what we’re going to define now! Let’s use Ruby to write a web component using the LitElement library.
Simply create app/components/date_picker_element.js.rb
:
import [ LitElement, html ], from: "lit"
import [ DuetDatePicker ], from: "@duetds/date-picker/custom-element"
import "@duetds/date-picker/dist/duet/themes/default.css"
customElements.define("duet-date-picker", DuetDatePicker)
class AppDatePicker < LitElement
self.properties = {
identifier: { type: String },
value: { type: String }
}
# If you need to add custom styles:
#
# self.styles = css <<~CSS
# :host {
# background: yellow;
# }
# CSS
def _handle_change(event)
console.log(event)
# Perhaps set a hidden form field with the value in event.detail.value...
end
def updated()
date_format = %r(^(\d{1,2})/(\d{1,2})/(\d{4})$)
self.shadow_root.query_selector("duet-date-picker")[:date_adapter] = {
parse: ->(value = "", create_date) {
matches = value.match(date_format)
create_date(matches[3], matches[2], matches[1]) if matches
},
format: ->(date) {
"#{date.get_month() + 1}/#{date.get_date()}/#{date.get_full_year()}"
},
}
end
def render()
html <<~HTML
<duet-date-picker @duetChange="#{@handle_change}" identifier="#{self.identifier}" value="#{self.value}"></duet-date-picker>
HTML
end
end
customElements.define("app-date-picker", AppDatePicker)
Now all you have to do is render the ViewComponent in a Rails view somewhere, and you’re done!
Date picker: <%= render DatePickerComponent.new(identifier: "closing_date", date: @model.date) %>
Framework-less open standard web components compiled and bundled with Webpack, yet written in Ruby. How cool is that?!