From 215884abe3c9d01072cffb64bf4f206f0a93b63c Mon Sep 17 00:00:00 2001 From: Valtteri Harmainen Date: Sat, 20 Apr 2019 17:26:48 +0300 Subject: [PATCH] Add examples inspired by react-router --- examples/frontend-links/README.md | 13 ++ examples/frontend-links/project.clj | 62 ++++++++ .../resources/public/index.html | 10 ++ .../frontend-links/src/backend/server.clj | 11 ++ .../frontend-links/src/frontend/core.cljs | 143 ++++++++++++++++++ examples/frontend-prompt/README.md | 13 ++ examples/frontend-prompt/project.clj | 62 ++++++++ .../resources/public/index.html | 10 ++ .../frontend-prompt/src/backend/server.clj | 11 ++ .../frontend-prompt/src/frontend/core.cljs | 70 +++++++++ 10 files changed, 405 insertions(+) create mode 100644 examples/frontend-links/README.md create mode 100644 examples/frontend-links/project.clj create mode 100644 examples/frontend-links/resources/public/index.html create mode 100644 examples/frontend-links/src/backend/server.clj create mode 100644 examples/frontend-links/src/frontend/core.cljs create mode 100644 examples/frontend-prompt/README.md create mode 100644 examples/frontend-prompt/project.clj create mode 100644 examples/frontend-prompt/resources/public/index.html create mode 100644 examples/frontend-prompt/src/backend/server.clj create mode 100644 examples/frontend-prompt/src/frontend/core.cljs diff --git a/examples/frontend-links/README.md b/examples/frontend-links/README.md new file mode 100644 index 00000000..72eddd36 --- /dev/null +++ b/examples/frontend-links/README.md @@ -0,0 +1,13 @@ +# reitit-frontend example + +## Usage + +```clj +> lein figwheel +``` + +Go with browser to http://localhost:3449 + +## License + +Copyright © 2018 Metosin Oy diff --git a/examples/frontend-links/project.clj b/examples/frontend-links/project.clj new file mode 100644 index 00000000..189d69fd --- /dev/null +++ b/examples/frontend-links/project.clj @@ -0,0 +1,62 @@ +(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.10.0"] + [ring-server "0.5.0"] + [reagent "0.8.1"] + [ring "1.7.1"] + [hiccup "1.0.5"] + [org.clojure/clojurescript "1.10.520"] + [metosin/reitit "0.3.1"] + [metosin/reitit-spec "0.3.1"] + [metosin/reitit-frontend "0.3.1"] + ;; Just for pretty printting the match + [fipp "0.6.14"]] + + :plugins [[lein-cljsbuild "1.1.7"] + [lein-figwheel "0.5.18"] + [cider/cider-nrepl "0.21.1"]] + + :repl-options {:nrepl-middleware [cider.piggieback/wrap-cljs-repl]} + + :source-paths ["src"] + :resource-paths ["resources" "target/cljsbuild"] + + :profiles + {:dev + {:dependencies + [[binaryage/devtools "0.9.10"] + [cider/piggieback "0.4.0"] + [figwheel-sidecar "0.5.18"]]}} + + :cljsbuild + {:builds + [{:id "app" + :figwheel true + :source-paths ["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] + :aot-cache true}} + {: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 + :aot-cache true}}]} + + :figwheel {:http-server-root "public" + :server-port 3449 + :nrepl-port 7002 + ;; Server index.html for all routes for HTML5 routing + :ring-handler backend.server/handler}) diff --git a/examples/frontend-links/resources/public/index.html b/examples/frontend-links/resources/public/index.html new file mode 100644 index 00000000..ce1dd45b --- /dev/null +++ b/examples/frontend-links/resources/public/index.html @@ -0,0 +1,10 @@ + + + + Reitit frontend example + + +
+ + + diff --git a/examples/frontend-links/src/backend/server.clj b/examples/frontend-links/src/backend/server.clj new file mode 100644 index 00000000..88171cfa --- /dev/null +++ b/examples/frontend-links/src/backend/server.clj @@ -0,0 +1,11 @@ +(ns backend.server + (:require [clojure.java.io :as io] + [ring.util.response :as resp] + [ring.middleware.content-type :as content-type])) + +(def handler + (-> (fn [request] + (or (resp/resource-response (:uri request) {:root "public"}) + (-> (resp/resource-response "index.html" {:root "public"}) + (resp/content-type "text/html")))) + content-type/wrap-content-type)) diff --git a/examples/frontend-links/src/frontend/core.cljs b/examples/frontend-links/src/frontend/core.cljs new file mode 100644 index 00000000..eb4e5861 --- /dev/null +++ b/examples/frontend-links/src/frontend/core.cljs @@ -0,0 +1,143 @@ +(ns frontend.core + (:require [clojure.string :as string] + [fipp.edn :as fedn] + [reagent.core :as r] + [reitit.coercion :as rc] + [reitit.coercion.spec :as rss] + [reitit.frontend :as rf] + [reitit.frontend.easy :as rfe] + [spec-tools.data-spec :as ds])) + +;; Components similar to react-router `Link`, `NavLink` and `Redirect` +;; with Reitit frontend. + +(defn home-page [] + [:div + [:h2 "Welcome to frontend"] + [:p "This is home page"]]) + +(defn about-page [] + [:div + [:h2 "About frontend"] + [:p "This is about page"]]) + +(defn redirect! + "If `push` is truthy, previous page will be left in history." + [{:keys [to path-params query-params push]}] + (if push + (rfe/push-state to path-params query-params) + (rfe/replace-state to path-params query-params))) + +(defn Redirect + "Component that only causes a redirect side-effect." + [props] + (redirect! props) + nil) + +(defn item-page [match] + (let [{:keys [path query]} (:parameters match) + {:keys [id]} path] + (if (< id 1) + [Redirect {:to ::frontpage}] + [:div + [:h2 "Selected item " id] + (when (:foo query) + [:p "Optional foo query param: " (:foo query)])]))) + +(def routes + [["/" + {:name ::frontpage + :view home-page}] + + ["/about" + {:name ::about + :view about-page}] + + ["/item/:id" + {:name ::item + :view item-page + :parameters + {:path {:id int?} + :query {(ds/opt :foo) keyword?}}}]]) + +(def router + (rf/router routes {:data {:coercion rss/coercion}})) + +(defonce current-match (r/atom nil)) + +(defn- resolve-href + [to path-params query-params] + (if (keyword? to) + (rfe/href to path-params query-params) + (let [match (rf/match-by-path router to) + route (-> match :data :name) + params (not-empty (or path-params (:path-params match))) + query (not-empty (or query-params (:query-params match)))] + (if match + (rfe/href route params query) + to)))) + +(defn Link + [{:keys [to path-params query-params active]} & children] + (let [href (resolve-href to path-params query-params)] + (into + [:a {:href href} (when active "> ")] ;; Apply styles or whatever + children))) + +(defn- name-matches? + [name path-params match] + (and (= name (-> match :data :name)) + (= (not-empty path-params) + (-> match :parameters :path not-empty)))) + +(defn- url-matches? + [url match] + (= (-> url (string/split #"\?") first) + (:path match))) + +(defn NavLink + [{:keys [to path-params] :as props} & children] + (let [active (or (name-matches? to path-params @current-match) + (url-matches? to @current-match))] + [Link (assoc props :active active) children])) + +(defn current-page [] + [:div + + [:h4 "Link"] + [:ul + [:li [Link {:to ::frontpage} "Frontpage"]] + [:li [Link {:to "/about"} "About"]] + [:li [Link {:to ::item :path-params {:id 1}} "Item 1"]] + [:li [Link {:to "/item/2?foo=bar"} "Item 2"]] + [:li [Link {:to "/item/-1"} "Item -1 (redirects to frontpage)"]] + [:li [Link {:to "http://www.google.fi"} "Google"]]] + + [:h4 "NavLink"] + [:ul + [:li [NavLink {:to ::frontpage} "Frontpage"]] + [:li [NavLink {:to "/about"} "About"]] + [:li [NavLink {:to ::item :path-params {:id 1}} "Item 1"]] + [:li [NavLink {:to "/item/2?foo=bar"} "Item 2"]] + [:li [NavLink {:to "/item/-1"} "Item -1 (redirects to frontpage)"]] + [:li [NavLink {:to "http://www.google.fi"} "Google"]]] + + (if @current-match + (let [view (:view (:data @current-match))] + [view @current-match])) + + [:pre (with-out-str (fedn/pprint @current-match))]]) + +(defn init! [] + (rfe/start! + router + (fn [m] (reset! current-match m)) + ;; set to false to enable HistoryAPI + {:use-fragment true}) + (r/render [current-page] (.getElementById js/document "app"))) + +(init!) + +(comment + (rf/match-by-path router "/about?kissa=1&koira=true") + (rf/match-by-path router "/item/2?kissa=1&koira=true")) diff --git a/examples/frontend-prompt/README.md b/examples/frontend-prompt/README.md new file mode 100644 index 00000000..72eddd36 --- /dev/null +++ b/examples/frontend-prompt/README.md @@ -0,0 +1,13 @@ +# reitit-frontend example + +## Usage + +```clj +> lein figwheel +``` + +Go with browser to http://localhost:3449 + +## License + +Copyright © 2018 Metosin Oy diff --git a/examples/frontend-prompt/project.clj b/examples/frontend-prompt/project.clj new file mode 100644 index 00000000..189d69fd --- /dev/null +++ b/examples/frontend-prompt/project.clj @@ -0,0 +1,62 @@ +(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.10.0"] + [ring-server "0.5.0"] + [reagent "0.8.1"] + [ring "1.7.1"] + [hiccup "1.0.5"] + [org.clojure/clojurescript "1.10.520"] + [metosin/reitit "0.3.1"] + [metosin/reitit-spec "0.3.1"] + [metosin/reitit-frontend "0.3.1"] + ;; Just for pretty printting the match + [fipp "0.6.14"]] + + :plugins [[lein-cljsbuild "1.1.7"] + [lein-figwheel "0.5.18"] + [cider/cider-nrepl "0.21.1"]] + + :repl-options {:nrepl-middleware [cider.piggieback/wrap-cljs-repl]} + + :source-paths ["src"] + :resource-paths ["resources" "target/cljsbuild"] + + :profiles + {:dev + {:dependencies + [[binaryage/devtools "0.9.10"] + [cider/piggieback "0.4.0"] + [figwheel-sidecar "0.5.18"]]}} + + :cljsbuild + {:builds + [{:id "app" + :figwheel true + :source-paths ["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] + :aot-cache true}} + {: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 + :aot-cache true}}]} + + :figwheel {:http-server-root "public" + :server-port 3449 + :nrepl-port 7002 + ;; Server index.html for all routes for HTML5 routing + :ring-handler backend.server/handler}) diff --git a/examples/frontend-prompt/resources/public/index.html b/examples/frontend-prompt/resources/public/index.html new file mode 100644 index 00000000..ce1dd45b --- /dev/null +++ b/examples/frontend-prompt/resources/public/index.html @@ -0,0 +1,10 @@ + + + + Reitit frontend example + + +
+ + + diff --git a/examples/frontend-prompt/src/backend/server.clj b/examples/frontend-prompt/src/backend/server.clj new file mode 100644 index 00000000..88171cfa --- /dev/null +++ b/examples/frontend-prompt/src/backend/server.clj @@ -0,0 +1,11 @@ +(ns backend.server + (:require [clojure.java.io :as io] + [ring.util.response :as resp] + [ring.middleware.content-type :as content-type])) + +(def handler + (-> (fn [request] + (or (resp/resource-response (:uri request) {:root "public"}) + (-> (resp/resource-response "index.html" {:root "public"}) + (resp/content-type "text/html")))) + content-type/wrap-content-type)) diff --git a/examples/frontend-prompt/src/frontend/core.cljs b/examples/frontend-prompt/src/frontend/core.cljs new file mode 100644 index 00000000..27754c1d --- /dev/null +++ b/examples/frontend-prompt/src/frontend/core.cljs @@ -0,0 +1,70 @@ +(ns frontend.core + (:require [fipp.edn :as fedn] + [reagent.core :as r] + [reitit.coercion :as rc] + [reitit.coercion.spec :as rss] + [reitit.frontend :as rf] + [reitit.frontend.easy :as rfe] + [spec-tools.data-spec :as ds])) + +;; Implementing conditional prompt on navigation with Reitit frontend. + +(defn home-page [] + [:div + [:h2 "Home"] + [:p "You will not be prompted to leave this page"]]) + +(defn prompt-page [] + [:div + [:h2 "Prompt"] + [:p "You will be prompted to leave this page"]]) + +(def routes + [["/" + {:name ::home + :view home-page}] + + ["/prompt" + {:name ::prompt + :view prompt-page + ;; Routes can contain arbitrary keys so we add custom :prompt + ;; key here. See how it's handled in `on-navigate` function. + :prompt "Are you sure you want to leave?" + ;; It would be possible to define a function here that resolves + ;; wheter prompting is needed or not but we'll keep it simple. + }]]) + +(def router + (rf/router routes {:data {:coercion rss/coercion}})) + +(defonce current-match (r/atom nil)) + +(defn current-page [] + [:div + + [:ul + [:li [:a {:href (rfe/href ::home)} "Home"]] + [:li [:a {:href (rfe/href ::prompt)} "Prompt page"]]] + + (if @current-match + (let [view (-> @current-match :data :view)] + [view @current-match])) + [:pre (with-out-str (fedn/pprint @current-match))]]) + +(defn on-navigate [m] + (if-let [prompt (and (not= @current-match m) + (-> @current-match :data :prompt))] + (if (js/window.confirm prompt) ;; Returns true if OK is pressed. + (reset! current-match m) + (.back js/window.history)) ;; Restore browser location + (reset! current-match m))) + +(defn init! [] + (rfe/start! + router + on-navigate + ;; set to false to enable HistoryAPI + {:use-fragment true}) + (r/render [current-page] (.getElementById js/document "app"))) + +(init!)