Zaid-Ajaj/Feliz.Router

(Question) pathMode without full redirect?

Closed this issue · 11 comments

Hello friend,

amazing job on making routing look so easy! I love it and started to use pretty fast. There is still one think I probably fully don't understand. When creating link for routing in pathMode to page/sub-page, I have two options:

  1. Add onClick to link element which dispatch custom command like Navigate and in update function do router.navigate. Downside of this approach is there is no href in link so user cannot see in bottom browser bar where is it heading to + it isn't so SEO friendly.
  2. Use router.format to generate href for link, but clicking on those will trigger full redirect (combining with onClick doesn't help)

I am familiar with Elmish.Navigation where having such link does not create redirect.

Is it like that by design? Do you know any workaround how to have nicely looking links but keep routing internal without full redirect?

Thanks for any help.

amazing job on making routing look so easy! I love it and started to use pretty fast.

Thank Roman ❤️ glad to hear you are enjoying this thing

I am familiar with Elmish.Navigation where having such link does not create redirect.

Is it like that by design? Do you know any workaround how to have nicely looking links but keep routing internal without full redirect?

This library isn't anything different than what Elmish.Navigation is doing under the hood. The problem with path full-redirects is that they happen on the server. Take a look at Vue Router - History Mode (router library for vue.js) which explains the same problem and how to handle this on different types of servers using URL rewrites.

Maybe @marcpiechura can chime in with his experience on how to properly handle this with a dotnet backend?

This library isn't anything different than what Elmish.Navigation is doing under the hood. The problem with path full-redirects is that they happen on the server.

Ok, now I'm confused. :) From my experience using Elmish.Navigation does not make any request on backend once loaded (the first request is different case, of course). It is somehow catched and handled completely on browser's side.

From my experience using Elmish.Navigation does not make any request on backend once loaded

Neither does Feliz.Router, it is the browser that thinks: "Oh I don't see the hash sign, this must go to the server" but the server only returns the same index.html page which is what this full-redirection is doing.

Do you have a repro example where Elmish.Navigation handles path routes (no hash sign) without this problem?

I have in usage on https://rezervace.mindfulyoga.cz/login (sorry, it is in Czech :)) - both links down does redirection without request to server. Repo is here: https://github.com/Dzoukr/Yobo/blob/master/src/Yobo.Client/Auth/Login/View.fs#L41-L43

Now I see where is it exactly - it uses Navigation.newUrl which does direct manipulation with history. https://github.com/Dzoukr/Yobo/blob/master/src/Yobo.Client/Router.fs#L49-L52

I think Feliz.Router doesn't have such direct function, does it?

I think Feliz.Router doesn't have such direct function, does it?

Of course it does - Router.navigate 🤦‍♂

I'm pretty close to resolve it, once done, I'll publish it here just for future reference.

Hmm this is a bit weird because Navigation.newUrl is implemented like this:

module Navigation = 
    /// Push new location into history and navigate there
    let newUrl (newUrl:string):Cmd<_> =
        [fun _ -> history.pushState((), "", newUrl)
                  let ev = CustomEvent.Create(NavigatedEvent)
                  window.dispatchEvent ev
                  |> ignore ]

and Router.navigate uses the internal Router.nav function after encoding the parts:

module internal Router = 
    let nav xs (mode: HistoryMode) : Elmish.Cmd<_> =
        Cmd.ofSub (fun _ ->
            if mode = HistoryMode.PushState
            then history.pushState ((), "", encodeParts xs)
            else history.replaceState ((), "", encodeParts xs)
            let ev = document.createEvent("CustomEvent")
            ev.initEvent (customNavigationEvent, true, true)
            window.dispatchEvent ev |> ignore
        )

They are doing the same when it comes to history.pushState so it should just works when you use onClick.

I just realized you use both href and onClick for the <a></a> tag in your application. I haven't seen this before tbh, not sure which on is being fired first: theOnClick handler was preventing the default behavior of the anchor, so maybe that is why the navigation is happening via the Navigation.newUrl and not via the href?

Just to be sure that I understand it correctly, for you pathMode does work in onClick events without full redirection? (It does work in the test project included in the repo)

I think I have it!!!

You can create custom function goToUrl

open Feliz.Router
open Fable.Core.JsInterop

let goToUrl (e: MouseEvent) =
    e.preventDefault()
    let href : string = !!e.currentTarget?attributes?href?value
    Router.navigate href |> List.map (fun f -> f ignore) |> ignore

and then you can have:

Html.a [ 
    prop.text "Click me"
    prop.href (Router.format("some-sub-path"))
    prop.onClick goToUrl 
]

which works exactly as expected (without making request)

Just to be sure that I understand it correctly, for you pathMode does work in onClick events without full redirection? (It does work in the test project included in the repo)

Yes, it does, but I need to handle it via custom Msg like you do in you demo repo here : https://github.com/Zaid-Ajaj/Feliz.Router/blob/master/demo/App.fs#L26

which works exactly as expected (without making request)

Awesome 🎉 I will probably add it to the docs

I need to handle it via custom Msg

You can do that like this: (also removing the ugliness of interop / commands)

type Msg = 
 | NavigateTo of string 

let update msg state = 
  match msg with 
  | NavigateTo href -> state, Router.navigate(href)

let goToUrl (dispatch: Msg -> unit) (href: string) (e: MouseEvent) =
    e.preventDefault()
    dispatch (NavigateTo href)

let render state dispatch = 
  let href = Router.format("some-sub-path")
  Html.a [ 
    prop.text "Click me"
    prop.href href 
    prop.onClick (goToUrl dispatch href) 
  ]

Hey, that's even way more cleaner! 🎉

Perfect, thanks a lot for all the help (and patience 😄)!!

No worries man! 😄