User-friendly apps start with dev-friendly tools
Agenda
- Designing a library
- More than code
- Tips and advice
Designing a library
type Model =
{ // ...
Firstname : string
FirstnameError : string }
type Msg =
// ...
| ChangeFirstname of string
let private init _ =
{ // ...
Firstname = ""
FirstnameError = "" }, Cmd.none
let private update msg model =
match msg with
// ...
| ChangeFirstname newValue ->
if newValue <> "" then
{ model with Firstname = newValue
FirstnameError = "" }, Cmd.none
else
{ model with Firstname = newValue
FirstnameError = "This field is required" }, Cmd.none
let private view model dispatch =
div [ ]
[ // ...
Field.div [ ]
[ Label.label [ ]
[ str "Firstname" ]
Control.div [ ]
[ Input.input [ Input.Value model.Firstname
Input.Placeholder "Ex: Maxime"
Input.OnChange (fun ev ->
ev.Value |> string |> ChangeFirstname |> dispatch
) ] ]
Help.help [ Help.Color IsDanger ]
[ str model.FirstnameError ] ]
// ...
]
Managing an input with Elmish
23 lines per input
- Code duplication
- Noisy views
- Error prone
- Takes times to write
- Server side validation ?
Minimal POC
Can a library solve the form problems ?
Macro feature analysis
macro feature analys
- List of fields
- Different kind of fields
- Errors management
- Some buttons
Modelize a simple Domain
type ValidationState =
| Valid
| Invalid of string
type ValidationState =
| Valid
| Invalid of string
type InputState =
{ Label : string
Placeholder : string option
Value : string
Validators : InputValidator list
ValidationInputState : ValidationState }
type ValidationState =
| Valid
| Invalid of string
type InputState =
{ Label : string
Placeholder : string option
Value : string
Validators : InputValidator list
ValidationInputState : ValidationState }
and InputValidator = InputState -> ValidationState
type ValidationState =
| Valid
| Invalid of string
type InputState =
{ Label : string
Placeholder : string option
Value : string
Validators : InputValidator list
ValidationInputState : ValidationState }
and InputValidator = InputState -> ValidationState
type FormState<'AppMsg> =
{ Fields : (Guid * InputState) list
OnFormMsg : (Msg -> 'AppMsg) option }
Use our domain
let form =
{ Fields =
[ (Guid.NewGuid(), { Label = "Firstname"
Placeholder = None
Value = ""
Validators = []
ValidationInputState = Valid })
(Guid.NewGuid(), { Label = "Lastname"
Placeholder = Some "Ex: Mangel"
Value = ""
Validators = [ isRequired ]
ValidationInputState = Valid }) ]
OnFormMsg = Some OnFormChange }
Can we do better ?
Expose an API to the user
let private createForm =
let firstname =
input {
label "Firstname"
placeholder "Ex: Maxime"
isRequired
}
let surname =
input {
label "Surname"
placeholder "Ex: Mangel"
isRequired
}
form {
addInput firstname
addInput surname
}
let private createForm =
Form.create ()
|> Form.addInput
( Form.Input.create ()
|> Form.Input.label "Firstname"
|> Form.Input.placeholder "Ex: Maxime"
|> Form.Input.isRequired )
|> Form.addInput
( Form.Input.create ()
|> Form.Input.label "Surname"
|> Form.Input.placeholder "Ex: Mangel"
|> Form.Input.isRequired )
Computation Expression
Pipeline
Fluent
let private createForm =
Form<Msg>
.Create(OnFormMsg)
.AddField(
BasicInput
.Create()
.WithLabel("Firstname")
.WithPlaceholder("Ex: Maxime")
.IsRequired()
)
.AddField(
BasicInput
.Create()
.WithLabel("Surname")
.WithPlaceholder("Ex: Mangel")
.IsRequired()
)
Expose an API to the user
Demo
Can a library solve the form problems ?
Before
After
- Code duplication
- Noisy views
- Error prone
- Takes time to write
23 lines per input
7 lines per input
14 lines for configuring the form
Everything is write in one place:
in the library
Improve our POC
Testing the whole features of a Form
Update our Domain
type Key = string
type SelectState =
{ Label : string
SelectedKey : Key option
Values : (Key * string) list
Placeholder : (Key * string) option
Validators : SelectValidator list
ValidationSelectState : ValidationState }
type Key = string
type SelectState =
{ Label : string
SelectedKey : Key option
Values : (Key * string) list
Placeholder : (Key * string) option
Validators : SelectValidator list
ValidationSelectState : ValidationState
IsLoading : bool
ValuesFromServer : JS.Promise<(Key * string) list> option }
type Key = string
type SelectState =
{ Label : string
SelectedKey : Key option
Values : (Key * string) list
Placeholder : (Key * string) option
Validators : SelectValidator list
ValidationSelectState : ValidationState
IsLoading : bool
ValuesFromServer : JS.Promise<(Key * string) list> option }
and SelectValidator = SelectState -> ValidationState
type Key = string
type SelectState =
{ Label : string
SelectedKey : Key option
Values : (Key * string) list
Placeholder : (Key * string) option
Validators : SelectValidator list
ValidationSelectState : ValidationState
IsLoading : bool
ValuesFromServer : JS.Promise<(Key * string) list> option }
and SelectValidator = SelectState -> ValidationState
type Fields =
| Input of InputState
| Select of SelectState
type Key = string
type SelectState =
{ Label : string
SelectedKey : Key option
Values : (Key * string) list
Placeholder : (Key * string) option
Validators : SelectValidator list
ValidationSelectState : ValidationState
IsLoading : bool
ValuesFromServer : JS.Promise<(Key * string) list> option }
and SelectValidator = SelectState -> ValidationState
type Fields =
| Input of InputState
| Select of SelectState
type FormState<'AppMsg> =
{ Fields : (Guid * Fields) list
OnFormMsg : (Msg -> 'AppMsg) option }
Support several fields type
Update our Domain
Support actions
type FormState<'AppMsg> =
{ Fields : (Guid * Field) list
OnFormMsg : (Msg -> 'AppMsg) option
ActionsArea : ReactElement }
{
"firstname": "Maxime",
"surname": "Mangel",
"email": "mangel.maxime@email.com",
"favLang": "9"
}
type InputState =
{ // ...
JsonLabel : string option
// ... }
type SelectState =
{ // ...
JsonLabel : string option
// ... }
Update our Domain
Support JSON serialization
Demo
Plan the next features
- Support custom fields
- Support server-side validation
- Remove the strongs deps over Fulma
- Add global error support
- Support authentification for server communications
- Easy data access
- Less boxing/unboxing
- Support custom loading animation
Since released
What did we learn ?
- Start simple
- Evaluate success
- Test feasibility step by step
- Don't try to create the perfect code on the first try
- Discuss with others
dev-friendly tools
means more than code
Keep your library focused on one task
Take care of maintenance cost
FAKE - F# Make
package.json scripts
Use relevant tests
First contact matters
Documentation
First contact matters
Demo
First contact matters
Interactive preview
First contact matters
Immediate usage
Second contact matters (too)
Tips and advices
Be a reader: read others people code it can inspire you and will give you new ideas on how to solve problems.
When motivation goes down look back to see the path you did
type Point =
{ X : int
Y : int }
static member Decoder =
Decode.decode
(fun x y ->
{ X = x
Y = y } )
|> Decode.required "x" Decode.int
|> Decode.required "y" Decode.int
type Point =
{ X : int
Y : int }
static member Decoder =
Decode.object
(fun get ->
{ X = get.Required.Field "x" Decode.int
Y = get.Required.Field "y" Decode.int } )
Thoth.Json
V1
V2
Look back
Fable REPL
V1
V2
Look back
Have some break and relax
Thank you!
OpenFsharp 2019 - User-friendly apps start with dev-friendly tools
By mangelmaxime
OpenFsharp 2019 - User-friendly apps start with dev-friendly tools
- 350