Submitting a form: show and feedback

I started using Elm relatively recently, only for a few weeks now, and it’s my first foray into functional programming. As I was working on a small feature to learn the language, I came across a situation where I wanted to simultaneously grab multiple values from a field. Well Html.Events.onSubmit wasn’t cutting it so I ended up creating my own event listener to do what I wanted.

I’m sharing this example here in case it is useful to anyone else, but also for feedback. It seems to work well but perhaps there are some pitfalls or gotchas with this approach that I’m not aware, as opposed to what I’ve seen before with updating the model using onInput with every keypress for each each field.

This approach seemed better for my case where I really don’t care what’s in the field until the user chooses to submit it.

Here’s the full example: https://ellie-app.com/7V9DpNRWffTa1

Here’s my event listener:

onFormSubmit : (FormData -> msg) -> Html.Attribute msg
onFormSubmit tagger =
    preventDefaultOn "submit" (D.map alwaysPreventDefault (D.map tagger formValues))
    

alwaysPreventDefault : msg -> ( msg, Bool )
alwaysPreventDefault msg =
  ( msg, True )


formValues : D.Decoder FormData
formValues =
    D.map2 FormData
        (D.at ["target", "elements", "0", "value"] D.string)
        (D.at ["target", "elements", "1", "value"] D.string)

Hopefully this helps someone and thank you in advance for any feedback, I’m becoming well aware of what I don’t yet know.

I copied some bits from the Html.Events implementation of different core events, so I’m still wrapping my head around exactly how that works.

1 Like

Neat! If you find yourself doing this a lot you might want to write some higher level primitives for decoding the data. Like “decodeField : String -> Decoder String”. That function could use D.andThen to check the name of the field and decode by name instead of position, which would make the decoder independent of the view order.

1 Like

Ah, so that’s how it’s done. I had thought of that, keying off the name field instead, recognizing that what I’ve got now could be pretty fragile, but didn’t know how to accomplish it. Thanks for the tip, that would definitely be better.

I think I’m making some progress on this approach, but I’m a bit stuck. I can see how I can first decode by name, and then use andThen to return the proper decoder based on the decoded name.

I’m having two issues, however:

  1. As I have it, it’s still dependent upon order, to a degree. If I moved the input[type=submit] element to be the first child I’d miss out on one of the input fields I want.
  2. I can’t figure out what decoder I need to return once I know the name. The issue I have is that the field I want, value is a sibling of name.
decodeFields : D.Decoder FormData
decodeFields =
    D.map2 FormData
        (D.at ["target", "elements", "0", "name"] D.string |> D.andThen decodeField)
        (D.at ["target", "elements", "1", "name"] D.string |> D.andThen decodeField)

decodeField : String -> D.Decoder String
decodeField name =
    case name of
        "one" ->
            D.succeed "1" -- temp
            
        "two" ->
            D.succeed "2" -- temp
            
        _ ->
            D.fail "oops"

Any hints or tips to point me in the right direction are appreciated. Thanks!

Can’t work through it completely right now, but what you probably want to do is decode target.elements to a Dict of field values and use that to get the field you want. Something close-ish to this maybe:

D.at ["target", "elements"] decodeFieldDict
   |> D.andThen (\fields -> D.succeed (Dict.get fieldLookingFor fields)) -- But also handle the error case

You might have to decode target.elements to a list of tuples before you can get it into a Dict…

That’s just off the top of my head though. I can work through it more later today if you’re still having issues.

1 Like

This didn’t end up turning out anything like I thought it would.

The value at target.elements isn’t an array - it’s an HTMLFormControlsCollection which looks to Elm like an object: {"0": ..., "1": ..., "2": ...}. Decoding that thing would be interesting. You’d probably have to decode it as a dict, convert that to a list, and iterate over that pulling out what you’re looking for. But maybe someone else here has a better idea.

Luckily there’s a much easier way. Fields with names are available right on the form object. So this works:

formValues : D.Decoder FormData
formValues =
    D.map2 FormData
        (withField "one" D.string)
        (withField "two" D.string)

withField : String -> D.Decoder a -> D.Decoder a
withField fieldName decoder =
    D.at ["target", fieldName, "value"] decoder

That’s very clever. I had spent the afternoon pursuing the first path you mentioned, to no avail. That was going to be a deep rabbit hole that I’d have had to set aside more time for.

Does your second solution work because Json.Decoder.at doesn’t require that each item in the list is a direct descendant of the previous one? The idea of using a variable had crossed my mind but I wouldn’t have arrived at such a simple solution.

Thanks for your time on this, it’s been a great learning experience for me.

This is great. It will let me simplify my project quite a bit, I think.
Updated example: https://ellie-app.com/7Vnd7FpYQsBa1

No, D.at does require each item in the list be a direct descendant of the previous one. Input fields with name attributes are available as properties of the HTMLFormElement object. See the documentation, at the bottom of the Properties section.

1 Like

Got it. I missed that before. Thanks again.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.