Announcing elm-lang/navigation for "routing in single-page apps"

1,509 views
Skip to first unread message

Evan Czaplicki

unread,
May 25, 2016, 6:02:03 PM5/25/16
to elm-d...@googlegroups.com
On Friday, Noah and I worked on "updating elm-history" so that folks can do "routing" with Elm 0.17. The results are these libraries:
I think they will cover the core functionality in a way that also promotes healthy architecture. If you disagree, I ask that you use these libraries before you share your opinion (or ideally the particular scenario you are having trouble with).


Details

The elm-lang/navigation library is the core thing. It lets you get notified about changes to the address bar. This may be the user typing in there or pressing the forward and back buttons on the browser. It also lets you "navigate to new URLs" so you can go to new URLs without reloading any assets.

The elm-lang/navigation library is designed such that you can parse URLs however you want. You can see a basic example of that here. The evancz/url-parser library is meant to handle more complex cases. You can see a bit of that in this example.

My URL parser is intended to be a baseline for exploration. There are probably cases it does not cover well. My goal right now is to point us towards good API design, not to be the URL parser.


Thanks

Big thanks to Noah for working through all this with me! And thank you to Aaron who helped review and talk through the API we ended up with. These were fun to work on :D

Magnus Rundberget

unread,
May 25, 2016, 7:37:50 PM5/25/16
to Elm Discuss
Tx for this !


The final piece I've been waiting for before upgrading one of my apps. I think I'll go ahead and rip out the existing router package I'm using (which has served me well enough btw) and see if I can't manage fine with just these two building block.

shame it's 01:30 over here, but there's always tomorrow :-)

cheers
-magnus

Cristian Garcia

unread,
May 25, 2016, 11:08:36 PM5/25/16
to Elm Discuss
Thanks a lot!

I think these libraries are incredibly important, solving the puzzle "How do I create a complex app with multiple views". I know multiple views have always been possible but the "#route" seems important for a web app. 

Evan, it would be very nice to have a "starter app" some how, something equivalent Phoenix's "mix phoenix.new your_app", that gives you a basic structure. To do a simple test I had to copy one of your examples piece by piece to get only what I needed. A basic "Pages.elm" included in this started project would speed up developers and help people trying to learn Elm to do SPA, this file could include the basic structure of the navigation stuff along with best practices.

Ryan Rempel

unread,
May 25, 2016, 11:39:56 PM5/25/16
to Elm Discuss
Very interesting work!

I think that one might (naively) have expected the `Location` to be exposed as a `Sub Location`. One could, then, use the sort of `Parser` which you provide for (to `map` the `Sub Location` to a `Sub msg`), and then supply the `Sub msg` when initializing the `Program` (as you might with any other permanent subscription).

Instead, the module provides for a different way of constructing a `Program` -- that is, an alternative to `Html.App.program` or `Html.App.programWithFlags`.

What this permits, at first glance, is providing for an entirely separate function to handle updates from navigation -- that is, a `urlUpdate` function which is separate from the usual `update` function. (The naive approach I mentioned above would only have an `update` function, with the location changes mapped to messages that the `update` function understands).

What problem does a separate `urlUpdate` function solve? My guess is that it solves a circularity problem. Consider (as in the example provided) incrementing or decrementing a counter. In the `update` method, in addition to updating the model, one wants to issue a command to change the URL. In the `urlUpdate` method, you just want to update the model. You don't want to change the URL, because that would trigger another round of `urlUpdate` -- you'd be in an infinite loop.

So, the separate `urlUpdate` function is a kind of a reminder not to issue further commands to change the location inside the `urlUpdate`. (Or, at least usually not -- I suppose there might be some scenarios where you might actually want a kind of "redirect", so long as the redirecting terminates eventually).

So, that's an interesting solution. Are there any disadvantages to it? I suppose that there are a couple of potential disadvantages.

One, I suppose, is a question of composability. This strategy is kind of a singleton -- at least, it's hard to see how you could use this strategy again, and then compose the two. But, perhaps you won't need this strategy again, or you'll figure out how to cross that bridge when you come to it.

The other potential disadvantage is a kind of duplication between the logic needed for `urlUpdate` and `update` -- both logic in the sense of dispatch to sub-components, and logic in the sense of the logic for the actual model changes. Though, I suppose that problem is relatively easy for clients of the library to solve -- one could imagine a third function, `universalUpdate`, which takes a `Bool` parameter that indicates whether the change is coming from `update` or `urlUpdate`. Then, both `update` and `urlUpdate` can call `universalUpdate`, which would know whether to change the URL via the `Bool` parameter. Or something like that -- it seems like a solvable problem for users of the library.

I suppose that is how one could solve the circularity issue in the "naive" subscription approach as well. That is, one might include, in the `Msg` type, an indication of whether the `Msg` is coming from "normal" user action or from the URL change. (And, thus, know, within the `update` method, whether to issue a URL change command).

Anyway, I hope I have not offered any opinions in the above comments -- I don't really have any opinions yet, just a few interesting things that I noticed.

--
Ryan Rempel

On Wednesday, May 25, 2016 at 5:02:03 PM UTC-5, Evan wrote:

Max Goldstein

unread,
May 26, 2016, 12:45:13 AM5/26/16
to Elm Discuss
Ryan, the big thing I think you're missing is the type of init. The initial state of the app is based on the URL the user navigates to. This allows for a crucial piece of SPAs, namely, the ability to retrieve state when based on the URL that the user first enters.

That being said, if we could tolerate some kind of "loading what the original URL was" state, one could simplify the API a fair bit. Requesting URL changes are just commands; we have those in update's return type already. Receiving them could be by a subscription that takes a Location -> Msg function. It's on the client to keep track of which messages are from the URL changing and which from other events.

Although I'm excited enough by the idea of synchronously determining the initial state of the app from the URL (first paragraph), I think Ryan raises a particularly good concern regarding composability-- what happens when there are three libraries that all want to define program but we can only use one? At the moment I think the best we can do is hope no such other libraries surface.

Ryan Rempel

unread,
May 26, 2016, 12:59:05 AM5/26/16
to Elm Discuss
Ah, it is good to know that, in the transition from signals to subscriptions, our old friend, the initial value, has not ceased to be special!

--
Ryan Rempel

Emilien Taque

unread,
May 26, 2016, 1:49:45 AM5/26/16
to Elm Discuss
AFAIK There isn't any circularity problem where you consider browser location as the reference, push changes to it through history API and let it flow from here through your app.

Bogdan Popa

unread,
May 26, 2016, 2:10:11 AM5/26/16
to Evan Czaplicki, elm-d...@googlegroups.com
I've updated elm-route's example app to use `elm-lang/navigation`[1].
The diff[2] looks pretty good if you ask me. Thanks guys!

1: https://github.com/Bogdanp/elm-route/tree/master/examples/app
2: https://github.com/Bogdanp/elm-route/commit/2b33522c09213b1197fe9512c9ac3fc745b1f16d

Evan Czaplicki <eva...@gmail.com> writes:

> On Friday, Noah and I worked on "updating elm-history" so that folks can do
> "routing" with Elm 0.17. The results are these libraries:
>
> - elm-lang/navigation
> <http://package.elm-lang.org/packages/elm-lang/navigation/latest/>
> - evancz/url-parser
> <http://package.elm-lang.org/packages/evancz/url-parser/latest/>
>
> I think they will cover the core functionality in a way that also promotes
> healthy architecture. If you disagree, I ask that you *use* these libraries
> before you share your opinion (or ideally the particular scenario you are
> having trouble with).
>
>
> Details
>
> The elm-lang/navigation library is the core thing. It lets you get notified
> about changes to the address bar. This may be the user typing in there or
> pressing the forward and back buttons on the browser. It also lets you
> "navigate to new URLs" so you can go to new URLs without reloading any
> assets.
>
> The elm-lang/navigation library is designed such that you can parse URLs
> however you want. You can see a basic example of that here
> <https://github.com/elm-lang/navigation/tree/master/examples>. The
> evancz/url-parser library is meant to handle more complex cases. You can
> see a bit of that in this example
> <https://github.com/evancz/url-parser/tree/master/examples>.
>
> My URL parser is intended to be a baseline for exploration. There are
> probably cases it does not cover well. My goal right now is to point us
> towards good API design, not to be *the* URL parser.
>
>
> Thanks
>
> Big thanks to Noah for working through all this with me! And thank you to
> Aaron who helped review and talk through the API we ended up with. These
> were fun to work on :D
>
> --
> You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

Fedor Nezhivoi

unread,
May 26, 2016, 6:09:35 AM5/26/16
to elm-d...@googlegroups.com
Shouldn't "s" function be named as "strict"?
--
Best regards,
Fedor Nezhivoi.

Gage Peterson

unread,
May 26, 2016, 2:28:54 PM5/26/16
to Elm Discuss
I was too, a bit confused to find out that it didn't show up in the subscriptions... and the fact that we're aren't using Html.App anymore. Not a huge deal but I think it would be nice to like only 3 or 2 ways to set a project to lower the cognitive load. Not a huge deal considering how closely related everything is in Elm. 

 The "s" command failed to be looked up properly by elm-oracle (probably a bug either in the vim config or oracle), I think "strict" or perhaps "exactly" would be a better name. People can always alias it if it gets too long. I'm a big fan of aliasing in the local scope of function rather than at the Library level. 

Very strong work however! It was very easy to understand overall. 100x better than what I've seen in JS in complexity and size. Bravo!

Simon

unread,
May 26, 2016, 4:31:14 PM5/26/16
to Elm Discuss
My initial tests with the code shows that it crashes pretty heavily. I was trying to use HTML5 urls rather than #/, so I made these changes in the example code. I should have dropped 9, not 8, but the result was a complete hang.
```
toUrl : Int -> String
toUrl count =
  "/counter/" ++ toString count
  -- "#/" ++ toString count


fromUrl : String -> Result String Int
fromUrl url =
  String.toInt (String.dropLeft 8 url)
```

Evan

unread,
May 26, 2016, 4:59:03 PM5/26/16
to Elm Discuss
Simon, just like I said here, create an http://sscce.org/ and open a new issue. Provide a minimal Elm program that exhibits the error, compiles, etc.

Read http://sscce.org/, particularly the part about self-contained.

Cristian Garcia

unread,
May 26, 2016, 5:36:07 PM5/26/16
to Evan Czaplicki, elm-d...@googlegroups.com
Max and Ryan have very good points:

1. The community was somehow expecting a subscription/effect system for the location, this way a library like e.g. Hop could build upon it. 

2. In theory, exposing an "update" function for the router that the user could use when a "Navigation" message comes in would work, so there is a concern of why this is exposed as a Program with the main preoccupation being that programs are not(?) composable.

3. There are now 2 different update functions, one that deals with regular messages and one that deals with "Navigation" messages, there is a worry if at some point this might cause duplication in functionality.

That said, this solution is pretty easy to understand, its relatively light-weight and enables users to do SPAs with less overhead than frameworks like Angular.


On Thu, May 26, 2016 at 3:55 PM Cristian Garcia <cris...@aristadev.com> wrote:
Max and Ryan have very good points:

1. The community was somehow expecting a subscription/effect system for the location, this way a library like e.g. Hop could build upon it. 

2. In theory, exposing an "update" function for the router that the user could use when a "Navigation" message comes in would work, so there is a concern of why this is exposed as a Program with the main preoccupation being that programs are not(?) composable.

3. There are now 2 different update functions, one that deals with regular messages and one that deals with "Navigation" messages, there is a worry if at some point this might cause duplication in functionality.

That said, this solution is pretty easy to understand, its relatively light-weight and enables users to do SPAs with less overhead than frameworks like Angular.

Emilien Taque

unread,
May 27, 2016, 5:17:24 AM5/27/16
to Elm Discuss
Migrated my main app to Navigation, reusing my route parser. Painless migration, works as expected, can still do proper transitions. Thumbs up!
Reply all
Reply to author
Forward
0 new messages