Rails bindings for Opal. (Changelog)
If you are upgrading an existing app, see PORTING.md for a focused 2.x -> 3.x migration guide.
In your Gemfile
gem 'opal-rails'opal-rails is tested against Rails 7.0, 8.0, and 8.1 with Opal 1.8 and Opal master (2.0dev) on Ruby 3.2 and 4.0. It requires Opal 1.7.0+ for the builder/watch dependency APIs it uses. Rails 6.x is no longer supported, Rails 7.1/7.2 do not have dedicated appraisal coverage, and Rails 8 requires Ruby 3.2+.
Run the opal:install Rails generator to create a build-based Opal setup:
bin/rails g opal:install
The generator now creates:
app/opal/application.rbfor greenfield apps, or reusesapp/assets/opalwhen migrating an older layoutconfig/initializers/opal.rbwith build-oriented defaultsapp/assets/builds/.keep- a
Procfile.deventry foropal: bin/rails opal:watch - a
bin/devlauncher when the app does not already have one - for Sprockets apps,
app/assets/config/manifest.jslinks forapp/assets/builds - for Sprockets test environments,
config.assets.debug = trueinconfig/environments/test.rbso rebuilt assets win over stale checked-in digests
For already-installed 2.x apps, do not treat bin/rails g opal:install as a drop-in migration step. Follow PORTING.md instead, and pin opal-rails to the 2.0 series until you are ready to port the app deliberately.
bin/dev is a small Foreman launcher for Procfile.dev; the generated script installs the foreman gem if it is missing and then starts both Rails and opal:watch together.
If the generator finds an existing multi-entrypoint Opal source root, it keeps that layout intact, configures config.opal.entrypoints = :all, and avoids inserting a default javascript_include_tag "application" when no application.rb entrypoint exists.
If the host app already has a non-Opal application.js, the generator keeps app/opal/application.rb as the source file but configures the built logical asset name as opal so the two pipelines do not collide.
It no longer depends on opal-sprockets or a Sprockets-specific helper loader at runtime.
The opal:assets generator now writes plain .rb files into the active Opal source root instead of generating app/assets/javascripts/*.js.rb files.
The following automatically gets added to your configuration for the compiler when running the opal:install Rails generator:
config/initializers/opal.rb
# Compiler options
Rails.application.configure do
config.opal.method_missing_enabled = true
config.opal.const_missing_enabled = true
config.opal.arity_check_enabled = true
config.opal.freezing_stubs_enabled = true
config.opal.dynamic_require_severity = :ignore
endCheck out the full list of the available configuration options at: lib/opal/config.rb.
opal-rails now also exposes an explicit build task for modern Rails-style asset generation.
The host Rails app is still responsible for choosing an asset server such as Propshaft or Sprockets to serve the built files from app/assets/builds.
Current build-oriented config keys are:
Rails.application.configure do
config.opal.source_path = Rails.root.join('app/opal')
config.opal.entrypoints_path = config.opal.source_path
config.opal.build_path = Rails.root.join('app/assets/builds')
config.opal.entrypoints = { 'application' => 'application.rb' }
config.opal.append_paths = []
config.opal.use_gems = []
endFor mixed-stack apps that already use another application.js, use an explicit logical name for the Opal output instead of colliding with the existing asset:
Rails.application.configure do
config.opal.source_path = Rails.root.join('app/opal')
config.opal.entrypoints_path = config.opal.source_path
config.opal.entrypoints = { 'opal' => 'application.rb' }
endWith that in place, you can build Opal entrypoints into browser-ready assets with:
bin/rails opal:buildopal-rails also hooks opal:build into assets:precompile automatically, and into test:prepare / spec:prepare when those tasks exist in the host app.
And clean only Opal-owned build outputs with:
bin/rails opal:clobberTo rebuild entrypoints while you develop, run:
bin/rails opal:watchThis writes *.js outputs, optional *.js.map files, and an Opal-owned manifest into app/assets/builds.
opal:clobber uses that manifest to remove only Opal-tracked outputs, leaving unrelated assets in app/assets/builds alone.
opal:watch uses the listen gem, tracks Opal/core and app dependency files, rebuilds affected entrypoints for known file changes, and falls back to a full rebuild when files are added or removed.
Any directories listed in config.opal.append_paths are also part of that watch scope, so shared templates or support files can trigger rebuilds too.
Include the built entrypoint in your layout with the normal Rails helper:
<%= javascript_include_tag "application", "data-turbo-track": "reload" %>Boot code should live in the built Opal entrypoint itself rather than in a helper-side loader shim.
If you are migrating an app that already keeps frontend Ruby under app/assets/opal, set config.opal.source_path and config.opal.entrypoints_path to that directory instead. If your asset server would otherwise expose raw files from that directory, exclude it in the host app configuration yourself. For example, Propshaft apps can add config.assets.excluded_paths << Rails.root.join('app/assets/opal'). Apps using the default app/opal layout do not need this.
For the default app/opal layout, opal-rails also ignores that source root in Rails autoloaders so frontend Opal files are not treated as application constants.
If you want one built asset per top-level Opal file, you can opt into bulk entrypoint discovery:
Rails.application.configure do
config.opal.source_path = Rails.root.join('app/assets/opal')
config.opal.entrypoints_path = config.opal.source_path
config.opal.entrypoints = :all
endIn :all mode, opal-rails compiles each top-level *.rb file in entrypoints_path to a same-name asset in app/assets/builds, ignores nested support files, and prunes stale Opal-owned outputs when an entrypoint file disappears.
The install generator will choose that :all configuration automatically for migration-friendly layouts that already have multiple top-level Opal entrypoints.
If you want to generate a controller-specific Opal file, use:
bin/rails g opal:assets dashboardThis now creates app/opal/dashboard.rb by default, or app/assets/opal/dashboard.rb for migration-friendly layouts.
The bundled test app and integration suite prebuild assets from app/opal into app/assets/builds instead of relying on app/assets/javascripts/*.js.rb request-time compilation.
You may optionally add configuration for rendering assigns when using the template handler from actions:
config/initializers/opal.rb
Rails.application.configure do
# ...
config.opal.assigns_in_templates = true
config.opal.assigns_in_templates = :locals # only locals
config.opal.assigns_in_templates = :ivars # only instance variables
endLocal and instance variables will be sent down to the view after converting their values to JSON.
This example assumes Rails 7 and having followed the Installation instructions.
1- Delete app/javascript/application.js
2- Enable the following lines in the generated app/opal/application.rb below require "opal":
puts "hello world!"
require "native"
$$[:document].addEventListener :DOMContentLoaded do
$$[:document][:body][:innerHTML] = '<h2>Hello World!</h2>'
end3- Run rails g scaffold welcome
4- Run rails db:migrate
5- Clear app/views/welcomes/index.html.erb (empty its content)
6- Run bin/rails opal:build
7- Run rails s
8- Visit http://localhost:3000/welcomes
In the browser webpage, you should see:
Also, you should see hello world! in the browser console.
Older opal-rails apps often used app/assets/javascripts/application.js.rb, manifest.js, and request-time Sprockets compilation.
See PORTING.md for the full 2.x -> 3.x checklist.
The build-first migration path is:
- move the Opal entrypoint to
app/opal/application.rbor keepapp/assets/opal/application.rbfor migration-friendly layouts; - configure
config.opal.source_path,config.opal.entrypoints_path, andconfig.opal.entrypointsinconfig/initializers/opal.rb; - build with
bin/rails opal:buildand watch withbin/rails opal:watch; - include the built asset with the normal
javascript_include_taghelper.
The documented path no longer relies on Sprockets directives such as require_tree or on helper-level Opal loader injection.
If the host app itself still uses Sprockets during migration, it can keep the minimal manifest entries needed to serve the built files, but opal-rails itself no longer requires opal-sprockets to compile or boot Opal assets.
You can use it for your views too:
# app/controllers/posts_controller.rb
def create
@post = Post.create!(params[:post])
render type: :js, locals: {comments_html: render_to_string(@post.comments)}
endAssigned instance that would normally be available in your views are converted to JSON objects first.
# app/views/posts/create.js.opal
post = Element.find('.post')
post.find('.title').html = @post[:title]
post.find('.body').html = @post[:body]
post.find('.comments').html = comments_htmlBy default opal-rails, will NOT forward any instance and local variable you'll pass to the template.
This behavior can be enabled by setting Rails.application.config.opal.assigns_in_templates to true in config/initializers/opal.rb:
Rails.application.configure do
# ...
config.opal.assigns_in_templates = true
# ...
endRequire haml-rails separately if you want to use Haml. opal-rails supports the optional :opal Haml filter on Haml 6 and newer only!
-# app/views/posts/show.html.haml
%article.post
%h1.title= post.title
.body= post.body
%a#show-comments Display Comments!
.comments(style="display:none;")
- post.comments.each do |comment|
.comment= comment.body
:opal
Document.ready? do
Element.find('#show-comments').on :click do |click|
click.prevent_default
click.current_target.hide
Element.find('.comments').effect(:fade_in)
end
endExtracted to (unreleased) opal-rspec-rails
Add this line to your Gemfile:
gem 'opal-rspec-rails', github: 'opal/opal-rspec-rails'Upcoming as opal-minitest-rails
As long as the templates are inside an Opal load path, you should be able to require them.
Let's say we have this template app/views/shared/test.haml:
.row
.col-sm-12
= @barWe need to make sure Opal can see and compile that template. Add the path to config.opal.append_paths:
# config/initializers/opal.rb
Rails.application.config.opal.append_paths << Rails.root.join('app', 'views', 'shared')Now, somewhere in application.rb you need to require that template, and you can just run it through Template:
# app/opal/application.rb
require 'opal'
require 'opal-haml'
require 'test'
@bar = "hello world"
template = Template['test']
template.render(self)
# => '<div class="row"><div class="col-sm-12">hello world</div></div>'Add gems to the Opal load path from config/initializers/opal.rb.
Example:
Rails.application.config.opal.use_gems << 'cannonbol'Both opal:build and opal:watch apply config.opal.use_gems and config.opal.append_paths when creating builders, so the same load-path behavior is used in direct builds, watch-mode rebuilds, and precompile hooks.
Run the specs:
bin/setup
bin/rake
bin/rake now runs the non-JS and browser-driven specs in separate RSpec phases so each browser pass starts from a fresh process.
Browser-driven specs auto-detect a usable local Chrome/Chromium binary, including Fedora's headless Chromium shell path when it is installed but not on PATH, and only skip when no supported browser binary can be found.
Inspect the test app:
bin/rackup
# visit localhost:9292
Tinker with a sandbox app:
bin/sandbox # re-creates the app, installs opal-rails, and runs an initial opal:build
bin/rails s # starts the sandbox app server
# or run bin/rails opal:watch in another shell while you edit Opal source files
# visit localhost:3000
© 2012-2024 Elia Schito
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.