Replace Closure Html5History

- Create History protocol and two implementations: FragmentHistory and
Html5History
- API follows now Html5 history, i.e. push-state and replace-state
- path-prefix is removed
This commit is contained in:
Juho Teperi 2018-07-24 13:52:09 +03:00
parent d54c05426c
commit 08156f6a6d
4 changed files with 249 additions and 147 deletions

View file

@ -11,10 +11,16 @@
(defn home-page [] (defn home-page []
[:div [:div
[:h2 "Welcome to frontend"] [:h2 "Welcome to frontend"]
[:button [:button
{:type "button" {:type "button"
:on-click #(rfe/set-token ::item {:id 3})} :on-click #(rfe/push-state ::item {:id 3})}
"Item 3"]]) "Item 3"]
[:button
{:type "button"
:on-click #(rfe/replace-state ::item {:id 4})}
"Replace State Item 4"] ])
(defn about-page [] (defn about-page []
[:div [:div

View file

@ -6,7 +6,20 @@
(defonce history (atom nil)) (defonce history (atom nil))
;; Doc-strings from reitit.frontend.history
;; remember to update both!
(defn start! (defn start!
"This registers event listeners on HTML5 history and hashchange events.
When using with development workflow like Figwheel, rememeber to
remove listeners using stop! call before calling start! again.
Parameters:
- router The Reitit routing tree.
- on-navigate Function to be called when route changes. Takes two parameters, ´token´ and ´history´ object.
Options:
- :use-fragment (default true) If true, onhashchange and location hash are used to store the token."
[routes on-navigate opts] [routes on-navigate opts]
(swap! history (fn [old-history] (swap! history (fn [old-history]
(rfh/stop! old-history) (rfh/stop! old-history)
@ -14,26 +27,26 @@
(defn href (defn href
([k] ([k]
(rfh/href @history k)) (rfh/href @history k nil nil))
([k params] ([k params]
(rfh/href @history k params)) (rfh/href @history k params nil))
([k params query] ([k params query]
(rfh/href @history k params query))) (rfh/href @history k params query)))
(defn set-token (defn push-state
"Sets the new route, leaving previous route in history." "Sets the new route, leaving previous route in history."
([k] ([k]
(rfh/set-token @history k)) (rfh/push-state @history k nil nil))
([k params] ([k params]
(rfh/set-token @history k params)) (rfh/push-state @history k params nil))
([k params query] ([k params query]
(rfh/set-token @history k params query))) (rfh/push-state @history k params query)))
(defn replace-token (defn replace-state
"Replaces current route. I.e. current route is not left on history." "Replaces current route. I.e. current route is not left on history."
([k] ([k]
(rfh/replace-token @history k)) (rfh/replace-state @history k nil nil))
([k params] ([k params]
(rfh/replace-token @history k params)) (rfh/replace-state @history k params nil))
([k params query] ([k params query]
(rfh/replace-token @history k params query))) (rfh/replace-state @history k params query)))

View file

@ -6,154 +6,159 @@
[goog.dom :as dom] [goog.dom :as dom]
[reitit.core :as r] [reitit.core :as r]
[reitit.frontend :as rf] [reitit.frontend :as rf]
[reitit.impl :as impl]) [reitit.impl :as impl]
(:import goog.history.Html5History [goog.events :as gevents])
goog.Uri)) (:import goog.Uri))
;; Token is for Closure HtmlHistory (defprotocol History
;; Path is for reitit (-init [this] "Create event listeners")
(-stop [this] "Remove event listeners")
(-on-navigate [this path])
(-get-path [this])
(-href [this path]))
(defn- token->path [history token] ;; This version listens for both pop-state and hash-change for
(if (.-useFragment_ history) ;; compatibility for old browsers not supporting History API.
;; If no fragment at all, default to "/" (defrecord FragmentHistory [on-navigate router listen-key last-fragment]
;; If fragment is present, the token already is prefixed with "/" History
(if (= "" token) (-init [this]
(.getPathPrefix history) ;; Link clicks and e.g. back button trigger both events, if fragment is same as previous ignore second event.
token) ;; For old browsers only the hash-change event is triggered.
(str (.getPathPrefix history) token))) (let [last-fragment (atom nil)
this (assoc this :last-fragment last-fragment)
handler (fn [e]
(let [path (-get-path this)]
(when (or (= goog.events.EventType.POPSTATE (.-type e))
(not= @last-fragment path))
(-on-navigate this path))))]
(-on-navigate this (-get-path this))
(assoc this
:listen-key (gevents/listen js/window
#js [goog.events.EventType.POPSTATE goog.events.EventType.HASHCHANGE]
handler
false))))
(-stop [this]
(gevents/unlistenByKey listen-key))
(-on-navigate [this path]
(reset! last-fragment path)
(on-navigate (rf/match-by-path router path) this))
(-get-path [this]
;; Remove #
;; "" or "#" should be same as "#/"
(let [fragment (subs (.. js/window -location -hash) 1)]
(if (= "" fragment)
"/"
fragment)))
(-href [this path]
(if path
(str "#" path))))
(defn- path->token [history path] (defrecord Html5History [on-navigate router listen-key click-listen-key]
(subs path (if (.-useFragment_ history) History
1 (-init [this]
(count (.getPathPrefix history))))) (let [handler
(fn [e]
(-on-navigate this (-get-path this)))
(defn- token->href [history token] current-domain
(if token (if (exists? js/location)
(str (if (.-useFragment_ history) (.getDomain (.parse Uri js/location)))
(str "#"))
(.getPathPrefix history)
token)))
(def ^:private current-domain (if (exists? js/location) ;; Prevent document load when clicking a elements, if the href points to URL that is part
(.getDomain (.parse Uri js/location)))) ;; of the routing tree."
ignore-anchor-click
(defn ignore-anchor-click (fn ignore-anchor-click
"Ignore click events from a elements, if the href points to URL that is part [e]
of the routing tree." ;; Returns the next matching anchestor of event target
[router history e] (when-let [el (.closest (.-target e) "a")]
;; Returns the next matching anchestor of event target (let [uri (.parse Uri (.-href el))]
(when-let [el (.closest (.-target e) "a")] (when (and (or (and (not (.hasScheme uri)) (not (.hasDomain uri)))
(let [uri (.parse Uri (.-href el))] (= current-domain (.getDomain uri)))
(when (and (or (and (not (.hasScheme uri)) (not (.hasDomain uri))) (not (.-altKey e))
(= current-domain (.getDomain uri))) (not (.-ctrlKey e))
(not (.-altKey e)) (not (.-metaKey e))
(not (.-ctrlKey e)) (not (.-shiftKey e))
(not (.-metaKey e)) (not (contains? #{"_blank" "self"} (.getAttribute el "target")))
(not (.-shiftKey e)) ;; Left button
(not (contains? #{"_blank" "self"} (.getAttribute el "target"))) (= 0 (.-button e))
;; Left button (reitit/match-by-path router (.getPath uri)))
(= 0 (.-button e)) (.preventDefault e)
(reitit/match-by-path router (.getPath uri))) (let [path (str (.getPath uri)
(.preventDefault e) (if (seq (.getQuery uri))
(.setToken history (path->token history (str (.getPath uri) (str "?" (.getQuery uri))))]
(if (seq (.getQuery uri)) (.pushState js/window.history nil "" path)
(str "?" (.getQuery uri)))))))))) (-on-navigate this path))))))]
(-on-navigate this (-get-path this))
(impl/goog-extend (assoc this
^{:jsdoc ["@constructor" :listen-key (gevents/listen js/window goog.events.EventType.POPSTATE handler false)
"@extends {Html5History.TokenTransformer}"]} :click-listen-key (e/listen js/document e/EventType.CLICK ignore-anchor-click))))
TokenTransformer (-on-navigate [this path]
Html5History.TokenTransformer (on-navigate (rf/match-by-path router path) this))
([] (-stop [this]
(this-as this (gevents/unlistenByKey listen-key)
(.call Html5History.TokenTransformer this))) (gevents/unlistenByKey click-listen-key))
(retrieveToken [path-prefix location] (-get-path [this]
(subs (.-pathname location) (count path-prefix))) (.. js/window -location -pathname))
(createUrl [token path-prefix location] (-href [this path]
;; Code in Closure also adds current query params path))
;; from location.
(str path-prefix token)))
(defn start! (defn start!
"This registers event listeners on either haschange or HTML5 history. "This registers event listeners on HTML5 history and hashchange events.
When using with development workflow like Figwheel, rememeber to When using with development workflow like Figwheel, rememeber to
remove listeners using stop! call before calling start! again. remove listeners using stop! call before calling start! again.
Parameters: Parameters:
- router The reitit routing tree. - router The Reitit routing tree.
- on-navigate Function to be called when route changes. - on-navigate Function to be called when route changes. Takes two parameters, ´match´ and ´history´ object.
Options: Options:
- :use-fragment (default true) If true, onhashchange and location hash are used to store the token. - :use-fragment (default true) If true, onhashchange and location hash are used to store current route."
- :path-prefix (default \"/\") If :use-fragment is false, this is prepended to all tokens, and is ([router on-navigate]
removed from start of the token before matching the route." (start! router on-navigate nil))
[router ([router
on-navigate on-navigate
{:keys [path-prefix use-fragment] {:keys [use-fragment]
:or {path-prefix "/" :or {use-fragment true}}]
use-fragment true}}] (let [opts {:router router
(let [history :on-navigate on-navigate}]
(doto (Html5History. nil (TokenTransformer.)) (-init (if use-fragment
(.setEnabled true) (map->FragmentHistory opts)
(.setPathPrefix path-prefix) (map->Html5History opts))))))
(.setUseFragment use-fragment))
event-key (defn stop! [history]
(e/listen history goog.history.EventType.NAVIGATE (if history
(fn [e] (-stop history)))
(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)
(.dispose history))}))
(defn stop! [{:keys [close-fn]}]
(if close-fn
(close-fn)))
(defn- match->token [history match k params query]
(some->> (r/match->path match query)
(path->token history)))
(defn href (defn href
([state k] ([history k]
(href state k nil)) (href history k nil))
([state k params] ([history k params]
(href state k params nil)) (href history k params nil))
([{:keys [router history]} k params query] ([history k params query]
(let [match (rf/match-by-name! router k params) (let [match (rf/match-by-name! (:router history) k params)]
token (match->token history match k params query)] (-href history (r/match->path match query)))))
(token->href history token))))
(defn set-token (defn push-state
"Sets the new route, leaving previous route in history." "Sets the new route, leaving previous route in history."
([state k] ([history k]
(set-token state k nil)) (push-state history k nil nil))
([state k params] ([history k params]
(set-token state k params nil)) (push-state history k params nil))
([{:keys [router history]} k params query] ([history k params query]
(let [match (rf/match-by-name! router k params) (let [match (rf/match-by-name! (:router history) k params)
token (match->token history match k params query)] path (r/match->path match query)]
(.setToken history token)))) ;; pushState and replaceState don't trigger popstate event so call on-navigate manually
(.pushState js/window.history nil "" (-href history path))
(-on-navigate history path))))
(defn replace-token (defn replace-state
"Replaces current route. I.e. current route is not left on history." "Replaces current route. I.e. current route is not left on history."
([state k] ([history k]
(replace-token state k nil)) (replace-state history k nil nil))
([state k params] ([history k params]
(replace-token state k params nil)) (replace-state history k params nil))
([{:keys [router history]} k params query] ([history k params query]
(let [match (rf/match-by-name! router k params) (let [match (rf/match-by-name! (:router history) k params)
token (match->token history match k params query)] path (r/match->path match query)]
(.replaceToken history token)))) (.replaceState js/window.history nil "" (-href history path))
(-on-navigate history path))))

View file

@ -1,9 +1,10 @@
(ns reitit.frontend.history-test (ns reitit.frontend.history-test
(:require [clojure.test :refer [deftest testing is are]] (:require [clojure.test :refer [deftest testing is are async]]
[reitit.core :as r] [reitit.core :as r]
[reitit.frontend :as rf] [reitit.frontend :as rf]
[reitit.frontend.history :as rfh] [reitit.frontend.history :as rfh]
[reitit.frontend.test-utils :refer [capture-console]])) [reitit.frontend.test-utils :refer [capture-console]]
[goog.events :as gevents]))
(def browser (exists? js/window)) (def browser (exists? js/window))
@ -14,6 +15,9 @@
(deftest fragment-history-test (deftest fragment-history-test
(when browser (when browser
(gevents/removeAll js/window goog.events.EventType.POPSTATE)
(gevents/removeAll js/window goog.events.EventType.HASHCHANGE)
(let [history (rfh/start! router (fn [_]) {:use-fragment true})] (let [history (rfh/start! router (fn [_]) {:use-fragment true})]
(testing "creating urls" (testing "creating urls"
@ -29,10 +33,48 @@
(is (= nil value)) (is (= nil value))
(is (= [{:type :warn (is (= [{:type :warn
:message ["missing route" ::asd]}] :message ["missing route" ::asd]}]
messages))))))) messages))))
(rfh/stop! history))))
(deftest fragment-history-routing-test
(when browser
(gevents/removeAll js/window goog.events.EventType.POPSTATE)
(gevents/removeAll js/window goog.events.EventType.HASHCHANGE)
(async done
(let [n (atom 0)
history (rfh/start! router
(fn [match history]
(let [url (rfh/-get-path history)]
(case (swap! n inc)
1 (do (is (= "/" url)
"start at root")
(rfh/push-state history ::foo))
2 (do (is (= "/foo" url)
"push-state")
(.back js/window.history))
3 (do (is (= "/" url)
"go back")
(rfh/push-state history ::bar {:id 1}))
4 (do (is (= "/bar/1" url)
"push-state 2")
(rfh/replace-state history ::bar {:id 2}))
5 (do (is (= "/bar/2" url)
"replace-state")
(.back js/window.history))
6 (do (is (= "/" url)
"go back after replace state")
(rfh/stop! history)
(done))
(do (is false "extra event")))))
{:use-fragment true})]))))
(deftest html5-history-test (deftest html5-history-test
(when browser (when browser
(gevents/removeAll js/window goog.events.EventType.POPSTATE)
(gevents/removeAll js/window goog.events.EventType.HASHCHANGE)
(let [history (rfh/start! router (fn [_]) {:use-fragment false})] (let [history (rfh/start! router (fn [_]) {:use-fragment false})]
(testing "creating urls" (testing "creating urls"
@ -48,4 +90,40 @@
(is (= nil value)) (is (= nil value))
(is (= [{:type :warn (is (= [{:type :warn
:message ["missing route" ::asd]}] :message ["missing route" ::asd]}]
messages))))))) messages))))
(rfh/stop! history))))
(deftest html5-history-routing-test
(when browser
(gevents/removeAll js/window goog.events.EventType.POPSTATE)
(gevents/removeAll js/window goog.events.EventType.HASHCHANGE)
(async done
(let [n (atom 0)
history (rfh/start! router
(fn [match history]
(let [url (rfh/-get-path history)]
(case (swap! n inc)
1 (do (rfh/push-state history ::frontpage))
2 (do (is (= "/" url)
"start at root")
(rfh/push-state history ::foo))
3 (do (is (= "/foo" url)
"push-state")
(.back js/window.history))
4 (do (is (= "/" url)
"go back")
(rfh/push-state history ::bar {:id 1}))
5 (do (is (= "/bar/1" url)
"push-state 2")
(rfh/replace-state history ::bar {:id 2}))
6 (do (is (= "/bar/2" url)
"replace-state")
(.back js/window.history))
7 (do (is (= "/" url)
"go back after replace state")
(rfh/stop! history)
(done))
(do (is false "extra event")))))
{:use-fragment false})]))))