explore a better transition api

This commit is contained in:
Ryan Haskell-Glatz 2019-10-20 22:15:10 -05:00
parent 329efa7944
commit 8efa0b5ee4
6 changed files with 290 additions and 45 deletions

View File

@ -19,7 +19,8 @@ view =
[ ( "Home", Route.Homepage () ) [ ( "Home", Route.Homepage () )
, ( "Counter", Route.Counter () ) , ( "Counter", Route.Counter () )
, ( "Cats", Route.Random () ) , ( "Cats", Route.Random () )
, ( "User", Route.Users_Slug "alice" )
-- , ( "User", Route.Users_Slug "alice" )
, ( "Settings", Route.Settings (Settings.Account ()) ) , ( "Settings", Route.Settings (Settings.Account ()) )
] ]

View File

@ -8,7 +8,7 @@ import Html.Attributes exposing (href, style)
layout : Application.Layout msg layout : Application.Layout msg
layout = layout =
{ view = view { view = view
, transition = Application.fade 1000 , transition = Application.none
} }

View File

@ -0,0 +1,122 @@
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes as Attr
import Html.Events as Events
import Internals.Utils as Utils
import TransitionStuff
main : Program () Model Msg
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
type alias Model =
{ state : TransitionStuff.State Page
, delay : Int
}
type Page
= Homepage
| About
| Contact
type Msg
= NavigateTo Page
init : flags -> ( Model, Cmd Msg )
init _ =
let
state =
{ visibility = TransitionStuff.Invisible, page = Homepage }
{ cmd, delay } =
TransitionStuff.transition delaysForPage
{ delay = 0
, msg = NavigateTo
, target = Homepage
, state = state
}
in
( { delay = delay, state = state }
, cmd
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NavigateTo target ->
let
state =
TransitionStuff.nextState target model.state
{ cmd, delay } =
TransitionStuff.transition delaysForPage
{ delay = model.delay
, msg = NavigateTo
, target = target
, state = state
}
in
( { model | state = state, delay = delay }
, cmd
)
delaysForPage : Page -> TransitionStuff.Animations
delaysForPage page =
case page of
Homepage ->
{ entering = 200
, exiting = 200
}
About ->
{ entering = 200
, exiting = 200
}
Contact ->
{ entering = 200
, exiting = 200
}
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
view : Model -> Html Msg
view model =
div
[ Attr.style "transition" ("opacity " ++ String.fromInt model.delay ++ "ms ease-in-out")
, Attr.style "opacity"
(if model.state.visibility == TransitionStuff.Visible then
"1"
else
"0"
)
]
[ p [] (List.map viewButton [ Homepage, About, Contact ])
, p [] [ text (Debug.toString model.state.page) ]
]
viewButton : Page -> Html Msg
viewButton page =
button
[ Events.onClick (NavigateTo page) ]
[ text (Debug.toString page) ]

View File

@ -0,0 +1,108 @@
module TransitionStuff exposing
( Animations
, State
, Visibility(..)
, nextState
, outIn
, transition
)
import Internals.Utils as Utils
type Visibility
= Invisible
| Visible
type alias State page =
{ page : page
, visibility : Visibility
}
type alias Animations =
{ entering : Int
, exiting : Int
}
outIn :
{ toAnimations : page -> Animations
, current : State page
, next : State page
}
-> Int
outIn { toAnimations, current, next } =
let
animations =
toAnimations current.page
in
case ( current.visibility, next.visibility ) of
( Visible, Invisible ) ->
0
( Invisible, Invisible ) ->
animations.exiting
( Invisible, Visible ) ->
animations.entering
( Visible, Visible ) ->
0
nextState : page -> State page -> State page
nextState target state =
case state.visibility of
Invisible ->
if state.page == target then
{ visibility = Visible, page = target }
else
{ visibility = Invisible, page = target }
Visible ->
if state.page == target then
state
else
{ visibility = Invisible, page = state.page }
isComplete : State page -> State page -> Bool
isComplete =
(==)
transition :
(page -> Animations)
->
{ delay : Int
, target : page
, state : State page
, msg : page -> msg
}
-> { delay : Int, cmd : Cmd msg }
transition toAnimations { delay, target, state, msg } =
if isComplete state (nextState target state) then
{ delay = delay
, cmd = Cmd.none
}
else
let
nextDelay =
outIn
{ toAnimations = toAnimations
, current = state
, next = nextState target state
}
in
{ delay = nextDelay
, cmd = Utils.delay nextDelay (msg target)
}
-- program stuff

View File

@ -98,15 +98,11 @@ create config =
} }
, subscriptions = , subscriptions =
subscriptions subscriptions
{ subscriptions = { subscriptions = config.pages.bundle >> .subscriptions
\model ->
config.pages.bundle model Page.Initial |> .subscriptions
} }
, view = , view =
view view
{ view = { view = \status model -> (config.pages.bundle model).view status
\model ->
config.pages.bundle model Page.Initial |> .view
, layout = config.layout , layout = config.layout
, transition = Transition.strategy config.layout.transition , transition = Transition.strategy config.layout.transition
} }
@ -120,7 +116,10 @@ create config =
type alias Model flags model = type alias Model flags model =
{ url : Url { urls :
{ previous : Maybe Url
, current : Url
}
, flags : flags , flags : flags
, key : Nav.Key , key : Nav.Key
, page : Transitionable model , page : Transitionable model
@ -143,7 +142,7 @@ init config flags url key =
|> (\route -> config.init route { parentSpeed = config.speed }) |> (\route -> config.init route { parentSpeed = config.speed })
|> (\page -> |> (\page ->
( { flags = flags ( { flags = flags
, url = url , urls = { previous = Nothing, current = url }
, key = key , key = key
, page = Transitionable.Ready page.model , page = Transitionable.Ready page.model
, speed = page.speed , speed = page.speed
@ -206,18 +205,18 @@ update config msg model =
( model, Cmd.none ) ( model, Cmd.none )
Link (Browser.Internal url) -> Link (Browser.Internal url) ->
if url == model.url && url.fragment == Nothing then if url == model.urls.current && url.fragment == Nothing then
( model, Cmd.none ) ( model, Cmd.none )
else if url.path == model.url.path then else if url.path == model.urls.current.path then
( model, Nav.load (Url.toString url) ) ( model, Nav.load (Url.toString url) )
else else
( if navigatingToNewLayout { old = model.url, new = url } then ( if navigatingWithinLayout { old = model.urls.current, new = url } then
{ model | page = Transitionable.begin model.page } { model | page = Transitionable.complete model.page }
else else
{ model | page = Transitionable.complete model.page } { model | page = Transitionable.begin model.page }
, Utils.delay model.speed (TransitionTo url) , Utils.delay model.speed (TransitionTo url)
) )
@ -242,7 +241,10 @@ update config msg model =
|> (\route -> config.init route { parentSpeed = config.speed }) |> (\route -> config.init route { parentSpeed = config.speed })
|> (\page -> |> (\page ->
( { model ( { model
| url = url | urls =
{ previous = Just model.urls.current
, current = url
}
, page = Transitionable.Complete page.model , page = Transitionable.Complete page.model
, speed = page.speed , speed = page.speed
} }
@ -257,8 +259,8 @@ update config msg model =
(config.update pageMsg (Transitionable.unwrap model.page)) (config.update pageMsg (Transitionable.unwrap model.page))
navigatingToNewLayout : { old : Url, new : Url } -> Bool navigatingWithinLayout : { old : Url, new : Url } -> Bool
navigatingToNewLayout urls = navigatingWithinLayout urls =
let let
firstSegment { path } = firstSegment { path } =
String.split "/" path |> List.drop 1 |> List.head String.split "/" path |> List.drop 1 |> List.head
@ -269,7 +271,7 @@ navigatingToNewLayout urls =
new = new =
firstSegment urls.new firstSegment urls.new
in in
old /= new || old == Nothing old == new && old /= Nothing
@ -289,13 +291,26 @@ subscriptions config model =
view : view :
{ view : model -> Html msg { view : Page.TransitionStatus -> model -> Html msg
, transition : Transition.Strategy (Html msg) , transition : Transition.Strategy (Html msg)
, layout : Layout msg , layout : Layout msg
} }
-> Model flags model -> Model flags model
-> Browser.Document (Msg msg) -> Browser.Document (Msg msg)
view config model = view config model =
let
isNavigatingWithinLayout =
model.urls.previous
|> Maybe.map (\previous -> navigatingWithinLayout { old = previous, new = model.urls.current })
|> Maybe.withDefault False
status =
if isNavigatingWithinLayout then
Page.Leaving
else
Page.Complete
in
{ title = "elm-app demo" { title = "elm-app demo"
, body = , body =
[ Html.map Page <| [ Html.map Page <|
@ -303,19 +318,19 @@ view config model =
Transitionable.Ready page -> Transitionable.Ready page ->
config.transition.beforeLoad config.transition.beforeLoad
{ layout = config.layout.view { layout = config.layout.view
, page = config.view page , page = config.view status page
} }
Transitionable.Transitioning page -> Transitionable.Transitioning page ->
config.transition.leavingPage config.transition.leavingPage
{ layout = config.layout.view { layout = config.layout.view
, page = config.view page , page = config.view status page
} }
Transitionable.Complete page -> Transitionable.Complete page ->
config.transition.enteringPage config.transition.enteringPage
{ layout = config.layout.view { layout = config.layout.view
, page = config.view page , page = config.view status page
} }
] ]
} }

View File

@ -46,7 +46,7 @@ type alias Recipe params pageModel pageMsg model msg =
type TransitionStatus type TransitionStatus
= Initial = Initial
| Leaving | Leaving
| Entering | Complete
type alias Init model msg = type alias Init model msg =
@ -60,11 +60,9 @@ type alias Init model msg =
type alias Bundle msg = type alias Bundle msg =
TransitionStatus { view : TransitionStatus -> Html msg
-> , subscriptions : Sub msg
{ view : Html msg }
, subscriptions : Sub msg
}
@ -88,8 +86,8 @@ static page { toModel, toMsg } =
} }
, update = \_ model -> ( toModel model, Cmd.none ) , update = \_ model -> ( toModel model, Cmd.none )
, bundle = , bundle =
\_ _ -> \_ ->
{ view = Html.map toMsg page.view { view = \_ -> Html.map toMsg page.view
, subscriptions = Sub.none , subscriptions = Sub.none
} }
} }
@ -122,8 +120,8 @@ sandbox page { toModel, toMsg } =
, Cmd.none , Cmd.none
) )
, bundle = , bundle =
\model _ -> \model ->
{ view = page.view model |> Html.map toMsg { view = \_ -> page.view model |> Html.map toMsg
, subscriptions = Sub.none , subscriptions = Sub.none
} }
} }
@ -159,8 +157,8 @@ element page { toModel, toMsg } =
page.update msg model page.update msg model
|> Tuple.mapBoth toModel (Cmd.map toMsg) |> Tuple.mapBoth toModel (Cmd.map toMsg)
, bundle = , bundle =
\model _ -> \model ->
{ view = page.view model |> Html.map toMsg { view = \_ -> page.view model |> Html.map toMsg
, subscriptions = Sub.none , subscriptions = Sub.none
} }
} }
@ -201,20 +199,21 @@ glue options { toModel, toMsg } =
options.pages.update msg model options.pages.update msg model
|> Tuple.mapBoth toModel (Cmd.map toMsg) |> Tuple.mapBoth toModel (Cmd.map toMsg)
, bundle = , bundle =
\model status -> \model ->
let let
page = page =
options.pages.bundle model status options.pages.bundle model
in in
{ view = { view =
options.layout.view \status ->
{ page = options.layout.view
div [] { page =
[ text (Debug.toString status) div []
, page.view [ text (Debug.toString status)
] , page.view status
} ]
|> Html.map toMsg }
|> Html.map toMsg
, subscriptions = Sub.none , subscriptions = Sub.none
} }
} }