jdumont

jdumont

"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?

Most Liked

jdumont

jdumont

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!

OvermindDL1

OvermindDL1

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!

jdumont

jdumont

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:

yurko

yurko

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

axelson

axelson

Scenic Core Team

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.

Where Next?

Popular in Questions Top

belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New

Other popular topics Top

chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30048 115
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 49522 488
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New

We're in Beta

About us Mission Statement