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!

Made with Slides.com