12

I'm working with an existing Rails app where the navigation must continue to be constructed on the backend (due to complexity and time limitations). The intended result is to have some of the pages generated with Elm, and some with Rails, using no hashes, and no full page reloads (at least for the Elm pages). The simplified version of the navigation looks like this:

<nav>
  <a href="rails-page-1">...
  <a href="rails-page-2">...
  <a href="elm-page-1">...
  <a href="elm-page-2">...
</nav>

<div id="elm-container"></div>

I've experimented with the Elm navigation package, and the elm-route-url, possibly coming close with the latter unless I'm fundamentally misunderstanding the package's capability.

Is there a way to accomplish this? I've gotten it working using hash tags, but no luck without them.

lonelyelk
  • 598
  • 9
  • 25
scoots
  • 715
  • 4
  • 16
  • I think it would be feasible to attach event listeners to your anchor links to preventDefault and to pass the intent through a port to Elm – Simon H Dec 16 '16 at 05:49
  • @SimonH It seems so, and I'm thinking about that option. I'd like to see about staying in Elm with this, if possible. – scoots Dec 16 '16 at 14:24

1 Answers1

7

using hash tags

Well you got a chuckle out of me.

I have this guy in my Helpers.elm file that I can use in lieu of Html.Events.click.

{-| Useful for overriding the default `<a>` behavior which
   causes a refresh, but can be used anywhere
-}
overrideClick : a -> Attribute a
overrideClick =
    Decode.succeed
        >> onWithOptions "click"
            { stopPropagation = False
            , preventDefault = True
            }

So on an a [ overrideClick (NavigateTo "/route"), href "/route" ] [ text "link" ] which would allow middle-clicking the element as well as using push state to update the navigation.

What you're needing is something similar on the JavaScript that works with pushState, and you don't want to ruin the middle-click experience. You can hijack all <a>s,preventDefault on its event to stop the browser from navigating, and push in the new state via the target's href. You can delegate the navigation's <a>s in the event handler. Since the Navigation runtime doesn't support listening to history changes externally (rather it appears to be using an effect manager), you'll have to push the value through a port -- luckily if you're using the Navigation package, you should already have the pieces.

On the Elm end, use UrlParser.parsePath in conjuction with one of the Navigation programs. Create a port to subscribe to using the same message that is used for it's internal url changes.

import Navigation exposing (Location)


port externalPush : (Location -> msg) -> Sub msg


type Msg
    = UrlChange Location
    | ...


main =
    Navigation.program UrlChange
        { ...
        , subscriptions : \_ -> externalPush UrlChange
        }

After the page load, use this:

const hijackNavClick = (event) => {
  // polyfill `matches` if needed
  if (event.target.matches("a[href]")) {
    // prevent the browser navigation
    event.preventDefault()
    // push the new url
    window.history.pushState({}, "", event.target.href)
    // send the new location into the Elm runtime via port
    // assuming `app` is the name of `Elm.Main.embed` or
    // whatever
    app.ports.externalPush.send(window.location)
  }
}


// your nav selector here
const nav = document.querySelector("nav")


nav.addEventListener("click", hijackNavClick, false)
toastal
  • 1,056
  • 8
  • 16
  • Thanks, I've taken your suggestions. Using this solution, is it necessary to use flags or ports to get it fully working? If not, can you elaborate on what happens on the Elm end? So far, my update isn't being triggered. – scoots Dec 19 '16 at 17:04
  • Arg... Yeah, I dug into the native code (https://github.com/elm-lang/navigation/blob/2.0.1/src/Native/Navigation.js) and it doesn't appear it listens to change events. I’ve updated the main answer with a port solution. – toastal Dec 19 '16 at 21:30