I love Ruby, and it is my go-to language for building web applications. Unfortunately, when dealing with the browser, Javascript is a necessary evil. As you can see, I am not a huge fan.
So when someone comes along offering a way to use Ruby on the browser, sign me up!
In the first part of this article, I will introduce Opal and show how to get Opal set up. After that, we’ll dive in and implement the first half of our example application. (I will keep you in suspense for now, unless you scroll down, which will spoil the fun.)
Key Takeaways
- Opal is a Ruby to JavaScript compiler that allows you to write and run Ruby code in the browser, effectively bypassing the need to write JavaScript.
- Installation of Opal is straightforward, involving the use of RubyGems to install both Opal and opal-jquery, which provides a Ruby-style syntax for interacting with the DOM.
- The article demonstrates the practical use of Opal by setting up and developing Conway’s Game of Life, showcasing how Ruby code is compiled into JavaScript and interacts with web elements.
- Opal integrates seamlessly with Guard to automate the compilation process, enhancing development efficiency by recompiling Ruby code into JavaScript upon each file change.
- The use of Opal allows for leveraging Ruby’s syntax and features in front-end development, improving code readability and maintainability while ensuring compatibility with existing JavaScript libraries and frameworks.
Hello, Opal!
That someone who came along happens to be Adam Beynon, creator of Opal. Opal is a Ruby to Javascript source-to-source compiler. In other words, Opal translates the Ruby that you write into Javascript.
Getting Opal
Fire up a terminal:
% gem install opal opal-jquery
Successfully installed opal-0.6.0
Successfully installed opal-jquery-0.2.0
2 gems installed
Notice that we are also installing opal-jquery
. This gem wraps jQuery and provides a Ruby syntax to interact with the DOM. More on that later.
Let’s give Opal in spin in irb
:
% irb
> require 'opal'
=> true
> Opal.compile("3.times { puts 'Ohai, Opal!' }")
=> "/* Generated by Opal 0.6.0 */\n(function($opal) {\n var $a, $b, TMP_1, self = $opal.top, $scope = $opal, nil = $opal.nil, $breaker = $opal.breaker, $slice = $opal.slice;\n\n $opal.add_stubs(['$times', '$puts']);\n return ($a = ($b = (3)).$times, $a._p = (TMP_1 = function(){var self = TMP_1._s || this;\n\n return self.$puts(\"Ohai, Opal!\")}, TMP_1._s = self, TMP_1), $a).call($b)\n})(Opal);\n"
Conway’s Game of Life in Opal
It’s time to get our hands dirty and feet wet with Opal. I have always wanted a reason to build Conway’s Game of Life, so that’s our goal.
In case you are not familiar with Conway’s Game of Life (or too lazy to read the Wikipedia entry):
It starts with an empty grid of square cells. Every cell is either alive or dead. Each cell interacts with its eight neighbors.
Here, there 5 cells that are alive. The rest are dead. The cell marked with a blue dot is shown together with its 8 neighbors marked with red dots.
At each tick, a cell can undergo a transition based on four rules:
Rule 1
Any live cell with fewer than two live neighbors dies, as if caused by under-population.
Rule 2
Any live cell with two or three live neighbors lives on to the next generation.
Rule 3
Any live cell with more than three live neighbors dies, as if by overcrowding.
Rule 4
Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.
Amazingly, with just these 4 simple rules we can observe very interesting patterns. Here’s an example, called the lightweight spaceship, taken from ConwayLife.com:
Here is the spaceship in action:
Setting up
Go ahead an create an empty directory, call it conway_gol
, and create the following directory structure:
├── Gemfile
├── Rakefile
├── app
│ ├── conway.rb
├── index.html
└── styles.css
1. Gemfile
Populate your Gemfile to look like this:
source 'https://rubygems.org'
gem 'opal'
gem 'opal-jquery'
gem 'guard'
gem 'guard-rake'
After that, install the gems:
% bundle install
2. Setting Up Guard
Notice that we’re including the guard
gem.
Guard is extremely handy for development with Opal. Since Opal is a compiler, it needs to compile the Ruby code that you have written into Javascript. Therefore, each time you make changes, you recompile the source.Guard makes this process slightly easier.
Guard watches certain files or directories based on rules set in a Guardfile
, which we shall create shortly. It also comes with a bunch of handy plugins. For example, guard-rake
runs a Rake task when files change.
Next, in the conway_gol
directory, create a Guardfile
using the following command:
% bundle exec guard init
00:30:21 - INFO - rake guard added to Guardfile, feel free to edit it
Include this rule in your Guardfile
.
guard 'rake', :task => 'build' do
watch %r{^app/.+\.rb$}
end
This watches for any changes in any Ruby file in the app
directory. Such a change will trigger the rake build
task, which we will write next.
3. Setting Up the Rakefile
In Rakefile
:
require 'opal'
require 'opal-jquery'
desc "Build our app to conway.js"
task :build do
env = Opal::Environment.new
env.append_path "app"
File.open("conway.js", "w+") do |out|
out << env["conway"].to_s
end
end
Here is what the build
Rake task does:
- Sets up the directory to which the Ruby files are stored
- Creates
conway.js
, the result of the Ruby -> Javascript compilation.
4. Static Files
In index.html
:
<!DOCTYPE html>
<html>
<head>
<script src="https://code.jquery.com/jquery-1.11.0.min.js"></script>
<script src="https://code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<canvas id="conwayCanvas"></canvas>
<script src="conway.js"></script>
</body>
</html>
We need jQuery. We are also using the HTML5 canvas
element, so the usual “this will not work on older versions of Internet Explorer” disclaimer applies.
Lastly, we put the Opal-generated conway.js
below the canvas element. This is so the canvas element is available to conway.js
.
In styles.css
:
* {
margin: 0;
padding: 0;
}
This style gets rid of any gaps at the border when our grid is drawn.
5. Working with Opal
Before we use Guard to automate the process, here is an example on how you would interact with Opal:
Go to app/conway.rb
, and type this in:
require 'opal'
x = (0..3).map do |n|
n * n * n
end.reduce(:+)
puts x
In your terminal, under the conway_gol
directory, run the Rake task:
% rake build
Open index.html
. The result appears on the developer console of your browser. Here is what it looks like in Chrome:
Just for kicks, go see what the generated conway.js
looks like. While you admire the generated Javascript, take a moment to reflect on the brilliance of the Opal team.
Since we have Guard all set up, in another terminal window, run this command:
% bundle exec guard
01:11:39 - INFO - Guard is using TerminalTitle to send notifications.
01:11:39 - INFO - Starting guard-rake build
01:11:39 - INFO - running build
01:11:41 - INFO - Guard is now watching at '/Users/Ben/conway_gol'
[1] guard(main)>
Here’s a tip: Anytime you want to re-run rake build
, simply press ‘Enter’ in the Guard terminal window:
[1] guard(main)> (Hit Enter)
01:13:42 - INFO - Run all
01:13:42 - INFO - running build
Try making a change in conway.rb
:
require 'opal'
x = (0..3).map do |n|
n * n * n
end.reduce(:*) # <- change to this
puts x
Observe that, when you save conway.rb
(or any Ruby file for that matter), the terminal window running Guard outputs this message:
09:25:11 - INFO - running build
Refreshing the browser will display the updated value.
Now that we have made sure everything works, go ahead and delete everything in conway.rb
– The fun begins now!
Let the Games Begin!
Our application will consist of 2 major components. The first piece is the game logic. The second piece is drawing of the canvas and handling canvas events.
Let’s tackle the second piece first.
1. Drawing a Blank Grid
This is what we’re shooting for:
The grid lines cover the entire browser viewport. This means that we need to access the height and width of the this viewport. More importantly, we need to access the canvas element on the DOM before we can start drawing anything.
The Grid
Class
Open conway.rb
in app
, and fill it in with this:
require 'opal'
require 'opal-jquery'
class Grid
attr_reader :height, :width, :canvas, :context, :max_x, :max_y
CELL_HEIGHT = 15;
CELL_WIDTH = 15;
def initialize
@height = `$(window).height()`
@width = `$(window).width()`
@canvas = `document.getElementById(#{canvas_id})`
@context = `#{canvas}.getContext('2d')`
@max_x = (height / CELL_HEIGHT).floor
@max_y = (width / CELL_WIDTH).floor
end
def draw_canvas
`#{canvas}.width = #{width}`
`#{canvas}.height = #{height}`
x = 0.5
until x >= width do
`#{context}.moveTo(#{x}, 0)`
`#{context}.lineTo(#{x}, #{height})`
x += CELL_WIDTH
end
y = 0.5
until y >= height do
`#{context}.moveTo(0, #{y})`
`#{context}.lineTo(#{width}, #{y})`
y += CELL_HEIGHT
end
`#{context}.strokeStyle = "#eee"`
`#{context}.stroke()`
end
def canvas_id
'conwayCanvas'
end
end
grid = Grid.new
grid.draw_canvas
We need to explicitly require opal
and opal-jquery
.
The Grid
class looks mostly like Ruby. At first glance, we have all the usual Ruby syntax. Let’s look at each part of this class in slightly more detail, starting with initialize
.
initialize
class Grid
attr_reader :height, :width, :canvas, :context, :max_x, :max_y
CELL_HEIGHT = 15;
CELL_WIDTH = 15;
def initialize
@height = `$(window).height()`
@width = `$(window).width()`
@canvas = `document.getElementById(#{canvas_id})`
@context = `#{canvas}.getContext('2d')`
@max_x = (height / CELL_HEIGHT).floor
@max_y = (width / CELL_WIDTH).floor
end
def canvas_id
'conwayCanvas'
end
### snip snip ###
end
In Opal, we can evaluate Javascript directly in back-ticks. For instance, to get the height of the browser viewport:
@height = `$(window).height()`
Opal stores the value of @height
as a Numeric
Ruby class. We are also using $
, which is calling a jQuery instance.
Working with Canvas
Don’t worry if you have never worked with the canvas
element before, since that’s not the main point of this article, anyway. Everything I know about canvas
came from Dive Into HTML5.
@canvas = `document.getElementById(#{canvas_id})`
To work with the canvas, you need a reference to it in the DOM. Look at how we can use string interpolation to fill in the canvas id via a canvas_id
method call.
@context = `#{canvas}.getContext('2d')`
More importantly, every canvas has a drawing context. All the drawing is done via this context. Notice how we use string interpolation once again to pass in canvas
, retrieve the context, and store it in @context
.
@max_x = (height / CELL_HEIGHT).floor
@max_y = (width / CELL_WIDTH).floor
@max_x
and @max_y
store the limits of the grid in terms of coordinates, which explains why we need to divide by CELL_HEIGHT
and CELL_WIDTH
.
draw_canvas
This is how we draw the grid lines on the canvas. canvas
is only called to set the width and height. All the drawing is handled with function calls to context
.
draw_canvas
is a nice example of how Opal lets you use Ruby and Javascript code together in perfect harmony.
def draw_canvas
`#{canvas}.width = #{width}`
`#{canvas}.height = #{height}`
x = 0.5
until x >= width do
`#{context}.moveTo(#{x}, 0)`
`#{context}.lineTo(#{x}, #{height})`
x += CELL_WIDTH
end
y = 0.5
until y >= height do
`#{context}.moveTo(0, #{y})`
`#{context}.lineTo(#{width}, #{y})`
y += CELL_HEIGHT
end
`#{context}.strokeStyle = "#eee"`
`#{context}.stroke()`
end
Let’s See the Canvas
Finally, getting the grid to draw is just a simple method call away:
grid = Grid.new
grid.draw_canvas
If you are running Guard press ‘Enter’, or you could run rake build
. Either way, when you open index.html
, you will see a glorious grid.
2. Adding Some Interactivity
Being able to draw grid lines on a canvas
– in Ruby, no less! – is all well and good, but entirely useless if we cannot do anything to it.
Let’s spice things up a little.
One of the things we can to do is fill in a cell. In order to do that, we need to know where we clicked, and then compute the cell’s position with respect to our grid. That is, we need to figure out the clicked coordinates based on the grid we drew.
Even before knowing where we clicked, we need to know when we clicked. In this section, we also look at how opal-jquery
lets us use Ruby to interact with jQuery’s event listeners.
Fill and Unfilling a Cell
The following methods draw a black square and clears a square of the same dimensions:
def fill_cell(x, y)
x *= CELL_WIDTH;
y *= CELL_HEIGHT;
`#{context}.fillStyle = "#000"`
`#{context}.fillRect(#{x.floor+1}, #{y.floor+1}, #{CELL_WIDTH-1}, #{CELL_HEIGHT-1})`
end
def unfill_cell(x, y)
x *= CELL_WIDTH;
y *= CELL_HEIGHT;
`#{context}.clearRect(#{x.floor+1}, #{y.floor+1}, #{CELL_WIDTH-1}, #{CELL_HEIGHT-1})`
end
Getting the Position of the Cursor
Here’s an example on translating Javascript into Ruby. I was too lazy and impatient to figure out how to implement this particular function. As it turns out, Dive Into HTML 5 already has an example in Javascript:
function getCursorPosition(event) {
var x;
var y;
if (event.pageX != undefined && event.pageY != undefined) {
x = event.pageX;
y = event.pageY;
}
else {
x = event.clientX + document.body.scrollLeft +
document.documentElement.scrollLeft;
y = event.clientY + document.body.scrollTop +
document.documentElement.scrollTop;
}
}
Now, let’s see what the Opal-flavored Ruby looks like:
def get_cursor_position(event)
if (event.page_x && event.page_y)
x = event.page_x;
y = event.page_y;
else
doc = Opal.Document[0]
x = event[:clientX] + doc.scrollLeft +
doc.documentElement.scrollLeft;
y = event[:clientY] + doc.body.scrollTop +
doc.documentElement.scrollTop;
end
end
As you can see, there’s almost a one to one conversion.
You might be wondering why are there 2 branches just to find the cursor position. The short answer is different browsers have different ways of implementing this functionality.
Discovering Methods
How did I know, for example, that the page_x
method exists for event
or that clientX
should be accessed using the hash notation?
def get_cursor_position(event)
`console.log(#{event})` # <- I cheated.
# code omitted
end
I did not use puts event
or even p event
. I picked console.log
instead.
Here’s why:
Using console.log
gives us much more detail, since event
is first and foremost, a Javascript object. Using puts
, p
or even inspect
doesn’t do much.
In the if
branch, we access event
using the dot notation, while in the else
branch, we treat event
like a hash.
In the green box, event.page_x
is a method call because there indeed is a page_x
function defined as $page_x: function { ... }
.
In the purple box, clientX
is a value. Therefore, it is accessed using the hash notation.
Here is some additional code to compute the coordinates respect to the grid.
def get_cursor_position(event)
## Previous code omitted ...
x -= `#{canvas}.offsetLeft`
y -= `#{canvas}.offsetTop`
x = (x / CELL_WIDTH).floor
y = (y / CELL_HEIGHT).floor
Coordinates.new(x: x, y: y)
end
Coordinates
and OpenStruct
I’ve snuck in a Coordinates
class. Interestingly, Opal has OpenStruct, too.
You can define Coordinates
like so:
require 'opal'
require 'opal-jquery'
require 'ostruct' # <- remember to do this!
class Grid
# ...
end
class Coordinates < OpenStruct; end
Just like the Ruby version, we need to require ostruct
in order to use it.
Event Listening
Finally, we have all the building blocks to listen for events. Both listeners will listen for events on canvas
.
The first event listener triggers on a single click mouse event. Once that happens, the cursor position is computed and the appropriate cell is filled.
The second event listener triggers on a double click mouse event. Again, the cursor position is computed and the appropriate cell is unfilled.
def add_mouse_event_listener
Element.find("##{canvas_id}").on :click do |event|
coords = get_cursor_position(event)
x, y = coords.x, coords.y
fill_cell(x, y)
end
Element.find("##{canvas_id}").on :dblclick do |event|
coords = get_cursor_position(event)
x, y = coords.x, coords.y
unfill_cell(x, y)
end
end
After drawing the canvas, register the mouse listener:
class Grid
# ...
end
grid = Grid.new
grid.draw_canvas
grid.add_mouse_event_listener # <- Add this!
Once our changes are built, go ahead and open index.html
. Try clicking on any grid to mark a cell and double clicking to unmark a cell.
Here’s my masterpiece:
The Full Source
For reference, here’s the full source code:
require 'opal'
require 'opal-jquery'
require 'ostruct'
class Grid
attr_reader :height, :width, :canvas, :context, :max_x, :max_y
CELL_HEIGHT = 15;
CELL_WIDTH = 15;
def initialize
@height = `$(window).height()` # Numeric!
@width = `$(window).width()` # A Numeric too!
@canvas = `document.getElementById(#{canvas_id})`
@context = `#{canvas}.getContext('2d')`
@max_x = (height / CELL_HEIGHT).floor # Defines the max limits
@max_y = (width / CELL_WIDTH).floor # of the grid
end
def draw_canvas
`#{canvas}.width = #{width}`
`#{canvas}.height = #{height}`
x = 0.5
until x >= width do
`#{context}.moveTo(#{x}, 0)`
`#{context}.lineTo(#{x}, #{height})`
x += CELL_WIDTH
end
y = 0.5
until y >= height do
`#{context}.moveTo(0, #{y})`
`#{context}.lineTo(#{width}, #{y})`
y += CELL_HEIGHT
end
`#{context}.strokeStyle = "#eee"`
`#{context}.stroke()`
end
def get_cursor_position(event)
puts event
p event
`console.log(#{event})`
if (event.page_x && event.page_y)
x = event.page_x;
y = event.page_y;
else
doc = Opal.Document[0]
x = e[:clientX] + doc.scrollLeft + doc.documentElement.scrollLeft;
y = e[:clientY] + doc.body.scrollTop + doc.documentElement.scrollTop;
end
x -= `#{canvas}.offsetLeft`
y -= `#{canvas}.offsetTop`
x = (x / CELL_WIDTH).floor
y = (y / CELL_HEIGHT).floor
Coordinates.new(x: x, y: y)
end
def fill_cell(x, y)
x *= CELL_WIDTH;
y *= CELL_HEIGHT;
`#{context}.fillStyle = "#000"`
`#{context}.fillRect(#{x.floor+1}, #{y.floor+1}, #{CELL_WIDTH-1}, #{CELL_HEIGHT-1})`
end
def unfill_cell(x, y)
x *= CELL_WIDTH;
y *= CELL_HEIGHT;
`#{context}.clearRect(#{x.floor+1}, #{y.floor+1}, #{CELL_WIDTH-1}, #{CELL_HEIGHT-1})`
end
def add_mouse_event_listener
Element.find("##{canvas_id}").on :click do |event|
coords = get_cursor_position(event)
x, y = coords.x, coords.y
fill_cell(x, y)
end
Element.find("##{canvas_id}").on :dblclick do |event|
coords = get_cursor_position(event)
x, y = coords.x, coords.y
unfill_cell(x, y)
end
end
def canvas_id
'conwayCanvas'
end
end
class Coordinates < OpenStruct; end
grid = Grid.new
grid.draw_canvas
grid.add_mouse_event_listener
Up Next …
In a future post, we’ll complete our Conway’s Game of Life application by implementing the game logic and hooking it to our grid. In doing so, we’ll also get to see a few more examples of how Opal gracefully blends Ruby and Javascript,
Thanks for reading!
Frequently Asked Questions (FAQs) about Opal Ruby Browser Basics
What is Opal Ruby and how does it work?
Opal Ruby is a source-to-source compiler that translates Ruby code into JavaScript. It allows developers to write front-end code in Ruby, which is then compiled into JavaScript for execution in the browser. This provides the advantage of using Ruby’s syntax and features for client-side development, while still ensuring compatibility with JavaScript-based web technologies.
How can I get started with Opal Ruby?
To get started with Opal Ruby, you need to install the Opal gem. Once installed, you can write Ruby code and compile it into JavaScript using the Opal compiler. You can also use Opal with popular web frameworks like Rails, which provides additional tools and features for building web applications.
What are the benefits of using Opal Ruby for browser-based development?
Opal Ruby provides several benefits for browser-based development. It allows developers to use Ruby’s syntax and features for client-side development, which can improve productivity and code readability. It also provides a seamless integration with JavaScript, allowing you to use JavaScript libraries and frameworks alongside your Ruby code.
Can I use Opal Ruby with Rails?
Yes, Opal Ruby can be used with Rails. This allows you to write both your front-end and back-end code in Ruby, providing a consistent development experience across your application. To use Opal with Rails, you need to add the opal-rails gem to your Gemfile and bundle install.
How does Opal Ruby compare to other JavaScript compilers?
Opal Ruby is unique in that it allows you to write front-end code in Ruby, a language typically used for back-end development. This provides a consistent development experience across your application and can improve productivity and code readability. However, like other JavaScript compilers, Opal Ruby ensures compatibility with JavaScript-based web technologies.
Is Opal Ruby suitable for large-scale applications?
Yes, Opal Ruby is suitable for large-scale applications. It provides a robust and efficient compilation process, and its seamless integration with JavaScript ensures compatibility with modern web technologies. Additionally, Opal Ruby’s compatibility with Rails makes it a good choice for large-scale web applications.
What are some common use cases for Opal Ruby?
Opal Ruby is commonly used for front-end development in web applications. It allows developers to write client-side code in Ruby, which is then compiled into JavaScript for execution in the browser. This is particularly useful for applications that use Rails, as it allows for a consistent development experience across the application.
How can I debug Opal Ruby code?
Debugging Opal Ruby code is similar to debugging JavaScript code. You can use browser-based debugging tools to step through your code, inspect variables, and identify issues. Additionally, Opal provides source maps, which allow you to debug your Ruby code directly in the browser.
Can I use JavaScript libraries and frameworks with Opal Ruby?
Yes, you can use JavaScript libraries and frameworks with Opal Ruby. Opal provides a seamless integration with JavaScript, allowing you to use JavaScript libraries and frameworks alongside your Ruby code.
What is the future of Opal Ruby?
Opal Ruby continues to be actively developed and maintained, with new features and improvements being added regularly. Its unique approach to front-end development and its seamless integration with JavaScript and Rails make it a promising choice for modern web development.
Benjamin is a Software Engineer at EasyMile, Singapore where he spends most of his time wrangling data pipelines and automating all the things. He is the author of The Little Elixir and OTP Guidebook and Mastering Ruby Closures Book. Deathly afraid of being irrelevant, is always trying to catch up on his ever-growing reading list. He blogs, codes and tweets.