Create forms with Websharper.Forms
In my previous posts, I have covered multiple aspects on how WebSharper can be used to make nice webapps by using animations, by tapping into external JS libaries or by using the built in router in UI.Next to make SPA. Today I would like to cover another aspect which is essential for making useful webapps - Forms.
Most of the site we visit on a daily basis have forms.
WebSharper.Forms is a library fully integrated with the reactive model of
brings composition of forms to the next level by making composition of forms, handling validation,
handling submit and displaying error an easy task.
WebSharper.Forms is available in alpha at the moment on nuget - https://www.nuget.org/packages/WebSharper.Forms.
The form that we will build in this tutorial will handle:
- Inline validation
- Submitting data
- Async operation
- Error handling from async operation
This are the requirements I gathered during my last project. I had to deal with many forms but overall, they all required this four points and nothing more.
All the forms that I built so far follow the same order of instructions:
- Follows a bunch of apply (
- Pipes some
Form.MapAsyncwhich are to be executed on submit,
Form.MapToResultto handle the result of the async call (4. and 5. can be combined with
- Pipes (
Form.WithSubmitto tell it that I want to submit something after a
Form.Renderwhich provides a way to transform the
Docwhich we can then embed in the page.
As an example, here is the full implementation of the form that we will use:
Form.Return (fun firstname lastname age -> firstname + " " + lastname, age) <*> (Form.Yield "" |> Validation.IsNotEmpty "First name is required.") <*> (Form.Yield "" |> Validation.IsNotEmpty "Last name is required.") <*> (Form.Yield 18) |> Form.MapAsync(fun (displayName, number) -> sendToBackend displayName number) |> Form.MapToResult (fun res -> match res with | Success s -> Success s | Result.Failure _ -> Result.Failure [ ErrorMessage.Create(customErrorId, "Backend failure") ]) |> Form.WithSubmit |> Form.Render(fun name lastname age submit -> form [ fieldset [ div [ Doc.Input  name ] Doc.ShowErrorInline submit.View name div [ Doc.Input  lastname ] Doc.ShowErrorInline submit.View lastname div [ Doc.IntInputUnchecked  age ] Doc.Button "Send" [ attr.``type`` "submit" ] submit.Trigger Doc.ShowCustomErrors submit.View ] ])
The first part of the form composed by the set of
Return <*> yield <*> yield <*> yield is very powerful.
If you want to read more about this type of composition, you can read this blog post from Tomas Petricek http://tomasp.net/blog/applicative-functors.aspx/.
Basically, it allows us to work directly with the input validated data in the function given in
Every interaction is done by composing
Form<_> and we compose
Since validation on the inputs is done at the
Form.Yield level, the values given to the function in
Form.Return are always valid and we can safely work with the values.
If our input is a
Form.Yield "" will return a
Form<string,_> and within the function in
Form.Return we can directly work with the
string given by the
Now it is interesting to look at the type to see how the composition works, the first
Form.Return has the following type:
Form<'T, 'D -> 'D> Form<(string -> string -> int -> string * int), ('a -> 'a)>
Form.Yield "" has the following type:
Form<string, ((Var<string> -> 'a) -> 'a)>
Return (putting them together with
<*>) will combine the types and returns:
Form<(string -> int -> string * int), ((Var<string> -> 'a) -> 'a)>
We basically removed one of the
string params from
'T and added a
Var<string> param in
By continuing the same way,
Form.Yield "" and
Form.Yield 0, we end up with:
Form<(string * int), ((Var<string> -> Var<string> -> Var<int> -> 'a) -> 'a)>
And it turns out that
string * int is our inputs combined in a tuple that we receive as argument in
Var<string> -> Var<string> -> Var<int> is what we receive in
Form.Render to render our form.
Wonderful, it seems to add up together!
Inline validation refers to the validation of the fields before being sent. It will help to prevent submitting the form for nothing. I might be stating the obvious but the server should still perform a validation on the input sent.
Validation is handled during
Form.Yield piped to
<*> (Form.Yield "" |> Validation.IsNotEmpty "Last name is required.")
What happens when data is invalid?
That’s the amazing part, when data is invalid, the function in the
Form.Return isn’t executed.
Failure is passed through and can be caught in a
Form.MapResult or directly in the
Form.Render to be display the error.
That is why we can safely assume that all the arguments in the
Form.Return function are valid arguments and we can perform the action we want.
Most of the time when sending a form we want to perfom a network request.
Those requests are usually
Form.MapAsync allows us to specify an
async function to be executed when the form is submitted.
This allows us to handle the result in
Form.MapToResult without worrying about the
async nature of the call.
Form.MapToResult is piped to perform an action when the result of the
async function is returned.
|> Form.MapAsync(fun (displayName, number) -> sendToBackend displayName number) |> Form.MapToResult (fun res -> match res with | Success s -> Success s | Result.Failure _ -> Result.Failure [ ErrorMessage.Create(customErrorId, "Backend failure") ])
When we want to use a
submit button and we want the form to be
triggered when that
submit button is clicked,
we need to pipe a
Form.WithSubmit function. This adds a special type at the end of the arguments of
The type becomes:
Form<(string * int), ((Var<string> -> Var<string> -> Var<int> -> Submitter<Result<string * int>> -> 'a) -> 'a)>
Submitter type exposes a
Trigger function which allows the form to be triggered and a
View which observe the
Result<'T> of the form.
Submitter is just a type hiding a
Trigger triggers the snapshot of the current value of the form.
If you are interested, you can find its definition here.
View can be used to display inline errors and errors returned from the
I pipe the submit after the
Form.Map otherwise you need to use
Form.TransmitView to observe the error which occurs during the mapping.
Also if you pipe the submit after the
Form.Map be sure to add at least one validation otherwise the
Form.Map will be executed one time on startup.
Finally we render the form and transform it to a
Doc. As we seen earlier, the arguments of the
Form.Render function are the
Var<_>(s) plus a
We basically construct the form and call
.Trigger on click.
|> Form.Render(fun name lastname age submit -> form [ fieldset [ div [ Doc.Input  name ] Doc.ShowErrorInline submit.View name div [ Doc.Input  lastname ] Doc.ShowErrorInline submit.View lastname div [ Doc.IntInputUnchecked  age ] Doc.Button "Send" [ attr.``type`` "submit" ] submit.Trigger Doc.ShowCustomErrors submit.View ] ])
Render call contains some extra functions,
These functions are extensions that I have created to simplify the display of errors.
Here’s the implementation:
let customErrorId = 5000 type Doc with static member ShowErrorInline view (rv: Var<_>)= View.Through(view, rv) |> View.Map (function Success _ -> Doc.Empty | Failure errs -> errs |> List.map (fun err -> p [ text err.Text ] :> Doc) |> Doc.Concat) |> Doc.EmbedView static member ShowCustomErrors view = Doc.ShowErrors view (fun errs -> errs |> List.filter (fun err -> err.Id = customErrorId) |> List.map (fun err -> p [ text err.Text ] :> Doc) |> Doc.Concat)
View.Through will filter the errors which are only related to the
I am using a
cutomErrorId to filter the errors that I created myself.
WebSharper.Forms looks intimidating, especially when you are not familiar with the apply notation.
But the concepts used in
WebSharper.Forms is very powerful as it allows us to hide behind the
Form<_> type and manipulate safe values to perform our actions.
The only validation needed is the validation during the
After getting used to it, I found the use of
WebSharper.Forms very beneficial as
it allowed me to rapidly build form flows and even after few weeks,
I can just have a glance at the code and directly understand what it is doing
(and we all know that it does not happen with every piece of code). Like always, if you have any comments, don’t hesitate to hit me on Twitter @Kimserey_Lam or leave a comment below.
Thanks for reading!