-
Notifications
You must be signed in to change notification settings - Fork 8
Lesson 2: small updates + issue with the audio buttons #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
We'll ease into the concept of Hotwire by progressively enhancing the
application's Search page navigation with a <kbd><kbd>⌘</kbd>
<kbd>k</kbd></kbd>-powered keyboard shortcut.

First, we re-introduce the `layouts/application.html.erb` template's
call to [javascript_importmap_tags][] to load our application's
[importmap]-backed JavaScript bundle.
```diff
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
- <%# javascript_importmap_tags %>
+ <%= javascript_importmap_tags %>
</head>
```
Then, we add [@github/hotkey][] to `config/importmap.rb`. Since we're using
[Importmap for Rails][] we can run `./bin/importmap pin @github/hotkey`. This
will resolve the package using [JSPM.org][].
```diff
--- a/config/importmap.rb
+++ b/config/importmap.rb
@@ -7,3 +7,4 @@ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js",
preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
pin "trix"
pin "@rails/actiontext", to: "actiontext.js"
+pin "@github/hotkey", to: "https://ga.jspm.io/npm:@github/[email protected]/dist/index.js"
```
Next, we'll attach a new `hotkey` controller to the document's `<body>` element.
```diff
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
- <body>
+ <body data-controller="hotkey">
<%= yield %>
</body>
</html>
```
We attach the controller to the `body` in an effort to create a large enough
[scope][]. By scoping this controller to the body, we can add the [targets][]
for the controller to any element in the document.
Now we can build the controller.
```js
// app/javascript/controllers/hotkey_controller.js
import ApplicationController from "controllers/application_controller"
import { install, uninstall } from "@github/hotkey"
export default class extends ApplicationController {
static targets = ["shortcut"]
shortcutTargetConnected(target) {
target.setAttribute("aria-keyshortcuts", target.getAttribute("data-hotkey"))
install(target)
}
shortcutTargetDisconnected(target) {
target.removeAttribute("aria-keyshortcuts")
uninstall(target)
}
}
```
We use [lifecycle callbacks][] to create two methods based on the name of the
[targets][]. The `shortcutTargetConnected` method will be invoked once the
target is connected to the DOM, while the `shortcutTargetConnected` method will
be invoked once the target is disconnected from the DOM.
Anytime an element with a `data-hotkey-target="shortcut"` data attribute appears
on the page, this controller will enable that element to be accessed via the
hotkey. Additionally, we add a `aria-keyshortcuts` attribute to the element and
set the value to whatever the `hotkey` attribute is. In this case, that's
"Meta+k". We do this in an effort to exposes the existence of the shortcut to
assistive technologies so the presence of the shortcut can be communicated to
its users.
In the style of its Rails-side counterparts, the `hotkey` controller imports and
extends a base level `ApplicationController`. The `ApplicationController` will
start as an empty class, but provides an opportunity to define shared actions,
logic, or utilities.
```js
// app/javascript/controllers/application_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
}
```
Finally, we'll mark the Search navigation with the
`[data-hotkey-target="shortcut"]` attribute, along with the
`[data-hotkey="Meta+k"]` to attach a keyboard listener.
```diff
--- a/app/views/episodes/podcast/_frame.html.erb
+++ b/app/views/episodes/podcast/_frame.html.erb
<section class="mt-10 lg:mt-12">
<div class="border border-gray-300 bg-white rounded-md shadow-sm text-slate-500 focus-within:ring">
- <%= link_to podcast_search_results_path(podcast), class: "outline-none" do %>
+ <%= link_to podcast_search_results_path(podcast), class: "outline-none",
+ data: {hotkey_target: "shortcut", hotkey: "Meta+k"} do %>
<div class="flex items-center gap-5 px-3 text-start">
<%= inline_svg_tag "icons/search.svg", class: "h-2.5 w-2.5" %>
<div class="flex-1 py-1">
<span class="font-mono text-sm leading-7">Search</span>
</div>
+
+ <kbd class="inline-flex items-center gap-1 font-mono text-sm leading-7">
+ <abbr title="Command" class="no-underline text-slate-300">⌘</abbr> K
+ </kbd>
</div>
<% end %>
</div>
```
You'll note that we add `[data-hotkey-target="shortcut"]` to an existing
anchor link. The [@github/hotkey][] library works by triggering a focus event on
form fields, or a click event on other elements. In this case, hitting `Meta+k`
will automatically click the link to the search page.
[javascript_importmap_tags]: https://github.com/rails/importmap-rails#preloading-pinned-modules
[importmap]: https://github.com/WICG/import-maps
[@github/hotkey]: https://github.com/github/hotkey
[Importmap for Rails]: https://github.com/rails/importmap-rails
[JSPM.org]: https://jspm.org
[scope]: https://stimulus.hotwired.dev/reference/controllers#scopes
[lifecycle callbacks]: https://stimulus.hotwired.dev/reference/lifecycle-callbacks#methods
[targets]: https://stimulus.hotwired.dev/reference/targets
Co-authored-by: Steve Polito <[email protected]>
First, ensure that the `[id="player"]` element is rendered with [data-turbo-permanent][] so that the same element instance is preserved as Turbo Drive navigates the rest of the application. Next, change the `[id="audio"]` element nested within the `[id="player"]` from a `<div>` into a [`<turbo-frame>` element][turbo-frame]. We'll drive that `<turbo-frame>` with some of our application's `<form>` elements, namely the ones that submit requests to the `GET /podcasts/:podcast_id/episodes/:id` route. To do so, we'll render those `<form>` elements with the [data-turbo-frame][] attribute set to `"audio"`. To prevent multiple submissions from re-loading the frame, we'll also introduce the `application#preventDefault` action with a [custom `:reload` Action Option][custom-action-option]. To grant that controller access to the entire document, we'll render it on the page's `<html>` element. The `preventDefault` action cancels the `submit` event. When combined with the `:reload` Action Option, it will only ever be invoked when the [SubmitEvent][] that's dispatched indicates that the `<form>` submission will drive the document or a targeted `<turbo-frame>` to an already-loaded URL. If that's the case, the Action Option will prevent the `submit` event from being dispatched to `application#preventDefault`. Finally, to control an already-loaded `<turbo-frame id="audio">` element that nests an `<audio>` element, this commit also introduces a `player` controller to manage the `<audio>` element's state, and a `play-button` Stimulus controller to drive those state changes. The `player` controller listens for [loadeddata][] events dispatched by the `<audio>` element when it's rendered by the `<turbo-frame>` element, then attempts to auto-play by the `loadeddata` event to the `player#play` action. The `play-button` controller utilizes the concept of an [Outlet][] to maintain direct access to a `player` Stimulus controller. The Outlet resolves the correct `<audio>` element based on a CSS selector that targets it by an `[id]` attribute generated by a call to `dom_id($EPISODE_INSTANCE, :audio)`. The `play-button` controller synchronizes the `<button>` element's `[aria-controls]` and `[aria-pressed]` attribute state with any `player`-side state changes, and routes `click` events to its own `play-button#toggle` action. [data-turbo-permanent]: https://turbo.hotwired.dev/handbook/building#persisting-elements-across-page-loads [turbo-frame]: https://turbo.hotwired.dev/handbook/frames [data-turbo-frame]: https://turbo.hotwired.dev/handbook/frames#targeting-navigation-into-or-out-of-a-frame [custom-action-option]: https://stimulus.hotwired.dev/reference/actions#options [SubmitEvent]: https://developer.mozilla.org/en-US/docs/Web/API/SubmitEvent [Outlet]: https://stimulus.hotwired.dev/reference/outlets [loadeddata]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadeddata_event
This commit progressively enhances the episodes index by automatically scrolling to the next page of episodes by leveraging [Turbo Frames][] and [Stimulus][]. We set `[data-turbo-action]` to `replace` in order to [_replace_][] visit history while scrolling. In other words, if the user were to click the back button, the browser would not navigate to the previous page, but instead would navigate to the previously visited page. [Turbo Frames]: https://turbo.hotwired.dev/handbook/frames [Stimulus]: https://stimulus.hotwired.dev [_replace_]: https://turbo.hotwired.dev/handbook/drive#application-visits Co-authored-by: Sean Doyle <[email protected]>
This commit extends the `element` Stimulus controller with a `requestSubmit` action. We'll render the `<form>` element with `[data-controller="element"]` and `[data-action="debounced:input->element#requestSubmit"]` to route every [input][] event to the `element#requestSubmit` action, which will submit the `<form>`. The `debounced:input` portion of the `[data-action]` attribute is refers to an event that's dispatched by the [debounced][] package, and is named after the built-in [input][] event. To preserve the query and its focus state throughout the live-search, we'll render the `<input>` element with the [data-turbo-permanent][] attribute. When combined with an `[id]` attribute that's consistent across the requesting document and the response body, Turbo Drive will carry the element instance forward through the navigation, and backward through a history restoration. We'll provide a `debounced` configuration to the page by rendering an in-line `<script type="module">` element that invokes `debounced.initialize` with a `JSON` object generated from values read from a call to [`config_for(:debounced)`][config_for]. In most environments, the `<form>` will wait 100 milliseconds between `input` events. In `test`, it'll submit immediately. Since the `<form>` will be making rapid submissions and those submissions will result in a sequence of Turbo Drive visits, we'll want to render the `<form>` with [data-turbo-action="replace"][] so that we _replace_ the current History entry, instead of creating a new entry for each and every keystroke. [input]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event [data-turbo-permanent]: https://turbo.hotwired.dev/handbook/building#persisting-elements-across-page-loads [debounced]: https://github.com/hopsoft/debounced#why [config_for]: https://edgeapi.rubyonrails.org/classes/Rails/Application.html#method-i-config_for [data-turbo-action="replace"]: https://turbo.hotwired.dev/handbook/drive#application-visits
to facilitate debugging extracted from #24
0f8daa0 to
187494c
Compare
|
@stevepolitodesign I have simplified this PR. CI fix is in the first PR 👍🏼 |
018a261 to
caa2c5b
Compare
Closes #24 Fixes typo and adds file path to non `diff` code blocks. Co-authored-by: Stefanni Brasil <[email protected]>
Closes #24 Fixes typo and adds file path to non `diff` code blocks. Co-authored-by: Stefanni Brasil <[email protected]>
|
@stefannibrasil I opened #27 to simplify the commit, since for now, I want to avoid making updates to the application, and just focus on updating the prose. I only mention this because the tests are all passing on |
This commit proposes the following: