From 48bbdba8edc7f9c9ef368295dd9fbe08a76f2bb2 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Fri, 24 Mar 2023 11:16:09 +0200 Subject: [PATCH] Implement navigate and set-query functions --- .../reitit-frontend/src/reitit/frontend.cljs | 26 ++++---- .../src/reitit/frontend/easy.cljs | 54 ++++++++++++---- .../src/reitit/frontend/history.cljs | 61 +++++++++++++++++-- test/cljs/reitit/frontend/core_test.cljs | 34 ++++++++--- test/cljs/reitit/frontend/easy_test.cljs | 38 +++++++++--- 5 files changed, 167 insertions(+), 46 deletions(-) diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs index 70dd5482..89b41fc0 100644 --- a/modules/reitit-frontend/src/reitit/frontend.cljs +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -12,7 +12,7 @@ (vec vs)))) (defn query-params - "Given goog.Uri, read query parameters into Clojure map." + "Given goog.Uri, read query parameters into a Clojure map." [^Uri uri] (let [q (.getQueryData uri)] (->> q @@ -20,6 +20,19 @@ (map (juxt keyword #(query-param q %))) (into {})))) +(defn set-query-params + "Given Reitit-frontend path, update the query params + with given function and arguments. + + Note: coercion is not applied to the query params" + [path new-query-or-update-fn] + (let [^goog.Uri uri (Uri/parse path) + new-query (if (fn? new-query-or-update-fn) + (new-query-or-update-fn (query-params uri)) + new-query-or-update-fn)] + (.setQueryData uri (QueryData/createFromMap (clj->js new-query))) + (.toString uri))) + (defn match-by-path "Given routing tree and current path, return match with possibly coerced parameters. Return nil if no match found. @@ -88,14 +101,3 @@ match) (do (js/console.warn "missing route" name) nil)))) - -(defn update-path-query-params - "Given Reitit-frontend path, update the query params - with given function and arguments. - - NOTE: coercion is not applied to the query params" - [path f & args] - (let [^goog.Uri uri (Uri/parse path) - new-query (apply f (query-params uri) args)] - (.setQueryData uri (QueryData/createFromMap (clj->js new-query))) - (.toString uri))) diff --git a/modules/reitit-frontend/src/reitit/frontend/easy.cljs b/modules/reitit-frontend/src/reitit/frontend/easy.cljs index 4c2518cf..bcd47f88 100644 --- a/modules/reitit-frontend/src/reitit/frontend/easy.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/easy.cljs @@ -2,8 +2,7 @@ "Easy wrapper over reitit.frontend.history, handling the state. Only one router can be active at a time." - (:require [reitit.frontend.history :as rfh] - [reitit.frontend :as rf])) + (:require [reitit.frontend.history :as rfh])) (defonce history (atom nil)) @@ -103,14 +102,43 @@ ([name path-params query-params] (rfh/replace-state @history name path-params query-params))) -(defn update-query - ;; TODO: Sync the docstring with other namespaces - "Takes the current location and updates the query params - with given fn and arguments." - [f & args] - ;; TODO: rfh version? - (let [current-path (rfh/-get-path @history) - new-path (apply rf/update-path-query-params current-path f args)] - ;; TODO: replaceState version - (.pushState js/window.history nil "" new-path) - (rfh/-on-navigate @history new-path))) +;; This duplicates previous two, but the map parameter will be easier way to +;; extend the functions, e.g. to work with fragment string. Toggling push vs +;; replace can be also simpler with a flag. +;; Navigate and set-query are also similer to react-router API. +(defn + ^{:see-also ["reitit.frontend.history/navigate"]} + navigate + "Updates the browser location and either pushes new entry to the history stack + or replaces the latest entry in the the history stack (controlled by + `replace` option) using URL built from a route defined by name given + parameters. + + Will also trigger on-navigate callback on Reitit frontend History handler. + + Note: currently collections in query-parameters are encoded as field-value + pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them + differently, convert the collections to strings first. + + See also: + https://developer.mozilla.org/en-US/docs/Web/API/History/pushState + https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" + ([name] + (rfh/navigate @history name)) + ([name {:keys [path-params query-params replace] :as opts}] + (rfh/navigate @history name opts))) + +(defn + ^{:see-also ["reitit.frontend.history/set-query"]} + set-query + "Update query parameters for the current route. + + New query params can be given as a map, or a function taking + the old params and returning the new modified params. + + Note: The query parameter values aren't coereced, so the + update fn will see string values for all query params." + ([new-query-or-update-fn] + (rfh/set-query @history new-query-or-update-fn)) + ([new-query-or-update-fn {:keys [replace] :as opts}] + (rfh/set-query @history new-query-or-update-fn opts))) diff --git a/modules/reitit-frontend/src/reitit/frontend/history.cljs b/modules/reitit-frontend/src/reitit/frontend/history.cljs index f95dcf12..3cbec5ab 100644 --- a/modules/reitit-frontend/src/reitit/frontend/history.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/history.cljs @@ -9,9 +9,9 @@ (defprotocol History (-init [this] "Create event listeners") (-stop [this] "Remove event listeners") - (-on-navigate [this path]) - (-get-path [this]) - (-href [this path])) + (-on-navigate [this path] "Find a match for current routing path and call on-navigate callback") + (-get-path [this] "Get the current routing path") + (-href [this path] "Converts given routing path to browser location")) ;; This version listens for both pop-state and hash-change for ;; compatibility for old browsers not supporting History API. @@ -177,7 +177,9 @@ (if history (-stop history))) -(defn href +(defn + ^{:see-also ["reitit.core/match->path"]} + href "Generate a URL for a route defined by name, with given path-params and query-params. The URL is formatted using Reitit frontend history handler, so using it with @@ -219,7 +221,9 @@ (.pushState js/window.history nil "" (-href history path)) (-on-navigate history path)))) -(defn replace-state +(defn + ^{:see-also ["reitit.core/match->path"]} + replace-state "Updates the browser location and replaces latest entry in the history stack using URL built from a route defined by name, with given path-params and query-params. @@ -241,3 +245,50 @@ path (reitit/match->path match query-params)] (.replaceState js/window.history nil "" (-href history path)) (-on-navigate history path)))) + +(defn + ^{:see-also ["reitit.core/match->path"]} + navigate + "Updates the browser location and either pushes new entry to the history stack + or replaces the latest entry in the the history stack (controlled by + `replace` option) using URL built from a route defined by name given + parameters. + + Will also trigger on-navigate callback on Reitit frontend History handler. + + Note: currently collections in query-parameters are encoded as field-value + pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them + differently, convert the collections to strings first. + + See also: + https://developer.mozilla.org/en-US/docs/Web/API/History/pushState + https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" + ([history name] + (navigate history name nil)) + ([history name {:keys [path-params query-params replace] :as opts}] + (let [match (rf/match-by-name! (:router history) name path-params) + path (reitit/match->path match query-params)] + (if replace + (.replaceState js/window.history nil "" (-href history path)) + (.pushState js/window.history nil "" (-href history path))) + (-on-navigate history path)))) + +(defn + ^{:see-also ["reitit.frontend/set-query-params"]} + set-query + "Update query parameters for the current route. + + New query params can be given as a map, or a function taking + the old params and returning the new modified params. + + Note: The query parameter values aren't coereced, so the + update fn will see string values for all query params." + ([history new-query-or-update-fn] + (set-query history new-query-or-update-fn nil)) + ([history new-query-or-update-fn {:keys [replace] :as opts}] + (let [current-path (-get-path history) + new-path (rf/set-query-params current-path new-query-or-update-fn)] + (if replace + (.replaceState js/window.history nil "" (-href history new-path)) + (.pushState js/window.history nil "" (-href history new-path))) + (-on-navigate history new-path)))) diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs index 541d2b84..ffa919c9 100644 --- a/test/cljs/reitit/frontend/core_test.cljs +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -8,6 +8,19 @@ [reitit.coercion.malli :as rcm] [reitit.frontend.test-utils :refer [capture-console]])) +(deftest query-params-test + (is (= {:foo "1"} + (rf/query-params (.parse goog.Uri "?foo=1")))) + + (is (= {:foo "1" :bar "aaa"} + (rf/query-params (.parse goog.Uri "?foo=1&bar=aaa")))) + + (is (= {:foo ""} + (rf/query-params (.parse goog.Uri "?foo=")))) + + (is (= {:foo ""} + (rf/query-params (.parse goog.Uri "?foo"))))) + (defn m [x] (assoc x :data nil :result nil)) @@ -228,23 +241,30 @@ :expires_in 3600}}}) (m (rf/match-by-path router "/5?mode=foo#access_token=foo&refresh_token=bar&provider_token=baz&token_type=bearer&expires_in=3600")))))))) -(deftest update-path-query-params-test +(deftest set-query-params-test (is (= "foo?bar=1" - (rf/update-path-query-params "foo" assoc :bar 1))) + (rf/set-query-params "foo" {:bar 1}) + (rf/set-query-params "foo" #(assoc % :bar 1)))) (testing "Keep fragment" (is (= "foo?bar=1&zzz=2#aaa" - (rf/update-path-query-params "foo?bar=1#aaa" assoc :zzz 2)))) + (rf/set-query-params "foo?bar=1#aaa" #(assoc % :zzz 2))))) (is (= "foo?asd=1&bar=1" - (rf/update-path-query-params "foo?asd=1" assoc :bar 1))) + (rf/set-query-params "foo?asd=1" #(assoc % :bar 1)))) (is (= "foo?bar=1" - (rf/update-path-query-params "foo?asd=1&bar=1" dissoc :asd))) + (rf/set-query-params "foo?asd=1&bar=1" #(dissoc % :asd)))) + + (is (= "foo?bar" + (rf/set-query-params "foo?asd=1&bar" #(dissoc % :asd)))) + + (is (= "foo?bar" + (rf/set-query-params "foo" #(assoc % :bar "")))) (is (= "foo" - (rf/update-path-query-params "foo?asd=1" dissoc :asd))) + (rf/set-query-params "foo?asd=1" #(dissoc % :asd)))) (testing "Need to coerce current values manually" (is (= "foo?foo=2" - (rf/update-path-query-params "foo?foo=1" update :foo #(inc (js/parseInt %))))))) + (rf/set-query-params "foo?foo=1" (fn [q] (update q :foo #(inc (js/parseInt %))))))))) diff --git a/test/cljs/reitit/frontend/easy_test.cljs b/test/cljs/reitit/frontend/easy_test.cljs index 7def2701..abe11545 100644 --- a/test/cljs/reitit/frontend/easy_test.cljs +++ b/test/cljs/reitit/frontend/easy_test.cljs @@ -30,33 +30,53 @@ (is (= "/" url) "start at root") (rfe/push-state ::foo)) + ;; 0. / + ;; 1. /foo 2 (do (is (= "/foo" url) "push-state") (.back js/window.history)) + ;; 0. / 3 (do (is (= "/" url) "go back") - (rfe/push-state ::bar {:id 1})) + (rfe/navigate ::bar {:path-params {:id 1}})) + ;; 0. / + ;; 1. /bar/1 4 (do (is (= "/bar/1" url) "push-state 2") (rfe/replace-state ::bar {:id 2})) + ;; 0. / + ;; 1. /bar/2 5 (do (is (= "/bar/2" url) "replace-state") - (.back js/window.history)) - 6 (do (is (= "/" url) - "go back after replace state") + (rfe/set-query {:a 1})) + ;; 0. / + ;; 1. /bar/2 + ;; 2. /bar/2?a=1 + 6 (do (is (= "/bar/2?a=1" url) + "update-query with map") + (rfe/set-query #(assoc % :b "foo") {:replace true})) + ;; 0. / + ;; 1. /bar/2 + ;; 2. /bar/2?a=1&b=foo + 7 (do (is (= "/bar/2?a=1&b=foo" url) + "update-query with fn") + (.go js/window.history -2)) + ;; 0. / + 8 (do (is (= "/" url) + "go back two events") ;; Reset to ensure old event listeners aren't called (rfe/start! router (fn on-navigate [match history] (let [url (rfh/-get-path history)] (case (swap! n inc) - 7 (do (is (= "/" url) + 9 (do (is (= "/" url) "start at root") (rfe/push-state ::foo)) - 8 (do (is (= "/foo" url) - "push-state") - (rfh/stop! @rfe/history) - (done)) + 10 (do (is (= "/foo" url) + "push-state") + (rfh/stop! @rfe/history) + (done)) (do (is false (str "extra event 2" {:n @n, :url url})) (done)))))