err0r500
handling discrepancy between a form UI and what should be persisted in the db
Hi,
I’m working on a form and I’m not sure how to handle a specific type of input so I’m looking for advices & best practices.
Here’s how it works : the (text) input has an optional numeric value attached to it (we can think of an article that may have an additional cost in a specific context).
The UI is supposed to look like this :
- first article : no additional field
- second article : additional field set to 14
- third article is being created and for now the user has just activated the additional field but not set it yet (so we’ve got something I don’t want in the db : “has_additional” true, but additional value is nil.
Here’s what I currently do :
I use a functional component for the UI with the state handled by the lv
I use an Ecto schema to model this and in order to get it to work I included in it an “has_additional” along with the value of the additional field which is a nullable number.
The problem I have is that I’m not sure how to be consistent :
- in the db : if the value is nil, the “has_additional” should be false (and the opposite)
- in the UI : the value may be nil and the “has_additional” true when the user just activated the toggle
(also, I’m not fond of having this “has_additional” field : having the value to nil or not should be enough )
The solutions I see :
- I’m thinking of using 2 different changesets : one for the validation and one for the persistence but it’s deeply buried so (in my understanding) I’d have to create a new Changeset for every schema.
- I guess I may also use a live component to encapsulate the toggle state
Not sure if I’m clear.
How would you handle this ?
Best,
Matthieu
Most Liked
garrison
Unfortunately you have stumbled upon one of the uncomfortable and unavoidable truths of application development, namely that your data model must pass through invalid states on its journey between valid states, and that any attempts to circumvent this inevitably lead to poor UX. Here is a very long article about this exact problem if you are so inclined. You may find it enlightening, or existentially sad.
I’m afraid there is no good solution to this problem - the root cause is SQL databases themselves, and we’re stuck with them for now. I know it feels weird, but the correct solution really is to keep both fields and then handle the logic on the other end when you read the rows back. To soften the blow, you could perhaps default the value to zero, which depending on your exact use case might make things easier. But just keep in mind, somewhere down the road you will run into a situation like this again, and there might not be an easy way out.
garrison
I’m not going to critique your approach any further (seems like you’ve arrived at a solution that works for you!), but I want to try to explain why this approach will never quite be enough - not in your case, but in a broad sense.
Imagine you decide that instead of a form-oriented design, you want to make your app real-time and interactive (features that are, rightfully, considered to be a selling point for LiveView). So, instead of writing a new row to the database on form submission, you instead write the row when it’s created and then update it live, with some PubSub magic so that others can see it. All good, right?
Well not exactly, because now you’re back to square one with the toggle. When the user clicks the toggle, you have to write that to the DB, so it has to pass validation! And if you don’t store an extra field for the toggle state, you will still have the UX issue from before where it clobbers the value of the field.
I want to emphasize that this is not a hypothetical. I have a LiveView app I’m working on (an RSS reader, alpha soon™) which works exactly like I just described. In particular, I have a UI control which allows a user to add link redirects, and each link redirect has a toggle switch to enable/disable it as well as fields to store the from/to patterns. It’s actually quite remarkably similar to your case.
Anyway, here’s the thing: my entire app is almost completely free of forms. Everything works in real time and is persisted back to Postgres. And just to make things more fun, I built full undo/redo support over the entire UI.
These things - realtime, undo/redo - would not work correctly if I structured my components as you have here. A user would hit the toggle by mistake, press undo, and the fields from that row would be gone. You could attempt to write more and more code to rectify this with special cases, but all you would end up with is an unmaintanable spaghetti nightmare. It can’t be done.
This is the point that article is trying to make. Features like undo/redo should be standard in every webapp, but they’re not because tools like SQL make them so hard to build. Once you have to persist the in-between states (in order to support undo), you can no longer maintain such a strict validation policy. As I said before, that toggle field is not redundant! The only reason you can get away with removing it is that you are avoiding persisting the in-between states to the database, and that is the compromise which harms UX. There is no way out.
linusdm
Wow, this is a good article! Thanks for sharing
It makes a lot of uncomfortable situations I’ve encountered in the past regarding UX/front-end design very explicit. They were uncomfortable in the sense that I’ve always thought there should be a simpler solution. But now it turns out UI is messy
very enlightening to read indeed.
The article is quite abstract, and doesn’t translate directly to code (especially because the author is referencing examples in a more traditional SPA-and-backend architecture). I think Phoenix applications already have good tools to separate “intent” from state by using Ecto changesets (a “changeset” is more or less a synonym for “intent” in this regard), and schemaless changesets if UI doesn’t exactly match the database structure. There is much more to it, and there is still the object(struct!)-rdbms impedance mismatch that doesn’t encourage you to store intermediate/invalid state.
I’m curious how you’re tackling things differently with this explicit “intent” in mind, using the existing building blocks the Elixir ecosystem is providing you to build the “live” RSS reader you’ve described. If this would take us too much off-topic we can split off the discussion. But I’m very interested in this topic, as it seems to uncover some problems I’ve run into again and again.
PS: I’m a Stanley too…
(this is a reference to the article)
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance










