Implement navigate and set-query functions

This commit is contained in:
Juho Teperi 2023-03-24 11:16:09 +02:00
parent f78116e346
commit 48bbdba8ed
5 changed files with 167 additions and 46 deletions

View file

@ -12,7 +12,7 @@
(vec vs)))) (vec vs))))
(defn query-params (defn query-params
"Given goog.Uri, read query parameters into Clojure map." "Given goog.Uri, read query parameters into a Clojure map."
[^Uri uri] [^Uri uri]
(let [q (.getQueryData uri)] (let [q (.getQueryData uri)]
(->> q (->> q
@ -20,6 +20,19 @@
(map (juxt keyword #(query-param q %))) (map (juxt keyword #(query-param q %)))
(into {})))) (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 (defn match-by-path
"Given routing tree and current path, return match with possibly "Given routing tree and current path, return match with possibly
coerced parameters. Return nil if no match found. coerced parameters. Return nil if no match found.
@ -88,14 +101,3 @@
match) match)
(do (js/console.warn "missing route" name) (do (js/console.warn "missing route" name)
nil)))) 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)))

View file

@ -2,8 +2,7 @@
"Easy wrapper over reitit.frontend.history, "Easy wrapper over reitit.frontend.history,
handling the state. Only one router can be active handling the state. Only one router can be active
at a time." at a time."
(:require [reitit.frontend.history :as rfh] (:require [reitit.frontend.history :as rfh]))
[reitit.frontend :as rf]))
(defonce history (atom nil)) (defonce history (atom nil))
@ -103,14 +102,43 @@
([name path-params query-params] ([name path-params query-params]
(rfh/replace-state @history name path-params query-params))) (rfh/replace-state @history name path-params query-params)))
(defn update-query ;; This duplicates previous two, but the map parameter will be easier way to
;; TODO: Sync the docstring with other namespaces ;; extend the functions, e.g. to work with fragment string. Toggling push vs
"Takes the current location and updates the query params ;; replace can be also simpler with a flag.
with given fn and arguments." ;; Navigate and set-query are also similer to react-router API.
[f & args] (defn
;; TODO: rfh version? ^{:see-also ["reitit.frontend.history/navigate"]}
(let [current-path (rfh/-get-path @history) navigate
new-path (apply rf/update-path-query-params current-path f args)] "Updates the browser location and either pushes new entry to the history stack
;; TODO: replaceState version or replaces the latest entry in the the history stack (controlled by
(.pushState js/window.history nil "" new-path) `replace` option) using URL built from a route defined by name given
(rfh/-on-navigate @history new-path))) 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)))

View file

@ -9,9 +9,9 @@
(defprotocol History (defprotocol History
(-init [this] "Create event listeners") (-init [this] "Create event listeners")
(-stop [this] "Remove event listeners") (-stop [this] "Remove event listeners")
(-on-navigate [this path]) (-on-navigate [this path] "Find a match for current routing path and call on-navigate callback")
(-get-path [this]) (-get-path [this] "Get the current routing path")
(-href [this path])) (-href [this path] "Converts given routing path to browser location"))
;; This version listens for both pop-state and hash-change for ;; This version listens for both pop-state and hash-change for
;; compatibility for old browsers not supporting History API. ;; compatibility for old browsers not supporting History API.
@ -177,7 +177,9 @@
(if history (if history
(-stop 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. "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 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)) (.pushState js/window.history nil "" (-href history path))
(-on-navigate 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 "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 using URL built from a route defined by name, with given path-params and
query-params. query-params.
@ -241,3 +245,50 @@
path (reitit/match->path match query-params)] path (reitit/match->path match query-params)]
(.replaceState js/window.history nil "" (-href history path)) (.replaceState js/window.history nil "" (-href history path))
(-on-navigate 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))))

View file

@ -8,6 +8,19 @@
[reitit.coercion.malli :as rcm] [reitit.coercion.malli :as rcm]
[reitit.frontend.test-utils :refer [capture-console]])) [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] (defn m [x]
(assoc x :data nil :result nil)) (assoc x :data nil :result nil))
@ -228,23 +241,30 @@
:expires_in 3600}}}) :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")))))))) (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" (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" (testing "Keep fragment"
(is (= "foo?bar=1&zzz=2#aaa" (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" (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" (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" (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" (testing "Need to coerce current values manually"
(is (= "foo?foo=2" (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 %)))))))))

View file

@ -30,33 +30,53 @@
(is (= "/" url) (is (= "/" url)
"start at root") "start at root")
(rfe/push-state ::foo)) (rfe/push-state ::foo))
;; 0. /
;; 1. /foo
2 (do (is (= "/foo" url) 2 (do (is (= "/foo" url)
"push-state") "push-state")
(.back js/window.history)) (.back js/window.history))
;; 0. /
3 (do (is (= "/" url) 3 (do (is (= "/" url)
"go back") "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) 4 (do (is (= "/bar/1" url)
"push-state 2") "push-state 2")
(rfe/replace-state ::bar {:id 2})) (rfe/replace-state ::bar {:id 2}))
;; 0. /
;; 1. /bar/2
5 (do (is (= "/bar/2" url) 5 (do (is (= "/bar/2" url)
"replace-state") "replace-state")
(.back js/window.history)) (rfe/set-query {:a 1}))
6 (do (is (= "/" url) ;; 0. /
"go back after replace state") ;; 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 ;; Reset to ensure old event listeners aren't called
(rfe/start! router (rfe/start! router
(fn on-navigate [match history] (fn on-navigate [match history]
(let [url (rfh/-get-path history)] (let [url (rfh/-get-path history)]
(case (swap! n inc) (case (swap! n inc)
7 (do (is (= "/" url) 9 (do (is (= "/" url)
"start at root") "start at root")
(rfe/push-state ::foo)) (rfe/push-state ::foo))
8 (do (is (= "/foo" url) 10 (do (is (= "/foo" url)
"push-state") "push-state")
(rfh/stop! @rfe/history) (rfh/stop! @rfe/history)
(done)) (done))
(do (do
(is false (str "extra event 2" {:n @n, :url url})) (is false (str "extra event 2" {:n @n, :url url}))
(done))))) (done)))))