Open Source Adventures: Episode 63: Accessing Browser APIs Directly with Opal Ruby
Opal Ruby really wants to be used with some wrappers and frameworks, but let's start by accessing browser APIs directly.
Note that this is not how you'd use Opal Ruby in the real world. You'd use various wrappers, and for basic browser APIs there's a lot of choice.
However if you write anything nontrivial, you'll need to interact with various new APIs and JavaScript packages, and then you'll have a choice of either going in raw, or writing wrappers.
First, let's get through all the boilerplate.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Click Counter</title>
<link rel="stylesheet" href="./app.css">
</head>
<body>
<button>Click</button>
<div id="summary""></div>
<script src="./app.js"></script>
</body>
</html>
app.css
body {
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 5px;
}
Gemfile
We just need opal
, and rake
for build command.
source "https://rubygems.org"
gem "opal", "~> 1.5"
gem "rake", "~> 13.0"
Rakefile
task default: "app.js"
file "app.js" => "app.rb" do
sh "opal -c app.rb >app.js"
end
app.rb
And now the actual code!
win = `window`
document = win.JS[:document]
summary = document.JS.querySelector("#summary")
button = document.JS.querySelector("button")
count = 0
button.JS.addEventListener("click") do
count += 1
summary.JS[:innerText] = "You clicked #{count} times"
end
The first thing to note is that you can run any JavaScript code with backticks. We'll need it to access the window
object.
We'd like to name that variable window
, but that would then compiles to var window = window
which doesn't actually work.
If we have a JavaScript object, we can use it in two ways - .JS[]
accesses JavaScript property, and .JS.foo()
does a function call. We need both syntaxes as win.JS.document
would compile to win.document()
, not quite what we want.
Let's see what all this compiles to:
Opal.queue(function(Opal) {/* Generated by Opal 1.5.0 */
var nil = Opal.nil, $rb_plus = Opal.rb_plus, win = nil, document = nil, summary = nil, button = nil, count = nil;
Opal.add_stubs('+');
win = window;
document = win["document"];
summary = document.querySelector("#summary");
button = document.querySelector("button");
count = 0;
return button.addEventListener("click", function $$1(){
count = $rb_plus(count, 1);
return summary["innerText"] = "You clicked " + (count) + " times";}, 0);
});
There's a bunch of extra return
s due to mismatch between Ruby and JavaScript, but they're mostly harmless. If you'd like to get rid of them, just add some nil
s at ends of blocks. return
at end of the whole script makes a lot less sense, but I guess they didn't care enough to special case it.
this
problem
Did you spot another problem? Why is Opal Ruby using function
not =>
there, and wouldn't that cause issues?
Of course it does. Let's modify the app slightly, by adding some puts
, which performs console.log
:
win = `window`
document = win.JS[:document]
summary = document.JS.querySelector("#summary")
button = document.JS.querySelector("button")
count = 0
button.JS.addEventListener("click") do
puts "Button clicked!"
count += 1
summary.JS[:innerText] = "You clicked #{count} times"
end
puts "Application started!"
This gets compiled to:
Opal.queue(function(Opal) {/* Generated by Opal 1.5.0 */
var self = Opal.top, nil = Opal.nil, $rb_plus = Opal.rb_plus, win = nil, document = nil, summary = nil, button = nil, count = nil;
Opal.add_stubs('puts,+');
win = window;
document = win["document"];
summary = document.querySelector("#summary");
button = document.querySelector("button");
count = 0;
button.addEventListener("click", function $$1(){var self = $$1.$$s == null ? this : $$1.$$s;
self.$puts("Button clicked!");
count = $rb_plus(count, 1);
return summary["innerText"] = "You clicked " + (count) + " times";}, {$$arity: 0, $$s: self});
return self.$puts("Application started!");
});
Application started!
part works just fine, but Button clicked!
crashes miserably, as it incorrectly changes self
to the wrong one.
If Opal Ruby was updated to use =>
like modern JavaScript code, this
inside the block would be correct, so the problem would be averted.
Story so far
As you can see, Opal Ruby interoperability with JavaScript world is quite poor. Fortunately we can sweep that nasty stuff into a wrapper, and there's plenty of wrappers for all the common things like browser APIs. If you want to seriously use Opal Ruby, you'll be writing a lot of wrappers as well.
And it's also high time for Opal Ruby to target ES6 - sticking to pre-ES6 code causes unnecessary issues like this
issue we ran into.
I deployed this on GitHub Pages, you can see it here.
Coming next
That's enough Opal Ruby for now. In the next episode we'll explore another technology.