rtfeldman / elm-spa-example

A Single Page Application written in Elm
https://dev.to/rtfeldman/tour-of-an-open-source-elm-spa
MIT License
3.28k stars 530 forks source link

Path-based routing #42

Open mewa opened 6 years ago

mewa commented 6 years ago

Current approach works well when you have hash-based routing.

However if you want to use path-based routing, you'd have to prevent default link behaviour in order to avoid loading the page every time, which in turn requires either:

  1. breaking modularity, since you'd have to supply the SetRoute message of the root module somehow or
  2. introducing redundant branches that handle route changes on sub-components (much like the HomeMsg and the like are wrapping the other sub-messages at the moment)
ivanceras commented 6 years ago

This looks like we have the same issue https://github.com/rtfeldman/elm-spa-example/issues/41

ericgj commented 6 years ago

You can just define a SetRoute Msg in the page where you want to change the route, and use Route.modifyUrl.

See: http://faq.elm-community.org/#how-do-i-navigate-to-a-new-route-from-within-a-nested-view-for-example-from-a-page-view-rather-than-the-top-level-of-my-app

mewa commented 6 years ago

Yes, that's what I've meant by introducing redundancy.

There are pages that don't contain any logic and you'd have to implement all the updates and messages just for this purpose.

ericgj commented 6 years ago

Ah, I see the crux of the issue now. You want to allow the user to change the route from a reused section of the page, e.g. the navbar in the header. The basic problem is that Views.Page.frame assumes all the msgs in the content are going to be page-level msgs, whereas SetRoute is a top-level msg and it would be really nice to be able to use it directly. A secondary problem is how to get SetRoute into Views.Page since it's a separate module from Main.

On the first problem, one solution is to rewrite frame in terms of the top-level msg type, and Html.map the content inside frame, instead of doing it in Main.viewPage. This means passing in the page-to-top-level msg constructors (for example, HomeMsg, SettingsMsg, etc.):

frame : (contentMsg-> msg) -> Bool -> Maybe User -> ActivePage -> Html contentMsg -> Html msg
frame tagger isLoading user page content =
    div [ class "page-frame" ]
        [ viewHeader page user isLoading
        , content |> Html.map tagger
        , viewFooter
        ]

Now, viewHeader and viewFooter generate Html Main.Msg directly, whereas the content has to be Html.map'd to get it to Html Main.Msg. This opens up the possibility for the navbar in viewHeader to issue top-level SetRoute msgs directly.

So then second problem becomes how to get SetRoute inside the header.

You could use essentially the same technique, which is also layed out in the Elm Guide on reusability: inject a msg constructor function, in this case SetRoute*:

frame : (contentMsg-> msg) -> (Route -> msg) -> Bool -> Maybe User -> ActivePage -> Html contentMsg -> Html msg
frame tagger setRoute isLoading user page content =
    div [ class "page-frame" ]
        [ viewHeader setRoute page user isLoading
        , content |> Html.map tagger
        , viewFooter
        ]

* Or to be precise, Just >> SetRoute since SetRoute is Maybe Route -> Msg.

Then you can pass this setRoute function down into navBarLink and use it in your link click handler.

I haven't tested this, but in principle something like this should work.

nimrev commented 6 years ago

I've had to deal with the same problem a couple of weeks ago.

@ericgj Your solution is great when we would only need to send messages from the layout.

@mewa @ivanceras In the case of wanting to do path based routing, we need to do be able to send a "new url" message from every page, using a click handler with preventDefault. The solution I used is using Html Msg everywhere instead of using Html.map and calling the view with for example Html LoginMsg.

For more info about this, see the NoMap way of doing parent-child communication: https://medium.com/@_rchaves_/child-parent-communication-in-elm-outmsg-vs-translator-vs-nomap-patterns-f51b2a25ecb1

Keep in mind, this requires a big rewrite.

I'm still not too happy about the way it looks now, but it allows me to send global messages from everywhere in my app. If someone has a better architecture for this problem, would love to know more.

dwayne commented 4 months ago

@mewa I didn't run into these issues with dwayne/elm-conduit even though I'm using path-based routing. I think one of the problems with the HTML provided by RealWorld is that it uses links where buttons would have been more appropriate. In my implementation I use buttons where it calls for button semantics. Using preventDefault on a link works as suggested by @nimrev but it's a code smell and indicates you're probably using a link in place of a button.

@nimrev I'm using a form of child-parent communication that I'm calling "the dispatch pattern". See if that approach works better for you.