Adding Real Features

Let's add some real features to our app!

If you’ve looked at the schedule page in your browser, you’ll see a button next to each day in the schedule labeled “Hide.” Clicking on it does nothing—yet.

Toggling the state with TypeScript

If you’ve looked at the markup for that page in app/views/schedule/show.html.erb, you’ll see an each loop for each day in the schedule. A few lines into that loop, you’ll see a button with the “Hide” text. I’ve marked up the appropriate DOM elements with some CSS classes that we’ll use in the TypeScript code:

Press + to interact
<section>
<section>
<div class="columns is-gapless">
<% @schedule.schedule_days.each do |schedule_day| %>
<div class="column has-text-centered"
data-associated="day-body:<%= schedule_day.day.by_example("2006-01-02") %>">
<%= schedule_day.day.by_example("Jan 2") %>
</div>
<% end %>
<div class="column">Show All</div>
</div>
<div class="columns is-centered">
<div class="column is-four-fifths">
<div class="control">
<%= text_field_tag("query", "",
placeholder: "Search concerts", type: "search",
id: "search_query", class: "input") %>
</div>
</div>
</div>
</section>
<br>
<section>
<% @schedule.schedule_days.each do |schedule_day| %>
<section class="js--day day-body"
id="day-body:<%= schedule_day.day.by_example("2006-01-02") %>">
<h2 class="title is-3">
<%= schedule_day.day.by_example("Monday, January 2, 2006") %>
<div class="button is-primary js--day-button">
<span class="js--button-text">Hide</span>
</div>
</h2>
<section class="js--day-text">
<% schedule_day.concerts.sort_by(&:start_time).each do |concert| %>
<article class="concert">
<div class="columns">
<div class="column is-size-4 is-one-fifth">
<%= concert.start_time.by_example("3:04 PM") %>
</div>
<div class="column is-three-fifths">
<div class="name">
<div class="is-size-5 has-text-weight-bold">
<%= link_to(concert.name, concert) %>
</div>
</div>
<div class="bands">
<%= concert.bands.map(&:name).join(", ") %>
</div>
<div class="genres">
<%= concert.genre_tags.split(",").to_sentence %>
</div>
<div class="has-text-grey has-text-weight-bold">
<%= concert.venue.name %>
</div>
</div>
<div class="column is-size-4 is-one-fifth">On Sale</div>
</div>
</article>
<% end %>
</section>
</section>
<br>
<% end %>
</section>
</section>

The code sample is in the /usercode directory (run the executable at the bottom of the page to find it), and if you compare it with the previous set of code in the last few lessons, you’ll see some changes that I’m not going to cover here in-depth. Specifically, I’ve added the ESLint code cleanup tool and the Prettier code formatter to help keep the code clean as I write it.

I’ve changed the name of the file app/javascripts/packs/application.js, to app/javascripts/packs/application.ts changing the file extension so that Webpacker will treat it as a TypeScript file.

The existing code in that file needs to be slightly modified for TypeScript. Specifically, the existing code uses require to load modules, and I’ve configured TypeScript not to allow that, so I’ve changed them all to use import. I’ll go into more detail on module importing in the TypeScript Chapter.

I’ve also written all the code we need for this show/hide toggle inside the application.ts file, which is not great practice as the code gets bigger, but is fine for now because it means we don’t need to deal with further module importing.

Here’s the code I’ve written to do this toggle, using no framework, just TypeScript and the standard Document Object Model (DOM) library:

Press + to interact
import * as ActiveStorage from "@rails/activestorage"
import Rails from "@rails/ujs"
import * as Channels from "channels"
import Turbolinks from "turbolinks"
Rails.start()
Turbolinks.start()
ActiveStorage.start()
class DayToggle {
day: HTMLElement
dayButton: HTMLElement
dayText: HTMLElement
buttonText: HTMLElement
constructor(day: HTMLElement) {
this.day = day
this.dayButton = this.day.querySelector(".js--day-button")
this.dayText = this.day.querySelector(".js--day-text")
this.buttonText = this.day.querySelector(".js--button-text")
}
initHandlers(): void {
this.onFilterButtonClick()
}
isHidden(): boolean {
return this.dayText.classList.contains("is-hidden")
}
onFilterButtonClick(): void {
this.dayButton.addEventListener("click", () => {
this.dayText.classList.toggle("is-hidden")
this.setText()
})
}
setText(): void {
this.buttonText.innerText = this.isHidden() ? "Show" : "Hide"
}
//
}
document.addEventListener("turbolinks:load", () => {
document.querySelectorAll(".js--day").forEach((element) => {
new DayToggle(element as HTMLElement).initHandlers()
})
})

Let’s go through this in some detail.

At the beginning of the ...