Update todo example.

This commit is contained in:
Dillon Kearns 2023-05-16 10:55:54 -07:00
parent c45269f3a2
commit 05845559fd
5 changed files with 111 additions and 84 deletions

View File

@ -7,10 +7,10 @@ module Effect exposing (Effect(..), none, batch, fromCmd, map, perform)
-} -}
import Browser.Navigation import Browser.Navigation
import Form.FormData exposing (FormData)
import Http import Http
import Json.Decode as Decode import Json.Decode as Decode
import Pages.Fetcher import Pages.Fetcher
import Pages.ProgramConfig exposing (FormData)
import Url exposing (Url) import Url exposing (Url)

View File

@ -11,7 +11,8 @@ import FatalError exposing (FatalError)
import Form import Form
import Form.Field as Field import Form.Field as Field
import Form.FieldView import Form.FieldView
import Form.Validation as Validation exposing (Combined, Field) import Form.Handler
import Form.Validation as Validation exposing (Field, Validation)
import Head import Head
import Head.Seo as Seo import Head.Seo as Seo
import Html exposing (Html) import Html exposing (Html)
@ -20,6 +21,7 @@ import Json.Decode as Decode
import Json.Encode as Encode import Json.Encode as Encode
import List.Nonempty import List.Nonempty
import MySession import MySession
import Pages.Form
import Pages.Script as Script import Pages.Script as Script
import Pages.Url import Pages.Url
import PagesMsg exposing (PagesMsg) import PagesMsg exposing (PagesMsg)
@ -86,9 +88,9 @@ type alias EnvVariables =
} }
form : Form.DoneForm String (BackendTask FatalError (Combined String EmailAddress)) data (List (Html (PagesMsg Msg))) Msg form : Pages.Form.FormWithServerValidations String EmailAddress data (List (Html (PagesMsg Msg)))
form = form =
Form.init Form.form
(\fieldEmail -> (\fieldEmail ->
{ combine = { combine =
Validation.succeed Validation.succeed
@ -119,7 +121,7 @@ form =
[ fieldEmail |> fieldView info "Email" [ fieldEmail |> fieldView info "Email"
, globalErrors info , globalErrors info
, Html.button [] , Html.button []
[ if info.isTransitioning then [ if info.submitting then
Html.text "Logging in..." Html.text "Logging in..."
else else
@ -132,15 +134,15 @@ form =
|> Form.hiddenKind ( "kind", "login" ) "Expected kind" |> Form.hiddenKind ( "kind", "login" ) "Expected kind"
logoutForm : Form.DoneForm String () data (List (Html (PagesMsg Msg))) Msg logoutForm : Form.HtmlForm String () data msg
logoutForm = logoutForm =
Form.init Form.form
{ combine = { combine =
Validation.succeed () Validation.succeed ()
, view = , view =
\info -> \info ->
[ Html.button [] [ Html.button []
[ if info.isTransitioning then [ if info.submitting then
Html.text "Logging out..." Html.text "Logging out..."
else else
@ -287,21 +289,21 @@ data routeParams =
) )
allForms : Form.ServerForms String (BackendTask FatalError (Combined String Action)) allForms : Pages.Form.Handler String EmailAddress
allForms = allForms =
logoutForm Form.Handler.init identity form
|> Form.toServerForm
|> Form.initCombinedServer (\_ -> Logout)
|> Form.combineServer LogIn form
--logoutForm
-- |> Form.Handler.init (\_ -> Logout)
-- |> Form.Handler.with LogIn form
action : RouteParams -> Request.Parser (BackendTask FatalError (Response ActionData ErrorPage)) action : RouteParams -> Request.Parser (BackendTask FatalError (Response ActionData ErrorPage))
action routeParams = action routeParams =
Request.map2 Tuple.pair Request.map2 Tuple.pair
(Request.oneOf (Request.formDataWithServerValidation allForms)
[ Request.formDataWithServerValidation allForms
]
)
Request.requestTime Request.requestTime
|> MySession.withSession |> MySession.withSession
(\( resolveFormBackendTask, requestTime ) session -> (\( resolveFormBackendTask, requestTime ) session ->
@ -323,13 +325,13 @@ action routeParams =
) )
|> BackendTask.succeed |> BackendTask.succeed
Ok ( _, Logout ) -> --Ok ( _, Logout ) ->
( Session.empty -- ( Session.empty
, Route.redirectTo Route.Login -- , Route.redirectTo Route.Login
) -- )
|> BackendTask.succeed -- |> BackendTask.succeed
--
Ok ( _, LogIn emailAddress ) -> Ok ( _, emailAddress ) ->
( okSession ( okSession
, { maybeError = Nothing , { maybeError = Nothing
, sentLink = True , sentLink = True
@ -372,7 +374,7 @@ type alias Data =
type alias ActionData = type alias ActionData =
{ maybeError : Maybe (Form.Response String) { maybeError : Maybe (Form.ServerResponse String)
, sentLink : Bool , sentLink : Bool
} }
@ -395,19 +397,22 @@ view app shared =
Html.div [] Html.div []
[ Html.text <| "Hello! You are already logged in as " ++ username [ Html.text <| "Hello! You are already logged in as " ++ username
, logoutForm , logoutForm
|> Form.toDynamicTransition |> Pages.Form.renderHtml []
|> Form.renderHtml "logout" Pages.Form.Parallel
[] (Form.options "logout")
(\_ -> Nothing)
app app
()
] ]
Nothing -> Nothing ->
Html.text "You aren't logged in yet." Html.text "You aren't logged in yet."
] ]
, form , form
|> Form.renderHtml "login" [] .maybeError app () |> Pages.Form.renderHtml []
Pages.Form.Serial
(Form.options "login"
|> Form.withServerResponse (app.action |> Maybe.andThen .maybeError)
)
app
] ]
] ]
} }

View File

@ -20,6 +20,7 @@ import Json.Decode as Decode
import Json.Encode as Encode import Json.Encode as Encode
import LoadingSpinner import LoadingSpinner
import MySession import MySession
import Pages.Form
import Pages.Transition exposing (FetcherSubmitStatus(..)) import Pages.Transition exposing (FetcherSubmitStatus(..))
import PagesMsg exposing (PagesMsg) import PagesMsg exposing (PagesMsg)
import Route import Route
@ -290,12 +291,12 @@ action routeParams =
|> Result.withDefault Session.empty |> Result.withDefault Session.empty
in in
case formResult of case formResult of
Ok actionInput -> Form.Valid actionInput ->
(okSession |> Session.get "sessionId" |> Maybe.withDefault "") (okSession |> Session.get "sessionId" |> Maybe.withDefault "")
|> performAction requestTime actionInput |> performAction requestTime actionInput
|> BackendTask.map (Tuple.pair okSession) |> BackendTask.map (Tuple.pair okSession)
Err _ -> Form.Invalid _ _ ->
BackendTask.succeed ( okSession, Response.render { errors = Nothing } ) BackendTask.succeed ( okSession, Response.render { errors = Nothing } )
) )
@ -353,7 +354,8 @@ view app shared model =
_ -> _ ->
allForms allForms
|> Form.Handler.run payload.fields |> Form.Handler.run payload.fields
|> Tuple.first |> Form.toResult
|> Result.toMaybe
) )
creatingItems : List Entry creatingItems : List Entry
@ -472,11 +474,10 @@ view app shared model =
case case
( allForms ( allForms
|> Form.Handler.run payload.fields |> Form.Handler.run payload.fields
|> Tuple.first
, status , status
) )
of of
( Just (Add newItem), Pages.Transition.FetcherComplete (Just parsedActionData) ) -> ( Form.Valid (Add newItem), Pages.Transition.FetcherComplete (Just parsedActionData) ) ->
parsedActionData.errors parsedActionData.errors
|> Maybe.map (Tuple.pair key) |> Maybe.map (Tuple.pair key)
@ -492,30 +493,32 @@ view app shared model =
[ section [ section
[ class "todoapp" ] [ class "todoapp" ]
[ addItemForm [ addItemForm
|> Form.toDynamicFetcher |> Pages.Form.renderHtml
|> Form.withOnSubmit (\_ -> NewItemSubmitted)
|> Form.renderHtml
("new-item-"
++ (model.nextId |> Time.posixToMillis |> String.fromInt)
)
[ class "create-form" [ class "create-form"
, hidden (not (List.isEmpty failedAddItemActions)) , hidden (not (List.isEmpty failedAddItemActions))
] ]
(\_ -> Nothing) Pages.Form.Parallel
(Form.options
("new-item-"
++ (model.nextId |> Time.posixToMillis |> String.fromInt)
)
|> Form.withInput Nothing
|> Form.withOnSubmit (\_ -> NewItemSubmitted)
)
app app
Nothing
, div [] , div []
(failedAddItemActions (failedAddItemActions
|> List.indexedMap |> List.indexedMap
(\index ( key, createFetcherErrors ) -> (\index ( key, createFetcherErrors ) ->
addItemForm addItemForm
|> Form.toDynamicFetcher |> Pages.Form.renderHtml
|> Form.withOnSubmit (\_ -> NewItemSubmitted)
|> Form.renderHtml key
[ class "create-form", hidden (index /= 0) ] [ class "create-form", hidden (index /= 0) ]
(\_ -> Nothing) Pages.Form.Parallel
(Form.options key
|> Form.withOnSubmit (\_ -> NewItemSubmitted)
|> Form.withInput (Just createFetcherErrors)
)
app app
(Just createFetcherErrors)
) )
) )
, viewEntries app optimisticVisibility optimisticEntities , viewEntries app optimisticVisibility optimisticEntities
@ -542,7 +545,7 @@ allForms =
|> Form.Handler.with CheckAll toggleAllForm |> Form.Handler.with CheckAll toggleAllForm
addItemForm : Form.HtmlForm String String (Maybe String) Msg addItemForm : Form.HtmlForm String String (Maybe String) msg
addItemForm = addItemForm =
Form.form Form.form
(\description -> (\description ->
@ -560,7 +563,7 @@ addItemForm =
, autofocus True , autofocus True
] ]
description description
, formState.data |> Maybe.map (\error -> Html.div [ class "error", id "new-todo-error" ] [ text error ]) |> Maybe.withDefault (text "") , formState.input |> Maybe.map (\error -> Html.div [ class "error", id "new-todo-error" ] [ text error ]) |> Maybe.withDefault (text "")
] ]
] ]
} }
@ -569,7 +572,7 @@ addItemForm =
|> Form.hiddenKind ( "kind", "new-item" ) "Expected kind" |> Form.hiddenKind ( "kind", "new-item" ) "Expected kind"
editItemForm : Form.HtmlForm String ( String, String ) Entry Msg editItemForm : Form.HtmlForm String ( String, String ) Entry msg
editItemForm = editItemForm =
Form.form Form.form
(\itemId description -> (\itemId description ->
@ -582,7 +585,7 @@ editItemForm =
[ FieldView.input [ FieldView.input
[ class "edit-input" [ class "edit-input"
, name "title" , name "title"
, id ("todo-" ++ formState.data.id) , id ("todo-" ++ formState.input.id)
] ]
description description
] ]
@ -590,18 +593,18 @@ editItemForm =
) )
|> Form.hiddenField "itemId" |> Form.hiddenField "itemId"
(Field.text (Field.text
|> Field.withInitialValue (.id >> Form.Value.string) |> Field.withInitialValue .id
|> Field.required "Must be present" |> Field.required "Must be present"
) )
|> Form.field "description" |> Form.field "description"
(Field.text (Field.text
|> Field.withInitialValue (.description >> Form.Value.string) |> Field.withInitialValue .description
|> Field.required "Must be present" |> Field.required "Must be present"
) )
|> Form.hiddenKind ( "kind", "edit-item" ) "Expected kind" |> Form.hiddenKind ( "kind", "edit-item" ) "Expected kind"
deleteItemForm : Form.HtmlForm String String Entry Msg deleteItemForm : Form.HtmlForm String String Entry msg
deleteItemForm = deleteItemForm =
Form.form Form.form
(\todoId -> (\todoId ->
@ -616,12 +619,12 @@ deleteItemForm =
|> Form.hiddenField "todoId" |> Form.hiddenField "todoId"
(Field.text (Field.text
|> Field.required "Must be present" |> Field.required "Must be present"
|> Field.withInitialValue (.id >> Form.Value.string) |> Field.withInitialValue .id
) )
|> Form.hiddenKind ( "kind", "delete" ) "Expected kind" |> Form.hiddenKind ( "kind", "delete" ) "Expected kind"
toggleAllForm : Form.HtmlForm String Bool { allCompleted : Bool } Msg toggleAllForm : Form.HtmlForm String Bool { allCompleted : Bool } msg
toggleAllForm = toggleAllForm =
Form.form Form.form
(\toggleTo -> (\toggleTo ->
@ -634,7 +637,7 @@ toggleAllForm =
[ classList [ classList
[ ( "toggle-all", True ) [ ( "toggle-all", True )
, ( "toggle", True ) , ( "toggle", True )
, ( "checked", formState.data.allCompleted ) , ( "checked", formState.input.allCompleted )
] ]
] ]
[ text "" ] [ text "" ]
@ -646,15 +649,12 @@ toggleAllForm =
) )
|> Form.hiddenField "toggleTo" |> Form.hiddenField "toggleTo"
(Field.checkbox (Field.checkbox
|> Field.withInitialValue |> Field.withInitialValue (not << .allCompleted)
(\{ allCompleted } ->
Form.Value.bool (not allCompleted)
)
) )
|> Form.hiddenKind ( "kind", "toggle-all" ) "Expected kind" |> Form.hiddenKind ( "kind", "toggle-all" ) "Expected kind"
checkItemForm : Form.HtmlForm String ( Bool, String ) Entry Msg checkItemForm : Form.HtmlForm String ( Bool, String ) Entry msg
checkItemForm = checkItemForm =
Form.form Form.form
(\todoId complete -> (\todoId complete ->
@ -665,7 +665,7 @@ checkItemForm =
, view = , view =
\formState -> \formState ->
[ Html.button [ class "toggle" ] [ Html.button [ class "toggle" ]
[ if formState.data.completed then [ if formState.input.completed then
Icon.complete Icon.complete
else else
@ -677,16 +677,16 @@ checkItemForm =
|> Form.hiddenField "todoId" |> Form.hiddenField "todoId"
(Field.text (Field.text
|> Field.required "Must be present" |> Field.required "Must be present"
|> Field.withInitialValue (.id >> Form.Value.string) |> Field.withInitialValue .id
) )
|> Form.hiddenField "complete" |> Form.hiddenField "complete"
(Field.checkbox (Field.checkbox
|> Field.withInitialValue (.completed >> not >> Form.Value.bool) |> Field.withInitialValue (.completed >> not)
) )
|> Form.hiddenKind ( "kind", "complete" ) "Expected kind" |> Form.hiddenKind ( "kind", "complete" ) "Expected kind"
clearCompletedForm : Form.HtmlForm String () { entriesCompleted : Int } Msg clearCompletedForm : Form.HtmlForm String () { entriesCompleted : Int } msg
clearCompletedForm = clearCompletedForm =
Form.form Form.form
{ combine = Validation.succeed () { combine = Validation.succeed ()
@ -694,9 +694,9 @@ clearCompletedForm =
\formState -> \formState ->
[ button [ button
[ class "clear-completed" [ class "clear-completed"
, hidden (formState.data.entriesCompleted == 0) , hidden (formState.input.entriesCompleted == 0)
] ]
[ text ("Clear completed (" ++ String.fromInt formState.data.entriesCompleted ++ ")") [ text ("Clear completed (" ++ String.fromInt formState.input.entriesCompleted ++ ")")
] ]
] ]
} }
@ -736,8 +736,12 @@ viewEntries app visibility entries =
, style "visibility" cssVisibility , style "visibility" cssVisibility
] ]
[ toggleAllForm [ toggleAllForm
|> Form.toDynamicFetcher |> Pages.Form.renderHtml []
|> Form.renderHtml "toggle-all" [] (\_ -> Nothing) app { allCompleted = allCompleted } Pages.Form.Parallel
(Form.options "toggle-all"
|> Form.withInput { allCompleted = allCompleted }
)
app
, Keyed.ul [ class "todo-list" ] <| , Keyed.ul [ class "todo-list" ] <|
List.map (viewKeyedEntry app) (List.filter isVisible entries) List.map (viewKeyedEntry app) (List.filter isVisible entries)
] ]
@ -762,18 +766,31 @@ viewEntry app todo =
[ div [ div
[ class "view" ] [ class "view" ]
[ checkItemForm [ checkItemForm
|> Form.toDynamicFetcher |> Pages.Form.renderHtml []
|> Form.renderHtml ("toggle-" ++ todo.id) [] (\_ -> Nothing) app todo Pages.Form.Parallel
(("toggle-" ++ todo.id)
|> Form.options
|> Form.withInput todo
)
app
, editItemForm , editItemForm
|> Form.toDynamicFetcher |> Pages.Form.renderHtml []
|> Form.renderHtml ("edit-" ++ todo.id) [] (\_ -> Nothing) app todo Pages.Form.Parallel
(Form.options ("edit-" ++ todo.id)
|> Form.withInput todo
)
app
, if todo.isSaving then , if todo.isSaving then
LoadingSpinner.view LoadingSpinner.view
else else
deleteItemForm deleteItemForm
|> Form.toDynamicFetcher |> Pages.Form.renderHtml []
|> Form.renderHtml ("delete-" ++ todo.id) [] (\_ -> Nothing) app todo Pages.Form.Parallel
(Form.options ("delete-" ++ todo.id)
|> Form.withInput todo
)
app
] ]
] ]
@ -863,8 +880,12 @@ visibilitySwap visibilityParam visibility actualVisibility =
viewControlsClear : App Data ActionData RouteParams -> Int -> Html (PagesMsg Msg) viewControlsClear : App Data ActionData RouteParams -> Int -> Html (PagesMsg Msg)
viewControlsClear app entriesCompleted = viewControlsClear app entriesCompleted =
clearCompletedForm clearCompletedForm
|> Form.toDynamicFetcher |> Pages.Form.renderHtml []
|> Form.renderHtml "clear-completed" [] (\_ -> Nothing) app { entriesCompleted = entriesCompleted } Pages.Form.Parallel
(Form.options "clear-completed"
|> Form.withInput { entriesCompleted = entriesCompleted }
)
app
infoFooter : Html msg infoFooter : Html msg

View File

@ -26,7 +26,7 @@ withSession =
Session.withSessionResult Session.withSessionResult
{ name = "mysession" { name = "mysession"
, secrets = secrets , secrets = secrets
, options = cookieOptions , options = Just cookieOptions
} }
@ -38,7 +38,7 @@ withSessionOrRedirect toRequest handler =
Session.withSessionResult Session.withSessionResult
{ name = "mysession" { name = "mysession"
, secrets = secrets , secrets = secrets
, options = cookieOptions , options = Just cookieOptions
} }
(\request sessionResult -> (\request sessionResult ->
sessionResult sessionResult
@ -68,7 +68,7 @@ expectSessionOrRedirect toRequest handler =
Session.withSessionResult Session.withSessionResult
{ name = "mysession" { name = "mysession"
, secrets = secrets , secrets = secrets
, options = cookieOptions , options = Just cookieOptions
} }
(\request sessionResult -> (\request sessionResult ->
sessionResult sessionResult

View File

@ -98,6 +98,7 @@ import Internal.Request
import Json.Decode import Json.Decode
import Json.Encode import Json.Encode
import List.NonEmpty import List.NonEmpty
import Pages.Form
import QueryParams import QueryParams
import Time import Time
import Url import Url
@ -891,7 +892,7 @@ runForm validation =
{-| -} {-| -}
formDataWithServerValidation : formDataWithServerValidation :
Form.Handler.Handler error (BackendTask FatalError (Validation.Validation error combined kind constraints)) Pages.Form.Handler error combined
-> Parser (BackendTask FatalError (Result (Form.ServerResponse error) ( Form.ServerResponse error, combined ))) -> Parser (BackendTask FatalError (Result (Form.ServerResponse error) ( Form.ServerResponse error, combined )))
formDataWithServerValidation formParsers = formDataWithServerValidation formParsers =
rawFormData rawFormData