Form that changes on user input without losing focus

Imagine you have a form that should change while the user edits it – such as an address form with country, state and city select fields where selecting one option should modify the available options in dependent select fields. Or with a conditionally enabled submit button, a live preview of some kind of results, or a combination of all of that.

There are many different ways to achieve this, but they often require logic in the frontend that is specific to the individual form. I am looking for an approach that uses server-side rendering, with all the benefits that come with it.

I came up with the following which works ok for me so far, and I’m interested to hear if others are doing something similar and if there are other or better approaches.

Possible solution:

The form is wrapped in a turbo frame and a stimulus controller. Each input field change triggers an action in the stimulus controller which POSTs the whole form plus an extra parameter. When this extra parameter is present, the receiving Rails controller action #create/#update builds the model, but does not try to validate and save but instead renders new/edit again. This way, the form can be rendered based on logic in the model with its attributes assigned from the input values of the latest user edit.

The rendered response includes the turbo frame with the new form content and therefore will replace the previous form content. At that point, the missing piece is to maintain focus on the currently focused input field. The stimulus controller achieves this by setting the data-turbo-permanent attribute on the element with the focus, to exempt it from the content swap, as suggested here:
Morphing overwrites the user's input on active element · Issue #1199 · hotwired/turbo · GitHub.

How it looks in an (incomplete) demo app:
example-form

Form: example-form/app/views/addresses/_form.html.erb at 2c081a1ef36bde48f62b58da9a7436b8b79bb4c8 · til/example-form · GitHub

Rails controller: example-form/app/controllers/addresses_controller.rb at 2c081a1ef36bde48f62b58da9a7436b8b79bb4c8 · til/example-form · GitHub

Stimulus controller: example-form/app/javascript/controllers/refresh_form_controller.js at 2c081a1ef36bde48f62b58da9a7436b8b79bb4c8 · til/example-form · GitHub

Drawbacks:

data-turbo-permanent only works as intended if it set on the element in both the old and the new HTML. However in this case the server does not know about the focus state and therefore can not set data-turbo-permanent when rendering the new HTML, so there’s a hack in the stimulus controller: in turbo:before-frame-render it copies the attribute to the new content (see the #copyPermanence function).

An id is required on each input or on one of its parents which needs to be considered when building the form.

User experience depends on fast server responses, it’s not as reliable as a frontend-based approach.

This sounds reasonable and is similar to what I use for a search form that submits GET requests when the input changes. I also have the data permanent set so the the input isn’t cleared between responses. I don’t follow what you mean by needing a hack? Wouldn’t your inputs always have permanent attribute set on it? Why do you need to differentiate

Always setting data-turbo-permanent on the same field works when that field itself never changes, such as the input in a search form. It does not work when the field itself sometimes changes – e.g. the state field in the example form with country+state+city: when selecting a country, the state field changes to contain only states of that country. When selecting a state, the state field itself should not change and not lose focus, that’s why I found the hack to be necessary.