Mar 2nd, 2017 - written by Kimserey with .
WebSharper came out with WebSharper.Forms. It is a terse DSL to build form, I posted a tutorial on it few months ago https://kimsereyblog.blogspot.co.uk/2016/03/create-forms-with-websharperforms.html. It’s very powerful as the abstraction handles most of the scenarios. Today I would like to show anothe way to create forms by building a form engine.
Defining a model is tricky. It needs to be both flexible enough to handle all needed scenario but it also needs to be simple enough that there aren’t too many options which would make the domain messy. I will give an example later to illustrate this.
For a form engine, the domain model is composed by the elements of form and the submission behaviors.
Form elements
For this example we will just implement the input
and input area
. Implementing the rest of the control will just be a repetition of those steps.
We start by defining the model as such:
1
2
3
4
5
6
7
8
9
type FormElement =
| TextInput of key: string
* title: string
* placeholder: string
* defaultValue: string option
| TextArea of key: string
* title: string
* numberOfLines: int
* defaultValue: string option
Submission behaviors
For the submission behaviors
, we will be allowing json Ajax submit or simple multiform post data.
So we can define the behaviors as such:
1
2
3
4
5
6
7
8
type FormSubmitter =
| AjaxPost of postHref: string
* redirectOnSuccessHref: string
* title: string
| AjaxPostFormData of postHref: string
* redirectOnSuccessHref: string
* title: string
Form
Now that we have both we can compose it into a form:
1
2
3
4
type Form =
{ Key: string
Elements: FormElement list
Submitter: FormSubmitter }
The role of the renderer is, given a model, to render the layout and build a doc.
So we start by the top level render form:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let private displayList (list: string) =
list.Split([| '\n' |])
|> Array.map (fun txt -> Doc.Concat [ text txt; br [] :> Doc ])
let private renderError error =
match error with
| None
| Some "" -> Doc.Empty
| Some err ->
pAttr
[ attr.``class`` "alert alert-danger" ]
(displayList err) :> Doc
let renderForm (form: Form) =
let values =
ListModel.Create
(fun (k, _) -> k)
(form.Elements |> List.map (fun e -> e, ""))
let error = Var.Create ""
formAttr
[ attr.id form.Key ]
[ error.View |> Doc.BindView (fun err -> )
renderElements values form.Elements
renderSubmitter form.Key values error form.Submitter ]
In order to save all the aggregate all the values before submitting it, we use a ListModel
which we will lens into
to modify the specific data.
If you never seen lenses, I recommend you to read my previous post on ListModel lenses https://kimsereyblog.blogspot.co.uk/2016/03/var-view-lens-listmodel-in-uinext.html.
We then define renderElements
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
let private renderElements (values: ListModel<FormElement, (FormElement * ValueState * string)>) (elements: FormElement list) =
let lensIntoValue = values.LensInto (fun (_, v) -> v) (fun (e, _) v -> e, v)
values.View
|> Doc.BindSeqCachedViewBy (fun (k, _) -> k) (fun el view ->
let value = lensIntoValue el
let initValue df =
match df with
| Some defaultValue -> value.Set defaultValue
| None -> ()
match el with
| TextInput (k, t, ph, df) ->
initValue df
divAttr
[ attr.``class`` "form-group" ]
[ labelAttr [ attr.``for`` k ] [ text t ]
Doc.Input
[ attr.id k
attr.``type`` "text"
attr.``class`` "form-control"
attr.placeholder ph ]
value ] :> Doc
| TextArea (k, t, n, df) ->
initValue df
divAttr
[ attr.``class`` "form-group" ]
[ labelAttr [ attr.``for`` k ] [ text t ]
Doc.InputArea
[ attr.id k
attr.rows (string n)
attr.``class`` "form-control" ]
(lensIntoValue el) ] :> Doc
As said earlier, we lense into the value to get a IRef<_>
which can then be passed to WebSharper UI.Next client Doc.InputX
functions.
Next we can implement renderSubmitter
which renders the submitters:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let private renderSubmitter key (values: ListModel<_, _>) (error: IRef<_>) submitter =
match submitter with
| AjaxPost (href, redirect, title) ->
Doc.Button title
[ attr.``class`` "btn btn-primary btn-block" ]
(fun () ->
async {
let! result = boxValuesJson values.Value |> AjaxHelper.postJson href
match result with
| AjaxHelper.Success res -> JS.Window.Location.Href <- redirect
| AjaxHelper.NotFound -> ()
| AjaxHelper.Error msg -> error.Value <- Some msg
}
|> Async.Ignore
|> Async.StartImmediate)
| AjaxPostFormData (href, redirect, title) ->
Doc.Button title
[ attr.``class`` "btn btn-primary btn-block" ]
(fun () ->
async {
let! result = boxValuesFormData values.Value |> AjaxHelper.postFormData href
match result with
| AjaxHelper.Success res -> JS.Window.Location.Href <- redirect
| AjaxHelper.NotFound -> ()
| AjaxHelper.Error msg -> error.Value <- Some msg
}
|> Async.Ignore
|> Async.StartImmediate)
The AjaxHelper is a module with helper functions to execute ajax calls. Notice that we create a DU of specific action. We defined AjaxPost and MultidataPost. This choice related to what I said in 1) “flexible enough to handle all needed scenario but it also needs to be simple enough that there aren’t too many options which would make the domain messy”. We could had a function pass as submitter behavior which would allow infinite possibilities but this would cause the domain to be harder to understand. A newcomer or even my future me will most likely be confused by what to pass in this function. Therefore choosing to express the possible actions explicitly with a DU is much better than leaving infinite options.
We are now able to render the whole form.
So we have a model, we defined the renderer for that model and lastely we defined how to submit the data. Thanks to this we can now create form very quickly and easily by instiating the model and passing it to the render function.
1
2
3
4
5
6
7
8
9
let renderSomeForm postHref redirectHref =
let form =
{ Key = "some-form"
Elements =
[ TextInput ("FirstName", "First name", "(eg. Kimserey)", None)
TextInput ("LastName", "Last name", "(eg. Lam)", None)]
Submitter = AjaxPost (postHref, redirectHref, "SUBMIT") }
renderForm form
The benefit of having this form engine is that for all the forms, we will not have to worry about how the HTML is composed and we will not have to care about how to handle the submission as we have specific behavior support which we can supply configure. There will be much less room for error as the form engine DSL restrict us to the exclusively supported field and submissions.
Today we saw how we could create a form engine with a simple domain which allows quick creation of forms in WebSharper.UI.Next
. Hope you liked this post. If you have any question leave it here or hit me on Twitter @Kimserey_Lam. See you next time!