err0r500

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

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

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

linusdm

Wow, this is a good article! Thanks for sharing :purple_heart: 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 :slight_smile: 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… :raised_hand: (this is a reference to the article)

Where Next?

Popular in Questions Top

fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
vac
Hi, I’m quite new in Elixir and I’m trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and I...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: The documentation above suggests that while ...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29603 241
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
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
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
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
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 records...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement