Create example

This commit is contained in:
Juho Teperi 2018-06-08 16:00:49 +03:00
parent 468a0947d2
commit 417f35a318
12 changed files with 322 additions and 79 deletions

1
.gitignore vendored
View file

@ -11,3 +11,4 @@ pom.xml.asc
/gh-pages
/node_modules
/_book
figwheel_server.log

View file

@ -0,0 +1 @@
../../../modules/reitit-core

View file

@ -0,0 +1 @@
../../../modules/reitit-frontend

View file

@ -0,0 +1 @@
../../../modules/reitit-schema

View file

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

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

View file

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

View file

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

View file

@ -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"]

View file

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

View file

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