Merge pull request #260 from vharmain/frontend-examples

Frontend examples
This commit is contained in:
Tommi Reiman 2019-04-29 10:26:14 +03:00 committed by GitHub
commit 59d68d65ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 676 additions and 1 deletions

View file

@ -0,0 +1,13 @@
# reitit-frontend example
## Usage
```clj
> lein figwheel
```
Go with browser to http://localhost:3449
## License
Copyright © 2018 Metosin Oy

View file

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

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>Reitit frontend example</title>
</head>
<body>
<div id="app"></div>
<script src="/js/app.js"></script>
</body>
</html>

View file

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

View file

@ -0,0 +1,147 @@
(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]
(r/create-class
{:component-did-mount (fn [this] (redirect! (r/props this)))
:component-did-update (fn [this [_ prev-props]]
(if (not= (r/props this) prev-props)
(redirect! (r/props this))))
:render (fn [this] 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 (or path-params (:path-params match))
query (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"))

View file

@ -0,0 +1,13 @@
# reitit-frontend example
## Usage
```clj
> lein figwheel
```
Go with browser to http://localhost:3449
## License
Copyright © 2018 Metosin Oy

View file

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

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>Reitit frontend example</title>
</head>
<body>
<div id="app"></div>
<script src="/js/app.js"></script>
</body>
</html>

View file

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

View file

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

View file

@ -0,0 +1,26 @@
# frontend-re-frame
A [re-frame](https://github.com/Day8/re-frame) application designed to ... well, that part is up to you.
## Development Mode
### Run application:
```
lein clean
lein figwheel dev
```
Figwheel will automatically push cljs changes to the browser.
Wait a bit, then browse to [http://localhost:3449](http://localhost:3449).
## Production Build
To compile clojurescript to javascript:
```
lein clean
lein cljsbuild once min
```

View file

@ -0,0 +1,56 @@
(defproject frontend-re-frame "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.10.0"]
[org.clojure/clojurescript "1.10.520"]
[metosin/reitit "0.3.1"]
[reagent "0.8.1"]
[re-frame "0.10.6"]]
: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]}
:min-lein-version "2.5.3"
:source-paths ["src/clj" "src/cljs"]
:clean-targets ^{:protect false} ["resources/public/js/compiled" "target"]
:figwheel
{:css-dirs ["resources/public/css"]
:server-port 3449
:nrepl-port 7002
:ring-handler backend.server/handler}
:profiles
{:dev
{:dependencies
[[binaryage/devtools "0.9.10"]
[cider/piggieback "0.4.0"]
[figwheel-sidecar "0.5.18"]]
:plugins [[lein-figwheel "0.5.18"]]}
:prod {}}
:cljsbuild
{:builds
[{:id "dev"
:source-paths ["src/cljs"]
:figwheel {:on-jsload "frontend-re-frame.core/mount-root"}
:compiler {:main frontend-re-frame.core
:output-to "resources/public/js/compiled/app.js"
:output-dir "resources/public/js/compiled/out"
:asset-path "js/compiled/out"
:source-map-timestamp true
:preloads [devtools.preload]
:external-config {:devtools/config {:features-to-install :all}}
}}
{:id "min"
:source-paths ["src/cljs"]
:compiler {:main frontend-re-frame.core
:output-to "resources/public/js/compiled/app.js"
:optimizations :advanced
:closure-defines {goog.DEBUG false}
:pretty-print false}}
]}
)

View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset='utf-8'>
</head>
<body>
<div id="app"></div>
<script src="js/compiled/app.js"></script>
<script>frontend_re_frame.core.init();</script>
</body>
</html>

View file

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

View file

@ -0,0 +1 @@
(ns frontend-re-frame.core)

View file

@ -0,0 +1,155 @@
(ns frontend-re-frame.core
(:require
[re-frame.core :as re-frame]
[reagent.core :as reagent]
[reitit.core :as r]
[reitit.coercion :as rc]
[reitit.coercion.spec :as rss]
[reitit.frontend :as rf]
[reitit.frontend.controllers :as rfc]
[reitit.frontend.easy :as rfe]))
;;; Events ;;;
(re-frame/reg-event-db
::initialize-db
(fn [_ _]
{:current-route nil}))
(re-frame/reg-event-fx
::navigate
(fn [db [_ route]]
;; See `navigate` effect in routes.cljs
{::navigate! route}))
(re-frame/reg-event-db
::navigated
(fn [db [_ new-match]]
(let [old-match (:current-route db)
controllers (rfc/apply-controllers (:controllers old-match) new-match)]
(assoc db :current-route (assoc new-match :controllers controllers)))))
;;; Subscriptions ;;;
(re-frame/reg-sub
::current-route
(fn [db]
(:current-route db)))
;;; Views ;;;
(defn home-page []
[:div
[:h1 "This is home page"]
[:button
;; Dispatch navigate event that triggers a (side)effect.
{:on-click #(re-frame/dispatch [::navigate ::sub-page2])}
"Go to sub-page 2"]])
(defn sub-page1 []
[:div
[:h1 "This is sub-page 1"]])
(defn sub-page2 []
[:div
[:h1 "This is sub-page 2"]])
;;; Effects ;;;
;; Triggering navigation from events.
(re-frame/reg-fx
::navigate!
(fn [k params query]
(rfe/push-state k params query)))
;;; Routes ;;;
(defn href
"Return relative url for given route. Url can be used in HTML links."
([k]
(href k nil nil))
([k params]
(href k params nil))
([k params query]
(rfe/href k params query)))
(def routes
["/"
[""
{:name ::home
:view home-page
:link-text "Home"
:controllers
[{;; Do whatever initialization needed for home page
;; I.e (re-frame/dispatch [::events/load-something-with-ajax])
:start (fn [& params](js/console.log "Entering home page"))
;; Teardown can be done here.
:stop (fn [& params] (js/console.log "Leaving home page"))}]}]
["sub-page1"
{:name ::sub-page1
:view sub-page1
:link-text "Sub page 1"
:controllers
[{:start (fn [& params] (js/console.log "Entering sub-page 1"))
:stop (fn [& params] (js/console.log "Leaving sub-page 1"))}]}]
["sub-page2"
{:name ::sub-page2
:view sub-page2
:link-text "Sub-page 2"
:controllers
[{:start (fn [& params] (js/console.log "Entering sub-page 2"))
:stop (fn [& params] (js/console.log "Leaving sub-page 2"))}]}]])
(defn on-navigate [new-match]
(when new-match
(re-frame/dispatch [::navigated new-match])))
(def router
(rf/router
routes
{:data {:coercion rss/coercion}}))
(defn init-routes! []
(js/console.log "initializing routes")
(rfe/start!
router
on-navigate
{:use-fragment true}))
(defn nav [{:keys [router current-route]}]
[:ul
(for [route-name (r/route-names router)
:let [route (r/match-by-name router route-name)
text (-> route :data :link-text)]]
[:li {:key route-name}
(when (= route-name (-> current-route :data :name))
"> ")
;; Create a normal links that user can click
[:a {:href (href route-name)} text]])])
(defn router-component [{:keys [router]}]
(let [current-route @(re-frame/subscribe [::current-route])]
[:div
[nav {:router router :current-route current-route}]
(when current-route
[(-> current-route :data :view)])]))
;;; Setup ;;;
(def debug? ^boolean goog.DEBUG)
(defn dev-setup []
(when debug?
(enable-console-print!)
(println "dev mode")))
(defn mount-root []
(re-frame/clear-subscription-cache!)
(init-routes!) ;; Reset routes on figwheel reload
(reagent/render [router-component {:router router}]
(.getElementById js/document "app")))
(defn ^:export init []
(re-frame/dispatch-sync [::initialize-db])
(dev-setup)
(mount-root))

View file

@ -67,7 +67,7 @@
([match]
(match->path match nil))
([match query-params]
(some-> match :path (cond-> query-params (str "?" (impl/query-string query-params))))))
(some-> match :path (cond-> (seq query-params) (str "?" (impl/query-string query-params))))))
;;
;; Different routers

View file

@ -354,6 +354,10 @@
(-> router
(r/match-by-name! ::route {:a "olipa", :b "kerran"})
(r/match->path))))
(is (= "/olipa/kerran"
(-> router
(r/match-by-name! ::route {:a "olipa", :b "kerran"})
(r/match->path {}))))
(is (= "/olipa/kerran?iso=p%C3%B6ril%C3%A4inen"
(-> router
(r/match-by-name! ::route {:a "olipa", :b "kerran"})