module Routes exposing (..) {-| This handles all the routing rules and delegates down to the pages. -} import Common exposing (..) import Ports import Html exposing (Html) import Http import Navigation as Nav exposing (Location) import Pages.Joke as JokePage import Pages.Front as FrontPage import Maybe.Extra as Maybe import RemoteData exposing (WebData, RemoteData(..)) import Task exposing (Task) type alias Pages = { front : Maybe FrontPage.Model , joke : Maybe JokePage.Model } type alias Model = { currentRoute : Route , nextRouteLoading : Bool , pages : Pages } type Msg = FetchRoute Route | FetchedRoute Route (WebData Pages) | ServeRoute Route (Maybe Pages) | IgnoreCommandResults | PageMsg PageMsg type PageMsg = IgnorePageMsg | FrontMsg FrontPage.Msg | JokeMsg JokePage.Msg {-| This is the functionality that a page is required to implement. -} type alias PageConfig msg model = { tagger : msg -> PageMsg -- tag page messages as route mssages , untagger : PageMsg -> Maybe msg -- unwrap tag messages, returning Nothing if it is not a tag message , getter : Pages -> Maybe model -- get the current model (or nothing if there is not one yet) , setter : model -> Pages -> Pages -- Update pages with a new page , updater : GlobalModel -> msg -> model -> ( GlobalModel, model, Cmd msg ) -- perform an update , viewer : GlobalModel -> model -> Html msg -- render the page to HTML , subber : model -> Sub msg -- get the subscriptions for the page , fetcher : GlobalModel -> Task Http.Error model -- fetch the initial model, which will be passed to updater , initMsg : msg -- The message that will be passed to the updater with the initial model } {-| This is what we wrap a PageConfig in, so that it is a consistent API across pages. -} type alias RouteConfig = { updater : GlobalModel -> PageMsg -> Model -> ( GlobalModel, Model, Cmd Msg ) , render : GlobalModel -> Pages -> Html Msg , subber : Pages -> Sub Msg , fetcher : GlobalModel -> Pages -> Task Http.Error Pages , init : GlobalModel -> Pages -> ( GlobalModel, Pages, Cmd Msg ) } initialPageConfig : PageConfig () () initialPageConfig = { tagger = const IgnorePageMsg , untagger = const Nothing , getter = const (Just ()) , setter = const identity , updater = (\global _ model -> ( global, model, Cmd.none )) , viewer = const2 <| Html.text "Loading..." , subber = const Sub.none , fetcher = const <| Task.succeed () , initMsg = () } frontPageConfig : PageConfig FrontPage.Msg FrontPage.Model frontPageConfig = { tagger = FrontMsg , untagger = \msg -> case msg of FrontMsg msg -> Just msg _ -> Nothing , getter = .front , setter = (\model pages -> { pages | front = Just model }) , updater = FrontPage.update , viewer = const FrontPage.view , subber = FrontPage.subscriptions , initMsg = FrontPage.Init , fetcher = const FrontPage.fetch } jokePageConfig : PageConfig JokePage.Msg JokePage.Model jokePageConfig = { tagger = JokeMsg , untagger = \msg -> case msg of JokeMsg msg -> Just msg _ -> Nothing , getter = .joke , setter = (\model pages -> { pages | joke = Just model }) , updater = JokePage.update , viewer = const JokePage.view , subber = JokePage.subscriptions , initMsg = JokePage.Init , fetcher = const JokePage.fetch } routeConfig : Route -> RouteConfig routeConfig route = let makeSubber : PageConfig msg model -> Pages -> Sub Msg makeSubber { tagger, subber, getter } pages = Sub.map PageMsg <| Sub.map tagger <| Maybe.unwrap Sub.none subber <| getter pages makeUpdater : PageConfig msg model -> GlobalModel -> PageMsg -> Model -> ( GlobalModel, Model, Cmd Msg ) makeUpdater { untagger, updater, getter, setter, tagger } = \globalModel msg model -> case ( untagger msg, getter model.pages ) of ( Nothing, _ ) -> ( globalModel, model, Cmd.none ) ( _, Nothing ) -> ( globalModel, model, Cmd.none ) ( Just msg, Just page ) -> let ( newGlobal, newPage, cmds ) = updater globalModel msg page newPages = setter newPage model.pages newModel = { model | pages = newPages } in ( newGlobal, newModel, Cmd.map PageMsg <| Cmd.map tagger cmds ) makeInit : PageConfig msg model -> GlobalModel -> Pages -> ( GlobalModel, Pages, Cmd Msg ) makeInit { updater, initMsg, getter, setter, tagger } globalModel pages = case getter pages of Nothing -> ( globalModel, pages, Cmd.none ) Just oldPage -> let ( newGlobal, page, pageCmd ) = updater globalModel initMsg oldPage in ( newGlobal, setter page pages, Cmd.map (tagger >> PageMsg) pageCmd ) toRouteConfig : PageConfig msg model -> RouteConfig toRouteConfig cfg = { subber = makeSubber cfg , updater = makeUpdater cfg , init = makeInit cfg , render = \global pages -> let defaultContent = Html.text "Loading..." in Html.map PageMsg <| Html.map cfg.tagger <| Maybe.unwrap defaultContent (cfg.viewer global) <| cfg.getter pages , fetcher = \global pages -> cfg.fetcher global |> Task.map (\newPage -> cfg.setter newPage pages) } in case route of InitialRoute -> toRouteConfig initialPageConfig FrontRoute -> toRouteConfig frontPageConfig JokeRoute -> toRouteConfig jokePageConfig subscriptions : Model -> Sub Msg subscriptions { pages, currentRoute } = (routeConfig currentRoute).subber pages init : GlobalModel -> ( GlobalModel, Model, Cmd Msg ) init global = let homepageRoute = FrontRoute model : Model model = { currentRoute = InitialRoute , nextRouteLoading = False , pages = { front = Nothing , joke = Nothing } } in ( global, model, message <| FetchRoute FrontRoute ) update : GlobalModel -> Msg -> Model -> ( GlobalModel, Model, Cmd Msg ) update global msg model = let noUpdate = ( global, model, Cmd.none ) in case msg of IgnoreCommandResults -> noUpdate FetchRoute route -> ( global, { model | nextRouteLoading = True }, fetchRoute global model.pages route ) FetchedRoute route data -> let label = "fetched route" ++ (routeToString route) in ( global , { model | nextRouteLoading = False } , Cmd.map (ServeRoute route) <| handleWebData label data ) ServeRoute route maybePages -> case maybePages of Nothing -> noUpdate Just oldPages -> let ( newGlobal, newPages, pageCmds ) = (routeConfig route).init global oldPages cmds = Cmd.batch [ Ports.routeLoadEnd (routeToString route) , Ports.routeChanged (routeToString route) , pageCmds ] in ( newGlobal , { model | pages = newPages , currentRoute = route } , cmds ) PageMsg msg -> (routeConfig model.currentRoute).updater global msg model view : GlobalModel -> Model -> Html Msg view global { pages, currentRoute } = (routeConfig currentRoute).render global pages parseLocation : Location -> Route parseLocation location = parseToRoute location routeToUrl : Route -> String routeToUrl = routeToString fetchRoute : GlobalModel -> Pages -> Route -> Cmd Msg fetchRoute global pages route = let fetchCmd : Cmd Msg fetchCmd = (routeConfig route).fetcher global pages |> RemoteData.fromTask |> Task.perform (FetchedRoute route) in Cmd.batch [ fetchCmd , Ports.routeLoadStart (routeToString route) ]