Add form state to track whether submit was attempted and expose in form state.

This commit is contained in:
Dillon Kearns 2022-06-14 08:43:08 -07:00
parent cb63a023b6
commit 9dc5a92671
8 changed files with 240 additions and 89 deletions

View File

@ -181,12 +181,13 @@ view maybeUrl sharedModel model app =
app.fetchers
|> List.filterMap
(\pending ->
case FormParser.runOnList pending.payload.fields actionFormDecoder of
( Just (SetQuantity itemId addAmount), _ ) ->
Just ( uuidToString itemId, addAmount )
_ ->
Nothing
-- TODO use the latest FormParser API for this example
--case FormParser.runOnList pending.payload.fields actionFormDecoder of
-- ( Just (SetQuantity itemId addAmount), _ ) ->
-- Just ( uuidToString itemId, addAmount )
--
-- _ ->
Nothing
)
|> Dict.fromList

View File

@ -147,7 +147,7 @@ form =
|> Maybe.withDefault []
errorsView field =
(if field.status == Pages.Form.Blurred || True then
(if field.status == Pages.Form.Blurred then
field
|> errors
|> List.map (\error -> Html.li [] [ Html.text error ])
@ -179,7 +179,21 @@ form =
)
)
|> FormParser.field "name" (Field.text |> Field.required "Required")
|> FormParser.field "description" (Field.text |> Field.required "Required")
|> FormParser.field "description"
(Field.text
|> Field.required "Required"
|> Field.withClientValidation
(\description ->
( Just description
, if (description |> String.length) < 5 then
[ "Description must be at last 5 characters"
]
else
[]
)
)
)
|> FormParser.field "price" (Field.int { invalid = \_ -> "Invalid int" } |> Field.required "Required")
|> FormParser.field "imageUrl" (Field.text |> Field.required "Required")
@ -196,7 +210,7 @@ view maybeUrl sharedModel model app =
pendingCreation =
form
|> FormParser.runNew
(app.pageFormState |> Dict.get "test" |> Maybe.withDefault Dict.empty)
(app.pageFormState |> Dict.get "test" |> Maybe.withDefault Pages.Form.init)
|> .result
|> parseIgnoreErrors
in

View File

@ -167,15 +167,15 @@ form =
, imageUrl = imageUrl.value
}
)
(\info name description price imageUrl ->
(\formState name description price imageUrl ->
let
errors field =
info.errors
formState.errors
|> Dict.get field.name
|> Maybe.withDefault []
errorsView field =
(if field.status == Pages.Form.Blurred || True then
(if formState.submitAttempted then
field
|> errors
|> List.map (\error -> Html.li [] [ Html.text error ])
@ -202,7 +202,15 @@ form =
, fieldView "Description" description
, fieldView "Price" price
, fieldView "Image" imageUrl
, Html.button [] [ Html.text "Update" ]
, Html.button []
[ Html.text
(if formState.isTransitioning then
"Updating..."
else
"Update"
)
]
]
)
)
@ -250,7 +258,7 @@ view maybeUrl sharedModel model app =
pendingCreation =
form
|> FormParser.runNew
(app.pageFormState |> Dict.get "test" |> Maybe.withDefault Dict.empty)
(app.pageFormState |> Dict.get "test" |> Maybe.withDefault Pages.Form.init)
|> .result
|> parseIgnoreErrors
in

View File

@ -80,7 +80,7 @@ update eventObject pageFormState =
previousValue : FormState
previousValue =
previousValue_
|> Maybe.withDefault Dict.empty
|> Maybe.withDefault init
in
previousValue
|> updateForm fieldEvent
@ -100,55 +100,84 @@ setField info pageFormState =
previousValue : FormState
previousValue =
previousValue_
|> Maybe.withDefault Dict.empty
|> Maybe.withDefault init
in
previousValue
|> Dict.update info.name
(\previousFieldValue_ ->
let
previousFieldValue : FieldState
previousFieldValue =
previousFieldValue_
|> Maybe.withDefault { value = "", status = NotVisited }
in
{ previousFieldValue | value = info.value }
|> Just
)
{ previousValue
| fields =
previousValue.fields
|> Dict.update info.name
(\previousFieldValue_ ->
let
previousFieldValue : FieldState
previousFieldValue =
previousFieldValue_
|> Maybe.withDefault { value = "", status = NotVisited }
in
{ previousFieldValue | value = info.value }
|> Just
)
}
|> Just
)
updateForm : FieldEvent -> FormState -> FormState
updateForm fieldEvent formState =
formState
|> Dict.update fieldEvent.name
(\previousValue_ ->
let
previousValue : FieldState
previousValue =
previousValue_
|> Maybe.withDefault { value = fieldEvent.value, status = NotVisited }
in
(case fieldEvent.event of
InputEvent newValue ->
{ previousValue | value = newValue }
{ formState
| fields =
formState.fields
|> Dict.update fieldEvent.name
(\previousValue_ ->
let
previousValue : FieldState
previousValue =
previousValue_
|> Maybe.withDefault { value = fieldEvent.value, status = NotVisited }
in
(case fieldEvent.event of
InputEvent newValue ->
{ previousValue | value = newValue }
FocusEvent ->
{ previousValue | status = previousValue.status |> increaseStatusTo Focused }
FocusEvent ->
{ previousValue | status = previousValue.status |> increaseStatusTo Focused }
BlurEvent ->
{ previousValue | status = previousValue.status |> increaseStatusTo Blurred }
)
|> Just
BlurEvent ->
{ previousValue | status = previousValue.status |> increaseStatusTo Blurred }
)
|> Just
)
}
setSubmitAttempted : String -> PageFormState -> PageFormState
setSubmitAttempted fieldId pageFormState =
pageFormState
|> Dict.update fieldId
(\maybeForm ->
case maybeForm of
Just formState ->
Just { formState | submitAttempted = True }
Nothing ->
Just { init | submitAttempted = True }
)
init : FormState
init =
{ fields = Dict.empty
, submitAttempted = False
}
type alias PageFormState =
Dict String FormState
type alias FormState =
Dict String FieldState
{ fields : Dict String FieldState
, submitAttempted : Bool
}
type alias FieldState =

View File

@ -1,6 +1,7 @@
module Pages.FormParser exposing (..)
import Dict exposing (Dict)
import Dict.Extra
import Html exposing (Html)
import Html.Attributes as Attr
import Html.Lazy
@ -25,18 +26,22 @@ type Parser error decoded
optional : String -> Parser error (Maybe String)
optional name =
(\errors form ->
( Just (form |> Dict.get name |> Maybe.map .value), errors )
( Just (form.fields |> Dict.get name |> Maybe.map .value), errors )
)
|> Parser
init : Form.FormState
init =
Debug.todo ""
{ fields = Dict.empty
, submitAttempted = False
}
type alias Context error =
{ errors : FieldErrors error
, isTransitioning : Bool
, submitAttempted : Bool
}
@ -65,7 +70,7 @@ field name (Field fieldParser) (CombinedParser definitions parseFn toInitialValu
let
--something : ( Maybe parsed, List error )
( maybeParsed, errors ) =
fieldParser.decode (Dict.get name formState |> Maybe.map .value)
fieldParser.decode (Dict.get name formState.fields |> Maybe.map .value)
parsedField : Maybe (ParsedField error parsed)
parsedField =
@ -80,7 +85,7 @@ field name (Field fieldParser) (CombinedParser definitions parseFn toInitialValu
rawField : RawField
rawField =
case formState |> Dict.get name of
case formState.fields |> Dict.get name of
Just info ->
{ name = name
, value = Just info.value
@ -206,6 +211,7 @@ runNew formState (CombinedParser fieldDefinitions parser _) =
{ errors =
parsed.result |> Tuple.second
, isTransitioning = False
, submitAttempted = formState.submitAttempted
}
in
{ result = parsed.result
@ -258,7 +264,8 @@ renderHelper formState (CombinedParser fieldDefinitions parser toInitialValues)
part2 =
formState.pageFormState
|> Dict.get formId
|> Maybe.withDefault Dict.empty
|> Maybe.withDefault init
|> .fields
fullFormState : Dict String Form.FieldState
fullFormState =
@ -270,7 +277,14 @@ renderHelper formState (CombinedParser fieldDefinitions parser toInitialValues)
, view : Context error -> ( List (Html.Attribute (Pages.Msg.Msg msg)), List (Html (Pages.Msg.Msg msg)) )
}
parsed =
parser fullFormState
parser thisFormState
thisFormState : Form.FormState
thisFormState =
formState.pageFormState
|> Dict.get formId
|> Maybe.withDefault Form.init
|> (\state -> { state | fields = fullFormState })
context =
{ errors =
@ -285,6 +299,7 @@ renderHelper formState (CombinedParser fieldDefinitions parser toInitialValues)
Nothing ->
False
, submitAttempted = thisFormState.submitAttempted
}
( formAttributes, children ) =
@ -294,13 +309,51 @@ renderHelper formState (CombinedParser fieldDefinitions parser toInitialValues)
(Form.listeners formId
++ [ -- TODO remove hardcoded method - make it part of the config for the form? Should the default be POST?
Attr.method "POST"
, Pages.Msg.onSubmit
, Pages.Msg.submitIfValid
(\fields ->
case
{ init
| fields =
fields
|> List.map (Tuple.mapSecond (\value -> { value = value, status = Form.NotVisited }))
|> Dict.fromList
}
|> parser
|> .result
|> toResult
of
Ok _ ->
True
Err _ ->
False
)
]
++ formAttributes
)
children
toResult : ( Maybe parsed, FieldErrors error ) -> Result (FieldErrors error) parsed
toResult ( maybeParsed, fieldErrors ) =
let
isEmptyDict : Bool
isEmptyDict =
if Dict.isEmpty fieldErrors then
True
else
fieldErrors
|> Dict.Extra.any (\_ errors -> List.isEmpty errors)
in
case ( maybeParsed, isEmptyDict ) of
( Just parsed, True ) ->
Ok parsed
_ ->
Err fieldErrors
render :
AppContext app data
-> CombinedParser error parsed data (Context error -> view)
@ -319,11 +372,13 @@ render formState (CombinedParser fieldDefinitions parser toInitialValues) =
, view : Context error -> view
}
parsed =
parser
(formState.pageFormState
|> Dict.get formId
|> Maybe.withDefault Dict.empty
)
parser thisFormState
thisFormState : Form.FormState
thisFormState =
formState.pageFormState
|> Dict.get formId
|> Maybe.withDefault Form.init
context =
{ errors =
@ -339,6 +394,7 @@ render formState (CombinedParser fieldDefinitions parser toInitialValues) =
--True
Nothing ->
False
, submitAttempted = thisFormState.submitAttempted
}
in
parsed.view context
@ -398,7 +454,7 @@ withError _ _ =
required : String -> error -> Parser error String
required name error =
(\errors form ->
case form |> Dict.get name |> Maybe.map .value of
case form.fields |> Dict.get name |> Maybe.map .value of
Just "" ->
( Just "", errors |> addError name error )
@ -414,7 +470,7 @@ required name error =
int : String -> error -> Parser error Int
int name error =
(\errors form ->
case form |> Dict.get name |> Maybe.map .value of
case form.fields |> Dict.get name |> Maybe.map .value of
Just "" ->
( Nothing, errors |> addError name error )
@ -543,14 +599,15 @@ run formState (Parser parser) =
parser Dict.empty formState
runOnList : List ( String, String ) -> Parser error decoded -> ( Maybe decoded, Dict String (List error) )
runOnList rawFormData (Parser parser) =
(rawFormData
|> List.map
(Tuple.mapSecond (\value_ -> { value = value_, status = Form.NotVisited }))
|> Dict.fromList
)
|> parser Dict.empty
--runOnList : List ( String, String ) -> Parser error decoded -> ( Maybe decoded, Dict String (List error) )
--runOnList rawFormData (Parser parser) =
-- (rawFormData
-- |> List.map
-- (Tuple.mapSecond (\value_ -> { value = value_, status = Form.NotVisited }))
-- |> Dict.fromList
-- )
-- |> parser Dict.empty
addError : String -> error -> Dict String (List error) -> Dict String (List error)

View File

@ -454,6 +454,30 @@ update config appMsg model =
, Submit fields
)
Pages.Msg.SubmitIfValid fields isValid ->
if isValid then
( { model
| transition =
Just
( -- TODO remove hardcoded number
-1
, Pages.Transition.Submitting fields
)
}
, Submit fields
)
else
( { model
| pageFormState =
model.pageFormState
|> Pages.Form.setSubmitAttempted
-- TODO remove hardcoded fieldId
"test"
}
, NoEffect
)
Pages.Msg.SubmitFetcher fields ->
( model
, SubmitFetcher fields

View File

@ -1,6 +1,7 @@
module Pages.Msg exposing
( Msg(..)
, map, onSubmit, fetcherOnSubmit
, submitIfValid
)
{-|
@ -21,6 +22,7 @@ import Json.Decode
type Msg userMsg
= UserMsg userMsg
| Submit FormDecoder.FormData
| SubmitIfValid FormDecoder.FormData Bool
| SubmitFetcher FormDecoder.FormData
| FormFieldEvent Json.Decode.Value
@ -32,6 +34,13 @@ onSubmit =
|> Html.Attributes.map Submit
{-| -}
submitIfValid : (List ( String, String ) -> Bool) -> Attribute (Msg userMsg)
submitIfValid isValid =
FormDecoder.formDataOnSubmit
|> Html.Attributes.map (\formData -> SubmitIfValid formData (isValid formData.fields))
{-| -}
fetcherOnSubmit : Attribute (Msg userMsg)
fetcherOnSubmit =
@ -49,6 +58,9 @@ map mapFn msg =
Submit info ->
Submit info
SubmitIfValid info isValid ->
SubmitIfValid info isValid
SubmitFetcher info ->
SubmitFetcher info

View File

@ -911,11 +911,13 @@ formParser formParser_ =
--something : ( Maybe decoded, Dict String (List String) )
( maybeDecoded, errors ) =
Pages.FormParser.run
(rawFormData
|> List.map
(Tuple.mapSecond (\value -> { value = value, status = Pages.Form.NotVisited }))
|> Dict.fromList
)
{ fields =
rawFormData
|> List.map
(Tuple.mapSecond (\value -> { value = value, status = Pages.Form.NotVisited }))
|> Dict.fromList
, submitAttempted = False
}
formParser_
in
case ( maybeDecoded, errors |> Dict.toList |> List.NonEmpty.fromList ) of
@ -942,11 +944,13 @@ formParserResult formParser_ =
--something : ( Maybe decoded, Dict String (List String) )
( maybeDecoded, errors ) =
Pages.FormParser.run
(rawFormData
|> List.map
(Tuple.mapSecond (\value -> { value = value, status = Pages.Form.NotVisited }))
|> Dict.fromList
)
{ fields =
rawFormData
|> List.map
(Tuple.mapSecond (\value -> { value = value, status = Pages.Form.NotVisited }))
|> Dict.fromList
, submitAttempted = False
}
formParser_
in
case ( maybeDecoded, errors |> Dict.toList |> List.NonEmpty.fromList ) of
@ -977,17 +981,19 @@ formParserResultNew formParser_ =
let
( maybeDecoded, errors ) =
Pages.FormParser.runNew
(rawFormData
|> List.map
(Tuple.mapSecond
(\value ->
{ value = value
, status = Pages.Form.NotVisited
}
{ fields =
rawFormData
|> List.map
(Tuple.mapSecond
(\value ->
{ value = value
, status = Pages.Form.NotVisited
}
)
)
)
|> Dict.fromList
)
|> Dict.fromList
, submitAttempted = False
}
formParser_
|> .result
in