My team and I have been working with Elm for almost a couple years now. During this time, I developed a set of modules to improve the way we deal with forms and we have been very happy with the results.
I haven’t seen our particular approach in any of the existing form packages that I could find. Thus, I decided to set up a repository with a basic implementation of the API alongside some examples and then request feedback here before potentially publishing a package (if all goes well).
This is a very nice API. I would not hesitate to make a package out of it, if I were you. Bundling all the form-values into a type and using a single message for all form changes seems very nice and will certainly lessen the burden of maintaining a large form with many fields. The mapping of values into an action message when pressing the submit button (i.e. Form.empty Login ...) also feels very clean, although a large form would require a large number of arguments.
A few nitpicks:
The naming of Form.empty sounds confusing to me. To me it sounds like I would initialize a form with only empty fields, but that is not the case. If it were called Form.map instead, the API would be recognizable from the Json.Decode-library (although I realize your implementation is closer to Json.Decode.Pipeline).
I would probably rename Form.append to Form.field for readability and to not impose the notion of working with a collection.
Use opaque types and helper functions instead of exposing the internal struture of a form, e.g. Form.isLoading model.form instead of model.form.state == Form.View.Loading.
You’ve probably already thought of these, and they are very minor issues. Great work and I hope that we’ll be seeing a real package soon.
Super cool! For me the only thing that was a bit confusing at first was that in the login form the Password had it’s validation in the update function whereas the email address had it’s validation in another function. For me intuitively they should both be found in the update function to also stay true to TEA. Can not wait to use it though otherwise.
The idea there was to try to showcase global form errors. Basically, the validation in the update function is simulating a backend server. In this particular case, I tried to show how the current Form.View renderer can deal with errors that are not directly related with a particular field.
I will think of a better example for it!
The thing is that validation has to be performed in the view, we can take advantage of it and only send Msg with data that we can trust. That way, we avoid duplicating logic in view and update.
First of all, thank you for your suggestions! I am glad you seem to like it.
I would advise using records and type aliases for this. Also, Forms are composable, so it should be easy to split them in different parts.
It initializes a form with no fields. It would be similar to Json.Decode.succeed. Form.map would have a different connotation, and it would probably be used to apply a function to the output of the form, like Json.Decode.map.
A Form represents a collection of fields. For me, Form.field would not exactly describe what the function does. I wouldn’t expect Form.field to take two Form values and return a new one. However, I agree that it would look more readable if we ignore what the |> operator is doing. There is also Form.appendMeta, but I guess we could rename it to Form.metaField or something along these lines. I will think about it!
You probably noticed this, but I want to remark that a Form type is completely opaque and should never be used in the Model, as it is mostly made of functions. In the examples, model.form is a Form.View.Model value. This is just a very simple record that is used to render the form with the particular renderer that would come with the package (anyone can implement a renderer). There is an example where I reuse model.form in two different forms.
We used to have Form.View.Model as an opaque type, but many times we need fine-grain control over the showErrors and state attributes, and using helpers felt like implementing setters and getters. That’s why I think a simple record is more appropriate. As I see it, the record is just a way to gather 3 different attributes that have nothing to do with each other, and that they could be passed individually to Form.View functions. I understand that an opaque type would give Form.View more room to change freely. Trade-off?
Thanks you very much @hecrj for this contribution to forms in elm, I feel like it’s one of the areas where best practices have to fully established themselves.
The question I have is around keeping in sync validations for the field and validations for the button. In the login example, if I only enter the password 1234, the field validation of password goes through, but there is no email. Am I missing something or do you have to somehow reference all field validations in the update function again?
You do not. The output of the form is obtained from the different fields in the form, which is then sent to update using an onSubmit event. The data that you receive in the update function is always validated data that can be trusted.
The 1234 password error is supposed to simulate an authentication error coming from a backend server. In other words, something that can not be validated on the client side. I am aware it is causing confusion, I will change the example right away!
EDIT: I removed the credentials validation in the login example and decided to showcase an external form error in the signup example instead, using a Task and Process.sleep to make it clear.
I imagine something like that could be built on top of this library. However, Elm helps me a lot when I write my forms, I don’t think I would like to build them using JSON unless I had a specific use case for it (like receiving forms as data from a backend).
Thanks! I think that the credit card and phone input could be used with the library as custom fields. At work, we have integrated elm-cropper in a custom field that allows you to choose a picture, preview it and crop it. I will try to showcase this in a future example.
I see that your links seem to focus on form UX. However, the goal of this library is not to become a form rendering library, but to become a library that focuses on a composable Form type that is decoupled from any particular rendering strategy. Specific renderers and custom fields could perfectly live in different packages.
Is there something in particular you wanted me to see or comment about?
Thank you for writing up. I’m a little bit surprised I have not seen this form api earlier. Some people might already use this pattern. I have used the same kind of API f-form as yours! Even though, the update function in view function is kind of discouraging, I think it’s more than fine! That part leads to pretty much simpler APIs for everything else.
Could you please extend the example to cover validation on blur and live interaction with a server (e.g. have a username field that checks with a server for availability after the user stopped typing)
An example of composability would also be most welcomed (e.g. have a simple address form that is packaged as such and can be included in some other form).
I have implemented three different examples that showcase different ways to approach form composition with a very simple address form. The resulting form is the same in all of them. You can find the examples here. I would really appreciate your feedback.
The idea of this library is not to provide these features out-of-the-box, but to provide a simple API for building and rendering simple forms, and an easy way to extend the base type to build more complex forms. At work, we use Form.Base to build our own Company.Form and Company.Form.View that use Company.Field.
However, I could implement them as examples to showcase how to extend the library. But before that, I’m mostly interested in what you would expect to see/what you would like.
Validation on blur for each field can be achieved with a renderer that keeps track of the blurred fields. The main challenge would be tracking each field consistently. I guess a first approach could use a List Bool or Set Int, although it would tie the renderer state with a particular form. Another (maybe better) approach would be extending Form.Base and Form.Field to add a unique identifier for each field, so the renderer can track them consistently.
Live validation with side-effects is not supported out-of-the-box, but it can be done with a custom field.
Would you be satisfied with these solutions? Do you have any suggestions?
Thank you for this. The extensible records approach is my favorite.
This work fine. In the interest of styling, maybe a form-error class should be added to the div holding the input so that the user can add a red border to the input or some other indicator.
I like the way that the behaviour of the form fields are determined by passing in a Record containing a mix of constants and functions - an ‘interface’. Actually, it feels like quite an Object Oriented style, but in a good way; no hidden state as in OO, and not overly complex as in Haskell type classes.
A Record is good because it explicitly names the arguments - where a simple higher order function would have anonymous arguments (by which I mean you cannot determine their name from the type signature alone).
I think this is something we are likely to see more of in Elm APIs.
If there are default arguments for some fields, you could also do a builder pattern, so that only fields the user want to set, need to be set.
Not published yet? I will try the WIP using elm-github-install in that case.
Good idea! Maybe we could make the view function accept some additional configuration, so the user can “decorate” each input, similar to what elm-sortable-table does.
Interesting. There are not any default arguments for now, but it might be a really good idea to offer some builders, especially for field attributes, so the library can change more freely (e.g. adding new attributes) without affecting user code. I will definitely think about it. Thanks!
I wanted to get some feedback before releasing a first version of the package. I will probably publish it soon. Once you try it, I would really appreciate it if you share your experience.