"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

Not sure what will be the best for you, so I’ll share our experiences:

  • Vanilla JS and server rendered responses, what Drab does but there was no Drab as we did it - for real-timey looking pieces of interface unless there are many of them works quite well and is dead simple

  • Vue - allows for a reasonably simple setup where you can have a “hybrid” approach, render most of the stuff on the server but encapsulate some pars in components that can communicate with API, updates themselves etc. Good for bigger Apps that are not SPA but have dynamic parts

  • React and React Native - that was an SPA and not just a form, it would also make sense to move all your rendering to frontend and provide an API on the server side (which can use WebSockets). Big plus here is that you can port features to the App relativlely simply and will also be able to provide one for Android

1 Like

Have you looked at Intercooler or Unpoly? They lean a bit more to server-side rendering than Stimulus appears to (I don’t have any direct experience with Stimulus). Using a library like this is great for progressive enhancement since they just enhance specific parts of your pages.

1 Like

I’ve previously said that I’d never build another React app after the car crash I experienced with my last one. That said it was a complete tech:requirement mismatch, whereas this one would be a better candidate for that approach. I think if I went this route, I’d go all-in on an Elm SPA for the web and a fully native iOS app. However I’m very aware that’s a lot of work for one person, and also unnecessary.

The approach I’m looking at with Stimulus and Turbolinks accomplishes pretty much the same as Intercooler/Unpoly from my tests, with the advantage going to Turbolinks because of their iOS bindings. That said, I agree that unpoly does offer a more complete, unified approach.

I think that Drab should work well here, but when I put together a quick prototype it felt like more than I needed. Plus, it was obvious that I was really working the wrong way, trying to drive the state from the client, whereas Drab wants to update the client based on server state.

I think having worked with React a lot previously I’ve been spoilt by the VDOM, the declarative templating and the one directional flow of data. That experience makes anything where you’re manually adding classes, appending new divs full of server-rendered HTML seem quite mechanical by comparison.

I feel like my ideal solution would look a little like React or Elm, but where the state and the templating is done serverside, and the client just updates over a channel. This doesn’t feel impossible given tools like Drab and Turbolinks, but I can’t seem to put together a polished version of this model in reality.

1 Like

Your ideal solution sounds a lot like Phoenix LiveView to me! Not sure you have time to wait for its arrival though.

I’ve actually been reading/watching up on it this morning, and it sounds pretty similar to what I described above. I must have read or heard someone who already knew about LiveView mention it out of context.

From what I’ve read I should be able to accomplish the same with Drab.Live, which I appear to have missed on first pass. Might need to revisit that.

I’m not sure it’s worth delaying progress on the app to wait for LiveView given that I haven’t seen a timeframe mentioned. That said, I’m fairly certain from what I’ve read so far, a hybrid-native iOS app using WebViews would be comedically simple using LiveView so it might be worth waiting…

I did really need an improved UX for the alpha testers, but maybe I hold fire on any client-side stuff for now and delay the early-access. There’s still loads to do, so maybe I can concentrate on building out the rest of the server-side stuff that I’ll need — user subscriptions, emails, DB queries, etc — for now and see what happens regarding LiveView.

I sometimes wonder why I’m not a carpenter instead! Far fewer choices and change! :joy:

1 Like

Just thought I should say that I’ve revisited Drab and am now using Drab.Live pretty extensively throughout the UI. I couldn’t quite make it work until I found the ability to save PIDs into Drab.Sessions from the controller (Plug.Session), which allowed me to updated my Agent with the temp structure in realtime.

The knockoff is that the UI now has the ability to drown my DB in queries, so am going to need to implement some form of caching, or change where values are stored throughout the form filling process.

Wanted to say a big thank-you to @grych, Drab is brilliant and the more I dig into it the more cool things I find!

4 Likes

Hmm? Drab was first and foremost a callback system to respond to client events, do some work, and return (potentially multiple times). What are you referencing?

Don’t forget Unpoly.js too.

The server state is the authorative state, the client state is used for input to the server state, as it should be as the database is on the server.

Cachex! I love it! So many features and it’s great on performance, and it’s trivial to send a command out to nodes to clear even down to specific data to force it to re-cache. I kind of use it like a ‘materialized view’ in a database that times out for updates over time, it’s awesome!

2 Likes

I had totally missed Drab.Live, which meant that I wasn’t getting the updated state from the server back in updated assigns. Now that I’ve worked out to read the documentation thoroughly before writing something off… how to go about it, I’ve got a pretty comfortable back & forth between server and client.

I’ll take a look at that.

Main criteria is loading a list of structs into it, and then querying that list by two of it’s string-based fields. Not your usual caching case I’d imagine, but probably not too complex.

My next challenge is reordering a list with drag & drop using Drab. This could be interesting! :wink:

2 Likes

Thank you for the useful recommendations and links! I will use them as I am trying to create an AR application for one school (like this). Actually, if you have any suggestions how to make an app more engaging and interesting for students, please share your thoughts:))

@Marko I think that recommendations about your project would fit best under a different thread so that this thread can remain focused on augmenting forms in a primarily server-rendered app. But it does sounds like you have quite an interesting project!

1 Like