Til, thank you very much for the link. I’ve tried to use your stimulus controller for my form but data-turbo-permanent inputs are still being recreated. So I suppose something’s wrong with my form, inputs or with setup.
jch, I’ve checked, and it looks like ids are unique.
Autosubmit controller submits form on user input (with small timeout to gather multiple quick keystrokes), submitted form updates form’s turbo-frame with the same partial as show action.
Here’s the partial with form (actually its two of them) updating dynamic form:
/ user, brewery, percentage, [focus]
- focus ||= nil
- percentage = 60 if !defined?(percentage) || percentage.nil?
.flex-1.flex.flex-col.gap-y-6
.w-full.h-32.bg-white.rounded-lg.p-4.grid.place-content-between.gap-3 class="grid-cols-[1fr_10fr_222px]"
.flex.text-lg.font-semibold.items-center About the Brewery
.min-w-full
.text-sm.p-0.button-ab-gray.h-fit Restore Data
.flex.gap-x-3.items-center
.text-ab-yellow Filled
.text-ab-yellow.bg-ab-lightyellow.text-sm.py-1.px-2.rounded
= "#{percentage}%"
.min-w-full.min-w-0.flex.items-center
.flex-1.min-w-full.overflow-hidden.rounded-full.h-2.bg-ab-lightyellow.relative
.absolute.top-0.left-0.bottom-0.rounded-full.h-2.bg-ab-yellow style="width: #{percentage}%"
.text-sm.p-0.button-ab-yellow.h-fit Submit for Moderation
= form_with id: dom_id(brewery, :form), model: brewery, url: account_brewery_profile_path, method: :patch, class: :contents,
data: { controller: "autosubmit-form" } do |f|
.w-full.bg-white.rounded-lg.p-4.flex.flex-col.gap-y-10
.font-semibold Basic Information
.grid.grid-cols-2.text-sm.gap-x-10
.flex.flex-col.gap-y-4
.flex.flex-col.gap-y-1
= f.label :name, "Name"
= f.text_field :name, class: "border rounded p-1.5",
placeholder: "For example, Brewery No. 1"
.flex.flex-col.gap-y-1
= f.label :town, "City"
.relative.w-full id="town_input_permanent" data-controller="autocomplete" data-turbo-permanent="true"
= f.text_field :town, class: "w-full border rounded p-1.5 peer",
placeholder: "Enter or select",
data: { controller: "input-change-get",
input_change_get_url_value: account_brewery_town_suggestions_path,
autocomplete_target: :input }
.absolute id=(dom_id(brewery, :town_suggestions))
.flex.flex-col.gap-y-1
= f.label :description, "Description"
= f.text_area :description, rows: 7, class: "border rounded p-1.5",
placeholder: "Tell us about your brewery. A good description will help promote it."
.flex.flex-col.gap-y-4
.flex.flex-col.gap-y-1
= f.label :logo, "Logo"
= f.label :logo
- if brewery.logo.attached?
= render partial: "account/editable_image_attachment",
locals: { attachment: brewery.logo, padding: true, form: f }
- else
.bg-ab-lightgray.rounded.flex.flex-col.justify-center.items-center.text-xs.cursor-pointer.p-10.gap-y-2 class="h-[120px]"
= image_tag "account/add-logo.svg", class: "w-6"
.text-xs.font-semibold.text-center Add Logo
= f.file_field :logo, multiple: false, class: :hidden
.flex-1.flex.flex-col.gap-y-1
/ goes ahead of everything to make label clicks work
= f.file_field :photos, multiple: true, class: :hidden
= f.label :photos, "Photos"
- if brewery.photos.attached?
.flex.flex-wrap.overflow-y-auto.gap-4.justify-start.items-start
= render collection: brewery.photos, partial: "account/editable_image_attachment",
as: :attachment, locals: { multiple: :photos, padding: false, form: f }
= f.label :photos, class: "border rounded bg-ab-lightgray flex flex-col justify-center items-center text-xs cursor-pointer p-10 gap-y-2 w-[100px] h-[100px]"
= image_tag "account/add-photo.svg", class: "w-6"
.text-xs.font-semibold.text-center Add Images
- else
= f.label :photos, class: "flex flex-wrap overflow-y-auto gap-4 justify-start items-start"
.w-full.h-full.flex-1.bg-ab-lightgray.rounded.flex.flex-col.justify-center.items-center.text-xs.cursor-pointer.p-10.gap-y-2
= image_tag "account/add-photo.svg", class: "w-6"
.text-xs.font-semibold.text-center Add Images
= form_with id: dom_id(brewery.payment_details, :form_payment_details), model: brewery.payment_details,
url: account_brewery_payment_detailses_path, method: :patch, class: :contents,
data: { controller: "autosubmit-form" } do |f|
.w-full.bg-white.rounded-lg.p-4.flex.flex-col.gap-y-4
.font-semibold Payment Details
.grid.grid-cols-3.text-sm.gap-x-10
.flex.flex-col.gap-y-4
.flex.flex-col.gap-y-2
.text-md.font-semibold Legal Information
.relative.w-full.flex.flex-col id="inn_permanent" data-controller="autocomplete"
= f.label :inn, "TIN", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :inn, class: "w-full border rounded p-1.5 peer",
placeholder: "2934879129",
data: { controller: "input-change-get",
input_change_get_url_value: account_brewery_inn_suggestions_path,
autocomplete_target: :input }
.absolute id=dom_id(brewery.payment_details, :inn_suggestions)
.flex.flex-col
= f.label :legal_name, "Legal Name", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :legal_name, class: "border rounded p-1.5",
placeholder: "OOO BREWERY NUMBER ONE"
.flex.flex-col
= f.label :kpp, "KPP", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :kpp, class: "border rounded p-1.5",
placeholder: "0293901212"
.flex.flex-col
= f.label :ogrn, "OGRN", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :ogrn, class: "border rounded p-1.5",
placeholder: "10965738293901"
.flex.flex-col
= f.label :legal_address, "Legal Address", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :legal_address, class: "border rounded p-1.5",
placeholder: "191002, St. Petersburg, Marata St., 35"
.flex.flex-col
= f.label :address, "Actual Address", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :address, class: "border rounded p-1.5",
placeholder: "191002, St. Petersburg, Marata St., 35"
.flex.flex-col.gap-y-2
.text-md.font-semibold Additional Information
.flex.flex-col
= f.label :okved, "OKVED", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :okved, class: "border rounded p-1.5 peer",
placeholder: "2934879129"
.flex.flex-col
= f.label :egais_id, "EGAIS ID", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :egais_id, class: "border rounded p-1.5",
placeholder: "34901234098"
.flex.flex-col.gap-y-4
.flex.flex-col.gap-y-2
.text-md.font-semibold Bank Account
.flex.flex-col
= f.label :current_account, "Current Account, No.", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :current_account, class: "border rounded p-1.5 peer",
placeholder: "1234214214"
.relative.w-full.flex.flex-col id="bik_permanent" data-controller="autocomplete" data-turbo-permanent="true"
= f.label :bik, "BIC", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :bik, class: "border rounded p-1.5 peer",
placeholder: "1234214214",
data: { controller: "input-change-get",
input_change_get_url_value: account_brewery_bik_suggestions_path,
autocomplete_target: :input }
.absolute id=dom_id(brewery.payment_details, :bik_suggestions)
.flex.flex-col
= f.label :bank_name, "Bank Name", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :bank_name, class: "border rounded p-1.5 peer",
placeholder: "OOO SBER"
.flex.flex-col
= f.label :correspondent_account, "Correspondent Account", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :correspondent_account, class: "border rounded p-1.5 peer",
placeholder: "1234214214"
.flex.flex-col.gap-y-2
.text-md.font-semibold Declaration
= f.label :declaration
- if brewery.payment_details.declaration.attached?
= render partial: "account/editable_image_attachment",
locals: { attachment: brewery.payment_details.declaration, padding: true, form: f }
- else
.flex-1.h-full.bg-ab-lightgray.rounded.flex.flex-col.justify-center.items-center.text-xs.cursor-pointer.p-10.gap-y-2
= image_tag "account/add-logo.svg", class: "w-6"
.text-xs.font-semibold.text-center Add Photo/Scan of Declaration
= f.file_field :declaration, multiple: false, class: :hidden
.flex.flex-col.gap-y-4
.flex.flex-col.gap-y-2
.text-md.font-semibold Manager
.flex.flex-col
= f.label :manager_full_name, "Full Name", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :manager_full_name, class: "border rounded p-1.5 peer",
placeholder: "Volkov Igor Vitalievich"
.flex.flex-col
= f.label :manager_position, "Position", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :manager_position, class: "border rounded p-1.5 peer",
placeholder: "Director"
.flex.flex-col.gap-y-2
.text-md.font-semibold Contact Information
.flex.flex-col
= f.label :manager_email_address, "Email", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :manager_email_address, class: "border rounded p-1.5 peer",
placeholder: "volkov@somemail.ru"
.flex.flex-col
= f.label :manager_phone_number, "Phone", class: "w-fit bg-white text-xs text-ab-darkgray ml-1 px-1 -mb-2 z-10 font-light"
= f.text_field :manager_phone_number, class: "border rounded p-1.5 peer",
placeholder: "+7 (___) ___ __ __"
.flex.flex-col.gap-y-2
.flex.flex-col
.text-md.mr-1.font-semibold Document
.text-xs.text-ab-darkgray on the basis of which the manager acts
= f.label :manager_legal_document
- if brewery.payment_details.manager_legal_document.attached?
= render partial: "account/editable_image_attachment",
locals: { attachment: brewery.payment_details.manager_legal_document, padding: true, form: f }
- else
.flex-1.h-full.bg-ab-lightgray.rounded.flex.flex-col.justify-center.items-center.text-xs.cursor-pointer.p-10.gap-y-2
= image_tag "account/add-logo.svg", class: "w-6"
.text-xs.font-semibold.text-center Add Photo/Scan of Document
= f.file_field :manager_legal_document, multiple: false, class: :hidden
And here’s stimulus controller:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
this.changed = false
this.timer = null
this.element.addEventListener('change', () => {
if (this.changed) this.onChanged()
})
this.element.addEventListener('input', (event) => {
this.changed = true
clearTimeout(this.timer)
this.timer = setTimeout(() => {
if (this.changed) this.onChanged()
}, 200)
})
this.element.addEventListener("focusin", this.#setPermanent)
this.element.addEventListener("focusout", this.#unsetPermanent)
this.element.addEventListener("turbo:before-frame-render", this.#copyPermanence)
}
disconnect() {
if (this.changed)
this.onChanged()
this.element.removeEventListener("focusin", this.#setPermanent)
this.element.removeEventListener("focusout", this.#unsetPermanent)
this.element.removeEventListener("turbo:before-frame-render", this.#copyPermanence)
}
onChanged() {
this.changed = false
this.submitWithFocusParam()
}
submitWithFocusParam() {
const form = this.element
form.requestSubmit();
}
#setPermanent(e) {
const el = e.target.closest(".input")
if (el) el.setAttribute("data-turbo-permanent", true)
}
#unsetPermanent(e) {
const el = e.target.closest(".input")
if (el) el.removeAttribute("data-turbo-permanent")
}
#copyPermanence(e) {
let needResubmit = false
for(const currentPermanentElement of e.target.querySelectorAll("[id][data-turbo-permanent]")) {
const newPermanentElement = e.detail.newFrame.querySelector(`#${currentPermanentElement.id}`)
if (newPermanentElement) {
newPermanentElement.dataset.turboPermanent = true
needResubmit ||= newPermanentElement.value != currentPermanentElement.value
newPermanentElement.value = currentPermanentElement.value
newPermanentElement.selectionStart = currentPermanentElement.selectionStart
console.log("set permanent, value:", newPermanentElement.value, "caret:", newPermanentElement.selectionStart)
}
}
if (needResubmit) {
this.changed = false
this.submitWithFocusParam()
}
}
}