I have a simple checklist adding functionality, with two different checklists on the page.
Turbo adds the form to the frame, and then i'm trying to set the focus using stimulus.
The trouble is, when submitting the form by hitting return, even though logging the target shows it has found the correct input, it simply sets the focus to the first instance of the controller on the page, instead of the one that just got connected. when submitting the form using the submit button, this works fine.
How can I make it work?
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["name"]
connect() {
console.log('Checklist controller connected');
this.focusInput();
}
focusInput() {
console.log(this.nameTarget)
console.log('set focus')
this.nameTarget.focus()
}
}
The form looks like this:
<div data-controller="checklist" >
<%= simple_form_for trip.checklist_items.new , data: { checklist_target: "form" } do |f| %>
<%= f.input :checklist_type, as: 'hidden', input_html: { value: checklist_type } %>
<%= f.input :trip_id, as: 'hidden', input_html: { value: trip.id } %>
<div class="row">
<div class="col-md-9">
<%= f.input :name, label: false, placeholder: 'E.G Ski Goggles', required: true, input_html: { data: { checklist_target: "name" } } %>
</div>
<div class="col-md-3">
<%= button_tag(type: 'submit', class: 'btn btn-primary btn-full') do %>
Add Item to Checklist <i class='bi bi-arrow-90deg-left icon-rotate-90'></i>
<% end %>
</div>
</div>
<% end %>
</div>
and the turbo stream
<%= turbo_stream.append "checklist_items_frame_#{@checklist_item.checklist_type}" do %>
<%= render partial: "checklist_items/checklist_item" , locals: { checklist_item: @checklist_item, highlight: true } %>
<% end %>
<%= turbo_stream.update "new_checklist_item_#{@checklist_item.checklist_type}" do %>
<%= render "form", trip: @checklist_item.trip, checklist_type: @checklist_item.checklist_type %>
<% end %>
Can confirm that clicking a button sets focus correctly, but hitting Ender from the input focuses the first input. I don't know why it behaves differently, but there is a solution.
Rendering the same form multiple times produces duplicate id
attributes on inputs. Removing these ids or making them unique fixes the issue.
This is a simplified setup:
# app/views/home/_form.html.erb
<div data-controller="checklist">
<%= form_with url: "/" do |f| %>
<%= f.hidden_field :type, value: type %>
<%= f.text_field :name, data: {checklist_target: "name"} %>
<%= f.submit %>
<% end %>
</div>
# app/views/home/create.turbo_stream.erb
<%= turbo_stream.update "new_checklist_item_#{params[:type]}" do %>
<%= render "form", type: params[:type] %>
<% end %>
<%= turbo_stream.after "new_checklist_item_#{params[:type]}" do %>
<div><%= params[:name] %></div>
<% end %>
# app/views/home/index.html.erb
<div id="new_checklist_item_one">
<%= render "form", type: "one" %>
</div>
<div id="new_checklist_item_two">
<%= render "form", type: "two" %>
</div>
<div id="new_checklist_item_three">
<%= render "form", type: "three" %>
</div>
// app/javascript/controllers/checklist_controller.js
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="checklist"
export default class extends Controller {
static targets = ["name"]
nameTargetConnected(target) {
target.focus()
}
}
The fix:
:namespace
- A namespace for your form to ensure uniqueness of id attributes on form elements. The namespace attribute will be prefixed with underscore on the generated HTML id.
https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with
<%= form_with url: "/", namespace: type do |f| %>
same for simple_form
:
<%= simple_form_for trip.checklist_items.new, namespace: checklist_type do |f| %>
Alternatively, you can remove the id
from the input:
<%= f.text_field :name, id: nil, data: {checklist_target: "name"} %>
for simple_form
:
<%= f.input :name,
input_html: {
id: nil,
data: {checklist_target: "name"}
}
%>