JavaScript code can access and manipulate the browser's DOM tree Show archive.org snapshot . Using JavaScript we can add interactive behavior to our interfaces that would not be possible through declarative rules in HTML and CSS alone.
Important
Work on this lesson in
teachermode.
Learning goals
- You can explain what the DOM is and how it relates to the HTML the server sent.
- You can select elements by CSS selector, on the whole document or within a parent element.
- You can react to user actions by registering event listeners and reading the event that was triggered.
- You can change an element's classes, attributes and content, and explain the difference between an HTML attribute and a DOM property.
- You can apply a change to many elements at once.
- You can explain why a script may run before the DOM exists, and how script placement,
deferandDOMContentLoadeddecide when it runs. - You can test JavaScript behavior with an E2E test that runs in a real browser (
js: true), and explain why it needs one. - You keep JavaScript out of views and organize it as one small file per component, loaded from the application's entrypoint.
Resources
Read what's new to you, skim what's familiar, skip what you already master. Stop when you can meet the learning goals.
Your agent can also generate an overview, a tutorial or an explanation for anything here, tailored to what you already know. Just ask.
Pick one introduction
- 📄 MDN: DOM scripting introduction Show archive.org snapshot — selecting, creating, changing and removing elements; live editors
- 📘 javascript.info: Document Show archive.org snapshot , Searching: getElement*, querySelector* Show archive.org snapshot and Introduction to browser events Show archive.org snapshot — the book version, with tasks
- 📄 The Odin Project: DOM manipulation and events Show archive.org snapshot — compact text with exercises
- ▶️ JavaScript DOM Crash Course Show archive.org snapshot — Traversy Media, 4 parts; from 2017 but plain DOM without jQuery, still accurate
For specific goals
- 📄 MDN:
Introduction to events
Show archive.org snapshot
—
addEventListener, the event object, removing listeners - 📄 HTML attributes vs. DOM properties Show archive.org snapshot — the difference the card's fourth goal is about
- 📄 MDN:
DOMContentLoadedevent Show archive.org snapshot — why a script in<head>may run too early - 📄 javascript.info:
Scripts: async, defer
Show archive.org snapshot
— when a script runs relative to parsing, and what
deferchanges - 📄 MDN:
Element.classListShow archive.org snapshot and the DOM reference Show archive.org snapshot — look things up here instead of a cheat sheet
Attributes vs. properties
An HTML attribute is part of the markup the server sent, written here as <element attribute>. A DOM property is a field on the JavaScript object the browser built from it, written as element.property. They are easy to confuse, because many attributes are mirrored as properties:
-
Only some are mirrored.
<element id>becomeselement.id,<a href>becomesa.href,<element class>becomeselement.className(andelement.classList), and<element data-movie-id>becomeselement.dataset.movieId. An attribute you invent yourself, like<element foo>, gets no property. -
Default vs. live.
<input value="Foo">holds the initial value,input.valuethe current one. After the user types into the field,input.getAttribute('value')is still"Foo", whileinput.valueis what they typed. The same goes for<input checked>andinput.checked. - Only strings in attributes. Attributes are always strings. Numbers, arrays, objects or functions can only live in properties.
Rule of thumb: use attributes to configure an element (they are in the HTML the server renders, e.g. <element data-…>), and properties to read or change its current state.
Exercises
Playing with the DOM
- Visit https://makandracards.com/makandra
- Right-click one of the cards in the list and choose Inspect. Look at the surrounding markup and find a CSS selector that matches every card element (and nothing else).
- Now open the Console tab and, using that selector with the DOM API:
- locate all card elements,
- read the text content of the second card,
- remove the first card.
- Reload the page. What happened to your change, and why?
Movie counter
In your MovieDB, in the movies list, add a button that says "Count movies".
Once the user clicks on that button, a piece of JavaScript will count the movies in the list above and replace the button text with the number of movies counted.
Tip
We want to avoid mixing HTML, JavaScript and CSS within the same file. Therefore, don't embed your JavaScript into your view. Instead, put your JavaScript code into a
.jsfile that is loaded by your layout.When your script runs decides whether the DOM exists yet. The browser builds the DOM while it parses the HTML, top to bottom:
- A plain
<script>runs as soon as the parser reaches it. In the<head>, the<body>hasn't been parsed yet, so your elements don't exist.- A script with
deferloads in parallel and runs once the whole document is parsed, just before theDOMContentLoadedShow archive.org snapshot event. Scripts withtype="module"behave the same by default. Your MovieDB layout loadsapplication.jswithdefer: true.- Code inside a
DOMContentLoadedlistener runs after parsing, no matter how the script was loaded. Withdeferyou don't need it, but it does no harm and keeps your code independent of the script tag.
Testing the counter
Write an E2E test for the counter: a feature spec that visits the movies list, clicks the button and expects the count.
Your feature specs so far ran without JavaScript. By default, Capybara uses a fast driver that only reads the HTML and never runs a script. To run a scenario in a real browser that executes your JavaScript, tag it with js: true. spec/features/branding_spec.rb in your MovieDB has two otherwise identical scenarios, one with and one without it (it uses the short form :js). Browser automation with Capybara and Selenium WebDriver covers this in depth later.
Note
You may want to write a unit test for the counter instead: it's a small component, so why boot a whole browser? That instinct is right. But unit-testing JavaScript needs more tooling, which you'll set up in Testing JavaScript with Jasmine. Until then, an E2E test is the only way to test frontend behavior: slower and heavier, but it covers the happy path.
Highlighting rows
In your MovieDB, write a CSS class that highlights a row in a list of records, e.g. in the index action of the movies resource.
Write some JavaScript that automatically applies that CSS class when the user's mouse hovers over a row. When the mouse pointer exits the element box, the CSS class is removed automaticallly.
Note
In a real application we would achieve this effect with a simple CSS rule.
Organize your components
Your application will eventually have a lot of JavaScripts. Putting them all into a single, giant JavaScript file is not sustainable. Instead, you should have a single short .js file for each component or concern.
For instance, it could look like this:
app/
assets/
application.js
application/
components/
count_items.js
highlight_rows.js
The component files are loaded from application.js.
Tip
Entrypoints are JavaScript files that can be loaded individually from your application layout. By default your Rails application has a single entrypoint
application.js, which shouldimportall other files.A case for multiple entrypoints would be an application that has two areas with a distinct look and feel. For instance there is (1) a visually ambitious frontend for consumers and (2) an admin area that has tightly packed data grids. Both areas would not share any CSS or JavaScript, so we would have two separate entrypoints.