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))))
(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)))

View file

@ -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)))

View file

@ -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))))

View file

@ -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 %)))))))))

View file

@ -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)))))