Create forms with Websharper.Forms
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 UI.Next
which
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.
What is needed to build a form
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.
Composing with WebSharper.Forms
All the forms that I built so far follow the same order of instructions:
- Calls
Form.Return
, - Follows a bunch of apply (
<*>
) ofForm.Yield
, - Pipes some
async
functionForm.MapAsync
which are to be executed on submit, - Pipes
Form.MapToResult
to handle the result of the async call (4. and 5. can be combined withMapToAsyncResult
), - Pipes (
|>
)Form.WithSubmit
to tell it that I want to submit something after abutton submit
click, - Pipes
Form.Render
which provides a way to transform theForm
to aDoc
which 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 Form.Return
.
Every interaction is done by composing Form<_>
and we compose Form<_>
elements.
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 string
input, Form.Yield ""
will return a Form<string,_>
and within the function in Form.Return
we can directly work with the string
given by the Form.Yield
.
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)>
And Form.Yield ""
has the following type:
Form<string, ((Var<string> -> 'a) -> 'a)>
Applying Yield
to 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 'D
.
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 Form.MapAsync
and 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
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 Validation.XX
.
<*> (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.
Instead a 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.
Mapping async function and result
Most of the time when sending a form we want to perfom a network request.
Those requests are usually async request
. 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") ])
Submitting data
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 'D
.
The type becomes:
Form<(string * int), ((Var<string> -> Var<string> -> Var<int> -> Submitter<Result<string * int>> -> 'a) -> 'a)>
The Submitter
type exposes a Trigger
function which allows the form to be triggered and a View
which observe the Result<'T>
of the form.
A Submitter
is just a type hiding a View.SnapshotOn
where Trigger
triggers the snapshot of the current value of the form.
If you are interested, you can find its definition here.
The View
can be used to display inline errors and errors returned from the async call
.
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.
|> Form.WithSubmit
Render
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 Submitter
.
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 ] ])
Some helpers
The Render
call contains some extra functions, Doc.ShowErrorInline
and Doc.ShowCustomErrors
.
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 Var<_>
given.
I am using a cutomErrorId
to filter the errors that I created myself.
The full code source can be found here .
Conclusion
At first 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 Yield
stage.
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!
This comment has been removed by the author.
ReplyDelete