There’s an old meme about JavaScript:
JavaScript: The Definitive Guide is 1096 pages long, and (from the same publisher) JavaScript: The Good Parts is 176 pages long.1
The JavaScript: The Slimmest Guide on this page is even smaller than JavaScript: The Good Parts. This is the tiniest introduction that I could come up with to get you ready to do what we want to do: make our Rails apps snappy, such that every click doesn’t require a full page reload.
The good news is that Ruby and JavaScript are quite similar! Basics like numbers, strings, arrays, math operators (+
, -
, *
, \
, %
, **
), and the receiver.message
syntax for calling methods on objects are the same.
There are differences, however. Go through the following Ruby statements and the roughly equivalent translations2 into JavaScript. Experiment with them as you read.
bin/server
app/views/misc/home.html.erb
.irb
or rails console
, where you can type any JavaScript expression and execute it immediately.<script>
element on the page and then refresh it to execute.Unlike Ruby, but like many languages (like CSS), JavaScript uses semicolons to separate lines/expressions:
Ruby:
p "howdy"
p "world"
JavaScript:
console.log("howdy");
console.log("world");
Nowadays, JavaScript can automatically insert semicolons in most cases. So the following will also work:
console.log("howdy")
console.log("world")
But this doesn’t work in all cases — just 99% of them. For now, in order to avoid confusing issues, my rule of thumb is to put a semicolon at the end of every line that doesn’t end in a curly bracket.
With that out of the way, let’s see and experiment with some Ruby expressions and their JavaScript equivalents. Wherever feasible, try typing out the JavaScript into a <script>
tag or into the JavaScript console, rather than copy-pasting. Develop some muscle memory.
Ruby:
p "howdy!"
JavaScript:
console.log("howdy!");
Ruby:
"Your lucky number is #{7 * 6}"
# => "Your lucky number is 42"
JavaScript:
`Your lucky number is ${7 * 6}`
// => Your lucky number is 42
Use backticks instead of double-quotes to create the string, and then use ${}
to interpolate instead of #{}
.
Ruby:
name = "Alice"
p "howdy, #{name}!"
name = "Bob"
p "hi, #{name}!"
JavaScript:
let name = "Alice";
console.log(`howdy ${name}!`);
name = "Bob";
console.log(`hi ${name}!`);
Difference: use let
when creating the variable for the first time.
Ruby:
PI = 3.14
JavaScript:
const pi = 3.14;
Difference: In Ruby, constants are ALL_CAPS
. In JavaScript, the are lowercase like variables, but we use the const
keyword to create them instead of let
.
In Ruby, we use constants over variables when we want to indicate to people reading the code that “this value will never change”. However, Ruby itself allows you to update constants if you want:
PI = 3.14
PI = 3
# => works fine
So there’s no real functional difference between constants and variables in Ruby.
However, in JavaScript, attempting to replace the value in a constant will result in an error:
const pi = 3.14;
pi = 3;
// Uncaught TypeError: Assignment to constant variable.
Ruby:
a_really_long_variable_name = 42
JavaScript:
let aReallyLongVariableName = 42;
Difference: In Ruby we prefer to snake_case when we want to indicate word separation within a single identifier. JavaScript prefers lowerCamelCase.
Both languages support either style; it’s just a matter of community preference/taste.
Ruby:
def greet
p "howdy!"
end
greet
JavaScript:
function greet() {
console.log("howdy!");
}
greet();
Difference: In JavaScript, if you don’t include the parentheses (even if there are no arguments), the function will not be executed; it will be referenced as an object (so that you can e.g. pass it as an argument to a method that takes a function as an input).
Ruby:
def greet(name)
p "howdy, #{name}!"
end
greet("Alice")
JavaScript:
function greet(name) {
console.log(`howdy, ${name}!`);
}
greet("Alice");
Ruby:
x = 5
x += 1
# x is now 6
JavaScript:
let x = 5;
x++
# x is now 6
Ruby:
for i in 2...6
p i * i
end
JavaScript:
for (let i = 2; i < 6; i++) {
console.log(i * i);
}
Ruby:
fruits = ["apples", "oranges", "bananas"]
p fruits[1]
p fruits.length
JavaScript:
let fruits = ["apples", "oranges", "bananas"];
console.log(fruits[1]);
console.log(fruits.length);
Ruby:
fruits = ["apples", "oranges", "cherries"]
fruits.each do |fruit|
p "I love #{fruit}!"
end
JavaScript:
let fruits = ["apples", "oranges", "cherries"];
fruits.forEach(function(fruit) {
console.log(`I love ${fruit}!`);
});
Difference: In Ruby we use do
to start a block and end
to close it. In JavaScript, we use function() {
to start a block and }
to close it. In JavaScript, these are called “anonymous functions” rather than “blocks”.
If the method provides values for block variables, in Ruby we name them within optional “pipes” | |
after the do
. In JavaScript, we name them within the parentheses ( )
(which are not optional) after function
.
In JavaScript, when you provide an anonymous function as an argument to another function, they are referred to as “callback functions”. Just as we use blocks a lot in Ruby, we use callbacks a ton in JavaScript.
For a random decimal greater than or equal to 0
and less than 1
:
Ruby:
win_probability = rand
JavaScript:
let win_probability = Math.random();
For a random integer:
Ruby:
dice_roll = rand(6)
JavaScript:
let dice_roll = Math.floor(Math.random() * 6);
Math.floor()
rounds a number down, so the above code will return an integer between 0 and 5.
Ruby:
MOVES = ["rock", "paper", "scissors"]
computer_move = MOVES.sample
p "The computer played #{computer_move}."
if computer_move == "rock"
p "You tied."
elsif computer_move == "paper"
p "You lost."
else
p "You won."
end
JavaScript:
const moves = ["rock", "paper", "scissors"];
let random_index = Math.floor(Math.random() * 3);
let computer_move = moves[random_index];
console.log(`The computer played ${computer_move}.`);
if (computer_move === "rock") {
console.log("You tied.");
} else if (computer_move === "paper") {
console.log("You lost.");
} else {
console.log("You won.");
}
Note: The comparisons use triple ===,
not double ==
.
Beware: There’s also a JS double ==
(non-strict equivalence) operator. In most cases, you should use ===
.
The other comparison operators work as you would expect in Ruby (!=
, <
, <=
, >
, >=
).
Try writing fizz buzz in JavaScript:
For the numbers 1 through 100, print “fizz” if the number is a multiple of 3, “buzz” if the number is a multiple of 5, and “fizzbuzz” if the number is a multiple of both.
Your output in the JavaScript console should look something like this:
1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz
16
17
fizz
19
buzz
// etc ...
89
fizzbuzz
91
92
fizz
94
buzz
fizz
97
98
fizz
buzz
If you can do this challenge, you’re in great shape!
JavaScript is okay, as far as languages go, but we were perfectly happy with Ruby. The only reason we’re interested in JavaScript is that it has a unique ability: the browser can execute it directly, and that means we can use it to modify HTML elements in our pages without needing to refresh the page. This is the key to making our apps feel like apps and not pages.
Let’s see how this works. Visit the homepage of our app, which contains the following code at the moment:
<h1>Cities</h1>
<ul id="cities">
<li id="aus" class="south">Austin</li>
<li id="bos" class="east">Boston</li>
<li id="ord" class="midwest">Chicago</li>
<li id="lax" class="west">Los Angeles</li>
<li id="jfk" class="east">New York</li>
<li id="sfo" class="west">San Francisco</li>
</ul>
In Chrome, open the JavaScript Console from the View > Developer menu. There, you have a REPL, much like irb
or rails console
, where you can type any JavaScript expression. (There’s also great autocomplete functionality in the Chrome dev tools, so you can use tab often.)
Importantly, there is an object available, document
, that represents the page itself:
document
Type the following into the JavaScript Console:
let items = document.getElementsByTagName("li")
items
We’ve created a variable, items
, and stored an array of all the li
elements on the page within it. Neat!
As you were typing, you might have noticed the autocomplete showing you other methods that were available — getElementsByClassName
, getElementByID
, etc. These are all ways that we can query the DOM (Document Object Model) and find HTML elements; exactly the way we did when we were writing CSS selectors.
Let’s assign the list item representing Chicago to a variable chi
:
let chi = document.getElementById("ord");
Now that we have an element in the variable chi
, we can retrieve its text with the innerText
property:
chi.innerText;
Crucially, we can modify the element:
chi.style.backgroundColor = "green"
(sets a CSS property value for the element)chi.remove()
(removes the element from the DOM)Poof! Let’s put it back in the DOM, but at the end of the list:
list = document.getElementById("cities_list");
list.append(chi)
Let’s add a blue border around the cities in the east:
eastItems = document.getElementsByClassName("east");
eastItems.style.border = "thin blue solid";
Oops! This errors out with Uncaught TypeError: Cannot set properties of undefined (setting 'border')
. That is because eastItems
currently contains an array of elements, not an individual element; and an array doesn’t respond to style
.
So, we’ll have to get the elements out one by one and then set the property:
eastItems[0].style.border = "thin blue solid";
eastItems[1].style.border = "thin blue solid";
That would be quite tedious if there were a hundred items, so let’s use a loop instead:
for (let i = 0; i < eastItems.length; i++) {
eastItems[i].style.border = "thin red solid";
}
We can also create new elements and add them to the DOM. Let’s construct an item for Denver and then insert it at the beginning of our list:
let den = document.createElement("li")
let den_text = document.createTextNode("Denver")
den.append(den_text)
den.style.backgroundColor = "orange"
den.className = "west"
den.id = "den"
list.prepend(den)
Great! So: with JavaScript, we can manipulate the DOM after the page renders. This opens up a world of possibilities.
Nowadays, plain vanilla JavaScript is pretty nice to use; but before, say, 2015, it was quite a different story. The language itself was missing lots of features that we Rubyists take for granted, there were many cross-browser inconsistencies, and life was hard.
Sound familiar? It was the same story with CSS; with CSS, developers came together to share their solutions to rough edges and cross-browser inconsistencies with frameworks like Bootstrap. In the JavaScript world, the go-to library to paper over JavaScript’s inconsistencies was jQuery.
Nowadays, jQuery isn’t absolutely essential to survival, the way it was for many years. Many developers, especially the developers of libraries like Bootstrap, have started to remove it and just use vanilla JavaScript, which does have some benefits in terms of performance/the amount of data users have to download.
However, I still use jQuery on almost every project. I recommend that you start out with it too, because as beginners, we’re not interested in grappling with the harder, “purer” way of doing things right now. We just want to get productive quickly.
So: let’s experiment with jQuery. Here’s a convenient CDN we can use to pull it in. Add the following line to your HTML document:
<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
And refresh the page.
jQuery is an incredibly useful library, and gives us access to a massive ecosystem of plugins; but basically, the way we use it starts out in just one way: by calling the jQuery()
function3.
The jQuery()
function is sort of like the document.getElementsByTag()
, document.getElementsByClassName()
, document.getElementByID()
, etc, functions that we met earlier — but all wrapped into one. You pass in any CSS selector (as a string) as an argument, and jQuery()
will return an array of matching elements. Try this in the JS console:
jQuery("li");
jQuery(".west");
jQuery("#ord");
Since we’re going to be using the jQuery()
function a lot, they conveniently aliased it as $()
, to save us a bunch of typing. So the above could also be shortened to:
$("li");
$(".west");
$("#best");
Once we’ve queried the DOM for the elements we’re interested in, jQuery provides many methods for manipulating them. Let’s try putting an element in a variable:
let chi = $("#ord");
Since we obtained chi
from the jQuery function, it is now a jQuery object, which means it has different methods from the objects returned by the vanilla getElementById()
, etc, methods.
You can get its inner text with the text()
function:
chi.text();
Note that if you tried chi.text
, it would return the function itself; not the actual text. In JavaScript, you have to execute the function explicitly by adding the parentheses, even if there are no arguments. (I forget to do this approximately 82 times per day.)
Let’s do what we did before in vanilla JS: set Chicago’s background-color
to green
:
chi.css("background-color", "green");
Any CSS property/value pair can be set with the css()
function.
And, just as before, we can remove it:
chi.remove();
And add it back to the end of the list:
list = $("#cities_list");
list.append(chi);
Let’s add a border to the cities in the east:
$(".east").css("border", "thin blue solid");
Ah ha! Notice that we didn’t have to write the loop ourselves this time; jQuery takes care of it for us.
To create a new element, we can pass a string containing the HTML directly to the $()
function, which is much more convenient than using createElement
and then adding content and attributes one by one:
let den = $('<li id="den" class="west">Denver</li>');
Much better. Remember to use single quotes to create the string, or alternatively to escape the inner double quotes.
Now, we can add the new item to the list:
list.append(den);
Sometimes we want to hide an element, but not remove it entirely from the DOM. For that, jQuery provides the convenient hide()
method:
chi.hide();
Poof! Let’s bring it back:
chi.show();
Or, if we’re not sure what state it’s in (visible or invisible) to start with, we can toggle:
chi.toggle();
chi.toggle();
chi.toggle();
That’s pretty abrupt, though. How about something a little smoother?
chi.hide();
chi.fadeIn();
chi.fadeOut();
chi.fadeToggle();
chi.fadeToggle();
chi.fadeToggle();
Or even smoooooother:
chi.fadeToggle(5000);
As I mentioned earlier, JavaScript is callback-heavy. Here’s a simple example:
In the smoooooother example above, we had the element take 5 seconds to fade. What if we want to remove the element after it’s faded out?
Try this:
chi.fadeToggle(5000);
chi.remove();
You’ll see that the element is removed before the fade completes (when we reload the page, we barely even see the element at all before it is removed).
What if we wanted to have something happen after the element faded out, but not until then? No problem! fadeToggle
(like most jQuery methods) accepts a callback that it will run once it completes:
chi.fadeToggle(5000, function() {
alert("All done!");
});
So now we can remove at the right time:
chi.fadeToggle(5000, function() {
chi.remove();
});
Slightly more idiomatic would be to use the this
keyword (which is sorta like Ruby’s self
) so that our callback is not dependent on variable names defined outside of it:
chi.fadeToggle(5000, function() {
this.remove();
});
Another common thing we’ll do is bind event handlers to elements. Try the following:
Add the following to the HTML:
<div id="bye">Click me</div>
Now let’s add a “click handler” to our new div that will toggle the western cities:
$("body").on("click", "#bye", function() {
$(".west").slideToggle();
});
This is saying: “Within <body>
, find elements with an id of bye
, and when they are clicked run the following callback.” Now, whenever the div is clicked, elements with class west
will slide up and down. Nice!
As you can see, we can use the on
method to react to "click"
s to any element; we’re no longer constrained to users only clicking <a>
s or <button>
s.
Examples of events that apply to only certain kinds of elements are "change"
(for <select>
s), "keydown"
(for <input>
s), and "submit"
(for forms).
If you look at the left sidebar of the jQuery docs, you’ll see they nicely organize methods by category:
It’s worth looking through the docs for what you need; but usually, you can Google “jQuery how to ____” and have pretty good success. E.g., “jquery how to add an element as the last child”.
Here are the jQuery methods that I wind up using most frequently. Read about and experiment with them. If you need to, refresh the page to reset the state of the elements:
"reset"
to clear all of the inp in a form)Note that if you’re using, for example, the Google Maps JS library, or any JS library that acts upon one of your HTML elements: you need to wait until the element that you want to call the method on has been loaded before it will work.
Unlike CSS, JavaScript runs asynchronously in the browser, so there’s no guarantee that just because the <script>
tag appears in the document after the element that the element will have been fully loaded in time for the code within the <script>
to affect it.
To solve this problem, jQuery adds a .ready
method4 to the $(document)
object that we can, you guessed it, provide a callback to. If we put all our code that binds event handlers to elements within this callback, it will execute only after the document has been fully loaded.
For example, to attach click handlers reliably, instead of this:
<script>
$("#zebra").on("click", ".giraffe", function() {
console.log("clicked!");
});
</script>
We should do something like the following:
<script>
$(document).ready(function() {
$("#zebra").on("click", ".giraffe", function() {
console.log("clicked!");
});
});
</script>
And that, from the perspective of a Rails developer who wants to improve the UX of their web application, is all we need to know about JavaScript — for the moment5.
Now, we’re ready to revamp the interactions in our UI. When someone clicks on a link or submits a form, in many cases (usually after an action that CRUDs some records in some table(s)) we end up redirecting them to the same URL that they were on before; but they wind up at the top of the page, losing their scroll position. Annoying!
Let’s see if we can use our new JavaScript skills to improve this experience — with a technique called Asynchronous JavaScript And XML — Ajax6. That’s coming up next.
The joke, back in the day, was about the large amount of cruft in the language that no one liked. Fortunately, the situation is much improved nowadays. ↩
I find that a “rosetta stone” approach is very helpful when learning a new programming language. ↩
Let me point out right away the the official jQuery documentation is quite good, so you should refer to it often. ↩
Read more about $(document).ready
here, if you’re interested. ↩
Sidenote: Compare your experience learning JavaScript for the first time to when you learned Ruby for the first time. How did it feel this time around to learn about variables, string interpolation, arrays, arguments, etc?
So much better, right? This was, for me, one of the best things about learning Ruby.
After learning (the friendly, welcoming) Ruby and building some pragmatic, real-world applications in Rails, and through that getting a pretty good understanding of programming concepts (variables, conditionals, loops, objects, methods, arguments, etc) — my second[^second_lang] language was so much easier, and the third was even easier still.
Conversely, every language has some distinctive characteristics about it that will change the way you think and make you better at your previous languages. Welcome to being a polyglot programmer 🙌🏾
This has hiring implications. Really good developers can get up to speed on a new language pretty quickly (especially if it’s a friendly language like Ruby). If there are other developers on the team to answer questions about the codebase/framework while they are working on small introductory tasks, then it shouldn’t be a dealbreaker that a strong developer has 0 years of experience in a stack, if they say they’re willing to learn. ↩
No one really uses XML for it anymore, but Ajax sounds much better than “Ajaj” (for JSON) or “Ajah” (for HTML), so we stuck with it 😉 ↩