# Terminal
bin/rails g stimulus hotkey
bin/rails g stimulus filter
# app/javascript/controllers/hotkey_controller.js
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="hotkey"
export default class extends Controller {
static targets = ["field"]
static values = { key: { type: String, default: "/" } }
connect() {
this.onKeydown = this.onKeydown.bind(this)
document.addEventListener("keydown", this.onKeydown)
}
disconnect() {
document.removeEventlistener("keydown", this.onKeydown)
}
onKeydown(event) {
if (event.key !== this.keyValue) return
const tag = document.activeElement?.tagName
if (tag === "INPUT" || tag === "TEXTAREA") return
event.preventDefault()
this.fieldTarget.focus()
this.fieldTarget.select()
}
}
# app/javascript/controllers/filter_controller.js
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="filter"
export default class extends Controller {
static targets = ["form", "input", "clear"]
static values = { debounce: { type: Number, default: 200 } }
connect() {
this.timeout = null
this.refreshClearState()
}
disconnect() {
clearTimeout(this.timeout)
}
submit() {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.formTarget.requestSubmit()
this.refreshClearState()
}, this.debounceValue)
}
clear() {
this.inputTarget.value = ""
this.inputTarget.focus()
this.formTarget.requestSubmit()
this.refreshClearState()
}
refreshClearState() {
if (!this.hasClearTarget) return
this.clearTarget.disabled = this.inputTarget.value.trim() === ""
}
}
# app/views/episodes/index.html.erb
<% content_for :title, "Episodes" %>
<header class="mb-6">
<h1 class="font-bold text-3xl text-slate-900">Episodes</h1>
</header>
<div data-controller="filter hotkey">
<%= form_with url: episodes_path, method: :get,
html: { role: "search", class: "flex gap-2 mb-6" },
data: { turbo_frame: "episodes_list", filter_target: "form" } do |f| %>
<%= f.search_field :q,
value: @query,
placeholder: "Search title, description, guest, or tag… (press / to focus)",
autocomplete: "off",
data: { hotkey_target: "field", filter_target: "input", action: "input->filter#submit" },
class: "flex-1 rounded border border-slate-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" %>
<button type="button"
data-filter-target="clear"
data-action="filter#clear"
class="rounded border border-slate-300 bg-white px-3 py-2 text-slate-700 hover:bg-slate-100 disabled:opacity-40"
<%= "disabled" if @query.blank? %>>
Clear
</button>
<% end %>
<%= turbo_frame_tag "episodes_list" do %>
<%= render "list", episodes: @episodes, query: @query %>
<% end %>
</div>