Merge pull request #604 from metosin/fix-377-fragment-strings

Fix #377, navigate to routes with fragment string in frontend
This commit is contained in:
Juho Teperi 2023-05-04 15:12:41 +03:00 committed by GitHub
commit 9f6565f097
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 63 additions and 24 deletions

View file

@ -18,7 +18,7 @@
[:div [:div
[:ul [:ul
[:li [:a {:href (rfe/href ::item {:id 1})} "Item 1"]] [:li [:a {:href (rfe/href ::item {:id 1})} "Item 1"]]
[:li [:a {:href (rfe/href ::item {:id 2} {:foo "bar"})} "Item 2"]]] [:li [:a {:href (rfe/href ::item {:id 2} {:foo "bar"} "zzz")} "Item 2"]]]
(when id (when id
[:h2 "Selected item " id]) [:h2 "Selected item " id])
[:p "Query params: " [:pre (pr-str query)]] [:p "Query params: " [:pre (pr-str query)]]

View file

@ -2,6 +2,7 @@
(:require [clojure.set :as set] (:require [clojure.set :as set]
[reitit.coercion :as coercion] [reitit.coercion :as coercion]
[reitit.core :as r] [reitit.core :as r]
[reitit.impl :as impl]
goog.Uri goog.Uri
goog.Uri.QueryData)) goog.Uri.QueryData))
@ -36,6 +37,16 @@
(.setQueryData uri (goog.Uri.QueryData/createFromMap (clj->js new-query))) (.setQueryData uri (goog.Uri.QueryData/createFromMap (clj->js new-query)))
(.toString uri))) (.toString uri)))
(defn
^{:see-also ["reitit.core/match->path"]}
match->path
"Create routing path from given match and optional query-string map and
optional fragment string."
[match query-params fragment]
(when-let [path (r/match->path match query-params)]
(cond-> path
(and fragment (seq fragment)) (str "#" (impl/form-encode fragment)))))
(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.

View file

@ -52,11 +52,13 @@
pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them
differently, convert the collections to strings first." differently, convert the collections to strings first."
([name] ([name]
(rfh/href @history name nil nil)) (rfh/href @history name nil nil nil))
([name path-params] ([name path-params]
(rfh/href @history name path-params nil)) (rfh/href @history name path-params nil nil))
([name path-params query-params] ([name path-params query-params]
(rfh/href @history name path-params query-params))) (rfh/href @history name path-params query-params nil))
([name path-params query-params fragment]
(rfh/href @history name path-params query-params fragment)))
(defn (defn
^{:see-also ["reitit.frontend.history/push-state"]} ^{:see-also ["reitit.frontend.history/push-state"]}
@ -74,11 +76,13 @@
See also: See also:
https://developer.mozilla.org/en-US/docs/Web/API/History/pushState" https://developer.mozilla.org/en-US/docs/Web/API/History/pushState"
([name] ([name]
(rfh/push-state @history name nil nil)) (rfh/push-state @history name nil nil nil))
([name path-params] ([name path-params]
(rfh/push-state @history name path-params nil)) (rfh/push-state @history name path-params nil nil))
([name path-params query-params] ([name path-params query-params]
(rfh/push-state @history name path-params query-params))) (rfh/push-state @history name path-params query-params nil))
([name path-params query-params fragment]
(rfh/push-state @history name path-params query-params fragment)))
(defn (defn
^{:see-also ["reitit.frontend.history/replace-state"]} ^{:see-also ["reitit.frontend.history/replace-state"]}
@ -96,11 +100,13 @@
See also: See also:
https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState"
([name] ([name]
(rfh/replace-state @history name nil nil)) (rfh/replace-state @history name nil nil nil))
([name path-params] ([name path-params]
(rfh/replace-state @history name path-params nil)) (rfh/replace-state @history name path-params nil nil))
([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 nil))
([name path-params query-params fragment]
(rfh/replace-state @history name path-params query-params fragment)))
;; This duplicates previous two, but the map parameter will be easier way to ;; 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 ;; extend the functions, e.g. to work with fragment string. Toggling push vs
@ -125,7 +131,7 @@
https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState"
([name] ([name]
(rfh/navigate @history name)) (rfh/navigate @history name))
([name {:keys [path-params query-params replace] :as opts}] ([name {:keys [path-params query-params replace fragment] :as opts}]
(rfh/navigate @history name opts))) (rfh/navigate @history name opts)))
(defn (defn

View file

@ -10,7 +10,7 @@
(-init [this] "Create event listeners") (-init [this] "Create event listeners")
(-stop [this] "Remove event listeners") (-stop [this] "Remove event listeners")
(-on-navigate [this path] "Find a match for current routing path and call on-navigate callback") (-on-navigate [this path] "Find a match for current routing path and call on-navigate callback")
(-get-path [this] "Get the current routing path") (-get-path [this] "Get the current routing path, including query string and fragment")
(-href [this path] "Converts given routing path to browser location")) (-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
@ -92,6 +92,7 @@
;; isContentEditable property is inherited from parents, ;; isContentEditable property is inherited from parents,
;; so if the anchor is inside contenteditable div, the property will be true. ;; so if the anchor is inside contenteditable div, the property will be true.
(not (.-isContentEditable el)) (not (.-isContentEditable el))
;; NOTE: Why doesn't this use frontend variant instead of core?
(reitit/match-by-path router (.getPath uri))))) (reitit/match-by-path router (.getPath uri)))))
(defrecord Html5History [on-navigate router listen-key click-listen-key] (defrecord Html5History [on-navigate router listen-key click-listen-key]
@ -194,8 +195,10 @@
([history name path-params] ([history name path-params]
(href history name path-params nil)) (href history name path-params nil))
([history name path-params query-params] ([history name path-params query-params]
(href history name path-params query-params nil))
([history name path-params query-params fragment]
(let [match (rf/match-by-name! (:router history) name path-params)] (let [match (rf/match-by-name! (:router history) name path-params)]
(-href history (reitit/match->path match query-params))))) (-href history (rf/match->path match query-params fragment)))))
(defn (defn
^{:see-also ["reitit.core/match->path"]} ^{:see-also ["reitit.core/match->path"]}
@ -212,12 +215,14 @@
See also: See also:
https://developer.mozilla.org/en-US/docs/Web/API/History/pushState" https://developer.mozilla.org/en-US/docs/Web/API/History/pushState"
([history name] ([history name]
(push-state history name nil nil)) (push-state history name nil nil nil))
([history name path-params] ([history name path-params]
(push-state history name path-params nil)) (push-state history name path-params nil nil))
([history name path-params query-params] ([history name path-params query-params]
(push-state history name path-params query-params nil))
([history name path-params query-params fragment]
(let [match (rf/match-by-name! (:router history) name path-params) (let [match (rf/match-by-name! (:router history) name path-params)
path (reitit/match->path match query-params)] path (rf/match->path match query-params fragment)]
;; pushState and replaceState don't trigger popstate event so call on-navigate manually ;; pushState and replaceState don't trigger popstate event so call on-navigate manually
(.pushState js/window.history nil "" (-href history path)) (.pushState js/window.history nil "" (-href history path))
(-on-navigate history path)))) (-on-navigate history path))))
@ -238,12 +243,14 @@
See also: See also:
https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState"
([history name] ([history name]
(replace-state history name nil nil)) (replace-state history name nil nil nil))
([history name path-params] ([history name path-params]
(replace-state history name path-params nil)) (replace-state history name path-params nil nil))
([history name path-params query-params] ([history name path-params query-params]
(replace-state history name path-params query-params nil))
([history name path-params query-params fragment]
(let [match (rf/match-by-name! (:router history) name path-params) (let [match (rf/match-by-name! (:router history) name path-params)
path (reitit/match->path match query-params)] path (rf/match->path match query-params fragment)]
(.replaceState js/window.history nil "" (-href history path)) (.replaceState js/window.history nil "" (-href history path))
(-on-navigate history path)))) (-on-navigate history path))))
@ -266,9 +273,9 @@
https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState"
([history name] ([history name]
(navigate history name nil)) (navigate history name nil))
([history name {:keys [path-params query-params replace] :as opts}] ([history name {:keys [path-params query-params fragment replace] :as opts}]
(let [match (rf/match-by-name! (:router history) name path-params) (let [match (rf/match-by-name! (:router history) name path-params)
path (reitit/match->path match query-params)] path (rf/match->path match query-params fragment)]
(if replace (if replace
(.replaceState js/window.history nil "" (-href history path)) (.replaceState js/window.history nil "" (-href history path))
(.pushState js/window.history nil "" (-href history path))) (.pushState js/window.history nil "" (-href history path)))

View file

@ -282,3 +282,16 @@
(testing "Need to coerce current values manually" (testing "Need to coerce current values manually"
(is (= "foo?foo=2" (is (= "foo?foo=2"
(rf/set-query-params "foo?foo=1" (fn [q] (update q :foo #(inc (js/parseInt %))))))))) (rf/set-query-params "foo?foo=1" (fn [q] (update q :foo #(inc (js/parseInt %)))))))))
(deftest match->path-test
(is (= "foo"
(rf/match->path {:path "foo"} nil nil)
(rf/match->path {:path "foo"} {} "")))
(is (= "foo?a=1&b=&c=foo+bar"
;; NOTE: This encoding differs from set-query
(rf/match->path {:path "foo"} {:a "1" :b "" :c "foo bar"} nil)))
(is (= "foo#aaa"
(rf/match->path {:path "foo"} nil "aaa")))
(testing "Fragment encoding"
(is (= "foo#foo+bar+%25"
(rf/match->path {:path "foo"} nil "foo bar %")))))

View file

@ -29,10 +29,10 @@
1 (do (is (some? (:popstate-listener history))) 1 (do (is (some? (:popstate-listener history)))
(is (= "/" url) (is (= "/" url)
"start at root") "start at root")
(rfe/push-state ::foo)) (rfe/push-state ::foo nil {:a 1} "foo bar"))
;; 0. / ;; 0. /
;; 1. /foo ;; 1. /foo?a=1#foo+bar
2 (do (is (= "/foo" url) 2 (do (is (= "/foo?a=1#foo+bar" url)
"push-state") "push-state")
(.back js/window.history)) (.back js/window.history))
;; 0. / ;; 0. /

View file

@ -26,6 +26,8 @@
(rfh/href history ::bar {:id 5}))) (rfh/href history ::bar {:id 5})))
(is (= "#/bar/5?q=x" (is (= "#/bar/5?q=x"
(rfh/href history ::bar {:id 5} {:q "x"}))) (rfh/href history ::bar {:id 5} {:q "x"})))
(is (= "#/bar/5?q=x#foo"
(rfh/href history ::bar {:id 5} {:q "x"} "foo")))
(let [{:keys [value messages]} (capture-console (let [{:keys [value messages]} (capture-console
(fn [] (fn []
(rfh/href history ::asd)))] (rfh/href history ::asd)))]