Fork me on GitHub
#reitit
<
2023-11-10
>
Michaël Salihi13:11:16

Hi, I am faced with the same needs as mentioned in this issue: https://github.com/ingesolvoll/kee-frame/issues/78 To be able to prevent navigation under certain conditions like when form contains unsaved changes on the front end. Since kee-frame now use reitit-frontend under the hood, is there any Reitit solutions to approach this problem?

Michaël Salihi13:11:39

Route or navigation guards?

Mario Trost07:11:53

Hi 👋 afaik, there is nothing built-in. I moved our re-frame app to reitit. The code guarding against losing changes when navigating around or completely away from the app (other page or closing the page) is rather gnarly.

Mario Trost07:11:10

I'm happy to give you some pointers, but I'm unsure how helpful that would be. Are you only interested in built-in helpers from reitit or also in a general approach to solving this?

Michaël Salihi08:11:52

Hi 👋 Thank you for your feedback. For now, I've put this aside in order to finish the sprint but I plan to dig deeper and study the Reitit and Kee-frame code. Our project uses the latter and it uses Reitit under the hood.

Michaël Salihi08:11:23

> I'm happy to give you some pointers, but I'm unsure how helpful that would be. Any pointers will be welcome so both 🙂 Thanks.

Mario Trost11:11:08

I built the initial simple implementation by combining the re-frame and prompt examples in the reitit repository. 1. Basic thing to do: on-navigate checks if it should show a prompt -> very simple case - https://github.com/metosin/reitit/blob/master/examples/frontend-prompt/src/frontend/core.cljs#L25-L33 2. Our own implementation works differenty - on-navigate checks for a ::protected field in the db - if it’s nil/false, do the navigation - else: Show a guard modal with cancel/confirm buttons. Content is generic plus whatever was stored in ::protected 3. It gets tricky when handling the browser’s back button. You want to open the guard modal when user clicks the the back button. You also want to disable back button when the guard modal is already opened. But it’s not possible to disable the back button or override it’s behaviour. A work around is explained here: you can push the current route on the history stack so navigating back leaves the user on the current page. - https://stackoverflow.com/a/64572567 For this we have a stop-browser-back and a start-browser-back function. They get called when we update the ::protected field in the db. Here they are: 4. Next case to keep in mind: When the back button leads away from your page/app or the user wants to close the tab/window. There you have to add/remove an event listener to the beforeunload event. This will open a browser alert like window that you can’t configure in any way. - https://stackoverflow.com/a/7317311 And all this has to work with the small, some time app specific, edge cases, for example: - Updating URL search params shouldn’t trigger anything: shouldn’t trigger - Some navigations don’t lose form state but change view: shouldn’t trigger - Some of our links are explicitly not handled by reitit but should be guarded: on-navigate isn’t called, so we have to open the modal AFTER the navigation and somehow revert the navigation if user doesn’t confirm Hope that gives you some useful starting points. Feel free to ask any questions.

Mario Trost11:11:20

Implementation for 4, ::on-unload-protection is dispatched on every form change, the protected? param could also be called edited?

(defn- before-unload-handler [event]
  (set! (.-returnValue event) "")
  (.preventDefault event))

(defn- clear-unload-protection []
  (.removeEventListener js/window "beforeunload" before-unload-handler))

(defn- set-unload-protection []
  (.addEventListener js/window "beforeunload" before-unload-handler))

(reg-fx
  ::on-unload-protection
  (fn [protected?]
    (if protected?
      (set-unload-protection)
      (clear-unload-protection))))

Mario Trost11:11:22

And then code for making 3. work:

(def ^:const double-state-marker "double")

(defn- stop-browser-back
  "It's not possible to disable navigate back in modern browsers.

   Workaround:
   1. Push current route on history stack.
   2. Navigating back then goes to same page, triggers 'popstate' event, which pushes current route again."
  [f]
  (.pushState js/window.history double-state-marker "" (.-href js/window.location))
  (set!
    js/window.onpopstate
    (fn []
      (.pushState js/window.history double-state-marker "" (.-href js/window.location))
      (when (fn? f) (f)))))

(defn- start-browser-back
  [f nav-back?]
  (set! js/window.onpopstate nil)
  (cond
    ;; if modal was opened by navigating back, go back two entries in history stack
    nav-back? (.go js/window.history -2)
    (fn? f)   (f)
    :else (when (= double-state-marker (.-state js/window.history))
            (.back js/window.history))))

Mario Trost11:11:57

Obviously, getting 3 or 4 wrong can lead to very unhappy users 😄

Michaël Salihi16:11:28

Awesome, thanks! Will read in details ASAP. 👍 🙏

🙏 1
Mario Trost19:11:18

Take your time, no need to hurry along other tasks and also no need to provide feedback or questions (but always welcome 🙂 )