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:
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.