From 9dc5a92671f9f9e56891ab392b94682475534f41 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 14 Jun 2022 08:43:08 -0700 Subject: [PATCH] Add form state to track whether submit was attempted and expose in form state. --- examples/smoothies/app/Route/Index.elm | 13 +-- examples/smoothies/app/Route/New.elm | 20 +++- .../smoothies/app/Route/SmoothieId_/Edit.elm | 18 +++- src/Pages/Form.elm | 95 ++++++++++------ src/Pages/FormParser.elm | 101 ++++++++++++++---- src/Pages/Internal/Platform.elm | 24 +++++ src/Pages/Msg.elm | 12 +++ src/Server/Request.elm | 46 ++++---- 8 files changed, 240 insertions(+), 89 deletions(-) diff --git a/examples/smoothies/app/Route/Index.elm b/examples/smoothies/app/Route/Index.elm index ed81811b..bae4f7d5 100644 --- a/examples/smoothies/app/Route/Index.elm +++ b/examples/smoothies/app/Route/Index.elm @@ -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 diff --git a/examples/smoothies/app/Route/New.elm b/examples/smoothies/app/Route/New.elm index 152afa8d..6c303a61 100644 --- a/examples/smoothies/app/Route/New.elm +++ b/examples/smoothies/app/Route/New.elm @@ -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 diff --git a/examples/smoothies/app/Route/SmoothieId_/Edit.elm b/examples/smoothies/app/Route/SmoothieId_/Edit.elm index 90c67cea..8b546d92 100644 --- a/examples/smoothies/app/Route/SmoothieId_/Edit.elm +++ b/examples/smoothies/app/Route/SmoothieId_/Edit.elm @@ -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 diff --git a/src/Pages/Form.elm b/src/Pages/Form.elm index da32bdf2..956bcfd2 100644 --- a/src/Pages/Form.elm +++ b/src/Pages/Form.elm @@ -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 = diff --git a/src/Pages/FormParser.elm b/src/Pages/FormParser.elm index 6f5f19f6..cd5ce1da 100644 --- a/src/Pages/FormParser.elm +++ b/src/Pages/FormParser.elm @@ -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) diff --git a/src/Pages/Internal/Platform.elm b/src/Pages/Internal/Platform.elm index 6b565ee0..37ff9379 100644 --- a/src/Pages/Internal/Platform.elm +++ b/src/Pages/Internal/Platform.elm @@ -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 diff --git a/src/Pages/Msg.elm b/src/Pages/Msg.elm index cf783e7c..e60a9867 100644 --- a/src/Pages/Msg.elm +++ b/src/Pages/Msg.elm @@ -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 diff --git a/src/Server/Request.elm b/src/Server/Request.elm index 87c1894c..6995e830 100644 --- a/src/Server/Request.elm +++ b/src/Server/Request.elm @@ -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