"Best" way to augment forms in primary server-rendered app

OK, I’ll start with a disclaimer. The term “best” is going to be subjective, and based on varying needs, preferences, experience, etc.

The situation I’m currently faced with is an app that has a need of quite intricate & interactive forms. The form spans multiple schemas in the backend of the app. Choices and inputs in later parts of the form are dependent on earlier selections.

At present I’m managing this with a temporary structure that uses an Ecto embedded_schema and an Agent to store the current state of the structure. I’ve assigned a route for each stage in the form, and the data from each Phoenix form is submitted to this temporary structure. Upon submission of the final form the temporary structure is transformed into all the required nested schemas and saved with a simple Repo.insert.

I like this method, as I have various temporary structures with different requirements and validation logic that all get saved in the same set of DB tables, which don’t need to enforce the same rules.

I’d like to improve the user experience throughout the forms. Things like searching for records to add a list (a la Lunr.js), reordering them with a simple drag and drop interface. This will help the user understand and recognise the structures they are populating.

The web is just going to be one client (another being a native iOS app). For that reason I wanted to leave as much logic as possible server-side to save rewriting it in both (and each future) clients.

However, I can’t decide on the best JS approach. So far I’ve tried:

  • Straight up ES6 JS using lots of document.getElementById
  • StimulusJS
  • Elm
  • Vue
  • Drab

Each have had their drawbacks, the main one being a less-than-perfect approach to progressive enhancement.

I don’t want or need to use an SPA for this problem, and would like to avoid client-side rendering if possible (mainly because of the repeated logic it requires).

That said, I tried both an Elm and a Vue component which takes over the page and manages the entire flow of the form having been given the required data up front. At the end of the form it could just submit the required information as JSON, or populate it to a form (might be a way for progressive enhancement that way). The big downside here is that it doesn’t utilise the server-side logic in the temporary structures, and requires a fair bit of duplicated logic.

Vanilla and Stimulus allow for this better than the others, but are lacking in state management. Storing state of this complexity in the DOM doesn’t really work well, especially compared to the more declarative options. I’d probably end up communicating over a Phoenix channel and having the server send back HTML fragments to place at certain points, which whilst acceptable can get out of hand quite quickly.

This was the approach I took with Drab, and it sort of ended being a server-dependent MUV (TEA) structure which worked quite well. That said I found Drab biased to broadcasting server-side events, rather than updating server-side state based on client-side events. It didn’t buy me much over straight JS, and it’s unclear how well I could reuse that in the iOS app.

At the moment I’ve got a preference for Stimulus and Turbolinks because they would allow for a hybrid approach in the iOS app, again reducing maintenance overhead, which given I’m a one-man team would be very helpful in getting this app out the door and on tickover.

At the moment I feel either the Vue/Elm or Stimulus/Channel approach is viable, it just depends on whether I want to lean on server-rendered HTML, or client-rendered HTML, the latter requiring more duplicated logic.

What’s your preference when you’ve got fiddly forms? How do you provide a good experience to the user without adding too much unnecessary complexity to your app?

5 Likes