import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "results"]
static outlets = ["results-list"]
static values = {
url: String
}
connect() {
this.timeout = null
}
async search() {
clearTimeout(this.timeout)
const query = this.inputTarget.value.trim()
if (query.length < 2) {
this.clearResults()
return
}
this.timeout = setTimeout(async () => {
this.showLoading()
const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(query)}`, {
headers: { "Accept": "text/vnd.turbo-stream.html" }
})
if (response.ok) {
const html = await response.text()
// Communicate with the results list controller
if (this.hasResultsListOutlet) {
this.resultsListOutlet.update(html)
}
}
}, 300)
}
showLoading() {
if (this.hasResultsListOutlet) {
this.resultsListOutlet.showLoading()
}
}
clearResults() {
if (this.hasResultsListOutlet) {
this.resultsListOutlet.clear()
}
}
}
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["container"]
update(html) {
this.containerTarget.innerHTML = html
this.element.classList.remove('hidden')
}
showLoading() {
this.containerTarget.innerHTML = '<div class="spinner"></div><p>Searching...</p>'
this.element.classList.remove('hidden')
}
clear() {
this.containerTarget.innerHTML = ''
this.element.classList.add('hidden')
}
}
<div data-controller="search" data-search-url-value="<%= search_posts_path %>" data-search-results-list-outlet=".results-list">
<div class="relative">
<input
type="search"
placeholder="Search posts..."
class="form-input"
data-search-target="input"
data-action="input->search#search" />
</div>
<div class="results-list hidden mt-2" data-controller="results-list">
<div class="bg-white rounded-lg shadow-lg p-4">
<div data-results-list-target="container"></div>
</div>
</div>
</div>
Outlets allow Stimulus controllers to reference and communicate with other controller instances, enabling composition without tight coupling. I define outlets by specifying which controller types to connect to, and Stimulus automatically finds matching controllers in the DOM. This pattern works well for coordinating behavior across components: a form controller might communicate with a modal controller, or a search controller with a results controller. Outlets provide typed references and callbacks when outlets connect or disconnect, making it easy to sync state. This is more maintainable than using custom events for every interaction, though events still have their place for loosely coupled scenarios.