From dd724f0d0e1143e4b89374c65214988232d29f3e Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Thu, 23 Mar 2023 15:13:27 +0200 Subject: [PATCH 01/12] Fix #600: Add frontend function to update query-params for current path --- modules/reitit-frontend/src/reitit/frontend.cljs | 11 +++++++++++ .../reitit-frontend/src/reitit/frontend/easy.cljs | 13 ++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs index faa258e0..70dd5482 100644 --- a/modules/reitit-frontend/src/reitit/frontend.cljs +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -88,3 +88,14 @@ 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 8a76b900..45223817 100644 --- a/modules/reitit-frontend/src/reitit/frontend/easy.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/easy.cljs @@ -2,7 +2,8 @@ "Easy wrapper over reitit.frontend.history, handling the state. Only one router can be active at a time." - (:require [reitit.frontend.history :as rfh])) + (:require [reitit.frontend.history :as rfh] + [reitit.frontend :as rf])) (defonce history (atom nil)) @@ -101,3 +102,13 @@ (rfh/replace-state @history name path-params nil)) ([name path-params query-params] (rfh/replace-state @history name path-params query-params))) + +(defn update-query + "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)] + (.pushState js/window.history nil "" new-path) + (rfh/-on-navigate @history new-path))) From e3e93eaffb817ed5461b340c88d919aac77dd4f1 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Thu, 23 Mar 2023 15:36:48 +0200 Subject: [PATCH 02/12] Add some tests --- .../src/reitit/frontend/easy.cljs | 2 ++ test/cljs/reitit/frontend/core_test.cljs | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/modules/reitit-frontend/src/reitit/frontend/easy.cljs b/modules/reitit-frontend/src/reitit/frontend/easy.cljs index 45223817..4c2518cf 100644 --- a/modules/reitit-frontend/src/reitit/frontend/easy.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/easy.cljs @@ -104,11 +104,13 @@ (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))) diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs index d0684398..220861a1 100644 --- a/test/cljs/reitit/frontend/core_test.cljs +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -227,3 +227,20 @@ :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 + (is (= "foo?bar=1" + (rf/update-path-query-params "foo" #(assoc % :bar 1)))) + + (is (= "foo?asd=1&bar=1" + (rf/update-path-query-params "foo?asd=1" #(assoc % :bar 1)))) + + (is (= "foo?bar=1" + (rf/update-path-query-params "foo?asd=1&bar=1" #(dissoc % :asd)))) + + (is (= "foo" + (rf/update-path-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 %))))))) From a558365252921129b8258ce6fda3cf90f650c67e Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Thu, 23 Mar 2023 15:41:48 +0200 Subject: [PATCH 03/12] Cleanup --- test/cljs/reitit/frontend/core_test.cljs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs index 220861a1..710c4e98 100644 --- a/test/cljs/reitit/frontend/core_test.cljs +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -230,16 +230,16 @@ (deftest update-path-query-params-test (is (= "foo?bar=1" - (rf/update-path-query-params "foo" #(assoc % :bar 1)))) + (rf/update-path-query-params "foo" assoc :bar 1))) (is (= "foo?asd=1&bar=1" - (rf/update-path-query-params "foo?asd=1" #(assoc % :bar 1)))) + (rf/update-path-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/update-path-query-params "foo?asd=1&bar=1" dissoc :asd))) (is (= "foo" - (rf/update-path-query-params "foo?asd=1" #(dissoc % :asd)))) + (rf/update-path-query-params "foo?asd=1" dissoc :asd))) (testing "Need to coerce current values manually" (is (= "foo?foo=2" From f78116e346f9ce952c027fb6e65eb928e6900f27 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Thu, 23 Mar 2023 15:46:25 +0200 Subject: [PATCH 04/12] Test fragment --- test/cljs/reitit/frontend/core_test.cljs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs index 710c4e98..541d2b84 100644 --- a/test/cljs/reitit/frontend/core_test.cljs +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -232,6 +232,10 @@ (is (= "foo?bar=1" (rf/update-path-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)))) + (is (= "foo?asd=1&bar=1" (rf/update-path-query-params "foo?asd=1" assoc :bar 1))) From 48bbdba8edc7f9c9ef368295dd9fbe08a76f2bb2 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Fri, 24 Mar 2023 11:16:09 +0200 Subject: [PATCH 05/12] 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))))) From dad8f530a672aa86a695b376f2295b8095d686d9 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Fri, 24 Mar 2023 11:32:22 +0200 Subject: [PATCH 06/12] Add example and update docs --- doc/frontend/browser.md | 12 +++++++++-- .../src/frontend/core.cljs | 21 ++++++++++++------- .../reitit-frontend/src/reitit/frontend.cljs | 16 +++++++------- .../src/reitit/frontend/history.cljs | 8 +++---- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/doc/frontend/browser.md b/doc/frontend/browser.md index 50349fcc..1541265b 100644 --- a/doc/frontend/browser.md +++ b/doc/frontend/browser.md @@ -2,8 +2,16 @@ Reitit includes two browser history integrations. -Functions follow HTML5 History API: `push-state` to change route, `replace-state` -to change route without leaving previous entry in browser history. +Main functions are `navigate` and `set-query`. Navigate is used to navigate +to named routes, and the options parameter can be used to control all +parameters and if `pushState` or `replaceState` should be used to control +browser history stack. The `set-query` function can be used to change +or modify query parameters for the current route, it takes either map of +new query params or function from old params to the new params. + +There are also secondary functions following HTML5 History API: +`push-state` to navigate to new route adding entry to the history and +`replace-state` to change route without leaving previous entry in browser history. ## Fragment router diff --git a/examples/frontend-controllers/src/frontend/core.cljs b/examples/frontend-controllers/src/frontend/core.cljs index 60cd6f17..6a2388f2 100644 --- a/examples/frontend-controllers/src/frontend/core.cljs +++ b/examples/frontend-controllers/src/frontend/core.cljs @@ -19,10 +19,17 @@ [:ul [:li [:a {:href (rfe/href ::item {:id 1})} "Item 1"]] [:li [:a {:href (rfe/href ::item {:id 2} {:foo "bar"})} "Item 2"]]] - (if id + (when id [:h2 "Selected item " id]) - (if (:foo query) - [:p "Optional foo query param: " (:foo query)])])) + [:p "Query params: " [:pre (pr-str query)]] + [:ul + [:li [:a {:on-click #(rfe/set-query {:a 1})} "set a=1"]] + [:li [:a {:on-click #(rfe/set-query {:a 2} {:replace true})} "set a=2 and replaceState"]] + [:li [:a {:on-click (fn [_] (rfe/set-query #(assoc % :foo "zzz")))} "add foo=zzz"]]] + [:button + {:on-click #(rfe/navigate ::item {:path-params {:id 3} + :query-params {:foo "aaa"}})} + "Navigate example, go to item 3"]])) (defonce match (r/atom nil)) @@ -31,9 +38,8 @@ [:ul [:li [:a {:href (rfe/href ::frontpage)} "Frontpage"]] [:li - [:a {:href (rfe/href ::item-list)} "Item list"] - ]] - (if @match + [:a {:href (rfe/href ::item-list)} "Item list"]]] + (when @match (let [view (:view (:data @match))] [view @match])) [:pre (with-out-str (fedn/pprint @match))]]) @@ -63,7 +69,8 @@ ["/:id" {:name ::item :parameters {:path {:id s/Int} - :query {(s/optional-key :foo) s/Keyword}} + :query {(s/optional-key :a) s/Int + (s/optional-key :foo) s/Keyword}} :controllers [{:parameters {:path [:id]} :start (fn [{:keys [path]}] (js/console.log "start" "item controller" (:id path))) diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs index 89b41fc0..38608e13 100644 --- a/modules/reitit-frontend/src/reitit/frontend.cljs +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -1,11 +1,11 @@ (ns reitit.frontend (:require [clojure.set :as set] [reitit.coercion :as coercion] - [reitit.core :as r]) - (:import goog.Uri - goog.Uri.QueryData)) + [reitit.core :as r] + goog.Uri + goog.Uri.QueryData)) -(defn- query-param [^QueryData q k] +(defn- query-param [^goog.uri.QueryData q k] (let [vs (.getValues q k)] (if (< (alength vs) 2) (aget vs 0) @@ -13,7 +13,7 @@ (defn query-params "Given goog.Uri, read query parameters into a Clojure map." - [^Uri uri] + [^goog.Uri uri] (let [q (.getQueryData uri)] (->> q (.getKeys) @@ -26,11 +26,11 @@ Note: coercion is not applied to the query params" [path new-query-or-update-fn] - (let [^goog.Uri uri (Uri/parse path) + (let [^goog.Uri uri (goog.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))) + (.setQueryData uri (goog.Uri.QueryData/createFromMap (clj->js new-query))) (.toString uri))) (defn match-by-path @@ -40,7 +40,7 @@ :on-coercion-error - a sideeffecting fn of `match exception -> nil`" ([router path] (match-by-path router path nil)) ([router path {:keys [on-coercion-error]}] - (let [uri (.parse Uri path) + (let [uri (.parse goog.Uri path) coerce! (if on-coercion-error (fn [match] (try (coercion/coerce! match) diff --git a/modules/reitit-frontend/src/reitit/frontend/history.cljs b/modules/reitit-frontend/src/reitit/frontend/history.cljs index 3cbec5ab..5764e10f 100644 --- a/modules/reitit-frontend/src/reitit/frontend/history.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/history.cljs @@ -3,8 +3,8 @@ events." (:require [goog.events :as gevents] [reitit.core :as reitit] - [reitit.frontend :as rf]) - (:import goog.Uri)) + [reitit.frontend :as rf] + goog.Uri)) (defprotocol History (-init [this] "Create event listeners") @@ -78,7 +78,7 @@ the page location is updated using History API." [router e el uri] (let [current-domain (if (exists? js/location) - (.getDomain (.parse Uri js/location)))] + (.getDomain (.parse goog.Uri js/location)))] (and (or (and (not (.hasScheme uri)) (not (.hasDomain uri))) (= current-domain (.getDomain uri))) (not (.-altKey e)) @@ -109,7 +109,7 @@ ignore-anchor-click (fn [e] ;; Returns the next matching ancestor of event target (when-let [el (closest-by-tag (event-target e) "a")] - (let [uri (.parse Uri (.-href el))] + (let [uri (.parse goog.Uri (.-href el))] (when (ignore-anchor-click-predicate router e el uri) (.preventDefault e) (let [path (str (.getPath uri) From e2217887e3d7c719f3551b76ba7a1aab53880c63 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Fri, 24 Mar 2023 13:59:02 +0200 Subject: [PATCH 07/12] Comments about differences to reitit.impl --- .../reitit-frontend/src/reitit/frontend.cljs | 3 +++ test/cljs/reitit/frontend/core_test.cljs | 20 ++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs index 38608e13..907a07a7 100644 --- a/modules/reitit-frontend/src/reitit/frontend.cljs +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -30,6 +30,9 @@ new-query (if (fn? new-query-or-update-fn) (new-query-or-update-fn (query-params uri)) new-query-or-update-fn)] + ;; NOTE: Differences to reitit.impl/query-string? + ;; reitit fn adds "=" even if value is empty string + ;; reitit encodes " " as "+" while browser and goog.Uri encode as "%20" (.setQueryData uri (goog.Uri.QueryData/createFromMap (clj->js new-query))) (.toString uri))) diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs index ffa919c9..a0e0abe0 100644 --- a/test/cljs/reitit/frontend/core_test.cljs +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -6,7 +6,8 @@ [schema.core :as s] [reitit.coercion.schema :as rcs] [reitit.coercion.malli :as rcm] - [reitit.frontend.test-utils :refer [capture-console]])) + [reitit.frontend.test-utils :refer [capture-console]] + [reitit.impl :as impl])) (deftest query-params-test (is (= {:foo "1"} @@ -244,7 +245,17 @@ (deftest set-query-params-test (is (= "foo?bar=1" (rf/set-query-params "foo" {:bar 1}) - (rf/set-query-params "foo" #(assoc % :bar 1)))) + (rf/set-query-params "foo" #(assoc % :bar 1)) + ;; Also compare to reitit.impl version which is used by match->path (and history fns) + (str "foo?" (impl/query-string {:bar 1})))) + + (testing "Encoding" + (is (= "foo?bar=foo%20bar" + (rf/set-query-params "foo" {:bar "foo bar"}) + (rf/set-query-params "foo" #(assoc % :bar "foo bar")) + ;; FIXME: Reitit.impl encodes space as "+" + ; (str "foo?" (impl/query-string {:bar "foo bar"})) + ))) (testing "Keep fragment" (is (= "foo?bar=1&zzz=2#aaa" @@ -260,7 +271,10 @@ (rf/set-query-params "foo?asd=1&bar" #(dissoc % :asd)))) (is (= "foo?bar" - (rf/set-query-params "foo" #(assoc % :bar "")))) + (rf/set-query-params "foo" #(assoc % :bar "")) + ;; FIXME: Reitit.impl adds "=" for empty string values + ; (str "foo?" (impl/query-string {:bar ""})) + )) (is (= "foo" (rf/set-query-params "foo?asd=1" #(dissoc % :asd)))) From 8174296fe79fa7f23b3c55c92655cc59f3dc9946 Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Thu, 13 Apr 2023 09:15:10 +0300 Subject: [PATCH 08/12] chore: add lein-ancient plugin --- project.clj | 1 + 1 file changed, 1 insertion(+) diff --git a/project.clj b/project.clj index 0960ec97..709e8d5e 100644 --- a/project.clj +++ b/project.clj @@ -54,6 +54,7 @@ :plugins [[jonase/eastwood "1.3.0"] ;[lein-virgil "0.1.7"] + [lein-ancient "1.0.0-RC3"] [lein-doo "0.1.11"] [lein-cljsbuild "1.1.8"] [lein-cloverage "1.2.4"] From 6e7acaedee02897fb0e07ae042c21ffc794110d7 Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Thu, 13 Apr 2023 09:16:04 +0300 Subject: [PATCH 09/12] chore: upgrade deps --- CHANGELOG.md | 7 ++++++- project.clj | 18 +++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9608744d..5addea5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,12 @@ We use [Break Versioning][breakver]. The version numbers follow a `. Date: Thu, 13 Apr 2023 09:18:56 +0300 Subject: [PATCH 10/12] chore: upgrade lein-parent plugin --- modules/reitit-core/project.clj | 2 +- modules/reitit-dev/project.clj | 2 +- modules/reitit-frontend/project.clj | 2 +- modules/reitit-http/project.clj | 2 +- modules/reitit-interceptors/project.clj | 2 +- modules/reitit-malli/project.clj | 2 +- modules/reitit-middleware/project.clj | 2 +- modules/reitit-openapi/project.clj | 2 +- modules/reitit-pedestal/project.clj | 2 +- modules/reitit-ring/project.clj | 2 +- modules/reitit-schema/project.clj | 2 +- modules/reitit-sieppari/project.clj | 2 +- modules/reitit-spec/project.clj | 2 +- modules/reitit-swagger-ui/project.clj | 2 +- modules/reitit-swagger/project.clj | 2 +- modules/reitit/project.clj | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/modules/reitit-core/project.clj b/modules/reitit-core/project.clj index fac1fff7..6c7c6680 100644 --- a/modules/reitit-core/project.clj +++ b/modules/reitit-core/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :dir "../.."} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :java-source-paths ["java-src"] diff --git a/modules/reitit-dev/project.clj b/modules/reitit-dev/project.clj index 3976a744..1add183f 100644 --- a/modules/reitit-dev/project.clj +++ b/modules/reitit-dev/project.clj @@ -5,7 +5,7 @@ :url "http://www.eclipse.org/legal/epl-v10.html"} :scm {:name "git" :url "https://github.com/metosin/reitit"} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-core] diff --git a/modules/reitit-frontend/project.clj b/modules/reitit-frontend/project.clj index 66417e1c..254a2b67 100644 --- a/modules/reitit-frontend/project.clj +++ b/modules/reitit-frontend/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :dir "../.."} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-core]]) diff --git a/modules/reitit-http/project.clj b/modules/reitit-http/project.clj index 607c68e0..78ded043 100644 --- a/modules/reitit-http/project.clj +++ b/modules/reitit-http/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :dir "../.."} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-core] diff --git a/modules/reitit-interceptors/project.clj b/modules/reitit-interceptors/project.clj index e688027d..cc7224e5 100644 --- a/modules/reitit-interceptors/project.clj +++ b/modules/reitit-interceptors/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :dir "../.."} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-ring] diff --git a/modules/reitit-malli/project.clj b/modules/reitit-malli/project.clj index 5b036be5..044d3bba 100644 --- a/modules/reitit-malli/project.clj +++ b/modules/reitit-malli/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :dir "../.."} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-core] diff --git a/modules/reitit-middleware/project.clj b/modules/reitit-middleware/project.clj index e98be6cb..c81b27f8 100644 --- a/modules/reitit-middleware/project.clj +++ b/modules/reitit-middleware/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :scm "../.."} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-ring] diff --git a/modules/reitit-openapi/project.clj b/modules/reitit-openapi/project.clj index 1fa0b322..079f8cac 100644 --- a/modules/reitit-openapi/project.clj +++ b/modules/reitit-openapi/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :dir "../.."} - :plugins [[lein-parent "0.3.8"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-core]]) diff --git a/modules/reitit-pedestal/project.clj b/modules/reitit-pedestal/project.clj index c1677aac..5e7ed9a3 100644 --- a/modules/reitit-pedestal/project.clj +++ b/modules/reitit-pedestal/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :dir "../.."} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[io.pedestal/pedestal.service] diff --git a/modules/reitit-ring/project.clj b/modules/reitit-ring/project.clj index 27386b51..2b6b8053 100644 --- a/modules/reitit-ring/project.clj +++ b/modules/reitit-ring/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :dir "../.."} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-core] diff --git a/modules/reitit-schema/project.clj b/modules/reitit-schema/project.clj index 989dba3c..e5e44be5 100644 --- a/modules/reitit-schema/project.clj +++ b/modules/reitit-schema/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :dir "../.."} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-core] diff --git a/modules/reitit-sieppari/project.clj b/modules/reitit-sieppari/project.clj index c719fdb3..ba69d102 100644 --- a/modules/reitit-sieppari/project.clj +++ b/modules/reitit-sieppari/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :dir "../.."} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-core] diff --git a/modules/reitit-spec/project.clj b/modules/reitit-spec/project.clj index 6f7afc90..b9cf5f84 100644 --- a/modules/reitit-spec/project.clj +++ b/modules/reitit-spec/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :dir "../.."} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-core] diff --git a/modules/reitit-swagger-ui/project.clj b/modules/reitit-swagger-ui/project.clj index e855f9ac..318076a3 100644 --- a/modules/reitit-swagger-ui/project.clj +++ b/modules/reitit-swagger-ui/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :dir "../.."} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-ring] diff --git a/modules/reitit-swagger/project.clj b/modules/reitit-swagger/project.clj index ee2401c3..a52ca20d 100644 --- a/modules/reitit-swagger/project.clj +++ b/modules/reitit-swagger/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :dir "../.."} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-core]]) diff --git a/modules/reitit/project.clj b/modules/reitit/project.clj index 41ec3d9a..14be2f42 100644 --- a/modules/reitit/project.clj +++ b/modules/reitit/project.clj @@ -6,7 +6,7 @@ :scm {:name "git" :url "https://github.com/metosin/reitit" :dir "../.."} - :plugins [[lein-parent "0.3.2"]] + :plugins [[lein-parent "0.3.9"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-core] From 3e786672445c0c83ff86de01a4f2a79c4b857417 Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Thu, 13 Apr 2023 09:37:04 +0300 Subject: [PATCH 11/12] doc: clean up upgraded deps in CHANGELOG.md - don't mention test deps - malli was 0.10.1 in previous release, not 0.10.4 --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5addea5d..8a43f061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,11 +19,8 @@ We use [Break Versioning][breakver]. The version numbers follow a `. Date: Mon, 13 Mar 2023 08:25:03 +0200 Subject: [PATCH 12/12] test: openapi3 + malli + mutually recursive schemas --- test/cljc/reitit/openapi_test.clj | 56 +++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/test/cljc/reitit/openapi_test.clj b/test/cljc/reitit/openapi_test.clj index 563125ed..0788432e 100644 --- a/test/cljc/reitit/openapi_test.clj +++ b/test/cljc/reitit/openapi_test.clj @@ -614,3 +614,59 @@ keys)))) (testing "spec is valid" (is (nil? (validate spec)))))))))) + +(deftest recursive-test + ;; Recursive schemas only properly supported for malli + ;; See https://github.com/metosin/schema-tools/issues/41 + (let [app (ring/ring-handler + (ring/router + [["/parameters" + {:post {:description "parameters" + :coercion malli/coercion + :parameters {:request + {:body + [:schema + {:registry {"friend" [:map + [:age int?] + [:pet [:ref "pet"]]] + "pet" [:map + [:name :string] + [:friends [:vector [:ref "friend"]]]]}} + "friend"]}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :request)})}}] + ["/openapi.json" + {:get {:handler (openapi/create-openapi-handler) + :openapi {:info {:title "" :version "0.0.1"}} + :no-doc true}}]] + {:validate reitit.ring.spec/validate + :data {:middleware [openapi/openapi-feature + rrc/coerce-request-middleware + rrc/coerce-response-middleware]}})) + spec (-> {:request-method :get + :uri "/openapi.json"} + app + :body)] + (is (= {:info {:title "" :version "0.0.1"} + :openapi "3.1.0" + :x-id #{:reitit.openapi/default} + :paths {"/parameters" + {:post + {:description "parameters" + :requestBody + {:content + {"application/json" + {:schema {:$ref "#/definitions/friend" + :definitions {"friend" {:properties {:age {:type "integer"} + :pet {:$ref "#/definitions/pet"}} + :required [:age :pet] + :type "object"} + "pet" {:properties {:friends {:items {:$ref "#/definitions/friend"} + :type "array"} + :name {:type "string"}} + :required [:name :friends] + :type "object"}}}}}}}}}} + spec)) + (testing "spec is valid" + (is (nil? (validate spec))))))