Midwest::Autocomplete
Overview
Midwest::Autocomplete is a controller concern that exposes a JSON endpoint for powering async autocomplete fields. Include it in any controller, call the autocomplete macro to configure which model and attributes to search, add one route, and the component handles the rest.
Setup
1. Include the concern
class UsersController < ApplicationController include Midwest::Autocomplete autocomplete :user, key: :name, # column searched with ILIKE value: :id, # submitted as the hidden field value support: :email # optional — shown as secondary text in dropdownend2. Add the route
Register the provided routing concern once, then apply it to any resource:
concern :midwest_autocomplete, Midwest::Routing::AUTOCOMPLETEresources :users, concerns: :midwest_autocompleteresources :tags, concerns: :midwest_autocomplete3. Wire up the component
Pass the route to url: on the autocomplete field:
<%= midwest_form_autocomplete( name: :user_id, label: "Assigned to", url: midwest_autocomplete_users_path) %>How it works
The component sends GET /users/midwest_autocomplete?q=<query> on each keystroke. An empty or absent q returns all records. Results are capped at 20.
The endpoint returns a JSON array:
[ { "value": "42", "label": "Ada Lovelace", "support": "ada@example.com" }, { "value": "17", "label": "Alan Turing", "support": "alan@example.com" }]autocomplete options
| Option | Required | Description |
|---|---|---|
key: |
yes | Attribute searched with a case-insensitive ILIKE query |
value: |
yes | Attribute returned as the option's form value |
support: |
no | Attribute shown as secondary text in the dropdown |
theme: |
no | Badge color theme passed through in the JSON response |
Turbo Frame mode
Midwest::AutocompleteFrame is an alternative to Midwest::Autocomplete that returns server-rendered HTML inside a <turbo-frame> instead of JSON. This gives you full control over each option's markup — use icons, avatars, badges, or any other component.
When to use turbo frames vs JSON
JSON (url:) |
Turbo frame (turbo_frame_url:) |
|
|---|---|---|
| Rendering | JS builds <li> elements client-side |
Server renders HTML, turbo replaces the frame |
| Custom markup | Limited (label + support text only) | Full Rails ERB — any component, any content |
| Dropdown-item style | No | Yes — wrap with midwest_autocomplete_option |
1. Include the concern
class UsersController < ApplicationController include Midwest::AutocompleteFrame autocomplete_frame :user, key: :name, value: :id, support: :emailend2. Add the route
concern :midwest_autocomplete_frame, Midwest::Routing::AUTOCOMPLETE_FRAMEresources :users, concerns: :midwest_autocomplete_frame3. Wire up the component
<%= midwest_form_autocomplete( name: :user_id, label: "Assigned to", turbo_frame_url: midwest_autocomplete_frame_users_path) %>Custom option markup
The midwest_autocomplete_option helper renders a correctly-wired <li> for use inside any turbo-frame response. Pass a block for fully custom content.
<%= midwest_autocomplete_option(value: user.id, label: user.name) %><%= midwest_autocomplete_option(value: user.id, label: user.name, support: user.email) %><%= midwest_autocomplete_option(value: user.id, label: user.name) do %> <%= midwest_avatar(name: user.name, size: :sm) %> <span class="midwest-autocomplete-option-label"><%= user.name %></span><% end %>Rolling your own turbo-frame action
If the default midwest_autocomplete_frame action doesn't fit your query logic, skip the concern and write the action yourself. Wrap the response in turbo_frame_tag and use midwest_autocomplete_option for each item:
def my_autocomplete @users = User.search(params[:q]).limit(20)end<%= turbo_frame_tag request.headers['Turbo-Frame'] do %> <ul class="midwest-autocomplete-list" role="listbox"> <% @users.each do |user| %> <%= midwest_autocomplete_option(value: user.id, label: user.name) do %> <%= midwest_avatar(name: user.name, size: :sm) %> <span class="midwest-autocomplete-option-label"><%= user.name %></span> <span class="midwest-autocomplete-option-support"><%= user.email %></span> <% end %> <% end %> </ul><% end %>