Skip to content

Conversation

@stefannibrasil
Copy link
Contributor

@stefannibrasil stefannibrasil commented Sep 17, 2024

This commit proposes the following:

  • adds a missing colon to a partial
  • adds path files as comments to code snippets
  • adds check in for ending lesson 2

seanpdoyle and others added 5 commits June 10, 2024 22:40
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.

![Demo of keyboard powered shortcut](./assets/lesson-1/demo.gif)

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
stefannibrasil added a commit that referenced this pull request Oct 1, 2024
to facilitate debugging

extracted from #24
@stefannibrasil
Copy link
Contributor Author

@stevepolitodesign I have simplified this PR. CI fix is in the first PR 👍🏼

@stevepolitodesign stevepolitodesign force-pushed the main branch 2 times, most recently from 018a261 to caa2c5b Compare October 22, 2024 20:43
stevepolitodesign added a commit that referenced this pull request Oct 25, 2024
Closes #24

Fixes typo and adds file path to non `diff` code blocks.

Co-authored-by: Stefanni Brasil <[email protected]>
stevepolitodesign added a commit that referenced this pull request Oct 25, 2024
Closes #24

Fixes typo and adds file path to non `diff` code blocks.

Co-authored-by: Stefanni Brasil <[email protected]>
@stevepolitodesign
Copy link
Contributor

@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 main, and for now, we've removed Tutorial Assistant. Because of this, I don't expect people to interact with this repository as originally intended, where they need to get tests to pass before moving on to the next lesson.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants