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 app.fetchers
|> List.filterMap |> List.filterMap
(\pending -> (\pending ->
case FormParser.runOnList pending.payload.fields actionFormDecoder of -- TODO use the latest FormParser API for this example
( Just (SetQuantity itemId addAmount), _ ) -> --case FormParser.runOnList pending.payload.fields actionFormDecoder of
Just ( uuidToString itemId, addAmount ) -- ( Just (SetQuantity itemId addAmount), _ ) ->
-- Just ( uuidToString itemId, addAmount )
_ -> --
Nothing -- _ ->
Nothing
) )
|> Dict.fromList |> Dict.fromList

View File

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

View File

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

View File

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

View File

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

View File

@ -454,6 +454,30 @@ update config appMsg model =
, Submit fields , 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 -> Pages.Msg.SubmitFetcher fields ->
( model ( model
, SubmitFetcher fields , SubmitFetcher fields

View File

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

View File

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