From 417f35a31832c23dce1acf228f61c31fff1fc365 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Fri, 8 Jun 2018 16:00:49 +0300 Subject: [PATCH] Create example --- .gitignore | 1 + examples/frontend/checkouts/reitit-core | 1 + examples/frontend/checkouts/reitit-frontend | 1 + examples/frontend/checkouts/reitit-schema | 1 + examples/frontend/project.clj | 51 ++++++++ examples/frontend/resources/public/index.html | 10 ++ examples/frontend/src/frontend/core.cljs | 50 ++++++++ .../reitit-frontend/src/reitit/frontend.cljs | 120 +++++++----------- .../src/reitit/frontend/history.cljs | 114 +++++++++++++++++ project.clj | 5 +- test/cljs/reitit/doo_runner.cljs | 6 +- test/cljs/reitit/frontend/core_test.cljs | 41 ++++++ 12 files changed, 322 insertions(+), 79 deletions(-) create mode 120000 examples/frontend/checkouts/reitit-core create mode 120000 examples/frontend/checkouts/reitit-frontend create mode 120000 examples/frontend/checkouts/reitit-schema create mode 100644 examples/frontend/project.clj create mode 100644 examples/frontend/resources/public/index.html create mode 100644 examples/frontend/src/frontend/core.cljs create mode 100644 modules/reitit-frontend/src/reitit/frontend/history.cljs create mode 100644 test/cljs/reitit/frontend/core_test.cljs diff --git a/.gitignore b/.gitignore index c65c7d42..7b183f21 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ pom.xml.asc /gh-pages /node_modules /_book +figwheel_server.log diff --git a/examples/frontend/checkouts/reitit-core b/examples/frontend/checkouts/reitit-core new file mode 120000 index 00000000..a59d247e --- /dev/null +++ b/examples/frontend/checkouts/reitit-core @@ -0,0 +1 @@ +../../../modules/reitit-core \ No newline at end of file diff --git a/examples/frontend/checkouts/reitit-frontend b/examples/frontend/checkouts/reitit-frontend new file mode 120000 index 00000000..20cdd448 --- /dev/null +++ b/examples/frontend/checkouts/reitit-frontend @@ -0,0 +1 @@ +../../../modules/reitit-frontend \ No newline at end of file diff --git a/examples/frontend/checkouts/reitit-schema b/examples/frontend/checkouts/reitit-schema new file mode 120000 index 00000000..a68c7f05 --- /dev/null +++ b/examples/frontend/checkouts/reitit-schema @@ -0,0 +1 @@ +../../../modules/reitit-schema \ No newline at end of file diff --git a/examples/frontend/project.clj b/examples/frontend/project.clj new file mode 100644 index 00000000..821e45b9 --- /dev/null +++ b/examples/frontend/project.clj @@ -0,0 +1,51 @@ +(defproject frontend "0.1.0-SNAPSHOT" + :description "FIXME: write description" + :url "http://example.com/FIXME" + :license {:name "Eclipse Public License" + :url "http://www.eclipse.org/legal/epl-v10.html"} + + :dependencies [[org.clojure/clojure "1.9.0"] + [ring-server "0.5.0"] + [reagent "0.8.1"] + [ring "1.6.3"] + [compojure "1.6.1"] + [hiccup "1.0.5"] + [org.clojure/clojurescript "1.10.238" :scope "provided"] + [metosin/reitit "0.1.3"] + [metosin/reitit-schema "0.1.3"] + [metosin/reitit-frontend "0.1.3"]] + + :plugins [[lein-cljsbuild "1.1.7"]] + + :source-paths [] + :resource-paths ["resources" "target/cljsbuild"] + + :cljsbuild + {:builds + [{:id "app" + :figwheel true + :source-paths ["src"] + :watch-paths ["src" "checkouts/reitit-frontend/src"] + :compiler {:main "frontend.core" + :asset-path "/js/out" + :output-to "target/cljsbuild/public/js/app.js" + :output-dir "target/cljsbuild/public/js/out" + :source-map true + :optimizations :none + :pretty-print true + :preloads [devtools.preload]}} + {:id "min" + :source-paths ["src"] + :compiler {:output-to "target/cljsbuild/public/js/app.js" + :output-dir "target/cljsbuild/public/js" + :source-map "target/cljsbuild/public/js/app.js.map" + :optimizations :advanced + :pretty-print false}}]} + + :figwheel + {:http-server-root "public" + :server-port 3449 + :nrepl-port 7002} + + :profiles {:dev {:dependencies [[binaryage/devtools "0.9.10"]] + :plugins [[lein-figwheel "0.5.16"]]}}) diff --git a/examples/frontend/resources/public/index.html b/examples/frontend/resources/public/index.html new file mode 100644 index 00000000..bbad514c --- /dev/null +++ b/examples/frontend/resources/public/index.html @@ -0,0 +1,10 @@ + + + + Reitit frontend example + + +
+ + + diff --git a/examples/frontend/src/frontend/core.cljs b/examples/frontend/src/frontend/core.cljs new file mode 100644 index 00000000..4f4d9276 --- /dev/null +++ b/examples/frontend/src/frontend/core.cljs @@ -0,0 +1,50 @@ +(ns frontend.core + (:require [reagent.core :as r] + [reitit.core :as rc] + [reitit.frontend :as rf] + [reitit.frontend.history :as rfh] + [reitit.coercion.schema :as rs])) + +(def router (atom nil)) + +(defn home-page [] + [:div + [:h2 "Welcome to frontend"] + [:div [:a {:href (rfh/href @router ::about)} "go to about page"]]]) + +(defn about-page [] + [:div + [:h2 "About frontend"] + [:a {:href "http://google.com"} "external link"] + [:div [:a {:href (rfh/href @router ::frontpage)} "go to the home page"]]]) + +(defonce match (r/atom nil)) + +(defn current-page [] + [:div + (if @match + [:div [(:view (:data @match))]]) + (pr-str @match)]) + +(def routes + (rc/router + ["" + ["" + {:name ::frontpage-root + :view home-page}] + ["/" + ["" + {:name ::frontpage + :view home-page}] + ["about" + {:name ::about + :view about-page}]]])) + +(defn init! [] + (reset! router (rfh/start! routes + (fn [m] (reset! match m)) + {:use-fragment true + :path-prefix "/"})) + (r/render [current-page] (.getElementById js/document "app"))) + +(init!) diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs index a50c4e5f..ae2e5816 100644 --- a/modules/reitit-frontend/src/reitit/frontend.cljs +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -1,19 +1,14 @@ (ns reitit.frontend - "Utilities to implement frontend routing using Reitit. - - Controller is way to declare as data the side-effects and optionally - other data related to the route." + "" (:require [reitit.core :as reitit] [clojure.string :as str] - goog.Uri - [reitit.coercion :as coercion])) - -;; -;; Utilities -;; + [reitit.coercion :as coercion] + [goog.events :as e] + [goog.dom :as dom]) + (:import goog.Uri)) (defn query-params - "Parse query-params from URL into a map." + "Given goog.Uri, read query parameters into Clojure map." [^goog.Uri uri] (let [q (.getQueryData uri)] (->> q @@ -21,70 +16,43 @@ (map (juxt keyword #(.get q %))) (into {})))) -(defn get-hash - "Given browser hash starting with #, remove the # and - end slashes." - [] - (-> js/location.hash - (subs 1) - (str/replace #"/$" ""))) +(defn query-string + "Given map, creates " + [m] + (str/join "&" (map (fn [[k v]] + (str (js/encodeURIComponent (name k)) + "=" + ;; FIXME: create protocol to handle how types are converted to string + ;; FIXME: array to multiple params + (if (coll? v) + (str/join "," (map #(js/encodeURIComponent %) v)) + (js/encodeURIComponent v)))) + m))) -;; -;; Controller implementation -;; +(defn match-by-path + "Given routing tree and current path, return match with possibly + coerced parameters. Return nil if no match found." + [router path] + (let [uri (.parse Uri path)] + (if-let [match (reitit/match-by-path router (.getPath uri))] + (let [q (query-params uri) + ;; Return uncoerced values if coercion is not enabled - so + ;; that tha parameters are always accessible from same property. + ;; FIXME: coerce! can't be used as it doesn't take query-params + parameters (if (:result match) + (coercion/coerce-request (:result match) {:query-params q + :path-params (:path-params match)}) + {:query q + :path (:param match)})] + (assoc match :parameters parameters))))) -(defn get-params - "Get controller parameters given match. If controller provides :params - function that will be called with the match. Default is nil." - [controller match] - (if-let [f (:params controller)] - (f match))) - -(defn apply-controller - "Run side-effects (:start or :stop) for controller. - The side-effect function is called with controller params." - [controller method] - (when-let [f (get controller method)] - (f (::params controller)))) - -(defn- pad-same-length [a b] - (concat a (take (- (count b) (count a)) (repeat nil)))) - -(defn apply-controllers - "Applies changes between current controllers and - those previously enabled. Resets controllers whose - parameters have changed." - [old-controllers new-match] - (let [new-controllers (map (fn [controller] - (assoc controller ::params (get-params controller new-match))) - (:controllers (:data new-match))) - changed-controllers (->> (map (fn [old new] - ;; different controllers, or params changed - (if (not= old new) - {:old old, :new new})) - (pad-same-length old-controllers new-controllers) - (pad-same-length new-controllers old-controllers)) - (keep identity) - vec)] - (doseq [controller (map :old changed-controllers)] - (apply-controller controller :stop)) - (doseq [controller (map :new changed-controllers)] - (apply-controller controller :start)) - new-controllers)) - -(defn hash-change [router hash] - (let [uri (goog.Uri/parse hash) - match (or (reitit/match-by-path router (.getPath uri)) - {:data {:name :not-found}}) - q (query-params uri) - ;; Coerce if coercion enabled - c (if (:result match) - (coercion/coerce-request (:result match) {:query-params q - :path-params (:params match)}) - {:query q - :path (:param match)}) - ;; Replace original params with coerced params - match (-> match - (assoc :query (:query c)) - (assoc :params (:path c)))] - match)) +(defn match-by-name + [router name params] + ;; FIXME: move router not initialized to re-frame integration? + (if router + (or (reitit/match-by-name router name params) + ;; FIXME: return nil? + (do + (js/console.error "Can't create URL for route " (pr-str name) (pr-str params)) + nil)) + ::not-initialized)) diff --git a/modules/reitit-frontend/src/reitit/frontend/history.cljs b/modules/reitit-frontend/src/reitit/frontend/history.cljs new file mode 100644 index 00000000..964a665a --- /dev/null +++ b/modules/reitit-frontend/src/reitit/frontend/history.cljs @@ -0,0 +1,114 @@ +(ns reitit.frontend.history + "" + (:require [reitit.core :as reitit] + [clojure.string :as string] + [goog.events :as e] + [goog.dom :as dom] + [reitit.frontend :as rf]) + (:import goog.history.Html5History + goog.Uri)) + +;; Token is for Closure HtmlHistory +;; Path is for reitit + +(defn- token->path [history token] + (if (.-useFragment_ history) + token + (str (.getPathPrefix history) token))) + +(defn- path->token [history path] + (subs path (if (.-useFragment_ history) + 1 + (count (.getPathPrefix history))))) + +(defn- token->href [history token] + (str (if (.-useFragment_ history) + (str "#")) + (.getPathPrefix history) + token)) + +(def ^:private current-domain (.getDomain (.parse Uri js/location))) + +(defn ignore-anchor-click + "Ignore click events from a elements, if the href points to URL that is part + of the routing tree." + [router history e] + ;; Returns the next matching anchestor of event target + (when-let [el (.closest (.-target e) "a")] + (let [uri (.parse Uri (.-href el))] + (when (and (or (and (not (.hasScheme uri)) (not (.hasDomain uri))) + (= current-domain (.getDomain uri))) + (not (.-altKey e)) + (not (.-ctrlKey e)) + (not (.-metaKey e)) + (not (.-shiftKey e)) + (not (contains? #{"_blank" "self"} (.getAttribute el "target"))) + ;; Left button + (= 0 (.-button e)) + (reitit/match-by-path router (.getPath uri))) + (.preventDefault e) + (.replaceToken history (path->token history (.getPath uri))))))) + +(defn start! + "Parameters: + - router The reitit routing tree. + - on-navigate Function to be called when route changes. + + Options: + - :use-fragment (default true) If true, onhashchange and location hash are used to store the token. + - :path-prefix (default \"/\") If :use-fragment is false, this is prepended to all tokens, and is + removed from start of the token before matching the route." + [router + on-navigate + {:keys [path-prefix use-fragment] + :or {path-prefix "/" + use-fragment true}}] + (let [history + (doto (Html5History.) + (.setEnabled true) + (.setPathPrefix path-prefix) + (.setUseFragment use-fragment)) + + event-key + (e/listen history goog.history.EventType.NAVIGATE + (fn [e] + (on-navigate (rf/match-by-path router (token->path history (.getToken history)))))) + + click-listen-key + (if-not use-fragment + (e/listen js/document e/EventType.CLICK + (partial ignore-anchor-click router history)))] + + ;; Trigger navigate event for current route + (on-navigate (rf/match-by-path router (token->path history (.getToken history)))) + + {:router router + :history history + :close-fn (fn [] + (e/unlistenByKey event-key) + (e/unlistenByKey click-listen-key) + (.setEnabled history false))})) + +(defn stop! [{:keys [close-fn]}] + (if close-fn + (close-fn))) + +(defn- match->token [history match k params] + ;; FIXME: query string + (if-let [path (:path match)] + (path->token history path))) + +(defn href + ([state k] + (href state k nil)) + ([state k params] + (href state k params nil)) + ([{:keys [router history]} k params query] + (let [match (rf/match-by-name router k params) + token (match->token history match k params)] + (token->href history token)))) + +(defn replace-token [{:keys [router history]} k params] + (let [match (rf/match-by-name router k params) + token (match->token history match k params)] + (.replaceToken history token))) diff --git a/project.clj b/project.clj index ab2f6e5a..c7172db6 100644 --- a/project.clj +++ b/project.clj @@ -62,7 +62,10 @@ [criterium "0.4.4"] [org.clojure/test.check "0.9.0"] [org.clojure/tools.namespace "0.2.11"] - [com.gfredericks/test.chuck "0.2.9"]]} + [com.gfredericks/test.chuck "0.2.9"] + + ;; https://github.com/bensu/doo/issues/180 + [fipp "0.6.12"]]} :perf {:jvm-opts ^:replace ["-server" "-Xmx4096m" "-Dclojure.compiler.direct-linking=true"] diff --git a/test/cljs/reitit/doo_runner.cljs b/test/cljs/reitit/doo_runner.cljs index 57b7dc2b..25f522c6 100644 --- a/test/cljs/reitit/doo_runner.cljs +++ b/test/cljs/reitit/doo_runner.cljs @@ -5,7 +5,8 @@ reitit.impl-test reitit.middleware-test reitit.ring-test - #_reitit.spec-test)) + #_reitit.spec-test + reitit.frontend.core-test)) (enable-console-print!) @@ -14,4 +15,5 @@ 'reitit.impl-test 'reitit.middleware-test 'reitit.ring-test - #_'reitit.spec-test) + #_'reitit.spec-test + 'reitit.frontend.core-test) diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs new file mode 100644 index 00000000..2d522a4b --- /dev/null +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -0,0 +1,41 @@ +(ns reitit.frontend.core-test + (:require [clojure.test :refer [deftest testing is are]] + [reitit.core :as r] + [reitit.frontend :as rf] + [reitit.coercion :as coercion] + [schema.core :as s] + [reitit.coercion.schema :as schema])) + +(deftest match-by-path-test + (testing "simple" + (let [router (r/router ["/" + ["" ::frontpage] + ["foo" ::foo] + ["bar" ::bar]])] + (is (= {:template "/" + :data {:name ::frontpage} + :path "/"} + (rf/match-by-path router "/"))) + (is (= {:template "/foo" + :data {:name ::foo} + :path "/foo"} + (rf/match-by-path router "/foo"))))) + + (testing "schema coercion" + (let [router (r/router ["/" + [":id" + {:name ::foo + :parameters {:path {:id s/Int} + :query {(s/optional-key :mode) s/Keyword}}}]])] + #_#_ + (is (= {:template "/5" + :data {:name ::foo} + :path "/5" + :parameters {:path {:id 5}}} + (rf/match-by-path router "/5"))) + (is (= {:template "/5?mode=foo" + :data {:name ::foo} + :path "/5?mode=foo" + :parameters {:path {:id 5} + :query {:mode :foo}}} + (rf/match-by-path router "/5?mode=foo"))))))