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 %>
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 %>
Share
Improve this question
edited Jan 30 at 19:53
Alex
30.1k10 gold badges66 silver badges81 bronze badges
asked Jan 29 at 19:39
WillWill
4,7443 gold badges40 silver badges70 bronze badges
2
- can you share your form. – Alex Commented Jan 30 at 8:15
- @Alex - ok added more of the code. thanks for looking – Will Commented Jan 30 at 9:23
2 Answers
Reset to default 1Can 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./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"}
}
%>
Alex's answer is better, but as a workaround this worked too. Set the focus immediately.
connect() {
this.nameTarget.focus()
this.checkFocusTime(1)
...
}
checkFocusTime(time){
setTimeout(() => {
this.focusInputUnlessActive()
}, time);
}