edkelly303/elm-yafl/2.0.0

Yafl

This library helps you build user input forms in Elm by creating and composing self-contained Widgets.

Table of contents

Creating Widgets

Widget

Turning Widgets into Fields

Field, defineFields, addWidget, addWidgetWithConfig, endFields

Turning Fields into forms

Model, Msg, init, load, update, ViewConfig, view, Wizard, viewWizard, Feedback, subscriptions, submit

Combining Fields

succeed, fail, failAt, map, andMap, andThen, choice, option

Customizing Fields

label, htmlBefore, htmlAfter

Validating Fields

error, errorAt, warning, warningAt

Communicating between Fields

HasId, NoId, identifier, intercept, send

Testing and debugging

studio, toDOT

Creating Widgets

Back to top

Widgets are the basic building blocks of this package. Each widget is effectively a little Elm application, with its own init, update, view and subscriptions functions, plus a couple of extra features.

This package doesn't supply any prebuilt widgets. Every app is unique, and it's unlikely that a prebuilt widget would precisely fit your use case. But the point is, this package gives you the power to create any types of widgets you choose, and compose them together very easily with minimal boilerplate.

Nevertheless, we'll provide some code samples for a few simple widgets that we can use in the code snippets in these docs.

module Examples exposing (..)


import Html as H
import Html.Attributes as HA
import Html.Events as HE
import Yafl


{-| A basic `Widget` that produces a `String`. Its internal
`model` and `msg` types are also `String`s.
-}
stringWidget : Yafl.Widget String String String
stringWidget =
    { init = ( "", Cmd.none )
    , update = \msg model -> ( msg, Cmd.none )
    , view =
        \{ label, id, feedback } model ->
            [ H.label [ HA.for id ] [ H.text label ]
            , H.input
                [ HA.id id
                , HA.type_ "text"
                , HA.value model
                , HE.onInput identity
                ]
                []
            , H.ul [] (List.map (\f -> H.li [] [ H.text f ]) feedback)
            ]
    , subscriptions = \model -> Sub.none
    , submit = \model -> Ok model
    , label = "String"
    }


{-| A `Widget` that produces an `Int`, based on the counter
example from the Elm Guide. Its internal `model` type is an
`Int`, but its `msg` type is a custom type.
-}
type CounterMsg
    = Increment
    | Decrement


counterWidget : Yafl.Widget () Int CounterMsg Int
counterWidget () =
    { init = ( 0, Cmd.none )
    , update =
        \msg model ->
            case msg of
                Increment ->
                    ( model + 1, Cmd.none )


                Decrement ->
                    ( model - 1, Cmd.none )
    , view =
        \{ label, id } model ->
            [ H.label [ HA.for id ] [ H.text label ]
            , H.fieldset
                [ HA.id id ]
                [ H.button [ HA.type_ "button", HE.onClick Decrement ] [ H.text "-" ]
                , H.output [] [ H.text (String.fromInt model) ]
                , H.button [ HA.type_ "button", HE.onClick Increment ] [ H.text "+" ]
                ]
            ]
    , subscriptions = \_ -> Sub.none
    , submit = \model -> Ok model
    , label = "Counter"
    }
type alias Widget config model msg output =
config -> InnerWidget model msg output

The Widget type is very similar to the record type that you would supply to Browser.element to create an Elm Program.

Turning Widgets into Fields

Back to top

Before we can use our Widgets to create a form, we need to convert them into Fields. This conversion process effectively combines the internal model and msg types of each widget to create composite types that we can use as the top-level model and msg for the entire form.

We perform this conversion using three functions: defineFields, addWidget, and endFields. The type signatures for these three functions are extremely terrifying, but fortunately we don't need to understand them - just follow the example below:

module Examples exposing (Model, Msg, fields)


import Yafl exposing (addWidget, defineFields, endFields)


fields =
    Yafl.defineFields
        (\string counter -> { string = string, counter = counter })
        |> Yafl.addWidget stringWidget
        |> Yafl.addWidget counterWidget
        |> Yafl.endFields


{- This gives us the following Model and Msg types for
   our form:
-}
type alias FormModel =
    ( Maybe String, ( Maybe Int, () ) )


type alias FormMsg =
    ( Maybe String, ( Maybe CounterMsg, () ) )
type Field formModel formMsg id widgetMsg input output

Forms are composed of Fields - this is the main data type we'll be using in this package.

ctor
-> { ctor : ctor
, widgets : b -> b
, modelGetters :
{ focus : focus -> focus
, appendToGetters : getters -> getters
}
, modelSetters :
{ focus : c -> c
, appendToSetters : setters -> setters
}
, modelBlanks : d -> d
, msgGetters :
{ focus : e -> e
, appendToGetters : f -> f
}
, msgSetters :
{ focus : g -> g
, appendToSetters : h -> h
}
, msgBlanks : i -> i
, apply : j -> j
}

Begin a definition of the fields you want to use in your forms.

Widget () widgetModel widgetMsg output
-> { apply :
( { blankModel : formModel
, blankMsg : formMsg
, ctor :
Field formModel formMsg NoId widgetMsg widgetMsg output -> fields
}
-> ( formMsg -> Maybe widgetMsg, previousMsgGetters )
-> ( Maybe widgetMsg -> formMsg -> formMsg, previousMsgSetters )
-> ( formModel -> Maybe widgetModel, previousModelGetters )
-> ( Maybe widgetModel -> formModel -> formModel
, previousModelSetters
)
-> ( InnerWidget widgetModel widgetMsg output, previousWidgets )
-> accForNext
)
-> toFolder5
, ctor : f
, widgets :
( InnerWidget widgetModel widgetMsg output, previousWidgets )
-> toWidgets
, modelBlanks :
( Maybe widgetModel, previousBlankModels ) -> toBlankModel
, modelGetters :
{ appendToGetters :
( tuple3 -> head3, nextModelGetters ) -> toModelGetters
, focus : tuple3 -> ( head3, tail4 )
}
, modelSetters :
{ appendToSetters :
( head2 -> tuple2 -> tuple2, nextModelSetters ) -> toModelSetters
, focus : (( head2, tail3 ) -> ( head2, tail3 )) -> tuple2 -> tuple2
}
, msgBlanks : ( Maybe widgetMsg, previousBlankMsgs ) -> toBlankMsg
, msgGetters :
{ appendToGetters :
( tuple1 -> head1, nextMsgGetters ) -> toMsgGetters
, focus : tuple1 -> ( head1, tail1 )
}
, msgSetters :
{ appendToSetters :
( head -> tuple -> tuple, nextMsgSetters ) -> toMsgSetters
, focus : (( head, tail ) -> ( head, tail )) -> tuple -> tuple
}
}
-> { apply :
( { blankModel : formModel
, blankMsg : formMsg
, ctor : fields
}
-> previousMsgGetters
-> previousMsgSetters
-> previousModelGetters
-> previousModelSetters
-> previousWidgets
-> accForNext
)
-> toFolder5
, ctor : f
, widgets : previousWidgets -> toWidgets
, modelBlanks : previousBlankModels -> toBlankModel
, modelGetters :
{ appendToGetters : nextModelGetters -> toModelGetters
, focus : tuple3 -> tail4
}
, modelSetters :
{ appendToSetters : nextModelSetters -> toModelSetters
, focus : (tail3 -> tail3) -> tuple2 -> tuple2
}
, msgBlanks : previousBlankMsgs -> toBlankMsg
, msgGetters :
{ appendToGetters : nextMsgGetters -> toMsgGetters
, focus : tuple1 -> tail1
}
, msgSetters :
{ appendToSetters : nextMsgSetters -> toMsgSetters
, focus : (tail -> tail) -> tuple -> tuple
}
}

Add a Widget to the definition of the Fields you want to use in your forms.

Widget config widgetModel widgetMsg output
-> { apply :
( { blankModel : formModel
, blankMsg : formMsg
, ctor :
( config
-> Field formModel formMsg NoId widgetMsg widgetMsg output
)
-> fields
}
-> ( formMsg -> Maybe widgetMsg, previousMsgGetters )
-> ( Maybe widgetMsg -> formMsg -> formMsg, previousMsgSetters )
-> ( formModel -> Maybe widgetModel, previousModelGetters )
-> ( Maybe widgetModel -> formModel -> formModel
, previousModelSetters
)
-> ( Widget config widgetModel widgetMsg output, previousWidgets )
-> accForNext
)
-> toFolder5
, ctor : f
, widgets :
( Widget config widgetModel widgetMsg output, previousWidgets )
-> toWidgets
, modelBlanks :
( Maybe widgetModel, previousBlankModels ) -> toBlankModel
, modelGetters :
{ appendToGetters :
( tuple3 -> head3, nextModelGetters ) -> toModelGetters
, focus : tuple3 -> ( head3, tail4 )
}
, modelSetters :
{ appendToSetters :
( head2 -> tuple2 -> tuple2, nextModelSetters ) -> toModelSetters
, focus : (( head2, tail3 ) -> ( head2, tail3 )) -> tuple2 -> tuple2
}
, msgBlanks : ( Maybe widgetMsg, previousBlankMsgs ) -> toBlankMsg
, msgGetters :
{ appendToGetters :
( tuple1 -> head1, nextMsgGetters ) -> toMsgGetters
, focus : tuple1 -> ( head1, tail1 )
}
, msgSetters :
{ appendToSetters :
( head -> tuple -> tuple, nextMsgSetters ) -> toMsgSetters
, focus : (( head, tail ) -> ( head, tail )) -> tuple -> tuple
}
}
-> { apply :
( { blankModel : formModel
, blankMsg : formMsg
, ctor : fields
}
-> previousMsgGetters
-> previousMsgSetters
-> previousModelGetters
-> previousModelSetters
-> previousWidgets
-> accForNext
)
-> toFolder5
, ctor : f
, widgets : previousWidgets -> toWidgets
, modelBlanks : previousBlankModels -> toBlankModel
, modelGetters :
{ appendToGetters : nextModelGetters -> toModelGetters
, focus : tuple3 -> tail4
}
, modelSetters :
{ appendToSetters : nextModelSetters -> toModelSetters
, focus : (tail3 -> tail3) -> tuple2 -> tuple2
}
, msgBlanks : previousBlankMsgs -> toBlankMsg
, msgGetters :
{ appendToGetters : nextMsgGetters -> toMsgGetters
, focus : tuple1 -> tail1
}
, msgSetters :
{ appendToSetters : nextMsgSetters -> toMsgSetters
, focus : (tail -> tail) -> tuple -> tuple
}
}

Add a configurable Widget to the definition of the Fields you want to use in your forms. Each time you use a Field derived from this Widget in your form, you will be able to pass in a config value.

{ apply :
(acc -> empty -> empty -> empty -> empty -> empty -> acc)
-> { blankModel : modelBlanks
, blankMsg : msgBlanks
, ctor : toFields
}
-> msgGetters
-> msgSetters
-> modelGetters
-> modelSetters
-> widgets
-> { blankModel : modelBlanks
, blankMsg : msgBlanks
, ctor : fields
}
, ctor : toFields
, widgets : () -> widgets
, modelBlanks : () -> modelBlanks
, modelGetters :
{ appendToGetters : () -> modelGetters
, focus : focus3
}
, modelSetters :
{ appendToSetters : () -> modelSetters
, focus : focus2
}
, msgBlanks : () -> msgBlanks
, msgGetters :
{ appendToGetters : () -> msgGetters
, focus : focus1
}
, msgSetters :
{ appendToSetters : () -> msgSetters
, focus : focus
}
}
-> fields

Finalize the definition of the Fields you want to use in your forms.

Turning Fields into forms

Once we've defined our Fields, we can start the fun part: making forms!

Imagine we just want a simple form that allows a user to choose an Int:

import Yafl
import Examples exposing (FormModel, FormMsg, fields)
import Html exposing (Html)


-- We can turn any Field into a form:


form =
    fields.counter


-- Initialize it with `Yafl.init` to get a (model, cmd)
-- tuple:


init =
    Yafl.init form


init


--: ( Yafl.Model FormModel Int, Cmd (Yafl.Msg FormMsg) )


-- The form's model can then be passed to `Yafl.view`,
-- `Yafl.update`, `Yafl.subscriptions` and `Yafl.submit`:


model =
    Tuple.first init


Yafl.view form model


--: List (Html (Yafl.Msg FormMsg))


Yafl.subscriptions form model


--: Sub (Yafl.Msg FormMsg)


Yafl.submit form model


--> Ok 0
type Model formModel output

The top-level model type for your form.

type Msg formMsg

The top-level message type for your form.

Field formModel formMsg id widgetMsg input output
-> ( Model formModel output, Cmd (Msg formMsg) )

Initialize your form

import Yafl
import Examples exposing (FormModel, FormMsg, fields)


fields.counter
    |> Yafl.init


--: ( Yafl.Model FormModel Int, Cmd (Yafl.Msg FormMsg) )
Field formModel formMsg id widgetMsg input output
-> input
-> Model formModel output
-> ( Model formModel output, Cmd (Msg formMsg) )

Load data into your form. A bit tricky to explain, but see the examples below:

For simple Fields, you just pass a value of the underlying Widget's msg type. This will dispatch the msg to the Field's update function, which may then update its model and optionally send a Cmd.

import Yafl
import Examples exposing (FormModel, FormMsg, CounterMsg(..), fields)


form =
    fields.counter


modelBeforeLoading =
    form
        |> Yafl.init
        |> Tuple.first


Yafl.submit form modelBeforeLoading
--> Ok 0


modelAfterLoading =
    modelBeforeLoading
        |> Yafl.load form Increment
        |> Tuple.first


Yafl.submit form modelAfterLoading
--> Ok 1

For Fields composed using andMap, you can pass in a record where each field is a Maybe widgetMsg. If the record field's value is Just, then the message will be dispatched to the Field's update function. If it's Nothing, then no message will be dispatched and the Field's model will remain unchanged.

import Yafl
import Examples exposing (FormModel, FormMsg, CounterMsg(..), fields)


form =
    Yafl.succeed (\int string -> ( int, string ))
        |> Yafl.andMap .a fields.counter
        |> Yafl.andMap .b fields.string


form
--: Yafl.Field FormModel FormMsg Never Never { a : Maybe CounterMsg, b : Maybe String } ( Int, String )


modelBeforeLoading =
    form
        |> Yafl.init
        |> Tuple.first


Yafl.submit form modelBeforeLoading
--> Ok (0, "")


modelAfterLoading =
    modelBeforeLoading
        |> Yafl.load form
            { a = Nothing
            , b = Just "hello"
            }
        |> Tuple.first


Yafl.submit form modelAfterLoading
--> Ok ( 0, "hello" )

For Fields composed using choice and option, it's a similar story, except that the options are nested within a record of the form { selected : Maybe Int, options : {...} }. The selected field is used to pick which of the options should be selected (it's zero-indexed, so 0 is the first option, 1 is the second, etc.)

import Yafl
import Examples exposing (FormModel, FormMsg, CounterMsg(..), fields)


type Foo
    = Bar Int
    | Qux String


form =
    Yafl.choice
        |> Yafl.option "Bar" .bar barField
        |> Yafl.option "Qux" .qux quxField


barField =
    Yafl.map Bar fields.counter


quxField =
    Yafl.map Qux fields.string


form
--: Yafl.Field FormModel FormMsg Never Never { selected : Maybe Int, options : Maybe { bar : Maybe CounterMsg, qux : Maybe String } } Foo


modelBeforeLoading =
    form
        |> Yafl.init
        |> Tuple.first


Yafl.submit form modelBeforeLoading
--> Ok (Bar 0)


modelAfterLoading =
    modelBeforeLoading
        |> Yafl.load form
            { selected = Just 1
            , options =
                Just
                    { bar = Nothing
                    , qux = Just "hello"
                    }
            }
        |> Tuple.first


Yafl.submit form modelAfterLoading
--> Ok (Qux "hello")
Field formModel formMsg id widgetMsg input output
-> Msg formMsg
-> Model formModel output
-> ( Model formModel output, Cmd (Msg formMsg) )

Update your form by supplying a Msg and Model

type alias ViewConfig =
{ label : String
, id : String
, feedback : List Feedback
}

Configuration passed into the view of each Field in your form.

Field formModel formMsg id widgetMsg input output
-> Model formModel output
-> List (Html (Msg formMsg))

View your form.

import Yafl
import Examples exposing (FormModel, FormMsg, fields)
import Html exposing (Html)


form =
    fields.string


model =
    form
        |> Yafl.init
        |> Tuple.first


Yafl.view form model


--: List (Html (Yafl.Msg FormMsg))
type alias Wizard formMsg =
{ stepView : List (Html (Msg formMsg))
, stepIndex : Int
, totalSteps : Int
, selectStepMsg : Int -> Msg formMsg
, isStepValid : Bool
}

A record produced by viewWizard, containing fields that are useful if you want to render your form as a multi-step "wizard".

  • stepView is the HTML produced by the view function for the current step of the wizard.

  • stepIndex is the zero-indexed number of the current step of the wizard.

  • totalSteps is the total number of steps in the wizard.

  • selectStepMsg lets you jump to a specified step of the wizard - for example, selectStepMsg (currentStep + 1) would move to the next step of the wizard. (In general, you'll want to take care not to let the user move to a stepIndex less than 0 or greater than totalSteps - 1, but there are possible designs where you might want to do this, so Yafl doesn't guard against it.)

  • isStepValid indicates whether all the fields displayed in the current step of the wizard pass validation (i.e. their submit functions return an Ok). You could use this to decide whether to display a "next" button in the view of the current step.

Field formModel formMsg id widgetMsg input output
-> Model formModel output
-> Wizard formMsg

View your form as a multi-step wizard. This will only work if the top-level field of your form is a product type created with succeed and andMap.

viewWizard doesn't produce a fully-fledged wizard view; it returns a value of type Wizard. Jump over to the Wizard docs for an explanation.

type alias Feedback =
{ isError : Bool
, message : String
}

Feedback produced when the submit function on a Field returns errors.

Field formModel formMsg id widgetMsg input output
-> Model formModel output
-> Sub (Msg formMsg)

Generate subscriptions for your form.

import Yafl
import Examples exposing (FormModel, FormMsg, fields)


form =
    fields.string


model =
    form
        |> Yafl.init
        |> Tuple.first


Yafl.subscriptions form model


--: Sub (Yafl.Msg FormMsg)
Field formModel formMsg id widgetMsg input output
-> Model formModel output
-> Result (List ( String, String )) output

Submit your form.

import Yafl
import Examples exposing (FormModel, FormMsg, fields)


form =
    fields.string


model =
    form
        |> Yafl.init
        |> Tuple.first


Yafl.submit form model


--> Ok ""

Combining Fields

Back to top

Succeeding and failing

In addition to the Fields that you define based on your Widgets, the package also provides succeed and fail, which can be useful in various ways when used with other combinators such as andMap. You may be familiar with similar functions from packages such as elm/json.

The views of these fields return an empty Html element. When submitted, succeed always returns an Ok, while fail always returns an Err.

succeed : output -> Field formModel formMsg Never Never input output

A Field that always successfully generates the value that you supply.

import Yafl


form =
    Yafl.succeed "Hurrah!"


model =
    form
        |> Yafl.init
        |> Tuple.first


Yafl.submit form model


--> Ok "Hurrah!"
fail : String -> Field formModel formMsg Never Never input output

A Field that always fails on submission with the error message that you supply.

import Yafl


form =
    Yafl.fail "Oh dear!"


model =
    form
        |> Yafl.init
        |> Tuple.first


Yafl.submit form model


--> Err [ ("0", "Oh dear!") ]
Field formModel formMsg HasId widgetMsg1 input1 output1
-> String
-> Field formModel formMsg Never Never input2 output2

Like fail, except it will display the error message on a different Field. This can be useful in multi-field validation, when you have an error that results from a combination of several fields, but you only want to display the error message on one specific field.

import Examples exposing (fields)
import Yafl


form =
    Yafl.succeed
        (\targetOutput failAtOutput -> targetOutput)
        |> Yafl.andMap .target targetField
        |> Yafl.andMap .failure failAtField


targetField =
    fields.string
        |> Yafl.identifier "target-field"


failAtField =
    Yafl.failAt targetField "Uh oh!"


output =
    Yafl.init form
        |> Tuple.first
        |> Yafl.submit form


-- The error is assigned to `targetField`,
-- not `failAtField`
output --> Err [ ( "target-field", "Uh oh!" ) ]

Converting input and output types

map :
(output -> output2)
-> Field formModel formMsg id widgetMsg input output
-> Field formModel formMsg id widgetMsg input output2

Convert the output of a Field from one type to another.

A common use case for this function is to create Fields that produce custom type variants.

import Yafl
import Examples exposing (FormModel, FormMsg, fields)


-- Example: Creating a custom type variant


type MyCustomType
    = Foo String


fooField =
    Yafl.map Foo (fields.string)


fooField
    |> Yafl.init
    |> Tuple.first
    |> Yafl.submit fooField


--> Ok (Foo "")
(input2 -> input)
-> Field formModel formMsg id widgetMsg input output
-> Field formModel formMsg id widgetMsg input2 output

Convert the input of a Field from one type to another.

This is useful if you want to control how data is loaded into your form with load.

import Examples exposing (FormModel, FormMsg, fields)
import Yafl


passwordField :
    Yafl.Field
        FormModel
        FormMsg
        Never
        Never
        -- Here's the `input` parameter:
        { password : Maybe String
        , confirm : Maybe String
        }
        String
passwordField =
    Yafl.succeed
        (\p c -> { password = p, confirm = c })
        |> Yafl.andMap .password fields.string
        |> Yafl.andMap .confirm fields.string
        |> Yafl.error
            (\{ password, confirm } ->
                if password == confirm then
                    Nothing


                else
                    Just "Passwords need to match"
            )
        |> Yafl.map .password


-- With the `.password` and `.confirm` functions that we
-- pass to `andMap` here, our `Field`'s `input` type
-- parameter is a record containing both of those fields.
-- By using `contraMap`, we can change the `input` type
-- to make the field a bit easier to use.


passwordFieldThatIsEasierToLoad :
    Yafl.Field
        FormModel
        FormMsg
        Never
        Never
        -- after `contraMap`, the `input` parameter
        -- is just a `String`
        String
        String
passwordFieldThatIsEasierToLoad =
    passwordField
        |> Yafl.contraMap
            (\string ->
                { password = Just string
                , confirm = Just string
                }
            )


-- DOC TESTS
passwordField --: Yafl.Field FormModel FormMsg Never Never { password : Maybe String, confirm : Maybe String } String
passwordFieldThatIsEasierToLoad --: Yafl.Field FormModel FormMsg Never Never String String

Building product types

(options -> Maybe input)
-> Field formModel formMsg id widgetMsg input output1
-> Field formModel formMsg Never Never options (output1 -> output2)
-> Field formModel formMsg Never Never options output2

Combine multiple fields. Use in combination with succeed.

import Yafl
import Examples exposing (FormModel, FormMsg, fields)


form =
    Yafl.succeed (\a b c -> { firstName = a, middleName = b, lastName = c })
        |> Yafl.andMap .firstName (fields.string |> Yafl.label "First name")
        |> Yafl.andMap .middleName (fields.string |> Yafl.label "Middle name")
        |> Yafl.andMap .lastName (fields.string |> Yafl.label "Last name")


model =
    Yafl.init form
        |> Tuple.first


Yafl.submit form model
--> Ok { firstName = "", middleName = "", lastName = "" }
(output -> Field formModel formMsg Never Never input output2)
-> Field formModel formMsg id widgetMsg input output
-> Field formModel formMsg id widgetMsg input output2

Check the result of submitting a Field. This can be useful if you want to convert an existing Widget to return a different output type.

(You can also use it for validating a field's output, but it will probably be better to use error or errorAt instead.)

Be warned: this is not a fully law-abiding monadic andThen - you shouldn't use it to return arbitrary Fields, you should only use it with succeed, fail and failAt

import Yafl
import Examples exposing (FormModel, FormMsg, fields)


-- Example 1: Repurposing an existing widget to return
-- a different type


fields.string
        |> Yafl.label "Enter a floating-point number"
        |> Yafl.andThen
            (\string ->
                case String.toFloat string of
                    Just float ->
                        Yafl.succeed float


                    Nothing ->
                        Yafl.fail
                            "That's not a valid float"
            )


--: Yafl.Field FormModel FormMsg Yafl.NoId String String Float


-- Example 2: Validating a field's output
-- (This works, but it's better to use `error`
-- and `errorAt`.)


fields.string
    |> Yafl.label "Enter the first name of a Beatle"
    |> Yafl.andThen
        (\name ->
            if List.member name
                [ "John", "Paul", "George", "Ringo" ]
            then
                Yafl.succeed name


            else
                Yafl.fail "Invalid Beatle"
        )


--: Yafl.Field FormModel FormMsg Yafl.NoId String String String

Building custom types

import Yafl
import Examples exposing (FormModel, FormMsg, fields)


type MyCustomType
    = Foo String
    | Bar Int


myCustomTypeField =
    Yafl.choice
        |> Yafl.option "Foo" .foo fooField
        |> Yafl.option "Bar" .bar barField


fooField =
    fields.string
        |> Yafl.map Foo


barField =
    fields.counter
        |> Yafl.map Bar


model =
    myCustomTypeField
        |> Yafl.init
        |> Tuple.first


Yafl.submit myCustomTypeField model


--> Ok (Foo "")
model
formMsg
Never
Never
{ selected : Maybe Int
, options : Maybe options
}
value

Begin defining a choice between multiple options.

This doesn't do anything useful on its own - it needs to be used in conjunction with option

import Yafl
import Examples exposing (FormModel, FormMsg, fields)


Yafl.choice


--: Yafl.Field FormModel FormMsg Never Never { selected : Maybe Int, options : Maybe {} } Int
String
-> (options -> Maybe input)
-> Field formModel formMsg id widgetMsg input value
-> Field
formModel
formMsg
Never
Never
{ selected : Maybe Int
, options : Maybe options
}
value
-> Field
formModel
formMsg
Never
Never
{ selected : Maybe Int
, options : Maybe options
}
value

Add an option to a choice.

The option will render as an HTML radio input in the view, so you need to provide a String to serve as a label, plus a Field that returns the actual type you want as output.

All the options of a given choice must return the same output type, although their internal model and msg types can be different.

If the user selects the radio button for this option, then the Field's view will be rendered underneath the fieldset containing the radio buttons.

import Yafl
import Examples exposing (FormModel, FormMsg, fields)


Yafl.choice
    |> Yafl.option
        "This is the label for the radio button"
        .counter
        (fields.counter
            |> Yafl.label "This is a label for the `counter` field"
        )


--: Yafl.Field FormModel FormMsg Never Never { options : Maybe { counter : Maybe Examples.CounterMsg }, selected : Maybe Int } Int

Customizing Fields

Back to top

String
-> Field formModel formMsg id widgetMsg input output
-> Field formModel formMsg id widgetMsg input output

Add a label to a Field.

import Yafl
import Examples exposing (FormModel, FormMsg, fields)




fields.string
    |> Yafl.label "What is your name?"


--: Yafl.Field FormModel FormMsg Yafl.NoId String String String
Html Never
-> Field formModel formMsg id widgetMsg input output
-> Field formModel formMsg id widgetMsg input output

Add some arbitrary HTML before the view of a field.

import Examples exposing (FormModel, FormMsg, fields)
import Html
import Yafl


form =
    fields.string
        |> Yafl.htmlBefore (Html.text "Here's some text")


form --: Yafl.Field FormModel FormMsg Yafl.NoId String String String
Html Never
-> Field formModel formMsg id widgetMsg input output
-> Field formModel formMsg id widgetMsg input output

Add some arbitrary HTML after the view of a field.

import Examples exposing (FormModel, FormMsg, fields)
import Html
import Yafl


form =
    fields.string
        |> Yafl.htmlAfter (Html.text "Here's some text")


form --: Yafl.Field FormModel FormMsg Yafl.NoId String String String

Validating fields

Back to top

(output -> Maybe String)
-> Field formModel formMsg id widgetMsg input output
-> Field formModel formMsg id widgetMsg input output

Validate a Field and specify an error message if validation fails.

import Yafl


form =
    Yafl.succeed 0
        |> Yafl.error
            (\int ->
                if int > 0 then
                    Nothing
                else
                    Just
                        ("Must be greater than 0, but the value is "
                            ++ String.fromInt int
                        )
            )


form
    |> Yafl.init
    |> Tuple.first
    |> Yafl.submit form


--> Err [ ( "0", "Must be greater than 0, but the value is 0" ) ]
Field formModel formMsg HasId widgetMsg2 input2 output2
-> (output -> Maybe String)
-> Field formModel formMsg id widgetMsg input output
-> Field formModel formMsg id widgetMsg input output

Validate a Field and specify an error to display on a different field. This is useful when you are doing validation that involves multiple fields, but you only want to display an error on one field.

import Yafl
import Examples exposing (fields)


passwordField =
    fields.string
        |> Yafl.identifier "password"


confirmField =
    fields.string
        |> Yafl.identifier "confirm"


form =
    Yafl.succeed
        (\password confirm -> { password = password, confirm = confirm })
        |> Yafl.andMap .passwordField passwordField
        |> Yafl.andMap .confirmField confirmField
        |> Yafl.errorAt confirmField
            (\{ password, confirm } ->
                if password == confirm then
                    Nothing
                else
                    Just "Passwords do not match"
            )


form
    |> Yafl.init
    |> Tuple.first
    |> Yafl.load form
        { passwordField = Just "password123"
        , confirmField = Just "password124"
        }
    |> Tuple.first
    |> Yafl.submit form


--> Err [ ( "confirm", "Passwords do not match" ) ]
(output -> Maybe String)
-> Field formModel formMsg id widgetMsg input output
-> Field formModel formMsg id widgetMsg input output

Validate a Field and specify a warning message if validation fails. A warning is like an error, but if you call submit on the Field, it will return Ok.

import Yafl


form =
    Yafl.succeed 0
        |> Yafl.warning
            (\int ->
                if int > 0 then
                    Nothing
                else
                    Just
                        ("Should be greater than 0, but the value is "
                            ++ String.fromInt int
                        )
            )


form
    |> Yafl.init
    |> Tuple.first
    --  since we used `warning` instead of `error`,
    --  submitting the form will succeed with an `Ok`
    |> Yafl.submit form


--> Ok 0
Field formModel formMsg HasId widgetMsg2 input2 output2
-> (output -> Maybe String)
-> Field formModel formMsg id widgetMsg input output
-> Field formModel formMsg id widgetMsg input output

Validate a Field and specify a warning to display on a different Field. This is useful when you are doing validation that involves multiple fields, but you only want to display a warning on one field.

import Examples exposing (fields)
import Yafl


passwordField =
    fields.string
        |> Yafl.identifier "password"


confirmField =
    fields.string
        |> Yafl.identifier "confirm"


form =
    Yafl.succeed
        (\password confirm -> { password = password, confirm = confirm })
        |> Yafl.andMap .passwordField passwordField
        |> Yafl.andMap .confirmField confirmField
        |> Yafl.warningAt confirmField
            (\{ password, confirm } ->
                if password == confirm then
                    Nothing


                else
                    Just "Passwords do not match"
            )


form
    |> Yafl.init
    |> Tuple.first
    |> Yafl.load form
        { passwordField = Just "password123"
        , confirmField = Just "password124"
        }
    |> Tuple.first
    --  since we used `warningAt` instead of `errorAt`,
    --  submitting the form will succeed with an `Ok`
    |> Yafl.submit form


--> Ok { password = "password123", confirm = "password124" }

Communicating between Fields

Back to top

type HasId

Indicates that a Field has been given an identifier, and can therefore be used with intercept, send, etc. See the docs for identifier.

type NoId

Indicates that a Field has not been given an identifier. See the docs for identifier.

String
-> Field formModel formMsg NoId widgetMsg input output
-> Field formModel formMsg HasId widgetMsg input output

Add a unique identifier to a Field, which can be used to send and intercept messages to that Field.

import Yafl
import Examples exposing (FormModel, FormMsg, fields)


myField =
    fields.string


myField


--: Yafl.Field FormModel FormMsg Yafl.NoId String String String


myFieldWithId =
    myField
        |> Yafl.identifier "any-string-as-long-as-it's-unique"


myFieldWithId


--: Yafl.Field FormModel FormMsg Yafl.HasId String String String


Yafl.send myFieldWithId "Hello!"


--: Cmd (Yafl.Msg FormMsg)

This identifier is also used as the id string in ViewConfig, which is passed into the view when the Field is rendered. When defining a Widget, you can use the id field of the ViewConfig to set the Html.Attributes.id of the HTML input.

Field formModel formMsg HasId widgetMsg input fieldOutput
-> Model formModel formOutput
-> Msg formMsg
-> Maybe widgetMsg

Intercept the top-level Msg sent to your form, and if it contains a message sent to the specified field, return that message.

import Yafl
import Examples exposing (CounterMsg, FormModel, FormMsg, fields)


myFieldWithId =
    fields.counter
        |> Yafl.identifier "any-string-as-long-as-it's-unique"


model =
    Yafl.init myFieldWithId
        |> Tuple.first


Yafl.intercept myFieldWithId model


--: Yafl.Msg FormMsg -> Maybe CounterMsg
Field formModel formMsg HasId widgetMsg input output
-> widgetMsg
-> Cmd (Msg formMsg)

Create a Cmd that will send a message to a specific option in a choice Field.

import Yafl
import Examples exposing (FormModel, FormMsg, fields)


myFieldWithId =
    fields.string
        |> Yafl.identifier "any-string-as-long-as-it's-unique"


Yafl.send myFieldWithId "Hello!"


--: Cmd (Yafl.Msg FormMsg)
isFormValid : Field formModel formMsg id widgetMsg input output -> Bool

Check that a form doesn't contain fields with duplicate identifiers.

Debugging

Back to top

(output -> String)
-> Field formModel formMsg id widgetMsg input output
-> Program () (Model formModel output) (Msg formMsg)

Turn a Field into an Elm Program that you can view in your browser with in elm reactor for testing purposes.

This should only be used in development - to help you avoid accidentally deploying it in production, you should pass in Debug.toString as the first argument.

toDOT : (model -> String) -> Model model output -> String

Convert a Model value into a Graphviz DOT String, which you can visualize using a tool such as https://dreampuf.github.io/GraphvizOnline

As the first argument, you should pass in Debug.toString.