Preprocessing values for inputs in a Phoenix (LiveView) form

I have a (LiveView) form with some inputs for fields, which are backed by relatively large precision/scale decimals. The problem I have is that when editing existing record, the inputs

<%= number_input f, :field, ... %>

get prepopulated with values taken from the database at its full scale:

Decimal.new("123.45000000")

and the input is then pre-filled with 123.45000000

What would be the suggested way for getting rid of all the trailing zeroes?

For testing I tried adding “value” attribute to the input but whenever I try to process something like Decimal.normalize(f.data.field) I get nil passed to normalize/1 instead of the number. Just giving f.data.field (or Decimal.normalize(Decimal.new("123.45000000")) for value works OTOH.

Since version 1.9.0 there is a function for that, see: Decimal.normalize/1

As you can see above I am quite aware of the existence of this function. The problem is not with “normalising” the number in general but with having the form inputs prepopulated with “normalised” values.

Oh, right - I didn’t read the last note, sorry :sweat_smile:

So the problem is not with decimal at all …

Why you try to access f.data.field? If I remember correctly there are 2 value maps in data and params keys, so if one is nil you should fetch the other one.

Have you tried Phoenix.HTML.Form.input_value instead? It’s doing exactly what I have described above.

Well, because I don’t know any better at the moment

This is editing an existing record and I see the values of the record are in fact there but for some not obvious to me yet reason I am unable to use them as argument to functions. I can put them directly as value: f.data.field though…

Heck, I am not even sure if this is what I should be doing as it feels “hacky” to me

Since we confirmed it’s not a problem with decimal dependency there is a very small amount of possible issues in your code:

  1. f could be a wrong variable (rare, but still possible)
  2. By any chance the data you pass to construct a form is nil instead of decimal
  3. Thee is a bug/misunderstanding of form construction behaviour, so said decimal should either be in params instead of data (behaviour misunderstanding case) or data instead of params` (bug case)

If you can confirm (by for example inspecting) that 1 and 2 is not a case then most probably it’s nothing else than what I have described in 3rd point. Whatever is true a dedicated functions like input_value/2 linked above should be preferred instead of accessing a struct’s field directly.

Of course it could be other issue not related to 3 points above like incorrectly accessing nested form fields, but this cannot be deducted from what you have shared here.

Edit: While above is still true (in context of my previous message) I have realised that there is some misunderstanding in our talk. First of all Decimal.normalize/1 cannot return nil value! I have confirmed it even by viewing it’s code. It’s either returning a normalised decimal or raises an error, so I guess it’s not related to this line, but there is a bug in other part of your code.

I would advise to reproduce your issue by creating a small script file. Here you can find an example ecto_sql script:

Using above code add decimal, phoenix_html and any other dependency you need and simply copy paste your controller/liveview function body into an example function. Try to debug it yourself with IO.inspect/1 or Kernel.dbg/1 and post it here if needed.

I highly appreciate your engagement and willingness to help but I probably was not clear or explicit enough because the answers to your questions are in fact up there.

  1. f is correct and holds correct Phoenix.HTML.Form struct. It works, fields are pre-filled with values taken from the data source (RDBMS)
  2. I do not pass nil. I pass the model / changeset and since the form is constructed correctly and works, there is no way that there is nil being passed but I’ll do a quick “crowbar” style test below in order to prove it
  3. As mentioned already, this is editing an existing record so I don’t expect things to be in params but again I’ll do the test
  4. (Edit) - Decimal.normalize/1 cannot return nil value. True. And I didn’t write that it returned nil. I wrote that:

Now for the quick “crowbar” test. Let’s put value: f.nonexistent into the input line. Result:

key :nonexistent not found in: %Phoenix.HTML.Form{
  source: #Ecto.Changeset<action: nil, changes: %{}, errors: [],
  [...]
  data: %MyApp.MyContext.MyModel{
    [...]
    field: Decimal.new("123.45000000"),
    [...]
  },
  action: nil,
  hidden: [{"_persistent_id", "0"}, {:id, 124}],
  params: %{"_persistent_id" => "0"},
  errors: [],
  options: [multipart: false],
  index: 0
}

So the form is correct, the form variable f is correct. The form struct’s data contains the field and field contains correct, non-nil value. And yet we are talking about a Schrödinger field’s value. If I don’t touch it - it is there:

value: f.data.field yields correct value

If I try to touch it - it disappears:

value: Decimal.normalize(f.data.field) yields:

no function clause matching in Decimal.normalize/1
[...]
Called with 1 arguments
1. nil

value: Decimal.normalize(input_value(f, :field)) yields:

no function clause matching in Decimal.normalize/1
[...]
Called with 1 arguments
1. nil

even more interesting: value: [Decimal.normalize(input_value(f, :field))] yields:

lists in Phoenix.HTML and templates may only contain integers representing bytes, binaries or other lists, got invalid entry: Decimal.new("123.45")

showing exactly what was supposed to be there…

Now, all of those may be a kind of an unexpected side effect of trying to do it all the wrong way (as my guts try to tell me) so my original question remains:

UPDATE 0: I begin to suspect that this appearing/disappearing value may somehow be caused by LiveView rendering the page twice… but even if I find it to be true then I am still unsure if there is no better approach for the problem at hand

UPDATE 1: Yes, I found the problem causing the Schrödinger value - all my fault. But this was actually a side-thread. The original question – w/o focusing on the errors of my original struggle with Decimal being passed nil on occasions – remains:

Anything better than what I came up with?

What was it?

Huh… (coughing, trying to hide the embarrassment)… :face_with_open_eyes_and_hand_over_mouth: The field in question is in has_many collection’s items, where at least one is required to be present. On editing existing records, there should be only valid items inside that collection but for the LiveView render of edit I was adding an empty (hence invalid) one, as if the whole record was new rather than existing/validated/persisted. This was obviously causing all the “weird”, while in fact completely correct, behaviour I mentioned above.

1 Like

A bit late reply as I was preparing something … bigger … on devtalk. :smiling_imp:

So problems with nested form fields … where did I heard about it? :smile:

Yeah, that’s definitely a completely different case. Glad you have found the problem. Now let’s see where we are …

If we again talk about Decimal value then you have almost the answer. Why almost? Because it does not accept nil …

iex> Mix.install [:decimal, :phoenix_html]
:ok
iex> form = %Phoenix.HTML.Form{data: %{field: Decimal.new("123.45000000")}}
%Phoenix.HTML.Form{
  source: nil,
  impl: nil,
  id: nil,
  name: nil,
  data: %{field: Decimal.new("123.45000000")},
  action: nil,
  hidden: [],
  params: %{},
  errors: [],
  options: [],
  index: nil
}
iex> value = form.data.field
Decimal.new("123.45000000")
iex> value && Decimal.normalize(value)
Decimal.new("123.45")
iex> value = nil
nil
iex> value && Decimal.normalize(value)
nil

As you can see the trailing zeros have disappeared and the nil value is easy to take care of as well …

That’s said still I recommend to go with a default value like Decimal.new("0") and use input_value, so no matter if you are at rendering step (edit page) or are submitting the form you always get a desired or default Decimal value. This way you do not have to worry about nil values.

Well… you were indirectly right! :smiley: Although not exactly a nested form’s problem but rather me adding an unwanted/unexpected collection member where it wasn’t meant to be. And masquerading this fact with some crashes :tired_face:

I use Decimal functions in many places. It’s not about this. It was a side-thread caused by the temporarily hard to explain errors I encountered.

It is more about “how to handle such cases”. I could come up with:

  • building a separate “read” model and transforming (normalising) data on reading from DB
  • doing the normalising in the end on the inputs themselves

The first feels like an overkill for such relatively small problem to solve, while the latter feels a bit “hacky”, like hammering the transformed values into places against the framework’s will :wink:

All that while doubting that my case is a very rare, isolated one and wondering if there is maybe already something built-in or a de facto standard Elixir/Ecto/Phoenix way of dealing with this kind of problems. Something I am simply not yet aware of.

I currently use input_value but having any default there doesn’t make much sense in the case at hand so I handle nil gracefully instead.

1 Like

I wound focus on thinking in different direction … If it was not for that case then why Decimal.normalise/1 has been added? Look that you can write a custom Ecto.Type that would do 1:1 of the Decimal stuff, but when it’s about to read a value from database you do again the same thing + normalisation. Whatever you choose is rather up to you. Such specific use cases rather don’t have a good practices guides.

I would rather look at your own code and decide what’s the best way comparing to overall style of your app. For example you may already have 1 or 2 custom Ecto.Type, so introducing another one would be relatively to your code style absolutely normal.

I also haven’t tried it, but there are other PostgreSQL-related solutions like selecting a column and cast it to real or converting a whole column to real type while working on Decimal at Elixir app side. As said I haven’t tested it, so I have no idea about performance, preserving precision and so on …